@elitedcs/ghl-mcp 3.72.2 → 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/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, today) {
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 ?? today,
4844
- dueDate: args.dueDate ?? args.issueDate ?? today,
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 today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
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
- today
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 digits = "23456789";
5193
+ const digits2 = "23456789";
5163
5194
  const symbols = "!@#$%&*?";
5164
- const all = upper + lower + digits + symbols;
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(digits, bytes[2]), pick(symbols, bytes[3])];
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 footer = buildFooter({
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, footer];
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 [head, ...rest] = value.split("|");
11693
- const owner = head.trim();
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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> &mdash; " + 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) + "&times;</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 &mdash; 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 ? " &middot; " + 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 today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
15580
- return Math.round((target - today) / 864e5);
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 ${money(e.price, contract.currency)}${e.cadence === "monthly" ? " a month" : ""}, and the floor you set for that work is ${money(floor, contract.currency)}. Both numbers are yours; nothing here is estimated.`,
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 money;
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
- money = (n, currency) => currency ? `${currency} ${n.toLocaleString("en-US")}` : n.toLocaleString("en-US");
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 import_zod56.z.string().regex(new RegExp(`^${ns}\\.[a-z0-9]+(_[a-z0-9]+)*$`), `must be a ${ns}.* ref`);
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 import_zod56, 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;
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
- import_zod56 = require("zod");
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 = import_zod56.z.string().regex(REF_RE, "must be a <namespace>.<snake_case_slug> ref (no real GHL IDs)");
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 = import_zod56.z.union([userRefSchema, import_zod56.z.literal(USER_PENDING_REF)]);
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 = import_zod56.z.object({
17924
+ userSchema = import_zod57.z.object({
16869
17925
  ref: userRefSchema,
16870
- firstName: import_zod56.z.string().min(1),
16871
- lastName: import_zod56.z.string().min(1),
16872
- email: import_zod56.z.string().regex(EMAIL_RE, "must be an email address (GHL creates the login from it)"),
16873
- role: import_zod56.z.enum(USER_ROLES),
16874
- phone: import_zod56.z.string().optional()
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 = import_zod56.z.object({
17932
+ stageSchema = import_zod57.z.object({
16877
17933
  ref: nsRef("stage"),
16878
- name: import_zod56.z.string(),
16879
- position: import_zod56.z.number().int().nonnegative()
17934
+ name: import_zod57.z.string(),
17935
+ position: import_zod57.z.number().int().nonnegative()
16880
17936
  });
16881
- pipelineSchema = import_zod56.z.object({
17937
+ pipelineSchema = import_zod57.z.object({
16882
17938
  ref: nsRef("pipeline"),
16883
- name: import_zod56.z.string(),
16884
- stages: import_zod56.z.array(stageSchema).min(1)
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 = import_zod56.z.object({
17957
+ customFieldSchema = import_zod57.z.object({
16902
17958
  ref: nsRef("field"),
16903
- name: import_zod56.z.string(),
16904
- dataType: import_zod56.z.enum(GHL_FIELD_DATATYPES),
16905
- model: import_zod56.z.enum(["contact", "opportunity"]).optional(),
16906
- options: import_zod56.z.array(import_zod56.z.string()).optional()
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 = import_zod56.z.object({
17964
+ tagSchema = import_zod57.z.object({
16909
17965
  ref: nsRef("tag"),
16910
- name: import_zod56.z.string()
17966
+ name: import_zod57.z.string()
16911
17967
  });
16912
- customValueSchema = import_zod56.z.object({
17968
+ customValueSchema = import_zod57.z.object({
16913
17969
  ref: nsRef("cv"),
16914
- name: import_zod56.z.string(),
16915
- value: import_zod56.z.string().optional(),
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 = import_zod56.z.object({
16928
- daysOfTheWeek: import_zod56.z.array(import_zod56.z.number().int().min(0).max(6)),
16929
- hours: import_zod56.z.array(
16930
- import_zod56.z.object({
16931
- openHour: import_zod56.z.number().int().min(0).max(23),
16932
- openMinute: import_zod56.z.number().int().min(0).max(59),
16933
- closeHour: import_zod56.z.number().int().min(0).max(23),
16934
- closeMinute: import_zod56.z.number().int().min(0).max(59)
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 = import_zod56.z.object({
17995
+ calendarSchema = import_zod57.z.object({
16940
17996
  ref: nsRef("calendar"),
16941
- name: import_zod56.z.string(),
16942
- calendarType: import_zod56.z.enum(CALENDAR_TYPES),
16943
- openHours: import_zod56.z.array(openHoursBlockSchema).optional(),
16944
- availabilityType: import_zod56.z.number().int().optional(),
16945
- requiresStaff: import_zod56.z.boolean().optional(),
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: import_zod56.z.number().int().positive().optional(),
16950
- slotDurationUnit: import_zod56.z.enum(CALENDAR_SLOT_UNITS).optional(),
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: import_zod56.z.number().int().positive().optional(),
18008
+ slotInterval: import_zod57.z.number().int().positive().optional(),
16953
18009
  /** Minutes of buffer after each appointment. */
16954
- slotBuffer: import_zod56.z.number().int().min(0).optional(),
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: import_zod56.z.array(userRefSchema).optional()
18013
+ teamMemberRefs: import_zod57.z.array(userRefSchema).optional()
16958
18014
  });
16959
- formFieldSchema = import_zod56.z.discriminatedUnion("type", [
16960
- import_zod56.z.object({
16961
- type: import_zod56.z.literal("standard"),
16962
- key: import_zod56.z.string(),
16963
- required: import_zod56.z.boolean().optional()
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
- import_zod56.z.object({
16966
- type: import_zod56.z.literal("custom"),
18021
+ import_zod57.z.object({
18022
+ type: import_zod57.z.literal("custom"),
16967
18023
  fieldRef: nsRef("field"),
16968
- required: import_zod56.z.boolean().optional()
18024
+ required: import_zod57.z.boolean().optional()
16969
18025
  })
16970
18026
  ]);
16971
- formSchema = import_zod56.z.object({
18027
+ formSchema = import_zod57.z.object({
16972
18028
  ref: nsRef("form"),
16973
- name: import_zod56.z.string(),
16974
- fields: import_zod56.z.array(formFieldSchema)
18029
+ name: import_zod57.z.string(),
18030
+ fields: import_zod57.z.array(formFieldSchema)
16975
18031
  });
16976
- pageSchema = import_zod56.z.object({
18032
+ pageSchema = import_zod57.z.object({
16977
18033
  ref: nsRef("page"),
16978
- name: import_zod56.z.string(),
16979
- role: import_zod56.z.string().optional(),
16980
- outline: import_zod56.z.string().optional(),
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 = import_zod56.z.object({
18042
+ funnelSchema = import_zod57.z.object({
16987
18043
  ref: nsRef("funnel"),
16988
- name: import_zod56.z.string(),
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: import_zod56.z.enum(FUNNEL_TARGETS).optional(),
16995
- host: import_zod56.z.enum(FUNNEL_HOSTS).optional(),
18050
+ target: import_zod57.z.enum(FUNNEL_TARGETS).optional(),
18051
+ host: import_zod57.z.enum(FUNNEL_HOSTS).optional(),
16996
18052
  // external only
16997
- domain: import_zod56.z.string().optional(),
18053
+ domain: import_zod57.z.string().optional(),
16998
18054
  // external only
16999
- pages: import_zod56.z.array(pageSchema)
18055
+ pages: import_zod57.z.array(pageSchema)
17000
18056
  });
17001
- emailAssetSchema = import_zod56.z.object({
18057
+ emailAssetSchema = import_zod57.z.object({
17002
18058
  ref: nsRef("email"),
17003
- name: import_zod56.z.string(),
17004
- subject: import_zod56.z.string().optional(),
17005
- bodyOutline: import_zod56.z.string().optional(),
17006
- body: import_zod56.z.string().optional(),
17007
- mergeTags: import_zod56.z.array(import_zod56.z.string()).optional()
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 = import_zod56.z.object({
18065
+ smsAssetSchema = import_zod57.z.object({
17010
18066
  ref: nsRef("sms"),
17011
- name: import_zod56.z.string(),
17012
- bodyOutline: import_zod56.z.string().optional(),
17013
- body: import_zod56.z.string().optional(),
17014
- mergeTags: import_zod56.z.array(import_zod56.z.string()).optional()
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 = import_zod56.z.object({
18072
+ emailTemplateSchema = import_zod57.z.object({
17017
18073
  ref: nsRef("email_template"),
17018
- name: import_zod56.z.string().min(1),
17019
- subject: import_zod56.z.string().min(1),
17020
- html: import_zod56.z.string().min(1)
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 = import_zod56.z.object({
18078
+ smsTemplateSchema = import_zod57.z.object({
17023
18079
  ref: nsRef("sms_template"),
17024
- name: import_zod56.z.string().min(1),
17025
- body: import_zod56.z.string().min(1)
18080
+ name: import_zod57.z.string().min(1),
18081
+ body: import_zod57.z.string().min(1)
17026
18082
  });
17027
- templatesSchema = import_zod56.z.object({
17028
- emails: import_zod56.z.array(emailTemplateSchema).optional(),
17029
- sms: import_zod56.z.array(smsTemplateSchema).optional()
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 = import_zod56.z.enum(["minutes", "hours", "days"]);
18087
+ waitUnit = import_zod57.z.enum(["minutes", "hours", "days"]);
17032
18088
  branchActionOptions = [
17033
- import_zod56.z.object({ type: import_zod56.z.literal("add_contact_tag"), tagRef: nsRef("tag") }),
17034
- import_zod56.z.object({ type: import_zod56.z.literal("remove_contact_tag"), tagRef: nsRef("tag") }),
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
- import_zod56.z.object({
17039
- type: import_zod56.z.literal("send_email"),
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
- import_zod56.z.object({
17044
- type: import_zod56.z.literal("send_sms"),
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
- import_zod56.z.object({ type: import_zod56.z.literal("wait"), value: import_zod56.z.number().positive(), unit: waitUnit }),
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
- import_zod56.z.object({
17058
- type: import_zod56.z.literal("wait_appointment"),
17059
- value: import_zod56.z.number().int().positive(),
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
- import_zod56.z.object({
17067
- type: import_zod56.z.literal("internal_notification"),
17068
- to: import_zod56.z.string().optional(),
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: import_zod56.z.string(),
17071
- body: import_zod56.z.string()
18126
+ title: import_zod57.z.string(),
18127
+ body: import_zod57.z.string()
17072
18128
  }),
17073
- import_zod56.z.object({
17074
- type: import_zod56.z.literal("update_contact_field"),
18129
+ import_zod57.z.object({
18130
+ type: import_zod57.z.literal("update_contact_field"),
17075
18131
  fieldRef: nsRef("field"),
17076
- value: import_zod56.z.string()
18132
+ value: import_zod57.z.string()
17077
18133
  }),
17078
- import_zod56.z.object({ type: import_zod56.z.literal("add_notes"), body: import_zod56.z.string() }),
17079
- import_zod56.z.object({
17080
- type: import_zod56.z.literal("task_notification"),
17081
- title: import_zod56.z.string(),
17082
- body: import_zod56.z.string().optional(),
17083
- dueDate: import_zod56.z.string().optional(),
17084
- assignedTo: import_zod56.z.string().optional(),
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
- import_zod56.z.object({ type: import_zod56.z.literal("assign_user"), userRef: userRefOrPendingSchema }),
17092
- import_zod56.z.object({ type: import_zod56.z.literal("remove_from_workflow"), workflowRef: nsRef("workflow") }),
17093
- import_zod56.z.object({ type: import_zod56.z.literal("add_to_workflow"), workflowRef: nsRef("workflow") }),
17094
- import_zod56.z.object({
17095
- type: import_zod56.z.literal("create_opportunity"),
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: import_zod56.z.string().optional(),
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: import_zod56.z.string().optional()
18162
+ value: import_zod57.z.string().optional()
17107
18163
  }),
17108
- import_zod56.z.object({
17109
- type: import_zod56.z.literal("update_opportunity"),
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: import_zod56.z.string().optional()
18169
+ value: import_zod57.z.string().optional()
17114
18170
  }),
17115
- import_zod56.z.object({
17116
- type: import_zod56.z.literal("goal_event"),
17117
- goalCondition: import_zod56.z.string(),
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: import_zod56.z.enum(["exit", "continue", "wait"]).optional()
18175
+ action: import_zod57.z.enum(["exit", "continue", "wait"]).optional()
17120
18176
  })
17121
18177
  ];
17122
- branchActionSchema = import_zod56.z.discriminatedUnion("type", branchActionOptions);
17123
- findOpportunitySchema = import_zod56.z.object({
17124
- type: import_zod56.z.literal("find_opportunity"),
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: import_zod56.z.array(branchActionSchema).default([]),
17127
- notFound: import_zod56.z.array(branchActionSchema).default([])
18182
+ found: import_zod57.z.array(branchActionSchema).default([]),
18183
+ notFound: import_zod57.z.array(branchActionSchema).default([])
17128
18184
  });
17129
- actionSchema = import_zod56.z.discriminatedUnion("type", [...branchActionOptions, findOpportunitySchema]);
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 = import_zod56.z.object({
17134
- type: import_zod56.z.string(),
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: import_zod56.z.enum(APPOINTMENT_STATUSES).optional(),
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: import_zod56.z.enum(["positive", "negative"]).optional(),
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: import_zod56.z.array(import_zod56.z.enum(CALL_STATUSES)).optional(),
17162
- callDirection: import_zod56.z.enum(["inbound", "outbound"]).optional(),
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: import_zod56.z.array(import_zod56.z.enum(NUMBER_VALIDATION_STATES)).optional()
18224
+ numberValidation: import_zod57.z.array(import_zod57.z.enum(NUMBER_VALIDATION_STATES)).optional()
17169
18225
  });
17170
- workflowSchema = import_zod56.z.object({
18226
+ workflowSchema = import_zod57.z.object({
17171
18227
  ref: nsRef("workflow"),
17172
- name: import_zod56.z.string(),
18228
+ name: import_zod57.z.string(),
17173
18229
  trigger: triggerSchema.optional(),
17174
- stopOnResponse: import_zod56.z.boolean().optional(),
17175
- actions: import_zod56.z.array(actionSchema).max(40)
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 = import_zod56.z.object({
18239
+ handoffSchema = import_zod57.z.object({
17184
18240
  ref: nsRef("handoff"),
17185
- owner: import_zod56.z.enum(["OPERATOR-UI", "OPERATOR-EXT", "TEAM", "JERRY-UI", "JERRY-EXT", "SASHA"]).transform((o) => HANDOFF_OWNER_LEGACY[o] ?? o),
17186
- title: import_zod56.z.string(),
17187
- trigger: import_zod56.z.string().optional(),
17188
- instruction: import_zod56.z.string(),
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: import_zod56.z.string(),
17191
- blocks: import_zod56.z.array(import_zod56.z.string()).optional()
18246
+ successCheck: import_zod57.z.string(),
18247
+ blocks: import_zod57.z.array(import_zod57.z.string()).optional()
17192
18248
  });
17193
- buildPlanSchema = import_zod56.z.object({
17194
- schemaVersion: import_zod56.z.string(),
17195
- planId: import_zod56.z.string(),
17196
- briefId: import_zod56.z.string(),
17197
- preset: import_zod56.z.string(),
17198
- summary: import_zod56.z.string().optional(),
17199
- users: import_zod56.z.array(userSchema).optional(),
17200
- pipelines: import_zod56.z.array(pipelineSchema).optional(),
17201
- customFields: import_zod56.z.array(customFieldSchema).optional(),
17202
- tags: import_zod56.z.array(tagSchema).optional(),
17203
- customValues: import_zod56.z.array(customValueSchema).optional(),
17204
- calendars: import_zod56.z.array(calendarSchema).optional(),
17205
- forms: import_zod56.z.array(formSchema).optional(),
17206
- funnels: import_zod56.z.array(funnelSchema).optional(),
17207
- emails: import_zod56.z.array(emailAssetSchema).optional(),
17208
- sms: import_zod56.z.array(smsAssetSchema).optional(),
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: import_zod56.z.array(workflowSchema).optional(),
17211
- handoffs: import_zod56.z.array(handoffSchema).optional(),
17212
- buildOrder: import_zod56.z.array(import_zod56.z.string()).optional(),
17213
- idMap: import_zod56.z.record(import_zod56.z.string()).optional()
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 renderReport(plan, result, ctx) {
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"}`);
@@ -18523,7 +19579,7 @@ var init_executor = __esm({
18523
19579
  WAIT_UNIT_MAP = {
18524
19580
  minutes: "minutes",
18525
19581
  hours: "hour",
18526
- days: "day"
19582
+ days: "days"
18527
19583
  };
18528
19584
  PENDING = (ref) => `__PENDING__:${ref}`;
18529
19585
  isPending = (v) => v.startsWith("__PENDING__:");
@@ -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 import_zod57, RunnerCheckSchema, RunnerResultSchema;
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
- import_zod57 = require("zod");
19359
- RunnerCheckSchema = import_zod57.z.object({
19360
- name: import_zod57.z.string().min(1),
19361
- state: import_zod57.z.enum(["CHECKED", "DONE_UNCHECKED"]),
19362
- evidence: import_zod57.z.string().optional(),
19363
- reason: import_zod57.z.string().optional(),
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: import_zod57.z.string().optional()
20421
+ plain: import_zod58.z.string().optional()
19366
20422
  }).strict();
19367
- RunnerResultSchema = import_zod57.z.object({
19368
- ok: import_zod57.z.boolean(),
19369
- checks: import_zod57.z.array(RunnerCheckSchema).default([]),
19370
- error: import_zod57.z.string().optional()
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 loose = hits.length ? hits : roster.filter((r) => r.name?.toLowerCase().includes(q2.toLowerCase()));
19820
- if (loose.length === 1) return { locationId: loose[0].locationId, name: loose[0].name, onTheBoard: true };
19821
- if (loose.length > 1) return { error: `More than one client matches "${q2}": ${loose.map((r) => r.name).join(", ")}. Use the full name.` };
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: import_zod58.z.string().optional().describe("One client's sub-account id. Omit for the whole roster.") },
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 money3 = monthlyFloor(saved, profile);
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 = money3.unpriced ? ` ${money3.unpriced} active monthly client${money3.unpriced === 1 ? " has" : "s have"} no price on file and ${money3.unpriced === 1 ? "is" : "are"} not included.` : "";
19851
- const basis = money3.byCurrency ? `No single total exists: these clients are billed in ${money3.byCurrency.length} different currencies, so they are listed separately rather than added together.${missing}` : money3.unpriced ? `A floor, not a total: ${money3.counted} monthly client${money3.counted === 1 ? "" : "s"} with a price on file.${missing}` : `${money3.counted} active monthly client${money3.counted === 1 ? "" : "s"}, all with a price on file.`;
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: money3.amount,
19856
- currency: money3.currency,
19857
- byCurrency: money3.byCurrency,
20911
+ amount: money5.amount,
20912
+ currency: money5.currency,
20913
+ byCurrency: money5.byCurrency,
19858
20914
  basis,
19859
- clientsCounted: money3.counted,
19860
- clientsWithoutAPrice: money3.unpriced
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: import_zod58.z.string().describe("The client's GHL sub-account id \u2014 the same id the board uses."),
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: import_zod58.z.string().nullable().optional().describe("How you refer to them, if it differs from the account name."),
19878
- offer: import_zod58.z.string().nullable().optional().describe("Which of your offers they are on. Matched against your agency profile so a typo is reported."),
19879
- price: import_zod58.z.number().min(0).nullable().optional().describe("What they actually pay \u2014 not your floor."),
19880
- cadence: import_zod58.z.enum(["one-time", "monthly"]).nullable().optional(),
19881
- currency: import_zod58.z.string().nullable().optional(),
19882
- startedOn: import_zod58.z.string().nullable().optional().describe("YYYY-MM-DD."),
19883
- renewsOn: import_zod58.z.string().nullable().optional().describe("YYYY-MM-DD."),
19884
- status: import_zod58.z.enum(["active", "paused", "ended"]).nullable().optional(),
19885
- owner: import_zod58.z.string().nullable().optional().describe("Which staff member runs this account."),
19886
- sops: import_zod58.z.array(import_zod58.z.string()).nullable().optional().describe("SOPs that apply to this client only. Leave empty to use your agency's."),
19887
- notes: import_zod58.z.string().nullable().optional(),
19888
- remove: import_zod58.z.boolean().optional().describe("Delete this client's record entirely.")
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: import_zod58.z.string().describe("The client's name as you refer to them, or their sub-account id.") },
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 import_zod58;
20988
+ var import_zod59;
19933
20989
  var init_client_engagements2 = __esm({
19934
20990
  "src/tools/client-engagements.ts"() {
19935
20991
  "use strict";
19936
- import_zod58 = require("zod");
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 digits = (word.match(/\d/g) ?? []).length;
21058
+ const digits2 = (word.match(/\d/g) ?? []).length;
20003
21059
  const letters = (word.match(/[A-Za-z]/g) ?? []).length;
20004
- if (digits < 2 || letters < 2) return false;
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 money3 = (b.money.dollarsPerMonth ?? -1) - (a.money.dollarsPerMonth ?? -1);
20013
- if (money3 !== 0) return money3;
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: import_zod59.z.record(import_zod59.z.unknown()).describe("The full JSON result returned by audit_workflows."),
20397
- audience: import_zod59.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."),
20398
- accountName: import_zod59.z.string().describe("The client's name as they should see it on the document."),
20399
- locationId: import_zod59.z.string().optional().describe("Sub-account id. Recorded on the document, never shown in the body."),
20400
- leadsPerMonth: import_zod59.z.number().min(0).optional().describe("Leads this account actually receives per month, if known. Without it, findings are not costed in dollars."),
20401
- closeRate: import_zod59.z.number().min(0).max(1).optional().describe("Share of reached leads that become clients, 0-1. Never guessed."),
20402
- monthlyValuePerClient: import_zod59.z.number().min(0).optional().describe("What one new client is worth per month. Defaults to your agency profile's minimum monthly fee."),
20403
- generatedAt: import_zod59.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
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: import_zod59.z.record(import_zod59.z.unknown()).describe("The verified observations payload from the site read: site, pagesRead, observations, limits. Passed through as-is."),
20462
- businessName: import_zod59.z.string().describe("The business's name as it should appear on the document."),
20463
- leadsPerMonth: import_zod59.z.number().min(0).optional().describe("THEIR leads per month, if they told you. Never your own figure."),
20464
- closeRate: import_zod59.z.number().min(0).max(1).optional().describe("THEIR close rate, 0-1, if they told you. Never guessed."),
20465
- monthlyValuePerClient: import_zod59.z.number().min(0).optional().describe("What one client is worth to THEM per month, if they told you."),
20466
- generatedAt: import_zod59.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
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 import_zod59;
21581
+ var import_zod60;
20526
21582
  var init_audit_report2 = __esm({
20527
21583
  "src/tools/audit-report.ts"() {
20528
21584
  "use strict";
20529
- import_zod59 = require("zod");
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 today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
20547
- return Math.round((target - today) / 864e5);
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 today = [...live?.appointmentsToday ?? []].sort((a, b) => {
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 && !today.length && !waitingOnYou.length && !runningNow.length && !neverStarted.length && !renewals.length && !underFloor.length && !newLeads.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 (today.length === 1) {
20605
- headline3 = `You have one call today \u2014 ${today[0].who ?? today[0].client} at ${today[0].at}.`;
20606
- } else if (today.length > 1) {
20607
- headline3 = today[0].startsAt ? `You have ${today.length} calls today, the first at ${today[0].at}.` : `You have ${today.length} calls today.`;
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 today = dateIn(now, operatorTimezone2);
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) !== today) continue;
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: import_zod60.z.boolean().optional().describe("Read today's calendars and lead counts from the connected accounts. Default true. false answers from local records alone."),
20898
- hoursBack: import_zod60.z.number().min(1).max(72).optional().describe("Count new leads over this many hours instead of since midnight.")
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 import_zod60;
21988
+ var import_zod61;
20933
21989
  var init_daily_brief2 = __esm({
20934
21990
  "src/tools/daily-brief.ts"() {
20935
21991
  "use strict";
20936
- import_zod60 = require("zod");
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: import_zod61.z.string().optional().describe(
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: import_zod61.z.string().describe("The snapshot id to share (from list_snapshots)."),
21015
- share_type: import_zod61.z.enum(SHARE_TYPES).describe(
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: import_zod61.z.string().optional().describe(
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 import_zod61, SnapshotSchema, SnapshotsResponseSchema, ShareLinkResponseSchema, SHARE_TYPES;
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
- import_zod61 = require("zod");
22106
+ import_zod62 = require("zod");
21051
22107
  init_ghl_client();
21052
22108
  init_tool_helpers();
21053
- SnapshotSchema = import_zod61.z.object({ id: import_zod61.z.string(), name: import_zod61.z.string(), type: import_zod61.z.string() }).passthrough();
21054
- SnapshotsResponseSchema = import_zod61.z.object({ snapshots: import_zod61.z.array(SnapshotSchema) }).passthrough();
21055
- ShareLinkResponseSchema = import_zod61.z.object({ id: import_zod61.z.string(), shareLink: import_zod61.z.string() }).passthrough();
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: import_zod62.z.string().optional().describe("Defaults to the active location.")
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: import_zod62.z.string().optional().describe("Defaults to the active location.")
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 import_zod62, PhoneNumberSchema, NumbersResponseSchema, PoolsResponseSchema;
22157
+ var import_zod63, PhoneNumberSchema, NumbersResponseSchema, PoolsResponseSchema;
21102
22158
  var init_phone = __esm({
21103
22159
  "src/tools/phone.ts"() {
21104
22160
  "use strict";
21105
- import_zod62 = require("zod");
22161
+ import_zod63 = require("zod");
21106
22162
  init_tool_helpers();
21107
- PhoneNumberSchema = import_zod62.z.object({ sid: import_zod62.z.string(), value: import_zod62.z.string(), title: import_zod62.z.string().optional() }).passthrough();
21108
- NumbersResponseSchema = import_zod62.z.object({ phoneNumbers: import_zod62.z.array(PhoneNumberSchema) }).passthrough();
21109
- PoolsResponseSchema = import_zod62.z.object({ pools: import_zod62.z.array(import_zod62.z.object({}).passthrough()) }).passthrough();
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: import_zod63.z.string().optional().describe("Defaults to the active location."),
21128
- windowDays: import_zod63.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
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 import_zod63, MetaTotalSchema, TotalSchema2, NumbersSchema, OPP_STATUSES;
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
- import_zod63 = require("zod");
22252
+ import_zod64 = require("zod");
21197
22253
  init_tool_helpers();
21198
22254
  init_contact_window();
21199
- MetaTotalSchema = import_zod63.z.object({ meta: import_zod63.z.object({ total: import_zod63.z.number() }).passthrough() }).passthrough();
21200
- TotalSchema2 = import_zod63.z.object({ total: import_zod63.z.number() }).passthrough();
21201
- NumbersSchema = import_zod63.z.object({ phoneNumbers: import_zod63.z.array(import_zod63.z.unknown()) }).passthrough();
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
  });
@@ -21493,6 +22549,36 @@ var init_plan_form = __esm({
21493
22549
  }
21494
22550
  });
21495
22551
 
22552
+ // src/intake-to-build/publish-report.ts
22553
+ function publishNote(r) {
22554
+ if (r.publishFailed.length) {
22555
+ const lead = r.published.length ? `Auto-published ${r.published.length} workflow(s) live, but ` : "";
22556
+ const detail = r.publishFailed.map((p) => `${p.name} (${p.error})`).join("; ");
22557
+ return `${lead}${r.publishFailed.length} workflow(s) were BUILT BUT COULD NOT BE PUBLISHED. They exist in GHL as DRAFT and are NOT running: ${detail}. You already opted in, so re-running with publishWorkflows will not help. If GHL rejected the workflow's contents, publishing by hand will hit the SAME error until the cause is fixed. Read the error above, fix it, then publish (see nextManualSteps).${r.published.length ? " Any gated workflow stays DRAFT until its handoff is met." : ""}`;
22558
+ }
22559
+ if (r.published.length) {
22560
+ return `Auto-published ${r.published.length} workflow(s) live (you opted in). Any gated workflow stays DRAFT until its handoff is met.`;
22561
+ }
22562
+ if (r.anyWorkflowCreated) {
22563
+ return "Workflows were built as DRAFT (the safe default). Review them in GHL and publish, or re-run with publishWorkflows:true to auto-publish ungated ones.";
22564
+ }
22565
+ return "No workflows were built in this run (none in the plan, or all already existed and were left untouched).";
22566
+ }
22567
+ function publishSummaryClause(r) {
22568
+ if (r.publishFailed.length) {
22569
+ const also = r.published.length ? ` (${r.published.length} did publish)` : "";
22570
+ return ` ${r.publishFailed.length} workflow(s) BUILT BUT NOT PUBLISHED${also}. They are DRAFT and not running, see publishNote.`;
22571
+ }
22572
+ if (r.published.length) return ` Published ${r.published.length} workflow(s) live.`;
22573
+ if (r.anyWorkflowCreated) return " Workflows are DRAFT (opt in with publishWorkflows to auto-publish).";
22574
+ return "";
22575
+ }
22576
+ var init_publish_report = __esm({
22577
+ "src/intake-to-build/publish-report.ts"() {
22578
+ "use strict";
22579
+ }
22580
+ });
22581
+
21496
22582
  // src/intake-to-build/customization.ts
21497
22583
  function industryPack(slug3) {
21498
22584
  if (!slug3) return void 0;
@@ -21724,11 +22810,11 @@ function validateBrief(input) {
21724
22810
  warnings: []
21725
22811
  };
21726
22812
  }
21727
- var import_zod64, BRIEF_SCHEMA_VERSION, PRESETS, presetSchema, BRIEF_SOURCES, briefSourceSchema, pricePointSchema, staffMemberSchema, BRIEF_CALENDAR_TYPES, briefCalendarSchema, TRI_STATES, briefSchema;
22813
+ var import_zod65, BRIEF_SCHEMA_VERSION, PRESETS, presetSchema, BRIEF_SOURCES, briefSourceSchema, pricePointSchema, staffMemberSchema, BRIEF_CALENDAR_TYPES, briefCalendarSchema, TRI_STATES, briefSchema;
21728
22814
  var init_brief = __esm({
21729
22815
  "src/intake-to-build/brief.ts"() {
21730
22816
  "use strict";
21731
- import_zod64 = require("zod");
22817
+ import_zod65 = require("zod");
21732
22818
  BRIEF_SCHEMA_VERSION = "0.1";
21733
22819
  PRESETS = [
21734
22820
  "generic",
@@ -21738,105 +22824,105 @@ var init_brief = __esm({
21738
22824
  "ecom",
21739
22825
  "agency"
21740
22826
  ];
21741
- presetSchema = import_zod64.z.enum(PRESETS);
22827
+ presetSchema = import_zod65.z.enum(PRESETS);
21742
22828
  BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
21743
- briefSourceSchema = import_zod64.z.enum(BRIEF_SOURCES);
21744
- pricePointSchema = import_zod64.z.object({
21745
- name: import_zod64.z.string(),
21746
- price: import_zod64.z.string()
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()
21747
22833
  });
21748
- staffMemberSchema = import_zod64.z.object({
21749
- name: import_zod64.z.string().min(1),
21750
- email: import_zod64.z.string().min(1),
22834
+ staffMemberSchema = import_zod65.z.object({
22835
+ name: import_zod65.z.string().min(1),
22836
+ email: import_zod65.z.string().min(1),
21751
22837
  /** Job title as the client wrote it ("Front desk", "Provider"); omitted when
21752
22838
  * the line had none. The plan decides admin/user from it. */
21753
- role: import_zod64.z.string().optional(),
21754
- mobile: import_zod64.z.string().optional()
22839
+ role: import_zod65.z.string().optional(),
22840
+ mobile: import_zod65.z.string().optional()
21755
22841
  });
21756
22842
  BRIEF_CALENDAR_TYPES = ["one_on_one", "round_robin", "class"];
21757
- briefCalendarSchema = import_zod64.z.object({
21758
- name: import_zod64.z.string().min(1),
21759
- type: import_zod64.z.enum(BRIEF_CALENDAR_TYPES),
21760
- staffNames: import_zod64.z.array(import_zod64.z.string()),
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()),
21761
22847
  /** Appointment length in minutes, when the client said one ("15 minutes").
21762
22848
  * Lands on the plan calendar as slotDuration (finding 25). */
21763
- durationMinutes: import_zod64.z.number().int().positive().optional()
22849
+ durationMinutes: import_zod65.z.number().int().positive().optional()
21764
22850
  });
21765
22851
  TRI_STATES = ["yes", "no", "unsure"];
21766
- briefSchema = import_zod64.z.object({
21767
- schemaVersion: import_zod64.z.string(),
21768
- briefId: import_zod64.z.string(),
22852
+ briefSchema = import_zod65.z.object({
22853
+ schemaVersion: import_zod65.z.string(),
22854
+ briefId: import_zod65.z.string(),
21769
22855
  preset: presetSchema,
21770
22856
  briefSource: briefSourceSchema,
21771
22857
  /** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
21772
- extended: import_zod64.z.record(import_zod64.z.unknown()).optional(),
21773
- business: import_zod64.z.object({
21774
- name: import_zod64.z.string(),
21775
- type: import_zod64.z.string().optional(),
21776
- website: import_zod64.z.string().optional(),
21777
- location: import_zod64.z.string().optional(),
21778
- timezone: import_zod64.z.string().optional(),
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(),
21779
22865
  // Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
21780
22866
  // the same tolerance reason as business.type (don't reject valid briefs).
21781
- teamSize: import_zod64.z.string().optional(),
21782
- monthlyLeadVolume: import_zod64.z.string().optional(),
21783
- hours: import_zod64.z.string().optional()
22867
+ teamSize: import_zod65.z.string().optional(),
22868
+ monthlyLeadVolume: import_zod65.z.string().optional(),
22869
+ hours: import_zod65.z.string().optional()
21784
22870
  }).passthrough(),
21785
- offer: import_zod64.z.object({
21786
- summary: import_zod64.z.string().optional(),
22871
+ offer: import_zod65.z.object({
22872
+ summary: import_zod65.z.string().optional(),
21787
22873
  // Parsed best-effort; tolerate a raw string when parsing was not possible.
21788
- pricePoints: import_zod64.z.union([import_zod64.z.array(pricePointSchema), import_zod64.z.string()]).optional(),
21789
- leadMagnet: import_zod64.z.string().optional(),
21790
- avgDealValue: import_zod64.z.string().optional()
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()
21791
22877
  }).passthrough().optional(),
21792
- audience: import_zod64.z.object({
21793
- ideal: import_zod64.z.string().optional(),
21794
- painPoints: import_zod64.z.array(import_zod64.z.string()).optional(),
21795
- objections: import_zod64.z.array(import_zod64.z.string()).optional()
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()
21796
22882
  }).passthrough().optional(),
21797
- goal: import_zod64.z.object({
22883
+ goal: import_zod65.z.object({
21798
22884
  // Kept as string: the form option labels are the canonical values, but
21799
22885
  // the contract sample shortens some (e.g. "high-touch"). See §7 note.
21800
- primary: import_zod64.z.string(),
21801
- salesStages: import_zod64.z.array(import_zod64.z.string()).optional(),
21802
- bookingNeeded: import_zod64.z.boolean().optional(),
21803
- followUpStyle: import_zod64.z.string().optional()
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()
21804
22890
  }).passthrough(),
21805
- channels: import_zod64.z.object({
21806
- email: import_zod64.z.boolean().optional(),
21807
- sms: import_zod64.z.boolean().optional(),
21808
- a2pStatus: import_zod64.z.string().optional(),
21809
- payment: import_zod64.z.string().optional(),
21810
- calendarConnected: import_zod64.z.boolean().optional(),
21811
- social: import_zod64.z.array(import_zod64.z.string()).optional(),
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(),
21812
22898
  /** "Do you already have a phone number in GoHighLevel?" */
21813
- hasPhoneNumber: import_zod64.z.enum(TRI_STATES).optional()
22899
+ hasPhoneNumber: import_zod65.z.enum(TRI_STATES).optional()
21814
22900
  }).passthrough().optional(),
21815
22901
  /** Section G — who is on the account and who gets pinged. */
21816
- team: import_zod64.z.object({
21817
- staff: import_zod64.z.array(staffMemberSchema).optional(),
22902
+ team: import_zod65.z.object({
22903
+ staff: import_zod65.z.array(staffMemberSchema).optional(),
21818
22904
  /** Who is notified about new leads: a staff name, or "the owner". */
21819
- notifyName: import_zod64.z.string().optional(),
22905
+ notifyName: import_zod65.z.string().optional(),
21820
22906
  /** Who takes booking / follow-up calls: a staff name, or "the owner". */
21821
- callsName: import_zod64.z.string().optional()
22907
+ callsName: import_zod65.z.string().optional()
21822
22908
  }).passthrough().optional(),
21823
22909
  /** Section H — every booking calendar the client asked for. */
21824
- calendars: import_zod64.z.array(briefCalendarSchema).optional(),
22910
+ calendars: import_zod65.z.array(briefCalendarSchema).optional(),
21825
22911
  /** Section I — copy inputs, verbatim. */
21826
- voice: import_zod64.z.object({
21827
- threeWords: import_zod64.z.string().optional(),
21828
- signatureLine: import_zod64.z.string().optional()
22912
+ voice: import_zod65.z.object({
22913
+ threeWords: import_zod65.z.string().optional(),
22914
+ signatureLine: import_zod65.z.string().optional()
21829
22915
  }).passthrough().optional(),
21830
- assets: import_zod64.z.object({
21831
- existingPipeline: import_zod64.z.string().optional(),
21832
- existingWorkflows: import_zod64.z.string().optional(),
21833
- brand: import_zod64.z.string().optional(),
21834
- notes: import_zod64.z.string().optional()
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()
21835
22921
  }).passthrough().optional(),
21836
- flags: import_zod64.z.array(import_zod64.z.string()).optional(),
22922
+ flags: import_zod65.z.array(import_zod65.z.string()).optional(),
21837
22923
  /** Parse-time notes from the normalizer (a staff line it could not read,
21838
22924
  * a calendar type it had to assume). Never fatal; surfaced by validate_brief. */
21839
- warnings: import_zod64.z.array(import_zod64.z.string()).optional()
22925
+ warnings: import_zod65.z.array(import_zod65.z.string()).optional()
21840
22926
  }).strict();
21841
22927
  }
21842
22928
  });
@@ -23424,6 +24510,7 @@ async function executeBackbone(plan, deps, opts = {}) {
23424
24510
  const built = [];
23425
24511
  const manual = [];
23426
24512
  const published = [];
24513
+ const publishFailed = [];
23427
24514
  const staff = [];
23428
24515
  const templates = { emails: [], sms: [] };
23429
24516
  let templatesUnverified = 0;
@@ -23447,6 +24534,7 @@ async function executeBackbone(plan, deps, opts = {}) {
23447
24534
  built,
23448
24535
  manual,
23449
24536
  published,
24537
+ publishFailed,
23450
24538
  halted: { atRef, type, reason },
23451
24539
  deferred,
23452
24540
  staff,
@@ -24124,6 +25212,7 @@ async function executeBackbone(plan, deps, opts = {}) {
24124
25212
  await deps.publishWorkflow(wfId);
24125
25213
  published.push(wf.ref);
24126
25214
  } catch (e) {
25215
+ publishFailed.push({ ref: wf.ref, name: wf.name, error: msg2(e) });
24127
25216
  manual.push({ ref: wf.ref, type: "workflow-publish", name: wf.name, reason: `[${wf.name}] built successfully but auto-publish failed (${msg2(e)}) \u2014 publish it manually in GHL.` });
24128
25217
  }
24129
25218
  }
@@ -24178,7 +25267,7 @@ async function executeBackbone(plan, deps, opts = {}) {
24178
25267
  }
24179
25268
  }
24180
25269
  }
24181
- return { ok: true, idMap, built, manual, published, deferred, staff, templates, templatesUnverified, templatesFormatNote, calendarPending, refreshed, missingSteps, recipients };
25270
+ return { ok: true, idMap, built, manual, published, publishFailed, deferred, staff, templates, templatesUnverified, templatesFormatNote, calendarPending, refreshed, missingSteps, recipients };
24182
25271
  }
24183
25272
  async function buildSimple(objects, type, list2, create, nameOf, pollForNew, idMap, built) {
24184
25273
  if (objects.length === 0) return null;
@@ -24922,7 +26011,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
24922
26011
  "get_intake_question_set",
24923
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.`,
24924
26013
  {
24925
- industry: import_zod65.z.string().optional().describe(`Industry pack slug to compose in (${INDUSTRY_PACKS.map((p) => p.slug).join(", ")}). Omit for the base set.`)
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.`)
24926
26015
  },
24927
26016
  async ({ industry }) => {
24928
26017
  const agency = readOverlay();
@@ -24953,7 +26042,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
24953
26042
  "validate_brief",
24954
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.",
24955
26044
  {
24956
- brief: import_zod65.z.record(import_zod65.z.unknown()).describe("The Brief object to validate.")
26045
+ brief: import_zod66.z.record(import_zod66.z.unknown()).describe("The Brief object to validate.")
24957
26046
  },
24958
26047
  async ({ brief }) => validateBrief(brief)
24959
26048
  );
@@ -24962,7 +26051,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
24962
26051
  "validate_build_plan",
24963
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}.",
24964
26053
  {
24965
- plan: import_zod65.z.record(import_zod65.z.unknown()).describe("The Build Plan object to validate.")
26054
+ plan: import_zod66.z.record(import_zod66.z.unknown()).describe("The Build Plan object to validate.")
24966
26055
  },
24967
26056
  async ({ plan }) => validateBuildPlan(plan)
24968
26057
  );
@@ -24970,25 +26059,25 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
24970
26059
  "apply_build_plan",
24971
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).',
24972
26061
  {
24973
- plan: import_zod65.z.record(import_zod65.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."),
24974
- fromPreset: import_zod65.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."),
24975
- brief: import_zod65.z.record(import_zod65.z.unknown()).optional().describe("fromPreset only: the \xA74 Brief (from normalize_submission_to_brief, or assembled from the cockpit intake answers)."),
24976
- preset: import_zod65.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)."),
24977
- copy: import_zod65.z.array(
24978
- import_zod65.z.object({
24979
- ref: import_zod65.z.string().describe("email_template.* or sms_template.* ref from the composed plan"),
24980
- subject: import_zod65.z.string().optional(),
24981
- html: import_zod65.z.string().optional(),
24982
- body: import_zod65.z.string().optional()
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()
24983
26072
  })
24984
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."),
24985
- useSavedPlan: import_zod65.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."),
24986
- replaceSavedPlan: import_zod65.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."),
24987
- mode: import_zod65.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
24988
- locationId: import_zod65.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
24989
- metHandoffs: import_zod65.z.array(import_zod65.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.'),
24990
- publishWorkflows: import_zod65.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
24991
- onConflict: import_zod65.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
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.")
24992
26081
  },
24993
26082
  async ({ plan: planArgIn, fromPreset, brief: briefArg, preset: presetArg, copy: copyArg, useSavedPlan, replaceSavedPlan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }) => {
24994
26083
  try {
@@ -25190,18 +26279,23 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
25190
26279
  templatesFormatNote: exec.templatesFormatNote,
25191
26280
  templatesNote: exec.templates.emails.length + exec.templates.sms.length ? `The plan's email and SMS copy was also saved as account-level templates (Marketing \u2192 Emails \u2192 Templates; Conversations \u2192 Templates) \u2014 the reusable version your team edits. Templates are created in the editor's own format (vibe-editor) so they open and edit in Marketing \u2192 Emails \u2192 Templates. Workflow steps carry the same copy inline; editing a template does not change a step already built.${exec.templatesUnverified ? ` ${exec.templatesUnverified} email template(s) were saved but their body was NOT seen in GHL's preview \u2014 see templates.emails[].warning; do not report them as written.` : exec.templates.emails.some((t) => t.status === "created") ? ` Every email template created this run had its body verified in GHL's preview.` : ""}` : void 0,
25192
26281
  published: exec.published,
25193
- publishNote: exec.published.length ? `Auto-published ${exec.published.length} workflow(s) live (you opted in). Any gated workflow stays DRAFT until its handoff is met.` : exec.built.some((b) => b.type === "workflow" && b.status === "created") ? "Workflows were built as DRAFT (the safe default). Review them in GHL and publish, or re-run with publishWorkflows:true to auto-publish ungated ones." : "No workflows were built in this run (none in the plan, or all already existed and were left untouched).",
26282
+ publishFailed: exec.publishFailed,
26283
+ publishNote: publishNote({
26284
+ published: exec.published,
26285
+ publishFailed: exec.publishFailed,
26286
+ anyWorkflowCreated: exec.built.some((b) => b.type === "workflow" && b.status === "created")
26287
+ }),
25194
26288
  externalWiring,
25195
26289
  externalWiringNote: externalWiring ? "For each target:\"external\" funnel: plug these into your self-hosted site form + lead bridge (templates/external-funnel/). custom formFields carry the VERIFIED GHL field id your form's `custom` object must send (never name-guess keys); add a triggerTag to start the speed-to-lead workflow; bookingUrl is the GHL calendar widget for the CTA. Anything in `unresolved` wasn't built yet \u2014 re-run after it is." : void 0,
25196
26290
  deferred: exec.deferred,
25197
26291
  deferredNote: `execute builds the WHOLE plan live: staff users (plan.users, via the agency key \u2014 bound by email when they already exist), CRM backbone (pipelines, custom fields, tags, custom values), calendars (with the plan's named team members), forms, funnels (GHL-built = funnel + named steps), account-level email/SMS templates (the plan's reusable copy), and workflows (DRAFT, with all steps incl. opportunity create/move; notification / task steps are NEVER dropped \u2014 with no staff member yet they ship as placeholders flagged staff_pending). Remaining manual steps are surfaced per item: pending staff (no agency key), staff-requiring calendars whose team members are pending (workflows that point at such a calendar are STILL built \u2014 trigger unpinned, flagged calendar_pending \u2014 never a halt), GHL funnel page content/design, EXTERNAL funnels (target:"external" \u2014 the user builds + hosts the site themselves and wires it back; not built here), workflow triggers other than contact_tag, and publishing the DRAFT workflows.`,
25198
26292
  nextManualSteps: [...execManualLines, ...manualLines, ...handoffLines],
25199
- summary: exec.ok ? `Built ${exec.built.filter((b) => b.status === "created").length} new object(s), bound ${exec.built.filter((b) => b.status === "existing" || b.status === "refreshed").length} existing${exec.refreshed.length ? ` (${exec.refreshed.length} workflow(s) refreshed in place \u2014 see refreshed[])` : ""}.${exec.staff.length ? ` Staff: ${exec.staff.filter((s) => s.status === "created").length} created, ${exec.staff.filter((s) => s.status === "existing").length} already in the account, ${exec.staff.filter((s) => s.status === "pending").length} pending.` : ""}${exec.templates.emails.length + exec.templates.sms.length ? ` Templates: ${exec.templates.emails.length} email (${exec.templates.emails.filter((t) => t.status === "created" && !t.warning).length} created with the body verified in the preview${exec.templatesUnverified ? `, ${exec.templatesUnverified} created but NOT verified \u2014 see templates.emails[].warning` : ""}${exec.templates.emails.filter((t) => t.status === "existing").length ? `, ${exec.templates.emails.filter((t) => t.status === "existing").length} already existed` : ""}${exec.templates.emails.filter((t) => t.status === "failed").length ? `, ${exec.templates.emails.filter((t) => t.status === "failed").length} failed` : ""}), ${exec.templates.sms.length} SMS.` : ""}${exec.published.length ? ` Published ${exec.published.length} workflow(s) live.` : exec.built.some((b) => b.type === "workflow" && b.status === "created") ? " Workflows are DRAFT (opt in with publishWorkflows to auto-publish)." : ""}${exec.manual.some((m) => m.type === "calendar") || exec.calendarPending.length ? ` Calendars: ${exec.manual.filter((m) => m.type === "calendar").length} deferred to manual; ${new Set(exec.calendarPending.filter((c) => c.status === "pending").map((c) => c.workflowRef)).size} workflow(s) built with the calendar unpinned${exec.calendarPending.some((c) => c.status === "calendar_ready") ? `, ${new Set(exec.calendarPending.filter((c) => c.status === "calendar_ready").map((c) => c.workflowRef)).size} ready to pin` : ""}.` : ""}${exec.manual.some((m) => m.type === "calendar_team") ? ` ${exec.manual.filter((m) => m.type === "calendar_team").length} calendar(s) were built WITHOUT the people the plan named \u2014 see nextManualSteps; do not report those calendars as staffed.` : ""}${exec.missingSteps.length ? ` NOT COMPLETE: ${exec.missingSteps.reduce((n, w) => n + w.steps.length, 0)} step(s) the plan asks for are still missing from ${exec.missingSteps.length} workflow(s) that already existed \u2014 add them by hand (see missingSteps).` : ""}${exec.manual.length ? ` ${exec.manual.length} item(s) need a manual step (see nextManualSteps).` : ""}${exec.deferred.length ? " Deferred: " + exec.deferred.map((d) => `${d.count} ${d.section}`).join(", ") + " (manual)." : ""}` : `HALTED at ${exec.halted?.atRef} (${exec.halted?.reason}). ${exec.built.length} object(s) were created before the halt \u2014 see idMap to resume or clean up. NEVER-CLOBBER means a re-run will bind those, not duplicate them.`
26293
+ summary: exec.ok ? `Built ${exec.built.filter((b) => b.status === "created").length} new object(s), bound ${exec.built.filter((b) => b.status === "existing" || b.status === "refreshed").length} existing${exec.refreshed.length ? ` (${exec.refreshed.length} workflow(s) refreshed in place \u2014 see refreshed[])` : ""}.${exec.staff.length ? ` Staff: ${exec.staff.filter((s) => s.status === "created").length} created, ${exec.staff.filter((s) => s.status === "existing").length} already in the account, ${exec.staff.filter((s) => s.status === "pending").length} pending.` : ""}${exec.templates.emails.length + exec.templates.sms.length ? ` Templates: ${exec.templates.emails.length} email (${exec.templates.emails.filter((t) => t.status === "created" && !t.warning).length} created with the body verified in the preview${exec.templatesUnverified ? `, ${exec.templatesUnverified} created but NOT verified \u2014 see templates.emails[].warning` : ""}${exec.templates.emails.filter((t) => t.status === "existing").length ? `, ${exec.templates.emails.filter((t) => t.status === "existing").length} already existed` : ""}${exec.templates.emails.filter((t) => t.status === "failed").length ? `, ${exec.templates.emails.filter((t) => t.status === "failed").length} failed` : ""}), ${exec.templates.sms.length} SMS.` : ""}${publishSummaryClause({ published: exec.published, publishFailed: exec.publishFailed, anyWorkflowCreated: exec.built.some((b) => b.type === "workflow" && b.status === "created") })}${exec.manual.some((m) => m.type === "calendar") || exec.calendarPending.length ? ` Calendars: ${exec.manual.filter((m) => m.type === "calendar").length} deferred to manual; ${new Set(exec.calendarPending.filter((c) => c.status === "pending").map((c) => c.workflowRef)).size} workflow(s) built with the calendar unpinned${exec.calendarPending.some((c) => c.status === "calendar_ready") ? `, ${new Set(exec.calendarPending.filter((c) => c.status === "calendar_ready").map((c) => c.workflowRef)).size} ready to pin` : ""}.` : ""}${exec.manual.some((m) => m.type === "calendar_team") ? ` ${exec.manual.filter((m) => m.type === "calendar_team").length} calendar(s) were built WITHOUT the people the plan named \u2014 see nextManualSteps; do not report those calendars as staffed.` : ""}${exec.missingSteps.length ? ` NOT COMPLETE: ${exec.missingSteps.reduce((n, w) => n + w.steps.length, 0)} step(s) the plan asks for are still missing from ${exec.missingSteps.length} workflow(s) that already existed \u2014 add them by hand (see missingSteps).` : ""}${exec.manual.length ? ` ${exec.manual.length} item(s) need a manual step (see nextManualSteps).` : ""}${exec.deferred.length ? " Deferred: " + exec.deferred.map((d) => `${d.count} ${d.section}`).join(", ") + " (manual)." : ""}` : `HALTED at ${exec.halted?.atRef} (${exec.halted?.reason}). ${exec.built.length} object(s) were created before the halt \u2014 see idMap to resume or clean up. NEVER-CLOBBER means a re-run will bind those, not duplicate them.`
25200
26294
  });
25201
26295
  }
25202
26296
  const collisions = result.items.filter((i) => i.status === "existing");
25203
26297
  const aborted = onConflict === "abort" && collisions.length > 0;
25204
- const report2 = renderReport(typedPlan, result, {
26298
+ const report2 = renderReport2(typedPlan, result, {
25205
26299
  mode: "dry_run",
25206
26300
  locationName,
25207
26301
  locationId: activeLocation,
@@ -25266,9 +26360,9 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
25266
26360
  "revert_build",
25267
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.`,
25268
26362
  {
25269
- runId: import_zod65.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.'),
25270
- confirm: import_zod65.z.literal("DELETE").optional().describe('Omit for a preview (no writes at all). Pass "DELETE" to actually remove the objects the preview listed.'),
25271
- locationId: import_zod65.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first).")
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).")
25272
26366
  },
25273
26367
  async ({ runId, confirm: confirm2, locationId: locationId2 }) => {
25274
26368
  try {
@@ -25446,10 +26540,10 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
25446
26540
  "install_intake_form",
25447
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.`,
25448
26542
  {
25449
- dryRun: import_zod65.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
25450
- formId: import_zod65.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
25451
- formName: import_zod65.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`),
25452
- industry: import_zod65.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.`)
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.`)
25453
26547
  },
25454
26548
  async ({ dryRun, formId, formName, industry }) => {
25455
26549
  try {
@@ -25552,10 +26646,10 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
25552
26646
  "normalize_submission_to_brief",
25553
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).',
25554
26648
  {
25555
- formId: import_zod65.z.string().describe("The intake form ID (from install_intake_form)."),
25556
- submissionId: import_zod65.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
25557
- fieldMap: import_zod65.z.record(import_zod65.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
25558
- preset: import_zod65.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
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.")
25559
26653
  },
25560
26654
  async ({ formId, submissionId, fieldMap, preset }) => {
25561
26655
  try {
@@ -25584,7 +26678,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
25584
26678
  const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
25585
26679
  resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
25586
26680
  }
25587
- const presetSchema2 = import_zod65.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
26681
+ const presetSchema2 = import_zod66.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
25588
26682
  const presetParsed = presetSchema2.safeParse(preset);
25589
26683
  const brief = normalizeSubmissionToBrief({
25590
26684
  others,
@@ -25605,17 +26699,18 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
25605
26699
  }
25606
26700
  );
25607
26701
  }
25608
- var import_zod65, customFieldItemSchema, sleep5;
26702
+ var import_zod66, customFieldItemSchema, sleep5;
25609
26703
  var init_intake_to_build = __esm({
25610
26704
  "src/tools/intake-to-build.ts"() {
25611
26705
  "use strict";
25612
- import_zod65 = require("zod");
26706
+ import_zod66 = require("zod");
25613
26707
  init_tool_helpers();
25614
26708
  init_form_builder();
25615
26709
  init_calendars();
25616
26710
  init_emails();
25617
26711
  init_user_provisioning();
25618
26712
  init_plan_form();
26713
+ init_publish_report();
25619
26714
  init_question_set();
25620
26715
  init_customization();
25621
26716
  init_intake_overlay();
@@ -25630,16 +26725,16 @@ var init_intake_to_build = __esm({
25630
26725
  init_plan_from_preset();
25631
26726
  init_executor();
25632
26727
  init_execute();
25633
- customFieldItemSchema = import_zod65.z.object({
25634
- id: import_zod65.z.string(),
25635
- name: import_zod65.z.string(),
25636
- fieldKey: import_zod65.z.string(),
25637
- dataType: import_zod65.z.string(),
25638
- model: import_zod65.z.string().optional(),
25639
- parentId: import_zod65.z.string().optional(),
25640
- position: import_zod65.z.number().optional(),
25641
- dateAdded: import_zod65.z.string().optional(),
25642
- picklistOptions: import_zod65.z.array(import_zod65.z.string()).optional()
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()
25643
26738
  }).passthrough();
25644
26739
  sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
25645
26740
  }
@@ -25666,6 +26761,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
25666
26761
  registerFunnelQaTools(wrap(FUNNEL_QA_MODULE), client, builderClient);
25667
26762
  registerUserGuideTools(wrap(USER_GUIDE_MODULE));
25668
26763
  registerAgencyProfileTools(wrap(AGENCY_PROFILE_MODULE));
26764
+ registerAssessmentTools(wrap(ASSESSMENT_MODULE), client);
25669
26765
  registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
25670
26766
  registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
25671
26767
  registerDailyBriefTools(wrap(DAILY_BRIEF_MODULE), registry2);
@@ -25714,7 +26810,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
25714
26810
  }
25715
26811
  return { registeredTools, gatedTools, planGatedTools };
25716
26812
  }
25717
- 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;
25718
26814
  var init_tools = __esm({
25719
26815
  "src/tools/index.ts"() {
25720
26816
  "use strict";
@@ -25767,6 +26863,7 @@ var init_tools = __esm({
25767
26863
  init_checkup();
25768
26864
  init_user_guide();
25769
26865
  init_agency_profile2();
26866
+ init_assessment();
25770
26867
  init_client_engagements2();
25771
26868
  init_audit_report2();
25772
26869
  init_daily_brief2();
@@ -25837,11 +26934,13 @@ var init_tools = __esm({
25837
26934
  FUNNEL_QA_MODULE = "funnel-qa";
25838
26935
  USER_GUIDE_MODULE = "user-guide";
25839
26936
  AGENCY_PROFILE_MODULE = "agency-profile";
26937
+ ASSESSMENT_MODULE = "assessment";
25840
26938
  CLIENT_ENGAGEMENTS_MODULE = "client-engagements";
25841
26939
  AUDIT_REPORT_MODULE = "audit-report";
25842
26940
  DAILY_BRIEF_MODULE = "daily-brief";
25843
26941
  CHECKUP_MODULE = "checkup";
25844
26942
  KNOWN_MODULES = /* @__PURE__ */ new Set([
26943
+ "assessment",
25845
26944
  ...publicApiTools.map(([, label]) => label),
25846
26945
  ...internalApiTools.map(([, label]) => label),
25847
26946
  FORM_BUILDER_MODULE,
@@ -25861,6 +26960,7 @@ var init_tools = __esm({
25861
26960
  ACCOUNT_EXPORT_MODULE
25862
26961
  ]);
25863
26962
  PUBLIC_API_MODULES = /* @__PURE__ */ new Set([
26963
+ "assessment",
25864
26964
  ...publicApiTools.map(([, label]) => label),
25865
26965
  USER_GUIDE_MODULE,
25866
26966
  // no client at all
@@ -26090,7 +27190,7 @@ function writeVerifiedBackup(configPath, originalBytes) {
26090
27190
  return backupPath;
26091
27191
  }
26092
27192
  function sha2562(bytes) {
26093
- return (0, import_node_crypto4.createHash)("sha256").update(bytes).digest("hex");
27193
+ return (0, import_node_crypto5.createHash)("sha256").update(bytes).digest("hex");
26094
27194
  }
26095
27195
  function baselineOf(bytes, stat) {
26096
27196
  return { size: stat.size, mtimeMs: stat.mtimeMs, hash: sha2562(bytes) };
@@ -26116,7 +27216,7 @@ function assertNotStale(configPath, baseline) {
26116
27216
  }
26117
27217
  async function atomicReplace(opts) {
26118
27218
  const dir = path19.dirname(opts.configPath);
26119
- const tempPath = path19.join(dir, `.${path19.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto4.randomBytes)(4).toString("hex")}`);
27219
+ const tempPath = path19.join(dir, `.${path19.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}`);
26120
27220
  const fd = fs20.openSync(tempPath, "w");
26121
27221
  try {
26122
27222
  fs20.writeFileSync(fd, opts.newBytes);
@@ -26257,14 +27357,14 @@ async function runInstall(argv, ioOverride) {
26257
27357
  return EXIT_ABORTED;
26258
27358
  }
26259
27359
  }
26260
- var fs20, os5, path19, import_node_crypto4, 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;
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;
26261
27361
  var init_config_installer = __esm({
26262
27362
  "src/config-installer.ts"() {
26263
27363
  "use strict";
26264
27364
  fs20 = __toESM(require("node:fs"));
26265
27365
  os5 = __toESM(require("node:os"));
26266
27366
  path19 = __toESM(require("node:path"));
26267
- import_node_crypto4 = require("node:crypto");
27367
+ import_node_crypto5 = require("node:crypto");
26268
27368
  import_node_util = require("node:util");
26269
27369
  import_json5 = __toESM(require("json5"));
26270
27370
  SERVER_KEY = "ghl";
@@ -26294,7 +27394,7 @@ var require_package = __commonJS({
26294
27394
  "package.json"(exports2, module2) {
26295
27395
  module2.exports = {
26296
27396
  name: "@elitedcs/ghl-mcp",
26297
- version: "3.72.2",
27397
+ version: "3.73.0",
26298
27398
  mcpName: "io.github.drjerryrelth/ghl-command",
26299
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.",
26300
27400
  main: "dist/index.js",
@@ -26304,6 +27404,7 @@ var require_package = __commonJS({
26304
27404
  files: [
26305
27405
  "dist/index.js",
26306
27406
  "dist/capture-helper.js",
27407
+ "dist/assessment.html",
26307
27408
  "templates/action-schemas.json",
26308
27409
  "templates/clinic-medspa.json",
26309
27410
  "templates/trigger-schemas.json",
@@ -26318,14 +27419,15 @@ var require_package = __commonJS({
26318
27419
  "!skills/blueprint/presets/clinic-launch-a2p.md"
26319
27420
  ],
26320
27421
  scripts: {
26321
- 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",
26322
27423
  setup: "node setup-wizard.mjs",
26323
27424
  catalogue: "node scripts/export-command-os-catalogue.mjs",
26324
27425
  start: "node dist/index.js",
26325
27426
  dev: "tsc --watch",
26326
27427
  test: "vitest run",
26327
27428
  "test:watch": "vitest",
26328
- prepublishOnly: "npm run build"
27429
+ prepublishOnly: "npm run build",
27430
+ "build:assessment": "node src/command-os/assessment/build-app.mjs"
26329
27431
  },
26330
27432
  keywords: [
26331
27433
  "mcp",
@@ -26788,21 +27890,21 @@ function agencyProfilePageHtml(p, clients = [], seatToken = "") {
26788
27890
  const floor = c.engagement ? floorFor(c.engagement, p) : void 0;
26789
27891
  const cur = e.currency || p.pricing?.currency || "";
26790
27892
  const recorded = Object.keys(e).filter((k) => k !== "updated_at").length > 0;
26791
- return ` <details class="cl" data-id="${esc(c.locationId)}"${recorded ? "" : ' data-empty="1"'}>
26792
- <summary><span class="cn">${esc(c.name)}</span> <span class="cs">${recorded ? esc([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 ${esc(fmt(floor, cur))}</span>` : ""}<span class="open">Open</span></summary>
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>
26793
27895
  <div class="row">
26794
- <div><label>Offer</label><input class="f" data-k="offer" value="${esc(e.offer)}" placeholder="${esc(p.offers?.[0]?.name || "Speed to Lead")}"></div>
26795
- <div><label>They pay</label><input class="f" data-k="price" inputmode="numeric" value="${esc(e.price)}"></div>
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>
26796
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>
26797
27899
  </div>
26798
27900
  <div class="row">
26799
- <div><label>Started</label><input class="f" data-k="startedOn" placeholder="2026-01-15" value="${esc(e.startedOn)}"></div>
26800
- <div><label>Renews</label><input class="f" data-k="renewsOn" placeholder="2027-01-15" value="${esc(e.renewsOn)}"></div>
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>
26801
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>
26802
- <div><label>Who runs it</label><input class="f" data-k="owner" value="${esc(e.owner)}" placeholder="${esc(p.staff?.[0]?.name || "")}"></div>
27904
+ <div><label>Who runs it</label><input class="f" data-k="owner" value="${esc2(e.owner)}" placeholder="${esc2(p.staff?.[0]?.name || "")}"></div>
26803
27905
  </div>
26804
27906
  <label>Anything specific to this client \u2014 one per line</label>
26805
- <textarea class="f" data-k="sops" placeholder="Call the owner before any send">${esc((e.sops ?? []).join("\n"))}</textarea>
27907
+ <textarea class="f" data-k="sops" placeholder="Call the owner before any send">${esc2((e.sops ?? []).join("\n"))}</textarea>
26806
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>
26807
27909
  </details>`;
26808
27910
  }).join("\n");
@@ -26816,7 +27918,7 @@ ${clientBlocks}
26816
27918
  <meta name="viewport" content="width=device-width,initial-scale=1">
26817
27919
  <title>Your agency \u2014 Command OS</title>
26818
27920
  <style>
26819
- :root{--bg:#faf9f7;--surface:#fff;--ink:#1a1a1a;--muted:#6b6b6b;--line:#e3e0da;--accent:${esc(p.accent || "#b8934a")}}
27921
+ :root{--bg:#faf9f7;--surface:#fff;--ink:#1a1a1a;--muted:#6b6b6b;--line:#e3e0da;--accent:${esc2(p.accent || "#b8934a")}}
26820
27922
  @media (prefers-color-scheme:dark){:root{--bg:#16151a;--surface:#1e1d23;--ink:#eceaf0;--muted:#a09daa;--line:#33313b}}
26821
27923
  html{font-size:22px}
26822
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}
@@ -26853,43 +27955,43 @@ ${clientBlocks}
26853
27955
  <form id="f">
26854
27956
  <fieldset><legend>Brand</legend>
26855
27957
  <label for="agencyName">Agency name, as clients see it</label>
26856
- <input id="agencyName" name="agencyName" value="${esc(p.agencyName)}" placeholder="Elite DCs">
27958
+ <input id="agencyName" name="agencyName" value="${esc2(p.agencyName)}" placeholder="Elite DCs">
26857
27959
  <div class="row">
26858
- <div><label for="accent">Brand colour</label><input type="color" id="accent" name="accent" value="${esc(p.accent || "#b8934a")}"></div>
26859
- <div><label for="website">Website</label><input id="website" name="website" value="${esc(p.website)}" placeholder="https://elitedcs.com"></div>
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>
26860
27962
  </div>
26861
27963
  <label for="logoUrl">Logo URL</label>
26862
- <input id="logoUrl" name="logoUrl" value="${esc(p.logoUrl)}" placeholder="https://elitedcs.com/logo.svg">
27964
+ <input id="logoUrl" name="logoUrl" value="${esc2(p.logoUrl)}" placeholder="https://elitedcs.com/logo.svg">
26863
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>
26864
27966
  <label for="legalName">Registered entity, if different</label>
26865
- <input id="legalName" name="legalName" value="${esc(p.legalName)}" placeholder="Elite DCS LLC \u2014 used on contracts and A2P">
27967
+ <input id="legalName" name="legalName" value="${esc2(p.legalName)}" placeholder="Elite DCS LLC \u2014 used on contracts and A2P">
26866
27968
  </fieldset>
26867
27969
 
26868
27970
  <fieldset><legend>Who messages come from</legend>
26869
27971
  <div class="row">
26870
- <div><label for="fromName">From name</label><input id="fromName" name="fromName" value="${esc(p.sender?.fromName)}" placeholder="Dr Jerry Relth"></div>
26871
- <div><label for="replyTo">Reply-to email</label><input id="replyTo" name="replyTo" value="${esc(p.sender?.replyTo)}"></div>
26872
- <div><label for="phone">Phone</label><input id="phone" name="phone" value="${esc(p.sender?.phone)}"></div>
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>
26873
27975
  </div>
26874
27976
  </fieldset>
26875
27977
 
26876
27978
  <fieldset><legend>What you sell</legend>
26877
27979
  <label for="offers">Offers \u2014 one per line</label>
26878
- <textarea id="offers" name="offers" placeholder="Speed to Lead \u2014 3000/mo&#10;Reactivation campaign \u2014 2500">${esc(offers)}</textarea>
27980
+ <textarea id="offers" name="offers" placeholder="Speed to Lead \u2014 3000/mo&#10;Reactivation campaign \u2014 2500">${esc2(offers)}</textarea>
26879
27981
  <p class="hint">Write them as "name \u2014 price". A price ending in /mo is recorded as monthly.</p>
26880
27982
  <div class="row">
26881
- <div><label for="minMonthly">Minimum monthly</label><input id="minMonthly" name="minMonthly" inputmode="numeric" value="${esc(p.pricing?.minMonthly)}"></div>
26882
- <div><label for="minSetup">Minimum setup fee</label><input id="minSetup" name="minSetup" inputmode="numeric" value="${esc(p.pricing?.minSetup)}"></div>
26883
- <div><label for="currency">Currency</label><input id="currency" name="currency" value="${esc(p.pricing?.currency || "USD")}"></div>
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>
26884
27986
  </div>
26885
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>
26886
27988
  </fieldset>
26887
27989
 
26888
27990
  <fieldset><legend>How you deliver</legend>
26889
27991
  <label for="sops">Delivery SOPs \u2014 one per line</label>
26890
- <textarea id="sops" name="sops" placeholder="Every build gets a rollback journal before go-live">${esc(sops)}</textarea>
27992
+ <textarea id="sops" name="sops" placeholder="Every build gets a rollback journal before go-live">${esc2(sops)}</textarea>
26891
27993
  <label for="staff">Staff \u2014 one per line, "name \xB7 role \xB7 email"</label>
26892
- <textarea id="staff" name="staff" placeholder="Dr Jerry Relth \xB7 owner \xB7 jerry@elitedcs.com">${esc(staff)}</textarea>
27994
+ <textarea id="staff" name="staff" placeholder="Dr Jerry Relth \xB7 owner \xB7 jerry@elitedcs.com">${esc2(staff)}</textarea>
26893
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>
26894
27996
  </fieldset>
26895
27997
 
@@ -26964,7 +28066,7 @@ document.querySelectorAll(".save-cl").forEach(btn=>btn.addEventListener("click",
26964
28066
  }));
26965
28067
  </script></body></html>`;
26966
28068
  }
26967
- var READ_NUMBER_JS, fmt, esc;
28069
+ var READ_NUMBER_JS, fmt, esc2;
26968
28070
  var init_agency_profile_page = __esm({
26969
28071
  "src/agency-profile-page.ts"() {
26970
28072
  "use strict";
@@ -26978,7 +28080,7 @@ var init_agency_profile_page = __esm({
26978
28080
  return Number.isFinite(n)?{value:n}:{bad:true};
26979
28081
  }`;
26980
28082
  fmt = (n, currency) => `${currency ? currency + " " : ""}${n.toLocaleString("en-US")}`;
26981
- esc = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
28083
+ esc2 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
26982
28084
  }
26983
28085
  });
26984
28086
 
@@ -26988,7 +28090,7 @@ function isHex(value) {
26988
28090
  }
26989
28091
  function readAgencyBranding(base = appDataDir()) {
26990
28092
  try {
26991
- const st = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(base, "cockpit-state.json"), "utf8"));
28093
+ const st = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(base, "cockpit-state.json"), "utf8"));
26992
28094
  const out = {};
26993
28095
  if (typeof st.agencyName === "string" && st.agencyName.trim()) out.agencyName = st.agencyName.trim();
26994
28096
  if (isHex(st.agencyAccent)) out.accent = st.agencyAccent;
@@ -27005,12 +28107,12 @@ function resolveBranding(overrides = {}, base) {
27005
28107
  const logoDataUri = overrides.logoDataUri ?? saved.logoDataUri;
27006
28108
  return { ...agencyName && { agencyName }, accent, ...logoDataUri && { logoDataUri } };
27007
28109
  }
27008
- var import_node_fs, import_node_path, DEFAULT_ACCENT;
28110
+ var import_node_fs2, import_node_path2, DEFAULT_ACCENT;
27009
28111
  var init_branding = __esm({
27010
28112
  "src/command-os/branding.ts"() {
27011
28113
  "use strict";
27012
- import_node_fs = require("node:fs");
27013
- import_node_path = require("node:path");
28114
+ import_node_fs2 = require("node:fs");
28115
+ import_node_path2 = require("node:path");
27014
28116
  init_credentials_store();
27015
28117
  DEFAULT_ACCENT = "#B8934A";
27016
28118
  }
@@ -27176,19 +28278,19 @@ function renderClientStatusHtml(input) {
27176
28278
  const stamp = friendlyStamp(input.checkedAt);
27177
28279
  const day = friendlyDate(input.checkedAt);
27178
28280
  const latest = scrubLatestCheck(input.latestCheck);
27179
- const latestHtml = latest ? `<section id="latest"><h2>What the latest check found</h2>${latest.summary ? `<p class="intro">${esc2(latest.summary)}</p>` : ""}${latest.issues?.length ? `<ul class="issues">${latest.issues.map((i) => `<li>${esc2(i)}</li>`).join("")}</ul>` : `<p class="intro">Nothing needed your attention this time.</p>`}</section>` : "";
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>` : "";
27180
28282
  const entry = (it) => {
27181
28283
  const cls = it.state === "in-place" ? "ok" : it.state === "waiting" ? "wait" : "soon";
27182
- const detail = it.state === "waiting" && (it.resolvedBy || it.roughly) ? `<p class="who"><b>Who sorts it:</b> ${esc2(scrubForClient(it.resolvedBy ?? "") || "not stated")}.${it.roughly ? ` <b>Roughly when:</b> ${esc2(scrubForClient(it.roughly))}.` : ""}</p>` : "";
27183
- const body = it.sentence ? `<p>${esc2(it.sentence)}</p>` : "";
27184
- return `<article class="item ${cls}"><span class="pill">${esc2(CLIENT_STATE_LABEL[it.state])}</span><h3>${esc2(it.title)}</h3>${body}${detail}</article>`;
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>`;
27185
28287
  };
27186
- const section2 = (id, title2, intro, list2) => list2.length ? `<section id="${id}"><h2>${esc2(title2)}</h2><p class="intro">${esc2(intro)}</p>${list2.map(entry).join("\n")}</section>` : "";
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>` : "";
27187
28289
  const footLeft = `${agency ? `Prepared by ${agency} for ` : "Prepared for "}${input.accountName} \xB7 last checked ${day}`;
27188
28290
  const revLine = input.revision && input.revision > 0 ? `Update ${input.revision} \xB7 last checked ${stamp}` : `Last checked ${stamp}`;
27189
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">
27190
- <title>${esc2(statusPageTitle(input))}</title>
27191
- <meta name="description" content="${esc2(`What is live in the ${input.accountName} account right now.`)}">
28292
+ <title>${esc3(statusPageTitle(input))}</title>
28293
+ <meta name="description" content="${esc3(`What is live in the ${input.accountName} account right now.`)}">
27192
28294
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
27193
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">
27194
28296
  <style>
@@ -27291,7 +28393,7 @@ function renderClientStatusHtml(input) {
27291
28393
  }
27292
28394
  </style></head><body>
27293
28395
  <div class="chrome"><div class="chrome-in">
27294
- <span class="who">${esc2(agency || input.accountName)}</span>
28396
+ <span class="who">${esc3(agency || input.accountName)}</span>
27295
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>
27296
28398
  <div class="tools">
27297
28399
  <button type="button" id="print">Print / save PDF</button>
@@ -27300,11 +28402,11 @@ function renderClientStatusHtml(input) {
27300
28402
  </div>
27301
28403
  </div></div>
27302
28404
  <div class="page"><div class="sheet">
27303
- ${agency || input.logoDataUri ? `<div class="brand">${input.logoDataUri ? `<img src="${esc2(input.logoDataUri)}" alt="${esc2(agency ?? "")}">` : ""}${agency ? `<span class="agency">${esc2(agency)}</span>` : ""}<span class="for">Prepared for<br><b>${esc2(input.accountName)}</b></span></div>` : ""}
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>` : ""}
27304
28406
  <p class="eyebrow">What is live in your account</p>
27305
- <h1>${esc2(input.accountName)}</h1>
27306
- <p class="stamp">${esc2(revLine)}</p>
27307
- <div class="verdict"><b>${esc2(headline3)}</b><span>${esc2(sub)}</span></div>
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>
27308
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>
27309
28411
  <div class="tiles">
27310
28412
  <div class="tile ok"><div class="n">${live.length}</div><div class="l">Live now</div></div>
@@ -27315,8 +28417,8 @@ ${latestHtml}
27315
28417
  ${section2("live", "Live in your account", "Each of these was read back from your account, not assumed.", live)}
27316
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)}
27317
28419
  ${section2("waiting", "Waiting on someone else", "Outside our hands. Here is who sorts each one, and roughly when.", waiting)}
27318
- ${next.length ? `<section id="next"><h2>What happens next</h2><ol>${next.map((n) => `<li>${esc2(n)}</li>`).join("")}</ol></section>` : ""}
27319
- <div class="foot"><span>${esc2(footLeft)}</span>${input.contact ? `<span>Questions? ${input.contact.href ? `<a href="${esc2(input.contact.href)}">${esc2(input.contact.label)}</a>` : esc2(input.contact.label)}</span>` : ""}</div>
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>
27320
28422
  </div></div>
27321
28423
  <script>
27322
28424
  (function(){
@@ -27343,7 +28445,7 @@ ${next.length ? `<section id="next"><h2>What happens next</h2><ol>${next.map((n)
27343
28445
  </script>
27344
28446
  </body></html>`;
27345
28447
  }
27346
- var CLIENT_STATE_LABEL, CLIENT_TITLE_RULES, DE_JARGON, LEAK_RULES, NEXT_STEP_RULES, toHex, esc2, cssStr;
28448
+ var CLIENT_STATE_LABEL, CLIENT_TITLE_RULES, DE_JARGON, LEAK_RULES, NEXT_STEP_RULES, toHex, esc3, cssStr;
27347
28449
  var init_client_status = __esm({
27348
28450
  "src/command-os/client-status.ts"() {
27349
28451
  "use strict";
@@ -27405,7 +28507,7 @@ var init_client_status = __esm({
27405
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." }
27406
28508
  ];
27407
28509
  toHex = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
27408
- esc2 = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
28510
+ esc3 = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
27409
28511
  cssStr = (s) => String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\r\n]+/g, " ");
27410
28512
  }
27411
28513
  });
@@ -27919,11 +29021,11 @@ function shouldAutoGenerateReview(prevStages, nextStages, hasReview) {
27919
29021
  function reviewDocumentHtml(review, opts) {
27920
29022
  const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
27921
29023
  const agency = opts.agencyName?.trim() || "";
27922
- const esc3 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
29024
+ const esc4 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
27923
29025
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
27924
- const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc3(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc3(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc3(g.fix)}</div>` : ""}</div>`;
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>`;
27925
29027
  const o = review.offer;
27926
- return `<!doctype html><html><head><meta charset="utf-8"><title>${esc3(opts.clientName)} \u2014 Growth Blueprint</title><style>
29028
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc4(opts.clientName)} \u2014 Growth Blueprint</title><style>
27927
29029
  @page{margin:0.6in}
27928
29030
  *{box-sizing:border-box}
27929
29031
  body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
@@ -27958,28 +29060,28 @@ function reviewDocumentHtml(review, opts) {
27958
29060
  <div class="wrap">
27959
29061
  <div class="cover">
27960
29062
  <p class="eyebrow">Growth Blueprint</p>
27961
- <h1>${esc3(opts.clientName)}</h1>
27962
- <p class="sub">${agency ? `Prepared by ${esc3(agency)} \xB7 ` : ""}${date}</p>
29063
+ <h1>${esc4(opts.clientName)}</h1>
29064
+ <p class="sub">${agency ? `Prepared by ${esc4(agency)} \xB7 ` : ""}${date}</p>
27963
29065
  </div>
27964
- ${review.headline ? `<div class="lead">${esc3(review.headline)}</div>` : ""}
29066
+ ${review.headline ? `<div class="lead">${esc4(review.headline)}</div>` : ""}
27965
29067
  ${review.ica ? `<div class="head">Who you're really selling to</div>
27966
- <p>${esc3(review.ica.who)}</p>
27967
- ${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${esc3(review.ica.painPoints.join(" \xB7 "))}</p>` : ""}
27968
- ${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${esc3(review.ica.objections.join(" \xB7 "))}</p>` : ""}
27969
- ${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${esc3(review.ica.buyingTrigger)}</p>` : ""}
27970
- ${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${esc3(review.ica.whereTheyAre)}</p>` : ""}` : ""}
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>` : ""}` : ""}
27971
29073
  ${o ? `<div class="head">Your offer, graded</div>
27972
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>
27973
- ${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${esc3(o.dreamOutcome)}</p>` : ""}
27974
- ${o.believability ? `<p class="kv"><b>Believable:</b> ${esc3(o.believability)}</p>` : ""}
27975
- ${o.speed ? `<p class="kv"><b>Speed to result:</b> ${esc3(o.speed)}</p>` : ""}
27976
- ${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${esc3(o.effort)}</p>` : ""}
27977
- ${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc3(o.biggestLever)}</p>` : ""}
27978
- ${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc3(o.recommendedOffer)}</div>` : ""}` : ""}
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>` : ""}` : ""}
27979
29081
  ${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
27980
- ${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc3(b.item)}</b><span>${esc3(b.because)}</span></div>`).join("")}` : ""}
27981
- ${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc3(u)}</li>`).join("")}</ul></div>` : ""}
27982
- <footer><span>${agency ? esc3(agency) : "Growth Blueprint"}</span><span>${esc3(opts.clientName)} \xB7 ${date}</span></footer>
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>
27983
29085
  </div></body></html>`;
27984
29086
  }
27985
29087
  var GAP_CHECKLIST;
@@ -30046,15 +31148,15 @@ init_credentials_store();
30046
31148
  init_setup_tool();
30047
31149
 
30048
31150
  // src/tools/skills.ts
30049
- var import_zod66 = require("zod");
31151
+ var import_zod67 = require("zod");
30050
31152
 
30051
31153
  // src/skill-installer.ts
30052
- var import_node_crypto3 = require("node:crypto");
31154
+ var import_node_crypto4 = require("node:crypto");
30053
31155
  var fs19 = __toESM(require("node:fs"));
30054
31156
  var path18 = __toESM(require("node:path"));
30055
31157
  var os4 = __toESM(require("node:os"));
30056
31158
  function sha256(buf) {
30057
- return (0, import_node_crypto3.createHash)("sha256").update(buf).digest("hex");
31159
+ return (0, import_node_crypto4.createHash)("sha256").update(buf).digest("hex");
30058
31160
  }
30059
31161
  function bundledSkillsDir(baseDir) {
30060
31162
  const candidate = path18.resolve(baseDir, "..", "skills");
@@ -30167,7 +31269,7 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
30167
31269
  "install_skills",
30168
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.",
30169
31271
  {
30170
- targetDir: import_zod66.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
31272
+ targetDir: import_zod67.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
30171
31273
  },
30172
31274
  async ({ targetDir }) => {
30173
31275
  try {
@@ -30204,18 +31306,18 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
30204
31306
 
30205
31307
  // src/agency-primer.ts
30206
31308
  var PRIMER_MAX = 1200;
30207
- var money2 = (n, cur) => `${cur ? cur + " " : ""}${n.toLocaleString("en-US")}`;
31309
+ var money4 = (n, cur) => `${cur ? cur + " " : ""}${n.toLocaleString("en-US")}`;
30208
31310
  function agencyPrimer(p) {
30209
31311
  const lines = [];
30210
31312
  const cur = p.pricing?.currency;
30211
31313
  if (p.agencyName) {
30212
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.`);
30213
31315
  }
30214
- const offers = (p.offers ?? []).map((o) => o.priceFloor !== void 0 ? `${o.name} (not below ${money2(o.priceFloor, cur)}${o.cadence === "monthly" ? " a month" : ""})` : o.name);
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);
30215
31317
  if (offers.length) lines.push(`What they sell: ${offers.join("; ")}.`);
30216
31318
  const floors = [];
30217
- if (p.pricing?.minMonthly !== void 0) floors.push(`${money2(p.pricing.minMonthly, cur)} a month`);
30218
- if (p.pricing?.minSetup !== void 0) floors.push(`${money2(p.pricing.minSetup, cur)} to set up`);
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`);
30219
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.`);
30220
31322
  if (p.sender?.fromName || p.sender?.replyTo) {
30221
31323
  const who = [p.sender?.fromName, p.sender?.replyTo].filter(Boolean).join(", ");