@elitedcs/ghl-mcp 3.72.3 → 3.73.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 +39 -0
- package/dist/assessment.html +789 -0
- package/dist/index.js +1544 -481
- package/guide/guide.html +1 -1
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -2354,6 +2354,7 @@ var init_product_manifest = __esm({
|
|
|
2354
2354
|
"use strict";
|
|
2355
2355
|
COMMAND_OS_ONLY_TOOLS = /* @__PURE__ */ new Set([]);
|
|
2356
2356
|
COMMAND_OS_ONLY_MODULES = /* @__PURE__ */ new Set([
|
|
2357
|
+
"assessment",
|
|
2357
2358
|
"agency-profile",
|
|
2358
2359
|
"client-engagements",
|
|
2359
2360
|
"audit-report",
|
|
@@ -2407,6 +2408,36 @@ var init_product_manifest = __esm({
|
|
|
2407
2408
|
does: "Turns a read-through of a PROSPECT\u2019s website into the assessment document you put in front of them: what contradicts itself, what a customer cannot find, each one carrying the quote and the page it came from, ranked. It never invents money, and it returns what the crawl refused so you can see what was thrown out rather than wonder what was missed.",
|
|
2408
2409
|
why: "This is the paid deliverable, and it is about a business whose GoHighLevel account you do not have \u2014 there is nothing in the $97 product that reads a stranger\u2019s website or knows the rule that your own price floor must never price their findings.",
|
|
2409
2410
|
layer: "Layer 4 \u2014 Delivery"
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
name: "save_assessment",
|
|
2414
|
+
does: "Saves an assessment into your own GoHighLevel: the prospect as a contact, the whole answer sheet as a note you can read in plain English, and the deal as an opportunity on your Assessments pipeline. If the record is too long for one note it is split and reassembled on load, never truncated.",
|
|
2415
|
+
why: "It is your business's filing cabinet, not ours \u2014 we hold nothing. That is also what lets a remote employee run the assessment on Tuesday and the owner read it on Wednesday, using the GoHighLevel logins and permissions they already have.",
|
|
2416
|
+
layer: "Layer 4 \u2014 Delivery"
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
name: "load_assessment",
|
|
2420
|
+
does: "Loads a saved assessment back off a contact so somebody else can pick it up, returning the most recent COMPLETE answer sheet. If a note was edited or a piece is missing it returns nothing at all rather than a partial record.",
|
|
2421
|
+
why: "A half-loaded assessment overwriting a real one is the worst thing this could do to an agency, so refusing is the feature. Nothing in the $97 product stores an interview or knows what a complete one looks like.",
|
|
2422
|
+
layer: "Layer 4 \u2014 Delivery"
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
name: "list_assessments",
|
|
2426
|
+
does: "Lists the assessments on your Assessments pipeline: who each is for, what stage it is at, and what the deal is worth.",
|
|
2427
|
+
why: "This is the agency owner's board and the list a remote employee picks their next job from. It reads across your own prospects, which is the agency's business rather than any one client's account.",
|
|
2428
|
+
layer: "Layer 4 \u2014 Delivery"
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
name: "read_assessment_transcript",
|
|
2432
|
+
does: "Turns a recording of the meeting into assessment answers. Every answer has to carry the owner's own words, and it checks those words actually appear in the transcript \u2014 anything citing a sentence that is not there is thrown out. Anything worked out rather than heard, like two or three calls a week becoming eleven a month, is held back for you to confirm.",
|
|
2433
|
+
why: "It is the guard between a recording and a document you hand a prospect. Nothing in the $97 product reads a conversation, and nothing in it has a reason to refuse a number that sounds right.",
|
|
2434
|
+
layer: "Layer 4 \u2014 Delivery"
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
name: "write_assessment_report",
|
|
2438
|
+
does: "Writes the finished assessment as a complete page on your own machine, in your branding, ready to hand to the prospect. It loads nothing from anywhere, so it opens on any host in any browser; it is marked not-to-be-indexed and given a random filename, because it carries another business's revenue and customer numbers.",
|
|
2439
|
+
why: "We never publish it. It is your prospect's data on your letterhead, so where it goes is yours to decide \u2014 and nothing in the $97 product produces a client-facing document at all.",
|
|
2440
|
+
layer: "Layer 4 \u2014 Delivery"
|
|
2410
2441
|
}
|
|
2411
2442
|
];
|
|
2412
2443
|
CATALOGUED_TOOLS = new Set(COMMAND_OS_CATALOGUE.map((t) => t.name));
|
|
@@ -4814,7 +4845,7 @@ var init_products = __esm({
|
|
|
4814
4845
|
});
|
|
4815
4846
|
|
|
4816
4847
|
// src/tools/invoices.ts
|
|
4817
|
-
function buildCreateInvoiceBody(args, resolvedLocationId, contact, businessName,
|
|
4848
|
+
function buildCreateInvoiceBody(args, resolvedLocationId, contact, businessName, today2) {
|
|
4818
4849
|
const currency = args.currency ?? args.items[0]?.currency ?? "USD";
|
|
4819
4850
|
return {
|
|
4820
4851
|
altId: resolvedLocationId,
|
|
@@ -4840,8 +4871,8 @@ function buildCreateInvoiceBody(args, resolvedLocationId, contact, businessName,
|
|
|
4840
4871
|
email: contact.email ?? "",
|
|
4841
4872
|
phoneNo: contact.phone ?? ""
|
|
4842
4873
|
},
|
|
4843
|
-
issueDate: args.issueDate ??
|
|
4844
|
-
dueDate: args.dueDate ?? args.issueDate ??
|
|
4874
|
+
issueDate: args.issueDate ?? today2,
|
|
4875
|
+
dueDate: args.dueDate ?? args.issueDate ?? today2,
|
|
4845
4876
|
// Real, payable invoice unless the caller explicitly asks for a test one.
|
|
4846
4877
|
liveMode: args.liveMode ?? true
|
|
4847
4878
|
};
|
|
@@ -4944,13 +4975,13 @@ function registerInvoiceTools(server2, client) {
|
|
|
4944
4975
|
businessName = l.name ?? "";
|
|
4945
4976
|
} catch {
|
|
4946
4977
|
}
|
|
4947
|
-
const
|
|
4978
|
+
const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4948
4979
|
const body = buildCreateInvoiceBody(
|
|
4949
4980
|
{ name, contactId, items, currency, discount, termsNotes, title: title2, issueDate, dueDate, liveMode },
|
|
4950
4981
|
resolvedLocationId,
|
|
4951
4982
|
contact,
|
|
4952
4983
|
businessName,
|
|
4953
|
-
|
|
4984
|
+
today2
|
|
4954
4985
|
);
|
|
4955
4986
|
return client.post("/invoices/", { body });
|
|
4956
4987
|
}
|
|
@@ -5159,12 +5190,12 @@ function buildCreateUserBody(args, opts = {}) {
|
|
|
5159
5190
|
function generateTemporaryPassword() {
|
|
5160
5191
|
const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
5161
5192
|
const lower = "abcdefghijkmnopqrstuvwxyz";
|
|
5162
|
-
const
|
|
5193
|
+
const digits2 = "23456789";
|
|
5163
5194
|
const symbols = "!@#$%&*?";
|
|
5164
|
-
const all = upper + lower +
|
|
5195
|
+
const all = upper + lower + digits2 + symbols;
|
|
5165
5196
|
const bytes = (0, import_node_crypto.randomBytes)(24);
|
|
5166
5197
|
const pick = (set, b) => set[b % set.length];
|
|
5167
|
-
const chars = [pick(upper, bytes[0]), pick(lower, bytes[1]), pick(
|
|
5198
|
+
const chars = [pick(upper, bytes[0]), pick(lower, bytes[1]), pick(digits2, bytes[2]), pick(symbols, bytes[3])];
|
|
5168
5199
|
for (let i = 4; i < 20; i++) chars.push(pick(all, bytes[i]));
|
|
5169
5200
|
for (let i = chars.length - 1; i > 0; i--) {
|
|
5170
5201
|
const j = bytes[(i + 20) % bytes.length] % (i + 1);
|
|
@@ -8419,14 +8450,14 @@ function composeOptinPage(params) {
|
|
|
8419
8450
|
});
|
|
8420
8451
|
const valueCards = buildValueCards({ cards: params.content.cards });
|
|
8421
8452
|
const faq = buildFaq({ title: params.content.faqTitle, items: params.content.faqs, imageUrl: params.images?.faqImageUrl });
|
|
8422
|
-
const
|
|
8453
|
+
const footer2 = buildFooter({
|
|
8423
8454
|
businessLegalName: params.business.legalName,
|
|
8424
8455
|
mailingAddress: params.business.mailingAddress,
|
|
8425
8456
|
privacyUrl: params.business.privacyUrl,
|
|
8426
8457
|
termsUrl: params.business.termsUrl,
|
|
8427
8458
|
logoUrl: params.images?.logoUrl
|
|
8428
8459
|
});
|
|
8429
|
-
const ordered = [topbar, hero, valueCards, formSection, faq,
|
|
8460
|
+
const ordered = [topbar, hero, valueCards, formSection, faq, footer2];
|
|
8430
8461
|
const base = baseEnvelopeParts();
|
|
8431
8462
|
let envelope = {
|
|
8432
8463
|
// sequence must reflect THIS page's order, not the donors' source pages
|
|
@@ -11689,8 +11720,8 @@ function encodeOwnership(owner, note) {
|
|
|
11689
11720
|
return note && note.trim() ? `${owner} | ${note.trim()}` : owner;
|
|
11690
11721
|
}
|
|
11691
11722
|
function decodeOwnership(value) {
|
|
11692
|
-
const [
|
|
11693
|
-
const owner =
|
|
11723
|
+
const [head2, ...rest] = value.split("|");
|
|
11724
|
+
const owner = head2.trim();
|
|
11694
11725
|
const note = rest.join("|").trim();
|
|
11695
11726
|
return { owner: isOwnerKind(owner) ? owner : null, note: note || void 0 };
|
|
11696
11727
|
}
|
|
@@ -15387,6 +15418,1031 @@ var init_agency_profile2 = __esm({
|
|
|
15387
15418
|
}
|
|
15388
15419
|
});
|
|
15389
15420
|
|
|
15421
|
+
// src/command-os/assessment/store.ts
|
|
15422
|
+
function money(n) {
|
|
15423
|
+
return "$" + Math.round(n).toLocaleString("en-US");
|
|
15424
|
+
}
|
|
15425
|
+
function stageFor(opts) {
|
|
15426
|
+
if (opts.outcome === "won") return "Won";
|
|
15427
|
+
if (opts.outcome === "lost") return "Lost";
|
|
15428
|
+
if (opts.proposalSent) return "Proposal sent";
|
|
15429
|
+
if (opts.verdict) return "Findings";
|
|
15430
|
+
return "Gathering";
|
|
15431
|
+
}
|
|
15432
|
+
function tagFor(stage) {
|
|
15433
|
+
return "assessment:" + stage.toLowerCase().replace(/\s+/g, "-");
|
|
15434
|
+
}
|
|
15435
|
+
function renderHuman(state, computed, q2, today2) {
|
|
15436
|
+
const lines = [];
|
|
15437
|
+
lines.push(`${NOTE_TITLE} \u2014 ${state.biz || "unnamed"}${state.trade ? " \u2014 " + state.trade : ""} \u2014 ${today2}`);
|
|
15438
|
+
if (computed) {
|
|
15439
|
+
const bits = [`Verdict: ${computed.verdict}`];
|
|
15440
|
+
if (computed.monthly > 0) bits.push(`leaking ${money(computed.monthly)}/month`);
|
|
15441
|
+
if (computed.annualStop > 0) bits.push(`${money(computed.annualStop)}/year expected to stop`);
|
|
15442
|
+
if (computed.hrsWk > 0) bits.push(`${computed.hrsWk.toFixed(1)} hrs/week`);
|
|
15443
|
+
lines.push(bits.join(" \xB7 "));
|
|
15444
|
+
} else {
|
|
15445
|
+
lines.push("Verdict: not computed yet");
|
|
15446
|
+
}
|
|
15447
|
+
const byArea = /* @__PURE__ */ new Map();
|
|
15448
|
+
const ids = [.../* @__PURE__ */ new Set([...Object.keys(state.ans), ...Object.keys(state.skip)])];
|
|
15449
|
+
for (const id of ids) {
|
|
15450
|
+
const ask = q2.ask(id);
|
|
15451
|
+
const area = q2.area(id);
|
|
15452
|
+
if (!ask || !area) continue;
|
|
15453
|
+
const skipped = state.skip[id] === true;
|
|
15454
|
+
const raw = state.ans[id];
|
|
15455
|
+
const answered = !skipped && raw !== void 0 && String(raw).trim() !== "";
|
|
15456
|
+
if (!skipped && !answered) continue;
|
|
15457
|
+
const value = skipped ? "(they did not know)" : String(raw).trim();
|
|
15458
|
+
const bucket = byArea.get(area) ?? [];
|
|
15459
|
+
bucket.push(` ${ask}
|
|
15460
|
+
${value}`);
|
|
15461
|
+
byArea.set(area, bucket);
|
|
15462
|
+
}
|
|
15463
|
+
for (const area of q2.areaOrder()) {
|
|
15464
|
+
const bucket = byArea.get(area);
|
|
15465
|
+
if (!bucket || bucket.length === 0) continue;
|
|
15466
|
+
lines.push("");
|
|
15467
|
+
lines.push(area.toUpperCase());
|
|
15468
|
+
lines.push(...bucket);
|
|
15469
|
+
}
|
|
15470
|
+
const asm = Object.entries(state.asm ?? {});
|
|
15471
|
+
if (asm.length) {
|
|
15472
|
+
lines.push("");
|
|
15473
|
+
lines.push("ASSUMPTIONS THE OPERATOR SET");
|
|
15474
|
+
for (const [k, v] of asm) {
|
|
15475
|
+
lines.push(` ${k.replace(/_/g, " ")}: ${Math.round((Number(v) || 0) * 100)}%${Number(v) > 0 ? "" : " (set to zero \u2014 its finding was dropped, not estimated)"}`);
|
|
15476
|
+
}
|
|
15477
|
+
}
|
|
15478
|
+
return lines.join("\n");
|
|
15479
|
+
}
|
|
15480
|
+
function saveId(payload) {
|
|
15481
|
+
let h = 2166136261;
|
|
15482
|
+
for (let i = 0; i < payload.length; i++) {
|
|
15483
|
+
h ^= payload.charCodeAt(i);
|
|
15484
|
+
h = Math.imul(h, 16777619);
|
|
15485
|
+
}
|
|
15486
|
+
return (h >>> 0).toString(36).padStart(6, "0").slice(0, 6);
|
|
15487
|
+
}
|
|
15488
|
+
function sanitiseHuman(text) {
|
|
15489
|
+
return text.split(MACHINE_MARKER).join("--- (marker text removed) ---");
|
|
15490
|
+
}
|
|
15491
|
+
function buildNote(state, computed, q2, today2) {
|
|
15492
|
+
const payload = JSON.stringify({ v: 1, savedAt: today2, state, computed });
|
|
15493
|
+
return `${sanitiseHuman(renderHuman(state, computed, q2, today2))}
|
|
15494
|
+
|
|
15495
|
+
${MACHINE_MARKER}
|
|
15496
|
+
${payload}`;
|
|
15497
|
+
}
|
|
15498
|
+
function parseNote(body) {
|
|
15499
|
+
if (typeof body !== "string") return null;
|
|
15500
|
+
const found = [...body.matchAll(MARKER_LINE)];
|
|
15501
|
+
if (found.length === 0) return null;
|
|
15502
|
+
const last = found[found.length - 1];
|
|
15503
|
+
const raw = body.slice(last.index + last[0].length).trim();
|
|
15504
|
+
if (!raw) return null;
|
|
15505
|
+
let parsed;
|
|
15506
|
+
try {
|
|
15507
|
+
parsed = JSON.parse(raw);
|
|
15508
|
+
} catch {
|
|
15509
|
+
return null;
|
|
15510
|
+
}
|
|
15511
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
15512
|
+
const p = parsed;
|
|
15513
|
+
const s = p.state;
|
|
15514
|
+
if (!s || typeof s !== "object" || typeof s.biz !== "string") return null;
|
|
15515
|
+
if (!s.ans || typeof s.ans !== "object") return null;
|
|
15516
|
+
return {
|
|
15517
|
+
state: { biz: s.biz, trade: s.trade, ans: s.ans, skip: s.skip ?? {}, asm: s.asm ?? {} },
|
|
15518
|
+
computed: p.computed ?? null
|
|
15519
|
+
};
|
|
15520
|
+
}
|
|
15521
|
+
function buildContact(state, stage, contact) {
|
|
15522
|
+
const name = (contact.name ?? "").trim();
|
|
15523
|
+
const parts = name.split(/\s+/).filter(Boolean);
|
|
15524
|
+
return {
|
|
15525
|
+
payload: {
|
|
15526
|
+
firstName: parts[0],
|
|
15527
|
+
lastName: parts.length > 1 ? parts.slice(1).join(" ") : void 0,
|
|
15528
|
+
name: name || void 0,
|
|
15529
|
+
companyName: state.biz,
|
|
15530
|
+
email: contact.email?.trim() || void 0,
|
|
15531
|
+
phone: contact.phone?.trim() || void 0,
|
|
15532
|
+
tags: [tagFor(stage), "assessment"],
|
|
15533
|
+
source: "Command OS assessment"
|
|
15534
|
+
},
|
|
15535
|
+
dedupable: Boolean(contact.email?.trim() || contact.phone?.trim())
|
|
15536
|
+
};
|
|
15537
|
+
}
|
|
15538
|
+
function buildOpportunity(state, computed, stage, proposalValue) {
|
|
15539
|
+
const value = typeof proposalValue === "number" && proposalValue > 0 ? proposalValue : Math.max(0, Math.round(computed?.annualStop ?? 0));
|
|
15540
|
+
return {
|
|
15541
|
+
name: `${state.biz || "Assessment"} \u2014 assessment`,
|
|
15542
|
+
stageName: stage,
|
|
15543
|
+
monetaryValue: value,
|
|
15544
|
+
status: stage === "Won" ? "won" : stage === "Lost" ? "lost" : "open"
|
|
15545
|
+
};
|
|
15546
|
+
}
|
|
15547
|
+
function buildNoteBodies(note, limit = NOTE_LIMIT) {
|
|
15548
|
+
if (note.length <= limit) return [note];
|
|
15549
|
+
const found = [...note.matchAll(MARKER_LINE)];
|
|
15550
|
+
if (found.length === 0) return [note.slice(0, limit)];
|
|
15551
|
+
const m = found[found.length - 1];
|
|
15552
|
+
const human = note.slice(0, m.index);
|
|
15553
|
+
const payload = note.slice(m.index + m[0].length).trim();
|
|
15554
|
+
const chunkCountGuess = Math.max(1, Math.ceil(payload.length / Math.max(1, limit - MACHINE_MARKER.length - 40)));
|
|
15555
|
+
const suffix = ` (${chunkCountGuess}/${chunkCountGuess} s=xxxxxx)
|
|
15556
|
+
`.length;
|
|
15557
|
+
const per = limit - (MACHINE_MARKER.length + suffix + 2);
|
|
15558
|
+
if (per <= 0) throw new Error("note limit too small to carry a machine block");
|
|
15559
|
+
const chunks = [];
|
|
15560
|
+
for (let i = 0; i < payload.length; i += per) chunks.push(payload.slice(i, i + per));
|
|
15561
|
+
const firstRoom = limit - (MACHINE_MARKER.length + suffix + 2) - chunks[0].length - 60;
|
|
15562
|
+
const head2 = firstRoom > 200 ? human.slice(0, firstRoom) + "\n\n[\u2026trimmed for length; the full record is in the blocks below]\n\n" : "";
|
|
15563
|
+
const n = chunks.length;
|
|
15564
|
+
const sid = saveId(payload);
|
|
15565
|
+
return chunks.map((c, i) => `${i === 0 ? head2 : ""}${MACHINE_MARKER} (${i + 1}/${n} s=${sid})
|
|
15566
|
+
${c}`);
|
|
15567
|
+
}
|
|
15568
|
+
function parseNotes(bodies) {
|
|
15569
|
+
if (!Array.isArray(bodies) || bodies.length === 0) return null;
|
|
15570
|
+
if (bodies.length === 1) return parseNote(bodies[0]);
|
|
15571
|
+
const parts = [];
|
|
15572
|
+
for (const body of bodies) {
|
|
15573
|
+
if (typeof body !== "string") continue;
|
|
15574
|
+
const found = [...body.matchAll(MARKER_LINE)];
|
|
15575
|
+
if (!found.length) continue;
|
|
15576
|
+
const m = found[found.length - 1];
|
|
15577
|
+
if (!m[1] || !m[2]) continue;
|
|
15578
|
+
parts.push({ k: Number(m[1]), n: Number(m[2]), sid: m[3] ?? "", text: body.slice(m.index + m[0].length).trim() });
|
|
15579
|
+
}
|
|
15580
|
+
if (!parts.length) return null;
|
|
15581
|
+
const sid = parts[0].sid;
|
|
15582
|
+
if (parts.some((p) => p.sid !== sid)) return null;
|
|
15583
|
+
const n = parts[0].n;
|
|
15584
|
+
if (parts.some((p) => p.n !== n)) return null;
|
|
15585
|
+
const seen = new Set(parts.map((p) => p.k));
|
|
15586
|
+
if (seen.size !== n) return null;
|
|
15587
|
+
parts.sort((a, b) => a.k - b.k);
|
|
15588
|
+
return parseNote(`${MACHINE_MARKER}
|
|
15589
|
+
${parts.map((p) => p.text).join("")}`);
|
|
15590
|
+
}
|
|
15591
|
+
function pickLatest(notes) {
|
|
15592
|
+
if (!Array.isArray(notes)) return null;
|
|
15593
|
+
const groups = /* @__PURE__ */ new Map();
|
|
15594
|
+
notes.forEach((note, i) => {
|
|
15595
|
+
const body = typeof note?.body === "string" ? note.body : null;
|
|
15596
|
+
if (!body) return;
|
|
15597
|
+
const found = [...body.matchAll(MARKER_LINE)];
|
|
15598
|
+
if (!found.length) return;
|
|
15599
|
+
const m = found[found.length - 1];
|
|
15600
|
+
const when = String(note?.dateAdded ?? "");
|
|
15601
|
+
const key = m[3] ? `s:${m[3]}` : `one:${i}`;
|
|
15602
|
+
const n = m[2] ? Number(m[2]) : 1;
|
|
15603
|
+
const g = groups.get(key) ?? { key, when, bodies: [], n };
|
|
15604
|
+
g.bodies.push(body);
|
|
15605
|
+
if (when > g.when) g.when = when;
|
|
15606
|
+
groups.set(key, g);
|
|
15607
|
+
});
|
|
15608
|
+
const ordered = [...groups.values()].sort((a, b) => a.when === b.when ? 0 : a.when > b.when ? -1 : 1);
|
|
15609
|
+
for (const g of ordered) {
|
|
15610
|
+
if (g.bodies.length !== g.n) continue;
|
|
15611
|
+
const parsed = g.bodies.length === 1 ? parseNote(g.bodies[0]) : parseNotes(g.bodies);
|
|
15612
|
+
if (parsed) return parsed;
|
|
15613
|
+
}
|
|
15614
|
+
return null;
|
|
15615
|
+
}
|
|
15616
|
+
var PIPELINE_NAME, NOTE_TITLE, MACHINE_MARKER, MARKER_LINE, NOTE_LIMIT;
|
|
15617
|
+
var init_store = __esm({
|
|
15618
|
+
"src/command-os/assessment/store.ts"() {
|
|
15619
|
+
"use strict";
|
|
15620
|
+
PIPELINE_NAME = "Assessments";
|
|
15621
|
+
NOTE_TITLE = "GHL COMMAND ASSESSMENT";
|
|
15622
|
+
MACHINE_MARKER = "--- machine block, do not edit below this line ---";
|
|
15623
|
+
MARKER_LINE = /^--- machine block, do not edit below this line ---(?: \((\d+)\/(\d+)(?: s=([a-z0-9]{6}))?\))?[ \t]*$/gm;
|
|
15624
|
+
NOTE_LIMIT = 2e4;
|
|
15625
|
+
}
|
|
15626
|
+
});
|
|
15627
|
+
|
|
15628
|
+
// src/command-os/assessment/engine.js
|
|
15629
|
+
function parseTyped(text, unit) {
|
|
15630
|
+
var t = String(text).trim().replace(/[^0-9.,\-]/g, "");
|
|
15631
|
+
if (t === "" || t === "-") return NaN;
|
|
15632
|
+
var grouped = unit !== "rate";
|
|
15633
|
+
if (grouped && /^-?\d{1,3}(\.\d{3})+$/.test(t)) t = t.replace(/\./g, "");
|
|
15634
|
+
if (grouped && /^-?\d{1,3}(,\d{3})+(\.\d+)?$/.test(t)) t = t.replace(/,/g, "");
|
|
15635
|
+
t = t.replace(/,/g, ".");
|
|
15636
|
+
var parts = t.split(".");
|
|
15637
|
+
if (parts.length > 2) return NaN;
|
|
15638
|
+
return parseFloat(t);
|
|
15639
|
+
}
|
|
15640
|
+
function compute(ans, skip, asm) {
|
|
15641
|
+
ans = ans || {};
|
|
15642
|
+
skip = skip || {};
|
|
15643
|
+
asm = asm || {};
|
|
15644
|
+
function raw(id) {
|
|
15645
|
+
if (skip[id]) return null;
|
|
15646
|
+
var v2 = ans[id];
|
|
15647
|
+
if (v2 === void 0 || v2 === null || String(v2).trim() === "") return null;
|
|
15648
|
+
var n = parseTyped(v2, UNIT[id]);
|
|
15649
|
+
return isFinite(n) ? n : null;
|
|
15650
|
+
}
|
|
15651
|
+
function share(id) {
|
|
15652
|
+
var n = raw(id);
|
|
15653
|
+
if (n === null) return null;
|
|
15654
|
+
var r = n > 1 ? n / 100 : n;
|
|
15655
|
+
return r >= 0 && r <= 1 ? r : null;
|
|
15656
|
+
}
|
|
15657
|
+
function a(k) {
|
|
15658
|
+
var n = Number(asm[k]);
|
|
15659
|
+
return isFinite(n) && n > 0 ? n : 0;
|
|
15660
|
+
}
|
|
15661
|
+
var v = raw("avg_value"), c = share("close_rate");
|
|
15662
|
+
var core = v !== null && v > 0 && c !== null && c > 0;
|
|
15663
|
+
var F = [], O = [];
|
|
15664
|
+
function add(id, title2, needs, assumption, fn, fix) {
|
|
15665
|
+
var missing = needs.filter(function(n) {
|
|
15666
|
+
return (n.share ? share(n.id) : raw(n.id)) === null;
|
|
15667
|
+
}).map(function(n) {
|
|
15668
|
+
return n.id;
|
|
15669
|
+
});
|
|
15670
|
+
if (!core) {
|
|
15671
|
+
O.push({ id, title: title2, missing: PRICING_CORE.filter(function(k) {
|
|
15672
|
+
return (k === "avg_value" ? v : c) === null;
|
|
15673
|
+
}) });
|
|
15674
|
+
return;
|
|
15675
|
+
}
|
|
15676
|
+
if (missing.length) {
|
|
15677
|
+
O.push({ id, title: title2, missing });
|
|
15678
|
+
return;
|
|
15679
|
+
}
|
|
15680
|
+
if (assumption && a(assumption) === 0) {
|
|
15681
|
+
O.push({ id, title: title2, missing: [], declined: assumption });
|
|
15682
|
+
return;
|
|
15683
|
+
}
|
|
15684
|
+
var m = fn();
|
|
15685
|
+
if (!isFinite(m) || m < 0) {
|
|
15686
|
+
O.push({ id, title: title2, missing: needs.map(function(n) {
|
|
15687
|
+
return n.id;
|
|
15688
|
+
}) });
|
|
15689
|
+
return;
|
|
15690
|
+
}
|
|
15691
|
+
if (m === 0) {
|
|
15692
|
+
O.push({ id, title: title2, missing: [], reason: "nothing to price here on your numbers" });
|
|
15693
|
+
return;
|
|
15694
|
+
}
|
|
15695
|
+
F.push({ id, title: title2, monthly: m, assumption: assumption || null, fix });
|
|
15696
|
+
}
|
|
15697
|
+
add(
|
|
15698
|
+
"missed",
|
|
15699
|
+
"Calls you never get back to",
|
|
15700
|
+
[{ id: "calls_missed" }, { id: "missed_never_return", share: true }],
|
|
15701
|
+
null,
|
|
15702
|
+
function() {
|
|
15703
|
+
return raw("calls_missed") * share("missed_never_return") * c * v;
|
|
15704
|
+
},
|
|
15705
|
+
"Call answering, day and night"
|
|
15706
|
+
);
|
|
15707
|
+
add(
|
|
15708
|
+
"waited",
|
|
15709
|
+
"Enquiries that waited",
|
|
15710
|
+
[{ id: "waited_month" }],
|
|
15711
|
+
"response_penalty",
|
|
15712
|
+
function() {
|
|
15713
|
+
return raw("waited_month") * a("response_penalty") * c * v;
|
|
15714
|
+
},
|
|
15715
|
+
"Instant reply on every channel"
|
|
15716
|
+
);
|
|
15717
|
+
if (core && raw("quotes_month") !== null && raw("quotes_won") !== null && raw("quotes_won") > raw("quotes_month")) {
|
|
15718
|
+
O.push({
|
|
15719
|
+
id: "quotes",
|
|
15720
|
+
title: "Quotes nobody chases",
|
|
15721
|
+
missing: [],
|
|
15722
|
+
reason: "you told us more quotes were won than were sent \u2014 worth checking before this is priced"
|
|
15723
|
+
});
|
|
15724
|
+
} else {
|
|
15725
|
+
add(
|
|
15726
|
+
"quotes",
|
|
15727
|
+
"Quotes nobody chases",
|
|
15728
|
+
[{ id: "quotes_month" }, { id: "quotes_won" }],
|
|
15729
|
+
"quote_recovery",
|
|
15730
|
+
function() {
|
|
15731
|
+
return (raw("quotes_month") - raw("quotes_won")) * a("quote_recovery") * v;
|
|
15732
|
+
},
|
|
15733
|
+
"Follow-up that runs itself"
|
|
15734
|
+
);
|
|
15735
|
+
}
|
|
15736
|
+
add(
|
|
15737
|
+
"noshow",
|
|
15738
|
+
"No-shows nobody rebooks",
|
|
15739
|
+
[{ id: "booked_month" }, { id: "show_rate", share: true }],
|
|
15740
|
+
"noshow_recovery",
|
|
15741
|
+
function() {
|
|
15742
|
+
return raw("booked_month") * (1 - share("show_rate")) * a("noshow_recovery") * c * v;
|
|
15743
|
+
},
|
|
15744
|
+
"Reminders and rebooking"
|
|
15745
|
+
);
|
|
15746
|
+
var monthly = F.reduce(function(s, f) {
|
|
15747
|
+
return s + f.monthly;
|
|
15748
|
+
}, 0);
|
|
15749
|
+
var stop = monthly * a("recovery");
|
|
15750
|
+
var oneoff = null;
|
|
15751
|
+
if (core && raw("dormant_count") !== null && a("reactivation") > 0) {
|
|
15752
|
+
oneoff = raw("dormant_count") * a("reactivation") * c * v;
|
|
15753
|
+
}
|
|
15754
|
+
var hrs = [];
|
|
15755
|
+
HOUR_KEYS.forEach(function(p) {
|
|
15756
|
+
var n = raw(p[0]);
|
|
15757
|
+
if (n !== null && n >= 0) hrs.push({ id: p[0], t: p[1], h: n });
|
|
15758
|
+
});
|
|
15759
|
+
var hrsWk = hrs.reduce(function(s, x) {
|
|
15760
|
+
return s + x.h;
|
|
15761
|
+
}, 0);
|
|
15762
|
+
var verdict, line2;
|
|
15763
|
+
if (!core) {
|
|
15764
|
+
verdict = "insufficient";
|
|
15765
|
+
line2 = "We could not put a number on anything yet, because the figures everything else is priced against are not known.";
|
|
15766
|
+
} else if (!F.length) {
|
|
15767
|
+
verdict = "insufficient";
|
|
15768
|
+
line2 = "We could not price a single leak from the numbers available.";
|
|
15769
|
+
} else if (monthly < v) {
|
|
15770
|
+
verdict = "thin";
|
|
15771
|
+
line2 = "Everything we found together is worth less than one new customer a month to you. That is worth saying plainly rather than dressing up.";
|
|
15772
|
+
} else {
|
|
15773
|
+
verdict = "findings";
|
|
15774
|
+
line2 = "We found " + F.length + " thing" + (F.length === 1 ? "" : "s") + " worth fixing.";
|
|
15775
|
+
}
|
|
15776
|
+
return {
|
|
15777
|
+
F,
|
|
15778
|
+
O,
|
|
15779
|
+
core,
|
|
15780
|
+
monthly,
|
|
15781
|
+
annual: monthly * 12,
|
|
15782
|
+
stop,
|
|
15783
|
+
annualStop: stop * 12,
|
|
15784
|
+
oneoff,
|
|
15785
|
+
hrs,
|
|
15786
|
+
hrsWk,
|
|
15787
|
+
verdict,
|
|
15788
|
+
line: line2
|
|
15789
|
+
};
|
|
15790
|
+
}
|
|
15791
|
+
var AREAS, Q, ASSUMPTIONS, HOUR_KEYS, PRICING_CORE, UNIT;
|
|
15792
|
+
var init_engine = __esm({
|
|
15793
|
+
"src/command-os/assessment/engine.js"() {
|
|
15794
|
+
"use strict";
|
|
15795
|
+
AREAS = [
|
|
15796
|
+
["money", "What the work is worth", "Before anything else I need two numbers, because everything today gets multiplied by them."],
|
|
15797
|
+
["phone", "The phone", "Let's start where most of it goes."],
|
|
15798
|
+
["speed", "How fast anyone replies", "Now the enquiries that don't come by phone."],
|
|
15799
|
+
["repeat", "The same questions, again", "This one is about your time rather than your money."],
|
|
15800
|
+
["quotes", "After the quote goes out", "Walk me through what happens after you send a price."],
|
|
15801
|
+
["booking", "Getting them in the diary", "How does an appointment actually get made?"],
|
|
15802
|
+
["dormant", "Everyone who went quiet", "Now the cheapest money in the building, and the part nobody works."],
|
|
15803
|
+
["reviews", "What people see about you", "Your reviews, and how they happen."],
|
|
15804
|
+
["website", "What your website does", "What the site is actually for."],
|
|
15805
|
+
["found", "Whether you get found", "And how people arrive in the first place."],
|
|
15806
|
+
["week", "Your own week", "Last part, and it's about you rather than the business."]
|
|
15807
|
+
];
|
|
15808
|
+
Q = [
|
|
15809
|
+
["avg_value", "money", "When you win a job, what's it worth on average?", "Give me the middle of it. Not your best month, not the tiny ones.", "currency", "Every finding today gets multiplied by this. Without it I can't put a number on anything."],
|
|
15810
|
+
["close_rate", "money", "When you actually get someone talking properly, how often does that turn into work?", "Out of ten real conversations, how many buy?", "rate", "Turns a lost enquiry into a lost job rather than a lost phone call."],
|
|
15811
|
+
["margin_basis", "money", "Do you want me to work in what the job brings in, or what you keep?", "", "text", "The report says which one it used, every time."],
|
|
15812
|
+
["capacity", "money", "If I found you thirty percent more work tomorrow, could you do it?", "What breaks first, crews or trucks or you?", "text", "If they can't deliver more, the whole report points at margin and time instead of volume."],
|
|
15813
|
+
["calls_month", "phone", "Roughly how many calls come in on a normal month?", "Think about a busy Tuesday and work out from there.", "count", "The base for everything else on the phone."],
|
|
15814
|
+
["calls_missed", "phone", "How many of those go to voicemail or just ring out?", "What happens when you're up a ladder and it rings?", "count", "The most common leak, and the one owners underestimate most."],
|
|
15815
|
+
["missed_never_return", "phone", "Of the ones you miss, how many never call back?", "Half? More?", "rate", "This is what stops us overclaiming. We only count the ones you genuinely lose."],
|
|
15816
|
+
["who_answers", "phone", "Who answers the phone when it rings?", "And what were they doing before it rang?", "text", "If it's you, this is a time finding as well as a money one."],
|
|
15817
|
+
["after_hours_calls", "phone", "What share of your calls come in outside working hours?", "Evenings and weekends. Storm season?", "rate", "Separates a staffing problem from an automation one."],
|
|
15818
|
+
["voicemail_fate", "phone", "When someone does leave a voicemail, what happens to it?", "", "text", "'I get to them at night' is the after-hours finding in their own words."],
|
|
15819
|
+
["nonphone_month", "speed", "How many enquiries a month come in some other way? Form, text, Facebook.", "", "count", "Counted separately from calls so nobody gets counted twice."],
|
|
15820
|
+
["first_response", "speed", "From one of those landing to a real person replying, how long is that usually?", "Same hour? Same day? Next morning?", "minutes", "The gap between arriving and being answered is where enquiries go cold."],
|
|
15821
|
+
["waited_month", "speed", "How many a month sit overnight before anyone replies?", "", "count", "The slow-response finding is priced on this count, not on your average."],
|
|
15822
|
+
["reply_channel", "speed", "When you do reply, do you call them or write back?", "", "text", "Shapes what we'd build, not what we'd charge."],
|
|
15823
|
+
["repeat_questions", "repeat", "What are the three questions you answer over and over every week?", "The ones where you know the answer before they finish asking.", "text", "Every one of these is a job a machine does perfectly and you do resentfully."],
|
|
15824
|
+
["repeat_time", "repeat", "How much of your week goes on answering those?", "", "hours", "Straight into the hours number."],
|
|
15825
|
+
["who_fields", "repeat", "Who fields them, you or someone else?", "", "text", "If it's a person on payroll, this finding has a salary attached."],
|
|
15826
|
+
["quotes_month", "quotes", "How many quotes or estimates go out in a month?", "", "count", "These are your warmest people. They already talked to you."],
|
|
15827
|
+
["quotes_won", "quotes", "How many of those turn into work?", "", "count", "The gap between sent and won is the follow-up finding."],
|
|
15828
|
+
["quote_followup", "quotes", "What happens to a quote after you send it?", "Be honest. Does anything chase it, or is it on them to call you?", "text", "'Nothing' is the most common answer in this whole assessment."],
|
|
15829
|
+
["quote_time", "quotes", "How many hours a week go on putting quotes together?", "", "hours", "Quoting is often the biggest single block of hours we find."],
|
|
15830
|
+
["quote_delay", "quotes", "How long between the site visit and them getting the price?", "", "text", "A quote that arrives four days later competes with three that arrived first."],
|
|
15831
|
+
["booked_month", "booking", "How many appointments or site visits get booked in a month?", "", "count", "The base for the no-show finding."],
|
|
15832
|
+
["show_rate", "booking", "Of those, how many actually happen?", "Nobody home, cancelled last minute, forgot.", "rate", "A no-show is a job you paid to get twice."],
|
|
15833
|
+
["reminders", "booking", "Does anything remind them before the appointment?", "", "text", "If the answer is 'I text them myself', that's both a time and a money finding."],
|
|
15834
|
+
["noshow_fate", "booking", "When someone no-shows, what happens next?", "", "text", "'Nothing' means the rebooking finding is real."],
|
|
15835
|
+
["dormant_count", "dormant", "How many people are sitting in your system who enquired once and never went anywhere?", "Old quotes, old customers, people who called in 2023.", "count", "These people already raised their hand once. The cheapest money you have."],
|
|
15836
|
+
["dormant_worked", "dormant", "Has anyone ever gone back to that list?", "", "text", "If nothing has ever been sent, the whole list is untouched revenue."],
|
|
15837
|
+
["past_customers", "dormant", "How many are past customers rather than people who never bought?", "", "count", "Past customers convert far better, and they need a different message."],
|
|
15838
|
+
["repeat_cycle", "dormant", "How often would a past customer normally need you again?", "", "text", "Decides whether reactivation is a one-off campaign or a standing programme."],
|
|
15839
|
+
["review_count", "reviews", "How many Google reviews do you have?", "", "count", "The number a new customer sees before they call you."],
|
|
15840
|
+
["review_ask", "reviews", "How do you ask for a review at the moment?", "Who remembers to do it, and when?", "text", "'When I remember' is why the newest one is months old."],
|
|
15841
|
+
["review_reply", "reviews", "Do you reply to them?", "", "text", "An unanswered bad review is the first thing a new customer reads."],
|
|
15842
|
+
["review_time", "reviews", "How many hours a week go on chasing reviews?", "", "hours", "Small on its own. Adds up with the rest."],
|
|
15843
|
+
["site_job", "website", "What do you want someone to do when they land on your site?", "Call, book, fill something in?", "text", "Most sites do not do the thing the owner assumes they do."],
|
|
15844
|
+
["site_capture", "website", "Can someone book you from the website without speaking to anyone?", "", "text", "If not, every after-hours visitor is gone."],
|
|
15845
|
+
["site_leads", "website", "How many enquiries a month come from the site itself?", "", "count", "Against traffic this gives the conversion finding, scoped to your trade."],
|
|
15846
|
+
["site_age", "website", "When was the site last touched?", "", "text", "Decides rebuild versus repair."],
|
|
15847
|
+
["lead_sources", "found", "Where does your work actually come from? Rank them.", "Word of mouth, Google, Facebook, repeat, trucks?", "text", "Tells us which leak matters most before we price any of them."],
|
|
15848
|
+
["paid_spend", "found", "Are you spending anything on advertising each month?", "", "currency", "If they are, every leak we find is also wasted ad spend."],
|
|
15849
|
+
["gbp_status", "found", "Do you manage your Google listing yourself?", "", "text", "The cheapest visibility fix there is, and usually neglected."],
|
|
15850
|
+
["admin_hours", "week", "How many hours a week go on paperwork and admin rather than actual work?", "Including the bits you do at night.", "hours", "The number the whole hours promise is measured against."],
|
|
15851
|
+
["evening_work", "week", "How much of that happens after dinner?", "", "hours", "The answer that makes owners sit up. Part of the number above, not extra."],
|
|
15852
|
+
["stop_doing", "week", "If you could stop doing one thing tomorrow and never do it again, what is it?", "", "text", "Their priority in their words. It leads the roadmap even if it isn't the biggest number."],
|
|
15853
|
+
["team_size", "week", "Who else is in the business?", "", "count", "Decides whether a fix is automation or delegation."],
|
|
15854
|
+
["growth_goal", "week", "What would a good twelve months look like?", "A number, if you have one.", "text", "The report closes against their goal, not ours."]
|
|
15855
|
+
];
|
|
15856
|
+
ASSUMPTIONS = [
|
|
15857
|
+
["recovery", "How much of the leak we actually stop", 0.5, "Assumes we recover half of what is leaking. Your number, and the most important one here."],
|
|
15858
|
+
["quote_recovery", "Chasing an unclosed quote recovers", 0.1, "Assumes chasing recovers 1 in 10. Your number, not ours."],
|
|
15859
|
+
["response_penalty", "Fall-off once an enquiry has waited", 0.25, "Assumes a 25% fall-off once they have waited. Your number."],
|
|
15860
|
+
["noshow_recovery", "A no-show that gets rebooked", 0.5, "Assumes half could be rebooked. Your number."],
|
|
15861
|
+
["reactivation", "Dormant contacts who respond", 0.02, "Assumes 2% come back. Your number. One-off, not annual."]
|
|
15862
|
+
];
|
|
15863
|
+
HOUR_KEYS = [
|
|
15864
|
+
["repeat_time", "Answering the same questions"],
|
|
15865
|
+
["quote_time", "Putting quotes together"],
|
|
15866
|
+
["review_time", "Chasing reviews"],
|
|
15867
|
+
["admin_hours", "Paperwork and admin"]
|
|
15868
|
+
];
|
|
15869
|
+
PRICING_CORE = ["avg_value", "close_rate"];
|
|
15870
|
+
UNIT = {};
|
|
15871
|
+
Q.forEach(function(q2) {
|
|
15872
|
+
UNIT[q2[0]] = q2[4];
|
|
15873
|
+
});
|
|
15874
|
+
}
|
|
15875
|
+
});
|
|
15876
|
+
|
|
15877
|
+
// src/command-os/assessment/transcript.ts
|
|
15878
|
+
function loose(s) {
|
|
15879
|
+
return String(s ?? "").replace(/[‘’ʼ]/g, "'").replace(/[“”]/g, '"').replace(/[–—]/g, "-").replace(/\s+/g, " ").trim().toLowerCase();
|
|
15880
|
+
}
|
|
15881
|
+
function digits(s) {
|
|
15882
|
+
return String(s ?? "").replace(/[^0-9]/g, "");
|
|
15883
|
+
}
|
|
15884
|
+
function valueIsInQuote(value, quote) {
|
|
15885
|
+
const v = loose(value);
|
|
15886
|
+
const q2 = loose(quote);
|
|
15887
|
+
if (v.length === 0) return false;
|
|
15888
|
+
if (q2.includes(v)) return true;
|
|
15889
|
+
const dv = digits(value);
|
|
15890
|
+
if (dv.length > 0 && digits(quote).includes(dv)) return true;
|
|
15891
|
+
return false;
|
|
15892
|
+
}
|
|
15893
|
+
function gateExtraction(transcript, proposed, knownIds) {
|
|
15894
|
+
const accepted = [];
|
|
15895
|
+
const held = [];
|
|
15896
|
+
const rejected = [];
|
|
15897
|
+
const patch = {};
|
|
15898
|
+
const hay = loose(transcript);
|
|
15899
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15900
|
+
for (const p of proposed ?? []) {
|
|
15901
|
+
const item = {
|
|
15902
|
+
questionId: String(p?.questionId ?? ""),
|
|
15903
|
+
value: String(p?.value ?? "").trim(),
|
|
15904
|
+
quote: String(p?.quote ?? "").trim(),
|
|
15905
|
+
reasoning: p?.reasoning
|
|
15906
|
+
};
|
|
15907
|
+
if (!knownIds.has(item.questionId)) {
|
|
15908
|
+
rejected.push({ ...item, because: `"${item.questionId}" is not a question this assessment asks.` });
|
|
15909
|
+
continue;
|
|
15910
|
+
}
|
|
15911
|
+
if (seen.has(item.questionId)) {
|
|
15912
|
+
rejected.push({ ...item, because: "the same question was answered twice; only the first was taken." });
|
|
15913
|
+
continue;
|
|
15914
|
+
}
|
|
15915
|
+
if (item.value === "") {
|
|
15916
|
+
rejected.push({ ...item, because: "no value was given." });
|
|
15917
|
+
continue;
|
|
15918
|
+
}
|
|
15919
|
+
if (item.quote === "") {
|
|
15920
|
+
rejected.push({ ...item, because: "no quote was given, so there is nothing tying this answer to what they said." });
|
|
15921
|
+
continue;
|
|
15922
|
+
}
|
|
15923
|
+
if (item.quote.length > MAX_QUOTE) {
|
|
15924
|
+
rejected.push({ ...item, because: `the quote is ${item.quote.length} characters. A quote that long is a summary, not a citation.` });
|
|
15925
|
+
continue;
|
|
15926
|
+
}
|
|
15927
|
+
if (hay.length === 0 || !hay.includes(loose(item.quote))) {
|
|
15928
|
+
rejected.push({ ...item, because: "that quote does not appear in the transcript." });
|
|
15929
|
+
continue;
|
|
15930
|
+
}
|
|
15931
|
+
seen.add(item.questionId);
|
|
15932
|
+
if (valueIsInQuote(item.value, item.quote)) {
|
|
15933
|
+
accepted.push(item);
|
|
15934
|
+
patch[item.questionId] = item.value;
|
|
15935
|
+
} else {
|
|
15936
|
+
held.push({
|
|
15937
|
+
...item,
|
|
15938
|
+
because: `"${item.value}" is not in what they said, so it was worked out rather than heard. Confirm it before it counts.`
|
|
15939
|
+
});
|
|
15940
|
+
}
|
|
15941
|
+
}
|
|
15942
|
+
return { accepted, held, rejected, patch };
|
|
15943
|
+
}
|
|
15944
|
+
function mergeAccepted(ans, result) {
|
|
15945
|
+
return { ...ans, ...result.patch };
|
|
15946
|
+
}
|
|
15947
|
+
var MAX_QUOTE;
|
|
15948
|
+
var init_transcript = __esm({
|
|
15949
|
+
"src/command-os/assessment/transcript.ts"() {
|
|
15950
|
+
"use strict";
|
|
15951
|
+
MAX_QUOTE = 400;
|
|
15952
|
+
}
|
|
15953
|
+
});
|
|
15954
|
+
|
|
15955
|
+
// src/command-os/assessment/report.js
|
|
15956
|
+
function esc(s) {
|
|
15957
|
+
return String(s === void 0 || s === null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
15958
|
+
}
|
|
15959
|
+
function money2(n) {
|
|
15960
|
+
return "$" + Math.round(Number(n) || 0).toLocaleString("en-US");
|
|
15961
|
+
}
|
|
15962
|
+
function safeColour(c) {
|
|
15963
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(String(c || "")) ? String(c) : "#1F4E79";
|
|
15964
|
+
}
|
|
15965
|
+
function renderReport(opts) {
|
|
15966
|
+
var state = opts.state || {};
|
|
15967
|
+
var r = opts.computed || {};
|
|
15968
|
+
var agency = opts.agency || {};
|
|
15969
|
+
var services = Array.isArray(opts.services) ? opts.services : [];
|
|
15970
|
+
var assumptionLabels = opts.assumptionLabels || {};
|
|
15971
|
+
var date = opts.date || "";
|
|
15972
|
+
var accent = safeColour(agency.accent);
|
|
15973
|
+
var biz = esc(state.biz || "your business");
|
|
15974
|
+
var F = Array.isArray(r.F) ? r.F : [];
|
|
15975
|
+
var O = Array.isArray(r.O) ? r.O : [];
|
|
15976
|
+
var hrs = Array.isArray(r.hrs) ? r.hrs : [];
|
|
15977
|
+
var out = [];
|
|
15978
|
+
out.push('<!doctype html><html lang="en"><head><meta charset="utf-8">');
|
|
15979
|
+
out.push('<meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
15980
|
+
out.push('<meta name="robots" content="noindex,nofollow,noarchive">');
|
|
15981
|
+
out.push("<title>Where " + biz + " is leaking</title>");
|
|
15982
|
+
out.push("<style>" + css(accent) + "</style></head><body><div class=wrap>");
|
|
15983
|
+
out.push("<header>");
|
|
15984
|
+
if (agency.name) out.push('<p class="by">Prepared by ' + esc(agency.name) + "</p>");
|
|
15985
|
+
out.push("<h1>Where " + biz + " is leaking</h1>");
|
|
15986
|
+
if (date) out.push('<p class="when">' + esc(date) + "</p>");
|
|
15987
|
+
out.push("</header>");
|
|
15988
|
+
if (r.verdict === "insufficient") {
|
|
15989
|
+
out.push('<div class="verdict stop"><b>We could not put a number on this yet</b>' + esc(r.line || "") + "</div>");
|
|
15990
|
+
out.push(measureFirst(O));
|
|
15991
|
+
out.push(footer(agency));
|
|
15992
|
+
return out.join("\n") + "</div></body></html>";
|
|
15993
|
+
}
|
|
15994
|
+
if (r.verdict === "thin") {
|
|
15995
|
+
out.push('<div class="verdict warn"><b>Honestly, there is not much here</b>' + esc(r.line || "") + "</div>");
|
|
15996
|
+
}
|
|
15997
|
+
out.push('<div class="hgrid">');
|
|
15998
|
+
if (r.hrsWk > 0) out.push(head(r.hrsWk.toFixed(1), "hours a week going into work a system should do"));
|
|
15999
|
+
if (r.annual > 0) out.push(head(money2(r.annual), "a year leaking, on your own numbers"));
|
|
16000
|
+
if (r.annualStop > 0) out.push(head(money2(r.annualStop), "a year we expect to actually stop"));
|
|
16001
|
+
out.push("</div>");
|
|
16002
|
+
if (opts.answered && opts.answered.length) {
|
|
16003
|
+
out.push("<h2>What we started from, in your words</h2><table>");
|
|
16004
|
+
for (var a = 0; a < opts.answered.length; a++) {
|
|
16005
|
+
var q2 = opts.answered[a];
|
|
16006
|
+
out.push("<tr><td>" + esc(q2.ask) + '</td><td class="num">' + (q2.unknown ? '<span class="unk">you did not know</span>' : esc(q2.value)) + "</td></tr>");
|
|
16007
|
+
}
|
|
16008
|
+
out.push("</table>");
|
|
16009
|
+
}
|
|
16010
|
+
if (F.length) {
|
|
16011
|
+
out.push("<h2>Where the money goes</h2><table>");
|
|
16012
|
+
out.push('<tr><th>What is happening</th><th class="num">A month</th></tr>');
|
|
16013
|
+
for (var i = 0; i < F.length; i++) {
|
|
16014
|
+
var f = F[i];
|
|
16015
|
+
var note = f.assumption ? '<span class="assume">' + esc(assumptionLabels[f.assumption] || "This is your assumption, not a measurement.") + "</span>" : "";
|
|
16016
|
+
out.push("<tr><td><strong>" + esc(f.title) + "</strong>" + note + '</td><td class="num">' + money2(f.monthly) + "</td></tr>");
|
|
16017
|
+
}
|
|
16018
|
+
out.push('<tr class="tot"><td><strong>Leaking, every month</strong></td><td class="num"><strong>' + money2(r.monthly) + "</strong></td></tr>");
|
|
16019
|
+
if (r.stop > 0) {
|
|
16020
|
+
out.push('<tr><td><strong>What we expect to actually stop</strong><span class="assume">' + esc(assumptionLabels.recovery || "No system catches everything. This is your figure.") + '</span></td><td class="num"><strong>' + money2(r.stop) + "</strong></td></tr>");
|
|
16021
|
+
}
|
|
16022
|
+
if (r.oneoff !== null && r.oneoff !== void 0 && r.oneoff > 0) {
|
|
16023
|
+
out.push('<tr><td>Your dormant list, worked once<span class="assume">' + esc(assumptionLabels.reactivation || "Your assumption, not a measurement.") + ' This is a one-off and is left out of every annual figure above.</span></td><td class="num">' + money2(r.oneoff) + "</td></tr>");
|
|
16024
|
+
}
|
|
16025
|
+
out.push("</table>");
|
|
16026
|
+
}
|
|
16027
|
+
var named = O.filter(function(o2) {
|
|
16028
|
+
return o2.reason || o2.declined;
|
|
16029
|
+
});
|
|
16030
|
+
if (named.length) {
|
|
16031
|
+
out.push("<h2>Named, and deliberately not priced</h2><ul>");
|
|
16032
|
+
for (var j = 0; j < named.length; j++) {
|
|
16033
|
+
var o = named[j];
|
|
16034
|
+
out.push("<li><strong>" + esc(o.title) + "</strong> — " + esc(o.declined ? "priced only on an assumption you chose not to make." : o.reason) + "</li>");
|
|
16035
|
+
}
|
|
16036
|
+
out.push("</ul><p class=small>We can see these matter and we are not going to invent a number for them.</p>");
|
|
16037
|
+
}
|
|
16038
|
+
if (hrs.length) {
|
|
16039
|
+
out.push("<h2>Where the time goes</h2><table>");
|
|
16040
|
+
for (var h = 0; h < hrs.length; h++) {
|
|
16041
|
+
out.push("<tr><td>" + esc(hrs[h].t) + '</td><td class="num">' + Number(hrs[h].h).toFixed(1) + " hrs</td></tr>");
|
|
16042
|
+
}
|
|
16043
|
+
out.push('<tr class="tot"><td><strong>A week</strong></td><td class="num"><strong>' + Number(r.hrsWk).toFixed(1) + "</strong></td></tr></table>");
|
|
16044
|
+
}
|
|
16045
|
+
if (F.length) {
|
|
16046
|
+
out.push("<h2>What we would do, in order</h2><table>");
|
|
16047
|
+
out.push("<tr><th>Fixes</th><th>What it is</th></tr>");
|
|
16048
|
+
for (var k = 0; k < F.length; k++) {
|
|
16049
|
+
var fx = F[k];
|
|
16050
|
+
var svc = services.find(function(s) {
|
|
16051
|
+
return Array.isArray(s.covers) && s.covers.indexOf(fx.id) !== -1;
|
|
16052
|
+
});
|
|
16053
|
+
out.push("<tr><td>" + esc(fx.title) + "</td><td>" + esc(svc ? svc.name : fx.fix || "") + "</td></tr>");
|
|
16054
|
+
}
|
|
16055
|
+
out.push("</table>");
|
|
16056
|
+
if (!services.length) {
|
|
16057
|
+
out.push('<p class="small">Service names above are descriptions, not a quote. Add your own offers so this reads in your language and at your prices.</p>');
|
|
16058
|
+
}
|
|
16059
|
+
}
|
|
16060
|
+
var priced = services.filter(function(s) {
|
|
16061
|
+
return typeof s.price === "number" && s.price > 0;
|
|
16062
|
+
});
|
|
16063
|
+
if (priced.length && r.annualStop > 0) {
|
|
16064
|
+
var total = priced.reduce(function(s, x) {
|
|
16065
|
+
return s + x.price;
|
|
16066
|
+
}, 0);
|
|
16067
|
+
out.push("<h2>The investment</h2><table>");
|
|
16068
|
+
for (var p = 0; p < priced.length; p++) {
|
|
16069
|
+
out.push("<tr><td>" + esc(priced[p].name) + '</td><td class="num">' + money2(priced[p].price) + "</td></tr>");
|
|
16070
|
+
}
|
|
16071
|
+
out.push('<tr class="tot"><td><strong>Total</strong></td><td class="num"><strong>' + money2(total) + "</strong></td></tr>");
|
|
16072
|
+
out.push('<tr><td>Against what we expect to stop, first year</td><td class="num">' + money2(r.annualStop) + "</td></tr>");
|
|
16073
|
+
if (total > 0) {
|
|
16074
|
+
var ratio = r.annualStop / total;
|
|
16075
|
+
out.push('<tr class="tot"><td><strong>You are spending $1 to stop $' + ratio.toFixed(2) + '</strong></td><td class="num"><strong>' + ratio.toFixed(1) + "×</strong></td></tr>");
|
|
16076
|
+
}
|
|
16077
|
+
out.push("</table>");
|
|
16078
|
+
}
|
|
16079
|
+
out.push(sourceLine(state, r, assumptionLabels));
|
|
16080
|
+
out.push(footer(agency));
|
|
16081
|
+
return out.join("\n") + "</div></body></html>";
|
|
16082
|
+
}
|
|
16083
|
+
function head(n, label) {
|
|
16084
|
+
return '<div><span class="hn">' + esc(n) + '</span><span class="hl">' + esc(label) + "</span></div>";
|
|
16085
|
+
}
|
|
16086
|
+
function measureFirst(O) {
|
|
16087
|
+
var missing = [];
|
|
16088
|
+
for (var i = 0; i < O.length; i++) {
|
|
16089
|
+
var m = O[i].missing || [];
|
|
16090
|
+
for (var j = 0; j < m.length; j++) if (missing.indexOf(m[j]) === -1) missing.push(m[j]);
|
|
16091
|
+
}
|
|
16092
|
+
if (!missing.length) return "";
|
|
16093
|
+
return "<h2>What to measure first</h2><p>These are the numbers that would let us price the rest. None of them need a system — a week of writing them down is enough.</p><ul><li>" + missing.map(esc).join("</li><li>") + "</li></ul>";
|
|
16094
|
+
}
|
|
16095
|
+
function sourceLine(state, r, labels) {
|
|
16096
|
+
var used = [];
|
|
16097
|
+
var F = r.F || [];
|
|
16098
|
+
for (var i = 0; i < F.length; i++) if (F[i].assumption && used.indexOf(F[i].assumption) === -1) used.push(F[i].assumption);
|
|
16099
|
+
var n = used.length + (r.stop > 0 ? 1 : 0);
|
|
16100
|
+
return '<p class="src">Every figure here came from what you told us' + (state.trade ? ", or from your own " + esc(state.trade) + " business" : "") + ". " + (n > 0 ? n + " of them " + (n === 1 ? "is an estimate" : "are estimates") + " we chose together, each marked where it is used. " : "") + "Nothing here is an industry average applied to your business.</p>";
|
|
16101
|
+
}
|
|
16102
|
+
function footer(agency) {
|
|
16103
|
+
if (!agency.name) return "";
|
|
16104
|
+
return "<footer>" + esc(agency.name) + (agency.contact ? " · " + esc(agency.contact) : "") + "</footer>";
|
|
16105
|
+
}
|
|
16106
|
+
function css(accent) {
|
|
16107
|
+
return [
|
|
16108
|
+
":root{--a:" + accent + ";--ink:#14171F;--mut:#4A5163;--rule:#D8DDE5;--pan:#F4F6F8}",
|
|
16109
|
+
"*{box-sizing:border-box}",
|
|
16110
|
+
"body{margin:0;background:#fff;color:var(--ink);font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}",
|
|
16111
|
+
".wrap{max-width:44rem;margin:0 auto;padding:2rem 1.1rem 4rem}",
|
|
16112
|
+
"header{border-bottom:3px solid var(--a);padding-bottom:.9rem;margin-bottom:1.4rem}",
|
|
16113
|
+
".by{font-size:.72rem;letter-spacing:.09em;text-transform:uppercase;color:var(--a);margin:0 0 .4rem;font-weight:600}",
|
|
16114
|
+
"h1{font-size:1.9rem;line-height:1.15;margin:0;font-weight:700}",
|
|
16115
|
+
".when{color:var(--mut);font-size:.82rem;margin:.4rem 0 0}",
|
|
16116
|
+
"h2{font-size:1.15rem;margin:2rem 0 .5rem;font-weight:700}",
|
|
16117
|
+
"table{border-collapse:collapse;width:100%;margin:.6rem 0;font-size:.94rem}",
|
|
16118
|
+
"th,td{text-align:left;padding:.5rem .6rem;border-bottom:1px solid var(--rule);vertical-align:top}",
|
|
16119
|
+
"th{font-size:.7rem;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);border-bottom:2px solid var(--rule)}",
|
|
16120
|
+
".num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}",
|
|
16121
|
+
"tr.tot td{background:var(--pan)}",
|
|
16122
|
+
".assume{display:block;font-size:.76rem;color:var(--mut);font-style:italic;margin-top:.2rem}",
|
|
16123
|
+
".unk{color:var(--mut);font-style:italic}",
|
|
16124
|
+
".hgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(9rem,1fr));gap:1rem;margin:1.2rem 0}",
|
|
16125
|
+
".hn{display:block;font-size:1.7rem;font-weight:700;color:var(--a);line-height:1.1}",
|
|
16126
|
+
".hl{display:block;font-size:.78rem;color:var(--mut);margin-top:.15rem}",
|
|
16127
|
+
".verdict{border-left:4px solid var(--a);background:var(--pan);padding:.75rem .9rem;margin:1rem 0;font-size:.94rem}",
|
|
16128
|
+
".verdict.warn{border-left-color:#8A5300}.verdict.stop{border-left-color:#9A3324}",
|
|
16129
|
+
".verdict b{display:block;margin-bottom:.15rem}",
|
|
16130
|
+
".src{margin-top:1.6rem;padding-top:.8rem;border-top:1px solid var(--rule);font-size:.82rem;color:var(--mut)}",
|
|
16131
|
+
".small{font-size:.82rem;color:var(--mut)}",
|
|
16132
|
+
"ul{padding-left:1.1rem}li{margin-bottom:.3rem}",
|
|
16133
|
+
"footer{margin-top:2rem;padding-top:.8rem;border-top:3px solid var(--a);font-size:.8rem;color:var(--mut)}",
|
|
16134
|
+
"@media print{.wrap{max-width:none}body{font-size:11pt}}"
|
|
16135
|
+
].join("");
|
|
16136
|
+
}
|
|
16137
|
+
var init_report = __esm({
|
|
16138
|
+
"src/command-os/assessment/report.js"() {
|
|
16139
|
+
"use strict";
|
|
16140
|
+
}
|
|
16141
|
+
});
|
|
16142
|
+
|
|
16143
|
+
// src/tools/assessment.ts
|
|
16144
|
+
function today() {
|
|
16145
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
16146
|
+
}
|
|
16147
|
+
function normalise(s) {
|
|
16148
|
+
return { biz: s.biz, trade: s.trade, ans: s.ans, skip: s.skip ?? {}, asm: s.asm ?? {} };
|
|
16149
|
+
}
|
|
16150
|
+
function computedFor(state) {
|
|
16151
|
+
const r = compute(state.ans, state.skip, state.asm);
|
|
16152
|
+
return {
|
|
16153
|
+
verdict: r.verdict === "insufficient" ? "insufficient" : r.verdict,
|
|
16154
|
+
monthly: r.monthly,
|
|
16155
|
+
annual: r.annual,
|
|
16156
|
+
annualStop: r.annualStop,
|
|
16157
|
+
hrsWk: r.hrsWk,
|
|
16158
|
+
findingCount: r.F.length
|
|
16159
|
+
};
|
|
16160
|
+
}
|
|
16161
|
+
async function findStage(client, locationId2, stage) {
|
|
16162
|
+
let raw;
|
|
16163
|
+
try {
|
|
16164
|
+
raw = await client.get("/opportunities/pipelines", { params: { locationId: locationId2 } });
|
|
16165
|
+
} catch (e) {
|
|
16166
|
+
return { error: `could not read pipelines: ${String(e)}` };
|
|
16167
|
+
}
|
|
16168
|
+
const pipelines = raw?.pipelines ?? [];
|
|
16169
|
+
const pipe = pipelines.find((p) => (p.name ?? "").trim().toLowerCase() === PIPELINE_NAME.toLowerCase());
|
|
16170
|
+
if (!pipe) {
|
|
16171
|
+
return { error: `no "${PIPELINE_NAME}" pipeline in this account. The answers were saved on the contact. To get the deal on a board, create a pipeline called "${PIPELINE_NAME}" with stages: Gathering, Findings, Proposal sent, Won, Lost \u2014 then save again.` };
|
|
16172
|
+
}
|
|
16173
|
+
const st = (pipe.stages ?? []).find((s) => (s.name ?? "").trim().toLowerCase() === stage.toLowerCase());
|
|
16174
|
+
if (!st) {
|
|
16175
|
+
return { error: `the "${PIPELINE_NAME}" pipeline has no "${stage}" stage. The answers were saved on the contact. Add that stage and save again.` };
|
|
16176
|
+
}
|
|
16177
|
+
return { pipelineId: pipe.id, stageId: st.id, stageName: stage };
|
|
16178
|
+
}
|
|
16179
|
+
function registerAssessmentTools(server2, client) {
|
|
16180
|
+
safeTool(
|
|
16181
|
+
server2,
|
|
16182
|
+
"save_assessment",
|
|
16183
|
+
"Save an assessment into YOUR OWN GoHighLevel: the prospect as a contact, the answer sheet as a note on that contact, and the deal as an opportunity in your Assessments pipeline. Nothing is stored anywhere else. Re-saving writes a new note rather than overwriting, so the history is kept and the newest is what loads back.",
|
|
16184
|
+
{
|
|
16185
|
+
locationId: import_zod56.z.string().optional().describe("Your own GHL sub-account. Defaults to the active one."),
|
|
16186
|
+
assessment: StateSchema,
|
|
16187
|
+
contactName: import_zod56.z.string().optional().describe("The person you spoke to."),
|
|
16188
|
+
email: import_zod56.z.string().optional(),
|
|
16189
|
+
phone: import_zod56.z.string().optional(),
|
|
16190
|
+
proposalValue: import_zod56.z.number().optional().describe("What you quoted, if you have quoted. Used as the opportunity value instead of the annual figure."),
|
|
16191
|
+
outcome: import_zod56.z.enum(["won", "lost"]).optional().describe("Set only when the deal has actually closed."),
|
|
16192
|
+
proposalSent: import_zod56.z.boolean().optional()
|
|
16193
|
+
},
|
|
16194
|
+
async (args) => {
|
|
16195
|
+
const locationId2 = client.resolveLocationId(args.locationId);
|
|
16196
|
+
const state = normalise(args.assessment);
|
|
16197
|
+
const computed = computedFor(state);
|
|
16198
|
+
const stage = stageFor({ verdict: computed.verdict, proposalSent: args.proposalSent, outcome: args.outcome });
|
|
16199
|
+
const { payload: contactBody, dedupable } = buildContact(state, stage, {
|
|
16200
|
+
name: args.contactName,
|
|
16201
|
+
email: args.email,
|
|
16202
|
+
phone: args.phone
|
|
16203
|
+
});
|
|
16204
|
+
const upserted = await client.post("/contacts/upsert", {
|
|
16205
|
+
body: { ...contactBody, locationId: locationId2 }
|
|
16206
|
+
});
|
|
16207
|
+
const contactId = upserted?.contact?.id ?? upserted?.id;
|
|
16208
|
+
if (!contactId) {
|
|
16209
|
+
return { saved: false, error: "GoHighLevel did not return a contact id, so nothing was written. Nothing partial was left behind." };
|
|
16210
|
+
}
|
|
16211
|
+
const bodies = buildNoteBodies(buildNote(state, computed, LOOKUP, today()));
|
|
16212
|
+
const noteIds = [];
|
|
16213
|
+
for (const body of bodies) {
|
|
16214
|
+
const note = await client.post(`/contacts/${contactId}/notes`, { body: { body } });
|
|
16215
|
+
const id = note?.note?.id ?? note?.id;
|
|
16216
|
+
if (id) noteIds.push(id);
|
|
16217
|
+
}
|
|
16218
|
+
if (noteIds.length !== bodies.length) {
|
|
16219
|
+
return {
|
|
16220
|
+
saved: false,
|
|
16221
|
+
contactId,
|
|
16222
|
+
error: `the answer sheet needed ${bodies.length} note(s) and only ${noteIds.length} were written. A part-written record will NOT load \u2014 it refuses rather than returning half an assessment. Save again.`
|
|
16223
|
+
};
|
|
16224
|
+
}
|
|
16225
|
+
const hit = await findStage(client, locationId2, stage);
|
|
16226
|
+
const opp = buildOpportunity(state, computed, stage, args.proposalValue);
|
|
16227
|
+
let opportunity = null;
|
|
16228
|
+
let opportunityNote = null;
|
|
16229
|
+
if ("error" in hit) {
|
|
16230
|
+
opportunityNote = hit.error;
|
|
16231
|
+
} else {
|
|
16232
|
+
opportunity = await client.post("/opportunities/", {
|
|
16233
|
+
body: {
|
|
16234
|
+
locationId: locationId2,
|
|
16235
|
+
contactId,
|
|
16236
|
+
name: opp.name,
|
|
16237
|
+
status: opp.status,
|
|
16238
|
+
monetaryValue: opp.monetaryValue,
|
|
16239
|
+
pipelineId: hit.pipelineId,
|
|
16240
|
+
pipelineStageId: hit.stageId,
|
|
16241
|
+
source: "Command OS assessment"
|
|
16242
|
+
}
|
|
16243
|
+
});
|
|
16244
|
+
}
|
|
16245
|
+
return {
|
|
16246
|
+
saved: true,
|
|
16247
|
+
contactId,
|
|
16248
|
+
noteIds,
|
|
16249
|
+
notesWritten: bodies.length,
|
|
16250
|
+
stage,
|
|
16251
|
+
tag: tagFor(stage),
|
|
16252
|
+
verdict: computed.verdict,
|
|
16253
|
+
monthly: Math.round(computed.monthly),
|
|
16254
|
+
hoursPerWeek: Number(computed.hrsWk.toFixed(1)),
|
|
16255
|
+
opportunity,
|
|
16256
|
+
opportunityNote,
|
|
16257
|
+
warning: dedupable ? null : "No email or phone was given, so this contact cannot be matched next time and a second assessment for the same business would create a second contact.",
|
|
16258
|
+
whereItLives: `Contact ${contactId} in location ${locationId2}. Anyone on your team with a GoHighLevel login can open it.`
|
|
16259
|
+
};
|
|
16260
|
+
}
|
|
16261
|
+
);
|
|
16262
|
+
safeTool(
|
|
16263
|
+
server2,
|
|
16264
|
+
"load_assessment",
|
|
16265
|
+
"Load a saved assessment back out of your GoHighLevel, so someone else on your team can pick it up. Reads the notes on a contact and returns the most recent complete answer sheet. If a note has been edited or a piece is missing it returns nothing rather than a partial record.",
|
|
16266
|
+
{
|
|
16267
|
+
contactId: import_zod56.z.string().describe("The contact the assessment was saved against.")
|
|
16268
|
+
},
|
|
16269
|
+
async ({ contactId }) => {
|
|
16270
|
+
const raw = await client.get(`/contacts/${contactId}/notes`);
|
|
16271
|
+
const ours = raw?.notes ?? [];
|
|
16272
|
+
const joined = pickLatest(ours);
|
|
16273
|
+
if (!joined) {
|
|
16274
|
+
return {
|
|
16275
|
+
loaded: false,
|
|
16276
|
+
notesRead: ours.length,
|
|
16277
|
+
error: "No complete assessment could be read from this contact's notes. That is deliberate: a partial or edited record is refused rather than returned, because a half-loaded assessment overwriting a real one is the worst thing this could do."
|
|
16278
|
+
};
|
|
16279
|
+
}
|
|
16280
|
+
const r = compute(joined.state.ans, joined.state.skip, joined.state.asm);
|
|
16281
|
+
return {
|
|
16282
|
+
loaded: true,
|
|
16283
|
+
notesRead: ours.length,
|
|
16284
|
+
assessment: joined.state,
|
|
16285
|
+
verdict: r.verdict,
|
|
16286
|
+
monthly: Math.round(r.monthly),
|
|
16287
|
+
annualStop: Math.round(r.annualStop),
|
|
16288
|
+
hoursPerWeek: Number(r.hrsWk.toFixed(1)),
|
|
16289
|
+
findings: r.F.map((f) => ({ title: f.title, monthly: Math.round(f.monthly), fix: f.fix })),
|
|
16290
|
+
notPriced: r.O.map((o) => ({ title: o.title, missing: o.missing, declined: o.declined ?? null, reason: o.reason ?? null }))
|
|
16291
|
+
};
|
|
16292
|
+
}
|
|
16293
|
+
);
|
|
16294
|
+
safeTool(
|
|
16295
|
+
server2,
|
|
16296
|
+
"list_assessments",
|
|
16297
|
+
"List the assessments on your Assessments pipeline: who they are for, what stage each is at, and what the deal is worth. This is the board an agency owner checks, and the list a remote employee picks their next job from.",
|
|
16298
|
+
{
|
|
16299
|
+
locationId: import_zod56.z.string().optional().describe("Your own GHL sub-account. Defaults to the active one."),
|
|
16300
|
+
stage: import_zod56.z.enum(["Gathering", "Findings", "Proposal sent", "Won", "Lost"]).optional().describe("Only this stage.")
|
|
16301
|
+
},
|
|
16302
|
+
async ({ locationId: loc, stage }) => {
|
|
16303
|
+
const locationId2 = client.resolveLocationId(loc);
|
|
16304
|
+
const pipes = await client.get("/opportunities/pipelines", { params: { locationId: locationId2 } });
|
|
16305
|
+
const list2 = pipes?.pipelines ?? [];
|
|
16306
|
+
const pipe = list2.find((p) => (p.name ?? "").trim().toLowerCase() === PIPELINE_NAME.toLowerCase());
|
|
16307
|
+
if (!pipe) {
|
|
16308
|
+
return {
|
|
16309
|
+
pipeline: null,
|
|
16310
|
+
assessments: [],
|
|
16311
|
+
note: `There is no "${PIPELINE_NAME}" pipeline in this account yet. Saved assessments still live on their contacts; a pipeline is what puts them on a board.`
|
|
16312
|
+
};
|
|
16313
|
+
}
|
|
16314
|
+
const params = { location_id: locationId2, pipeline_id: pipe.id };
|
|
16315
|
+
if (stage) {
|
|
16316
|
+
const st = (pipe.stages ?? []).find((s) => (s.name ?? "").trim().toLowerCase() === stage.toLowerCase());
|
|
16317
|
+
if (st) params.pipeline_stage_id = st.id;
|
|
16318
|
+
}
|
|
16319
|
+
const found = await client.get("/opportunities/search", { params });
|
|
16320
|
+
return { pipeline: { id: pipe.id, name: pipe.name }, result: found };
|
|
16321
|
+
}
|
|
16322
|
+
);
|
|
16323
|
+
safeTool(
|
|
16324
|
+
server2,
|
|
16325
|
+
"read_assessment_transcript",
|
|
16326
|
+
"Turn a recorded conversation into assessment answers, WITHOUT letting anything into the record that the owner did not say. You read the transcript and propose answers; each one must carry the verbatim words it came from. This checks every quote actually appears in the transcript and throws out the ones that do not, and holds back anything you worked out rather than heard so the operator can confirm it. Use it after a coffee-shop meeting, a Zoom call or a Plaud recording.",
|
|
16327
|
+
{
|
|
16328
|
+
transcript: import_zod56.z.string().min(20).describe("The recording, as text. Paste the whole thing."),
|
|
16329
|
+
proposed: import_zod56.z.array(import_zod56.z.object({
|
|
16330
|
+
questionId: import_zod56.z.string().describe("The assessment question this answers."),
|
|
16331
|
+
value: import_zod56.z.string().describe("What to store, in the unit the question wants."),
|
|
16332
|
+
quote: import_zod56.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."),
|
|
16333
|
+
reasoning: import_zod56.z.string().optional().describe("How you got from the quote to the value, when they are not the same.")
|
|
16334
|
+
})).describe("One entry per question you could answer. Leave out anything the conversation did not cover."),
|
|
16335
|
+
existing: import_zod56.z.record(import_zod56.z.union([import_zod56.z.string(), import_zod56.z.number()])).optional().describe("Answers already captured, so this adds to them rather than replacing them.")
|
|
16336
|
+
},
|
|
16337
|
+
async ({ transcript, proposed, existing }) => {
|
|
16338
|
+
const gated = gateExtraction(transcript, proposed, QUESTION_IDS);
|
|
16339
|
+
const merged = mergeAccepted(existing ?? {}, gated);
|
|
16340
|
+
return {
|
|
16341
|
+
applied: gated.accepted.length,
|
|
16342
|
+
answers: merged,
|
|
16343
|
+
confirmBeforeTheseCount: gated.held.map((h) => ({
|
|
16344
|
+
question: ASK.get(h.questionId) ?? h.questionId,
|
|
16345
|
+
proposedValue: h.value,
|
|
16346
|
+
theyActuallySaid: h.quote,
|
|
16347
|
+
howItWasWorkedOut: h.reasoning ?? null,
|
|
16348
|
+
why: h.because
|
|
16349
|
+
})),
|
|
16350
|
+
thrownOut: gated.rejected.map((r) => ({ questionId: r.questionId, value: r.value, why: r.because })),
|
|
16351
|
+
note: gated.rejected.some((r) => /does not appear in the transcript/.test(r.because)) ? "One or more answers cited words that are not in the transcript. Those were discarded, not stored. That check is the only thing standing between a recording and an invented figure in a client's report." : null
|
|
16352
|
+
};
|
|
16353
|
+
}
|
|
16354
|
+
);
|
|
16355
|
+
safeTool(
|
|
16356
|
+
server2,
|
|
16357
|
+
"write_assessment_report",
|
|
16358
|
+
"Write the finished assessment report to a file on THIS machine, ready to hand to the prospect. The file is a complete, self-contained page: no fonts, scripts or images loaded from anywhere, so it opens on any host, in any browser, forever. It is written in the operator's branding and carries nothing about the software that produced it. We never publish it anywhere ourselves \u2014 the operator owns where it goes.",
|
|
16359
|
+
{
|
|
16360
|
+
assessment: StateSchema,
|
|
16361
|
+
agencyName: import_zod56.z.string().optional().describe("Whose letterhead this goes out on. Read from your agency profile if you have one set."),
|
|
16362
|
+
agencyContact: import_zod56.z.string().optional().describe("Email or phone for the footer."),
|
|
16363
|
+
accent: import_zod56.z.string().optional().describe("Brand colour as a hex code, e.g. #8A2B2B."),
|
|
16364
|
+
services: import_zod56.z.array(import_zod56.z.object({
|
|
16365
|
+
name: import_zod56.z.string().describe("Your service, in the words you sell it in."),
|
|
16366
|
+
price: import_zod56.z.number().optional().describe("Your price. Leave it out and the report shows the plan without an investment section rather than inventing a number."),
|
|
16367
|
+
covers: import_zod56.z.array(import_zod56.z.string()).describe("Which findings it fixes: missed, waited, quotes, noshow.")
|
|
16368
|
+
})).optional().describe("YOUR menu at YOUR prices. Never ours."),
|
|
16369
|
+
outDir: import_zod56.z.string().optional().describe("Where to write it. Defaults to ~/Documents/assessments.")
|
|
16370
|
+
},
|
|
16371
|
+
async (args) => {
|
|
16372
|
+
const state = normalise(args.assessment);
|
|
16373
|
+
const r = compute(state.ans, state.skip, state.asm);
|
|
16374
|
+
const labels = {};
|
|
16375
|
+
for (const a of ASSUMPTIONS) labels[a[0]] = a[3];
|
|
16376
|
+
const answered = Q.filter((q2) => {
|
|
16377
|
+
const id = q2[0];
|
|
16378
|
+
return state.skip[id] === true || state.ans[id] !== void 0 && String(state.ans[id]).trim() !== "";
|
|
16379
|
+
}).map((q2) => state.skip[q2[0]] ? { ask: q2[2], unknown: true } : { ask: q2[2], value: String(state.ans[q2[0]]) });
|
|
16380
|
+
const html2 = renderReport({
|
|
16381
|
+
state,
|
|
16382
|
+
computed: r,
|
|
16383
|
+
agency: { name: args.agencyName, contact: args.agencyContact, accent: args.accent },
|
|
16384
|
+
services: args.services ?? [],
|
|
16385
|
+
assumptionLabels: labels,
|
|
16386
|
+
date: (/* @__PURE__ */ new Date()).toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" }),
|
|
16387
|
+
answered
|
|
16388
|
+
});
|
|
16389
|
+
const dir = args.outDir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), "Documents", "assessments");
|
|
16390
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
16391
|
+
const slug3 = (state.biz || "assessment").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
16392
|
+
const file = (0, import_node_path.join)(dir, `${slug3}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.html`);
|
|
16393
|
+
(0, import_node_fs.writeFileSync)(file, html2, "utf8");
|
|
16394
|
+
return {
|
|
16395
|
+
written: file,
|
|
16396
|
+
verdict: r.verdict,
|
|
16397
|
+
findings: r.F.length,
|
|
16398
|
+
monthly: Math.round(r.monthly),
|
|
16399
|
+
hoursPerWeek: Number(r.hrsWk.toFixed(1)),
|
|
16400
|
+
brandedAs: args.agencyName ?? null,
|
|
16401
|
+
priced: (args.services ?? []).some((s) => typeof s.price === "number" && s.price > 0),
|
|
16402
|
+
whatToDoWithIt: [
|
|
16403
|
+
"Open it and read it before anyone else does.",
|
|
16404
|
+
"Send it as the file, or drop it on your own website and send the link.",
|
|
16405
|
+
"It is marked not-to-be-indexed and its filename is random, because it contains your prospect's revenue and customer numbers."
|
|
16406
|
+
],
|
|
16407
|
+
notDoneForYou: "We do not publish this anywhere. It is your prospect's business data on your letterhead, so where it goes is your call and your host.",
|
|
16408
|
+
warning: args.agencyName ? null : "No agency name was given, so it goes out with no letterhead. Set your agency profile or pass agencyName."
|
|
16409
|
+
};
|
|
16410
|
+
}
|
|
16411
|
+
);
|
|
16412
|
+
}
|
|
16413
|
+
var import_zod56, import_node_crypto3, import_node_fs, import_node_os, import_node_path, QUESTION_IDS, AREA_TITLE, ASK, AREA_OF, LOOKUP, StateSchema;
|
|
16414
|
+
var init_assessment = __esm({
|
|
16415
|
+
"src/tools/assessment.ts"() {
|
|
16416
|
+
"use strict";
|
|
16417
|
+
import_zod56 = require("zod");
|
|
16418
|
+
init_tool_helpers();
|
|
16419
|
+
init_store();
|
|
16420
|
+
init_engine();
|
|
16421
|
+
init_transcript();
|
|
16422
|
+
init_report();
|
|
16423
|
+
import_node_crypto3 = require("node:crypto");
|
|
16424
|
+
import_node_fs = require("node:fs");
|
|
16425
|
+
import_node_os = require("node:os");
|
|
16426
|
+
import_node_path = require("node:path");
|
|
16427
|
+
QUESTION_IDS = new Set(Q.map((q2) => q2[0]));
|
|
16428
|
+
AREA_TITLE = new Map(AREAS.map((a) => [a[0], a[1]]));
|
|
16429
|
+
ASK = new Map(Q.map((q2) => [q2[0], q2[2]]));
|
|
16430
|
+
AREA_OF = new Map(Q.map((q2) => [q2[0], q2[1]]));
|
|
16431
|
+
LOOKUP = {
|
|
16432
|
+
ask: (id) => ASK.get(id),
|
|
16433
|
+
area: (id) => AREA_TITLE.get(AREA_OF.get(id) ?? ""),
|
|
16434
|
+
areaOrder: () => AREAS.map((a) => a[1])
|
|
16435
|
+
};
|
|
16436
|
+
StateSchema = import_zod56.z.object({
|
|
16437
|
+
biz: import_zod56.z.string().min(1).describe("The prospect's business name."),
|
|
16438
|
+
trade: import_zod56.z.string().optional().describe("Their trade. Decides which benchmarks may be applied."),
|
|
16439
|
+
ans: import_zod56.z.record(import_zod56.z.union([import_zod56.z.string(), import_zod56.z.number()])).describe("questionId -> answer, exactly as typed."),
|
|
16440
|
+
skip: import_zod56.z.record(import_zod56.z.boolean()).optional().describe("questionId -> true where they did not know."),
|
|
16441
|
+
asm: import_zod56.z.record(import_zod56.z.number()).optional().describe("assumption id -> rate as a decimal.")
|
|
16442
|
+
});
|
|
16443
|
+
}
|
|
16444
|
+
});
|
|
16445
|
+
|
|
15390
16446
|
// src/client-engagements.ts
|
|
15391
16447
|
function clientEngagementsPath() {
|
|
15392
16448
|
const override = process.env[CLIENT_ENGAGEMENTS_ENV];
|
|
@@ -15576,8 +16632,8 @@ function daysUntil(then, now) {
|
|
|
15576
16632
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(then);
|
|
15577
16633
|
if (!m) return void 0;
|
|
15578
16634
|
const target = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
15579
|
-
const
|
|
15580
|
-
return Math.round((target -
|
|
16635
|
+
const today2 = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
|
|
16636
|
+
return Math.round((target - today2) / 864e5);
|
|
15581
16637
|
}
|
|
15582
16638
|
function whenInWords(days) {
|
|
15583
16639
|
if (days === 0) return "today";
|
|
@@ -15640,7 +16696,7 @@ function clientWeek(input) {
|
|
|
15640
16696
|
if (below && floor !== void 0) {
|
|
15641
16697
|
doNext.push({
|
|
15642
16698
|
title: `You are billing ${input.clientName} below your own floor`,
|
|
15643
|
-
why: `They pay ${
|
|
16699
|
+
why: `They pay ${money3(e.price, contract.currency)}${e.cadence === "monthly" ? " a month" : ""}, and the floor you set for that work is ${money3(floor, contract.currency)}. Both numbers are yours; nothing here is estimated.`,
|
|
15644
16700
|
needs: "you"
|
|
15645
16701
|
});
|
|
15646
16702
|
}
|
|
@@ -15665,12 +16721,12 @@ function clientWeek(input) {
|
|
|
15665
16721
|
notKnown
|
|
15666
16722
|
};
|
|
15667
16723
|
}
|
|
15668
|
-
var
|
|
16724
|
+
var money3;
|
|
15669
16725
|
var init_client_week = __esm({
|
|
15670
16726
|
"src/client-week.ts"() {
|
|
15671
16727
|
"use strict";
|
|
15672
16728
|
init_client_engagements();
|
|
15673
|
-
|
|
16729
|
+
money3 = (n, currency) => currency ? `${currency} ${n.toLocaleString("en-US")}` : n.toLocaleString("en-US");
|
|
15674
16730
|
}
|
|
15675
16731
|
});
|
|
15676
16732
|
|
|
@@ -16424,7 +17480,7 @@ var init_question_set = __esm({
|
|
|
16424
17480
|
|
|
16425
17481
|
// src/intake-to-build/plan.ts
|
|
16426
17482
|
function nsRef(ns) {
|
|
16427
|
-
return
|
|
17483
|
+
return import_zod57.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
|
|
16428
17484
|
}
|
|
16429
17485
|
function refNamespace(ref) {
|
|
16430
17486
|
return ref.split(".")[0];
|
|
@@ -16834,11 +17890,11 @@ function validateBuildPlan(input) {
|
|
|
16834
17890
|
referencesScanned: scanned
|
|
16835
17891
|
};
|
|
16836
17892
|
}
|
|
16837
|
-
var
|
|
17893
|
+
var import_zod57, REF_NAMESPACES, REF_RE, refSchema, USER_PENDING_REF, userRefSchema, userRefOrPendingSchema, USER_ROLES, EMAIL_RE, userSchema, stageSchema, pipelineSchema, GHL_FIELD_DATATYPES, customFieldSchema, tagSchema, customValueSchema, CALENDAR_TYPES, TEAMLESS_CALENDAR_TYPES, SINGLE_STAFF_CALENDAR_TYPE, openHoursBlockSchema, CALENDAR_SLOT_UNITS, calendarSchema, formFieldSchema, formSchema, pageSchema, FUNNEL_TARGETS, FUNNEL_HOSTS, funnelSchema, emailAssetSchema, smsAssetSchema, emailTemplateSchema, smsTemplateSchema, templatesSchema, waitUnit, branchActionOptions, branchActionSchema, findOpportunitySchema, actionSchema, APPOINTMENT_STATUSES, CALL_STATUSES, NUMBER_VALIDATION_STATES, triggerSchema, workflowSchema, HANDOFF_OWNER_LEGACY, handoffSchema, buildPlanSchema, PLAN_ERROR_CODES, NURTURE_MIN_DAYS, WAIT_UNIT_DAYS, isNurtureName, isSpeedName, KNOWN_STANDARD_FORM_KEYS;
|
|
16838
17894
|
var init_plan = __esm({
|
|
16839
17895
|
"src/intake-to-build/plan.ts"() {
|
|
16840
17896
|
"use strict";
|
|
16841
|
-
|
|
17897
|
+
import_zod57 = require("zod");
|
|
16842
17898
|
REF_NAMESPACES = [
|
|
16843
17899
|
"pipeline",
|
|
16844
17900
|
"stage",
|
|
@@ -16859,29 +17915,29 @@ var init_plan = __esm({
|
|
|
16859
17915
|
"sms_template"
|
|
16860
17916
|
];
|
|
16861
17917
|
REF_RE = new RegExp(`^(${REF_NAMESPACES.join("|")})\\.[a-z0-9]+(_[a-z0-9]+)*$`);
|
|
16862
|
-
refSchema =
|
|
17918
|
+
refSchema = import_zod57.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
|
|
16863
17919
|
USER_PENDING_REF = "user.__pending__";
|
|
16864
17920
|
userRefSchema = nsRef("user");
|
|
16865
|
-
userRefOrPendingSchema =
|
|
17921
|
+
userRefOrPendingSchema = import_zod57.z.union([userRefSchema, import_zod57.z.literal(USER_PENDING_REF)]);
|
|
16866
17922
|
USER_ROLES = ["admin", "user"];
|
|
16867
17923
|
EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
16868
|
-
userSchema =
|
|
17924
|
+
userSchema = import_zod57.z.object({
|
|
16869
17925
|
ref: userRefSchema,
|
|
16870
|
-
firstName:
|
|
16871
|
-
lastName:
|
|
16872
|
-
email:
|
|
16873
|
-
role:
|
|
16874
|
-
phone:
|
|
17926
|
+
firstName: import_zod57.z.string().min(1),
|
|
17927
|
+
lastName: import_zod57.z.string().min(1),
|
|
17928
|
+
email: import_zod57.z.string().regex(EMAIL_RE, "must be an email address (GHL creates the login from it)"),
|
|
17929
|
+
role: import_zod57.z.enum(USER_ROLES),
|
|
17930
|
+
phone: import_zod57.z.string().optional()
|
|
16875
17931
|
});
|
|
16876
|
-
stageSchema =
|
|
17932
|
+
stageSchema = import_zod57.z.object({
|
|
16877
17933
|
ref: nsRef("stage"),
|
|
16878
|
-
name:
|
|
16879
|
-
position:
|
|
17934
|
+
name: import_zod57.z.string(),
|
|
17935
|
+
position: import_zod57.z.number().int().nonnegative()
|
|
16880
17936
|
});
|
|
16881
|
-
pipelineSchema =
|
|
17937
|
+
pipelineSchema = import_zod57.z.object({
|
|
16882
17938
|
ref: nsRef("pipeline"),
|
|
16883
|
-
name:
|
|
16884
|
-
stages:
|
|
17939
|
+
name: import_zod57.z.string(),
|
|
17940
|
+
stages: import_zod57.z.array(stageSchema).min(1)
|
|
16885
17941
|
});
|
|
16886
17942
|
GHL_FIELD_DATATYPES = [
|
|
16887
17943
|
"TEXT",
|
|
@@ -16898,21 +17954,21 @@ var init_plan = __esm({
|
|
|
16898
17954
|
"FILE_UPLOAD",
|
|
16899
17955
|
"SIGNATURE"
|
|
16900
17956
|
];
|
|
16901
|
-
customFieldSchema =
|
|
17957
|
+
customFieldSchema = import_zod57.z.object({
|
|
16902
17958
|
ref: nsRef("field"),
|
|
16903
|
-
name:
|
|
16904
|
-
dataType:
|
|
16905
|
-
model:
|
|
16906
|
-
options:
|
|
17959
|
+
name: import_zod57.z.string(),
|
|
17960
|
+
dataType: import_zod57.z.enum(GHL_FIELD_DATATYPES),
|
|
17961
|
+
model: import_zod57.z.enum(["contact", "opportunity"]).optional(),
|
|
17962
|
+
options: import_zod57.z.array(import_zod57.z.string()).optional()
|
|
16907
17963
|
});
|
|
16908
|
-
tagSchema =
|
|
17964
|
+
tagSchema = import_zod57.z.object({
|
|
16909
17965
|
ref: nsRef("tag"),
|
|
16910
|
-
name:
|
|
17966
|
+
name: import_zod57.z.string()
|
|
16911
17967
|
});
|
|
16912
|
-
customValueSchema =
|
|
17968
|
+
customValueSchema = import_zod57.z.object({
|
|
16913
17969
|
ref: nsRef("cv"),
|
|
16914
|
-
name:
|
|
16915
|
-
value:
|
|
17970
|
+
name: import_zod57.z.string(),
|
|
17971
|
+
value: import_zod57.z.string().optional(),
|
|
16916
17972
|
filledBy: refSchema.optional()
|
|
16917
17973
|
});
|
|
16918
17974
|
CALENDAR_TYPES = [
|
|
@@ -16924,128 +17980,128 @@ var init_plan = __esm({
|
|
|
16924
17980
|
];
|
|
16925
17981
|
TEAMLESS_CALENDAR_TYPES = /* @__PURE__ */ new Set(["event"]);
|
|
16926
17982
|
SINGLE_STAFF_CALENDAR_TYPE = "round_robin";
|
|
16927
|
-
openHoursBlockSchema =
|
|
16928
|
-
daysOfTheWeek:
|
|
16929
|
-
hours:
|
|
16930
|
-
|
|
16931
|
-
openHour:
|
|
16932
|
-
openMinute:
|
|
16933
|
-
closeHour:
|
|
16934
|
-
closeMinute:
|
|
17983
|
+
openHoursBlockSchema = import_zod57.z.object({
|
|
17984
|
+
daysOfTheWeek: import_zod57.z.array(import_zod57.z.number().int().min(0).max(6)),
|
|
17985
|
+
hours: import_zod57.z.array(
|
|
17986
|
+
import_zod57.z.object({
|
|
17987
|
+
openHour: import_zod57.z.number().int().min(0).max(23),
|
|
17988
|
+
openMinute: import_zod57.z.number().int().min(0).max(59),
|
|
17989
|
+
closeHour: import_zod57.z.number().int().min(0).max(23),
|
|
17990
|
+
closeMinute: import_zod57.z.number().int().min(0).max(59)
|
|
16935
17991
|
})
|
|
16936
17992
|
)
|
|
16937
17993
|
});
|
|
16938
17994
|
CALENDAR_SLOT_UNITS = ["mins", "hours"];
|
|
16939
|
-
calendarSchema =
|
|
17995
|
+
calendarSchema = import_zod57.z.object({
|
|
16940
17996
|
ref: nsRef("calendar"),
|
|
16941
|
-
name:
|
|
16942
|
-
calendarType:
|
|
16943
|
-
openHours:
|
|
16944
|
-
availabilityType:
|
|
16945
|
-
requiresStaff:
|
|
17997
|
+
name: import_zod57.z.string(),
|
|
17998
|
+
calendarType: import_zod57.z.enum(CALENDAR_TYPES),
|
|
17999
|
+
openHours: import_zod57.z.array(openHoursBlockSchema).optional(),
|
|
18000
|
+
availabilityType: import_zod57.z.number().int().optional(),
|
|
18001
|
+
requiresStaff: import_zod57.z.boolean().optional(),
|
|
16946
18002
|
/** Slot length (finding 25, 2026-08-26). GoHighLevel defaults to 30-minute
|
|
16947
18003
|
* slots when this is omitted — a brief that says "Discovery Call, 15
|
|
16948
18004
|
* minutes" must land here as slotDuration: 15. Unit defaults to "mins". */
|
|
16949
|
-
slotDuration:
|
|
16950
|
-
slotDurationUnit:
|
|
18005
|
+
slotDuration: import_zod57.z.number().int().positive().optional(),
|
|
18006
|
+
slotDurationUnit: import_zod57.z.enum(CALENDAR_SLOT_UNITS).optional(),
|
|
16951
18007
|
/** Minutes between slot start times (defaults to the slot duration in GHL). */
|
|
16952
|
-
slotInterval:
|
|
18008
|
+
slotInterval: import_zod57.z.number().int().positive().optional(),
|
|
16953
18009
|
/** Minutes of buffer after each appointment. */
|
|
16954
|
-
slotBuffer:
|
|
18010
|
+
slotBuffer: import_zod57.z.number().int().min(0).optional(),
|
|
16955
18011
|
/** v2: the plan users on this calendar (the executor assigns them after the
|
|
16956
18012
|
* users exist). Omitted → the executor's staff handoff applies as before. */
|
|
16957
|
-
teamMemberRefs:
|
|
18013
|
+
teamMemberRefs: import_zod57.z.array(userRefSchema).optional()
|
|
16958
18014
|
});
|
|
16959
|
-
formFieldSchema =
|
|
16960
|
-
|
|
16961
|
-
type:
|
|
16962
|
-
key:
|
|
16963
|
-
required:
|
|
18015
|
+
formFieldSchema = import_zod57.z.discriminatedUnion("type", [
|
|
18016
|
+
import_zod57.z.object({
|
|
18017
|
+
type: import_zod57.z.literal("standard"),
|
|
18018
|
+
key: import_zod57.z.string(),
|
|
18019
|
+
required: import_zod57.z.boolean().optional()
|
|
16964
18020
|
}),
|
|
16965
|
-
|
|
16966
|
-
type:
|
|
18021
|
+
import_zod57.z.object({
|
|
18022
|
+
type: import_zod57.z.literal("custom"),
|
|
16967
18023
|
fieldRef: nsRef("field"),
|
|
16968
|
-
required:
|
|
18024
|
+
required: import_zod57.z.boolean().optional()
|
|
16969
18025
|
})
|
|
16970
18026
|
]);
|
|
16971
|
-
formSchema =
|
|
18027
|
+
formSchema = import_zod57.z.object({
|
|
16972
18028
|
ref: nsRef("form"),
|
|
16973
|
-
name:
|
|
16974
|
-
fields:
|
|
18029
|
+
name: import_zod57.z.string(),
|
|
18030
|
+
fields: import_zod57.z.array(formFieldSchema)
|
|
16975
18031
|
});
|
|
16976
|
-
pageSchema =
|
|
18032
|
+
pageSchema = import_zod57.z.object({
|
|
16977
18033
|
ref: nsRef("page"),
|
|
16978
|
-
name:
|
|
16979
|
-
role:
|
|
16980
|
-
outline:
|
|
18034
|
+
name: import_zod57.z.string(),
|
|
18035
|
+
role: import_zod57.z.string().optional(),
|
|
18036
|
+
outline: import_zod57.z.string().optional(),
|
|
16981
18037
|
formRef: nsRef("form").optional(),
|
|
16982
18038
|
calendarRef: nsRef("calendar").optional()
|
|
16983
18039
|
});
|
|
16984
18040
|
FUNNEL_TARGETS = ["ghl", "external"];
|
|
16985
18041
|
FUNNEL_HOSTS = ["cloudflare", "vercel"];
|
|
16986
|
-
funnelSchema =
|
|
18042
|
+
funnelSchema = import_zod57.z.object({
|
|
16987
18043
|
ref: nsRef("funnel"),
|
|
16988
|
-
name:
|
|
18044
|
+
name: import_zod57.z.string(),
|
|
16989
18045
|
// Where the funnel is built. "ghl" (default) = funnel + named steps in GHL.
|
|
16990
18046
|
// "external" = the subscriber builds + hosts the site themselves (Cloudflare/
|
|
16991
18047
|
// Vercel) and wires its form back to this GHL sub-account (POWER-USER path —
|
|
16992
18048
|
// see blueprint-funnel-targets-spec.md §9). The executor does NOT build or
|
|
16993
18049
|
// deploy an external funnel; it surfaces the GHL-side wiring info.
|
|
16994
|
-
target:
|
|
16995
|
-
host:
|
|
18050
|
+
target: import_zod57.z.enum(FUNNEL_TARGETS).optional(),
|
|
18051
|
+
host: import_zod57.z.enum(FUNNEL_HOSTS).optional(),
|
|
16996
18052
|
// external only
|
|
16997
|
-
domain:
|
|
18053
|
+
domain: import_zod57.z.string().optional(),
|
|
16998
18054
|
// external only
|
|
16999
|
-
pages:
|
|
18055
|
+
pages: import_zod57.z.array(pageSchema)
|
|
17000
18056
|
});
|
|
17001
|
-
emailAssetSchema =
|
|
18057
|
+
emailAssetSchema = import_zod57.z.object({
|
|
17002
18058
|
ref: nsRef("email"),
|
|
17003
|
-
name:
|
|
17004
|
-
subject:
|
|
17005
|
-
bodyOutline:
|
|
17006
|
-
body:
|
|
17007
|
-
mergeTags:
|
|
18059
|
+
name: import_zod57.z.string(),
|
|
18060
|
+
subject: import_zod57.z.string().optional(),
|
|
18061
|
+
bodyOutline: import_zod57.z.string().optional(),
|
|
18062
|
+
body: import_zod57.z.string().optional(),
|
|
18063
|
+
mergeTags: import_zod57.z.array(import_zod57.z.string()).optional()
|
|
17008
18064
|
});
|
|
17009
|
-
smsAssetSchema =
|
|
18065
|
+
smsAssetSchema = import_zod57.z.object({
|
|
17010
18066
|
ref: nsRef("sms"),
|
|
17011
|
-
name:
|
|
17012
|
-
bodyOutline:
|
|
17013
|
-
body:
|
|
17014
|
-
mergeTags:
|
|
18067
|
+
name: import_zod57.z.string(),
|
|
18068
|
+
bodyOutline: import_zod57.z.string().optional(),
|
|
18069
|
+
body: import_zod57.z.string().optional(),
|
|
18070
|
+
mergeTags: import_zod57.z.array(import_zod57.z.string()).optional()
|
|
17015
18071
|
});
|
|
17016
|
-
emailTemplateSchema =
|
|
18072
|
+
emailTemplateSchema = import_zod57.z.object({
|
|
17017
18073
|
ref: nsRef("email_template"),
|
|
17018
|
-
name:
|
|
17019
|
-
subject:
|
|
17020
|
-
html:
|
|
18074
|
+
name: import_zod57.z.string().min(1),
|
|
18075
|
+
subject: import_zod57.z.string().min(1),
|
|
18076
|
+
html: import_zod57.z.string().min(1)
|
|
17021
18077
|
});
|
|
17022
|
-
smsTemplateSchema =
|
|
18078
|
+
smsTemplateSchema = import_zod57.z.object({
|
|
17023
18079
|
ref: nsRef("sms_template"),
|
|
17024
|
-
name:
|
|
17025
|
-
body:
|
|
18080
|
+
name: import_zod57.z.string().min(1),
|
|
18081
|
+
body: import_zod57.z.string().min(1)
|
|
17026
18082
|
});
|
|
17027
|
-
templatesSchema =
|
|
17028
|
-
emails:
|
|
17029
|
-
sms:
|
|
18083
|
+
templatesSchema = import_zod57.z.object({
|
|
18084
|
+
emails: import_zod57.z.array(emailTemplateSchema).optional(),
|
|
18085
|
+
sms: import_zod57.z.array(smsTemplateSchema).optional()
|
|
17030
18086
|
});
|
|
17031
|
-
waitUnit =
|
|
18087
|
+
waitUnit = import_zod57.z.enum(["minutes", "hours", "days"]);
|
|
17032
18088
|
branchActionOptions = [
|
|
17033
|
-
|
|
17034
|
-
|
|
18089
|
+
import_zod57.z.object({ type: import_zod57.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
|
|
18090
|
+
import_zod57.z.object({ type: import_zod57.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
|
|
17035
18091
|
// send_email / send_sms: point at a 5.8 asset (`emailRef`/`smsRef`, the 0.1
|
|
17036
18092
|
// way) or a 5.8a template (`templateRef`, v2). At least one is required —
|
|
17037
18093
|
// enforced by validateBuildPlan so the message reads the same as a dead ref.
|
|
17038
|
-
|
|
17039
|
-
type:
|
|
18094
|
+
import_zod57.z.object({
|
|
18095
|
+
type: import_zod57.z.literal("send_email"),
|
|
17040
18096
|
emailRef: nsRef("email").optional(),
|
|
17041
18097
|
templateRef: nsRef("email_template").optional()
|
|
17042
18098
|
}),
|
|
17043
|
-
|
|
17044
|
-
type:
|
|
18099
|
+
import_zod57.z.object({
|
|
18100
|
+
type: import_zod57.z.literal("send_sms"),
|
|
17045
18101
|
smsRef: nsRef("sms").optional(),
|
|
17046
18102
|
templateRef: nsRef("sms_template").optional()
|
|
17047
18103
|
}),
|
|
17048
|
-
|
|
18104
|
+
import_zod57.z.object({ type: import_zod57.z.literal("wait"), value: import_zod57.z.number().positive(), unit: waitUnit }),
|
|
17049
18105
|
// Appointment-relative wait ("wait until N BEFORE the appointment").
|
|
17050
18106
|
// Only works when the workflow has an appointment in context (i.e. an
|
|
17051
18107
|
// `appointment` trigger) — enforced by validateBuildPlan. Expands to GHL's
|
|
@@ -17054,91 +18110,91 @@ var init_plan = __esm({
|
|
|
17054
18110
|
// (GHL stores whole minutes). Only "before" is emitted today — that's the
|
|
17055
18111
|
// shape we captured + proved; "after" (post-appointment follow-up) is
|
|
17056
18112
|
// deferred until its shape is captured from a real workflow.
|
|
17057
|
-
|
|
17058
|
-
type:
|
|
17059
|
-
value:
|
|
18113
|
+
import_zod57.z.object({
|
|
18114
|
+
type: import_zod57.z.literal("wait_appointment"),
|
|
18115
|
+
value: import_zod57.z.number().int().positive(),
|
|
17060
18116
|
unit: waitUnit
|
|
17061
18117
|
}),
|
|
17062
18118
|
// internal_notification: WHO gets pinged is a plan ref (`userRef`) resolved
|
|
17063
18119
|
// by the executor, or the pending sentinel. `to` (a literal user id or the
|
|
17064
18120
|
// old "assigned_user" hint) is still accepted for 0.1 plans and warned on.
|
|
17065
18121
|
// One of the two is required — enforced by validateBuildPlan.
|
|
17066
|
-
|
|
17067
|
-
type:
|
|
17068
|
-
to:
|
|
18122
|
+
import_zod57.z.object({
|
|
18123
|
+
type: import_zod57.z.literal("internal_notification"),
|
|
18124
|
+
to: import_zod57.z.string().optional(),
|
|
17069
18125
|
userRef: userRefOrPendingSchema.optional(),
|
|
17070
|
-
title:
|
|
17071
|
-
body:
|
|
18126
|
+
title: import_zod57.z.string(),
|
|
18127
|
+
body: import_zod57.z.string()
|
|
17072
18128
|
}),
|
|
17073
|
-
|
|
17074
|
-
type:
|
|
18129
|
+
import_zod57.z.object({
|
|
18130
|
+
type: import_zod57.z.literal("update_contact_field"),
|
|
17075
18131
|
fieldRef: nsRef("field"),
|
|
17076
|
-
value:
|
|
18132
|
+
value: import_zod57.z.string()
|
|
17077
18133
|
}),
|
|
17078
|
-
|
|
17079
|
-
|
|
17080
|
-
type:
|
|
17081
|
-
title:
|
|
17082
|
-
body:
|
|
17083
|
-
dueDate:
|
|
17084
|
-
assignedTo:
|
|
18134
|
+
import_zod57.z.object({ type: import_zod57.z.literal("add_notes"), body: import_zod57.z.string() }),
|
|
18135
|
+
import_zod57.z.object({
|
|
18136
|
+
type: import_zod57.z.literal("task_notification"),
|
|
18137
|
+
title: import_zod57.z.string(),
|
|
18138
|
+
body: import_zod57.z.string().optional(),
|
|
18139
|
+
dueDate: import_zod57.z.string().optional(),
|
|
18140
|
+
assignedTo: import_zod57.z.string().optional(),
|
|
17085
18141
|
/** v2: the plan user the task is assigned to (or the pending sentinel). */
|
|
17086
18142
|
userRef: userRefOrPendingSchema.optional()
|
|
17087
18143
|
}),
|
|
17088
18144
|
// assign_user (v2): GHL "Assign to user" — the contact's owner becomes the
|
|
17089
18145
|
// referenced plan user. Round-robin among several users is a calendar
|
|
17090
18146
|
// concern (teamMemberRefs), not this step's.
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
type:
|
|
18147
|
+
import_zod57.z.object({ type: import_zod57.z.literal("assign_user"), userRef: userRefOrPendingSchema }),
|
|
18148
|
+
import_zod57.z.object({ type: import_zod57.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
|
|
18149
|
+
import_zod57.z.object({ type: import_zod57.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
|
|
18150
|
+
import_zod57.z.object({
|
|
18151
|
+
type: import_zod57.z.literal("create_opportunity"),
|
|
17096
18152
|
pipelineRef: nsRef("pipeline"),
|
|
17097
18153
|
stageRef: nsRef("stage"),
|
|
17098
18154
|
// Opportunity name (merge fields allowed). Defaults to the contact's name.
|
|
17099
18155
|
// Required by GHL's create node; without it the create silently no-ops.
|
|
17100
|
-
name:
|
|
18156
|
+
name: import_zod57.z.string().optional(),
|
|
17101
18157
|
// Opportunity monetary value (the deal/sale dollar amount). A string so it
|
|
17102
18158
|
// can be a literal ("2500") OR a merge field ("{{contact.package_value}}").
|
|
17103
18159
|
// Optional — omitted → GHL leaves the value unset. Shape captured from Lux
|
|
17104
18160
|
// Bio "14. Package Sale". Lux models the lifecycle by pipeline STAGE, not GHL
|
|
17105
18161
|
// won/lost status, so a "won" opp = move to the closing stage WITH this value.
|
|
17106
|
-
value:
|
|
18162
|
+
value: import_zod57.z.string().optional()
|
|
17107
18163
|
}),
|
|
17108
|
-
|
|
17109
|
-
type:
|
|
18164
|
+
import_zod57.z.object({
|
|
18165
|
+
type: import_zod57.z.literal("update_opportunity"),
|
|
17110
18166
|
pipelineRef: nsRef("pipeline"),
|
|
17111
18167
|
stageRef: nsRef("stage"),
|
|
17112
18168
|
// Opportunity monetary value (see create_opportunity.value). Optional.
|
|
17113
|
-
value:
|
|
18169
|
+
value: import_zod57.z.string().optional()
|
|
17114
18170
|
}),
|
|
17115
|
-
|
|
17116
|
-
type:
|
|
17117
|
-
goalCondition:
|
|
18171
|
+
import_zod57.z.object({
|
|
18172
|
+
type: import_zod57.z.literal("goal_event"),
|
|
18173
|
+
goalCondition: import_zod57.z.string(),
|
|
17118
18174
|
// GHL's GoalAction enum (extracted 2026-05-18): continue | wait | exit.
|
|
17119
|
-
action:
|
|
18175
|
+
action: import_zod57.z.enum(["exit", "continue", "wait"]).optional()
|
|
17120
18176
|
})
|
|
17121
18177
|
];
|
|
17122
|
-
branchActionSchema =
|
|
17123
|
-
findOpportunitySchema =
|
|
17124
|
-
type:
|
|
18178
|
+
branchActionSchema = import_zod57.z.discriminatedUnion("type", branchActionOptions);
|
|
18179
|
+
findOpportunitySchema = import_zod57.z.object({
|
|
18180
|
+
type: import_zod57.z.literal("find_opportunity"),
|
|
17125
18181
|
pipelineRef: nsRef("pipeline"),
|
|
17126
|
-
found:
|
|
17127
|
-
notFound:
|
|
18182
|
+
found: import_zod57.z.array(branchActionSchema).default([]),
|
|
18183
|
+
notFound: import_zod57.z.array(branchActionSchema).default([])
|
|
17128
18184
|
});
|
|
17129
|
-
actionSchema =
|
|
18185
|
+
actionSchema = import_zod57.z.discriminatedUnion("type", [...branchActionOptions, findOpportunitySchema]);
|
|
17130
18186
|
APPOINTMENT_STATUSES = ["new", "confirmed", "showed", "noshow", "cancelled", "invalid"];
|
|
17131
18187
|
CALL_STATUSES = ["busy", "canceled", "voicemail", "no-answer", "completed"];
|
|
17132
18188
|
NUMBER_VALIDATION_STATES = ["not_valid", "sms_incapable"];
|
|
17133
|
-
triggerSchema =
|
|
17134
|
-
type:
|
|
18189
|
+
triggerSchema = import_zod57.z.object({
|
|
18190
|
+
type: import_zod57.z.string(),
|
|
17135
18191
|
formRef: nsRef("form").optional(),
|
|
17136
18192
|
tagRef: nsRef("tag").optional(),
|
|
17137
18193
|
calendarRef: nsRef("calendar").optional(),
|
|
17138
18194
|
pipelineRef: nsRef("pipeline").optional(),
|
|
17139
18195
|
stageRef: nsRef("stage").optional(),
|
|
17140
18196
|
// Required for a native `appointment` trigger (the status it fires on).
|
|
17141
|
-
appointmentStatus:
|
|
18197
|
+
appointmentStatus: import_zod57.z.enum(APPOINTMENT_STATUSES).optional(),
|
|
17142
18198
|
// ── customer_reply scoping ──────────────────────────────────────
|
|
17143
18199
|
// A bare customer_reply trigger fires on EVERY inbound reply from EVERY
|
|
17144
18200
|
// contact. That is almost never what a plan means: "alert the owner when a
|
|
@@ -17152,27 +18208,27 @@ var init_plan = __esm({
|
|
|
17152
18208
|
// Both optional and additive — omitting them keeps the long-standing
|
|
17153
18209
|
// fires-on-any-reply baseline.
|
|
17154
18210
|
hasTagRef: nsRef("tag").optional(),
|
|
17155
|
-
replyIntent:
|
|
18211
|
+
replyIntent: import_zod57.z.enum(["positive", "negative"]).optional(),
|
|
17156
18212
|
// ── call_status scoping (missed-call text-back) ─────────────────
|
|
17157
18213
|
// GHL's call_status trigger fires on a completed call attempt. The states it
|
|
17158
18214
|
// exposes are exactly what a "missed call" means operationally. Required for
|
|
17159
18215
|
// a call_status trigger — without them the trigger would fire on EVERY call
|
|
17160
18216
|
// including answered ones, so the executor refuses rather than guess.
|
|
17161
|
-
callStatuses:
|
|
17162
|
-
callDirection:
|
|
18217
|
+
callStatuses: import_zod57.z.array(import_zod57.z.enum(CALL_STATUSES)).optional(),
|
|
18218
|
+
callDirection: import_zod57.z.enum(["inbound", "outbound"]).optional(),
|
|
17163
18219
|
// ── validation_error (GHL UI: "Number validation") ──────────────
|
|
17164
18220
|
// Fires after a phone number passes or fails a validation check. The UI shows
|
|
17165
18221
|
// "Not valid" / "SMS incapable"; the WIRE values are snake_case. Required for
|
|
17166
18222
|
// a validation_error trigger — without them the executor refuses rather than
|
|
17167
18223
|
// guess which failure states to fire on.
|
|
17168
|
-
numberValidation:
|
|
18224
|
+
numberValidation: import_zod57.z.array(import_zod57.z.enum(NUMBER_VALIDATION_STATES)).optional()
|
|
17169
18225
|
});
|
|
17170
|
-
workflowSchema =
|
|
18226
|
+
workflowSchema = import_zod57.z.object({
|
|
17171
18227
|
ref: nsRef("workflow"),
|
|
17172
|
-
name:
|
|
18228
|
+
name: import_zod57.z.string(),
|
|
17173
18229
|
trigger: triggerSchema.optional(),
|
|
17174
|
-
stopOnResponse:
|
|
17175
|
-
actions:
|
|
18230
|
+
stopOnResponse: import_zod57.z.boolean().optional(),
|
|
18231
|
+
actions: import_zod57.z.array(actionSchema).max(40)
|
|
17176
18232
|
// house rule: <=40 actions/workflow
|
|
17177
18233
|
});
|
|
17178
18234
|
HANDOFF_OWNER_LEGACY = {
|
|
@@ -17180,37 +18236,37 @@ var init_plan = __esm({
|
|
|
17180
18236
|
"JERRY-EXT": "OPERATOR-EXT",
|
|
17181
18237
|
"SASHA": "TEAM"
|
|
17182
18238
|
};
|
|
17183
|
-
handoffSchema =
|
|
18239
|
+
handoffSchema = import_zod57.z.object({
|
|
17184
18240
|
ref: nsRef("handoff"),
|
|
17185
|
-
owner:
|
|
17186
|
-
title:
|
|
17187
|
-
trigger:
|
|
17188
|
-
instruction:
|
|
18241
|
+
owner: import_zod57.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
|
|
18242
|
+
title: import_zod57.z.string(),
|
|
18243
|
+
trigger: import_zod57.z.string().optional(),
|
|
18244
|
+
instruction: import_zod57.z.string(),
|
|
17189
18245
|
produces: refSchema.nullable().optional(),
|
|
17190
|
-
successCheck:
|
|
17191
|
-
blocks:
|
|
18246
|
+
successCheck: import_zod57.z.string(),
|
|
18247
|
+
blocks: import_zod57.z.array(import_zod57.z.string()).optional()
|
|
17192
18248
|
});
|
|
17193
|
-
buildPlanSchema =
|
|
17194
|
-
schemaVersion:
|
|
17195
|
-
planId:
|
|
17196
|
-
briefId:
|
|
17197
|
-
preset:
|
|
17198
|
-
summary:
|
|
17199
|
-
users:
|
|
17200
|
-
pipelines:
|
|
17201
|
-
customFields:
|
|
17202
|
-
tags:
|
|
17203
|
-
customValues:
|
|
17204
|
-
calendars:
|
|
17205
|
-
forms:
|
|
17206
|
-
funnels:
|
|
17207
|
-
emails:
|
|
17208
|
-
sms:
|
|
18249
|
+
buildPlanSchema = import_zod57.z.object({
|
|
18250
|
+
schemaVersion: import_zod57.z.string(),
|
|
18251
|
+
planId: import_zod57.z.string(),
|
|
18252
|
+
briefId: import_zod57.z.string(),
|
|
18253
|
+
preset: import_zod57.z.string(),
|
|
18254
|
+
summary: import_zod57.z.string().optional(),
|
|
18255
|
+
users: import_zod57.z.array(userSchema).optional(),
|
|
18256
|
+
pipelines: import_zod57.z.array(pipelineSchema).optional(),
|
|
18257
|
+
customFields: import_zod57.z.array(customFieldSchema).optional(),
|
|
18258
|
+
tags: import_zod57.z.array(tagSchema).optional(),
|
|
18259
|
+
customValues: import_zod57.z.array(customValueSchema).optional(),
|
|
18260
|
+
calendars: import_zod57.z.array(calendarSchema).optional(),
|
|
18261
|
+
forms: import_zod57.z.array(formSchema).optional(),
|
|
18262
|
+
funnels: import_zod57.z.array(funnelSchema).optional(),
|
|
18263
|
+
emails: import_zod57.z.array(emailAssetSchema).optional(),
|
|
18264
|
+
sms: import_zod57.z.array(smsAssetSchema).optional(),
|
|
17209
18265
|
templates: templatesSchema.optional(),
|
|
17210
|
-
workflows:
|
|
17211
|
-
handoffs:
|
|
17212
|
-
buildOrder:
|
|
17213
|
-
idMap:
|
|
18266
|
+
workflows: import_zod57.z.array(workflowSchema).optional(),
|
|
18267
|
+
handoffs: import_zod57.z.array(handoffSchema).optional(),
|
|
18268
|
+
buildOrder: import_zod57.z.array(import_zod57.z.string()).optional(),
|
|
18269
|
+
idMap: import_zod57.z.record(import_zod57.z.string()).optional()
|
|
17214
18270
|
}).strict();
|
|
17215
18271
|
PLAN_ERROR_CODES = {
|
|
17216
18272
|
/** A workflow named like /nurture/i whose waits add up to < 30 days. */
|
|
@@ -18320,7 +19376,7 @@ function groupCalendarPending(entries) {
|
|
|
18320
19376
|
}
|
|
18321
19377
|
return [...byRef.values()];
|
|
18322
19378
|
}
|
|
18323
|
-
function
|
|
19379
|
+
function renderReport2(plan, result, ctx) {
|
|
18324
19380
|
const L = [];
|
|
18325
19381
|
const { summary } = result;
|
|
18326
19382
|
L.push(`Blueprint build ${ctx.mode === "dry_run" ? "PREVIEW (dry run \u2014 no changes written)" : "REPORT"}`);
|
|
@@ -19351,23 +20407,23 @@ function friendlyDate(stamp) {
|
|
|
19351
20407
|
if (isNaN(d.getTime())) return stamp;
|
|
19352
20408
|
return d.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", timeZone: "UTC" });
|
|
19353
20409
|
}
|
|
19354
|
-
var
|
|
20410
|
+
var import_zod58, RunnerCheckSchema, RunnerResultSchema;
|
|
19355
20411
|
var init_handoff_pack = __esm({
|
|
19356
20412
|
"src/command-os/handoff-pack.ts"() {
|
|
19357
20413
|
"use strict";
|
|
19358
|
-
|
|
19359
|
-
RunnerCheckSchema =
|
|
19360
|
-
name:
|
|
19361
|
-
state:
|
|
19362
|
-
evidence:
|
|
19363
|
-
reason:
|
|
20414
|
+
import_zod58 = require("zod");
|
|
20415
|
+
RunnerCheckSchema = import_zod58.z.object({
|
|
20416
|
+
name: import_zod58.z.string().min(1),
|
|
20417
|
+
state: import_zod58.z.enum(["CHECKED", "DONE_UNCHECKED"]),
|
|
20418
|
+
evidence: import_zod58.z.string().optional(),
|
|
20419
|
+
reason: import_zod58.z.string().optional(),
|
|
19364
20420
|
/** One sentence a client can read: what is in place, or what is missing and why. No tool names, no ids. */
|
|
19365
|
-
plain:
|
|
20421
|
+
plain: import_zod58.z.string().optional()
|
|
19366
20422
|
}).strict();
|
|
19367
|
-
RunnerResultSchema =
|
|
19368
|
-
ok:
|
|
19369
|
-
checks:
|
|
19370
|
-
error:
|
|
20423
|
+
RunnerResultSchema = import_zod58.z.object({
|
|
20424
|
+
ok: import_zod58.z.boolean(),
|
|
20425
|
+
checks: import_zod58.z.array(RunnerCheckSchema).default([]),
|
|
20426
|
+
error: import_zod58.z.string().optional()
|
|
19371
20427
|
}).strict();
|
|
19372
20428
|
}
|
|
19373
20429
|
});
|
|
@@ -19816,9 +20872,9 @@ function resolveClient(who, roster) {
|
|
|
19816
20872
|
const byId2 = roster.find((r) => r.locationId === q2);
|
|
19817
20873
|
if (byId2) return { locationId: byId2.locationId, name: byId2.name, onTheBoard: true };
|
|
19818
20874
|
const hits = roster.filter((r) => r.name?.trim().toLowerCase() === q2.toLowerCase());
|
|
19819
|
-
const
|
|
19820
|
-
if (
|
|
19821
|
-
if (
|
|
20875
|
+
const loose2 = hits.length ? hits : roster.filter((r) => r.name?.toLowerCase().includes(q2.toLowerCase()));
|
|
20876
|
+
if (loose2.length === 1) return { locationId: loose2[0].locationId, name: loose2[0].name, onTheBoard: true };
|
|
20877
|
+
if (loose2.length > 1) return { error: `More than one client matches "${q2}": ${loose2.map((r) => r.name).join(", ")}. Use the full name.` };
|
|
19822
20878
|
return { error: `No client called "${q2}" is on your board or in your records.` };
|
|
19823
20879
|
}
|
|
19824
20880
|
function line(locationId2, e, roster, profile) {
|
|
@@ -19838,26 +20894,26 @@ function registerClientEngagementTools(server2, registry2) {
|
|
|
19838
20894
|
server2,
|
|
19839
20895
|
"get_client_engagements",
|
|
19840
20896
|
"Read what each of YOUR clients bought \u2014 the offer they are on, what they pay, monthly or one-time, when it started, when it renews, who runs the account, and any SOPs specific to them. Use this before quoting, renewing, writing a proposal or planning a client's week, instead of asking the user again. Also returns your recurring revenue as a FLOOR (the sum of active monthly clients that have a price recorded) plus a count of the ones that do not, because a total assembled from partial data is not a total. Pass locationId for one client, or nothing for all of them. The locationId in the reply is for calling set_client_engagement \u2014 refer to clients by name when you answer the user, never by id. Local file on this machine; never touches GoHighLevel.",
|
|
19841
|
-
{ locationId:
|
|
20897
|
+
{ locationId: import_zod59.z.string().optional().describe("One client's sub-account id. Omit for the whole roster.") },
|
|
19842
20898
|
async ({ locationId: locationId2 }) => {
|
|
19843
20899
|
const profile = readAgencyProfile();
|
|
19844
20900
|
const saved = readClientEngagements();
|
|
19845
20901
|
const roster = knownClients(registry2, readBoardState());
|
|
19846
20902
|
const entries = Object.entries(saved.clients).filter(([id]) => !locationId2 || id === locationId2);
|
|
19847
20903
|
const clients = entries.map(([id, e]) => line(id, e, roster, profile));
|
|
19848
|
-
const
|
|
20904
|
+
const money5 = monthlyFloor(saved, profile);
|
|
19849
20905
|
const noRecord = roster.filter((r) => !saved.clients[r.locationId]).map((r) => r.name).filter(Boolean);
|
|
19850
|
-
const missing =
|
|
19851
|
-
const basis =
|
|
20906
|
+
const missing = money5.unpriced ? ` ${money5.unpriced} active monthly client${money5.unpriced === 1 ? " has" : "s have"} no price on file and ${money5.unpriced === 1 ? "is" : "are"} not included.` : "";
|
|
20907
|
+
const basis = money5.byCurrency ? `No single total exists: these clients are billed in ${money5.byCurrency.length} different currencies, so they are listed separately rather than added together.${missing}` : money5.unpriced ? `A floor, not a total: ${money5.counted} monthly client${money5.counted === 1 ? "" : "s"} with a price on file.${missing}` : `${money5.counted} active monthly client${money5.counted === 1 ? "" : "s"}, all with a price on file.`;
|
|
19852
20908
|
return {
|
|
19853
20909
|
clients,
|
|
19854
20910
|
recurring: {
|
|
19855
|
-
amount:
|
|
19856
|
-
currency:
|
|
19857
|
-
byCurrency:
|
|
20911
|
+
amount: money5.amount,
|
|
20912
|
+
currency: money5.currency,
|
|
20913
|
+
byCurrency: money5.byCurrency,
|
|
19858
20914
|
basis,
|
|
19859
|
-
clientsCounted:
|
|
19860
|
-
clientsWithoutAPrice:
|
|
20915
|
+
clientsCounted: money5.counted,
|
|
20916
|
+
clientsWithoutAPrice: money5.unpriced
|
|
19861
20917
|
},
|
|
19862
20918
|
...locationId2 ? {} : { onYourBoardWithNothingRecorded: noRecord }
|
|
19863
20919
|
};
|
|
@@ -19868,24 +20924,24 @@ function registerClientEngagementTools(server2, registry2) {
|
|
|
19868
20924
|
"set_client_engagement",
|
|
19869
20925
|
"Record or update what ONE client bought \u2014 offer, price, monthly or one-time, start and renewal dates, status, who runs the account, and SOPs specific to them. Send only the fields you are changing; anything you omit is left exactly as it was, so this is safe to call repeatedly. To CLEAR a field pass null for it, and to remove the client's record entirely pass remove:true. Dates are YYYY-MM-DD. Local file on this machine; never touches GoHighLevel.",
|
|
19870
20926
|
{
|
|
19871
|
-
locationId:
|
|
20927
|
+
locationId: import_zod59.z.string().describe("The client's GHL sub-account id \u2014 the same id the board uses."),
|
|
19872
20928
|
// Every editable field is NULLABLE, not just optional. The description
|
|
19873
20929
|
// promises "pass null to clear", and a schema that rejects null would make
|
|
19874
20930
|
// that promise a lie the tests never caught, because they cleared fields by
|
|
19875
20931
|
// calling the store directly instead of through this surface (Codex,
|
|
19876
20932
|
// 2026-08-28). Absent = untouched; null = clear; a value = set.
|
|
19877
|
-
clientName:
|
|
19878
|
-
offer:
|
|
19879
|
-
price:
|
|
19880
|
-
cadence:
|
|
19881
|
-
currency:
|
|
19882
|
-
startedOn:
|
|
19883
|
-
renewsOn:
|
|
19884
|
-
status:
|
|
19885
|
-
owner:
|
|
19886
|
-
sops:
|
|
19887
|
-
notes:
|
|
19888
|
-
remove:
|
|
20933
|
+
clientName: import_zod59.z.string().nullable().optional().describe("How you refer to them, if it differs from the account name."),
|
|
20934
|
+
offer: import_zod59.z.string().nullable().optional().describe("Which of your offers they are on. Matched against your agency profile so a typo is reported."),
|
|
20935
|
+
price: import_zod59.z.number().min(0).nullable().optional().describe("What they actually pay \u2014 not your floor."),
|
|
20936
|
+
cadence: import_zod59.z.enum(["one-time", "monthly"]).nullable().optional(),
|
|
20937
|
+
currency: import_zod59.z.string().nullable().optional(),
|
|
20938
|
+
startedOn: import_zod59.z.string().nullable().optional().describe("YYYY-MM-DD."),
|
|
20939
|
+
renewsOn: import_zod59.z.string().nullable().optional().describe("YYYY-MM-DD."),
|
|
20940
|
+
status: import_zod59.z.enum(["active", "paused", "ended"]).nullable().optional(),
|
|
20941
|
+
owner: import_zod59.z.string().nullable().optional().describe("Which staff member runs this account."),
|
|
20942
|
+
sops: import_zod59.z.array(import_zod59.z.string()).nullable().optional().describe("SOPs that apply to this client only. Leave empty to use your agency's."),
|
|
20943
|
+
notes: import_zod59.z.string().nullable().optional(),
|
|
20944
|
+
remove: import_zod59.z.boolean().optional().describe("Delete this client's record entirely.")
|
|
19889
20945
|
},
|
|
19890
20946
|
async (args) => {
|
|
19891
20947
|
const { locationId: locationId2, remove, ...rest } = args;
|
|
@@ -19909,7 +20965,7 @@ function registerClientEngagementTools(server2, registry2) {
|
|
|
19909
20965
|
server2,
|
|
19910
20966
|
"client_this_week",
|
|
19911
20967
|
"Answer 'what should I be doing for this client this week' for ONE client, from what you already know: what they bought, where their build has stopped, what is waiting on you versus on Command OS, a renewal that is close, whether the price is under your own floor, and the SOP that applies. Names the client by name or by account id. Everything it cannot know is listed plainly rather than guessed. Local files on this machine; never touches GoHighLevel.",
|
|
19912
|
-
{ client:
|
|
20968
|
+
{ client: import_zod59.z.string().describe("The client's name as you refer to them, or their sub-account id.") },
|
|
19913
20969
|
async ({ client }) => {
|
|
19914
20970
|
const state = readBoardState();
|
|
19915
20971
|
const roster = knownClients(registry2, state);
|
|
@@ -19929,11 +20985,11 @@ function registerClientEngagementTools(server2, registry2) {
|
|
|
19929
20985
|
}
|
|
19930
20986
|
);
|
|
19931
20987
|
}
|
|
19932
|
-
var
|
|
20988
|
+
var import_zod59;
|
|
19933
20989
|
var init_client_engagements2 = __esm({
|
|
19934
20990
|
"src/tools/client-engagements.ts"() {
|
|
19935
20991
|
"use strict";
|
|
19936
|
-
|
|
20992
|
+
import_zod59 = require("zod");
|
|
19937
20993
|
init_tool_helpers();
|
|
19938
20994
|
init_agency_profile();
|
|
19939
20995
|
init_client_engagements();
|
|
@@ -19999,9 +21055,9 @@ function looksLikeRawId(word) {
|
|
|
19999
21055
|
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;
|
|
20000
21056
|
if (/^[0-9a-f]{24}$/.test(word)) return true;
|
|
20001
21057
|
if (!/^[A-Za-z0-9]{17,}$/.test(word)) return false;
|
|
20002
|
-
const
|
|
21058
|
+
const digits2 = (word.match(/\d/g) ?? []).length;
|
|
20003
21059
|
const letters = (word.match(/[A-Za-z]/g) ?? []).length;
|
|
20004
|
-
if (
|
|
21060
|
+
if (digits2 < 2 || letters < 2) return false;
|
|
20005
21061
|
const longestLetterRun = Math.max(0, ...(word.match(/[a-z]+|[A-Z]+/g) ?? []).map((r) => r.length));
|
|
20006
21062
|
return longestLetterRun <= 3;
|
|
20007
21063
|
}
|
|
@@ -20009,8 +21065,8 @@ function rankFindings(findings) {
|
|
|
20009
21065
|
return [...findings].sort((a, b) => {
|
|
20010
21066
|
const s = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
20011
21067
|
if (s !== 0) return s;
|
|
20012
|
-
const
|
|
20013
|
-
if (
|
|
21068
|
+
const money5 = (b.money.dollarsPerMonth ?? -1) - (a.money.dollarsPerMonth ?? -1);
|
|
21069
|
+
if (money5 !== 0) return money5;
|
|
20014
21070
|
const leads = (b.money.leadsPerMonth ?? -1) - (a.money.leadsPerMonth ?? -1);
|
|
20015
21071
|
if (leads !== 0) return leads;
|
|
20016
21072
|
const effort = (a.minutes ?? Number.MAX_SAFE_INTEGER) - (b.minutes ?? Number.MAX_SAFE_INTEGER);
|
|
@@ -20393,14 +21449,14 @@ function registerAuditReportTools(server2) {
|
|
|
20393
21449
|
"build_audit_report",
|
|
20394
21450
|
"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.",
|
|
20395
21451
|
{
|
|
20396
|
-
audit:
|
|
20397
|
-
audience:
|
|
20398
|
-
accountName:
|
|
20399
|
-
locationId:
|
|
20400
|
-
leadsPerMonth:
|
|
20401
|
-
closeRate:
|
|
20402
|
-
monthlyValuePerClient:
|
|
20403
|
-
generatedAt:
|
|
21452
|
+
audit: import_zod60.z.record(import_zod60.z.unknown()).describe("The full JSON result returned by audit_workflows."),
|
|
21453
|
+
audience: import_zod60.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."),
|
|
21454
|
+
accountName: import_zod60.z.string().describe("The client's name as they should see it on the document."),
|
|
21455
|
+
locationId: import_zod60.z.string().optional().describe("Sub-account id. Recorded on the document, never shown in the body."),
|
|
21456
|
+
leadsPerMonth: import_zod60.z.number().min(0).optional().describe("Leads this account actually receives per month, if known. Without it, findings are not costed in dollars."),
|
|
21457
|
+
closeRate: import_zod60.z.number().min(0).max(1).optional().describe("Share of reached leads that become clients, 0-1. Never guessed."),
|
|
21458
|
+
monthlyValuePerClient: import_zod60.z.number().min(0).optional().describe("What one new client is worth per month. Defaults to your agency profile's minimum monthly fee."),
|
|
21459
|
+
generatedAt: import_zod60.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
|
|
20404
21460
|
},
|
|
20405
21461
|
async (args) => {
|
|
20406
21462
|
const a = args;
|
|
@@ -20458,12 +21514,12 @@ function registerAuditReportTools(server2) {
|
|
|
20458
21514
|
"build_site_audit_report",
|
|
20459
21515
|
"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.",
|
|
20460
21516
|
{
|
|
20461
|
-
observations:
|
|
20462
|
-
businessName:
|
|
20463
|
-
leadsPerMonth:
|
|
20464
|
-
closeRate:
|
|
20465
|
-
monthlyValuePerClient:
|
|
20466
|
-
generatedAt:
|
|
21517
|
+
observations: import_zod60.z.record(import_zod60.z.unknown()).describe("The verified observations payload from the site read: site, pagesRead, observations, limits. Passed through as-is."),
|
|
21518
|
+
businessName: import_zod60.z.string().describe("The business's name as it should appear on the document."),
|
|
21519
|
+
leadsPerMonth: import_zod60.z.number().min(0).optional().describe("THEIR leads per month, if they told you. Never your own figure."),
|
|
21520
|
+
closeRate: import_zod60.z.number().min(0).max(1).optional().describe("THEIR close rate, 0-1, if they told you. Never guessed."),
|
|
21521
|
+
monthlyValuePerClient: import_zod60.z.number().min(0).optional().describe("What one client is worth to THEM per month, if they told you."),
|
|
21522
|
+
generatedAt: import_zod60.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
|
|
20467
21523
|
},
|
|
20468
21524
|
async (args) => {
|
|
20469
21525
|
const a = args;
|
|
@@ -20522,11 +21578,11 @@ function registerAuditReportTools(server2) {
|
|
|
20522
21578
|
}
|
|
20523
21579
|
);
|
|
20524
21580
|
}
|
|
20525
|
-
var
|
|
21581
|
+
var import_zod60;
|
|
20526
21582
|
var init_audit_report2 = __esm({
|
|
20527
21583
|
"src/tools/audit-report.ts"() {
|
|
20528
21584
|
"use strict";
|
|
20529
|
-
|
|
21585
|
+
import_zod60 = require("zod");
|
|
20530
21586
|
init_tool_helpers();
|
|
20531
21587
|
init_audit_report();
|
|
20532
21588
|
init_audit_translate();
|
|
@@ -20543,12 +21599,12 @@ function daysUntil2(then, now) {
|
|
|
20543
21599
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(then);
|
|
20544
21600
|
if (!m) return void 0;
|
|
20545
21601
|
const target = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
20546
|
-
const
|
|
20547
|
-
return Math.round((target -
|
|
21602
|
+
const today2 = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
|
|
21603
|
+
return Math.round((target - today2) / 864e5);
|
|
20548
21604
|
}
|
|
20549
21605
|
function dailyBrief(input) {
|
|
20550
21606
|
const { now, queue, profile, engagements, roster, live } = input;
|
|
20551
|
-
const
|
|
21607
|
+
const today2 = [...live?.appointmentsToday ?? []].sort((a, b) => {
|
|
20552
21608
|
if (a.startsAt && b.startsAt) return a.startsAt.localeCompare(b.startsAt);
|
|
20553
21609
|
if (a.startsAt) return -1;
|
|
20554
21610
|
if (b.startsAt) return 1;
|
|
@@ -20599,12 +21655,12 @@ function dailyBrief(input) {
|
|
|
20599
21655
|
notChecked.push("Only the accounts you have connected are looked at. Anything outside GoHighLevel \u2014 email, calls, invoices, ads \u2014 is not in this brief.");
|
|
20600
21656
|
const unread = live?.unreachable ?? [];
|
|
20601
21657
|
const halfRead = live?.partial ?? [];
|
|
20602
|
-
const quiet = !unread.length && !halfRead.length && !
|
|
21658
|
+
const quiet = !unread.length && !halfRead.length && !today2.length && !waitingOnYou.length && !runningNow.length && !neverStarted.length && !renewals.length && !underFloor.length && !newLeads.length;
|
|
20603
21659
|
let headline3;
|
|
20604
|
-
if (
|
|
20605
|
-
headline3 = `You have one call today \u2014 ${
|
|
20606
|
-
} else if (
|
|
20607
|
-
headline3 =
|
|
21660
|
+
if (today2.length === 1) {
|
|
21661
|
+
headline3 = `You have one call today \u2014 ${today2[0].who ?? today2[0].client} at ${today2[0].at}.`;
|
|
21662
|
+
} else if (today2.length > 1) {
|
|
21663
|
+
headline3 = today2[0].startsAt ? `You have ${today2.length} calls today, the first at ${today2[0].at}.` : `You have ${today2.length} calls today.`;
|
|
20608
21664
|
} else if (waitingOnYou.length) {
|
|
20609
21665
|
headline3 = `${waitingOnYou.length} thing${waitingOnYou.length === 1 ? "" : "s"} waiting on you. First: ${waitingOnYou[0].title}`;
|
|
20610
21666
|
} else if (renewals.length) {
|
|
@@ -20626,7 +21682,7 @@ function dailyBrief(input) {
|
|
|
20626
21682
|
} else {
|
|
20627
21683
|
headline3 = "Nothing needs you this morning.";
|
|
20628
21684
|
}
|
|
20629
|
-
return { date: localDate(now), headline: headline3, today, waitingOnYou, runningNow, neverStarted, renewals, underFloor, newLeads, notChecked, quiet };
|
|
21685
|
+
return { date: localDate(now), headline: headline3, today: today2, waitingOnYou, runningNow, neverStarted, renewals, underFloor, newLeads, notChecked, quiet };
|
|
20630
21686
|
}
|
|
20631
21687
|
var nameFor;
|
|
20632
21688
|
var init_daily_brief = __esm({
|
|
@@ -20716,13 +21772,13 @@ function describeAppointment(raw, account2, operatorTimezone2) {
|
|
|
20716
21772
|
return appointment;
|
|
20717
21773
|
}
|
|
20718
21774
|
function todaysAppointments(raws, account2, now, operatorTimezone2) {
|
|
20719
|
-
const
|
|
21775
|
+
const today2 = dateIn(now, operatorTimezone2);
|
|
20720
21776
|
const out = [];
|
|
20721
21777
|
for (const raw of raws) {
|
|
20722
21778
|
if (!appointmentIsOn(raw.appointmentStatus)) continue;
|
|
20723
21779
|
const when = parseWhen(raw.startTime);
|
|
20724
21780
|
if (!when) continue;
|
|
20725
|
-
if (when.kind === "instant" && dateIn(when.at, operatorTimezone2) !==
|
|
21781
|
+
if (when.kind === "instant" && dateIn(when.at, operatorTimezone2) !== today2) continue;
|
|
20726
21782
|
const described = describeAppointment(raw, account2, operatorTimezone2);
|
|
20727
21783
|
if (described) out.push(described);
|
|
20728
21784
|
}
|
|
@@ -20894,8 +21950,8 @@ function registerDailyBriefTools(server2, registry2) {
|
|
|
20894
21950
|
"daily_brief",
|
|
20895
21951
|
"The morning brief across ALL your clients: today's appointments first, then what is waiting on YOU, what Command OS is running, renewals inside 30 days, clients under your own price floor, and new leads overnight. Use it to answer 'what do I need to do today' or 'anything I'm missing' without opening a single sub-account. Reads today's calendar and new-contact counts live from each connected account; pass live=false for records only. Accounts that could not be read are named with the reason \u2014 it never reports an account as quiet when it simply could not get in.",
|
|
20896
21952
|
{
|
|
20897
|
-
live:
|
|
20898
|
-
hoursBack:
|
|
21953
|
+
live: import_zod61.z.boolean().optional().describe("Read today's calendars and lead counts from the connected accounts. Default true. false answers from local records alone."),
|
|
21954
|
+
hoursBack: import_zod61.z.number().min(1).max(72).optional().describe("Count new leads over this many hours instead of since midnight.")
|
|
20899
21955
|
},
|
|
20900
21956
|
async ({ live, hoursBack }) => {
|
|
20901
21957
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -20929,11 +21985,11 @@ function registerDailyBriefTools(server2, registry2) {
|
|
|
20929
21985
|
}
|
|
20930
21986
|
);
|
|
20931
21987
|
}
|
|
20932
|
-
var
|
|
21988
|
+
var import_zod61;
|
|
20933
21989
|
var init_daily_brief2 = __esm({
|
|
20934
21990
|
"src/tools/daily-brief.ts"() {
|
|
20935
21991
|
"use strict";
|
|
20936
|
-
|
|
21992
|
+
import_zod61 = require("zod");
|
|
20937
21993
|
init_tool_helpers();
|
|
20938
21994
|
init_ghl_client();
|
|
20939
21995
|
init_agency_profile();
|
|
@@ -20986,7 +22042,7 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
20986
22042
|
"list_snapshots",
|
|
20987
22043
|
"List the agency's GHL snapshots (id, name, type) so you can pick the right one by name before applying it to a sub-account. Reads the agency/company-scoped key (not a sub-account PIT). companyId defaults to the active location's company; pass it explicitly to target a different company you have the agency key for. Read-only.",
|
|
20988
22044
|
{
|
|
20989
|
-
companyId:
|
|
22045
|
+
companyId: import_zod62.z.string().optional().describe(
|
|
20990
22046
|
"Agency/company ID whose snapshots to list. Defaults to the active location's company. Must match the company your agency key is scoped to."
|
|
20991
22047
|
)
|
|
20992
22048
|
},
|
|
@@ -21011,11 +22067,11 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
21011
22067
|
"create_snapshot_share_link",
|
|
21012
22068
|
"Create a shareable load link for one of the agency's snapshots (returns a gohighlevel.com/?share=... URL to import the snapshot into a sub-account). Uses the agency/company-scoped key. WARNING: this is NOT idempotent \u2014 each call mints a NEW link, and there is no API to list or revoke links (revoke in the GHL UI under the snapshot's share settings). Pick share_type deliberately. Use list_snapshots first to get the snapshot id.",
|
|
21013
22069
|
{
|
|
21014
|
-
snapshot_id:
|
|
21015
|
-
share_type:
|
|
22070
|
+
snapshot_id: import_zod62.z.string().describe("The snapshot id to share (from list_snapshots)."),
|
|
22071
|
+
share_type: import_zod62.z.enum(SHARE_TYPES).describe(
|
|
21016
22072
|
"Share link type. 'link' = standard share link; 'permanent_link' = non-expiring; 'agency_link' = share to agencies; 'location_link' = load into a sub-account/location; 'marketplace_link' = marketplace listing. No default \u2014 choose intentionally."
|
|
21017
22073
|
),
|
|
21018
|
-
companyId:
|
|
22074
|
+
companyId: import_zod62.z.string().optional().describe(
|
|
21019
22075
|
"Agency/company ID that owns the snapshot. Defaults to the active location's company. Must match the company your agency key is scoped to."
|
|
21020
22076
|
)
|
|
21021
22077
|
},
|
|
@@ -21043,16 +22099,16 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
21043
22099
|
}
|
|
21044
22100
|
);
|
|
21045
22101
|
}
|
|
21046
|
-
var
|
|
22102
|
+
var import_zod62, SnapshotSchema, SnapshotsResponseSchema, ShareLinkResponseSchema, SHARE_TYPES;
|
|
21047
22103
|
var init_snapshots = __esm({
|
|
21048
22104
|
"src/tools/snapshots.ts"() {
|
|
21049
22105
|
"use strict";
|
|
21050
|
-
|
|
22106
|
+
import_zod62 = require("zod");
|
|
21051
22107
|
init_ghl_client();
|
|
21052
22108
|
init_tool_helpers();
|
|
21053
|
-
SnapshotSchema =
|
|
21054
|
-
SnapshotsResponseSchema =
|
|
21055
|
-
ShareLinkResponseSchema =
|
|
22109
|
+
SnapshotSchema = import_zod62.z.object({ id: import_zod62.z.string(), name: import_zod62.z.string(), type: import_zod62.z.string() }).passthrough();
|
|
22110
|
+
SnapshotsResponseSchema = import_zod62.z.object({ snapshots: import_zod62.z.array(SnapshotSchema) }).passthrough();
|
|
22111
|
+
ShareLinkResponseSchema = import_zod62.z.object({ id: import_zod62.z.string(), shareLink: import_zod62.z.string() }).passthrough();
|
|
21056
22112
|
SHARE_TYPES = [
|
|
21057
22113
|
"link",
|
|
21058
22114
|
"permanent_link",
|
|
@@ -21070,7 +22126,7 @@ function registerPhoneTools(server2, client) {
|
|
|
21070
22126
|
"list_phone_numbers",
|
|
21071
22127
|
"List the LC Phone numbers provisioned for a location (sid, number, label). Read-only. Use to verify or count purchased numbers (e.g. provisioning step 11). Number purchase is not exposed (billable write).",
|
|
21072
22128
|
{
|
|
21073
|
-
locationId:
|
|
22129
|
+
locationId: import_zod63.z.string().optional().describe("Defaults to the active location.")
|
|
21074
22130
|
},
|
|
21075
22131
|
async ({ locationId: locationId2 }) => {
|
|
21076
22132
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -21088,7 +22144,7 @@ function registerPhoneTools(server2, client) {
|
|
|
21088
22144
|
"list_number_pools",
|
|
21089
22145
|
"List LC Phone number pools configured for a location. Read-only.",
|
|
21090
22146
|
{
|
|
21091
|
-
locationId:
|
|
22147
|
+
locationId: import_zod63.z.string().optional().describe("Defaults to the active location.")
|
|
21092
22148
|
},
|
|
21093
22149
|
async ({ locationId: locationId2 }) => {
|
|
21094
22150
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -21098,15 +22154,15 @@ function registerPhoneTools(server2, client) {
|
|
|
21098
22154
|
}
|
|
21099
22155
|
);
|
|
21100
22156
|
}
|
|
21101
|
-
var
|
|
22157
|
+
var import_zod63, PhoneNumberSchema, NumbersResponseSchema, PoolsResponseSchema;
|
|
21102
22158
|
var init_phone = __esm({
|
|
21103
22159
|
"src/tools/phone.ts"() {
|
|
21104
22160
|
"use strict";
|
|
21105
|
-
|
|
22161
|
+
import_zod63 = require("zod");
|
|
21106
22162
|
init_tool_helpers();
|
|
21107
|
-
PhoneNumberSchema =
|
|
21108
|
-
NumbersResponseSchema =
|
|
21109
|
-
PoolsResponseSchema =
|
|
22163
|
+
PhoneNumberSchema = import_zod63.z.object({ sid: import_zod63.z.string(), value: import_zod63.z.string(), title: import_zod63.z.string().optional() }).passthrough();
|
|
22164
|
+
NumbersResponseSchema = import_zod63.z.object({ phoneNumbers: import_zod63.z.array(PhoneNumberSchema) }).passthrough();
|
|
22165
|
+
PoolsResponseSchema = import_zod63.z.object({ pools: import_zod63.z.array(import_zod63.z.object({}).passthrough()) }).passthrough();
|
|
21110
22166
|
}
|
|
21111
22167
|
});
|
|
21112
22168
|
|
|
@@ -21124,8 +22180,8 @@ function registerAccountHealthTools(server2, client) {
|
|
|
21124
22180
|
"get_account_health_summary",
|
|
21125
22181
|
"Account-health summary for a location, composed from existing reads (GHL has no reporting API). Returns: total contacts + NEW contacts in the window; total opportunities + counts by status (open/won/lost/abandoned); total conversations; phone-number count. Every metric is explicitly labeled all_time vs window (with start/end) \u2014 windowed and all-time numbers are never conflated. Sections that can't be read return status:'unavailable' (never a misleading 0). Revenue and appointments are intentionally excluded (not reachable / too costly via the public API).",
|
|
21126
22182
|
{
|
|
21127
|
-
locationId:
|
|
21128
|
-
windowDays:
|
|
22183
|
+
locationId: import_zod64.z.string().optional().describe("Defaults to the active location."),
|
|
22184
|
+
windowDays: import_zod64.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
|
|
21129
22185
|
},
|
|
21130
22186
|
async ({ locationId: locationId2, windowDays }) => {
|
|
21131
22187
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -21189,16 +22245,16 @@ function registerAccountHealthTools(server2, client) {
|
|
|
21189
22245
|
}
|
|
21190
22246
|
);
|
|
21191
22247
|
}
|
|
21192
|
-
var
|
|
22248
|
+
var import_zod64, MetaTotalSchema, TotalSchema2, NumbersSchema, OPP_STATUSES;
|
|
21193
22249
|
var init_account_health = __esm({
|
|
21194
22250
|
"src/tools/account-health.ts"() {
|
|
21195
22251
|
"use strict";
|
|
21196
|
-
|
|
22252
|
+
import_zod64 = require("zod");
|
|
21197
22253
|
init_tool_helpers();
|
|
21198
22254
|
init_contact_window();
|
|
21199
|
-
MetaTotalSchema =
|
|
21200
|
-
TotalSchema2 =
|
|
21201
|
-
NumbersSchema =
|
|
22255
|
+
MetaTotalSchema = import_zod64.z.object({ meta: import_zod64.z.object({ total: import_zod64.z.number() }).passthrough() }).passthrough();
|
|
22256
|
+
TotalSchema2 = import_zod64.z.object({ total: import_zod64.z.number() }).passthrough();
|
|
22257
|
+
NumbersSchema = import_zod64.z.object({ phoneNumbers: import_zod64.z.array(import_zod64.z.unknown()) }).passthrough();
|
|
21202
22258
|
OPP_STATUSES = ["open", "won", "lost", "abandoned"];
|
|
21203
22259
|
}
|
|
21204
22260
|
});
|
|
@@ -21754,11 +22810,11 @@ function validateBrief(input) {
|
|
|
21754
22810
|
warnings: []
|
|
21755
22811
|
};
|
|
21756
22812
|
}
|
|
21757
|
-
var
|
|
22813
|
+
var import_zod65, BRIEF_SCHEMA_VERSION, PRESETS, presetSchema, BRIEF_SOURCES, briefSourceSchema, pricePointSchema, staffMemberSchema, BRIEF_CALENDAR_TYPES, briefCalendarSchema, TRI_STATES, briefSchema;
|
|
21758
22814
|
var init_brief = __esm({
|
|
21759
22815
|
"src/intake-to-build/brief.ts"() {
|
|
21760
22816
|
"use strict";
|
|
21761
|
-
|
|
22817
|
+
import_zod65 = require("zod");
|
|
21762
22818
|
BRIEF_SCHEMA_VERSION = "0.1";
|
|
21763
22819
|
PRESETS = [
|
|
21764
22820
|
"generic",
|
|
@@ -21768,105 +22824,105 @@ var init_brief = __esm({
|
|
|
21768
22824
|
"ecom",
|
|
21769
22825
|
"agency"
|
|
21770
22826
|
];
|
|
21771
|
-
presetSchema =
|
|
22827
|
+
presetSchema = import_zod65.z.enum(PRESETS);
|
|
21772
22828
|
BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
|
|
21773
|
-
briefSourceSchema =
|
|
21774
|
-
pricePointSchema =
|
|
21775
|
-
name:
|
|
21776
|
-
price:
|
|
22829
|
+
briefSourceSchema = import_zod65.z.enum(BRIEF_SOURCES);
|
|
22830
|
+
pricePointSchema = import_zod65.z.object({
|
|
22831
|
+
name: import_zod65.z.string(),
|
|
22832
|
+
price: import_zod65.z.string()
|
|
21777
22833
|
});
|
|
21778
|
-
staffMemberSchema =
|
|
21779
|
-
name:
|
|
21780
|
-
email:
|
|
22834
|
+
staffMemberSchema = import_zod65.z.object({
|
|
22835
|
+
name: import_zod65.z.string().min(1),
|
|
22836
|
+
email: import_zod65.z.string().min(1),
|
|
21781
22837
|
/** Job title as the client wrote it ("Front desk", "Provider"); omitted when
|
|
21782
22838
|
* the line had none. The plan decides admin/user from it. */
|
|
21783
|
-
role:
|
|
21784
|
-
mobile:
|
|
22839
|
+
role: import_zod65.z.string().optional(),
|
|
22840
|
+
mobile: import_zod65.z.string().optional()
|
|
21785
22841
|
});
|
|
21786
22842
|
BRIEF_CALENDAR_TYPES = ["one_on_one", "round_robin", "class"];
|
|
21787
|
-
briefCalendarSchema =
|
|
21788
|
-
name:
|
|
21789
|
-
type:
|
|
21790
|
-
staffNames:
|
|
22843
|
+
briefCalendarSchema = import_zod65.z.object({
|
|
22844
|
+
name: import_zod65.z.string().min(1),
|
|
22845
|
+
type: import_zod65.z.enum(BRIEF_CALENDAR_TYPES),
|
|
22846
|
+
staffNames: import_zod65.z.array(import_zod65.z.string()),
|
|
21791
22847
|
/** Appointment length in minutes, when the client said one ("15 minutes").
|
|
21792
22848
|
* Lands on the plan calendar as slotDuration (finding 25). */
|
|
21793
|
-
durationMinutes:
|
|
22849
|
+
durationMinutes: import_zod65.z.number().int().positive().optional()
|
|
21794
22850
|
});
|
|
21795
22851
|
TRI_STATES = ["yes", "no", "unsure"];
|
|
21796
|
-
briefSchema =
|
|
21797
|
-
schemaVersion:
|
|
21798
|
-
briefId:
|
|
22852
|
+
briefSchema = import_zod65.z.object({
|
|
22853
|
+
schemaVersion: import_zod65.z.string(),
|
|
22854
|
+
briefId: import_zod65.z.string(),
|
|
21799
22855
|
preset: presetSchema,
|
|
21800
22856
|
briefSource: briefSourceSchema,
|
|
21801
22857
|
/** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
|
|
21802
|
-
extended:
|
|
21803
|
-
business:
|
|
21804
|
-
name:
|
|
21805
|
-
type:
|
|
21806
|
-
website:
|
|
21807
|
-
location:
|
|
21808
|
-
timezone:
|
|
22858
|
+
extended: import_zod65.z.record(import_zod65.z.unknown()).optional(),
|
|
22859
|
+
business: import_zod65.z.object({
|
|
22860
|
+
name: import_zod65.z.string(),
|
|
22861
|
+
type: import_zod65.z.string().optional(),
|
|
22862
|
+
website: import_zod65.z.string().optional(),
|
|
22863
|
+
location: import_zod65.z.string().optional(),
|
|
22864
|
+
timezone: import_zod65.z.string().optional(),
|
|
21809
22865
|
// Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
|
|
21810
22866
|
// the same tolerance reason as business.type (don't reject valid briefs).
|
|
21811
|
-
teamSize:
|
|
21812
|
-
monthlyLeadVolume:
|
|
21813
|
-
hours:
|
|
22867
|
+
teamSize: import_zod65.z.string().optional(),
|
|
22868
|
+
monthlyLeadVolume: import_zod65.z.string().optional(),
|
|
22869
|
+
hours: import_zod65.z.string().optional()
|
|
21814
22870
|
}).passthrough(),
|
|
21815
|
-
offer:
|
|
21816
|
-
summary:
|
|
22871
|
+
offer: import_zod65.z.object({
|
|
22872
|
+
summary: import_zod65.z.string().optional(),
|
|
21817
22873
|
// Parsed best-effort; tolerate a raw string when parsing was not possible.
|
|
21818
|
-
pricePoints:
|
|
21819
|
-
leadMagnet:
|
|
21820
|
-
avgDealValue:
|
|
22874
|
+
pricePoints: import_zod65.z.union([import_zod65.z.array(pricePointSchema), import_zod65.z.string()]).optional(),
|
|
22875
|
+
leadMagnet: import_zod65.z.string().optional(),
|
|
22876
|
+
avgDealValue: import_zod65.z.string().optional()
|
|
21821
22877
|
}).passthrough().optional(),
|
|
21822
|
-
audience:
|
|
21823
|
-
ideal:
|
|
21824
|
-
painPoints:
|
|
21825
|
-
objections:
|
|
22878
|
+
audience: import_zod65.z.object({
|
|
22879
|
+
ideal: import_zod65.z.string().optional(),
|
|
22880
|
+
painPoints: import_zod65.z.array(import_zod65.z.string()).optional(),
|
|
22881
|
+
objections: import_zod65.z.array(import_zod65.z.string()).optional()
|
|
21826
22882
|
}).passthrough().optional(),
|
|
21827
|
-
goal:
|
|
22883
|
+
goal: import_zod65.z.object({
|
|
21828
22884
|
// Kept as string: the form option labels are the canonical values, but
|
|
21829
22885
|
// the contract sample shortens some (e.g. "high-touch"). See §7 note.
|
|
21830
|
-
primary:
|
|
21831
|
-
salesStages:
|
|
21832
|
-
bookingNeeded:
|
|
21833
|
-
followUpStyle:
|
|
22886
|
+
primary: import_zod65.z.string(),
|
|
22887
|
+
salesStages: import_zod65.z.array(import_zod65.z.string()).optional(),
|
|
22888
|
+
bookingNeeded: import_zod65.z.boolean().optional(),
|
|
22889
|
+
followUpStyle: import_zod65.z.string().optional()
|
|
21834
22890
|
}).passthrough(),
|
|
21835
|
-
channels:
|
|
21836
|
-
email:
|
|
21837
|
-
sms:
|
|
21838
|
-
a2pStatus:
|
|
21839
|
-
payment:
|
|
21840
|
-
calendarConnected:
|
|
21841
|
-
social:
|
|
22891
|
+
channels: import_zod65.z.object({
|
|
22892
|
+
email: import_zod65.z.boolean().optional(),
|
|
22893
|
+
sms: import_zod65.z.boolean().optional(),
|
|
22894
|
+
a2pStatus: import_zod65.z.string().optional(),
|
|
22895
|
+
payment: import_zod65.z.string().optional(),
|
|
22896
|
+
calendarConnected: import_zod65.z.boolean().optional(),
|
|
22897
|
+
social: import_zod65.z.array(import_zod65.z.string()).optional(),
|
|
21842
22898
|
/** "Do you already have a phone number in GoHighLevel?" */
|
|
21843
|
-
hasPhoneNumber:
|
|
22899
|
+
hasPhoneNumber: import_zod65.z.enum(TRI_STATES).optional()
|
|
21844
22900
|
}).passthrough().optional(),
|
|
21845
22901
|
/** Section G — who is on the account and who gets pinged. */
|
|
21846
|
-
team:
|
|
21847
|
-
staff:
|
|
22902
|
+
team: import_zod65.z.object({
|
|
22903
|
+
staff: import_zod65.z.array(staffMemberSchema).optional(),
|
|
21848
22904
|
/** Who is notified about new leads: a staff name, or "the owner". */
|
|
21849
|
-
notifyName:
|
|
22905
|
+
notifyName: import_zod65.z.string().optional(),
|
|
21850
22906
|
/** Who takes booking / follow-up calls: a staff name, or "the owner". */
|
|
21851
|
-
callsName:
|
|
22907
|
+
callsName: import_zod65.z.string().optional()
|
|
21852
22908
|
}).passthrough().optional(),
|
|
21853
22909
|
/** Section H — every booking calendar the client asked for. */
|
|
21854
|
-
calendars:
|
|
22910
|
+
calendars: import_zod65.z.array(briefCalendarSchema).optional(),
|
|
21855
22911
|
/** Section I — copy inputs, verbatim. */
|
|
21856
|
-
voice:
|
|
21857
|
-
threeWords:
|
|
21858
|
-
signatureLine:
|
|
22912
|
+
voice: import_zod65.z.object({
|
|
22913
|
+
threeWords: import_zod65.z.string().optional(),
|
|
22914
|
+
signatureLine: import_zod65.z.string().optional()
|
|
21859
22915
|
}).passthrough().optional(),
|
|
21860
|
-
assets:
|
|
21861
|
-
existingPipeline:
|
|
21862
|
-
existingWorkflows:
|
|
21863
|
-
brand:
|
|
21864
|
-
notes:
|
|
22916
|
+
assets: import_zod65.z.object({
|
|
22917
|
+
existingPipeline: import_zod65.z.string().optional(),
|
|
22918
|
+
existingWorkflows: import_zod65.z.string().optional(),
|
|
22919
|
+
brand: import_zod65.z.string().optional(),
|
|
22920
|
+
notes: import_zod65.z.string().optional()
|
|
21865
22921
|
}).passthrough().optional(),
|
|
21866
|
-
flags:
|
|
22922
|
+
flags: import_zod65.z.array(import_zod65.z.string()).optional(),
|
|
21867
22923
|
/** Parse-time notes from the normalizer (a staff line it could not read,
|
|
21868
22924
|
* a calendar type it had to assume). Never fatal; surfaced by validate_brief. */
|
|
21869
|
-
warnings:
|
|
22925
|
+
warnings: import_zod65.z.array(import_zod65.z.string()).optional()
|
|
21870
22926
|
}).strict();
|
|
21871
22927
|
}
|
|
21872
22928
|
});
|
|
@@ -24955,7 +26011,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
24955
26011
|
"get_intake_question_set",
|
|
24956
26012
|
`Return the Intake-to-Build question set (the questions the installed intake form asks) plus each question's GHL field mapping and the Brief field it feeds. Read-only. Pass industry (e.g. "clinic", "med-spa") to see the tailored set \u2014 base questions + that industry's pack + the agency's own overlay, exactly what install_intake_form installs for that industry; omit it for the base set. Use this to review or render the intake before installing it.`,
|
|
24957
26013
|
{
|
|
24958
|
-
industry:
|
|
26014
|
+
industry: import_zod66.z.string().optional().describe(`Industry pack slug to compose in (${INDUSTRY_PACKS.map((p) => p.slug).join(", ")}). Omit for the base set.`)
|
|
24959
26015
|
},
|
|
24960
26016
|
async ({ industry }) => {
|
|
24961
26017
|
const agency = readOverlay();
|
|
@@ -24986,7 +26042,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
24986
26042
|
"validate_brief",
|
|
24987
26043
|
"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.",
|
|
24988
26044
|
{
|
|
24989
|
-
brief:
|
|
26045
|
+
brief: import_zod66.z.record(import_zod66.z.unknown()).describe("The Brief object to validate.")
|
|
24990
26046
|
},
|
|
24991
26047
|
async ({ brief }) => validateBrief(brief)
|
|
24992
26048
|
);
|
|
@@ -24995,7 +26051,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
24995
26051
|
"validate_build_plan",
|
|
24996
26052
|
"Validate an Intake-to-Build Build Plan against the \xA75 schema AND check ref-integrity: every symbolic ref (pipeline.*, stage.*, tag.*, ...) must resolve to a defined object of the right type, and refs must be unique. Dead references are reported as errors here, before any build runs \u2014 the structural guard against 'an invalid ID silently kills downstream actions'. Returns {valid, errors, warnings, referencesScanned}.",
|
|
24997
26053
|
{
|
|
24998
|
-
plan:
|
|
26054
|
+
plan: import_zod66.z.record(import_zod66.z.unknown()).describe("The Build Plan object to validate.")
|
|
24999
26055
|
},
|
|
25000
26056
|
async ({ plan }) => validateBuildPlan(plan)
|
|
25001
26057
|
);
|
|
@@ -25003,25 +26059,25 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
25003
26059
|
"apply_build_plan",
|
|
25004
26060
|
'Take an APPROVED Blueprint \xA75 build plan + the CURRENT confirmed sub-account and build it. PREFERRED for a fresh build: pass fromPreset:true + the \xA74 brief and the plan is COMPOSED SERVER-SIDE from the industry preset in milliseconds (structure, workflows, send-ready copy token-filled from the brief) \u2014 never author 100 workflow actions by hand when a preset covers the brief; see the fromPreset parameter. mode:"dry_run" (default) writes NOTHING \u2014 it resolves refs, expands each workflow\'s logical actions to native GHL JSON, runs the NEVER-CLOBBER existing-asset scan, and returns a two-part report. Run it FIRST. mode:"execute" performs LIVE writes for the CRM backbone (pipelines+stages, custom fields, tags, custom values), calendars, AND forms: never clobbers (same-named objects are bound to their existing id, never modified), verifies each create by read-back before resolving its ref, halts on the first failure returning the partial idMap, and is idempotent (re-run = no-op). Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole account user (auto-assigns you as the team member); with 0 or 2+ users they\'re surfaced as a manual step, not auto-staffed to a guess. CALENDAR TEAM: after creating any calendar that names staff, the team is READ BACK \u2014 a 200 is not evidence. A one-off `event` calendar has no team-member concept (GHL accepts teamMembers and drops them silently), so a calendar naming staff is never built as one, and if the people still do not land the run reports it AND emits a manual step naming the person and the calendar (manual type `calendar_team`) \u2014 never a silent success. Forms build with their standard + custom fields (custom fieldRefs resolve to the real fields created earlier in the run). Funnels: a GHL funnel (target:"ghl", default) builds structurally (funnel + named steps; page content/HTML is a manual step \u2014 plans carry outlines); a funnel with target:"external" is NOT built or deployed here \u2014 the user builds + hosts the site themselves (Cloudflare/Vercel) and wires its form back to this GHL sub-account (surfaced as a manual wiring step). Workflows build as DRAFT with all their logical actions expanded to native GHL JSON (incl. opportunity create/move steps, re-enabled v3.41.0) and chained; a contact_tag trigger is built automatically, other trigger types are surfaced as a manual step; the operator reviews + publishes. STAFF (Tier 1 v2): plan.users are created through the agency key (the create_user path \u2014 bound by email when they already exist, no invented password) BEFORE calendars and workflows, so calendars get their named team members (teamMemberRefs) and notification / task steps target real user ids (userRef). Without an agency key the run still proceeds: those users are pending, and every step that notifies them is STILL BUILT with a placeholder target and flagged staff_pending ("Add a staff member, then assign this step") \u2014 never dropped, never pointed at a user from another account. TEMPLATES: plan.templates (emails + sms) are saved as account-level email templates / SMS snippets before the workflows (never-clobber by name); the steps keep the same copy inline and the template ids come back in `templates`. Email templates are created in the editor\'s own format (vibe-editor) so they open and edit in Marketing \u2192 Emails \u2192 Templates, and each saved body is READ BACK from GHL\'s preview \u2014 a template whose copy is not seen there is reported with a warning and counted in templatesUnverified, never claimed as written. Always confirms the active location and validates the plan before any write. RE-RUNS: pass useSavedPlan:true (no plan) to build from the plan saved on this machine by the earlier execute; a differently named plan is refused while a saved one exists unless replaceSavedPlan:true \u2014 re-authoring a plan on a re-run drifts and duplicates objects (a second pipeline beside the first).',
|
|
25005
26061
|
{
|
|
25006
|
-
plan:
|
|
25007
|
-
fromPreset:
|
|
25008
|
-
brief:
|
|
25009
|
-
preset:
|
|
25010
|
-
copy:
|
|
25011
|
-
|
|
25012
|
-
ref:
|
|
25013
|
-
subject:
|
|
25014
|
-
html:
|
|
25015
|
-
body:
|
|
26062
|
+
plan: import_zod66.z.record(import_zod66.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."),
|
|
26063
|
+
fromPreset: import_zod66.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."),
|
|
26064
|
+
brief: import_zod66.z.record(import_zod66.z.unknown()).optional().describe("fromPreset only: the \xA74 Brief (from normalize_submission_to_brief, or assembled from the cockpit intake answers)."),
|
|
26065
|
+
preset: import_zod66.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)."),
|
|
26066
|
+
copy: import_zod66.z.array(
|
|
26067
|
+
import_zod66.z.object({
|
|
26068
|
+
ref: import_zod66.z.string().describe("email_template.* or sms_template.* ref from the composed plan"),
|
|
26069
|
+
subject: import_zod66.z.string().optional(),
|
|
26070
|
+
html: import_zod66.z.string().optional(),
|
|
26071
|
+
body: import_zod66.z.string().optional()
|
|
25016
26072
|
})
|
|
25017
26073
|
).optional().describe("fromPreset only: client-specific copy overrides \u2014 subject/html for an email template, body for an SMS template. Copy ONLY; structure is never overridable. Overrides land on the template AND its reviewable asset. An unknown ref is an error, never a silent no-op."),
|
|
25018
|
-
useSavedPlan:
|
|
25019
|
-
replaceSavedPlan:
|
|
25020
|
-
mode:
|
|
25021
|
-
locationId:
|
|
25022
|
-
metHandoffs:
|
|
25023
|
-
publishWorkflows:
|
|
25024
|
-
onConflict:
|
|
26074
|
+
useSavedPlan: import_zod66.z.boolean().optional().describe("Load the approved plan saved for the active location by an earlier execute (plans/<locationId>.json on this machine) instead of passing one. The right choice for ANY re-run: re-authoring a plan drifts and duplicates objects."),
|
|
26075
|
+
replaceSavedPlan: import_zod66.z.boolean().optional().describe("Only with a new `plan` whose planId differs from the saved one: confirms the operator approved the new plan, so it replaces the saved plan on execute. Without it, a differently named plan is refused while a saved plan exists."),
|
|
26076
|
+
mode: import_zod66.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
|
|
26077
|
+
locationId: import_zod66.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
|
|
26078
|
+
metHandoffs: import_zod66.z.array(import_zod66.z.string()).optional().describe('Handoff refs the operator has already satisfied (e.g. ["handoff.a2p"]) \u2014 lifts their gate so dependent workflows are not held DRAFT.'),
|
|
26079
|
+
publishWorkflows: import_zod66.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
|
|
26080
|
+
onConflict: import_zod66.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
|
|
25025
26081
|
},
|
|
25026
26082
|
async ({ plan: planArgIn, fromPreset, brief: briefArg, preset: presetArg, copy: copyArg, useSavedPlan, replaceSavedPlan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
|
|
25027
26083
|
try {
|
|
@@ -25239,7 +26295,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
25239
26295
|
}
|
|
25240
26296
|
const collisions = result.items.filter((i) => i.status === "existing");
|
|
25241
26297
|
const aborted = onConflict === "abort" && collisions.length > 0;
|
|
25242
|
-
const report2 =
|
|
26298
|
+
const report2 = renderReport2(typedPlan, result, {
|
|
25243
26299
|
mode: "dry_run",
|
|
25244
26300
|
locationName,
|
|
25245
26301
|
locationId: activeLocation,
|
|
@@ -25304,9 +26360,9 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
25304
26360
|
"revert_build",
|
|
25305
26361
|
`Undo a Blueprint build: remove the objects a recorded apply_build_plan run CREATED in the current sub-account, and nothing else. PREVIEWS BY DEFAULT \u2014 with no confirm it writes nothing and returns a plain-English list of exactly what would be removed, grouped, plus what it will leave alone and why. Pass confirm:"DELETE" to actually remove them. Acts on one recorded run: pass runId, or omit it for the most recent run in the ACTIVE account (runs from any other sub-account are in a different journal and can never be reached from here). The one hard rule: it removes only objects the run's never-clobber ledger recorded as CREATED \u2014 anything the build merely BOUND to (a pipeline, tag, calendar or workflow that was already in the account) is never touched, so a build run against an account that already had assets cannot delete the client's own work. Deletes run in the exact reverse of the build order (workflows first, then templates, funnels and their pages, forms, calendars, custom values, tags, custom fields, pipelines) so nothing is removed while something else still points at it. A refusal from GoHighLevel never aborts the run: refusals are collected and reported at the end with what to do about each. After the deletes the account is re-read independently and each object is reported as confirmed gone or as accepted-but-unconfirmed. Idempotent: run it twice and the second run finds nothing left and says so. Two kinds are deliberately NEVER removed automatically \u2014 staff users (a person's login; removing it can strip them off appointments and assignments) and text-message templates (GoHighLevel exposes no delete for them) \u2014 both are listed with where to remove them by hand.`,
|
|
25306
26362
|
{
|
|
25307
|
-
runId:
|
|
25308
|
-
confirm:
|
|
25309
|
-
locationId:
|
|
26363
|
+
runId: import_zod66.z.string().optional().describe('The recorded build run to undo (the `runId` apply_build_plan returned, e.g. "r-20260827T0142-9c1e"). Omit for the most recent run recorded for the active sub-account.'),
|
|
26364
|
+
confirm: import_zod66.z.literal("DELETE").optional().describe('Omit for a preview (no writes at all). Pass "DELETE" to actually remove the objects the preview listed.'),
|
|
26365
|
+
locationId: import_zod66.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first).")
|
|
25310
26366
|
},
|
|
25311
26367
|
async ({ runId, confirm: confirm2, locationId: locationId2 }) => {
|
|
25312
26368
|
try {
|
|
@@ -25484,10 +26540,10 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
25484
26540
|
"install_intake_form",
|
|
25485
26541
|
`Install the Intake-to-Build client-intake form into the CURRENT GHL location (whatever get_current_location returns). Account-agnostic, zero hardcoded IDs. Creates any missing intake custom fields (idempotent \u2014 reused on re-run), then builds the form with the proven GHL form-builder field shapes and verifies it. Pass industry (e.g. "clinic", "med-spa") to install the TAILORED set \u2014 base questions + that industry's pack + the agency's own overlay (added / removed questions), the same set the Command OS cockpit asks \u2014 so no cockpit answer is left without a form field; omit it for the base set. Returns {formId, fieldMap} \u2014 keep fieldMap; normalize_submission_to_brief uses it. Pass dryRun:true to preview what would be created without writing. Pass formId to update an existing intake form in place (e.g. to add an industry's questions to a form installed without one) instead of creating a new one.`,
|
|
25486
26542
|
{
|
|
25487
|
-
dryRun:
|
|
25488
|
-
formId:
|
|
25489
|
-
formName:
|
|
25490
|
-
industry:
|
|
26543
|
+
dryRun: import_zod66.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
|
|
26544
|
+
formId: import_zod66.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
|
|
26545
|
+
formName: import_zod66.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`),
|
|
26546
|
+
industry: import_zod66.z.string().optional().describe(`Industry pack to install with the base set (${INDUSTRY_PACKS.map((p) => p.slug).join(", ")}). Omit for the base set only.`)
|
|
25491
26547
|
},
|
|
25492
26548
|
async ({ dryRun, formId, formName, industry }) => {
|
|
25493
26549
|
try {
|
|
@@ -25590,10 +26646,10 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
25590
26646
|
"normalize_submission_to_brief",
|
|
25591
26647
|
'Read an intake form submission and normalize it into a \xA74 Brief (briefSource:"intake_form"). Pass the formId; by default the most recent submission is used (or pass submissionId). The intakeKey->customFieldId map is taken from fieldMap if provided (the install_intake_form output, most robust), otherwise reconstructed from the live form. Returns {brief, validation, submissionId} \u2014 validation flags any missing required fields (e.g. an incomplete submission).',
|
|
25592
26648
|
{
|
|
25593
|
-
formId:
|
|
25594
|
-
submissionId:
|
|
25595
|
-
fieldMap:
|
|
25596
|
-
preset:
|
|
26649
|
+
formId: import_zod66.z.string().describe("The intake form ID (from install_intake_form)."),
|
|
26650
|
+
submissionId: import_zod66.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
|
|
26651
|
+
fieldMap: import_zod66.z.record(import_zod66.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
|
|
26652
|
+
preset: import_zod66.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
|
|
25597
26653
|
},
|
|
25598
26654
|
async ({ formId, submissionId, fieldMap, preset }) => {
|
|
25599
26655
|
try {
|
|
@@ -25622,7 +26678,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
25622
26678
|
const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
|
|
25623
26679
|
resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
|
|
25624
26680
|
}
|
|
25625
|
-
const presetSchema2 =
|
|
26681
|
+
const presetSchema2 = import_zod66.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
|
|
25626
26682
|
const presetParsed = presetSchema2.safeParse(preset);
|
|
25627
26683
|
const brief = normalizeSubmissionToBrief({
|
|
25628
26684
|
others,
|
|
@@ -25643,11 +26699,11 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
25643
26699
|
}
|
|
25644
26700
|
);
|
|
25645
26701
|
}
|
|
25646
|
-
var
|
|
26702
|
+
var import_zod66, customFieldItemSchema, sleep5;
|
|
25647
26703
|
var init_intake_to_build = __esm({
|
|
25648
26704
|
"src/tools/intake-to-build.ts"() {
|
|
25649
26705
|
"use strict";
|
|
25650
|
-
|
|
26706
|
+
import_zod66 = require("zod");
|
|
25651
26707
|
init_tool_helpers();
|
|
25652
26708
|
init_form_builder();
|
|
25653
26709
|
init_calendars();
|
|
@@ -25669,16 +26725,16 @@ var init_intake_to_build = __esm({
|
|
|
25669
26725
|
init_plan_from_preset();
|
|
25670
26726
|
init_executor();
|
|
25671
26727
|
init_execute();
|
|
25672
|
-
customFieldItemSchema =
|
|
25673
|
-
id:
|
|
25674
|
-
name:
|
|
25675
|
-
fieldKey:
|
|
25676
|
-
dataType:
|
|
25677
|
-
model:
|
|
25678
|
-
parentId:
|
|
25679
|
-
position:
|
|
25680
|
-
dateAdded:
|
|
25681
|
-
picklistOptions:
|
|
26728
|
+
customFieldItemSchema = import_zod66.z.object({
|
|
26729
|
+
id: import_zod66.z.string(),
|
|
26730
|
+
name: import_zod66.z.string(),
|
|
26731
|
+
fieldKey: import_zod66.z.string(),
|
|
26732
|
+
dataType: import_zod66.z.string(),
|
|
26733
|
+
model: import_zod66.z.string().optional(),
|
|
26734
|
+
parentId: import_zod66.z.string().optional(),
|
|
26735
|
+
position: import_zod66.z.number().optional(),
|
|
26736
|
+
dateAdded: import_zod66.z.string().optional(),
|
|
26737
|
+
picklistOptions: import_zod66.z.array(import_zod66.z.string()).optional()
|
|
25682
26738
|
}).passthrough();
|
|
25683
26739
|
sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25684
26740
|
}
|
|
@@ -25705,6 +26761,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
|
|
|
25705
26761
|
registerFunnelQaTools(wrap(FUNNEL_QA_MODULE), client, builderClient);
|
|
25706
26762
|
registerUserGuideTools(wrap(USER_GUIDE_MODULE));
|
|
25707
26763
|
registerAgencyProfileTools(wrap(AGENCY_PROFILE_MODULE));
|
|
26764
|
+
registerAssessmentTools(wrap(ASSESSMENT_MODULE), client);
|
|
25708
26765
|
registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
|
|
25709
26766
|
registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
|
|
25710
26767
|
registerDailyBriefTools(wrap(DAILY_BRIEF_MODULE), registry2);
|
|
@@ -25753,7 +26810,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
|
|
|
25753
26810
|
}
|
|
25754
26811
|
return { registeredTools, gatedTools, planGatedTools };
|
|
25755
26812
|
}
|
|
25756
|
-
var publicApiTools, internalApiTools, VALIDATORS_MODULE, DIAGNOSTICS_MODULE, LOCATION_SWITCHER_MODULE, SNAPSHOTS_MODULE, ACCOUNT_EXPORT_MODULE, FORM_BUILDER_MODULE, INTAKE_TO_BUILD_MODULE, EMAIL_BUILDER_MODULE, FUNNEL_QA_MODULE, USER_GUIDE_MODULE, AGENCY_PROFILE_MODULE, CLIENT_ENGAGEMENTS_MODULE, AUDIT_REPORT_MODULE, DAILY_BRIEF_MODULE, CHECKUP_MODULE, KNOWN_MODULES, PUBLIC_API_MODULES, INTERNAL_API_MODULES, DUAL_CLIENT_MODULES;
|
|
26813
|
+
var publicApiTools, internalApiTools, VALIDATORS_MODULE, DIAGNOSTICS_MODULE, LOCATION_SWITCHER_MODULE, SNAPSHOTS_MODULE, ACCOUNT_EXPORT_MODULE, FORM_BUILDER_MODULE, INTAKE_TO_BUILD_MODULE, EMAIL_BUILDER_MODULE, FUNNEL_QA_MODULE, USER_GUIDE_MODULE, AGENCY_PROFILE_MODULE, ASSESSMENT_MODULE, CLIENT_ENGAGEMENTS_MODULE, AUDIT_REPORT_MODULE, DAILY_BRIEF_MODULE, CHECKUP_MODULE, KNOWN_MODULES, PUBLIC_API_MODULES, INTERNAL_API_MODULES, DUAL_CLIENT_MODULES;
|
|
25757
26814
|
var init_tools = __esm({
|
|
25758
26815
|
"src/tools/index.ts"() {
|
|
25759
26816
|
"use strict";
|
|
@@ -25806,6 +26863,7 @@ var init_tools = __esm({
|
|
|
25806
26863
|
init_checkup();
|
|
25807
26864
|
init_user_guide();
|
|
25808
26865
|
init_agency_profile2();
|
|
26866
|
+
init_assessment();
|
|
25809
26867
|
init_client_engagements2();
|
|
25810
26868
|
init_audit_report2();
|
|
25811
26869
|
init_daily_brief2();
|
|
@@ -25876,11 +26934,13 @@ var init_tools = __esm({
|
|
|
25876
26934
|
FUNNEL_QA_MODULE = "funnel-qa";
|
|
25877
26935
|
USER_GUIDE_MODULE = "user-guide";
|
|
25878
26936
|
AGENCY_PROFILE_MODULE = "agency-profile";
|
|
26937
|
+
ASSESSMENT_MODULE = "assessment";
|
|
25879
26938
|
CLIENT_ENGAGEMENTS_MODULE = "client-engagements";
|
|
25880
26939
|
AUDIT_REPORT_MODULE = "audit-report";
|
|
25881
26940
|
DAILY_BRIEF_MODULE = "daily-brief";
|
|
25882
26941
|
CHECKUP_MODULE = "checkup";
|
|
25883
26942
|
KNOWN_MODULES = /* @__PURE__ */ new Set([
|
|
26943
|
+
"assessment",
|
|
25884
26944
|
...publicApiTools.map(([, label]) => label),
|
|
25885
26945
|
...internalApiTools.map(([, label]) => label),
|
|
25886
26946
|
FORM_BUILDER_MODULE,
|
|
@@ -25900,6 +26960,7 @@ var init_tools = __esm({
|
|
|
25900
26960
|
ACCOUNT_EXPORT_MODULE
|
|
25901
26961
|
]);
|
|
25902
26962
|
PUBLIC_API_MODULES = /* @__PURE__ */ new Set([
|
|
26963
|
+
"assessment",
|
|
25903
26964
|
...publicApiTools.map(([, label]) => label),
|
|
25904
26965
|
USER_GUIDE_MODULE,
|
|
25905
26966
|
// no client at all
|
|
@@ -26129,7 +27190,7 @@ function writeVerifiedBackup(configPath, originalBytes) {
|
|
|
26129
27190
|
return backupPath;
|
|
26130
27191
|
}
|
|
26131
27192
|
function sha2562(bytes) {
|
|
26132
|
-
return (0,
|
|
27193
|
+
return (0, import_node_crypto5.createHash)("sha256").update(bytes).digest("hex");
|
|
26133
27194
|
}
|
|
26134
27195
|
function baselineOf(bytes, stat) {
|
|
26135
27196
|
return { size: stat.size, mtimeMs: stat.mtimeMs, hash: sha2562(bytes) };
|
|
@@ -26155,7 +27216,7 @@ function assertNotStale(configPath, baseline) {
|
|
|
26155
27216
|
}
|
|
26156
27217
|
async function atomicReplace(opts) {
|
|
26157
27218
|
const dir = path19.dirname(opts.configPath);
|
|
26158
|
-
const tempPath = path19.join(dir, `.${path19.basename(opts.configPath)}.tmp-${process.pid}-${(0,
|
|
27219
|
+
const tempPath = path19.join(dir, `.${path19.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}`);
|
|
26159
27220
|
const fd = fs20.openSync(tempPath, "w");
|
|
26160
27221
|
try {
|
|
26161
27222
|
fs20.writeFileSync(fd, opts.newBytes);
|
|
@@ -26296,14 +27357,14 @@ async function runInstall(argv, ioOverride) {
|
|
|
26296
27357
|
return EXIT_ABORTED;
|
|
26297
27358
|
}
|
|
26298
27359
|
}
|
|
26299
|
-
var fs20, os5, path19,
|
|
27360
|
+
var fs20, os5, path19, import_node_crypto5, import_node_util, import_json5, SERVER_KEY, DESIRED_ENTRY, EXIT_OK, EXIT_USAGE, EXIT_REFUSED, EXIT_ABORTED, RENAME_RETRY_DELAYS_MS, LOCK_ERROR_CODES, BOM_UTF8, InstallStop;
|
|
26300
27361
|
var init_config_installer = __esm({
|
|
26301
27362
|
"src/config-installer.ts"() {
|
|
26302
27363
|
"use strict";
|
|
26303
27364
|
fs20 = __toESM(require("node:fs"));
|
|
26304
27365
|
os5 = __toESM(require("node:os"));
|
|
26305
27366
|
path19 = __toESM(require("node:path"));
|
|
26306
|
-
|
|
27367
|
+
import_node_crypto5 = require("node:crypto");
|
|
26307
27368
|
import_node_util = require("node:util");
|
|
26308
27369
|
import_json5 = __toESM(require("json5"));
|
|
26309
27370
|
SERVER_KEY = "ghl";
|
|
@@ -26333,7 +27394,7 @@ var require_package = __commonJS({
|
|
|
26333
27394
|
"package.json"(exports2, module2) {
|
|
26334
27395
|
module2.exports = {
|
|
26335
27396
|
name: "@elitedcs/ghl-mcp",
|
|
26336
|
-
version: "3.
|
|
27397
|
+
version: "3.73.0",
|
|
26337
27398
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
26338
27399
|
description: "GoHighLevel MCP Server for Claude. 247 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.",
|
|
26339
27400
|
main: "dist/index.js",
|
|
@@ -26343,6 +27404,7 @@ var require_package = __commonJS({
|
|
|
26343
27404
|
files: [
|
|
26344
27405
|
"dist/index.js",
|
|
26345
27406
|
"dist/capture-helper.js",
|
|
27407
|
+
"dist/assessment.html",
|
|
26346
27408
|
"templates/action-schemas.json",
|
|
26347
27409
|
"templates/clinic-medspa.json",
|
|
26348
27410
|
"templates/trigger-schemas.json",
|
|
@@ -26357,14 +27419,15 @@ var require_package = __commonJS({
|
|
|
26357
27419
|
"!skills/blueprint/presets/clinic-launch-a2p.md"
|
|
26358
27420
|
],
|
|
26359
27421
|
scripts: {
|
|
26360
|
-
build: "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",
|
|
27422
|
+
build: "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 && node src/command-os/assessment/build-app.mjs && cp src/command-os/assessment/app.html dist/assessment.html",
|
|
26361
27423
|
setup: "node setup-wizard.mjs",
|
|
26362
27424
|
catalogue: "node scripts/export-command-os-catalogue.mjs",
|
|
26363
27425
|
start: "node dist/index.js",
|
|
26364
27426
|
dev: "tsc --watch",
|
|
26365
27427
|
test: "vitest run",
|
|
26366
27428
|
"test:watch": "vitest",
|
|
26367
|
-
prepublishOnly: "npm run build"
|
|
27429
|
+
prepublishOnly: "npm run build",
|
|
27430
|
+
"build:assessment": "node src/command-os/assessment/build-app.mjs"
|
|
26368
27431
|
},
|
|
26369
27432
|
keywords: [
|
|
26370
27433
|
"mcp",
|
|
@@ -26827,21 +27890,21 @@ function agencyProfilePageHtml(p, clients = [], seatToken = "") {
|
|
|
26827
27890
|
const floor = c.engagement ? floorFor(c.engagement, p) : void 0;
|
|
26828
27891
|
const cur = e.currency || p.pricing?.currency || "";
|
|
26829
27892
|
const recorded = Object.keys(e).filter((k) => k !== "updated_at").length > 0;
|
|
26830
|
-
return ` <details class="cl" data-id="${
|
|
26831
|
-
<summary><span class="cn">${
|
|
27893
|
+
return ` <details class="cl" data-id="${esc2(c.locationId)}"${recorded ? "" : ' data-empty="1"'}>
|
|
27894
|
+
<summary><span class="cn">${esc2(c.name)}</span> <span class="cs">${recorded ? esc2([e.offer, e.price !== void 0 ? `${fmt(e.price, cur)}${e.cadence === "monthly" ? "/mo" : ""}` : "", e.status && e.status !== "active" ? e.status : ""].filter(Boolean).join(" \xB7 ") || "recorded") : "nothing recorded yet"}</span>${under && floor !== void 0 ? `<span class="warn">under your floor of ${esc2(fmt(floor, cur))}</span>` : ""}<span class="open">Open</span></summary>
|
|
26832
27895
|
<div class="row">
|
|
26833
|
-
<div><label>Offer</label><input class="f" data-k="offer" value="${
|
|
26834
|
-
<div><label>They pay</label><input class="f" data-k="price" inputmode="numeric" value="${
|
|
27896
|
+
<div><label>Offer</label><input class="f" data-k="offer" value="${esc2(e.offer)}" placeholder="${esc2(p.offers?.[0]?.name || "Speed to Lead")}"></div>
|
|
27897
|
+
<div><label>They pay</label><input class="f" data-k="price" inputmode="numeric" value="${esc2(e.price)}"></div>
|
|
26835
27898
|
<div><label>How often</label><select class="f" data-k="cadence"><option value=""${e.cadence ? "" : " selected"}>\u2014</option><option value="monthly"${e.cadence === "monthly" ? " selected" : ""}>Monthly</option><option value="one-time"${e.cadence === "one-time" ? " selected" : ""}>One-time</option></select></div>
|
|
26836
27899
|
</div>
|
|
26837
27900
|
<div class="row">
|
|
26838
|
-
<div><label>Started</label><input class="f" data-k="startedOn" placeholder="2026-01-15" value="${
|
|
26839
|
-
<div><label>Renews</label><input class="f" data-k="renewsOn" placeholder="2027-01-15" value="${
|
|
27901
|
+
<div><label>Started</label><input class="f" data-k="startedOn" placeholder="2026-01-15" value="${esc2(e.startedOn)}"></div>
|
|
27902
|
+
<div><label>Renews</label><input class="f" data-k="renewsOn" placeholder="2027-01-15" value="${esc2(e.renewsOn)}"></div>
|
|
26840
27903
|
<div><label>Status</label><select class="f" data-k="status"><option value=""${e.status ? "" : " selected"}>\u2014</option><option value="active"${e.status === "active" ? " selected" : ""}>Active</option><option value="paused"${e.status === "paused" ? " selected" : ""}>Paused</option><option value="ended"${e.status === "ended" ? " selected" : ""}>Ended</option></select></div>
|
|
26841
|
-
<div><label>Who runs it</label><input class="f" data-k="owner" value="${
|
|
27904
|
+
<div><label>Who runs it</label><input class="f" data-k="owner" value="${esc2(e.owner)}" placeholder="${esc2(p.staff?.[0]?.name || "")}"></div>
|
|
26842
27905
|
</div>
|
|
26843
27906
|
<label>Anything specific to this client \u2014 one per line</label>
|
|
26844
|
-
<textarea class="f" data-k="sops" placeholder="Call the owner before any send">${
|
|
27907
|
+
<textarea class="f" data-k="sops" placeholder="Call the owner before any send">${esc2((e.sops ?? []).join("\n"))}</textarea>
|
|
26845
27908
|
<div class="row" style="margin-top:.6rem"><div><button type="button" class="save-cl">Save this client</button> <span class="cmsg small"></span></div></div>
|
|
26846
27909
|
</details>`;
|
|
26847
27910
|
}).join("\n");
|
|
@@ -26855,7 +27918,7 @@ ${clientBlocks}
|
|
|
26855
27918
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
26856
27919
|
<title>Your agency \u2014 Command OS</title>
|
|
26857
27920
|
<style>
|
|
26858
|
-
:root{--bg:#faf9f7;--surface:#fff;--ink:#1a1a1a;--muted:#6b6b6b;--line:#e3e0da;--accent:${
|
|
27921
|
+
:root{--bg:#faf9f7;--surface:#fff;--ink:#1a1a1a;--muted:#6b6b6b;--line:#e3e0da;--accent:${esc2(p.accent || "#b8934a")}}
|
|
26859
27922
|
@media (prefers-color-scheme:dark){:root{--bg:#16151a;--surface:#1e1d23;--ink:#eceaf0;--muted:#a09daa;--line:#33313b}}
|
|
26860
27923
|
html{font-size:22px}
|
|
26861
27924
|
body{margin:0;background:var(--bg);color:var(--ink);font:1rem/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
@@ -26892,43 +27955,43 @@ ${clientBlocks}
|
|
|
26892
27955
|
<form id="f">
|
|
26893
27956
|
<fieldset><legend>Brand</legend>
|
|
26894
27957
|
<label for="agencyName">Agency name, as clients see it</label>
|
|
26895
|
-
<input id="agencyName" name="agencyName" value="${
|
|
27958
|
+
<input id="agencyName" name="agencyName" value="${esc2(p.agencyName)}" placeholder="Elite DCs">
|
|
26896
27959
|
<div class="row">
|
|
26897
|
-
<div><label for="accent">Brand colour</label><input type="color" id="accent" name="accent" value="${
|
|
26898
|
-
<div><label for="website">Website</label><input id="website" name="website" value="${
|
|
27960
|
+
<div><label for="accent">Brand colour</label><input type="color" id="accent" name="accent" value="${esc2(p.accent || "#b8934a")}"></div>
|
|
27961
|
+
<div><label for="website">Website</label><input id="website" name="website" value="${esc2(p.website)}" placeholder="https://elitedcs.com"></div>
|
|
26899
27962
|
</div>
|
|
26900
27963
|
<label for="logoUrl">Logo URL</label>
|
|
26901
|
-
<input id="logoUrl" name="logoUrl" value="${
|
|
27964
|
+
<input id="logoUrl" name="logoUrl" value="${esc2(p.logoUrl)}" placeholder="https://elitedcs.com/logo.svg">
|
|
26902
27965
|
<p class="hint">Must start with https. A logo link that isn't https is refused, because this one goes on pages your prospects open.</p>
|
|
26903
27966
|
<label for="legalName">Registered entity, if different</label>
|
|
26904
|
-
<input id="legalName" name="legalName" value="${
|
|
27967
|
+
<input id="legalName" name="legalName" value="${esc2(p.legalName)}" placeholder="Elite DCS LLC \u2014 used on contracts and A2P">
|
|
26905
27968
|
</fieldset>
|
|
26906
27969
|
|
|
26907
27970
|
<fieldset><legend>Who messages come from</legend>
|
|
26908
27971
|
<div class="row">
|
|
26909
|
-
<div><label for="fromName">From name</label><input id="fromName" name="fromName" value="${
|
|
26910
|
-
<div><label for="replyTo">Reply-to email</label><input id="replyTo" name="replyTo" value="${
|
|
26911
|
-
<div><label for="phone">Phone</label><input id="phone" name="phone" value="${
|
|
27972
|
+
<div><label for="fromName">From name</label><input id="fromName" name="fromName" value="${esc2(p.sender?.fromName)}" placeholder="Dr Jerry Relth"></div>
|
|
27973
|
+
<div><label for="replyTo">Reply-to email</label><input id="replyTo" name="replyTo" value="${esc2(p.sender?.replyTo)}"></div>
|
|
27974
|
+
<div><label for="phone">Phone</label><input id="phone" name="phone" value="${esc2(p.sender?.phone)}"></div>
|
|
26912
27975
|
</div>
|
|
26913
27976
|
</fieldset>
|
|
26914
27977
|
|
|
26915
27978
|
<fieldset><legend>What you sell</legend>
|
|
26916
27979
|
<label for="offers">Offers \u2014 one per line</label>
|
|
26917
|
-
<textarea id="offers" name="offers" placeholder="Speed to Lead \u2014 3000/mo Reactivation campaign \u2014 2500">${
|
|
27980
|
+
<textarea id="offers" name="offers" placeholder="Speed to Lead \u2014 3000/mo Reactivation campaign \u2014 2500">${esc2(offers)}</textarea>
|
|
26918
27981
|
<p class="hint">Write them as "name \u2014 price". A price ending in /mo is recorded as monthly.</p>
|
|
26919
27982
|
<div class="row">
|
|
26920
|
-
<div><label for="minMonthly">Minimum monthly</label><input id="minMonthly" name="minMonthly" inputmode="numeric" value="${
|
|
26921
|
-
<div><label for="minSetup">Minimum setup fee</label><input id="minSetup" name="minSetup" inputmode="numeric" value="${
|
|
26922
|
-
<div><label for="currency">Currency</label><input id="currency" name="currency" value="${
|
|
27983
|
+
<div><label for="minMonthly">Minimum monthly</label><input id="minMonthly" name="minMonthly" inputmode="numeric" value="${esc2(p.pricing?.minMonthly)}"></div>
|
|
27984
|
+
<div><label for="minSetup">Minimum setup fee</label><input id="minSetup" name="minSetup" inputmode="numeric" value="${esc2(p.pricing?.minSetup)}"></div>
|
|
27985
|
+
<div><label for="currency">Currency</label><input id="currency" name="currency" value="${esc2(p.pricing?.currency || "USD")}"></div>
|
|
26923
27986
|
</div>
|
|
26924
27987
|
<p class="hint">The floors are the numbers below which you don't take the work. Anything quoting a price checks them first.</p>
|
|
26925
27988
|
</fieldset>
|
|
26926
27989
|
|
|
26927
27990
|
<fieldset><legend>How you deliver</legend>
|
|
26928
27991
|
<label for="sops">Delivery SOPs \u2014 one per line</label>
|
|
26929
|
-
<textarea id="sops" name="sops" placeholder="Every build gets a rollback journal before go-live">${
|
|
27992
|
+
<textarea id="sops" name="sops" placeholder="Every build gets a rollback journal before go-live">${esc2(sops)}</textarea>
|
|
26930
27993
|
<label for="staff">Staff \u2014 one per line, "name \xB7 role \xB7 email"</label>
|
|
26931
|
-
<textarea id="staff" name="staff" placeholder="Dr Jerry Relth \xB7 owner \xB7 jerry@elitedcs.com">${
|
|
27994
|
+
<textarea id="staff" name="staff" placeholder="Dr Jerry Relth \xB7 owner \xB7 jerry@elitedcs.com">${esc2(staff)}</textarea>
|
|
26932
27995
|
<p class="hint">Who may approve what is recorded here for reference. The approval gates themselves are still enforced by the ritual, not by this list.</p>
|
|
26933
27996
|
</fieldset>
|
|
26934
27997
|
|
|
@@ -27003,7 +28066,7 @@ document.querySelectorAll(".save-cl").forEach(btn=>btn.addEventListener("click",
|
|
|
27003
28066
|
}));
|
|
27004
28067
|
</script></body></html>`;
|
|
27005
28068
|
}
|
|
27006
|
-
var READ_NUMBER_JS, fmt,
|
|
28069
|
+
var READ_NUMBER_JS, fmt, esc2;
|
|
27007
28070
|
var init_agency_profile_page = __esm({
|
|
27008
28071
|
"src/agency-profile-page.ts"() {
|
|
27009
28072
|
"use strict";
|
|
@@ -27017,7 +28080,7 @@ var init_agency_profile_page = __esm({
|
|
|
27017
28080
|
return Number.isFinite(n)?{value:n}:{bad:true};
|
|
27018
28081
|
}`;
|
|
27019
28082
|
fmt = (n, currency) => `${currency ? currency + " " : ""}${n.toLocaleString("en-US")}`;
|
|
27020
|
-
|
|
28083
|
+
esc2 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
27021
28084
|
}
|
|
27022
28085
|
});
|
|
27023
28086
|
|
|
@@ -27027,7 +28090,7 @@ function isHex(value) {
|
|
|
27027
28090
|
}
|
|
27028
28091
|
function readAgencyBranding(base = appDataDir()) {
|
|
27029
28092
|
try {
|
|
27030
|
-
const st = JSON.parse((0,
|
|
28093
|
+
const st = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(base, "cockpit-state.json"), "utf8"));
|
|
27031
28094
|
const out = {};
|
|
27032
28095
|
if (typeof st.agencyName === "string" && st.agencyName.trim()) out.agencyName = st.agencyName.trim();
|
|
27033
28096
|
if (isHex(st.agencyAccent)) out.accent = st.agencyAccent;
|
|
@@ -27044,12 +28107,12 @@ function resolveBranding(overrides = {}, base) {
|
|
|
27044
28107
|
const logoDataUri = overrides.logoDataUri ?? saved.logoDataUri;
|
|
27045
28108
|
return { ...agencyName && { agencyName }, accent, ...logoDataUri && { logoDataUri } };
|
|
27046
28109
|
}
|
|
27047
|
-
var
|
|
28110
|
+
var import_node_fs2, import_node_path2, DEFAULT_ACCENT;
|
|
27048
28111
|
var init_branding = __esm({
|
|
27049
28112
|
"src/command-os/branding.ts"() {
|
|
27050
28113
|
"use strict";
|
|
27051
|
-
|
|
27052
|
-
|
|
28114
|
+
import_node_fs2 = require("node:fs");
|
|
28115
|
+
import_node_path2 = require("node:path");
|
|
27053
28116
|
init_credentials_store();
|
|
27054
28117
|
DEFAULT_ACCENT = "#B8934A";
|
|
27055
28118
|
}
|
|
@@ -27215,19 +28278,19 @@ function renderClientStatusHtml(input) {
|
|
|
27215
28278
|
const stamp = friendlyStamp(input.checkedAt);
|
|
27216
28279
|
const day = friendlyDate(input.checkedAt);
|
|
27217
28280
|
const latest = scrubLatestCheck(input.latestCheck);
|
|
27218
|
-
const latestHtml = latest ? `<section id="latest"><h2>What the latest check found</h2>${latest.summary ? `<p class="intro">${
|
|
28281
|
+
const latestHtml = latest ? `<section id="latest"><h2>What the latest check found</h2>${latest.summary ? `<p class="intro">${esc3(latest.summary)}</p>` : ""}${latest.issues?.length ? `<ul class="issues">${latest.issues.map((i) => `<li>${esc3(i)}</li>`).join("")}</ul>` : `<p class="intro">Nothing needed your attention this time.</p>`}</section>` : "";
|
|
27219
28282
|
const entry = (it) => {
|
|
27220
28283
|
const cls = it.state === "in-place" ? "ok" : it.state === "waiting" ? "wait" : "soon";
|
|
27221
|
-
const detail = it.state === "waiting" && (it.resolvedBy || it.roughly) ? `<p class="who"><b>Who sorts it:</b> ${
|
|
27222
|
-
const body = it.sentence ? `<p>${
|
|
27223
|
-
return `<article class="item ${cls}"><span class="pill">${
|
|
28284
|
+
const detail = it.state === "waiting" && (it.resolvedBy || it.roughly) ? `<p class="who"><b>Who sorts it:</b> ${esc3(scrubForClient(it.resolvedBy ?? "") || "not stated")}.${it.roughly ? ` <b>Roughly when:</b> ${esc3(scrubForClient(it.roughly))}.` : ""}</p>` : "";
|
|
28285
|
+
const body = it.sentence ? `<p>${esc3(it.sentence)}</p>` : "";
|
|
28286
|
+
return `<article class="item ${cls}"><span class="pill">${esc3(CLIENT_STATE_LABEL[it.state])}</span><h3>${esc3(it.title)}</h3>${body}${detail}</article>`;
|
|
27224
28287
|
};
|
|
27225
|
-
const section2 = (id, title2, intro, list2) => list2.length ? `<section id="${id}"><h2>${
|
|
28288
|
+
const section2 = (id, title2, intro, list2) => list2.length ? `<section id="${id}"><h2>${esc3(title2)}</h2><p class="intro">${esc3(intro)}</p>${list2.map(entry).join("\n")}</section>` : "";
|
|
27226
28289
|
const footLeft = `${agency ? `Prepared by ${agency} for ` : "Prepared for "}${input.accountName} \xB7 last checked ${day}`;
|
|
27227
28290
|
const revLine = input.revision && input.revision > 0 ? `Update ${input.revision} \xB7 last checked ${stamp}` : `Last checked ${stamp}`;
|
|
27228
28291
|
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex">
|
|
27229
|
-
<title>${
|
|
27230
|
-
<meta name="description" content="${
|
|
28292
|
+
<title>${esc3(statusPageTitle(input))}</title>
|
|
28293
|
+
<meta name="description" content="${esc3(`What is live in the ${input.accountName} account right now.`)}">
|
|
27231
28294
|
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
27232
28295
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=DM+Sans:wght@400;500;700&display=swap">
|
|
27233
28296
|
<style>
|
|
@@ -27330,7 +28393,7 @@ function renderClientStatusHtml(input) {
|
|
|
27330
28393
|
}
|
|
27331
28394
|
</style></head><body>
|
|
27332
28395
|
<div class="chrome"><div class="chrome-in">
|
|
27333
|
-
<span class="who">${
|
|
28396
|
+
<span class="who">${esc3(agency || input.accountName)}</span>
|
|
27334
28397
|
<nav>${latestHtml ? '<a href="#latest">Latest check</a>' : ""}${live.length ? '<a href="#live">Live now</a>' : ""}${notYet.length ? '<a href="#soon">To confirm</a>' : ""}${waiting.length ? '<a href="#waiting">Waiting</a>' : ""}${next.length ? '<a href="#next">What happens next</a>' : ""}</nav>
|
|
27335
28398
|
<div class="tools">
|
|
27336
28399
|
<button type="button" id="print">Print / save PDF</button>
|
|
@@ -27339,11 +28402,11 @@ function renderClientStatusHtml(input) {
|
|
|
27339
28402
|
</div>
|
|
27340
28403
|
</div></div>
|
|
27341
28404
|
<div class="page"><div class="sheet">
|
|
27342
|
-
${agency || input.logoDataUri ? `<div class="brand">${input.logoDataUri ? `<img src="${
|
|
28405
|
+
${agency || input.logoDataUri ? `<div class="brand">${input.logoDataUri ? `<img src="${esc3(input.logoDataUri)}" alt="${esc3(agency ?? "")}">` : ""}${agency ? `<span class="agency">${esc3(agency)}</span>` : ""}<span class="for">Prepared for<br><b>${esc3(input.accountName)}</b></span></div>` : ""}
|
|
27343
28406
|
<p class="eyebrow">What is live in your account</p>
|
|
27344
|
-
<h1>${
|
|
27345
|
-
<p class="stamp">${
|
|
27346
|
-
<div class="verdict"><b>${
|
|
28407
|
+
<h1>${esc3(input.accountName)}</h1>
|
|
28408
|
+
<p class="stamp">${esc3(revLine)}</p>
|
|
28409
|
+
<div class="verdict"><b>${esc3(headline3)}</b><span>${esc3(sub)}</span></div>
|
|
27347
28410
|
<p class="living">This page checks itself. Every time your account is re-checked, this page is rewritten at the same address, so what you see here is what is true today. Bookmark it and come back whenever you want to know where things stand.</p>
|
|
27348
28411
|
<div class="tiles">
|
|
27349
28412
|
<div class="tile ok"><div class="n">${live.length}</div><div class="l">Live now</div></div>
|
|
@@ -27354,8 +28417,8 @@ ${latestHtml}
|
|
|
27354
28417
|
${section2("live", "Live in your account", "Each of these was read back from your account, not assumed.", live)}
|
|
27355
28418
|
${section2("soon", "Built, not confirmed yet", "The work is done. These are the ones we have not been able to confirm for you yet, and why.", notYet)}
|
|
27356
28419
|
${section2("waiting", "Waiting on someone else", "Outside our hands. Here is who sorts each one, and roughly when.", waiting)}
|
|
27357
|
-
${next.length ? `<section id="next"><h2>What happens next</h2><ol>${next.map((n) => `<li>${
|
|
27358
|
-
<div class="foot"><span>${
|
|
28420
|
+
${next.length ? `<section id="next"><h2>What happens next</h2><ol>${next.map((n) => `<li>${esc3(n)}</li>`).join("")}</ol></section>` : ""}
|
|
28421
|
+
<div class="foot"><span>${esc3(footLeft)}</span>${input.contact ? `<span>Questions? ${input.contact.href ? `<a href="${esc3(input.contact.href)}">${esc3(input.contact.label)}</a>` : esc3(input.contact.label)}</span>` : ""}</div>
|
|
27359
28422
|
</div></div>
|
|
27360
28423
|
<script>
|
|
27361
28424
|
(function(){
|
|
@@ -27382,7 +28445,7 @@ ${next.length ? `<section id="next"><h2>What happens next</h2><ol>${next.map((n)
|
|
|
27382
28445
|
</script>
|
|
27383
28446
|
</body></html>`;
|
|
27384
28447
|
}
|
|
27385
|
-
var CLIENT_STATE_LABEL, CLIENT_TITLE_RULES, DE_JARGON, LEAK_RULES, NEXT_STEP_RULES, toHex,
|
|
28448
|
+
var CLIENT_STATE_LABEL, CLIENT_TITLE_RULES, DE_JARGON, LEAK_RULES, NEXT_STEP_RULES, toHex, esc3, cssStr;
|
|
27386
28449
|
var init_client_status = __esm({
|
|
27387
28450
|
"src/command-os/client-status.ts"() {
|
|
27388
28451
|
"use strict";
|
|
@@ -27444,7 +28507,7 @@ var init_client_status = __esm({
|
|
|
27444
28507
|
{ re: /handoff call|on your own|without help/i, step: "On the handover call, do one task in the account yourself. That is what confirms you can run it without us." }
|
|
27445
28508
|
];
|
|
27446
28509
|
toHex = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
27447
|
-
|
|
28510
|
+
esc3 = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
27448
28511
|
cssStr = (s) => String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\r\n]+/g, " ");
|
|
27449
28512
|
}
|
|
27450
28513
|
});
|
|
@@ -27958,11 +29021,11 @@ function shouldAutoGenerateReview(prevStages, nextStages, hasReview) {
|
|
|
27958
29021
|
function reviewDocumentHtml(review, opts) {
|
|
27959
29022
|
const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
|
|
27960
29023
|
const agency = opts.agencyName?.trim() || "";
|
|
27961
|
-
const
|
|
29024
|
+
const esc4 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
27962
29025
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
|
|
27963
|
-
const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${
|
|
29026
|
+
const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc4(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc4(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc4(g.fix)}</div>` : ""}</div>`;
|
|
27964
29027
|
const o = review.offer;
|
|
27965
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>${
|
|
29028
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc4(opts.clientName)} \u2014 Growth Blueprint</title><style>
|
|
27966
29029
|
@page{margin:0.6in}
|
|
27967
29030
|
*{box-sizing:border-box}
|
|
27968
29031
|
body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
|
|
@@ -27997,28 +29060,28 @@ function reviewDocumentHtml(review, opts) {
|
|
|
27997
29060
|
<div class="wrap">
|
|
27998
29061
|
<div class="cover">
|
|
27999
29062
|
<p class="eyebrow">Growth Blueprint</p>
|
|
28000
|
-
<h1>${
|
|
28001
|
-
<p class="sub">${agency ? `Prepared by ${
|
|
29063
|
+
<h1>${esc4(opts.clientName)}</h1>
|
|
29064
|
+
<p class="sub">${agency ? `Prepared by ${esc4(agency)} \xB7 ` : ""}${date}</p>
|
|
28002
29065
|
</div>
|
|
28003
|
-
${review.headline ? `<div class="lead">${
|
|
29066
|
+
${review.headline ? `<div class="lead">${esc4(review.headline)}</div>` : ""}
|
|
28004
29067
|
${review.ica ? `<div class="head">Who you're really selling to</div>
|
|
28005
|
-
<p>${
|
|
28006
|
-
${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${
|
|
28007
|
-
${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${
|
|
28008
|
-
${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${
|
|
28009
|
-
${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${
|
|
29068
|
+
<p>${esc4(review.ica.who)}</p>
|
|
29069
|
+
${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${esc4(review.ica.painPoints.join(" \xB7 "))}</p>` : ""}
|
|
29070
|
+
${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${esc4(review.ica.objections.join(" \xB7 "))}</p>` : ""}
|
|
29071
|
+
${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${esc4(review.ica.buyingTrigger)}</p>` : ""}
|
|
29072
|
+
${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${esc4(review.ica.whereTheyAre)}</p>` : ""}` : ""}
|
|
28010
29073
|
${o ? `<div class="head">Your offer, graded</div>
|
|
28011
29074
|
<div class="scorebar"><span class="scorenum">${o.score}/10</span><span class="track"><span class="fill" style="width:${Math.max(0, Math.min(100, o.score * 10))}%"></span></span></div>
|
|
28012
|
-
${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${
|
|
28013
|
-
${o.believability ? `<p class="kv"><b>Believable:</b> ${
|
|
28014
|
-
${o.speed ? `<p class="kv"><b>Speed to result:</b> ${
|
|
28015
|
-
${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${
|
|
28016
|
-
${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${
|
|
28017
|
-
${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${
|
|
29075
|
+
${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${esc4(o.dreamOutcome)}</p>` : ""}
|
|
29076
|
+
${o.believability ? `<p class="kv"><b>Believable:</b> ${esc4(o.believability)}</p>` : ""}
|
|
29077
|
+
${o.speed ? `<p class="kv"><b>Speed to result:</b> ${esc4(o.speed)}</p>` : ""}
|
|
29078
|
+
${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${esc4(o.effort)}</p>` : ""}
|
|
29079
|
+
${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc4(o.biggestLever)}</p>` : ""}
|
|
29080
|
+
${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc4(o.recommendedOffer)}</div>` : ""}` : ""}
|
|
28018
29081
|
${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
|
|
28019
|
-
${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${
|
|
28020
|
-
${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${
|
|
28021
|
-
<footer><span>${agency ?
|
|
29082
|
+
${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc4(b.item)}</b><span>${esc4(b.because)}</span></div>`).join("")}` : ""}
|
|
29083
|
+
${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc4(u)}</li>`).join("")}</ul></div>` : ""}
|
|
29084
|
+
<footer><span>${agency ? esc4(agency) : "Growth Blueprint"}</span><span>${esc4(opts.clientName)} \xB7 ${date}</span></footer>
|
|
28022
29085
|
</div></body></html>`;
|
|
28023
29086
|
}
|
|
28024
29087
|
var GAP_CHECKLIST;
|
|
@@ -30085,15 +31148,15 @@ init_credentials_store();
|
|
|
30085
31148
|
init_setup_tool();
|
|
30086
31149
|
|
|
30087
31150
|
// src/tools/skills.ts
|
|
30088
|
-
var
|
|
31151
|
+
var import_zod67 = require("zod");
|
|
30089
31152
|
|
|
30090
31153
|
// src/skill-installer.ts
|
|
30091
|
-
var
|
|
31154
|
+
var import_node_crypto4 = require("node:crypto");
|
|
30092
31155
|
var fs19 = __toESM(require("node:fs"));
|
|
30093
31156
|
var path18 = __toESM(require("node:path"));
|
|
30094
31157
|
var os4 = __toESM(require("node:os"));
|
|
30095
31158
|
function sha256(buf) {
|
|
30096
|
-
return (0,
|
|
31159
|
+
return (0, import_node_crypto4.createHash)("sha256").update(buf).digest("hex");
|
|
30097
31160
|
}
|
|
30098
31161
|
function bundledSkillsDir(baseDir) {
|
|
30099
31162
|
const candidate = path18.resolve(baseDir, "..", "skills");
|
|
@@ -30206,7 +31269,7 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
|
|
|
30206
31269
|
"install_skills",
|
|
30207
31270
|
"Install (or repair) the guided skills bundled with GHL Command \u2014 Blueprint (build a whole client account from one intake), Clone Site (clone and rebrand a live web page for a client, with a rights declaration and a pre-launch liability report), and GHL Reports (verified counts, lists, and weekly reports without token burn) \u2014 into your ~/.claude/skills/ so Claude can use them. Runs automatically on startup; call this to verify what is installed, or to re-install after deleting a skill. NEVER overwrites files you have edited (your version is kept and reported). Restart Claude after install for new skills to load.",
|
|
30208
31271
|
{
|
|
30209
|
-
targetDir:
|
|
31272
|
+
targetDir: import_zod67.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
|
|
30210
31273
|
},
|
|
30211
31274
|
async ({ targetDir }) => {
|
|
30212
31275
|
try {
|
|
@@ -30243,18 +31306,18 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
30243
31306
|
|
|
30244
31307
|
// src/agency-primer.ts
|
|
30245
31308
|
var PRIMER_MAX = 1200;
|
|
30246
|
-
var
|
|
31309
|
+
var money4 = (n, cur) => `${cur ? cur + " " : ""}${n.toLocaleString("en-US")}`;
|
|
30247
31310
|
function agencyPrimer(p) {
|
|
30248
31311
|
const lines = [];
|
|
30249
31312
|
const cur = p.pricing?.currency;
|
|
30250
31313
|
if (p.agencyName) {
|
|
30251
31314
|
lines.push(`You are working for ${p.agencyName}${p.legalName && p.legalName !== p.agencyName ? ` (registered as ${p.legalName})` : ""}. Use this name on anything client-facing.`);
|
|
30252
31315
|
}
|
|
30253
|
-
const offers = (p.offers ?? []).map((o) => o.priceFloor !== void 0 ? `${o.name} (not below ${
|
|
31316
|
+
const offers = (p.offers ?? []).map((o) => o.priceFloor !== void 0 ? `${o.name} (not below ${money4(o.priceFloor, cur)}${o.cadence === "monthly" ? " a month" : ""})` : o.name);
|
|
30254
31317
|
if (offers.length) lines.push(`What they sell: ${offers.join("; ")}.`);
|
|
30255
31318
|
const floors = [];
|
|
30256
|
-
if (p.pricing?.minMonthly !== void 0) floors.push(`${
|
|
30257
|
-
if (p.pricing?.minSetup !== void 0) floors.push(`${
|
|
31319
|
+
if (p.pricing?.minMonthly !== void 0) floors.push(`${money4(p.pricing.minMonthly, cur)} a month`);
|
|
31320
|
+
if (p.pricing?.minSetup !== void 0) floors.push(`${money4(p.pricing.minSetup, cur)} to set up`);
|
|
30258
31321
|
if (floors.length) lines.push(`Never quote below their floors: ${floors.join(", ")}. If asked to go lower, say so rather than doing it quietly.`);
|
|
30259
31322
|
if (p.sender?.fromName || p.sender?.replyTo) {
|
|
30260
31323
|
const who = [p.sender?.fromName, p.sender?.replyTo].filter(Boolean).join(", ");
|