@elitedcs/ghl-mcp 3.83.7 → 3.83.8

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
@@ -545,7 +545,14 @@ var init_account_rules = __esm({
545
545
  });
546
546
 
547
547
  // src/ghl-client.ts
548
- var GHL_BASE_URL, GHL_API_VERSION, MAX_RETRIES, BASE_DELAY_MS, GHLClient;
548
+ function requestError(error) {
549
+ if (error !== null && typeof error === "object") requestErrors.add(error);
550
+ return error;
551
+ }
552
+ function isGHLRequestError(error) {
553
+ return error !== null && typeof error === "object" && requestErrors.has(error);
554
+ }
555
+ var GHL_BASE_URL, GHL_API_VERSION, MAX_RETRIES, BASE_DELAY_MS, requestErrors, GHLClient;
549
556
  var init_ghl_client = __esm({
550
557
  "src/ghl-client.ts"() {
551
558
  "use strict";
@@ -556,6 +563,7 @@ var init_ghl_client = __esm({
556
563
  GHL_API_VERSION = "2021-07-28";
557
564
  MAX_RETRIES = 3;
558
565
  BASE_DELAY_MS = 500;
566
+ requestErrors = /* @__PURE__ */ new WeakSet();
559
567
  GHLClient = class _GHLClient {
560
568
  apiKey;
561
569
  defaultLocationId;
@@ -639,7 +647,7 @@ var init_ghl_client = __esm({
639
647
  } catch (error) {
640
648
  clearTimeout(timeout);
641
649
  if (error instanceof Error && error.name === "AbortError") {
642
- throw new Error(`Request timeout (30s): ${method} ${path39}`);
650
+ throw requestError(new Error(`Request timeout (30s): ${method} ${path39}`));
643
651
  }
644
652
  if (!inBuildRequestScope() && !options.noRetry && attempt < MAX_RETRIES) {
645
653
  const delay5 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
@@ -648,7 +656,7 @@ var init_ghl_client = __esm({
648
656
  await new Promise((r) => setTimeout(r, delay5));
649
657
  return this.request(method, path39, options, attempt + 1);
650
658
  }
651
- throw error;
659
+ throw requestError(error);
652
660
  } finally {
653
661
  clearTimeout(timeout);
654
662
  }
@@ -665,12 +673,17 @@ var init_ghl_client = __esm({
665
673
  errorBody = await response.text();
666
674
  } catch {
667
675
  }
668
- throw new Error(
676
+ throw requestError(new Error(
669
677
  `GHL API Error ${response.status} ${response.statusText}: ${method} ${path39}
670
678
  ${errorBody}`
671
- );
679
+ ));
680
+ }
681
+ let text;
682
+ try {
683
+ text = await response.text();
684
+ } catch (error) {
685
+ throw requestError(error);
672
686
  }
673
- const text = await response.text();
674
687
  if (!text) return {};
675
688
  try {
676
689
  return JSON.parse(text);
@@ -15488,11 +15501,11 @@ function deriveBrandDna(raw, opts = {}) {
15488
15501
  return m ? parseFloat(m[1]) : null;
15489
15502
  };
15490
15503
  const sizePx = raw.typography.sizes.map((s) => ({ v: px(s.value), count: s.count })).filter((s) => s.v !== null);
15491
- const h1 = b?.h1Size ?? sizePx.filter((s) => s.v >= 32).sort((a, z82) => z82.v - a.v)[0]?.v;
15492
- const bodySize = b?.bodySize ?? sizePx.filter((s) => s.v >= 14 && s.v <= 20).sort((a, z82) => z82.count - a.count)[0]?.v;
15504
+ const h1 = b?.h1Size ?? sizePx.filter((s) => s.v >= 32).sort((a, z84) => z84.v - a.v)[0]?.v;
15505
+ const bodySize = b?.bodySize ?? sizePx.filter((s) => s.v >= 14 && s.v <= 20).sort((a, z84) => z84.count - a.count)[0]?.v;
15493
15506
  const sizes = { ...h1 && { h1 }, ...bodySize && { body: bodySize } };
15494
15507
  const scaleRatio = h1 && bodySize ? Math.round(Math.pow(h1 / bodySize, 1 / 4) * 1e3) / 1e3 : void 0;
15495
- const radiiPx = raw.radii.map((r) => px(r.value.split(/\s+/)[0])).filter((n) => n !== null && n < 100).sort((a, z82) => a - z82);
15508
+ const radiiPx = raw.radii.map((r) => px(r.value.split(/\s+/)[0])).filter((n) => n !== null && n < 100).sort((a, z84) => a - z84);
15496
15509
  const fcRadius = fc?.borderRadius ? px(fc.borderRadius) : null;
15497
15510
  const distinct = [...new Set(radiiPx)];
15498
15511
  const radius = fcRadius !== null ? { sm: Math.max(0, fcRadius - 2), md: fcRadius, lg: fcRadius * 2 } : distinct.length ? { sm: distinct[0], md: distinct[Math.floor(distinct.length / 2)], lg: distinct[distinct.length - 1] } : { sm: 4, md: 8, lg: 16 };
@@ -27038,6 +27051,37 @@ var init_client_preflight = __esm({
27038
27051
  }
27039
27052
  });
27040
27053
 
27054
+ // src/command-os/design/brief-validation.ts
27055
+ function designValidationMessage(error) {
27056
+ return [...new Set(error.issues.map((issue) => {
27057
+ const field = issue.path.join(".");
27058
+ if (field === "options.slides") return SLIDES_MESSAGE;
27059
+ if (field === "content.points" && issue.code === "too_big") return POINTS_MESSAGE;
27060
+ if (field.startsWith("content.points")) return "Enter each point as text.";
27061
+ if (field === "content.headline") return "Enter a headline.";
27062
+ return "Check the design details before trying again.";
27063
+ }))].join(" ");
27064
+ }
27065
+ var import_zod52, slideSchema, pointsSchema, slidesMin, slidesMax, pointsMax, DESIGN_FORM_LIMITS, SLIDES_MESSAGE, POINTS_MESSAGE;
27066
+ var init_brief_validation = __esm({
27067
+ "src/command-os/design/brief-validation.ts"() {
27068
+ "use strict";
27069
+ import_zod52 = require("zod");
27070
+ init_schema();
27071
+ slideSchema = DesignBriefSchema.shape.options.unwrap().shape.slides.unwrap();
27072
+ pointsSchema = DesignBriefSchema.shape.content.shape.points;
27073
+ slidesMin = slideSchema.minValue;
27074
+ slidesMax = slideSchema.maxValue;
27075
+ pointsMax = import_zod52.z.toJSONSchema(pointsSchema).maxItems;
27076
+ if (typeof slidesMin !== "number" || !Number.isFinite(slidesMin) || typeof slidesMax !== "number" || !Number.isFinite(slidesMax) || typeof pointsMax !== "number" || !Number.isFinite(pointsMax)) {
27077
+ throw new Error("The design schema must expose a minimum and maximum slide count and a maximum point count.");
27078
+ }
27079
+ DESIGN_FORM_LIMITS = { slidesMin, slidesMax, pointsMax };
27080
+ SLIDES_MESSAGE = `Choose a whole number of slides from ${DESIGN_FORM_LIMITS.slidesMin} to ${DESIGN_FORM_LIMITS.slidesMax}.`;
27081
+ POINTS_MESSAGE = `Use at most ${DESIGN_FORM_LIMITS.pointsMax} points.`;
27082
+ }
27083
+ });
27084
+
27041
27085
  // src/command-os/design/run-jobs.ts
27042
27086
  function validateRunRequest(input) {
27043
27087
  const a = input ?? {};
@@ -27050,13 +27094,13 @@ function validateRunRequest(input) {
27050
27094
  if (headline3.length < 3 || headline3.length > 300) return { ok: false, error: "headline must be between 3 and 300 characters." };
27051
27095
  const points = Array.isArray(a.points) ? a.points.filter((p) => typeof p === "string" && p.trim().length > 0).map((p) => p.trim()) : [];
27052
27096
  if (points.length === 0) return { ok: false, error: "points must be a non-empty list." };
27053
- if (points.length > 20) return { ok: false, error: "points: 20 at most." };
27097
+ if (points.length > DESIGN_FORM_LIMITS.pointsMax) return { ok: false, error: POINTS_MESSAGE };
27054
27098
  if (points.some((p) => p.includes("|"))) return { ok: false, error: "a point may not contain the | character; it separates them on the command line." };
27055
27099
  if (points.some((p) => p.length > 400)) return { ok: false, error: "a point may be 400 characters at most." };
27056
27100
  const model = str3("model");
27057
27101
  if (model && !MODEL.test(model)) return { ok: false, error: "model must be a plain engine name." };
27058
- const slides = typeof a.slides === "number" && Number.isInteger(a.slides) ? a.slides : void 0;
27059
- if (slides !== void 0 && (slides < 1 || slides > 40)) return { ok: false, error: "slides must be between 1 and 40." };
27102
+ const slides = a.slides;
27103
+ if (slides !== void 0 && (typeof slides !== "number" || !Number.isInteger(slides) || slides < DESIGN_FORM_LIMITS.slidesMin || slides > DESIGN_FORM_LIMITS.slidesMax)) return { ok: false, error: SLIDES_MESSAGE };
27060
27104
  let refusal = null;
27061
27105
  const optional = (k, max = 400) => {
27062
27106
  const v = str3(k);
@@ -27102,6 +27146,9 @@ function argvFor(r, outDir) {
27102
27146
  return argv;
27103
27147
  }
27104
27148
  function startDesignRun(request, opts = {}) {
27149
+ const checked = validateRunRequest(request);
27150
+ if (!checked.ok) throw new Error(checked.error);
27151
+ request = checked.value;
27105
27152
  const id = (0, import_node_crypto6.randomUUID)();
27106
27153
  const outDir = (0, import_node_path5.join)(designRunsDir(), id);
27107
27154
  (0, import_node_fs5.mkdirSync)(outDir, { recursive: true });
@@ -27179,6 +27226,7 @@ var init_run_jobs = __esm({
27179
27226
  import_node_crypto6 = require("node:crypto");
27180
27227
  init_credentials_store();
27181
27228
  init_schema();
27229
+ init_brief_validation();
27182
27230
  SLUG2 = /^[a-z0-9][a-z0-9-]{0,63}$/;
27183
27231
  MODEL = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
27184
27232
  JOINED_FLAG = /^--[a-z][a-z-]*=/;
@@ -28605,7 +28653,7 @@ var require_package = __commonJS({
28605
28653
  "package.json"(exports2, module2) {
28606
28654
  module2.exports = {
28607
28655
  name: "@elitedcs/ghl-mcp",
28608
- version: "3.83.7",
28656
+ version: "3.83.8",
28609
28657
  mcpName: "io.github.drjerryrelth/ghl-command",
28610
28658
  description: "GoHighLevel MCP Server for Claude. 250 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
28611
28659
  main: "dist/index.js",
@@ -30520,6 +30568,16 @@ border-bottom:1px solid var(--rule);font-size:var(--fs-sm)}
30520
30568
  </div>
30521
30569
 
30522
30570
  <script>
30571
+ // Shared by the save schema and the standalone page (inlined by build-app.mjs).
30572
+ var CLIENT_FIELDS = {
30573
+ name: { max: 120, label: "Client name" },
30574
+ role: { max: 80, label: "Client role" },
30575
+ email: { max: 200, label: "Client email" },
30576
+ phone: { max: 40, label: "Client phone" },
30577
+ website: { max: 300, label: "Client website" },
30578
+ city: { max: 120, label: "Client city" }
30579
+ };
30580
+
30523
30581
  /**
30524
30582
  * The assessment engine \u2014 the ONE place the questions and the findings maths live.
30525
30583
  *
@@ -31257,7 +31315,7 @@ function inputFor(q){
31257
31315
  * assessment belongs to, so Save to GoHighLevel knows where to put it.
31258
31316
  */
31259
31317
  function escH(s){return String(s==null?"":s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}
31260
- function cf(k,ph,type){var c=S.client||{};return '<input class="a" type="'+(type||"text")+'" placeholder="'+escH(ph)+'" value="'+escH(c[k]||"")+'" oninput="setClient(\\''+k+'\\',this.value)">'}
31318
+ function cf(k,ph,type){var c=S.client||{};return '<input class="a" type="'+(type||"text")+'" maxlength="'+CLIENT_FIELDS[k].max+'" placeholder="'+escH(ph)+'" value="'+escH(c[k]||"")+'" oninput="setClient(\\''+k+'\\',this.value)">'}
31261
31319
  function setClient(k,v){if(!S.client)S.client={};S.client[k]=v;save();head()}
31262
31320
  function setSaveTo(v){S.saveTo=v;save();try{localStorage.setItem(K+":saveTo",v)}catch(e){}}
31263
31321
  function accountSelectHtml(){
@@ -31365,7 +31423,7 @@ function openReport(){
31365
31423
  }
31366
31424
  function showSource(){
31367
31425
  var text=reportHtml(), name=slug(S.biz)+"-report.html";
31368
- dump(savedMsg(saveFile(name,text,"text/html"),name), text);
31426
+ offerContents(savedMsg(saveFile(name,text,"text/html"),name), text);
31369
31427
  }
31370
31428
  function draw(){
31371
31429
  head();
@@ -31396,6 +31454,17 @@ function dump(title,text){
31396
31454
  var t=w.querySelector("textarea");t.value=text;t.focus();t.select();
31397
31455
  w.scrollIntoView({behavior:"smooth",block:"nearest"});
31398
31456
  }
31457
+ /** Offer a copy fallback without opening the file's source on a normal save. */
31458
+ function offerContents(message,text){
31459
+ var old=document.getElementById("dump");if(old)old.remove();
31460
+ var previous=document.getElementById("file-contents");if(previous)previous.remove();
31461
+ var box=document.createElement("div");box.id="file-contents";box.className="note";
31462
+ var note=document.createElement("p");note.textContent=message;box.appendChild(note);
31463
+ var button=document.createElement("button");button.type="button";button.className="mini";
31464
+ button.textContent="Show the file's contents";
31465
+ button.onclick=function(){dump("Select all, copy, and save the file's contents.",text)};
31466
+ box.appendChild(button);document.getElementById("body").appendChild(box);
31467
+ }
31399
31468
  function slug(s){return String(s||"assessment").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"assessment"}
31400
31469
  /**
31401
31470
  * Write a real file to the operator's downloads.
@@ -31421,17 +31490,17 @@ function saveFile(name,text,type){
31421
31490
  * simply refuses \u2014 which is exactly what some Chrome configurations do to a page
31422
31491
  * opened from file://, the way this one ships. So a true return means "asked",
31423
31492
  * never "saved", so announcing a completed save on the strength of it would be
31424
- * a confident claim about something unobserved. The copy box is shown every
31425
- * time and the wording is true under both outcomes.
31493
+ * a confident claim about something unobserved. The copy box is offered behind an explicit control
31494
+ * and the wording is true under both outcomes.
31426
31495
  */
31427
31496
  function savedMsg(ok,name){
31428
31497
  return ok
31429
- ? "Asked your browser to save it as "+name+". If it did not appear in your downloads, it was blocked \u2014 the same text is here, so select all, copy, and save it as "+name+"."
31430
- : "Your browser would not save the file. Select all, copy, and save it as "+name+".";
31498
+ ? "Asked your browser to save it as "+name+". If it did not appear in your downloads, choose Show the file's contents to copy and save it as "+name+"."
31499
+ : "Your browser would not save the file. Choose Show the file's contents to copy and save it as "+name+".";
31431
31500
  }
31432
31501
  function dl(){
31433
31502
  var text=JSON.stringify(S,null,2), name=slug(S.biz)+"-answers.json";
31434
- dump(savedMsg(saveFile(name,text,"application/json"),name), text);
31503
+ offerContents(savedMsg(saveFile(name,text,"application/json"),name), text);
31435
31504
  }
31436
31505
  function wipe(){if(!confirm)return;S.ans={};S.skip={};S.biz="";S.trade="";S.client={};S.tab="ask";S.area="client";save();draw()}
31437
31506
  /** Save into the chosen account through Command OS on this computer: the same
@@ -32384,12 +32453,12 @@ ${d.voice.samples.length ? `<h2>Voice</h2><ul>${d.voice.samples.map((s) => `<li>
32384
32453
  ${opts.screenshot ? `<h2>Their site, as rendered</h2><img class="shot" src="/brand/${encodeURIComponent(d.slug)}/screenshot.png" alt="Screenshot of ${esc5(d.name)}'s website">` : ""}
32385
32454
  <h2>Make something in this brand</h2>
32386
32455
  <p class="small">Runs on <b>your own Claude</b>, on this computer, never our key. ${esc5(RUN_EXPECTATION)} The run keeps going if you close this tab. Write the headline and the points you want made, not instructions.</p>
32387
- <form id="make"><div class="two">
32456
+ <form id="make" novalidate><div class="two">
32388
32457
  <div><label for="mk-medium">What</label><select id="mk-medium"><option value="deck">A deck</option><option value="site">A landing page</option><option value="report">A report</option></select></div>
32389
- <div><label for="mk-slides">Slides (a deck only)</label><input id="mk-slides" type="number" min="1" max="40" value="6"></div>
32458
+ <div><label for="mk-slides">Slides (a deck only)</label><input id="mk-slides" type="number" min="${DESIGN_FORM_LIMITS.slidesMin}" max="${DESIGN_FORM_LIMITS.slidesMax}" step="1" value="6"></div>
32390
32459
  </div>
32391
32460
  <label for="mk-headline">Headline</label><input id="mk-headline" type="text" placeholder="The one sentence this has to land" autocomplete="off">
32392
- <label for="mk-points">Points, one per line</label><input id="mk-points" type="text" placeholder="first point | second point | third point" autocomplete="off">
32461
+ <label for="mk-points">Points, one per line</label><input id="mk-points" data-max-points="${DESIGN_FORM_LIMITS.pointsMax}" type="text" placeholder="first point | second point | third point" autocomplete="off">
32393
32462
  <div class="two">
32394
32463
  <div><label for="mk-audience">Who it is for</label><input id="mk-audience" type="text" placeholder="clinic owners with two locations" autocomplete="off"></div>
32395
32464
  <div><label for="mk-cta">Button label</label><input id="mk-cta" type="text" placeholder="Book the first visit" autocomplete="off"></div>
@@ -32430,6 +32499,9 @@ document.getElementById("make").addEventListener("submit",async function(e){
32430
32499
  var g=function(id){return document.getElementById(id).value.trim()};
32431
32500
  var pts=g("mk-points").split("|").map(function(x){return x.trim()}).filter(Boolean);
32432
32501
  if(!g("mk-headline")||!pts.length){say("mk-status","A headline and at least one point, please.","err");return}
32502
+ if(pts.length>${DESIGN_FORM_LIMITS.pointsMax}){say("mk-status",${scriptJson(POINTS_MESSAGE)},"err");return}
32503
+ var slideCount=Number(g("mk-slides"));
32504
+ if(g("mk-medium")==="deck"&&(!Number.isInteger(slideCount)||slideCount<${DESIGN_FORM_LIMITS.slidesMin}||slideCount>${DESIGN_FORM_LIMITS.slidesMax})){say("mk-status",${scriptJson(SLIDES_MESSAGE)},"err");return}
32433
32505
  var body={brand:${scriptJson(d.slug)},medium:g("mk-medium"),headline:g("mk-headline"),points:pts};
32434
32506
  if(g("mk-audience"))body.audience=g("mk-audience");
32435
32507
  if(g("mk-cta"))body.cta=g("mk-cta");
@@ -32468,6 +32540,7 @@ var esc5, STYLE, SHELL_JS, foot;
32468
32540
  var init_brand_page = __esm({
32469
32541
  "src/command-os/design/brand-page.ts"() {
32470
32542
  "use strict";
32543
+ init_brief_validation();
32471
32544
  init_run_expectations();
32472
32545
  init_fonts();
32473
32546
  init_cockpit_shell();
@@ -32905,6 +32978,22 @@ var init_capability_runs = __esm({
32905
32978
  }
32906
32979
  });
32907
32980
 
32981
+ // src/command-os/assessment/client-fields.js
32982
+ var CLIENT_FIELDS;
32983
+ var init_client_fields = __esm({
32984
+ "src/command-os/assessment/client-fields.js"() {
32985
+ "use strict";
32986
+ CLIENT_FIELDS = {
32987
+ name: { max: 120, label: "Client name" },
32988
+ role: { max: 80, label: "Client role" },
32989
+ email: { max: 200, label: "Client email" },
32990
+ phone: { max: 40, label: "Client phone" },
32991
+ website: { max: 300, label: "Client website" },
32992
+ city: { max: 120, label: "Client city" }
32993
+ };
32994
+ }
32995
+ });
32996
+
32908
32997
  // src/command-os/assessment/transcript.ts
32909
32998
  function loose(s) {
32910
32999
  return String(s ?? "").replace(/[‘’ʼ]/g, "'").replace(/[“”]/g, '"').replace(/[–—]/g, "-").replace(/\s+/g, " ").trim().toLowerCase();
@@ -33212,6 +33301,20 @@ var init_report = __esm({
33212
33301
  });
33213
33302
 
33214
33303
  // src/tools/assessment.ts
33304
+ function assessmentValidationMessage(error) {
33305
+ if (!(error instanceof import_zod53.z.ZodError)) return void 0;
33306
+ const seen = /* @__PURE__ */ new Set();
33307
+ return error.issues.flatMap((issue) => {
33308
+ const key = issue.path.join(".");
33309
+ if (seen.has(key)) return [];
33310
+ seen.add(key);
33311
+ const field = issue.path[0] === "client" ? Object.entries(CLIENT_FIELDS).find(([name]) => name === issue.path[1])?.[1] : void 0;
33312
+ if (field && issue.code === "too_big") return [`${field.label} is too long, ${field.max} characters at most.`];
33313
+ if (field) return [`Enter ${field.label.toLowerCase()} as text.`];
33314
+ if (key === "biz") return ["Enter the business name before saving."];
33315
+ return ["Check the assessment answers before saving."];
33316
+ }).join(" ");
33317
+ }
33215
33318
  function today() {
33216
33319
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
33217
33320
  }
@@ -33240,7 +33343,7 @@ async function createPipeline(client, locationId2) {
33240
33343
  });
33241
33344
  return { ok: true };
33242
33345
  } catch (e) {
33243
- return { error: `could not create the "${PIPELINE_NAME}" pipeline: ${String(e)}` };
33346
+ return { error: `could not create the "${PIPELINE_NAME}" pipeline: ${String(e)}`, failure: { detail: (0, import_node_util3.inspect)(e) } };
33244
33347
  }
33245
33348
  }
33246
33349
  async function findStage(client, locationId2, stage, mayCreate = false) {
@@ -33252,7 +33355,7 @@ async function findStage(client, locationId2, stage, mayCreate = false) {
33252
33355
  try {
33253
33356
  pipelines = await read2();
33254
33357
  } catch (e) {
33255
- return { error: `could not read pipelines: ${String(e)}` };
33358
+ return { error: `could not read pipelines: ${String(e)}`, failure: { detail: (0, import_node_util3.inspect)(e) } };
33256
33359
  }
33257
33360
  const named = (list2) => list2.find((p) => (p.name ?? "").trim().toLowerCase() === PIPELINE_NAME.toLowerCase());
33258
33361
  let pipe = named(pipelines);
@@ -33262,7 +33365,7 @@ async function findStage(client, locationId2, stage, mayCreate = false) {
33262
33365
  try {
33263
33366
  pipe = named(await read2());
33264
33367
  } catch (e) {
33265
- return { error: `created the "${PIPELINE_NAME}" pipeline but could not read it back: ${String(e)}` };
33368
+ return { error: `created the "${PIPELINE_NAME}" pipeline but could not read it back: ${String(e)}`, failure: { detail: (0, import_node_util3.inspect)(e) } };
33266
33369
  }
33267
33370
  if (!pipe) return { error: `created the "${PIPELINE_NAME}" pipeline but it is not in the account when read back. Nothing was assumed; try saving again.` };
33268
33371
  }
@@ -33275,7 +33378,7 @@ async function findStage(client, locationId2, stage, mayCreate = false) {
33275
33378
  }
33276
33379
  return { pipelineId: pipe.id, stageId: st.id, stageName: stage };
33277
33380
  }
33278
- async function saveAssessment(client, locationId2, args) {
33381
+ async function saveAssessment(client, locationId2, args, onOpportunityNoteFailure) {
33279
33382
  const state = normalise(args.assessment);
33280
33383
  const computed = computedFor(state);
33281
33384
  const stage = stageFor({ verdict: computed.verdict, proposalSent: args.proposalSent, outcome: args.outcome });
@@ -33311,6 +33414,7 @@ async function saveAssessment(client, locationId2, args) {
33311
33414
  let opportunityNote = null;
33312
33415
  if ("error" in hit) {
33313
33416
  opportunityNote = hit.error;
33417
+ if (hit.failure) onOpportunityNoteFailure?.(hit.failure);
33314
33418
  } else {
33315
33419
  const body = {
33316
33420
  locationId: locationId2,
@@ -33362,11 +33466,11 @@ function registerAssessmentTools(server2, client) {
33362
33466
  "attach_assessment",
33363
33467
  "Attach a saved assessment to a client card, so the numbers from the sales conversation follow them into the intake and their build. Keep your own account active. Nothing is written to a GoHighLevel account. Use fill to save the overlapping intake answers, detach to remove the snapshot, or purchases to record what they bought.",
33364
33468
  {
33365
- clientLocationId: import_zod52.z.string().min(1).describe("The destination client card. This is not the account holding the saved assessment."),
33366
- action: import_zod52.z.enum(["get", "attach", "fill", "detach", "purchases"]).default("attach"),
33367
- contactId: import_zod52.z.string().optional().describe("The saved assessment contact from list_assessments in your active account."),
33368
- opportunityId: import_zod52.z.string().optional(),
33369
- purchasedFeatureIds: import_zod52.z.array(import_zod52.z.string()).optional().describe("Feature ids returned by get. Record only what the client bought. An empty list explicitly means none.")
33469
+ clientLocationId: import_zod53.z.string().min(1).describe("The destination client card. This is not the account holding the saved assessment."),
33470
+ action: import_zod53.z.enum(["get", "attach", "fill", "detach", "purchases"]).default("attach"),
33471
+ contactId: import_zod53.z.string().optional().describe("The saved assessment contact from list_assessments in your active account."),
33472
+ opportunityId: import_zod53.z.string().optional(),
33473
+ purchasedFeatureIds: import_zod53.z.array(import_zod53.z.string()).optional().describe("Feature ids returned by get. Record only what the client bought. An empty list explicitly means none.")
33370
33474
  },
33371
33475
  async (args) => {
33372
33476
  const [{ readCockpitState: readCockpitState2, writeCockpitState: writeCockpitState2 }, { TokenRegistry: TokenRegistry2 }, { composeQuestionSet: composeQuestionSet2 }, { readOverlay: readOverlay2 }] = await Promise.all([
@@ -33390,15 +33494,15 @@ function registerAssessmentTools(server2, client) {
33390
33494
  "save_assessment",
33391
33495
  "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.",
33392
33496
  {
33393
- locationId: import_zod52.z.string().optional().describe("Your own GHL sub-account. Defaults to the active one."),
33497
+ locationId: import_zod53.z.string().optional().describe("Your own GHL sub-account. Defaults to the active one."),
33394
33498
  assessment: StateSchema,
33395
- contactName: import_zod52.z.string().optional().describe("The person you spoke to."),
33396
- email: import_zod52.z.string().optional(),
33397
- phone: import_zod52.z.string().optional(),
33398
- proposalValue: import_zod52.z.number().optional().describe("What you quoted, if you have quoted. Used as the opportunity value instead of the annual figure."),
33399
- outcome: import_zod52.z.enum(["won", "lost"]).optional().describe("Set only when the deal has actually closed."),
33400
- proposalSent: import_zod52.z.boolean().optional(),
33401
- createPipeline: import_zod52.z.boolean().optional().describe('Create the "Assessments" pipeline in your own GoHighLevel if it is not there yet. Off by default: it is a write to your account, so it is asked for rather than assumed. Say yes the first time and never again.')
33499
+ contactName: import_zod53.z.string().optional().describe("The person you spoke to."),
33500
+ email: import_zod53.z.string().optional(),
33501
+ phone: import_zod53.z.string().optional(),
33502
+ proposalValue: import_zod53.z.number().optional().describe("What you quoted, if you have quoted. Used as the opportunity value instead of the annual figure."),
33503
+ outcome: import_zod53.z.enum(["won", "lost"]).optional().describe("Set only when the deal has actually closed."),
33504
+ proposalSent: import_zod53.z.boolean().optional(),
33505
+ createPipeline: import_zod53.z.boolean().optional().describe('Create the "Assessments" pipeline in your own GoHighLevel if it is not there yet. Off by default: it is a write to your account, so it is asked for rather than assumed. Say yes the first time and never again.')
33402
33506
  },
33403
33507
  async (args) => saveAssessment(client, client.resolveLocationId(args.locationId), args)
33404
33508
  );
@@ -33407,7 +33511,7 @@ function registerAssessmentTools(server2, client) {
33407
33511
  "load_assessment",
33408
33512
  "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.",
33409
33513
  {
33410
- contactId: import_zod52.z.string().describe("The contact the assessment was saved against.")
33514
+ contactId: import_zod53.z.string().describe("The contact the assessment was saved against.")
33411
33515
  },
33412
33516
  async ({ contactId }) => {
33413
33517
  const { record: joined, notesRead } = await loadAssessmentRecord(client, contactId);
@@ -33437,8 +33541,8 @@ function registerAssessmentTools(server2, client) {
33437
33541
  "list_assessments",
33438
33542
  "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.",
33439
33543
  {
33440
- locationId: import_zod52.z.string().optional().describe("Your own GHL sub-account. Defaults to the active one."),
33441
- stage: import_zod52.z.enum(["Gathering", "Findings", "Proposal sent", "Won", "Lost"]).optional().describe("Only this stage.")
33544
+ locationId: import_zod53.z.string().optional().describe("Your own GHL sub-account. Defaults to the active one."),
33545
+ stage: import_zod53.z.enum(["Gathering", "Findings", "Proposal sent", "Won", "Lost"]).optional().describe("Only this stage.")
33442
33546
  },
33443
33547
  async ({ locationId: loc, stage }) => {
33444
33548
  const locationId2 = client.resolveLocationId(loc);
@@ -33464,14 +33568,14 @@ function registerAssessmentTools(server2, client) {
33464
33568
  "read_assessment_transcript",
33465
33569
  "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.",
33466
33570
  {
33467
- transcript: import_zod52.z.string().min(20).describe("The recording, as text. Paste the whole thing."),
33468
- proposed: import_zod52.z.array(import_zod52.z.object({
33469
- questionId: import_zod52.z.string().describe("The assessment question this answers."),
33470
- value: import_zod52.z.string().describe("What to store, in the unit the question wants."),
33471
- quote: import_zod52.z.string().describe("The owner's own words, VERBATIM from the transcript. Do not tidy it up \u2014 if it is not in the transcript the answer is thrown out."),
33472
- reasoning: import_zod52.z.string().optional().describe("How you got from the quote to the value, when they are not the same.")
33571
+ transcript: import_zod53.z.string().min(20).describe("The recording, as text. Paste the whole thing."),
33572
+ proposed: import_zod53.z.array(import_zod53.z.object({
33573
+ questionId: import_zod53.z.string().describe("The assessment question this answers."),
33574
+ value: import_zod53.z.string().describe("What to store, in the unit the question wants."),
33575
+ quote: import_zod53.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."),
33576
+ reasoning: import_zod53.z.string().optional().describe("How you got from the quote to the value, when they are not the same.")
33473
33577
  })).describe("One entry per question you could answer. Leave out anything the conversation did not cover."),
33474
- existing: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.union([import_zod52.z.string(), import_zod52.z.number()])).optional().describe("Answers already captured, so this adds to them rather than replacing them.")
33578
+ existing: import_zod53.z.record(import_zod53.z.string(), import_zod53.z.union([import_zod53.z.string(), import_zod53.z.number()])).optional().describe("Answers already captured, so this adds to them rather than replacing them.")
33475
33579
  },
33476
33580
  async ({ transcript, proposed, existing }) => {
33477
33581
  const gated = gateExtraction(transcript, proposed, QUESTION_UNITS);
@@ -33497,15 +33601,15 @@ function registerAssessmentTools(server2, client) {
33497
33601
  "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.",
33498
33602
  {
33499
33603
  assessment: StateSchema,
33500
- agencyName: import_zod52.z.string().optional().describe("Whose letterhead this goes out on. Read from your agency profile if you have one set."),
33501
- agencyContact: import_zod52.z.string().optional().describe("Email or phone for the footer."),
33502
- accent: import_zod52.z.string().optional().describe("Brand colour as a hex code, e.g. #8A2B2B."),
33503
- services: import_zod52.z.array(import_zod52.z.object({
33504
- name: import_zod52.z.string().describe("Your service, in the words you sell it in."),
33505
- price: import_zod52.z.number().optional().describe("Your price. Leave it out and the report shows the plan without an investment section rather than inventing a number."),
33506
- covers: import_zod52.z.array(import_zod52.z.string()).describe("Which findings it fixes: missed, waited, quotes, noshow.")
33604
+ agencyName: import_zod53.z.string().optional().describe("Whose letterhead this goes out on. Read from your agency profile if you have one set."),
33605
+ agencyContact: import_zod53.z.string().optional().describe("Email or phone for the footer."),
33606
+ accent: import_zod53.z.string().optional().describe("Brand colour as a hex code, e.g. #8A2B2B."),
33607
+ services: import_zod53.z.array(import_zod53.z.object({
33608
+ name: import_zod53.z.string().describe("Your service, in the words you sell it in."),
33609
+ price: import_zod53.z.number().optional().describe("Your price. Leave it out and the report shows the plan without an investment section rather than inventing a number."),
33610
+ covers: import_zod53.z.array(import_zod53.z.string()).describe("Which findings it fixes: missed, waited, quotes, noshow.")
33507
33611
  })).optional().describe("YOUR menu at YOUR prices. Never ours."),
33508
- outDir: import_zod52.z.string().optional().describe("Where to write it. Defaults to ~/Documents/assessments.")
33612
+ outDir: import_zod53.z.string().optional().describe("Where to write it. Defaults to ~/Documents/assessments.")
33509
33613
  },
33510
33614
  async (args) => {
33511
33615
  const state = normalise(args.assessment);
@@ -33564,11 +33668,13 @@ function registerAssessmentTools(server2, client) {
33564
33668
  }
33565
33669
  );
33566
33670
  }
33567
- var import_zod52, import_node_crypto8, import_node_fs6, import_node_os, import_node_path6, QUESTION_UNITS, AREA_TITLE, ASK, AREA_OF, ASSUMPTION_LABEL, LOOKUP, StateSchema;
33671
+ var import_node_util3, import_zod53, import_node_crypto8, import_node_fs6, import_node_os, import_node_path6, QUESTION_UNITS, AREA_TITLE, ASK, AREA_OF, ASSUMPTION_LABEL, LOOKUP, StateSchema;
33568
33672
  var init_assessment = __esm({
33569
33673
  "src/tools/assessment.ts"() {
33570
33674
  "use strict";
33571
- import_zod52 = require("zod");
33675
+ import_node_util3 = require("node:util");
33676
+ import_zod53 = require("zod");
33677
+ init_client_fields();
33572
33678
  init_attachment();
33573
33679
  init_attachment_actions();
33574
33680
  init_tool_helpers();
@@ -33591,13 +33697,13 @@ var init_assessment = __esm({
33591
33697
  areaOrder: () => AREAS.map((a) => a[1]),
33592
33698
  assumption: (id) => ASSUMPTION_LABEL.get(id)
33593
33699
  };
33594
- StateSchema = import_zod52.z.object({
33595
- biz: import_zod52.z.string().min(1).describe("The prospect's business name."),
33596
- trade: import_zod52.z.string().optional().describe("Their trade. Decides which benchmarks may be applied."),
33597
- ans: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.union([import_zod52.z.string(), import_zod52.z.number()])).describe("questionId -> answer, exactly as typed."),
33598
- skip: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.boolean()).optional().describe("questionId -> true where they did not know."),
33599
- asm: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.number()).optional().describe("assumption id -> rate as a decimal."),
33600
- client: import_zod52.z.object({ name: import_zod52.z.string().max(120), role: import_zod52.z.string().max(80), email: import_zod52.z.string().max(200), phone: import_zod52.z.string().max(40), website: import_zod52.z.string().max(300), city: import_zod52.z.string().max(120) }).partial().optional().describe("The person and place this is for, as typed on The client step. Rides with the answers so a load brings it back.")
33700
+ StateSchema = import_zod53.z.object({
33701
+ biz: import_zod53.z.string().min(1).describe("The prospect's business name."),
33702
+ trade: import_zod53.z.string().optional().describe("Their trade. Decides which benchmarks may be applied."),
33703
+ ans: import_zod53.z.record(import_zod53.z.string(), import_zod53.z.union([import_zod53.z.string(), import_zod53.z.number()])).describe("questionId -> answer, exactly as typed."),
33704
+ skip: import_zod53.z.record(import_zod53.z.string(), import_zod53.z.boolean()).optional().describe("questionId -> true where they did not know."),
33705
+ asm: import_zod53.z.record(import_zod53.z.string(), import_zod53.z.number()).optional().describe("assumption id -> rate as a decimal."),
33706
+ client: import_zod53.z.object({ name: import_zod53.z.string().max(CLIENT_FIELDS.name.max), role: import_zod53.z.string().max(CLIENT_FIELDS.role.max), email: import_zod53.z.string().max(CLIENT_FIELDS.email.max), phone: import_zod53.z.string().max(CLIENT_FIELDS.phone.max), website: import_zod53.z.string().max(CLIENT_FIELDS.website.max), city: import_zod53.z.string().max(CLIENT_FIELDS.city.max) }).partial().optional().describe("The person and place this is for, as typed on The client step. Rides with the answers so a load brings it back.")
33601
33707
  });
33602
33708
  }
33603
33709
  });
@@ -33987,6 +34093,7 @@ __export(dashboard_exports, {
33987
34093
  intakeRequestError: () => intakeRequestError,
33988
34094
  isAllowedCockpitHost: () => isAllowedCockpitHost,
33989
34095
  isLoopback: () => isLoopback,
34096
+ isUpstreamFailure: () => isUpstreamFailure,
33990
34097
  launcherAlreadyOffered: () => launcherAlreadyOffered,
33991
34098
  listClientDocs: () => listClientDocs,
33992
34099
  mayServeSeat: () => mayServeSeat,
@@ -34459,19 +34566,34 @@ function memberFacingError(e, own = []) {
34459
34566
  const raw = e instanceof Error ? e.message : String(e);
34460
34567
  return own.includes(raw) ? raw : readFailureText(e);
34461
34568
  }
34462
- function newClientFailure(error, surface = "command-os") {
34569
+ function isUpstreamFailure(error) {
34570
+ if (typeof error === "string") return /^\s*(?:\{\s*"|\[\s*\{)/.test(error);
34571
+ const seen = /* @__PURE__ */ new Set();
34572
+ let current = error;
34573
+ while (current !== null && typeof current === "object" && !seen.has(current)) {
34574
+ seen.add(current);
34575
+ if (isGHLRequestError(current) || current instanceof TypeError || current instanceof DOMException) return true;
34576
+ if ("name" in current && (current.name === "AbortError" || current.name === "TimeoutError")) return true;
34577
+ if ("code" in current && typeof current.code === "string" && /^(?:E[A-Z0-9_]+|UND_ERR_[A-Z0-9_]+)$/.test(current.code)) return true;
34578
+ current = "cause" in current ? current.cause : void 0;
34579
+ }
34580
+ return false;
34581
+ }
34582
+ function newClientFailure(error, surface = "command-os", account2) {
34463
34583
  const connect2 = surface === "command-os" ? "paste its key into Command OS" : "register it in GHL Command with its key";
34464
- const refusal = subAccountRefusal(error, "create", surface);
34584
+ const refusal = account2 ? void 0 : subAccountRefusal(error, "create", surface);
34465
34585
  if (refusal) return refusal;
34466
34586
  const raw = error instanceof Error ? error.message : String(error);
34467
- if (raw === CREATE_PLAN_REFUSAL) return surface === "command-os" ? raw : "Create it in GoHighLevel and register it in GHL Command with its key.";
34468
- if (raw === "Choose the client industry." || raw === "Connect your agency in Your agency before creating a sub-account.") return raw;
34469
- if (raw === "business name is required") return "Enter the business name, then create the sub-account.";
34470
- if (raw.startsWith("No agency key registered.")) return `No agency key is registered; add your agency Private Integration key to let GHL Command create sub-accounts on GoHighLevel's Agency Pro plan, or on other plans create it in GoHighLevel and ${connect2}.`;
34471
- if (raw.startsWith("Couldn't resolve the agency company ID")) return "The agency company ID is missing; check the agency registered in GHL Command before creating the sub-account.";
34472
- if (/GHL API Error 401/.test(raw)) return "GoHighLevel refused the agency key; check whether the agency Private Integration has expired or been revoked before trying again.";
34473
- if (/GHL API Error 403/.test(raw)) return "GoHighLevel refused access for this agency; check the agency Private Integration permissions and plan in GoHighLevel before trying again.";
34474
- if (/GHL API Error 422/.test(raw)) return "GoHighLevel rejected the sub-account details; check the business and owner details before trying again.";
34587
+ if (account2 && !account2.fromCaughtError && !isUpstreamFailure(error)) return raw;
34588
+ if (raw === CREATE_PLAN_REFUSAL && !account2) return surface === "command-os" ? raw : "Create it in GoHighLevel and register it in GHL Command with its key.";
34589
+ if (!account2 && (raw === "Choose the client industry." || raw === "Connect your agency in Your agency before creating a sub-account.")) return raw;
34590
+ if (!account2 && raw === "business name is required") return "Enter the business name, then create the sub-account.";
34591
+ if (!account2 && raw.startsWith("No agency key registered.")) return `No agency key is registered; add your agency Private Integration key to let GHL Command create sub-accounts on GoHighLevel's Agency Pro plan, or on other plans create it in GoHighLevel and ${connect2}.`;
34592
+ if (!account2 && raw.startsWith("Couldn't resolve the agency company ID")) return "The agency company ID is missing; check the agency registered in GHL Command before creating the sub-account.";
34593
+ if (/GHL API Error 401/.test(raw)) return account2 ? `${account2.failed}: the key for this account was refused. Check the account key and its permissions in GoHighLevel. ${account2.next}` : "GoHighLevel refused the agency key; check whether the agency Private Integration has expired or been revoked before trying again.";
34594
+ if (/GHL API Error 403/.test(raw)) return account2 ? `${account2.failed}: the key for this account was refused. Check the account key and its permissions in GoHighLevel. ${account2.next}` : "GoHighLevel refused access for this agency; check the agency Private Integration permissions and plan in GoHighLevel before trying again.";
34595
+ if (/GHL API Error 422/.test(raw)) return account2 ? `${account2.failed}: GoHighLevel rejected the details. Check the entered details in this account. ${account2.next}` : "GoHighLevel rejected the sub-account details; check the business and owner details before trying again.";
34596
+ if (account2) return `${account2.failed}. ${account2.next}`;
34475
34597
  return `GHL Command could not confirm whether the sub-account was created; check GoHighLevel before trying again to avoid creating a duplicate, and ${connect2} if it exists.`;
34476
34598
  }
34477
34599
  function insideDir(dir, p) {
@@ -35940,7 +36062,11 @@ async function runDashboard(argv) {
35940
36062
  res.end(JSON.stringify(result));
35941
36063
  } catch (e) {
35942
36064
  res.writeHead(400, { "Content-Type": "application/json" });
35943
- res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "The assessment could not be attached." }));
36065
+ logQuietly(`client assessment: ${(0, import_node_util4.inspect)(e)}`);
36066
+ res.end(JSON.stringify({ ok: false, error: newClientFailure(e, "command-os", {
36067
+ failed: "The assessment could not be loaded or attached",
36068
+ next: "Check the selected account and refresh assessments before attaching again."
36069
+ }) }));
35944
36070
  }
35945
36071
  });
35946
36072
  return;
@@ -36076,16 +36202,32 @@ async function runDashboard(argv) {
36076
36202
  const c = clientFor(locationId2);
36077
36203
  if (!c) throw new Error("No key on file for that account. Register it in GHL Command first.");
36078
36204
  const assessment = StateSchema.parse(args.assessment);
36205
+ let opportunityNoteFailure;
36079
36206
  const out = await saveAssessment(c, locationId2, {
36080
36207
  assessment,
36081
36208
  contactName: args.contactName || void 0,
36082
36209
  email: args.email || void 0,
36083
36210
  phone: args.phone || void 0,
36084
36211
  createPipeline: args.createPipeline === true
36212
+ }, (failure) => {
36213
+ opportunityNoteFailure = failure;
36085
36214
  });
36215
+ if (out.opportunityNote && (opportunityNoteFailure || isUpstreamFailure(out.opportunityNote))) {
36216
+ logQuietly(`assessment pipeline: ${out.opportunityNote}
36217
+ ${opportunityNoteFailure?.detail ?? ""}`);
36218
+ out.opportunityNote = newClientFailure(out.opportunityNote, "command-os", {
36219
+ fromCaughtError: Boolean(opportunityNoteFailure),
36220
+ failed: "The answers were saved, but the Assessments pipeline could not be checked",
36221
+ next: "Check the Assessments pipeline in GoHighLevel before saving again."
36222
+ });
36223
+ }
36086
36224
  res.end(JSON.stringify({ ok: true, ...out }));
36087
36225
  } catch (e) {
36088
- res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : String(e) }));
36226
+ logQuietly(`assessment save: ${(0, import_node_util4.inspect)(e)}`);
36227
+ res.end(JSON.stringify({ ok: false, error: assessmentValidationMessage(e) ?? newClientFailure(e, "command-os", {
36228
+ failed: "The assessment save could not be confirmed",
36229
+ next: "Check the contact and its notes in GoHighLevel before saving again."
36230
+ }) }));
36089
36231
  }
36090
36232
  })();
36091
36233
  });
@@ -36816,7 +36958,11 @@ async function runDashboard(argv) {
36816
36958
  }
36817
36959
  } catch (e) {
36818
36960
  res.writeHead(400, { "Content-Type": "application/json" });
36819
- res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "The handoff result is uncertain. Check Conversations before retrying." }));
36961
+ logQuietly(`handoff email: ${(0, import_node_util4.inspect)(e)}`);
36962
+ res.end(JSON.stringify({ ok: false, error: newClientFailure(e, "command-os", {
36963
+ failed: url === "/api/handoff-prepare" ? "The handoff email could not be prepared" : "The handoff email send could not be confirmed",
36964
+ next: "Check Conversations in GoHighLevel before trying again."
36965
+ }) }));
36820
36966
  }
36821
36967
  });
36822
36968
  return;
@@ -37311,10 +37457,11 @@ async function runDashboard(argv) {
37311
37457
  process.on("SIGTERM", stop);
37312
37458
  });
37313
37459
  }
37314
- var fs27, path26, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, BOARD_ROUTES, BOARD_TITLES, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS;
37460
+ var import_node_util4, fs27, path26, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, BOARD_ROUTES, BOARD_TITLES, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS;
37315
37461
  var init_dashboard = __esm({
37316
37462
  "src/dashboard.ts"() {
37317
37463
  "use strict";
37464
+ import_node_util4 = require("node:util");
37318
37465
  init_verify_readback();
37319
37466
  init_intake_notification();
37320
37467
  init_approved_status_writer();
@@ -37552,7 +37699,7 @@ Token registry: ${registeredCount} location(s) registered${versionLine}`
37552
37699
  "switch_location",
37553
37700
  "Switch the active GHL sub-account. Automatically swaps the API key from the token registry if available. After switching, all tools default to the new location.",
37554
37701
  {
37555
- locationId: import_zod53.z.string().describe("The Location ID to switch to.")
37702
+ locationId: import_zod54.z.string().describe("The Location ID to switch to.")
37556
37703
  },
37557
37704
  async ({ locationId: locationId2 }) => withSwitchLock(async () => {
37558
37705
  const previousId = client.defaultLocationId;
@@ -37628,9 +37775,9 @@ Still on: ${previousId || "none"}${hint}` }],
37628
37775
  "register_location",
37629
37776
  "Add a GHL sub-account to the token registry so switch_location can automatically use its API key. Each sub-account needs its own Private Integration key created in GHL Settings > Integrations.",
37630
37777
  {
37631
- locationId: import_zod53.z.string().describe("The GHL Location ID (from Settings > Business Profile)."),
37632
- name: import_zod53.z.string().describe("A friendly name for this sub-account (e.g. 'PNTracker', 'Med Spa Template')."),
37633
- apiKey: import_zod53.z.string().describe("The Private Integration API key for this sub-account (starts with 'pit-').")
37778
+ locationId: import_zod54.z.string().describe("The GHL Location ID (from Settings > Business Profile)."),
37779
+ name: import_zod54.z.string().describe("A friendly name for this sub-account (e.g. 'PNTracker', 'Med Spa Template')."),
37780
+ apiKey: import_zod54.z.string().describe("The Private Integration API key for this sub-account (starts with 'pit-').")
37634
37781
  },
37635
37782
  async ({ locationId: locationId2, name, apiKey: apiKey2 }) => {
37636
37783
  const result = await registerVerifiedLocation(registry2, locationId2, apiKey2);
@@ -37652,7 +37799,7 @@ You can now use switch_location to switch to this sub-account.`
37652
37799
  "register_agency_key",
37653
37800
  "Store the AGENCY-level (company-scoped) API key in the token registry. This key powers agency-wide tools: list_snapshots, create_snapshot_share_link, and list_available_locations across all sub-accounts. Create it at the AGENCY level in GHL (Agency Settings > Private Integrations) \u2014 it is different from a sub-account's key. The key is validated before saving.",
37654
37801
  {
37655
- apiKey: import_zod53.z.string().describe("The agency-level Private Integration API key (starts with 'pit-'). Must be created in AGENCY settings, not inside a sub-account.")
37802
+ apiKey: import_zod54.z.string().describe("The agency-level Private Integration API key (starts with 'pit-'). Must be created in AGENCY settings, not inside a sub-account.")
37656
37803
  },
37657
37804
  async ({ apiKey: apiKey2 }) => {
37658
37805
  if (!registry2) {
@@ -37706,7 +37853,7 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
37706
37853
  "unregister_location",
37707
37854
  "Remove a GHL sub-account from the token registry.",
37708
37855
  {
37709
- locationId: import_zod53.z.string().describe("The Location ID to remove.")
37856
+ locationId: import_zod54.z.string().describe("The Location ID to remove.")
37710
37857
  },
37711
37858
  async ({ locationId: locationId2 }) => {
37712
37859
  if (!registry2) {
@@ -37732,12 +37879,12 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
37732
37879
  "register_company_firebase",
37733
37880
  "Register a GHL company's Firebase credentials so the workflow builder and all Firebase-gated tools work when you switch into THAT company's sub-accounts. Firebase refresh tokens are company-scoped, so managing a client's GHL (e.g. an account where you're an admin user) requires that company's own token. Capture the values from a browser session logged into the client's account. The tool stores them under the company ID the Firebase token itself authenticates as (decoded from the token), so you do NOT need to hunt down the exact internal company ID \u2014 pass whatever ID you have and it self-corrects. After this, switch_location to any of that company's locations authenticates the workflow builder correctly. DevTools capture steps: elitedcs.com/ghl-mcp-firebase.",
37734
37881
  {
37735
- companyId: import_zod53.z.string().describe("A GHL company/agency ID for this client (from switch_location/register_location output, or the GHL agency URL). Best-effort only: the tool re-keys the entry to the company ID the Firebase token actually authenticates as, which can differ from the ID shown in the agency URL. Just pass what you have."),
37736
- name: import_zod53.z.string().describe("Friendly name for this client/company (e.g. 'Acme Health \u2014 main agency')."),
37737
- ghl_firebase_refresh_token: import_zod53.z.string().min(10).describe("Firebase refresh token captured from a browser session logged into THIS company's GHL. value.stsTokenManager.refreshToken in the firebase:authUser IndexedDB row."),
37738
- ghl_user_id: import_zod53.z.string().min(5).describe("Firebase User ID (uid) from the same session. value.uid in the firebase:authUser row."),
37739
- ghl_firebase_api_key: import_zod53.z.string().optional().describe("Firebase API key (starts with 'AIza'). Optional \u2014 defaults to your home Firebase API key, which is identical across GHL accounts."),
37740
- test_location_id: import_zod53.z.string().optional().describe("Optional but recommended: a registered location ID belonging to this company. The tool then makes a real workflow-builder call to confirm these credentials actually work for this company before you rely on them.")
37882
+ companyId: import_zod54.z.string().describe("A GHL company/agency ID for this client (from switch_location/register_location output, or the GHL agency URL). Best-effort only: the tool re-keys the entry to the company ID the Firebase token actually authenticates as, which can differ from the ID shown in the agency URL. Just pass what you have."),
37883
+ name: import_zod54.z.string().describe("Friendly name for this client/company (e.g. 'Acme Health \u2014 main agency')."),
37884
+ ghl_firebase_refresh_token: import_zod54.z.string().min(10).describe("Firebase refresh token captured from a browser session logged into THIS company's GHL. value.stsTokenManager.refreshToken in the firebase:authUser IndexedDB row."),
37885
+ ghl_user_id: import_zod54.z.string().min(5).describe("Firebase User ID (uid) from the same session. value.uid in the firebase:authUser row."),
37886
+ ghl_firebase_api_key: import_zod54.z.string().optional().describe("Firebase API key (starts with 'AIza'). Optional \u2014 defaults to your home Firebase API key, which is identical across GHL accounts."),
37887
+ test_location_id: import_zod54.z.string().optional().describe("Optional but recommended: a registered location ID belonging to this company. The tool then makes a real workflow-builder call to confirm these credentials actually work for this company before you rely on them.")
37741
37888
  },
37742
37889
  async (args) => {
37743
37890
  if (!registry2) {
@@ -37841,7 +37988,7 @@ Now run switch_location to any of this company's sub-accounts \u2014 the workflo
37841
37988
  "unregister_company_firebase",
37842
37989
  "Remove a company's Firebase credentials from the registry. Workflow-builder tools will stop working for that company's sub-accounts until re-registered.",
37843
37990
  {
37844
- companyId: import_zod53.z.string().describe("The company ID to remove Firebase credentials for.")
37991
+ companyId: import_zod54.z.string().describe("The company ID to remove Firebase credentials for.")
37845
37992
  },
37846
37993
  async ({ companyId }) => {
37847
37994
  if (!registry2) {
@@ -37909,8 +38056,8 @@ ${lines.join("\n")}
37909
38056
  "list_available_locations",
37910
38057
  "List all GHL sub-accounts (locations) accessible with the current or agency API key. Shows locations that exist in the GHL account \u2014 use register_location to add their tokens. Offset-based pagination via skip/limit.",
37911
38058
  {
37912
- limit: import_zod53.z.number().optional().describe("Max locations to return. Defaults to 20."),
37913
- skip: import_zod53.z.number().optional().describe("Number to skip for pagination.")
38059
+ limit: import_zod54.z.number().optional().describe("Max locations to return. Defaults to 20."),
38060
+ skip: import_zod54.z.number().optional().describe("Number to skip for pagination.")
37914
38061
  },
37915
38062
  async ({ limit, skip }) => {
37916
38063
  try {
@@ -37954,21 +38101,21 @@ ${lines.join("\n")}
37954
38101
  "create_sub_account",
37955
38102
  "Create a NEW GHL sub-account (location) under the agency, optionally loading a snapshot at creation \u2014 the first step of fully programmatic client provisioning (create \u2192 Blueprint build \u2192 verify_funnel). Requires an agency-level API key with the locations.write scope (add via register_agency_key). After creating, this probes whether the agency key can operate the new location and auto-registers it in the token registry when it can (switch_location then works immediately); if the probe fails, create a Private Integration Token inside the new sub-account and run register_location. Company Firebase (register_company_firebase) covers the internal-API builder tools in the new location either way.",
37956
38103
  {
37957
- name: import_zod53.z.string().min(1).describe("Business / sub-account name."),
37958
- snapshot_id: import_zod53.z.string().optional().describe("Snapshot ID to load at creation (see list_snapshots)."),
37959
- company_id: import_zod53.z.string().optional().describe("Agency company ID. Defaults to the active location's registered companyId."),
37960
- first_name: import_zod53.z.string().optional().describe("Prospect/owner first name."),
37961
- last_name: import_zod53.z.string().optional().describe("Prospect/owner last name."),
37962
- email: import_zod53.z.string().email().optional().describe("Prospect/owner email."),
37963
- phone: import_zod53.z.string().optional().describe("Business phone (E.164, e.g. +15551234567)."),
37964
- address: import_zod53.z.string().optional().describe("Street address."),
37965
- city: import_zod53.z.string().optional(),
37966
- state: import_zod53.z.string().optional(),
37967
- postal_code: import_zod53.z.string().optional(),
37968
- country: import_zod53.z.string().optional().describe("Two-letter country code. Defaults to US."),
37969
- website: import_zod53.z.string().optional(),
37970
- timezone: import_zod53.z.string().optional().describe("IANA timezone, e.g. America/Los_Angeles."),
37971
- auto_register: import_zod53.z.boolean().optional().describe("Probe the new location with the agency key and add it to the token registry on success. Default true.")
38104
+ name: import_zod54.z.string().min(1).describe("Business / sub-account name."),
38105
+ snapshot_id: import_zod54.z.string().optional().describe("Snapshot ID to load at creation (see list_snapshots)."),
38106
+ company_id: import_zod54.z.string().optional().describe("Agency company ID. Defaults to the active location's registered companyId."),
38107
+ first_name: import_zod54.z.string().optional().describe("Prospect/owner first name."),
38108
+ last_name: import_zod54.z.string().optional().describe("Prospect/owner last name."),
38109
+ email: import_zod54.z.string().email().optional().describe("Prospect/owner email."),
38110
+ phone: import_zod54.z.string().optional().describe("Business phone (E.164, e.g. +15551234567)."),
38111
+ address: import_zod54.z.string().optional().describe("Street address."),
38112
+ city: import_zod54.z.string().optional(),
38113
+ state: import_zod54.z.string().optional(),
38114
+ postal_code: import_zod54.z.string().optional(),
38115
+ country: import_zod54.z.string().optional().describe("Two-letter country code. Defaults to US."),
38116
+ website: import_zod54.z.string().optional(),
38117
+ timezone: import_zod54.z.string().optional().describe("IANA timezone, e.g. America/Los_Angeles."),
38118
+ auto_register: import_zod54.z.boolean().optional().describe("Probe the new location with the agency key and add it to the token registry on success. Default true.")
37972
38119
  },
37973
38120
  async (args) => {
37974
38121
  if (readAgencyProfile().canCreateSubAccounts === false) return { content: [{ type: "text", text: "Create it in GoHighLevel and register it in GHL Command with its key." }], isError: true };
@@ -37991,9 +38138,9 @@ ${lines.join("\n")}
37991
38138
  "delete_sub_account",
37992
38139
  "Permanently delete a GHL sub-account (location) via the agency API. IRREVERSIBLE \u2014 every contact, workflow, funnel, and setting inside the sub-account is destroyed. Refuses to delete the currently active location. Also removes the location from the token registry. Requires an agency-level key with locations.write.",
37993
38140
  {
37994
- location_id: import_zod53.z.string().min(1).describe("The location ID to delete."),
37995
- confirm: import_zod53.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
37996
- delete_twilio: import_zod53.z.boolean().optional().describe("Also delete the location's Twilio sub-account. Default false.")
38141
+ location_id: import_zod54.z.string().min(1).describe("The location ID to delete."),
38142
+ confirm: import_zod54.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
38143
+ delete_twilio: import_zod54.z.boolean().optional().describe("Also delete the location's Twilio sub-account. Default false.")
37997
38144
  },
37998
38145
  async ({ location_id, delete_twilio }) => {
37999
38146
  const agencyKey = registry2?.getAgencyKey();
@@ -38029,11 +38176,11 @@ ${lines.join("\n")}
38029
38176
  }
38030
38177
  );
38031
38178
  }
38032
- var import_zod53, WRONG_LOCATION_KEY, DIFFERENT_LOCATION_KEY, switchChain;
38179
+ var import_zod54, WRONG_LOCATION_KEY, DIFFERENT_LOCATION_KEY, switchChain;
38033
38180
  var init_location_switcher = __esm({
38034
38181
  "src/tools/location-switcher.ts"() {
38035
38182
  "use strict";
38036
- import_zod53 = require("zod");
38183
+ import_zod54 = require("zod");
38037
38184
  init_ghl_client();
38038
38185
  init_sub_account_errors();
38039
38186
  init_agency_profile();
@@ -38059,8 +38206,8 @@ function registerBulkOperationTools(server2, client) {
38059
38206
  "bulk_add_tags",
38060
38207
  "Add tags to multiple contacts at once. Rate-limited to avoid API throttling. Returns a summary of successes and failures.",
38061
38208
  {
38062
- contactIds: import_zod54.z.array(import_zod54.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to tag."),
38063
- tags: import_zod54.z.array(import_zod54.z.string()).min(1, "At least one tag required.").describe("Tags to add to each contact.")
38209
+ contactIds: import_zod55.z.array(import_zod55.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to tag."),
38210
+ tags: import_zod55.z.array(import_zod55.z.string()).min(1, "At least one tag required.").describe("Tags to add to each contact.")
38064
38211
  },
38065
38212
  async ({ contactIds, tags }) => {
38066
38213
  const results = { success: 0, failed: 0, errors: [] };
@@ -38082,8 +38229,8 @@ function registerBulkOperationTools(server2, client) {
38082
38229
  "bulk_remove_tags",
38083
38230
  "Remove tags from multiple contacts at once. Rate-limited.",
38084
38231
  {
38085
- contactIds: import_zod54.z.array(import_zod54.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs."),
38086
- tags: import_zod54.z.array(import_zod54.z.string()).min(1, "At least one tag required.").describe("Tags to remove from each contact.")
38232
+ contactIds: import_zod55.z.array(import_zod55.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs."),
38233
+ tags: import_zod55.z.array(import_zod55.z.string()).min(1, "At least one tag required.").describe("Tags to remove from each contact.")
38087
38234
  },
38088
38235
  async ({ contactIds, tags }) => {
38089
38236
  const results = { success: 0, failed: 0, errors: [] };
@@ -38104,8 +38251,8 @@ function registerBulkOperationTools(server2, client) {
38104
38251
  "bulk_update_contacts",
38105
38252
  "Update the same field(s) on multiple contacts at once. Rate-limited. Example: set a custom field value, change source, update address for a batch of contacts.",
38106
38253
  {
38107
- contactIds: import_zod54.z.array(import_zod54.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
38108
- fields: import_zod54.z.record(import_zod54.z.string(), import_zod54.z.unknown()).describe("Fields to set on each contact (e.g. {customField: {id: 'xxx', value: 'yyy'}}, {source: 'Import'}).")
38254
+ contactIds: import_zod55.z.array(import_zod55.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
38255
+ fields: import_zod55.z.record(import_zod55.z.string(), import_zod55.z.unknown()).describe("Fields to set on each contact (e.g. {customField: {id: 'xxx', value: 'yyy'}}, {source: 'Import'}).")
38109
38256
  },
38110
38257
  async ({ contactIds, fields }) => {
38111
38258
  if (Object.keys(fields).length === 0) {
@@ -38131,8 +38278,8 @@ function registerBulkOperationTools(server2, client) {
38131
38278
  "bulk_add_to_workflow",
38132
38279
  "Enroll multiple contacts into a workflow at once. Rate-limited.",
38133
38280
  {
38134
- contactIds: import_zod54.z.array(import_zod54.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to enroll."),
38135
- workflowId: import_zod54.z.string().describe("The workflow ID to enroll contacts into.")
38281
+ contactIds: import_zod55.z.array(import_zod55.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to enroll."),
38282
+ workflowId: import_zod55.z.string().describe("The workflow ID to enroll contacts into.")
38136
38283
  },
38137
38284
  async ({ contactIds, workflowId }) => {
38138
38285
  const results = { success: 0, failed: 0, errors: [] };
@@ -38153,8 +38300,8 @@ function registerBulkOperationTools(server2, client) {
38153
38300
  "bulk_delete_contacts",
38154
38301
  "Delete multiple contacts at once. IRREVERSIBLE. Rate-limited. Use with extreme caution.",
38155
38302
  {
38156
- contactIds: import_zod54.z.array(import_zod54.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to permanently delete."),
38157
- confirm: import_zod54.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
38303
+ contactIds: import_zod55.z.array(import_zod55.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to permanently delete."),
38304
+ confirm: import_zod55.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
38158
38305
  },
38159
38306
  async ({ contactIds, confirm: confirm2 }) => {
38160
38307
  if (confirm2 !== "DELETE") {
@@ -38180,11 +38327,11 @@ ${deleted.join("\n")}` : "";
38180
38327
  }
38181
38328
  );
38182
38329
  }
38183
- var import_zod54;
38330
+ var import_zod55;
38184
38331
  var init_bulk_operations = __esm({
38185
38332
  "src/tools/bulk-operations.ts"() {
38186
38333
  "use strict";
38187
- import_zod54 = require("zod");
38334
+ import_zod55 = require("zod");
38188
38335
  init_tool_helpers();
38189
38336
  }
38190
38337
  });
@@ -38612,8 +38759,8 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
38612
38759
  "export_account",
38613
38760
  "Export a complete inventory of the GHL sub-account: location info, contacts (count + sample), pipelines with stages, workflows (with full actions if builder auth is configured), funnels with pages, forms, custom fields, custom values, tags, calendars, and users. Returns a comprehensive JSON report for auditing or backup.",
38614
38761
  {
38615
- locationId: import_zod55.z.string().optional().describe("Location ID to export. Uses default if not specified."),
38616
- includeContacts: import_zod55.z.boolean().optional().describe("Include contact list (first 100). Defaults to false for speed.")
38762
+ locationId: import_zod56.z.string().optional().describe("Location ID to export. Uses default if not specified."),
38763
+ includeContacts: import_zod56.z.boolean().optional().describe("Include contact list (first 100). Defaults to false for speed.")
38617
38764
  },
38618
38765
  async ({ locationId: locationId2, includeContacts }) => {
38619
38766
  try {
@@ -38743,8 +38890,8 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
38743
38890
  "compare_locations",
38744
38891
  "Compare two GHL sub-accounts side by side. For pipelines, workflows, custom fields, tags and forms it returns the count on each side plus the NAMES present in one account and missing from the other, so a setup gap reads directly. If a section cannot be read (commonly the API key has no access to the second location), that section is reported as unavailable with the reason \u2014 never as a count of zero.",
38745
38892
  {
38746
- locationA: import_zod55.z.string().describe("First Location ID."),
38747
- locationB: import_zod55.z.string().describe("Second Location ID.")
38893
+ locationA: import_zod56.z.string().describe("First Location ID."),
38894
+ locationB: import_zod56.z.string().describe("Second Location ID.")
38748
38895
  },
38749
38896
  async ({ locationA, locationB }) => {
38750
38897
  try {
@@ -38814,11 +38961,11 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
38814
38961
  "build_offboarding_kit",
38815
38962
  "Assemble the complete exit package for a sub-account: a full contact export, an inventory of everything the account contains, an access map of every person and connected account that can reach it, the recorded owner of each asset, a key-rotation checklist, and a plain-language handoff document the next operator can follow. Read-only: it changes nothing. locationId is REQUIRED and never defaults, because exporting the wrong client's account is the one mistake this tool must not be able to make. Assets nobody has recorded an owner for come back under ownership.unanswered \u2014 answer those with record_asset_ownership and they stay answered. Key VALUES are never included: GoHighLevel does not expose them to any tool, so the kit lists where each key lives instead.",
38816
38963
  {
38817
- locationId: import_zod55.z.string().describe("Sub-account to build the kit for. REQUIRED \u2014 this tool never falls back to the active account."),
38818
- contacts: import_zod55.z.enum(["summary", "csv"]).optional().describe(
38964
+ locationId: import_zod56.z.string().describe("Sub-account to build the kit for. REQUIRED \u2014 this tool never falls back to the active account."),
38965
+ contacts: import_zod56.z.enum(["summary", "csv"]).optional().describe(
38819
38966
  "'summary' (default) returns contact counts only. 'csv' also returns the full contact export as CSV text, which can be large \u2014 ask for it when you are ready to save the file."
38820
38967
  ),
38821
- maxContacts: import_zod55.z.number().int().positive().optional().describe(`Ceiling on contacts read. Defaults to ${DEFAULT_MAX_CONTACTS}. If the ceiling is hit, the kit says so in the handoff document itself rather than quietly returning a short list.`)
38968
+ maxContacts: import_zod56.z.number().int().positive().optional().describe(`Ceiling on contacts read. Defaults to ${DEFAULT_MAX_CONTACTS}. If the ceiling is hit, the kit says so in the handoff document itself rather than quietly returning a short list.`)
38822
38969
  },
38823
38970
  async ({ locationId: locationId2, contacts, maxContacts }) => {
38824
38971
  const originalKey = client.getApiKey();
@@ -38972,12 +39119,12 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
38972
39119
  "record_asset_ownership",
38973
39120
  "Record who owns an asset attached to a sub-account: the client, the agency, something borrowed for the engagement, or an outside vendor. Stored as custom values inside the client's own account, so the record outlives this tool and the client can read it without asking anyone. Answer these once at setup and build_offboarding_kit needs no interview later. Re-recording the same asset updates it in place instead of adding a duplicate. locationId is REQUIRED: writing ownership into the wrong client's account is worth preventing outright.",
38974
39121
  {
38975
- locationId: import_zod55.z.string().describe("Sub-account to record ownership in. REQUIRED \u2014 never falls back to the active account."),
38976
- assets: import_zod55.z.array(
38977
- import_zod55.z.object({
38978
- asset: import_zod55.z.string().describe("What it is, in the words you would say out loud. e.g. 'Website domain' or 'Phone number +16025551234'."),
38979
- owner: import_zod55.z.enum(OWNER_KINDS).describe("client = stays with them. agency = leaves with you. borrowed = must be returned or replaced at exit. third-party = an outside vendor controls it."),
38980
- note: import_zod55.z.string().optional().describe("Anything the next person needs, e.g. 'registered on the client's own GoDaddy account'.")
39122
+ locationId: import_zod56.z.string().describe("Sub-account to record ownership in. REQUIRED \u2014 never falls back to the active account."),
39123
+ assets: import_zod56.z.array(
39124
+ import_zod56.z.object({
39125
+ asset: import_zod56.z.string().describe("What it is, in the words you would say out loud. e.g. 'Website domain' or 'Phone number +16025551234'."),
39126
+ owner: import_zod56.z.enum(OWNER_KINDS).describe("client = stays with them. agency = leaves with you. borrowed = must be returned or replaced at exit. third-party = an outside vendor controls it."),
39127
+ note: import_zod56.z.string().optional().describe("Anything the next person needs, e.g. 'registered on the client's own GoDaddy account'.")
38981
39128
  })
38982
39129
  ).min(1).describe("One entry per asset. Send several at once.")
38983
39130
  },
@@ -39096,11 +39243,11 @@ function buildLocationComparison(dataA, dataB, sections) {
39096
39243
  }
39097
39244
  return { comparison, unavailable };
39098
39245
  }
39099
- var import_zod55, OFF_PLATFORM_ASSETS;
39246
+ var import_zod56, OFF_PLATFORM_ASSETS;
39100
39247
  var init_account_export = __esm({
39101
39248
  "src/tools/account-export.ts"() {
39102
39249
  "use strict";
39103
- import_zod55 = require("zod");
39250
+ import_zod56 = require("zod");
39104
39251
  init_tool_helpers();
39105
39252
  init_contact_export();
39106
39253
  init_asset_ownership();
@@ -39125,8 +39272,8 @@ function registerWorkflowClonerTools(server2, builderClient) {
39125
39272
  "clone_workflow",
39126
39273
  "Deep clone a workflow \u2014 creates an exact copy with new IDs for all actions, triggers, and references. The clone starts as a draft. Useful for creating templates or duplicating workflows across projects.",
39127
39274
  {
39128
- sourceWorkflowId: import_zod56.z.string().describe("The workflow ID to clone."),
39129
- newName: import_zod56.z.string().describe("Name for the cloned workflow.")
39275
+ sourceWorkflowId: import_zod57.z.string().describe("The workflow ID to clone."),
39276
+ newName: import_zod57.z.string().describe("Name for the cloned workflow.")
39130
39277
  },
39131
39278
  async ({ sourceWorkflowId, newName }) => {
39132
39279
  try {
@@ -39254,11 +39401,11 @@ function registerWorkflowClonerTools(server2, builderClient) {
39254
39401
  }
39255
39402
  );
39256
39403
  }
39257
- var import_zod56, crypto6;
39404
+ var import_zod57, crypto6;
39258
39405
  var init_workflow_cloner = __esm({
39259
39406
  "src/tools/workflow-cloner.ts"() {
39260
39407
  "use strict";
39261
- import_zod56 = require("zod");
39408
+ import_zod57 = require("zod");
39262
39409
  crypto6 = __toESM(require("crypto"));
39263
39410
  }
39264
39411
  });
@@ -39300,11 +39447,11 @@ ${text2}`);
39300
39447
  "list_smart_lists",
39301
39448
  "List smart lists (dynamic / saved-filter lists) in a location. Smart Lists are saved searches over contacts or opportunities \u2014 agencies use them to segment by complex criteria. Filters and columns aren't returned in the list view; use get_smart_list for the full filter spec.",
39302
39449
  {
39303
- objectKey: import_zod57.z.enum(OBJECT_KEYS).describe("The object type the lists segment over. 'contacts' for contact-segments, 'opportunity' for opportunity-segments. Required \u2014 GHL rejects requests without it."),
39304
- query: import_zod57.z.string().optional().describe("Free-text search across smart list names."),
39305
- limit: import_zod57.z.number().optional().describe("Max smart lists per page. Defaults to 20 on GHL's side."),
39306
- startAfter: import_zod57.z.string().optional().describe("Cursor for pagination \u2014 pass the last list's id from the previous page."),
39307
- locationId: import_zod57.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39450
+ objectKey: import_zod58.z.enum(OBJECT_KEYS).describe("The object type the lists segment over. 'contacts' for contact-segments, 'opportunity' for opportunity-segments. Required \u2014 GHL rejects requests without it."),
39451
+ query: import_zod58.z.string().optional().describe("Free-text search across smart list names."),
39452
+ limit: import_zod58.z.number().optional().describe("Max smart lists per page. Defaults to 20 on GHL's side."),
39453
+ startAfter: import_zod58.z.string().optional().describe("Cursor for pagination \u2014 pass the last list's id from the previous page."),
39454
+ locationId: import_zod58.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39308
39455
  },
39309
39456
  async ({ objectKey, query, limit, startAfter, locationId: locationId2 }) => {
39310
39457
  try {
@@ -39337,10 +39484,10 @@ ${await res.text()}`);
39337
39484
  "create_contact_smart_list",
39338
39485
  "Create a CONTACT smart list (saved filter over contacts) with working filters \u2014 the owner-exception views a dashboard needs. Uses GHL's contacts/smartlist service and its captured OR-of-AND filter envelope. `tagFilters` is the common case: each entry is a list of tag names the contact must ALL carry. For anything else pass `rawFilters` (leaf objects: {field, operator, value, options}). NOTE: create_smart_list (the older tool) targets the opportunity-list service and cannot express contact filters.",
39339
39486
  {
39340
- name: import_zod57.z.string().describe("Display name for the smart list."),
39341
- tagFilters: import_zod57.z.array(import_zod57.z.array(import_zod57.z.string())).optional().describe('Tag conditions. Each inner array is one filter requiring ALL those tags, e.g. [["escalated"]] for "has the escalated tag".'),
39342
- rawFilters: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Advanced: raw filter leaves {field, operator, value, options}. Combined with tagFilters into one AND group."),
39343
- locationId: import_zod57.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39487
+ name: import_zod58.z.string().describe("Display name for the smart list."),
39488
+ tagFilters: import_zod58.z.array(import_zod58.z.array(import_zod58.z.string())).optional().describe('Tag conditions. Each inner array is one filter requiring ALL those tags, e.g. [["escalated"]] for "has the escalated tag".'),
39489
+ rawFilters: import_zod58.z.array(import_zod58.z.record(import_zod58.z.string(), import_zod58.z.unknown())).optional().describe("Advanced: raw filter leaves {field, operator, value, options}. Combined with tagFilters into one AND group."),
39490
+ locationId: import_zod58.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39344
39491
  },
39345
39492
  async ({ name, tagFilters, rawFilters, locationId: locationId2 }) => {
39346
39493
  try {
@@ -39364,7 +39511,7 @@ ${await res.text()}`);
39364
39511
  "get_contact_smart_list",
39365
39512
  "Get a CONTACT smart list by id INCLUDING its filterSpecs. Use this to verify a contact smart list's filters after creating it \u2014 get_smart_list targets the opportunity-list service and returns the record without any filters, which reads as 'no filters set'.",
39366
39513
  {
39367
- listId: import_zod57.z.string().describe("The contact smart list id.")
39514
+ listId: import_zod58.z.string().describe("The contact smart list id.")
39368
39515
  },
39369
39516
  async ({ listId }) => {
39370
39517
  try {
@@ -39382,8 +39529,8 @@ ${await res.text()}`);
39382
39529
  "delete_contact_smart_list",
39383
39530
  "Delete a CONTACT smart list by id.",
39384
39531
  {
39385
- listId: import_zod57.z.string().describe("The contact smart list id."),
39386
- confirm: import_zod57.z.literal("DELETE").describe('Type "DELETE" to confirm.')
39532
+ listId: import_zod58.z.string().describe("The contact smart list id."),
39533
+ confirm: import_zod58.z.literal("DELETE").describe('Type "DELETE" to confirm.')
39387
39534
  },
39388
39535
  async ({ listId }) => {
39389
39536
  try {
@@ -39401,8 +39548,8 @@ ${await res.text()}`);
39401
39548
  "get_smart_list",
39402
39549
  "Get a single smart list by ID with its full configuration: filters, columns, permissions, and metadata. The filters array is what defines who/what is in the list.",
39403
39550
  {
39404
- listId: import_zod57.z.string().describe("The smart list ID (from list_smart_lists or a previous create_smart_list response)."),
39405
- locationId: import_zod57.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39551
+ listId: import_zod58.z.string().describe("The smart list ID (from list_smart_lists or a previous create_smart_list response)."),
39552
+ locationId: import_zod58.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39406
39553
  },
39407
39554
  async ({ listId, locationId: locationId2 }) => {
39408
39555
  try {
@@ -39418,13 +39565,13 @@ ${await res.text()}`);
39418
39565
  "create_smart_list",
39419
39566
  "Create a new smart list (dynamic filter list). Required: name + objectKey. Filters define the saved-search criteria \u2014 pass an empty array to create an empty list and add filters later via update_smart_list. The shape of filters/columns is opaque here; query an existing smart list with get_smart_list to see the format GHL expects.",
39420
39567
  {
39421
- name: import_zod57.z.string().describe("Display name for the smart list."),
39422
- objectKey: import_zod57.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
39423
- filters: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Array of filter objects. Each object has fields like {field, operator, value} \u2014 exact shape varies by filter type. See get_smart_list on an existing list to learn the format."),
39424
- columns: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Array of column definitions for the smart list view in GHL UI. Each defines which contact/opportunity field shows as a column. Defaults to GHL's standard columns if omitted."),
39425
- pipelineIds: import_zod57.z.array(import_zod57.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs to restrict this smart list to. Empty array = all pipelines."),
39426
- defaultInPipelines: import_zod57.z.array(import_zod57.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
39427
- locationId: import_zod57.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39568
+ name: import_zod58.z.string().describe("Display name for the smart list."),
39569
+ objectKey: import_zod58.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
39570
+ filters: import_zod58.z.array(import_zod58.z.record(import_zod58.z.string(), import_zod58.z.unknown())).optional().describe("Array of filter objects. Each object has fields like {field, operator, value} \u2014 exact shape varies by filter type. See get_smart_list on an existing list to learn the format."),
39571
+ columns: import_zod58.z.array(import_zod58.z.record(import_zod58.z.string(), import_zod58.z.unknown())).optional().describe("Array of column definitions for the smart list view in GHL UI. Each defines which contact/opportunity field shows as a column. Defaults to GHL's standard columns if omitted."),
39572
+ pipelineIds: import_zod58.z.array(import_zod58.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs to restrict this smart list to. Empty array = all pipelines."),
39573
+ defaultInPipelines: import_zod58.z.array(import_zod58.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
39574
+ locationId: import_zod58.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39428
39575
  },
39429
39576
  async ({ name, objectKey, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
39430
39577
  try {
@@ -39445,13 +39592,13 @@ ${await res.text()}`);
39445
39592
  "update_smart_list",
39446
39593
  "Update an existing smart list's name, filters, or columns. The objectKey CANNOT be changed after creation (GHL rejects with 422 if you try). Use get_smart_list first to inspect the current filters; partial updates work \u2014 pass only the fields you want to change.",
39447
39594
  {
39448
- listId: import_zod57.z.string().describe("The smart list ID to update."),
39449
- name: import_zod57.z.string().optional().describe("New display name."),
39450
- filters: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Replace the filter array entirely. To add a filter, fetch the current list, append, and pass the new array."),
39451
- columns: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())).optional().describe("Replace the column array entirely."),
39452
- pipelineIds: import_zod57.z.array(import_zod57.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
39453
- defaultInPipelines: import_zod57.z.array(import_zod57.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
39454
- locationId: import_zod57.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39595
+ listId: import_zod58.z.string().describe("The smart list ID to update."),
39596
+ name: import_zod58.z.string().optional().describe("New display name."),
39597
+ filters: import_zod58.z.array(import_zod58.z.record(import_zod58.z.string(), import_zod58.z.unknown())).optional().describe("Replace the filter array entirely. To add a filter, fetch the current list, append, and pass the new array."),
39598
+ columns: import_zod58.z.array(import_zod58.z.record(import_zod58.z.string(), import_zod58.z.unknown())).optional().describe("Replace the column array entirely."),
39599
+ pipelineIds: import_zod58.z.array(import_zod58.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
39600
+ defaultInPipelines: import_zod58.z.array(import_zod58.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
39601
+ locationId: import_zod58.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39455
39602
  },
39456
39603
  async ({ listId, name, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
39457
39604
  try {
@@ -39476,9 +39623,9 @@ ${await res.text()}`);
39476
39623
  "delete_smart_list",
39477
39624
  "Permanently delete a smart list. IRREVERSIBLE. The list configuration is removed but the contacts/opportunities themselves are NOT touched \u2014 smart lists are just saved filters. Any workflow trigger / dashboard / report that referenced this list by ID will stop working.",
39478
39625
  {
39479
- listId: import_zod57.z.string().describe("The smart list ID to delete."),
39480
- confirm: import_zod57.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
39481
- locationId: import_zod57.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39626
+ listId: import_zod58.z.string().describe("The smart list ID to delete."),
39627
+ confirm: import_zod58.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
39628
+ locationId: import_zod58.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39482
39629
  },
39483
39630
  async ({ listId, locationId: locationId2 }) => {
39484
39631
  try {
@@ -39491,11 +39638,11 @@ ${await res.text()}`);
39491
39638
  }
39492
39639
  );
39493
39640
  }
39494
- var import_zod57, SMARTLIST_BASE, OBJECT_KEYS, CONTACT_SMARTLIST_BASE, CONTACT_SMARTLIST_DELETE, DEFAULT_CONTACT_SMARTLIST_COLUMNS;
39641
+ var import_zod58, SMARTLIST_BASE, OBJECT_KEYS, CONTACT_SMARTLIST_BASE, CONTACT_SMARTLIST_DELETE, DEFAULT_CONTACT_SMARTLIST_COLUMNS;
39495
39642
  var init_smart_lists = __esm({
39496
39643
  "src/tools/smart-lists.ts"() {
39497
39644
  "use strict";
39498
- import_zod57 = require("zod");
39645
+ import_zod58 = require("zod");
39499
39646
  init_workflow_builder_client();
39500
39647
  init_tool_helpers();
39501
39648
  SMARTLIST_BASE = "https://backend.leadconnectorhq.com/lists/dynamic";
@@ -39534,7 +39681,7 @@ ${text2}`);
39534
39681
  "get_review_link_list",
39535
39682
  "List the review-link destinations configured for a location \u2014 the platforms (Google, Facebook, etc.) where review requests send contacts. Each entry has a label and the public review URL. Useful for: building review-request workflows (the workflow goal condition `review_request_clicked` references these review-link ids), and auditing which review platforms a sub-account has connected.",
39536
39683
  {
39537
- locationId: import_zod58.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39684
+ locationId: import_zod59.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39538
39685
  },
39539
39686
  async ({ locationId: locationId2 }) => {
39540
39687
  const loc = locationId2 ?? client.locationId;
@@ -39546,13 +39693,13 @@ ${text2}`);
39546
39693
  "list_reviews",
39547
39694
  "List the reviews a location has received (Google, Facebook, etc.) with rating, author, text, reply status, sentiment score, and source. Filter by an exact star rating or a range \u2014 'everything under 4 stars' is minRating 1 + maxRating 3. NOTE: location is resolved through nested filter params internally \u2014 a flat locationId is what caused the long-standing 'No Location Found' error, now fixed. Responding to a review is not yet available via API. Requires Firebase auth.",
39548
39695
  {
39549
- locationId: import_zod58.z.string().optional().describe("Location ID. Falls back to the active builder client's location."),
39550
- pageNumber: import_zod58.z.number().optional().describe("1-based page number. Defaults to 1."),
39551
- pageSize: import_zod58.z.number().optional().describe("Results per page. Defaults to 10."),
39552
- rating: import_zod58.z.number().min(1).max(5).optional().describe("Exact star rating (1-5). Takes precedence over minRating/maxRating."),
39553
- minRating: import_zod58.z.number().min(1).max(5).optional().describe("Lowest star rating to include (1-5), inclusive."),
39554
- maxRating: import_zod58.z.number().min(1).max(5).optional().describe("Highest star rating to include (1-5), inclusive. For 'under 4 stars', pass 3."),
39555
- includeDeleted: import_zod58.z.boolean().optional().describe("Include deleted reviews. Defaults to false.")
39696
+ locationId: import_zod59.z.string().optional().describe("Location ID. Falls back to the active builder client's location."),
39697
+ pageNumber: import_zod59.z.number().optional().describe("1-based page number. Defaults to 1."),
39698
+ pageSize: import_zod59.z.number().optional().describe("Results per page. Defaults to 10."),
39699
+ rating: import_zod59.z.number().min(1).max(5).optional().describe("Exact star rating (1-5). Takes precedence over minRating/maxRating."),
39700
+ minRating: import_zod59.z.number().min(1).max(5).optional().describe("Lowest star rating to include (1-5), inclusive."),
39701
+ maxRating: import_zod59.z.number().min(1).max(5).optional().describe("Highest star rating to include (1-5), inclusive. For 'under 4 stars', pass 3."),
39702
+ includeDeleted: import_zod59.z.boolean().optional().describe("Include deleted reviews. Defaults to false.")
39556
39703
  },
39557
39704
  async ({ locationId: locationId2, pageNumber, pageSize, rating, minRating, maxRating, includeDeleted }) => {
39558
39705
  const loc = locationId2 ?? client.locationId;
@@ -39590,11 +39737,11 @@ function buildReviewsQuery(locationId2, opts = {}) {
39590
39737
  }).join("&");
39591
39738
  return `/reviews?${query}`;
39592
39739
  }
39593
- var import_zod58, REPUTATION_BASE;
39740
+ var import_zod59, REPUTATION_BASE;
39594
39741
  var init_reputation = __esm({
39595
39742
  "src/tools/reputation.ts"() {
39596
39743
  "use strict";
39597
- import_zod58 = require("zod");
39744
+ import_zod59 = require("zod");
39598
39745
  init_workflow_builder_client();
39599
39746
  init_tool_helpers();
39600
39747
  REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
@@ -39610,15 +39757,15 @@ function registerEmailCampaignTools(server2, builderClient) {
39610
39757
  "create_email_campaign",
39611
39758
  "Create an email campaign / broadcast DRAFT from an existing email template. Requires a templateId (create one first with create_email_template). The campaign is created as a draft \u2014 to actually SEND or schedule it, finish in the GHL UI: the send/schedule endpoint isn't available through the API yet. There's also no API delete for campaigns, so drafts created here are removed via the GHL UI. Despite those limits, this gets the campaign 90% built \u2014 template, subject, sender, name all set programmatically.",
39612
39759
  {
39613
- templateId: import_zod59.z.string().describe("ID of an email template (from create_email_template or list_email_templates) to use as the campaign body."),
39614
- name: import_zod59.z.string().optional().describe("Internal campaign name (shown in the campaigns list, not to recipients). Defaults to a GHL-generated name."),
39615
- subject: import_zod59.z.string().optional().describe("Email subject line recipients see."),
39616
- fromName: import_zod59.z.string().optional().describe("Sender display name."),
39617
- fromEmail: import_zod59.z.string().optional().describe("Sender email address. Must be a verified sending address in the location."),
39618
- isPlainText: import_zod59.z.boolean().optional().describe("Send as plain text instead of HTML. Defaults to false."),
39619
- enableResendToUnopened: import_zod59.z.boolean().optional().describe("Auto-resend to contacts who didn't open. Defaults to false."),
39620
- hasUtmTracking: import_zod59.z.boolean().optional().describe("Append UTM tracking params to links. Defaults to false."),
39621
- locationId: import_zod59.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39760
+ templateId: import_zod60.z.string().describe("ID of an email template (from create_email_template or list_email_templates) to use as the campaign body."),
39761
+ name: import_zod60.z.string().optional().describe("Internal campaign name (shown in the campaigns list, not to recipients). Defaults to a GHL-generated name."),
39762
+ subject: import_zod60.z.string().optional().describe("Email subject line recipients see."),
39763
+ fromName: import_zod60.z.string().optional().describe("Sender display name."),
39764
+ fromEmail: import_zod60.z.string().optional().describe("Sender email address. Must be a verified sending address in the location."),
39765
+ isPlainText: import_zod60.z.boolean().optional().describe("Send as plain text instead of HTML. Defaults to false."),
39766
+ enableResendToUnopened: import_zod60.z.boolean().optional().describe("Auto-resend to contacts who didn't open. Defaults to false."),
39767
+ hasUtmTracking: import_zod60.z.boolean().optional().describe("Append UTM tracking params to links. Defaults to false."),
39768
+ locationId: import_zod60.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39622
39769
  },
39623
39770
  async ({ templateId, name, subject, fromName, fromEmail, isPlainText, enableResendToUnopened, hasUtmTracking, locationId: locationId2 }) => {
39624
39771
  const loc = locationId2 ?? client.locationId;
@@ -39645,11 +39792,11 @@ ${text2}`);
39645
39792
  }
39646
39793
  );
39647
39794
  }
39648
- var import_zod59, SVC_BASE;
39795
+ var import_zod60, SVC_BASE;
39649
39796
  var init_email_campaigns = __esm({
39650
39797
  "src/tools/email-campaigns.ts"() {
39651
39798
  "use strict";
39652
- import_zod59 = require("zod");
39799
+ import_zod60 = require("zod");
39653
39800
  init_workflow_builder_client();
39654
39801
  init_tool_helpers();
39655
39802
  SVC_BASE = "https://services.leadconnectorhq.com";
@@ -39676,7 +39823,7 @@ ${text2}`);
39676
39823
  "list_membership_offers",
39677
39824
  "List a location's membership offers and products in one call. Returns { products: [...], offers: [...] }. Products are courses/communities; offers are the access grants (what a contact gets enrolled in). Use the returned ids with membership trigger conditions like offer_access_granted / product_completed.",
39678
39825
  {
39679
- locationId: import_zod60.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39826
+ locationId: import_zod61.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39680
39827
  },
39681
39828
  async ({ locationId: locationId2 }) => {
39682
39829
  const loc = locationId2 ?? client.locationId;
@@ -39688,8 +39835,8 @@ ${text2}`);
39688
39835
  "list_membership_categories",
39689
39836
  "List all membership/course categories in a location. Categories group lessons inside a course/product. Use the returned ids with the category_completed / category_started trigger conditions. READ-ONLY.",
39690
39837
  {
39691
- limit: import_zod60.z.number().optional().describe("Max categories to return. Defaults to a large value (effectively all)."),
39692
- locationId: import_zod60.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39838
+ limit: import_zod61.z.number().optional().describe("Max categories to return. Defaults to a large value (effectively all)."),
39839
+ locationId: import_zod61.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39693
39840
  },
39694
39841
  async ({ limit, locationId: locationId2 }) => {
39695
39842
  const loc = locationId2 ?? client.locationId;
@@ -39701,8 +39848,8 @@ ${text2}`);
39701
39848
  "list_membership_lessons",
39702
39849
  "List all membership/course lessons in a location. Use the returned ids with the lesson_completed / lesson_started trigger conditions. READ-ONLY.",
39703
39850
  {
39704
- limit: import_zod60.z.number().optional().describe("Max lessons to return. Defaults to a large value (effectively all)."),
39705
- locationId: import_zod60.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39851
+ limit: import_zod61.z.number().optional().describe("Max lessons to return. Defaults to a large value (effectively all)."),
39852
+ locationId: import_zod61.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39706
39853
  },
39707
39854
  async ({ limit, locationId: locationId2 }) => {
39708
39855
  const loc = locationId2 ?? client.locationId;
@@ -39714,9 +39861,9 @@ ${text2}`);
39714
39861
  "create_course",
39715
39862
  "Create a membership course (a 'product') in a location. Creates the course shell with a title and description; add categories (create_membership_category) and lessons (create_membership_lesson) into it, and an offer (create_membership_offer) to grant access. Returns the new product, including its id. Requires Firebase auth. NOTE: GoHighLevel's membership API exposes no delete or update for this, so it cannot be removed or renamed through the product afterwards \u2014 only in the GHL UI. Create deliberately.",
39716
39863
  {
39717
- title: import_zod60.z.string().describe("Course title."),
39718
- description: import_zod60.z.string().optional().describe("Course description. Defaults to empty."),
39719
- locationId: import_zod60.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39864
+ title: import_zod61.z.string().describe("Course title."),
39865
+ description: import_zod61.z.string().optional().describe("Course description. Defaults to empty."),
39866
+ locationId: import_zod61.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39720
39867
  },
39721
39868
  async ({ title: title2, description, locationId: locationId2 }) => {
39722
39869
  const loc = locationId2 ?? client.locationId;
@@ -39728,13 +39875,13 @@ ${text2}`);
39728
39875
  "create_membership_category",
39729
39876
  "Create a category (module/section) inside a membership course. Categories group lessons. Needs the productId of the course (from create_course or list_membership_offers). Returns the new category, including its id (use it as categoryId when creating lessons). Requires Firebase auth. NOTE: GoHighLevel's membership API exposes no delete or update for this, so it cannot be removed or renamed through the product afterwards \u2014 only in the GHL UI. Create deliberately.",
39730
39877
  {
39731
- title: import_zod60.z.string().describe("Category title (e.g. 'Module 1')."),
39732
- productId: import_zod60.z.string().describe("The course/product id this category belongs to."),
39733
- description: import_zod60.z.string().optional().describe("Category description. Defaults to empty."),
39734
- visibility: import_zod60.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
39735
- sequenceNo: import_zod60.z.number().optional().describe("Display order within the course. Defaults to 0."),
39736
- dripDays: import_zod60.z.number().optional().describe("Days after enrollment before this category unlocks. Defaults to 0 (no drip)."),
39737
- locationId: import_zod60.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39878
+ title: import_zod61.z.string().describe("Category title (e.g. 'Module 1')."),
39879
+ productId: import_zod61.z.string().describe("The course/product id this category belongs to."),
39880
+ description: import_zod61.z.string().optional().describe("Category description. Defaults to empty."),
39881
+ visibility: import_zod61.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
39882
+ sequenceNo: import_zod61.z.number().optional().describe("Display order within the course. Defaults to 0."),
39883
+ dripDays: import_zod61.z.number().optional().describe("Days after enrollment before this category unlocks. Defaults to 0 (no drip)."),
39884
+ locationId: import_zod61.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39738
39885
  },
39739
39886
  async ({ title: title2, productId, description, visibility, sequenceNo, dripDays, locationId: locationId2 }) => {
39740
39887
  const loc = locationId2 ?? client.locationId;
@@ -39746,14 +39893,14 @@ ${text2}`);
39746
39893
  "create_membership_lesson",
39747
39894
  "Create a lesson (a 'post') inside a membership course category. Needs both the categoryId (from create_membership_category) and the productId of the course. Description is the lesson body as HTML. Returns the new lesson, including its id. Requires Firebase auth. NOTE: GoHighLevel's membership API exposes no delete or update for this, so it cannot be removed or renamed through the product afterwards \u2014 only in the GHL UI. Create deliberately.",
39748
39895
  {
39749
- title: import_zod60.z.string().describe("Lesson title."),
39750
- categoryId: import_zod60.z.string().describe("The category id this lesson belongs to (from create_membership_category)."),
39751
- productId: import_zod60.z.string().describe("The course/product id this lesson belongs to."),
39752
- description: import_zod60.z.string().optional().describe("Lesson body as HTML. Defaults to empty."),
39753
- contentType: import_zod60.z.enum(["video", "audio", "text", "pdf", "assignment"]).optional().describe("Lesson content type. Defaults to 'video'."),
39754
- visibility: import_zod60.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
39755
- sequenceNo: import_zod60.z.number().optional().describe("Display order within the category. Defaults to 0."),
39756
- locationId: import_zod60.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39896
+ title: import_zod61.z.string().describe("Lesson title."),
39897
+ categoryId: import_zod61.z.string().describe("The category id this lesson belongs to (from create_membership_category)."),
39898
+ productId: import_zod61.z.string().describe("The course/product id this lesson belongs to."),
39899
+ description: import_zod61.z.string().optional().describe("Lesson body as HTML. Defaults to empty."),
39900
+ contentType: import_zod61.z.enum(["video", "audio", "text", "pdf", "assignment"]).optional().describe("Lesson content type. Defaults to 'video'."),
39901
+ visibility: import_zod61.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
39902
+ sequenceNo: import_zod61.z.number().optional().describe("Display order within the category. Defaults to 0."),
39903
+ locationId: import_zod61.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39757
39904
  },
39758
39905
  async ({ title: title2, categoryId, productId, description, contentType, visibility, sequenceNo, locationId: locationId2 }) => {
39759
39906
  const loc = locationId2 ?? client.locationId;
@@ -39765,12 +39912,12 @@ ${text2}`);
39765
39912
  "create_membership_offer",
39766
39913
  "Create a membership offer \u2014 the access grant that enrolls contacts into one or more courses/products. Link it to course product ids. Defaults to a free offer; for paid, set type to 'recurring' or 'one_time' with an amount. Returns the new offer, including its id (referenced by the offer_access_granted trigger). Requires Firebase auth. NOTE: GoHighLevel's membership API exposes no delete or update for this, so it cannot be removed or renamed through the product afterwards \u2014 only in the GHL UI. Create deliberately.",
39767
39914
  {
39768
- title: import_zod60.z.string().describe("Offer title (shown at checkout / in the offer list)."),
39769
- productIds: import_zod60.z.array(import_zod60.z.string()).describe("Course/product ids this offer grants access to (from create_course or list_membership_offers)."),
39770
- type: import_zod60.z.enum(["free", "recurring", "one_time"]).optional().describe("Offer type. Defaults to 'free'."),
39771
- amount: import_zod60.z.number().optional().describe("Price for paid offers. Defaults to 0 (free)."),
39772
- currency: import_zod60.z.string().optional().describe("Currency code for paid offers. Defaults to 'USD'."),
39773
- locationId: import_zod60.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39915
+ title: import_zod61.z.string().describe("Offer title (shown at checkout / in the offer list)."),
39916
+ productIds: import_zod61.z.array(import_zod61.z.string()).describe("Course/product ids this offer grants access to (from create_course or list_membership_offers)."),
39917
+ type: import_zod61.z.enum(["free", "recurring", "one_time"]).optional().describe("Offer type. Defaults to 'free'."),
39918
+ amount: import_zod61.z.number().optional().describe("Price for paid offers. Defaults to 0 (free)."),
39919
+ currency: import_zod61.z.string().optional().describe("Currency code for paid offers. Defaults to 'USD'."),
39920
+ locationId: import_zod61.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
39774
39921
  },
39775
39922
  async ({ title: title2, productIds, type, amount, currency, locationId: locationId2 }) => {
39776
39923
  const loc = locationId2 ?? client.locationId;
@@ -39837,11 +39984,11 @@ function buildOfferPayload(o) {
39837
39984
  currency: o.currency ?? "USD"
39838
39985
  };
39839
39986
  }
39840
- var import_zod60, MEMBERSHIP_BASE2;
39987
+ var import_zod61, MEMBERSHIP_BASE2;
39841
39988
  var init_memberships = __esm({
39842
39989
  "src/tools/memberships.ts"() {
39843
39990
  "use strict";
39844
- import_zod60 = require("zod");
39991
+ import_zod61 = require("zod");
39845
39992
  init_workflow_builder_client();
39846
39993
  init_tool_helpers();
39847
39994
  MEMBERSHIP_BASE2 = "https://backend.leadconnectorhq.com/membership";
@@ -39937,7 +40084,7 @@ function registerTemplateDeployerTools(server2, client) {
39937
40084
  "get_template_questionnaire",
39938
40085
  "Get the questionnaire for a specific template. Returns all the questions that need to be answered before deploying. Present these to the user one at a time in a conversational style.",
39939
40086
  {
39940
- templateFile: import_zod61.z.string().describe("Path to the template JSON file (from list_templates).")
40087
+ templateFile: import_zod62.z.string().describe("Path to the template JSON file (from list_templates).")
39941
40088
  },
39942
40089
  async ({ templateFile }) => {
39943
40090
  try {
@@ -39970,11 +40117,11 @@ function registerTemplateDeployerTools(server2, client) {
39970
40117
  "deploy_template",
39971
40118
  "Deploy a template to set up a GHL sub-account. Creates tags, custom fields, pipelines with stages, calendars, workflows, and forms based on the template and the user's questionnaire answers. This is the main setup automation tool.",
39972
40119
  {
39973
- templateFile: import_zod61.z.string().describe("Path to the template JSON file."),
39974
- answers: import_zod61.z.record(import_zod61.z.string(), import_zod61.z.unknown()).describe("Questionnaire answers keyed by question ID (e.g. {business_name: 'My Clinic', business_phone: '+15551234567', ...})."),
39975
- locationId: import_zod61.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
39976
- dryRun: import_zod61.z.boolean().optional().describe("If true, shows what would be created without actually creating anything. Defaults to false."),
39977
- overwriteBusinessInfo: import_zod61.z.boolean().optional().describe(
40120
+ templateFile: import_zod62.z.string().describe("Path to the template JSON file."),
40121
+ answers: import_zod62.z.record(import_zod62.z.string(), import_zod62.z.unknown()).describe("Questionnaire answers keyed by question ID (e.g. {business_name: 'My Clinic', business_phone: '+15551234567', ...})."),
40122
+ locationId: import_zod62.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
40123
+ dryRun: import_zod62.z.boolean().optional().describe("If true, shows what would be created without actually creating anything. Defaults to false."),
40124
+ overwriteBusinessInfo: import_zod62.z.boolean().optional().describe(
39978
40125
  "Allow this deploy to CHANGE business details the sub-account already has (legal name, address, city, state, postal code, country, phone, email, website). Off by default: those are the fields A2P / 10DLC brand registration is filed against, so changing them on a live account can put SMS registration at risk. Blank fields are always filled without needing this. Only pass true when you intend to correct the account's business record and know its A2P state."
39979
40126
  )
39980
40127
  },
@@ -40354,45 +40501,45 @@ ${errors.join("\n")}` : dryRun ? "\nNo errors!" : "\nNo errors \u2014 every item
40354
40501
  }
40355
40502
  );
40356
40503
  }
40357
- var import_zod61, fs28, path27, TemplateSchema;
40504
+ var import_zod62, fs28, path27, TemplateSchema;
40358
40505
  var init_template_deployer = __esm({
40359
40506
  "src/tools/template-deployer.ts"() {
40360
40507
  "use strict";
40361
- import_zod61 = require("zod");
40508
+ import_zod62 = require("zod");
40362
40509
  init_workflow_builder_client();
40363
40510
  init_tool_helpers();
40364
40511
  fs28 = __toESM(require("fs"));
40365
40512
  path27 = __toESM(require("path"));
40366
40513
  init_workflow_builder_client();
40367
- TemplateSchema = import_zod61.z.object({
40368
- templateName: import_zod61.z.string(),
40369
- templateVersion: import_zod61.z.string().optional(),
40370
- description: import_zod61.z.string().optional().default(""),
40371
- questionnaire: import_zod61.z.array(import_zod61.z.object({
40372
- id: import_zod61.z.string(),
40373
- question: import_zod61.z.string(),
40374
- type: import_zod61.z.string(),
40375
- required: import_zod61.z.boolean().optional(),
40376
- placeholder: import_zod61.z.string().optional()
40514
+ TemplateSchema = import_zod62.z.object({
40515
+ templateName: import_zod62.z.string(),
40516
+ templateVersion: import_zod62.z.string().optional(),
40517
+ description: import_zod62.z.string().optional().default(""),
40518
+ questionnaire: import_zod62.z.array(import_zod62.z.object({
40519
+ id: import_zod62.z.string(),
40520
+ question: import_zod62.z.string(),
40521
+ type: import_zod62.z.string(),
40522
+ required: import_zod62.z.boolean().optional(),
40523
+ placeholder: import_zod62.z.string().optional()
40377
40524
  })).optional().default([]),
40378
- location: import_zod61.z.record(import_zod61.z.string(), import_zod61.z.unknown()).optional(),
40379
- tags: import_zod61.z.array(import_zod61.z.string()).optional(),
40380
- customFields: import_zod61.z.array(import_zod61.z.object({
40381
- name: import_zod61.z.string(),
40382
- dataType: import_zod61.z.string()
40525
+ location: import_zod62.z.record(import_zod62.z.string(), import_zod62.z.unknown()).optional(),
40526
+ tags: import_zod62.z.array(import_zod62.z.string()).optional(),
40527
+ customFields: import_zod62.z.array(import_zod62.z.object({
40528
+ name: import_zod62.z.string(),
40529
+ dataType: import_zod62.z.string()
40383
40530
  })).optional(),
40384
- pipelines: import_zod61.z.array(import_zod61.z.object({
40385
- name: import_zod61.z.string(),
40386
- stages: import_zod61.z.array(import_zod61.z.object({ position: import_zod61.z.number(), name: import_zod61.z.string() }))
40531
+ pipelines: import_zod62.z.array(import_zod62.z.object({
40532
+ name: import_zod62.z.string(),
40533
+ stages: import_zod62.z.array(import_zod62.z.object({ position: import_zod62.z.number(), name: import_zod62.z.string() }))
40387
40534
  })).optional(),
40388
- workflows: import_zod61.z.array(import_zod61.z.object({
40389
- name: import_zod61.z.string(),
40390
- condition: import_zod61.z.string().optional(),
40391
- actions: import_zod61.z.array(import_zod61.z.record(import_zod61.z.string(), import_zod61.z.unknown())).optional().default([])
40535
+ workflows: import_zod62.z.array(import_zod62.z.object({
40536
+ name: import_zod62.z.string(),
40537
+ condition: import_zod62.z.string().optional(),
40538
+ actions: import_zod62.z.array(import_zod62.z.record(import_zod62.z.string(), import_zod62.z.unknown())).optional().default([])
40392
40539
  })).optional(),
40393
- calendars: import_zod61.z.array(import_zod61.z.object({
40394
- name: import_zod61.z.string(),
40395
- description: import_zod61.z.string().optional()
40540
+ calendars: import_zod62.z.array(import_zod62.z.object({
40541
+ name: import_zod62.z.string(),
40542
+ description: import_zod62.z.string().optional()
40396
40543
  })).optional()
40397
40544
  });
40398
40545
  }
@@ -40872,8 +41019,8 @@ function registerCheckupTools(server2, installedVersion, client, builderClient,
40872
41019
  "run_checkup",
40873
41020
  `The full GHL Command checkup \u2014 the answer to "am I using it all, and is it all still working?". Three sections: (1) CONNECTION, the same install checks as health_check; (2) WHAT STILL WORKS, one live read-only probe against every GHL capability family, which distinguishes "working but you own none" from "GoHighLevel changed something and this is broken"; (3) WHAT YOU ARE NOT USING YET, derived from what the account actually contains, each line carrying the exact sentence to paste back. Read-only and safe on live client accounts. Use format:'json' for a machine-readable report. Run it monthly, after a GHL update, or any time something feels off.`,
40874
41021
  {
40875
- locationId: import_zod62.z.string().optional().describe("Sub-account to check. Defaults to the active location."),
40876
- format: import_zod62.z.enum(["report", "json"]).optional().describe("'report' (default) is the plain-English checkup. 'json' returns the same data for scripts and support tickets.")
41022
+ locationId: import_zod63.z.string().optional().describe("Sub-account to check. Defaults to the active location."),
41023
+ format: import_zod63.z.enum(["report", "json"]).optional().describe("'report' (default) is the plain-English checkup. 'json' returns the same data for scripts and support tickets.")
40877
41024
  },
40878
41025
  async ({ locationId: locationId2, format }) => {
40879
41026
  let loc;
@@ -40908,11 +41055,11 @@ function registerCheckupTools(server2, installedVersion, client, builderClient,
40908
41055
  }
40909
41056
  );
40910
41057
  }
40911
- var import_zod62, PROBES, BUILDER_PROBE_KEY, CONFIRM_ATTEMPTS, CONFIRM_DELAY_MS, BUILDER_AUTH_FAILURE, VERDICT_SYMBOL;
41058
+ var import_zod63, PROBES, BUILDER_PROBE_KEY, CONFIRM_ATTEMPTS, CONFIRM_DELAY_MS, BUILDER_AUTH_FAILURE, VERDICT_SYMBOL;
40912
41059
  var init_checkup = __esm({
40913
41060
  "src/tools/checkup.ts"() {
40914
41061
  "use strict";
40915
- import_zod62 = require("zod");
41062
+ import_zod63 = require("zod");
40916
41063
  init_diagnostics();
40917
41064
  PROBES = [
40918
41065
  { key: "contacts", label: "Contacts", path: "/contacts/", params: { locationId: "{loc}", limit: 1 }, listKeys: ["contacts"] },
@@ -40996,7 +41143,7 @@ function registerUserGuideTools(server2) {
40996
41143
  server2.tool(
40997
41144
  "get_user_guide",
40998
41145
  "List what is in the GHL Command user guide \u2014 the library of plain-English guides with copy-paste prompts (count and report contacts, build a nurture sequence, audit workflows, and more). Ships with every install, free or paid. By default this only RETURNS the contents list and the guide's file path; it changes nothing on screen. Set open=true ONLY when the person has actually asked to see the guide, because opening it takes over their screen with a browser window in front of whatever they are doing.",
40999
- { open: import_zod63.z.boolean().optional().describe("Open the guide in the browser. Takes over the person's screen \u2014 only when they asked for it.") },
41146
+ { open: import_zod64.z.boolean().optional().describe("Open the guide in the browser. Takes over the person's screen \u2014 only when they asked for it.") },
41000
41147
  async ({ open }) => {
41001
41148
  const guidePath = resolveGuidePath();
41002
41149
  if (!fs29.existsSync(guidePath)) {
@@ -41032,12 +41179,12 @@ ${guidePath}`;
41032
41179
  }
41033
41180
  );
41034
41181
  }
41035
- var import_child_process6, import_zod63, fs29, path28;
41182
+ var import_child_process6, import_zod64, fs29, path28;
41036
41183
  var init_user_guide = __esm({
41037
41184
  "src/tools/user-guide.ts"() {
41038
41185
  "use strict";
41039
41186
  import_child_process6 = require("child_process");
41040
- import_zod63 = require("zod");
41187
+ import_zod64 = require("zod");
41041
41188
  fs29 = __toESM(require("fs"));
41042
41189
  path28 = __toESM(require("path"));
41043
41190
  }
@@ -41075,30 +41222,30 @@ function registerAgencyProfileTools(server2) {
41075
41222
  "set_agency_profile",
41076
41223
  "Save or update YOUR agency's profile (see get_agency_profile). Send only the fields you are changing \u2014 anything you omit is left exactly as it was, so this is safe to call repeatedly to fill the profile in a few pieces. To CLEAR a field, pass null for it. The brand colour must be #rrggbb. Local file on this machine; never touches GoHighLevel.",
41077
41224
  {
41078
- agencyName: import_zod64.z.string().optional().describe("How the agency is named to clients."),
41079
- legalName: import_zod64.z.string().optional().describe("Registered entity, when it differs from the trading name (contracts, A2P)."),
41080
- accent: import_zod64.z.string().optional().describe("Brand colour as #rrggbb. Anything else is rejected rather than stored."),
41081
- logoUrl: import_zod64.z.string().optional(),
41082
- website: import_zod64.z.string().optional(),
41083
- sender: import_zod64.z.object({
41084
- fromName: import_zod64.z.string().optional(),
41085
- replyTo: import_zod64.z.string().optional(),
41086
- phone: import_zod64.z.string().optional()
41225
+ agencyName: import_zod65.z.string().optional().describe("How the agency is named to clients."),
41226
+ legalName: import_zod65.z.string().optional().describe("Registered entity, when it differs from the trading name (contracts, A2P)."),
41227
+ accent: import_zod65.z.string().optional().describe("Brand colour as #rrggbb. Anything else is rejected rather than stored."),
41228
+ logoUrl: import_zod65.z.string().optional(),
41229
+ website: import_zod65.z.string().optional(),
41230
+ sender: import_zod65.z.object({
41231
+ fromName: import_zod65.z.string().optional(),
41232
+ replyTo: import_zod65.z.string().optional(),
41233
+ phone: import_zod65.z.string().optional()
41087
41234
  }).optional().describe("Who a client-facing message comes from."),
41088
- offers: import_zod64.z.array(OfferSchema).optional(),
41089
- pricing: import_zod64.z.object({
41090
- minMonthly: import_zod64.z.number().min(0).optional(),
41091
- minSetup: import_zod64.z.number().min(0).optional(),
41092
- currency: import_zod64.z.string().optional()
41235
+ offers: import_zod65.z.array(OfferSchema).optional(),
41236
+ pricing: import_zod65.z.object({
41237
+ minMonthly: import_zod65.z.number().min(0).optional(),
41238
+ minSetup: import_zod65.z.number().min(0).optional(),
41239
+ currency: import_zod65.z.string().optional()
41093
41240
  }).optional(),
41094
- delivery: import_zod64.z.object({
41095
- sops: import_zod64.z.array(import_zod64.z.string()).optional(),
41096
- hoursToDeliverTarget: import_zod64.z.number().min(0).optional()
41241
+ delivery: import_zod65.z.object({
41242
+ sops: import_zod65.z.array(import_zod65.z.string()).optional(),
41243
+ hoursToDeliverTarget: import_zod65.z.number().min(0).optional()
41097
41244
  }).optional(),
41098
- staff: import_zod64.z.array(StaffSchema).optional(),
41099
- defaults: import_zod64.z.object({
41100
- timezone: import_zod64.z.string().optional(),
41101
- industry: import_zod64.z.string().optional()
41245
+ staff: import_zod65.z.array(StaffSchema).optional(),
41246
+ defaults: import_zod65.z.object({
41247
+ timezone: import_zod65.z.string().optional(),
41248
+ industry: import_zod65.z.string().optional()
41102
41249
  }).optional()
41103
41250
  },
41104
41251
  async (args) => {
@@ -41109,24 +41256,24 @@ function registerAgencyProfileTools(server2) {
41109
41256
  }
41110
41257
  );
41111
41258
  }
41112
- var import_zod64, OfferSchema, StaffSchema;
41259
+ var import_zod65, OfferSchema, StaffSchema;
41113
41260
  var init_agency_profile2 = __esm({
41114
41261
  "src/tools/agency-profile.ts"() {
41115
41262
  "use strict";
41116
- import_zod64 = require("zod");
41263
+ import_zod65 = require("zod");
41117
41264
  init_tool_helpers();
41118
41265
  init_agency_profile();
41119
- OfferSchema = import_zod64.z.object({
41120
- name: import_zod64.z.string().describe("What you sell it as, in the words you use on a call."),
41121
- priceFloor: import_zod64.z.number().min(0).optional().describe("The number below which you don't take the work."),
41122
- cadence: import_zod64.z.enum(["one-time", "monthly"]).optional(),
41123
- notes: import_zod64.z.string().optional()
41266
+ OfferSchema = import_zod65.z.object({
41267
+ name: import_zod65.z.string().describe("What you sell it as, in the words you use on a call."),
41268
+ priceFloor: import_zod65.z.number().min(0).optional().describe("The number below which you don't take the work."),
41269
+ cadence: import_zod65.z.enum(["one-time", "monthly"]).optional(),
41270
+ notes: import_zod65.z.string().optional()
41124
41271
  });
41125
- StaffSchema = import_zod64.z.object({
41126
- name: import_zod64.z.string(),
41127
- role: import_zod64.z.string().optional(),
41128
- email: import_zod64.z.string().optional(),
41129
- mayApprove: import_zod64.z.array(import_zod64.z.string()).optional().describe("Advisory list of what this person signs off on. The approval gates are enforced by the ritual, not by this field.")
41272
+ StaffSchema = import_zod65.z.object({
41273
+ name: import_zod65.z.string(),
41274
+ role: import_zod65.z.string().optional(),
41275
+ email: import_zod65.z.string().optional(),
41276
+ mayApprove: import_zod65.z.array(import_zod65.z.string()).optional().describe("Advisory list of what this person signs off on. The approval gates are enforced by the ritual, not by this field.")
41130
41277
  });
41131
41278
  }
41132
41279
  });
@@ -42615,7 +42762,7 @@ function checkConversion(p, ctx, rendered) {
42615
42762
  {
42616
42763
  const ctaColours = primaries.map((c) => ({ c, ch: chroma(c.bg) })).filter((x) => x.ch !== null);
42617
42764
  const all = [...ctaColours.map((x) => ({ hex: hexOf(x.c.bg), ch: x.ch })), ...p.sections.map((s) => ({ hex: hexOf(s.bg), ch: chroma(s.bg) })).filter((x) => x.hex && x.ch !== null)];
42618
- const hottest = all.sort((a, z82) => z82.ch - a.ch)[0] ?? null;
42765
+ const hottest = all.sort((a, z84) => z84.ch - a.ch)[0] ?? null;
42619
42766
  const bad = [];
42620
42767
  if (primaries.length === 0) bad.push("no primary CTA on the page for the hot colour to be on");
42621
42768
  if (!hottest) bad.push("no readable background colour on any CTA or section");
@@ -42642,7 +42789,7 @@ function checkConversion(p, ctx, rendered) {
42642
42789
  if (real.length > 1) bad.push(`primary CTAs split across ${real.length} destinations (${real.join(", ")})`);
42643
42790
  if (real.length === 0) bad.push("no primary CTA resolves to a form, calendar, tel: or checkout");
42644
42791
  if (p.docHeight > 5e3 && primaries.length < 5) bad.push(`${primaries.length} primary CTAs on a ${Math.round(p.docHeight)} px page (needs 5+)`);
42645
- const ys = primaries.map((c) => c.y).sort((a, z82) => a - z82);
42792
+ const ys = primaries.map((c) => c.y).sort((a, z84) => a - z84);
42646
42793
  let maxGap = ys.length ? ys[0] : p.docHeight;
42647
42794
  for (let i = 1; i < ys.length; i++) maxGap = Math.max(maxGap, ys[i] - ys[i - 1]);
42648
42795
  if (ys.length) maxGap = Math.max(maxGap, p.docHeight - ys[ys.length - 1]);
@@ -43634,23 +43781,23 @@ function normaliseNull(v) {
43634
43781
  for (const k of ["firstBreakdown", "fix"]) if (o[k] === "null" || o[k] === "") o[k] = null;
43635
43782
  return o;
43636
43783
  }
43637
- var import_zod65, COPY_STANDARD, Score, CopyGradeSchema;
43784
+ var import_zod66, COPY_STANDARD, Score, CopyGradeSchema;
43638
43785
  var init_copy_grade = __esm({
43639
43786
  "src/command-os/design/copy-grade.ts"() {
43640
43787
  "use strict";
43641
- import_zod65 = require("zod");
43788
+ import_zod66 = require("zod");
43642
43789
  init_copy();
43643
43790
  COPY_STANDARD = copy_default;
43644
- Score = import_zod65.z.number().int().min(1).max(5);
43645
- CopyGradeSchema = import_zod65.z.object({
43646
- verdict: import_zod65.z.enum(["PASS", "FAIL"]),
43647
- icp: import_zod65.z.string(),
43648
- icpInferred: import_zod65.z.boolean().optional(),
43649
- scores: import_zod65.z.object({ attention: Score, resonance: Score, belief: Score, action: Score }),
43650
- firstBreakdown: import_zod65.z.enum(["attention", "resonance", "belief", "action"]).nullable(),
43651
- evidence: import_zod65.z.record(import_zod65.z.string(), import_zod65.z.string()).optional(),
43652
- fix: import_zod65.z.string().nullable(),
43653
- summary: import_zod65.z.string()
43791
+ Score = import_zod66.z.number().int().min(1).max(5);
43792
+ CopyGradeSchema = import_zod66.z.object({
43793
+ verdict: import_zod66.z.enum(["PASS", "FAIL"]),
43794
+ icp: import_zod66.z.string(),
43795
+ icpInferred: import_zod66.z.boolean().optional(),
43796
+ scores: import_zod66.z.object({ attention: Score, resonance: Score, belief: Score, action: Score }),
43797
+ firstBreakdown: import_zod66.z.enum(["attention", "resonance", "belief", "action"]).nullable(),
43798
+ evidence: import_zod66.z.record(import_zod66.z.string(), import_zod66.z.string()).optional(),
43799
+ fix: import_zod66.z.string().nullable(),
43800
+ summary: import_zod66.z.string()
43654
43801
  });
43655
43802
  }
43656
43803
  });
@@ -43743,7 +43890,9 @@ async function defaultVerify(file2, medium, brand, outDir, mode, headless = true
43743
43890
  }
43744
43891
  async function runDesign(opts) {
43745
43892
  const dna = resolveBrand(opts.brand);
43746
- const brief = DesignBriefSchema.parse({ schemaVersion: 1, brandSlug: dna.slug, ...opts.brief });
43893
+ const checkedBrief = DesignBriefSchema.safeParse({ schemaVersion: 1, brandSlug: dna.slug, ...opts.brief });
43894
+ if (!checkedBrief.success) throw new Error(designValidationMessage(checkedBrief.error));
43895
+ const brief = checkedBrief.data;
43747
43896
  const medium = brief.medium;
43748
43897
  const outDir = (0, import_node_path7.resolve)(opts.outDir);
43749
43898
  (0, import_node_fs7.mkdirSync)(outDir, { recursive: true });
@@ -43837,6 +43986,7 @@ var init_run = __esm({
43837
43986
  init_verify();
43838
43987
  init_looks();
43839
43988
  init_store();
43989
+ init_brief_validation();
43840
43990
  init_schema();
43841
43991
  DEFAULT_MODEL = "opus";
43842
43992
  FIRST = `You have no tools in this session. Write the complete file now and reply with ONLY the HTML, in one \`\`\`html fenced block, nothing before or after it. Do not describe it; do not ask; output the file.`;
@@ -43850,13 +44000,13 @@ function registerDesignStudioTools(server2) {
43850
44000
  "extract_brand",
43851
44001
  "Read a website's brand and store it on this machine: six colours, two faces, radius, logo, headline voice, each with its source and a confidence. Asks nothing of any account. Static read first; add useBrowser:true for a rendered pass in the automation browser (needed for script-built sites); a Firecrawl key, if you have one, is used as a fourth source. State your rights to the site once; it is recorded, not checked.",
43852
44002
  {
43853
- url: import_zod66.z.string().url().describe("The site to read, e.g. https://example-clinic.com"),
43854
- rights: import_zod66.z.enum(RIGHTS).describe("own \xB7 client-authorized \xB7 written-permission \xB7 none. Recorded on the brand; with 'none' the logo is still stored for reference but flagged."),
43855
- name: import_zod66.z.string().optional().describe("Brand name; defaults to the page title."),
43856
- slug: import_zod66.z.string().optional().describe("Folder name for the brand; defaults from the name."),
43857
- useBrowser: import_zod66.z.boolean().optional().describe("Also render the page in the automation browser for computed colours, the real display face and a screenshot. Automatic when the static read looks like a script-built shell."),
43858
- firecrawlKey: import_zod66.z.string().optional().describe("Optional. Uses Firecrawl's branding format as an extra source. Never stored."),
43859
- overwrite: import_zod66.z.boolean().optional().describe("Replace an existing brand with this slug. Default false: refuses rather than clobbers.")
44003
+ url: import_zod67.z.string().url().describe("The site to read, e.g. https://example-clinic.com"),
44004
+ rights: import_zod67.z.enum(RIGHTS).describe("own \xB7 client-authorized \xB7 written-permission \xB7 none. Recorded on the brand; with 'none' the logo is still stored for reference but flagged."),
44005
+ name: import_zod67.z.string().optional().describe("Brand name; defaults to the page title."),
44006
+ slug: import_zod67.z.string().optional().describe("Folder name for the brand; defaults from the name."),
44007
+ useBrowser: import_zod67.z.boolean().optional().describe("Also render the page in the automation browser for computed colours, the real display face and a screenshot. Automatic when the static read looks like a script-built shell."),
44008
+ firecrawlKey: import_zod67.z.string().optional().describe("Optional. Uses Firecrawl's branding format as an extra source. Never stored."),
44009
+ overwrite: import_zod67.z.boolean().optional().describe("Replace an existing brand with this slug. Default false: refuses rather than clobbers.")
43860
44010
  },
43861
44011
  async (a) => {
43862
44012
  const raw = await extractStatic(a.url);
@@ -43889,9 +44039,9 @@ function registerDesignStudioTools(server2) {
43889
44039
  "import_reference_look",
43890
44040
  `Fetch one reference look (Apple, Notion, Stripe, Linear and about seventy more) from ${REFERENCE_SOURCE.repo} at a pinned commit onto this machine, as a starting palette: colours, faces, radius, tone. Never the reference brand's logo, never its mark-usage rules, and the pages you make must not name it. Then pull the client's own logo and accent over it with set_brand.`,
43891
44041
  {
43892
- look: import_zod66.z.string().describe("The reference slug, e.g. apple, notion, stripe, linear.app. list_brands shows them all."),
43893
- as: import_zod66.z.string().optional().describe("Store under this slug instead (e.g. 'northside-clinic-start'). Default: 'look-<look>'."),
43894
- overwrite: import_zod66.z.boolean().optional()
44042
+ look: import_zod67.z.string().describe("The reference slug, e.g. apple, notion, stripe, linear.app. list_brands shows them all."),
44043
+ as: import_zod67.z.string().optional().describe("Store under this slug instead (e.g. 'northside-clinic-start'). Default: 'look-<look>'."),
44044
+ overwrite: import_zod67.z.boolean().optional()
43895
44045
  },
43896
44046
  async (a) => {
43897
44047
  const entry = listReferenceLooks().find((e) => e.slug === a.look);
@@ -43921,7 +44071,7 @@ function registerDesignStudioTools(server2) {
43921
44071
  server2,
43922
44072
  "get_brand",
43923
44073
  "Read one brand in full: colours with OKLCH twins and contrast-corrected text shades, faces and what renders, shape, logo, voice, and the provenance of every value. House looks (calm-clinic, bold-local, quiet-luxury, night-studio, warm-practice, civic-clear) read without being stored first.",
43924
- { slug: import_zod66.z.string().describe("A stored brand's slug or a house look's slug.") },
44074
+ { slug: import_zod67.z.string().describe("A stored brand's slug or a house look's slug.") },
43925
44075
  async ({ slug: slug3 }) => {
43926
44076
  const house = houseLooks().find((d) => d.slug === slug3);
43927
44077
  const dna = house ?? readBrand(slugify(slug3));
@@ -43933,22 +44083,22 @@ function registerDesignStudioTools(server2) {
43933
44083
  "set_brand",
43934
44084
  "Change values on a stored brand, or copy a house look into a stored brand. Send only what changes; every change is recorded in the brand's provenance with who and when. Colours are #rrggbb; faces are family names (a licensed face is kept and its Google fallback recorded).",
43935
44085
  {
43936
- slug: import_zod66.z.string().describe("The brand to change, or a house-look slug to copy."),
43937
- as: import_zod66.z.string().optional().describe("When copying a house look: the new slug to store it under."),
43938
- name: import_zod66.z.string().optional(),
43939
- accent: import_zod66.z.string().optional(),
43940
- bg: import_zod66.z.string().optional(),
43941
- fg: import_zod66.z.string().optional(),
43942
- muted: import_zod66.z.string().optional(),
43943
- border: import_zod66.z.string().optional(),
43944
- surface: import_zod66.z.string().optional(),
43945
- displayFont: import_zod66.z.string().optional(),
43946
- bodyFont: import_zod66.z.string().optional(),
43947
- radiusMd: import_zod66.z.number().min(0).max(64).optional(),
43948
- logoSvg: import_zod66.z.string().optional().describe("Inline SVG for the client's own logo; replaces the stored one."),
43949
- tone: import_zod66.z.string().optional(),
43950
- audience: import_zod66.z.string().optional(),
43951
- changedBy: import_zod66.z.string().optional().describe("Who is making the change; goes into provenance.")
44086
+ slug: import_zod67.z.string().describe("The brand to change, or a house-look slug to copy."),
44087
+ as: import_zod67.z.string().optional().describe("When copying a house look: the new slug to store it under."),
44088
+ name: import_zod67.z.string().optional(),
44089
+ accent: import_zod67.z.string().optional(),
44090
+ bg: import_zod67.z.string().optional(),
44091
+ fg: import_zod67.z.string().optional(),
44092
+ muted: import_zod67.z.string().optional(),
44093
+ border: import_zod67.z.string().optional(),
44094
+ surface: import_zod67.z.string().optional(),
44095
+ displayFont: import_zod67.z.string().optional(),
44096
+ bodyFont: import_zod67.z.string().optional(),
44097
+ radiusMd: import_zod67.z.number().min(0).max(64).optional(),
44098
+ logoSvg: import_zod67.z.string().optional().describe("Inline SVG for the client's own logo; replaces the stored one."),
44099
+ tone: import_zod67.z.string().optional(),
44100
+ audience: import_zod67.z.string().optional(),
44101
+ changedBy: import_zod67.z.string().optional().describe("Who is making the change; goes into provenance.")
43952
44102
  },
43953
44103
  async (a) => {
43954
44104
  const house = houseLooks().find((d2) => d2.slug === a.slug);
@@ -44015,19 +44165,19 @@ function registerDesignStudioTools(server2) {
44015
44165
  "compose_design_brief",
44016
44166
  "The pack your own Claude writes from: brand tokens ready to paste, the rules it will be graded on with thresholds, the output contract (one self-contained file, the data markers the verifier reads), the sameness tells as banned, and the file name. Calls no model. Write the file, then lint_design_contract, then verify_design.",
44017
44167
  {
44018
- medium: import_zod66.z.enum(MEDIA).describe("deck (16:9 slides) \xB7 site (one responsive page) \xB7 report (a branded document)"),
44019
- brandSlug: import_zod66.z.string().describe("A stored brand or a house look."),
44020
- headline: import_zod66.z.string().describe("The one-line message."),
44021
- points: import_zod66.z.array(import_zod66.z.string()).max(12).describe("Deck: 3\u20135 points. Site: the sections in order. Report: the section titles."),
44022
- audience: import_zod66.z.string().optional(),
44023
- job: import_zod66.z.string().optional().describe("Site: the ONE thing a visitor should do."),
44024
- ctaLabel: import_zod66.z.string().optional(),
44025
- proof: import_zod66.z.string().optional().describe("One line of proof for the hero."),
44026
- mode: import_zod66.z.enum(["presenter", "document"]).optional().describe("Deck only. Default presenter."),
44027
- ratio: import_zod66.z.number().min(1.1).max(1.7).optional().describe("Type scale ratio. Default 1.333."),
44028
- theme: import_zod66.z.enum(["light", "dark", "both"]).optional().describe("Default both."),
44029
- logo: import_zod66.z.enum(["bottom-left", "bottom-right", "top-left", "omit"]).optional().describe("Deck logo placement. Default bottom-left."),
44030
- slides: import_zod66.z.number().int().min(3).max(24).optional()
44168
+ medium: import_zod67.z.enum(MEDIA).describe("deck (16:9 slides) \xB7 site (one responsive page) \xB7 report (a branded document)"),
44169
+ brandSlug: import_zod67.z.string().describe("A stored brand or a house look."),
44170
+ headline: import_zod67.z.string().describe("The one-line message."),
44171
+ points: import_zod67.z.array(import_zod67.z.string()).max(12).describe("Deck: 3\u20135 points. Site: the sections in order. Report: the section titles."),
44172
+ audience: import_zod67.z.string().optional(),
44173
+ job: import_zod67.z.string().optional().describe("Site: the ONE thing a visitor should do."),
44174
+ ctaLabel: import_zod67.z.string().optional(),
44175
+ proof: import_zod67.z.string().optional().describe("One line of proof for the hero."),
44176
+ mode: import_zod67.z.enum(["presenter", "document"]).optional().describe("Deck only. Default presenter."),
44177
+ ratio: import_zod67.z.number().min(1.1).max(1.7).optional().describe("Type scale ratio. Default 1.333."),
44178
+ theme: import_zod67.z.enum(["light", "dark", "both"]).optional().describe("Default both."),
44179
+ logo: import_zod67.z.enum(["bottom-left", "bottom-right", "top-left", "omit"]).optional().describe("Deck logo placement. Default bottom-left."),
44180
+ slides: import_zod67.z.number().int().min(3).max(24).optional()
44031
44181
  },
44032
44182
  async (a) => {
44033
44183
  const dna = houseLooks().find((d) => d.slug === a.brandSlug) ?? readBrand(slugify(a.brandSlug));
@@ -44046,7 +44196,7 @@ function registerDesignStudioTools(server2) {
44046
44196
  server2,
44047
44197
  "lint_design_contract",
44048
44198
  "Check a written HTML file honours the output contract before grading: section and role markers present and consistent, no outside scripts, one h1 on a page, every image described. Deterministic; no browser. A file that fails this is not graded by verify_design.",
44049
- { file: import_zod66.z.string().describe("Path to the .html file."), medium: import_zod66.z.enum(MEDIA) },
44199
+ { file: import_zod67.z.string().describe("Path to the .html file."), medium: import_zod67.z.enum(MEDIA) },
44050
44200
  async ({ file: file2, medium }) => {
44051
44201
  const abs = path30.resolve(file2);
44052
44202
  if (!fs31.existsSync(abs)) throw new Error(`No file at ${abs}.`);
@@ -44059,14 +44209,14 @@ function registerDesignStudioTools(server2) {
44059
44209
  "verify_design",
44060
44210
  "Render a deck or page (a local .html or a published URL) in the automation browser and measure it against the design rules and the sameness tells. Every rule on its own line: measured, proxy or judgment call, with the value and the evidence. Writes verification.json, verification.html and screenshots to outDir. PASS means zero failed measured rules and NOTHING MORE: every rule measures craft \u2014 type, spacing, colour, contrast, structure, performance and the machine-made tells \u2014 and none of them measures whether the page persuades anyone. A page can pass every rule and still be boring and unsellable. Never report a pass as a quality claim; judge the writing separately. GoHighLevel pages: pass placement:'ghl' to grade on the builder subset.",
44061
44211
  {
44062
- target: import_zod66.z.string().describe("A local .html path or an http(s) URL."),
44063
- medium: import_zod66.z.enum(MEDIA),
44064
- brandSlug: import_zod66.z.string().optional().describe("The brand to measure against (accent share, logo, faces)."),
44065
- outDir: import_zod66.z.string().optional().describe("Where to write the report. Default: <file dir>/verify-<name>/ or ./verify-<host>/ for URLs."),
44066
- placement: import_zod66.z.enum(["html", "ghl"]).optional(),
44067
- mode: import_zod66.z.enum(["presenter", "document"]).optional(),
44068
- ratio: import_zod66.z.number().optional(),
44069
- logo: import_zod66.z.enum(["keep", "omit"]).optional()
44212
+ target: import_zod67.z.string().describe("A local .html path or an http(s) URL."),
44213
+ medium: import_zod67.z.enum(MEDIA),
44214
+ brandSlug: import_zod67.z.string().optional().describe("The brand to measure against (accent share, logo, faces)."),
44215
+ outDir: import_zod67.z.string().optional().describe("Where to write the report. Default: <file dir>/verify-<name>/ or ./verify-<host>/ for URLs."),
44216
+ placement: import_zod67.z.enum(["html", "ghl"]).optional(),
44217
+ mode: import_zod67.z.enum(["presenter", "document"]).optional(),
44218
+ ratio: import_zod67.z.number().optional(),
44219
+ logo: import_zod67.z.enum(["keep", "omit"]).optional()
44070
44220
  },
44071
44221
  async (a) => {
44072
44222
  const brand = a.brandSlug ? houseLooks().find((d) => d.slug === a.brandSlug) ?? readBrand(slugify(a.brandSlug)) : void 0;
@@ -44097,18 +44247,18 @@ function registerDesignStudioTools(server2) {
44097
44247
  "check_page_theme",
44098
44248
  "After Page Studio has saved a GoHighLevel page with a brand_to_theme theme, read the page back and prove the brand landed: each of the six slots is measured in the data GoHighLevel actually saved, and the donor template's own colour or face must be GONE \u2014 a half-finished swap is the failure this catches, and it is invisible in a preview snapshot. Also confirms the page's preview address by fetching it, so verify_design has a real URL to grade. Reads only: no key, no account API, nothing written.",
44099
44249
  {
44100
- pageDataDownloadUrl: import_zod66.z.string().url().optional().describe("The pageDataDownloadUrl from get_page_full. The page data is fetched here so the whole envelope never has to pass through the conversation."),
44101
- file: import_zod66.z.string().optional().describe("Alternative to the URL: a local .json file holding the saved page data."),
44102
- brandSlug: import_zod66.z.string().optional().describe("The stored brand whose theme was sent. Defaults to comparing against the theme passed in `theme`."),
44103
- theme: import_zod66.z.object({
44104
- primary: import_zod66.z.string().optional(),
44105
- primarySoft: import_zod66.z.string().optional(),
44106
- surface: import_zod66.z.string().optional(),
44107
- textDark: import_zod66.z.string().optional(),
44108
- headingFont: import_zod66.z.string().optional(),
44109
- bodyFont: import_zod66.z.string().optional()
44250
+ pageDataDownloadUrl: import_zod67.z.string().url().optional().describe("The pageDataDownloadUrl from get_page_full. The page data is fetched here so the whole envelope never has to pass through the conversation."),
44251
+ file: import_zod67.z.string().optional().describe("Alternative to the URL: a local .json file holding the saved page data."),
44252
+ brandSlug: import_zod67.z.string().optional().describe("The stored brand whose theme was sent. Defaults to comparing against the theme passed in `theme`."),
44253
+ theme: import_zod67.z.object({
44254
+ primary: import_zod67.z.string().optional(),
44255
+ primarySoft: import_zod67.z.string().optional(),
44256
+ surface: import_zod67.z.string().optional(),
44257
+ textDark: import_zod67.z.string().optional(),
44258
+ headingFont: import_zod67.z.string().optional(),
44259
+ bodyFont: import_zod67.z.string().optional()
44110
44260
  }).optional().describe("The theme that was sent, if it was not brand_to_theme's output for brandSlug."),
44111
- pageId: import_zod66.z.string().optional().describe("The page id. With it, the preview address is derived and FETCHED once, and reported only if it answered.")
44261
+ pageId: import_zod67.z.string().optional().describe("The page id. With it, the preview address is derived and FETCHED once, and reported only if it answered.")
44112
44262
  },
44113
44263
  async (a) => {
44114
44264
  if (!a.pageDataDownloadUrl && !a.file) throw new Error("Pass pageDataDownloadUrl (from get_page_full) or file.");
@@ -44148,10 +44298,10 @@ function registerDesignStudioTools(server2) {
44148
44298
  "grade_copy",
44149
44299
  "Grade whether a page or deck would actually persuade the reader it is for \u2014 the half the design rules cannot reach. Scores the four sequential jobs persuasive copy must do, in reading order (Attention, Resonance, Belief, Action), names the FIRST place the right prospect falls off, and gives a concrete rewrite of that one thing. This is a JUDGEMENT made by a model on YOUR OWN Claude subscription, never our key, and it is deliberately separate from verify_design: a page is ready when both pass, and a design pass on its own says nothing about whether anyone would buy.",
44150
44300
  {
44151
- file: import_zod66.z.string().optional().describe("Path to the .html file to grade."),
44152
- text: import_zod66.z.string().optional().describe("Alternative to file: the copy itself."),
44153
- icp: import_zod66.z.string().optional().describe("Who the page is for, in one line, e.g. 'agency owners drowning in client onboarding'. Without it the grader infers a reader and says so \u2014 and grading against the wrong reader is the commonest way this comes back wrong."),
44154
- model: import_zod66.z.string().optional().describe("Which engine grades it. Defaults to the runner's default.")
44301
+ file: import_zod67.z.string().optional().describe("Path to the .html file to grade."),
44302
+ text: import_zod67.z.string().optional().describe("Alternative to file: the copy itself."),
44303
+ icp: import_zod67.z.string().optional().describe("Who the page is for, in one line, e.g. 'agency owners drowning in client onboarding'. Without it the grader infers a reader and says so \u2014 and grading against the wrong reader is the commonest way this comes back wrong."),
44304
+ model: import_zod67.z.string().optional().describe("Which engine grades it. Defaults to the runner's default.")
44155
44305
  },
44156
44306
  async (a) => {
44157
44307
  if (!a.file && !a.text) throw new Error("Pass file or text.");
@@ -44186,7 +44336,7 @@ function registerDesignStudioTools(server2) {
44186
44336
  server2,
44187
44337
  "brand_to_theme",
44188
44338
  "The six-slot theme Page Studio's compose_page / compose_website accept, derived from a stored brand, with the warnings about what the donor cannot carry. A re-skin of the donor template, not a port of the design system. Pass the theme straight through as compose_page's `theme`.",
44189
- { slug: import_zod66.z.string() },
44339
+ { slug: import_zod67.z.string() },
44190
44340
  async ({ slug: slug3 }) => {
44191
44341
  const d = houseLooks().find((x) => x.slug === slug3) ?? readBrand(slugify(slug3));
44192
44342
  const { warnings, ...theme } = d.pageStudioTheme;
@@ -44194,11 +44344,11 @@ function registerDesignStudioTools(server2) {
44194
44344
  }
44195
44345
  );
44196
44346
  }
44197
- var import_zod66, fs31, path30, summarise2;
44347
+ var import_zod67, fs31, path30, summarise2;
44198
44348
  var init_design_studio = __esm({
44199
44349
  "src/tools/design-studio.ts"() {
44200
44350
  "use strict";
44201
- import_zod66 = require("zod");
44351
+ import_zod67 = require("zod");
44202
44352
  fs31 = __toESM(require("node:fs"));
44203
44353
  path30 = __toESM(require("node:path"));
44204
44354
  init_tool_helpers();
@@ -45022,9 +45172,9 @@ function registerConnectorTools(server2) {
45022
45172
  "connect_system",
45023
45173
  "Store a connector's credentials on THIS machine so that system can be read. The key is written next to your GoHighLevel credentials, readable only by you, and is never sent anywhere, never returned by any tool, and never written to a log. Today: Stripe, which answers who owes you money, what each person or company pays you and when they renew.",
45024
45174
  {
45025
- system: import_zod67.z.enum(["stripe"]).describe("The system to connect. Stripe is the one that is built."),
45026
- key: import_zod67.z.string().min(8).describe("A Stripe RESTRICTED key with read access to subscriptions, invoices and customers. A restricted key is deliberate: nothing here writes, so nothing here needs a key that can."),
45027
- label: import_zod67.z.string().optional().describe("Which account this is, so you can tell two apart later.")
45175
+ system: import_zod68.z.enum(["stripe"]).describe("The system to connect. Stripe is the one that is built."),
45176
+ key: import_zod68.z.string().min(8).describe("A Stripe RESTRICTED key with read access to subscriptions, invoices and customers. A restricted key is deliberate: nothing here writes, so nothing here needs a key that can."),
45177
+ label: import_zod68.z.string().optional().describe("Which account this is, so you can tell two apart later.")
45028
45178
  },
45029
45179
  async ({ system, key, label }) => {
45030
45180
  const h = await health(stripeFetcher(key));
@@ -45078,7 +45228,7 @@ function registerConnectorTools(server2) {
45078
45228
  server2,
45079
45229
  "disconnect_system",
45080
45230
  "Remove a connector's credentials from this machine. The data already written into your income records stays; only the ability to read that system again is removed.",
45081
- { system: import_zod67.z.enum(["stripe"]) },
45231
+ { system: import_zod68.z.enum(["stripe"]) },
45082
45232
  async ({ system }) => ({
45083
45233
  removed: disconnect(system),
45084
45234
  note: "Anything already written into your income records is untouched."
@@ -45089,10 +45239,10 @@ function registerConnectorTools(server2) {
45089
45239
  "sync_stripe",
45090
45240
  "Read your Stripe and fill in what your records do not know: what each person or company pays you, when they renew, and who owes you money. Handles both shapes of income \u2014 clients whose GoHighLevel accounts you run (confirmLinks) and people who simply pay you, like software subscribers and event fees (recordAsIncome). DRY RUN BY DEFAULT \u2014 it shows you every change before making any. A client whose Stripe customer cannot be matched to one of your clients is reported, never guessed at.",
45091
45241
  {
45092
- apply: import_zod67.z.boolean().optional().describe("Write the changes into your income records. Defaults to false: look first."),
45093
- confirmLinks: import_zod67.z.array(import_zod67.z.object({
45094
- stripeCustomerId: import_zod67.z.string(),
45095
- clientKey: import_zod67.z.string().describe("The client in your records this Stripe customer is.")
45242
+ apply: import_zod68.z.boolean().optional().describe("Write the changes into your income records. Defaults to false: look first."),
45243
+ confirmLinks: import_zod68.z.array(import_zod68.z.object({
45244
+ stripeCustomerId: import_zod68.z.string(),
45245
+ clientKey: import_zod68.z.string().describe("The client in your records this Stripe customer is.")
45096
45246
  })).optional().describe("Matches you are confirming \u2014 a Stripe customer that IS one of the clients already in your records. An email match alone is never enough to move money against a client's name."),
45097
45247
  // ADDED 2026-09-01, and it is the half that was missing. confirmLinks can
45098
45248
  // only point a Stripe customer at a record that already exists. On Jerry's
@@ -45100,10 +45250,10 @@ function registerConnectorTools(server2) {
45100
45250
  // buy software and the record only held GoHighLevel accounts. There was no
45101
45251
  // move that turned "Stripe knows Dane pays $97" into a record — so the
45102
45252
  // brief kept saying "no client has a commercial record yet" forever.
45103
- recordAsIncome: import_zod67.z.array(import_zod67.z.object({
45104
- stripeCustomerId: import_zod67.z.string(),
45105
- kind: import_zod67.z.enum(["software", "other"]).describe("'software' for a licence or subscription, 'other' for an event fee, revenue share or training. Use confirmLinks instead if this customer is a client whose GoHighLevel account you run."),
45106
- clientName: import_zod67.z.string().optional().describe("What to call them. Defaults to the name on the Stripe customer.")
45253
+ recordAsIncome: import_zod68.z.array(import_zod68.z.object({
45254
+ stripeCustomerId: import_zod68.z.string(),
45255
+ kind: import_zod68.z.enum(["software", "other"]).describe("'software' for a licence or subscription, 'other' for an event fee, revenue share or training. Use confirmLinks instead if this customer is a client whose GoHighLevel account you run."),
45256
+ clientName: import_zod68.z.string().optional().describe("What to call them. Defaults to the name on the Stripe customer.")
45107
45257
  })).optional().describe("Stripe customers that are income in their own right, with no GoHighLevel account. Creates a record keyed by the Stripe customer id and fills in what they pay and when they renew, from Stripe.")
45108
45258
  },
45109
45259
  async ({ apply, confirmLinks, recordAsIncome }) => {
@@ -45263,11 +45413,11 @@ function registerConnectorTools(server2) {
45263
45413
  }
45264
45414
  );
45265
45415
  }
45266
- var import_zod67;
45416
+ var import_zod68;
45267
45417
  var init_connectors = __esm({
45268
45418
  "src/tools/connectors.ts"() {
45269
45419
  "use strict";
45270
- import_zod67 = require("zod");
45420
+ import_zod68 = require("zod");
45271
45421
  init_tool_helpers();
45272
45422
  init_store3();
45273
45423
  init_stripe();
@@ -45327,22 +45477,22 @@ function removeCalendarSecret() {
45327
45477
  throw new Error("The Google Calendar sign-in could not be removed from this machine.");
45328
45478
  }
45329
45479
  }
45330
- var fs33, path32, import_node_crypto9, import_zod68, CALENDAR_SCOPE, SecretSchema, googleCalendarPath;
45480
+ var fs33, path32, import_node_crypto9, import_zod69, CALENDAR_SCOPE, SecretSchema, googleCalendarPath;
45331
45481
  var init_google_calendar_store = __esm({
45332
45482
  "src/command-os/connectors/google-calendar-store.ts"() {
45333
45483
  "use strict";
45334
45484
  fs33 = __toESM(require("node:fs"));
45335
45485
  path32 = __toESM(require("node:path"));
45336
45486
  import_node_crypto9 = require("node:crypto");
45337
- import_zod68 = require("zod");
45487
+ import_zod69 = require("zod");
45338
45488
  init_credentials_store();
45339
45489
  CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
45340
- SecretSchema = import_zod68.z.object({
45341
- clientId: import_zod68.z.string().min(1),
45342
- clientSecret: import_zod68.z.string().min(1),
45343
- refreshToken: import_zod68.z.string().min(8),
45344
- scope: import_zod68.z.literal(CALENDAR_SCOPE),
45345
- connectedAt: import_zod68.z.string()
45490
+ SecretSchema = import_zod69.z.object({
45491
+ clientId: import_zod69.z.string().min(1),
45492
+ clientSecret: import_zod69.z.string().min(1),
45493
+ refreshToken: import_zod69.z.string().min(8),
45494
+ scope: import_zod69.z.literal(CALENDAR_SCOPE),
45495
+ connectedAt: import_zod69.z.string()
45346
45496
  });
45347
45497
  googleCalendarPath = () => path32.join(appDataDir(), "google-calendar.json");
45348
45498
  }
@@ -45455,7 +45605,7 @@ function createGoogleCalendar(deps = {}) {
45455
45605
  for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
45456
45606
  if (pageToken) url.searchParams.set("pageToken", pageToken);
45457
45607
  const data = parsed(
45458
- import_zod69.z.object({ items: import_zod69.z.array(item).optional(), nextPageToken: import_zod69.z.string().optional(), kind: import_zod69.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path39.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
45608
+ import_zod70.z.object({ items: import_zod70.z.array(item).optional(), nextPageToken: import_zod70.z.string().optional(), kind: import_zod70.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path39.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
45459
45609
  await request(url.href, { headers: { Authorization: `Bearer ${access}` } })
45460
45610
  );
45461
45611
  result.push(...data.items ?? []);
@@ -45658,14 +45808,14 @@ function createGoogleCalendar(deps = {}) {
45658
45808
  close: clearPending2
45659
45809
  };
45660
45810
  }
45661
- var import_node_http, import_node_crypto10, import_node_child_process5, import_zod69, TOKEN_URL, REVOKE_URL, API, tokenSchema, calendarSchema2, eventSchema;
45811
+ var import_node_http, import_node_crypto10, import_node_child_process5, import_zod70, TOKEN_URL, REVOKE_URL, API, tokenSchema, calendarSchema2, eventSchema;
45662
45812
  var init_google_calendar = __esm({
45663
45813
  "src/command-os/connectors/google-calendar.ts"() {
45664
45814
  "use strict";
45665
45815
  import_node_http = require("node:http");
45666
45816
  import_node_crypto10 = require("node:crypto");
45667
45817
  import_node_child_process5 = require("node:child_process");
45668
- import_zod69 = require("zod");
45818
+ import_zod70 = require("zod");
45669
45819
  init_credentials_store();
45670
45820
  init_agency_profile();
45671
45821
  init_account_read_retry();
@@ -45673,22 +45823,22 @@ var init_google_calendar = __esm({
45673
45823
  TOKEN_URL = "https://oauth2.googleapis.com/token";
45674
45824
  REVOKE_URL = "https://oauth2.googleapis.com/revoke";
45675
45825
  API = "https://www.googleapis.com/calendar/v3";
45676
- tokenSchema = import_zod69.z.object({ access_token: import_zod69.z.string().min(1), refresh_token: import_zod69.z.string().optional(), scope: import_zod69.z.string().optional() });
45677
- calendarSchema2 = import_zod69.z.object({
45678
- id: import_zod69.z.string().min(1),
45679
- summary: import_zod69.z.string().optional(),
45680
- summaryOverride: import_zod69.z.string().optional(),
45681
- primary: import_zod69.z.boolean().optional(),
45682
- timeZone: import_zod69.z.string().optional(),
45683
- accessRole: import_zod69.z.enum(["none", "freeBusyReader", "reader", "writer", "owner"]).optional()
45826
+ tokenSchema = import_zod70.z.object({ access_token: import_zod70.z.string().min(1), refresh_token: import_zod70.z.string().optional(), scope: import_zod70.z.string().optional() });
45827
+ calendarSchema2 = import_zod70.z.object({
45828
+ id: import_zod70.z.string().min(1),
45829
+ summary: import_zod70.z.string().optional(),
45830
+ summaryOverride: import_zod70.z.string().optional(),
45831
+ primary: import_zod70.z.boolean().optional(),
45832
+ timeZone: import_zod70.z.string().optional(),
45833
+ accessRole: import_zod70.z.enum(["none", "freeBusyReader", "reader", "writer", "owner"]).optional()
45684
45834
  });
45685
- eventSchema = import_zod69.z.object({
45686
- summary: import_zod69.z.string().optional(),
45687
- status: import_zod69.z.string().optional(),
45688
- start: import_zod69.z.object({ date: import_zod69.z.string().optional(), dateTime: import_zod69.z.string().optional() }),
45689
- end: import_zod69.z.object({ date: import_zod69.z.string().optional(), dateTime: import_zod69.z.string().optional() }),
45690
- attendees: import_zod69.z.array(import_zod69.z.object({ email: import_zod69.z.string().optional(), displayName: import_zod69.z.string().optional(), self: import_zod69.z.boolean().optional() })).optional(),
45691
- attendeesOmitted: import_zod69.z.boolean().optional()
45835
+ eventSchema = import_zod70.z.object({
45836
+ summary: import_zod70.z.string().optional(),
45837
+ status: import_zod70.z.string().optional(),
45838
+ start: import_zod70.z.object({ date: import_zod70.z.string().optional(), dateTime: import_zod70.z.string().optional() }),
45839
+ end: import_zod70.z.object({ date: import_zod70.z.string().optional(), dateTime: import_zod70.z.string().optional() }),
45840
+ attendees: import_zod70.z.array(import_zod70.z.object({ email: import_zod70.z.string().optional(), displayName: import_zod70.z.string().optional(), self: import_zod70.z.boolean().optional() })).optional(),
45841
+ attendeesOmitted: import_zod70.z.boolean().optional()
45692
45842
  });
45693
45843
  }
45694
45844
  });
@@ -45699,7 +45849,7 @@ function registerGoogleCalendarTools(server2, calendar = createGoogleCalendar())
45699
45849
  server2,
45700
45850
  "connect_google_calendar",
45701
45851
  "Connect your own Google Calendar with read-only access. The first run opens Google sign-in and saves nothing. The second run finishes after reading your calendars. Use status when asked what is connected. Use disconnect to remove the local sign-in and revoke it with Google. No tool shows a secret.",
45702
- { action: import_zod70.z.enum(["start", "finish", "status", "disconnect"]).optional().describe("Use start to open sign-in. Use finish after browser sign-in. Use status to show only the last four characters. Use disconnect to remove and revoke access.") },
45852
+ { action: import_zod71.z.enum(["start", "finish", "status", "disconnect"]).optional().describe("Use start to open sign-in. Use finish after browser sign-in. Use status to show only the last four characters. Use disconnect to remove and revoke access.") },
45703
45853
  async ({ action }) => calendar.connect(action)
45704
45854
  );
45705
45855
  safeTool(
@@ -45707,18 +45857,18 @@ function registerGoogleCalendarTools(server2, calendar = createGoogleCalendar())
45707
45857
  "read_my_calendar",
45708
45858
  "Read today or the week ahead from your own Google Calendar. Each entry shows its time in your timezone and the other people attending. All-day entries say all day. An empty window says so. Your primary calendar is read unless you name another one. For a morning brief, show this as a separate personal calendar section and never count these entries as client appointments.",
45709
45859
  {
45710
- days: import_zod70.z.number().int().min(1).max(31).optional().describe("Use 1 for today. Use 7 for the week ahead. Use 3 for the next three days."),
45711
- calendar: import_zod70.z.string().min(1).optional().describe("The calendar name or identifier. Defaults to your primary calendar."),
45712
- timezone: import_zod70.z.string().min(1).optional().describe("Your IANA timezone, such as America/Phoenix. Defaults to your agency timezone, then your primary Google Calendar timezone, then this machine's timezone.")
45860
+ days: import_zod71.z.number().int().min(1).max(31).optional().describe("Use 1 for today. Use 7 for the week ahead. Use 3 for the next three days."),
45861
+ calendar: import_zod71.z.string().min(1).optional().describe("The calendar name or identifier. Defaults to your primary calendar."),
45862
+ timezone: import_zod71.z.string().min(1).optional().describe("Your IANA timezone, such as America/Phoenix. Defaults to your agency timezone, then your primary Google Calendar timezone, then this machine's timezone.")
45713
45863
  },
45714
45864
  async (options) => calendar.read(options)
45715
45865
  );
45716
45866
  }
45717
- var import_zod70;
45867
+ var import_zod71;
45718
45868
  var init_google_calendar2 = __esm({
45719
45869
  "src/tools/google-calendar.ts"() {
45720
45870
  "use strict";
45721
- import_zod70 = require("zod");
45871
+ import_zod71 = require("zod");
45722
45872
  init_tool_helpers();
45723
45873
  init_google_calendar();
45724
45874
  }
@@ -45947,12 +46097,12 @@ function registerDeliveryAuditTools(server2) {
45947
46097
  "audit_my_delivery",
45948
46098
  "Audit your OWN agency: how many hours a month each recurring job costs you, ranked worst first, with the ones that can already be taken off you named. Call it with nothing to see the questions; call it again with answers as you get them \u2014 they are saved, so you can finish it over several sittings. A job you cannot put minutes against is named as needing measuring and is never estimated.",
45949
46099
  {
45950
- answers: import_zod71.z.record(import_zod71.z.string(), import_zod71.z.object({
45951
- minutes: import_zod71.z.union([import_zod71.z.number(), import_zod71.z.string()]).optional().describe("Minutes for ONE instance of the job. 'unknown' is a real answer and is recorded as one."),
45952
- timesPerMonth: import_zod71.z.union([import_zod71.z.number(), import_zod71.z.string()]).optional().describe("How many times a month it happens.")
46100
+ answers: import_zod72.z.record(import_zod72.z.string(), import_zod72.z.object({
46101
+ minutes: import_zod72.z.union([import_zod72.z.number(), import_zod72.z.string()]).optional().describe("Minutes for ONE instance of the job. 'unknown' is a real answer and is recorded as one."),
46102
+ timesPerMonth: import_zod72.z.union([import_zod72.z.number(), import_zod72.z.string()]).optional().describe("How many times a month it happens.")
45953
46103
  })).optional().describe("Keyed by task id, from the questions this tool returns. Merges with what is already saved."),
45954
- available: import_zod71.z.record(import_zod71.z.string(), import_zod71.z.boolean()).optional().describe("Which tools this operator actually has, if it is not all of them. Omit on a normal licence."),
45955
- reset: import_zod71.z.boolean().optional().describe("Throw away the saved answers and start the audit again. Only on an explicit ask.")
46104
+ available: import_zod72.z.record(import_zod72.z.string(), import_zod72.z.boolean()).optional().describe("Which tools this operator actually has, if it is not all of them. Omit on a normal licence."),
46105
+ reset: import_zod72.z.boolean().optional().describe("Throw away the saved answers and start the audit again. Only on an explicit ask.")
45956
46106
  },
45957
46107
  async ({ answers, available, reset }) => {
45958
46108
  if (reset) resetDeliveryAudit();
@@ -45994,11 +46144,11 @@ function registerDeliveryAuditTools(server2) {
45994
46144
  }
45995
46145
  );
45996
46146
  }
45997
- var import_zod71, tasks, areas, areaLabel;
46147
+ var import_zod72, tasks, areas, areaLabel;
45998
46148
  var init_delivery_audit = __esm({
45999
46149
  "src/tools/delivery-audit.ts"() {
46000
46150
  "use strict";
46001
- import_zod71 = require("zod");
46151
+ import_zod72 = require("zod");
46002
46152
  init_tool_helpers();
46003
46153
  init_audit();
46004
46154
  init_store4();
@@ -46341,20 +46491,20 @@ function registerAgencyBuildTools(server2, knownTools) {
46341
46491
  "plan_automation",
46342
46492
  "Plan one of YOUR OWN automations \u2014 something specific to your agency, built out of the tools you already have. Give it the goal, what you found when you looked at the account (every id you are going to touch), and the steps. It refuses a plan that targets an id nobody looked at, a plan whose steps cannot run in any order, and a step with no reason on it. It writes nothing: it produces a plan you read and approve.",
46343
46493
  {
46344
- goal: import_zod72.z.string().min(4).describe("What this build is for, in your own words."),
46345
- findings: import_zod72.z.array(import_zod72.z.object({
46346
- kind: import_zod72.z.string().describe("pipeline, custom_field, workflow, calendar \u2014 whatever it is."),
46347
- id: import_zod72.z.string().describe("The real id, as the account reported it."),
46348
- name: import_zod72.z.string().optional(),
46349
- observedAt: import_zod72.z.string().optional().describe("When you looked. Defaults to now. Anything older than an hour is treated as not having looked.")
46494
+ goal: import_zod73.z.string().min(4).describe("What this build is for, in your own words."),
46495
+ findings: import_zod73.z.array(import_zod73.z.object({
46496
+ kind: import_zod73.z.string().describe("pipeline, custom_field, workflow, calendar \u2014 whatever it is."),
46497
+ id: import_zod73.z.string().describe("The real id, as the account reported it."),
46498
+ name: import_zod73.z.string().optional(),
46499
+ observedAt: import_zod73.z.string().optional().describe("When you looked. Defaults to now. Anything older than an hour is treated as not having looked.")
46350
46500
  })).describe("What you found when you explored. Every existing id a step targets has to be in here."),
46351
- steps: import_zod72.z.array(import_zod72.z.object({
46352
- id: import_zod72.z.string().describe("Short, unique within this plan."),
46353
- tool: import_zod72.z.string().describe("The tool that does it."),
46354
- why: import_zod72.z.string().min(3).describe("Plain English. This is what the operator reads before approving."),
46355
- targets: import_zod72.z.array(targetSchema).default([]),
46356
- creates: import_zod72.z.array(import_zod72.z.object({ ref: import_zod72.z.string(), kind: import_zod72.z.string() })).optional().describe("What this step brings into existence, for later steps to point at."),
46357
- args: import_zod72.z.record(import_zod72.z.string(), import_zod72.z.unknown()).optional().describe("The arguments, kept verbatim \u2014 the approval covers these, so changing one voids it.")
46501
+ steps: import_zod73.z.array(import_zod73.z.object({
46502
+ id: import_zod73.z.string().describe("Short, unique within this plan."),
46503
+ tool: import_zod73.z.string().describe("The tool that does it."),
46504
+ why: import_zod73.z.string().min(3).describe("Plain English. This is what the operator reads before approving."),
46505
+ targets: import_zod73.z.array(targetSchema).default([]),
46506
+ creates: import_zod73.z.array(import_zod73.z.object({ ref: import_zod73.z.string(), kind: import_zod73.z.string() })).optional().describe("What this step brings into existence, for later steps to point at."),
46507
+ args: import_zod73.z.record(import_zod73.z.string(), import_zod73.z.unknown()).optional().describe("The arguments, kept verbatim \u2014 the approval covers these, so changing one voids it.")
46358
46508
  })).min(1)
46359
46509
  },
46360
46510
  async ({ goal, findings, steps }) => {
@@ -46398,9 +46548,9 @@ function registerAgencyBuildTools(server2, knownTools) {
46398
46548
  "approve_automation",
46399
46549
  "Record that the OPERATOR has read this exact plan and approved it. Requires the plan's fingerprint: if a single argument changed since they read it, the approval is refused, because it covered a different document. Nothing may be recorded as applied until this has been done.",
46400
46550
  {
46401
- buildId: import_zod72.z.string(),
46402
- planId: import_zod72.z.string().describe("The fingerprint from plan_automation. It must match the plan as it stands now."),
46403
- approvedBy: import_zod72.z.string().describe("Who approved it. Their name, not yours.")
46551
+ buildId: import_zod73.z.string(),
46552
+ planId: import_zod73.z.string().describe("The fingerprint from plan_automation. It must match the plan as it stands now."),
46553
+ approvedBy: import_zod73.z.string().describe("Who approved it. Their name, not yours.")
46404
46554
  },
46405
46555
  async ({ buildId, planId, approvedBy }) => {
46406
46556
  const a = readAutomation(buildId);
@@ -46418,14 +46568,14 @@ function registerAgencyBuildTools(server2, knownTools) {
46418
46568
  "record_automation_step",
46419
46569
  "Record what happened when a step was run: applied, with how to undo it, or failed, with what came back. A step cannot be recorded as done without its undo \u2014 a write with no way back is the one thing this refuses \u2014 and a step cannot be recorded before the thing it depends on has run. A step that FAILED can be run again once you have fixed what stopped it: record the second attempt with retry:true and both attempts are kept.",
46420
46570
  {
46421
- buildId: import_zod72.z.string(),
46422
- stepId: import_zod72.z.string(),
46423
- state: import_zod72.z.enum(["applied", "failed"]),
46424
- undo: import_zod72.z.string().optional().describe("Required on 'applied'. How to reverse this exact write, specifically enough to act on months later."),
46425
- because: import_zod72.z.string().optional().describe("Required on 'failed'. What the account actually said."),
46426
- result: import_zod72.z.unknown().optional().describe("What came back, kept so a stall can be diagnosed without re-running anything."),
46427
- resolved: import_zod72.z.record(import_zod72.z.string(), import_zod72.z.string()).optional().describe("The real ids this step created, keyed by the ref later steps use."),
46428
- retry: import_zod72.z.boolean().optional().describe("This is a second run of a step that failed. Both attempts are kept, and the earlier failure stops blocking what came after it.")
46571
+ buildId: import_zod73.z.string(),
46572
+ stepId: import_zod73.z.string(),
46573
+ state: import_zod73.z.enum(["applied", "failed"]),
46574
+ undo: import_zod73.z.string().optional().describe("Required on 'applied'. How to reverse this exact write, specifically enough to act on months later."),
46575
+ because: import_zod73.z.string().optional().describe("Required on 'failed'. What the account actually said."),
46576
+ result: import_zod73.z.unknown().optional().describe("What came back, kept so a stall can be diagnosed without re-running anything."),
46577
+ resolved: import_zod73.z.record(import_zod73.z.string(), import_zod73.z.string()).optional().describe("The real ids this step created, keyed by the ref later steps use."),
46578
+ retry: import_zod73.z.boolean().optional().describe("This is a second run of a step that failed. Both attempts are kept, and the earlier failure stops blocking what came after it.")
46429
46579
  },
46430
46580
  async ({ buildId, stepId, state, undo, because, result, resolved: resolved2, retry }) => {
46431
46581
  const a = readAutomation(buildId);
@@ -46448,8 +46598,8 @@ function registerAgencyBuildTools(server2, knownTools) {
46448
46598
  "abandon_automation",
46449
46599
  "Stop a build for good. It does NOT undo anything and does not pretend to: it hands you back everything that is still applied to the account, so abandoning the plan and cleaning up after it stay two separate acts. A build abandoned with no reason recorded is one somebody re-plans identically next month, so a reason is required.",
46450
46600
  {
46451
- buildId: import_zod72.z.string(),
46452
- because: import_zod72.z.string().min(3).describe("Why it is being stopped. Kept with the build.")
46601
+ buildId: import_zod73.z.string(),
46602
+ because: import_zod73.z.string().min(3).describe("Why it is being stopped. Kept with the build.")
46453
46603
  },
46454
46604
  async ({ buildId, because }) => {
46455
46605
  const a = readAutomation(buildId);
@@ -46469,7 +46619,7 @@ function registerAgencyBuildTools(server2, knownTools) {
46469
46619
  server2,
46470
46620
  "automation_status",
46471
46621
  "Where your own builds actually are. A build that stopped is reported as STALLED, naming the step and what the account said \u2014 never as a percentage, because a build that is '70% complete' is one somebody lives with for three weeks. Also gives you the undo list, newest first.",
46472
- { buildId: import_zod72.z.string().optional().describe("One build. Omit for all of them.") },
46622
+ { buildId: import_zod73.z.string().optional().describe("One build. Omit for all of them.") },
46473
46623
  async ({ buildId }) => {
46474
46624
  const all = readAutomations();
46475
46625
  const ids = buildId ? [buildId] : Object.keys(all);
@@ -46500,17 +46650,17 @@ function registerAgencyBuildTools(server2, knownTools) {
46500
46650
  }
46501
46651
  );
46502
46652
  }
46503
- var import_zod72, targetSchema;
46653
+ var import_zod73, targetSchema;
46504
46654
  var init_agency_build = __esm({
46505
46655
  "src/tools/agency-build.ts"() {
46506
46656
  "use strict";
46507
- import_zod72 = require("zod");
46657
+ import_zod73 = require("zod");
46508
46658
  init_tool_helpers();
46509
46659
  init_automation();
46510
46660
  init_store5();
46511
- targetSchema = import_zod72.z.union([
46512
- import_zod72.z.object({ existing: import_zod72.z.string().describe("A real id, which must appear in `findings`."), kind: import_zod72.z.string() }),
46513
- import_zod72.z.object({ ref: import_zod72.z.string().describe("Something an earlier step in this plan creates."), kind: import_zod72.z.string() })
46661
+ targetSchema = import_zod73.z.union([
46662
+ import_zod73.z.object({ existing: import_zod73.z.string().describe("A real id, which must appear in `findings`."), kind: import_zod73.z.string() }),
46663
+ import_zod73.z.object({ ref: import_zod73.z.string().describe("Something an earlier step in this plan creates."), kind: import_zod73.z.string() })
46514
46664
  ]);
46515
46665
  }
46516
46666
  });
@@ -46684,7 +46834,7 @@ function registerClientEngagementTools(server2, registry2) {
46684
46834
  server2,
46685
46835
  "get_income_sources",
46686
46836
  "Read every source of income you have recorded \u2014 clients whose GoHighLevel accounts you run, software subscribers, event fees, revenue shares \u2014 with 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 everything active and monthly that has a price recorded, whatever kind of income it is) plus a count of the ones that do not, because a total assembled from partial data is not a total. Pass locationId for one record, or nothing for all of them. The locationId in the reply is for calling set_income_source \u2014 refer to people and companies by name when you answer the user, never by id. Local file on this machine; never touches GoHighLevel.",
46687
- { locationId: import_zod73.z.string().optional().describe("One record's id \u2014 a sub-account id for a client, or the key you recorded other income under. Omit for the whole roster.") },
46837
+ { locationId: import_zod74.z.string().optional().describe("One record's id \u2014 a sub-account id for a client, or the key you recorded other income under. Omit for the whole roster.") },
46688
46838
  async ({ locationId: locationId2 }) => {
46689
46839
  const profile = readAgencyProfile();
46690
46840
  const saved = readClientEngagements();
@@ -46730,32 +46880,32 @@ function registerClientEngagementTools(server2, registry2) {
46730
46880
  "set_income_source",
46731
46881
  "Record or update ONE source of income \u2014 a client whose GoHighLevel account you run, a software subscriber, an event fee \u2014 with its offer, price, monthly or one-time, start and renewal dates, status, who runs the account, and SOPs specific to it. 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 record entirely pass remove:true. Dates are YYYY-MM-DD. Local file on this machine; never touches GoHighLevel.",
46732
46882
  {
46733
- locationId: import_zod73.z.string().optional().describe("The client's GHL sub-account id \u2014 the same id the board uses. Use this for a client whose GoHighLevel account you run. For income with no sub-account, use `key` instead."),
46883
+ locationId: import_zod74.z.string().optional().describe("The client's GHL sub-account id \u2014 the same id the board uses. Use this for a client whose GoHighLevel account you run. For income with no sub-account, use `key` instead."),
46734
46884
  // WIDENED 2026-09-01 (Jerry: "different clients pay for different things.
46735
46885
  // My income comes from a variety of sources"). Two argument names for one
46736
46886
  // slot, rather than one argument whose name lies about half its uses: a
46737
46887
  // Stripe-only software subscriber has no locationId, and calling their
46738
46888
  // Stripe customer id a "locationId" is the kind of description-contradicts-
46739
46889
  // the-code defect this product keeps paying for.
46740
- key: import_zod73.z.string().optional().describe("Identifier for income that is NOT a GoHighLevel account you run \u2014 a software subscriber, an event fee, a revenue share. Anything stable and unique that is NOT one of your GoHighLevel account ids; a Stripe customer id is ideal because it makes the Stripe pairing automatic. Send this instead of locationId, never both."),
46741
- kind: import_zod73.z.enum(["client", "software", "other"]).nullable().optional().describe("What kind of income this is. 'client' (the default) = you run a GoHighLevel account for them, so they belong on the build board and are measured against your price floor. 'software' = they bought a licence or subscription. 'other' = events, revenue share, training, referrals. Only 'client' ever appears as a build."),
46890
+ key: import_zod74.z.string().optional().describe("Identifier for income that is NOT a GoHighLevel account you run \u2014 a software subscriber, an event fee, a revenue share. Anything stable and unique that is NOT one of your GoHighLevel account ids; a Stripe customer id is ideal because it makes the Stripe pairing automatic. Send this instead of locationId, never both."),
46891
+ kind: import_zod74.z.enum(["client", "software", "other"]).nullable().optional().describe("What kind of income this is. 'client' (the default) = you run a GoHighLevel account for them, so they belong on the build board and are measured against your price floor. 'software' = they bought a licence or subscription. 'other' = events, revenue share, training, referrals. Only 'client' ever appears as a build."),
46742
46892
  // Every editable field is NULLABLE, not just optional. The description
46743
46893
  // promises "pass null to clear", and a schema that rejects null would make
46744
46894
  // that promise a lie the tests never caught, because they cleared fields by
46745
46895
  // calling the store directly instead of through this surface (Codex,
46746
46896
  // 2026-08-28). Absent = untouched; null = clear; a value = set.
46747
- clientName: import_zod73.z.string().nullable().optional().describe("How you refer to them, if it differs from the account name."),
46748
- offer: import_zod73.z.string().nullable().optional().describe("Which of your offers they are on. Matched against your agency profile so a typo is reported."),
46749
- price: import_zod73.z.number().min(0).nullable().optional().describe("What they actually pay \u2014 not your floor."),
46750
- cadence: import_zod73.z.enum(["one-time", "monthly"]).nullable().optional(),
46751
- currency: import_zod73.z.string().nullable().optional(),
46752
- startedOn: import_zod73.z.string().nullable().optional().describe("YYYY-MM-DD."),
46753
- renewsOn: import_zod73.z.string().nullable().optional().describe("YYYY-MM-DD."),
46754
- status: import_zod73.z.enum(["active", "paused", "ended"]).nullable().optional(),
46755
- owner: import_zod73.z.string().nullable().optional().describe("Which staff member runs this account."),
46756
- sops: import_zod73.z.array(import_zod73.z.string()).nullable().optional().describe("SOPs that apply to this one only. Leave empty to use your agency's."),
46757
- notes: import_zod73.z.string().nullable().optional(),
46758
- remove: import_zod73.z.boolean().optional().describe("Delete this record entirely.")
46897
+ clientName: import_zod74.z.string().nullable().optional().describe("How you refer to them, if it differs from the account name."),
46898
+ offer: import_zod74.z.string().nullable().optional().describe("Which of your offers they are on. Matched against your agency profile so a typo is reported."),
46899
+ price: import_zod74.z.number().min(0).nullable().optional().describe("What they actually pay \u2014 not your floor."),
46900
+ cadence: import_zod74.z.enum(["one-time", "monthly"]).nullable().optional(),
46901
+ currency: import_zod74.z.string().nullable().optional(),
46902
+ startedOn: import_zod74.z.string().nullable().optional().describe("YYYY-MM-DD."),
46903
+ renewsOn: import_zod74.z.string().nullable().optional().describe("YYYY-MM-DD."),
46904
+ status: import_zod74.z.enum(["active", "paused", "ended"]).nullable().optional(),
46905
+ owner: import_zod74.z.string().nullable().optional().describe("Which staff member runs this account."),
46906
+ sops: import_zod74.z.array(import_zod74.z.string()).nullable().optional().describe("SOPs that apply to this one only. Leave empty to use your agency's."),
46907
+ notes: import_zod74.z.string().nullable().optional(),
46908
+ remove: import_zod74.z.boolean().optional().describe("Delete this record entirely.")
46759
46909
  },
46760
46910
  async (args) => {
46761
46911
  const { locationId: locationId2, key, remove, ...rest } = args;
@@ -46805,7 +46955,7 @@ function registerClientEngagementTools(server2, registry2) {
46805
46955
  server2,
46806
46956
  "client_this_week",
46807
46957
  "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 already in motion, 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.",
46808
- { client: import_zod73.z.string().describe("The client's name as you refer to them, or their sub-account id.") },
46958
+ { client: import_zod74.z.string().describe("The client's name as you refer to them, or their sub-account id.") },
46809
46959
  async ({ client }) => {
46810
46960
  const state = readBoardState();
46811
46961
  const roster = knownClients(registry2, state);
@@ -46825,11 +46975,11 @@ function registerClientEngagementTools(server2, registry2) {
46825
46975
  }
46826
46976
  );
46827
46977
  }
46828
- var import_zod73;
46978
+ var import_zod74;
46829
46979
  var init_client_engagements2 = __esm({
46830
46980
  "src/tools/client-engagements.ts"() {
46831
46981
  "use strict";
46832
- import_zod73 = require("zod");
46982
+ import_zod74 = require("zod");
46833
46983
  init_tool_helpers();
46834
46984
  init_agency_profile();
46835
46985
  init_client_engagements();
@@ -47289,14 +47439,14 @@ function registerAuditReportTools(server2) {
47289
47439
  "build_audit_report",
47290
47440
  "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.",
47291
47441
  {
47292
- audit: import_zod74.z.record(import_zod74.z.string(), import_zod74.z.unknown()).describe("The full JSON result returned by audit_workflows."),
47293
- audience: import_zod74.z.enum(["internal", "prospect"]).optional().describe("Who reads it. 'internal' (default) is a report about an account you already run, and may use your own minimum monthly as what a client is worth. 'prospect' is a document about somebody else's business: your numbers are NEVER used, so findings stay uncosted unless you supply THEIR lead volume, close rate and client value."),
47294
- accountName: import_zod74.z.string().describe("The client's name as they should see it on the document."),
47295
- locationId: import_zod74.z.string().optional().describe("Sub-account id. Recorded on the document, never shown in the body."),
47296
- leadsPerMonth: import_zod74.z.number().min(0).optional().describe("Leads this account actually receives per month, if known. Without it, findings are not costed in dollars."),
47297
- closeRate: import_zod74.z.number().min(0).max(1).optional().describe("Share of reached leads that become clients, 0-1. Never guessed."),
47298
- monthlyValuePerClient: import_zod74.z.number().min(0).optional().describe("What one new client is worth per month. Defaults to your agency profile's minimum monthly fee."),
47299
- generatedAt: import_zod74.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
47442
+ audit: import_zod75.z.record(import_zod75.z.string(), import_zod75.z.unknown()).describe("The full JSON result returned by audit_workflows."),
47443
+ audience: import_zod75.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."),
47444
+ accountName: import_zod75.z.string().describe("The client's name as they should see it on the document."),
47445
+ locationId: import_zod75.z.string().optional().describe("Sub-account id. Recorded on the document, never shown in the body."),
47446
+ leadsPerMonth: import_zod75.z.number().min(0).optional().describe("Leads this account actually receives per month, if known. Without it, findings are not costed in dollars."),
47447
+ closeRate: import_zod75.z.number().min(0).max(1).optional().describe("Share of reached leads that become clients, 0-1. Never guessed."),
47448
+ monthlyValuePerClient: import_zod75.z.number().min(0).optional().describe("What one new client is worth per month. Defaults to your agency profile's minimum monthly fee."),
47449
+ generatedAt: import_zod75.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
47300
47450
  },
47301
47451
  async (args) => {
47302
47452
  const a = args;
@@ -47354,12 +47504,12 @@ function registerAuditReportTools(server2) {
47354
47504
  "build_site_audit_report",
47355
47505
  "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.",
47356
47506
  {
47357
- observations: import_zod74.z.record(import_zod74.z.string(), import_zod74.z.unknown()).describe("The verified observations payload from the site read: site, pagesRead, observations, limits. Passed through as-is."),
47358
- businessName: import_zod74.z.string().describe("The business's name as it should appear on the document."),
47359
- leadsPerMonth: import_zod74.z.number().min(0).optional().describe("THEIR leads per month, if they told you. Never your own figure."),
47360
- closeRate: import_zod74.z.number().min(0).max(1).optional().describe("THEIR close rate, 0-1, if they told you. Never guessed."),
47361
- monthlyValuePerClient: import_zod74.z.number().min(0).optional().describe("What one client is worth to THEM per month, if they told you."),
47362
- generatedAt: import_zod74.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
47507
+ observations: import_zod75.z.record(import_zod75.z.string(), import_zod75.z.unknown()).describe("The verified observations payload from the site read: site, pagesRead, observations, limits. Passed through as-is."),
47508
+ businessName: import_zod75.z.string().describe("The business's name as it should appear on the document."),
47509
+ leadsPerMonth: import_zod75.z.number().min(0).optional().describe("THEIR leads per month, if they told you. Never your own figure."),
47510
+ closeRate: import_zod75.z.number().min(0).max(1).optional().describe("THEIR close rate, 0-1, if they told you. Never guessed."),
47511
+ monthlyValuePerClient: import_zod75.z.number().min(0).optional().describe("What one client is worth to THEM per month, if they told you."),
47512
+ generatedAt: import_zod75.z.string().optional().describe("ISO timestamp for the document. Defaults to now.")
47363
47513
  },
47364
47514
  async (args) => {
47365
47515
  const a = args;
@@ -47418,11 +47568,11 @@ function registerAuditReportTools(server2) {
47418
47568
  }
47419
47569
  );
47420
47570
  }
47421
- var import_zod74;
47571
+ var import_zod75;
47422
47572
  var init_audit_report2 = __esm({
47423
47573
  "src/tools/audit-report.ts"() {
47424
47574
  "use strict";
47425
- import_zod74 = require("zod");
47575
+ import_zod75 = require("zod");
47426
47576
  init_tool_helpers();
47427
47577
  init_audit_report();
47428
47578
  init_audit_translate();
@@ -47690,9 +47840,9 @@ function registerDailyBriefTools(server2, registry2) {
47690
47840
  "daily_brief",
47691
47841
  "The morning brief across ALL your clients: today's appointments first, then what is waiting on YOU, what is already running, renewals inside 30 days, clients PAYING YOU WHO HAVE BOOKED NOTHING IN THREE WEEKS, 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.",
47692
47842
  {
47693
- live: import_zod75.z.boolean().optional().describe("Read today's calendars and lead counts from the connected accounts. Default true. false answers from local records alone."),
47694
- hoursBack: import_zod75.z.number().min(1).max(72).optional().describe("Count new leads over this many hours instead of since midnight."),
47695
- quietAfterDays: import_zod75.z.number().min(1).max(180).optional().describe("How many days a paying client has to book nothing before the brief says so. Defaults to 21.")
47843
+ live: import_zod76.z.boolean().optional().describe("Read today's calendars and lead counts from the connected accounts. Default true. false answers from local records alone."),
47844
+ hoursBack: import_zod76.z.number().min(1).max(72).optional().describe("Count new leads over this many hours instead of since midnight."),
47845
+ quietAfterDays: import_zod76.z.number().min(1).max(180).optional().describe("How many days a paying client has to book nothing before the brief says so. Defaults to 21.")
47696
47846
  },
47697
47847
  async ({ live, hoursBack, quietAfterDays }) => {
47698
47848
  const now = /* @__PURE__ */ new Date();
@@ -47729,11 +47879,11 @@ function registerDailyBriefTools(server2, registry2) {
47729
47879
  }
47730
47880
  );
47731
47881
  }
47732
- var import_zod75;
47882
+ var import_zod76;
47733
47883
  var init_daily_brief2 = __esm({
47734
47884
  "src/tools/daily-brief.ts"() {
47735
47885
  "use strict";
47736
- import_zod75 = require("zod");
47886
+ import_zod76 = require("zod");
47737
47887
  init_tool_helpers();
47738
47888
  init_ghl_client();
47739
47889
  init_agency_profile();
@@ -47786,7 +47936,7 @@ function registerSnapshotTools(server2, client, registry2) {
47786
47936
  "list_snapshots",
47787
47937
  "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.",
47788
47938
  {
47789
- companyId: import_zod76.z.string().optional().describe(
47939
+ companyId: import_zod77.z.string().optional().describe(
47790
47940
  "Agency/company ID whose snapshots to list. Defaults to the active location's company. Must match the company your agency key is scoped to."
47791
47941
  )
47792
47942
  },
@@ -47811,11 +47961,11 @@ function registerSnapshotTools(server2, client, registry2) {
47811
47961
  "create_snapshot_share_link",
47812
47962
  "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.",
47813
47963
  {
47814
- snapshot_id: import_zod76.z.string().describe("The snapshot id to share (from list_snapshots)."),
47815
- share_type: import_zod76.z.enum(SHARE_TYPES).describe(
47964
+ snapshot_id: import_zod77.z.string().describe("The snapshot id to share (from list_snapshots)."),
47965
+ share_type: import_zod77.z.enum(SHARE_TYPES).describe(
47816
47966
  "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."
47817
47967
  ),
47818
- companyId: import_zod76.z.string().optional().describe(
47968
+ companyId: import_zod77.z.string().optional().describe(
47819
47969
  "Agency/company ID that owns the snapshot. Defaults to the active location's company. Must match the company your agency key is scoped to."
47820
47970
  )
47821
47971
  },
@@ -47843,16 +47993,16 @@ function registerSnapshotTools(server2, client, registry2) {
47843
47993
  }
47844
47994
  );
47845
47995
  }
47846
- var import_zod76, SnapshotSchema, SnapshotsResponseSchema, ShareLinkResponseSchema, SHARE_TYPES;
47996
+ var import_zod77, SnapshotSchema, SnapshotsResponseSchema, ShareLinkResponseSchema, SHARE_TYPES;
47847
47997
  var init_snapshots = __esm({
47848
47998
  "src/tools/snapshots.ts"() {
47849
47999
  "use strict";
47850
- import_zod76 = require("zod");
48000
+ import_zod77 = require("zod");
47851
48001
  init_ghl_client();
47852
48002
  init_tool_helpers();
47853
- SnapshotSchema = import_zod76.z.object({ id: import_zod76.z.string(), name: import_zod76.z.string(), type: import_zod76.z.string() }).passthrough();
47854
- SnapshotsResponseSchema = import_zod76.z.object({ snapshots: import_zod76.z.array(SnapshotSchema) }).passthrough();
47855
- ShareLinkResponseSchema = import_zod76.z.object({ id: import_zod76.z.string(), shareLink: import_zod76.z.string() }).passthrough();
48003
+ SnapshotSchema = import_zod77.z.object({ id: import_zod77.z.string(), name: import_zod77.z.string(), type: import_zod77.z.string() }).passthrough();
48004
+ SnapshotsResponseSchema = import_zod77.z.object({ snapshots: import_zod77.z.array(SnapshotSchema) }).passthrough();
48005
+ ShareLinkResponseSchema = import_zod77.z.object({ id: import_zod77.z.string(), shareLink: import_zod77.z.string() }).passthrough();
47856
48006
  SHARE_TYPES = [
47857
48007
  "link",
47858
48008
  "permanent_link",
@@ -47870,7 +48020,7 @@ function registerPhoneTools(server2, client) {
47870
48020
  "list_phone_numbers",
47871
48021
  "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).",
47872
48022
  {
47873
- locationId: import_zod77.z.string().optional().describe("Defaults to the active location.")
48023
+ locationId: import_zod78.z.string().optional().describe("Defaults to the active location.")
47874
48024
  },
47875
48025
  async ({ locationId: locationId2 }) => {
47876
48026
  const loc = client.resolveLocationId(locationId2);
@@ -47888,7 +48038,7 @@ function registerPhoneTools(server2, client) {
47888
48038
  "list_number_pools",
47889
48039
  "List LC Phone number pools configured for a location. Read-only.",
47890
48040
  {
47891
- locationId: import_zod77.z.string().optional().describe("Defaults to the active location.")
48041
+ locationId: import_zod78.z.string().optional().describe("Defaults to the active location.")
47892
48042
  },
47893
48043
  async ({ locationId: locationId2 }) => {
47894
48044
  const loc = client.resolveLocationId(locationId2);
@@ -47898,15 +48048,15 @@ function registerPhoneTools(server2, client) {
47898
48048
  }
47899
48049
  );
47900
48050
  }
47901
- var import_zod77, PhoneNumberSchema, NumbersResponseSchema, PoolsResponseSchema;
48051
+ var import_zod78, PhoneNumberSchema, NumbersResponseSchema, PoolsResponseSchema;
47902
48052
  var init_phone = __esm({
47903
48053
  "src/tools/phone.ts"() {
47904
48054
  "use strict";
47905
- import_zod77 = require("zod");
48055
+ import_zod78 = require("zod");
47906
48056
  init_tool_helpers();
47907
- PhoneNumberSchema = import_zod77.z.object({ sid: import_zod77.z.string(), value: import_zod77.z.string(), title: import_zod77.z.string().optional() }).passthrough();
47908
- NumbersResponseSchema = import_zod77.z.object({ phoneNumbers: import_zod77.z.array(PhoneNumberSchema) }).passthrough();
47909
- PoolsResponseSchema = import_zod77.z.object({ pools: import_zod77.z.array(import_zod77.z.object({}).passthrough()) }).passthrough();
48057
+ PhoneNumberSchema = import_zod78.z.object({ sid: import_zod78.z.string(), value: import_zod78.z.string(), title: import_zod78.z.string().optional() }).passthrough();
48058
+ NumbersResponseSchema = import_zod78.z.object({ phoneNumbers: import_zod78.z.array(PhoneNumberSchema) }).passthrough();
48059
+ PoolsResponseSchema = import_zod78.z.object({ pools: import_zod78.z.array(import_zod78.z.object({}).passthrough()) }).passthrough();
47910
48060
  }
47911
48061
  });
47912
48062
 
@@ -47924,8 +48074,8 @@ function registerAccountHealthTools(server2, client) {
47924
48074
  "get_account_health_summary",
47925
48075
  "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).",
47926
48076
  {
47927
- locationId: import_zod78.z.string().optional().describe("Defaults to the active location."),
47928
- windowDays: import_zod78.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
48077
+ locationId: import_zod79.z.string().optional().describe("Defaults to the active location."),
48078
+ windowDays: import_zod79.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
47929
48079
  },
47930
48080
  async ({ locationId: locationId2, windowDays }) => {
47931
48081
  const loc = client.resolveLocationId(locationId2);
@@ -47989,16 +48139,16 @@ function registerAccountHealthTools(server2, client) {
47989
48139
  }
47990
48140
  );
47991
48141
  }
47992
- var import_zod78, MetaTotalSchema, TotalSchema2, NumbersSchema, OPP_STATUSES;
48142
+ var import_zod79, MetaTotalSchema, TotalSchema2, NumbersSchema, OPP_STATUSES;
47993
48143
  var init_account_health = __esm({
47994
48144
  "src/tools/account-health.ts"() {
47995
48145
  "use strict";
47996
- import_zod78 = require("zod");
48146
+ import_zod79 = require("zod");
47997
48147
  init_tool_helpers();
47998
48148
  init_contact_window();
47999
- MetaTotalSchema = import_zod78.z.object({ meta: import_zod78.z.object({ total: import_zod78.z.number() }).passthrough() }).passthrough();
48000
- TotalSchema2 = import_zod78.z.object({ total: import_zod78.z.number() }).passthrough();
48001
- NumbersSchema = import_zod78.z.object({ phoneNumbers: import_zod78.z.array(import_zod78.z.unknown()) }).passthrough();
48149
+ MetaTotalSchema = import_zod79.z.object({ meta: import_zod79.z.object({ total: import_zod79.z.number() }).passthrough() }).passthrough();
48150
+ TotalSchema2 = import_zod79.z.object({ total: import_zod79.z.number() }).passthrough();
48151
+ NumbersSchema = import_zod79.z.object({ phoneNumbers: import_zod79.z.array(import_zod79.z.unknown()) }).passthrough();
48002
48152
  OPP_STATUSES = ["open", "won", "lost", "abandoned"];
48003
48153
  }
48004
48154
  });
@@ -48191,11 +48341,11 @@ function validateBrief(input) {
48191
48341
  warnings: []
48192
48342
  };
48193
48343
  }
48194
- var import_zod79, BRIEF_SCHEMA_VERSION, PRESETS, presetSchema, BRIEF_SOURCES, briefSourceSchema, pricePointSchema, staffMemberSchema, BRIEF_CALENDAR_TYPES, briefCalendarSchema, TRI_STATES, briefSchema;
48344
+ var import_zod80, BRIEF_SCHEMA_VERSION, PRESETS, presetSchema, BRIEF_SOURCES, briefSourceSchema, pricePointSchema, staffMemberSchema, BRIEF_CALENDAR_TYPES, briefCalendarSchema, TRI_STATES, briefSchema;
48195
48345
  var init_brief2 = __esm({
48196
48346
  "src/intake-to-build/brief.ts"() {
48197
48347
  "use strict";
48198
- import_zod79 = require("zod");
48348
+ import_zod80 = require("zod");
48199
48349
  BRIEF_SCHEMA_VERSION = "0.1";
48200
48350
  PRESETS = [
48201
48351
  "generic",
@@ -48205,105 +48355,105 @@ var init_brief2 = __esm({
48205
48355
  "ecom",
48206
48356
  "agency"
48207
48357
  ];
48208
- presetSchema = import_zod79.z.enum(PRESETS);
48358
+ presetSchema = import_zod80.z.enum(PRESETS);
48209
48359
  BRIEF_SOURCES = ["agency_os", "business_os", "intake_form", "hybrid"];
48210
- briefSourceSchema = import_zod79.z.enum(BRIEF_SOURCES);
48211
- pricePointSchema = import_zod79.z.object({
48212
- name: import_zod79.z.string(),
48213
- price: import_zod79.z.string()
48360
+ briefSourceSchema = import_zod80.z.enum(BRIEF_SOURCES);
48361
+ pricePointSchema = import_zod80.z.object({
48362
+ name: import_zod80.z.string(),
48363
+ price: import_zod80.z.string()
48214
48364
  });
48215
- staffMemberSchema = import_zod79.z.object({
48216
- name: import_zod79.z.string().min(1),
48217
- email: import_zod79.z.string().min(1),
48365
+ staffMemberSchema = import_zod80.z.object({
48366
+ name: import_zod80.z.string().min(1),
48367
+ email: import_zod80.z.string().min(1),
48218
48368
  /** Job title as the client wrote it ("Front desk", "Provider"); omitted when
48219
48369
  * the line had none. The plan decides admin/user from it. */
48220
- role: import_zod79.z.string().optional(),
48221
- mobile: import_zod79.z.string().optional()
48370
+ role: import_zod80.z.string().optional(),
48371
+ mobile: import_zod80.z.string().optional()
48222
48372
  });
48223
48373
  BRIEF_CALENDAR_TYPES = ["one_on_one", "round_robin", "class"];
48224
- briefCalendarSchema = import_zod79.z.object({
48225
- name: import_zod79.z.string().min(1),
48226
- type: import_zod79.z.enum(BRIEF_CALENDAR_TYPES),
48227
- staffNames: import_zod79.z.array(import_zod79.z.string()),
48374
+ briefCalendarSchema = import_zod80.z.object({
48375
+ name: import_zod80.z.string().min(1),
48376
+ type: import_zod80.z.enum(BRIEF_CALENDAR_TYPES),
48377
+ staffNames: import_zod80.z.array(import_zod80.z.string()),
48228
48378
  /** Appointment length in minutes, when the client said one ("15 minutes").
48229
48379
  * Lands on the plan calendar as slotDuration (finding 25). */
48230
- durationMinutes: import_zod79.z.number().int().positive().optional()
48380
+ durationMinutes: import_zod80.z.number().int().positive().optional()
48231
48381
  });
48232
48382
  TRI_STATES = ["yes", "no", "unsure"];
48233
- briefSchema = import_zod79.z.object({
48234
- schemaVersion: import_zod79.z.string(),
48235
- briefId: import_zod79.z.string(),
48383
+ briefSchema = import_zod80.z.object({
48384
+ schemaVersion: import_zod80.z.string(),
48385
+ briefId: import_zod80.z.string(),
48236
48386
  preset: presetSchema,
48237
48387
  briefSource: briefSourceSchema,
48238
48388
  /** Partner-OS deep structures (ICA / offer / brand-DNA), carried verbatim. */
48239
- extended: import_zod79.z.record(import_zod79.z.string(), import_zod79.z.unknown()).optional(),
48240
- business: import_zod79.z.object({
48241
- name: import_zod79.z.string(),
48242
- type: import_zod79.z.string().optional(),
48243
- website: import_zod79.z.string().optional(),
48244
- location: import_zod79.z.string().optional(),
48245
- timezone: import_zod79.z.string().optional(),
48389
+ extended: import_zod80.z.record(import_zod80.z.string(), import_zod80.z.unknown()).optional(),
48390
+ business: import_zod80.z.object({
48391
+ name: import_zod80.z.string(),
48392
+ type: import_zod80.z.string().optional(),
48393
+ website: import_zod80.z.string().optional(),
48394
+ location: import_zod80.z.string().optional(),
48395
+ timezone: import_zod80.z.string().optional(),
48246
48396
  // Ratified additions (atlas 2026-06-15). Enum-ish but kept as strings for
48247
48397
  // the same tolerance reason as business.type (don't reject valid briefs).
48248
- teamSize: import_zod79.z.string().optional(),
48249
- monthlyLeadVolume: import_zod79.z.string().optional(),
48250
- hours: import_zod79.z.string().optional()
48398
+ teamSize: import_zod80.z.string().optional(),
48399
+ monthlyLeadVolume: import_zod80.z.string().optional(),
48400
+ hours: import_zod80.z.string().optional()
48251
48401
  }).passthrough(),
48252
- offer: import_zod79.z.object({
48253
- summary: import_zod79.z.string().optional(),
48402
+ offer: import_zod80.z.object({
48403
+ summary: import_zod80.z.string().optional(),
48254
48404
  // Parsed best-effort; tolerate a raw string when parsing was not possible.
48255
- pricePoints: import_zod79.z.union([import_zod79.z.array(pricePointSchema), import_zod79.z.string()]).optional(),
48256
- leadMagnet: import_zod79.z.string().optional(),
48257
- avgDealValue: import_zod79.z.string().optional()
48405
+ pricePoints: import_zod80.z.union([import_zod80.z.array(pricePointSchema), import_zod80.z.string()]).optional(),
48406
+ leadMagnet: import_zod80.z.string().optional(),
48407
+ avgDealValue: import_zod80.z.string().optional()
48258
48408
  }).passthrough().optional(),
48259
- audience: import_zod79.z.object({
48260
- ideal: import_zod79.z.string().optional(),
48261
- painPoints: import_zod79.z.array(import_zod79.z.string()).optional(),
48262
- objections: import_zod79.z.array(import_zod79.z.string()).optional()
48409
+ audience: import_zod80.z.object({
48410
+ ideal: import_zod80.z.string().optional(),
48411
+ painPoints: import_zod80.z.array(import_zod80.z.string()).optional(),
48412
+ objections: import_zod80.z.array(import_zod80.z.string()).optional()
48263
48413
  }).passthrough().optional(),
48264
- goal: import_zod79.z.object({
48414
+ goal: import_zod80.z.object({
48265
48415
  // Kept as string: the form option labels are the canonical values, but
48266
48416
  // the contract sample shortens some (e.g. "high-touch"). See §7 note.
48267
- primary: import_zod79.z.string(),
48268
- salesStages: import_zod79.z.array(import_zod79.z.string()).optional(),
48269
- bookingNeeded: import_zod79.z.boolean().optional(),
48270
- followUpStyle: import_zod79.z.string().optional()
48417
+ primary: import_zod80.z.string(),
48418
+ salesStages: import_zod80.z.array(import_zod80.z.string()).optional(),
48419
+ bookingNeeded: import_zod80.z.boolean().optional(),
48420
+ followUpStyle: import_zod80.z.string().optional()
48271
48421
  }).passthrough(),
48272
- channels: import_zod79.z.object({
48273
- email: import_zod79.z.boolean().optional(),
48274
- sms: import_zod79.z.boolean().optional(),
48275
- a2pStatus: import_zod79.z.string().optional(),
48276
- payment: import_zod79.z.string().optional(),
48277
- calendarConnected: import_zod79.z.boolean().optional(),
48278
- social: import_zod79.z.array(import_zod79.z.string()).optional(),
48422
+ channels: import_zod80.z.object({
48423
+ email: import_zod80.z.boolean().optional(),
48424
+ sms: import_zod80.z.boolean().optional(),
48425
+ a2pStatus: import_zod80.z.string().optional(),
48426
+ payment: import_zod80.z.string().optional(),
48427
+ calendarConnected: import_zod80.z.boolean().optional(),
48428
+ social: import_zod80.z.array(import_zod80.z.string()).optional(),
48279
48429
  /** "Do you already have a phone number in GoHighLevel?" */
48280
- hasPhoneNumber: import_zod79.z.enum(TRI_STATES).optional()
48430
+ hasPhoneNumber: import_zod80.z.enum(TRI_STATES).optional()
48281
48431
  }).passthrough().optional(),
48282
48432
  /** Section G — who is on the account and who gets pinged. */
48283
- team: import_zod79.z.object({
48284
- staff: import_zod79.z.array(staffMemberSchema).optional(),
48433
+ team: import_zod80.z.object({
48434
+ staff: import_zod80.z.array(staffMemberSchema).optional(),
48285
48435
  /** Who is notified about new leads: a staff name, or "the owner". */
48286
- notifyName: import_zod79.z.string().optional(),
48436
+ notifyName: import_zod80.z.string().optional(),
48287
48437
  /** Who takes booking / follow-up calls: a staff name, or "the owner". */
48288
- callsName: import_zod79.z.string().optional()
48438
+ callsName: import_zod80.z.string().optional()
48289
48439
  }).passthrough().optional(),
48290
48440
  /** Section H — every booking calendar the client asked for. */
48291
- calendars: import_zod79.z.array(briefCalendarSchema).optional(),
48441
+ calendars: import_zod80.z.array(briefCalendarSchema).optional(),
48292
48442
  /** Section I — copy inputs, verbatim. */
48293
- voice: import_zod79.z.object({
48294
- threeWords: import_zod79.z.string().optional(),
48295
- signatureLine: import_zod79.z.string().optional()
48443
+ voice: import_zod80.z.object({
48444
+ threeWords: import_zod80.z.string().optional(),
48445
+ signatureLine: import_zod80.z.string().optional()
48296
48446
  }).passthrough().optional(),
48297
- assets: import_zod79.z.object({
48298
- existingPipeline: import_zod79.z.string().optional(),
48299
- existingWorkflows: import_zod79.z.string().optional(),
48300
- brand: import_zod79.z.string().optional(),
48301
- notes: import_zod79.z.string().optional()
48447
+ assets: import_zod80.z.object({
48448
+ existingPipeline: import_zod80.z.string().optional(),
48449
+ existingWorkflows: import_zod80.z.string().optional(),
48450
+ brand: import_zod80.z.string().optional(),
48451
+ notes: import_zod80.z.string().optional()
48302
48452
  }).passthrough().optional(),
48303
- flags: import_zod79.z.array(import_zod79.z.string()).optional(),
48453
+ flags: import_zod80.z.array(import_zod80.z.string()).optional(),
48304
48454
  /** Parse-time notes from the normalizer (a staff line it could not read,
48305
48455
  * a calendar type it had to assume). Never fatal; surfaced by validate_brief. */
48306
- warnings: import_zod79.z.array(import_zod79.z.string()).optional()
48456
+ warnings: import_zod80.z.array(import_zod80.z.string()).optional()
48307
48457
  }).strict();
48308
48458
  }
48309
48459
  });
@@ -51421,7 +51571,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
51421
51571
  "get_intake_question_set",
51422
51572
  `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.`,
51423
51573
  {
51424
- industry: import_zod80.z.string().optional().describe(`Industry pack slug to compose in (${INDUSTRY_PACKS.map((p) => p.slug).join(", ")}). Omit for the base set.`)
51574
+ industry: import_zod81.z.string().optional().describe(`Industry pack slug to compose in (${INDUSTRY_PACKS.map((p) => p.slug).join(", ")}). Omit for the base set.`)
51425
51575
  },
51426
51576
  async ({ industry }) => {
51427
51577
  const agency = readOverlay();
@@ -51452,32 +51602,32 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
51452
51602
  "validate_brief",
51453
51603
  "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.",
51454
51604
  {
51455
- brief: import_zod80.z.record(import_zod80.z.string(), import_zod80.z.unknown()).describe("The Brief object to validate.")
51605
+ brief: import_zod81.z.record(import_zod81.z.string(), import_zod81.z.unknown()).describe("The Brief object to validate.")
51456
51606
  },
51457
51607
  async ({ brief }) => validateBrief(brief)
51458
51608
  );
51459
51609
  const buildPlanSchema2 = {
51460
- plan: import_zod80.z.record(import_zod80.z.string(), import_zod80.z.unknown()).optional().describe("The approved \xA75 Build Plan object. Omit it and pass useSavedPlan:true to build from the plan already saved for this account."),
51461
- fromPreset: import_zod80.z.boolean().optional().describe("PLAN-FROM-PRESET (PRD row 4d): compose the \xA75 plan SERVER-SIDE from the industry preset + the \xA74 brief, deterministically, in milliseconds \u2014 never author workflow actions by hand when a preset covers the brief. Pass `brief` (required) and optionally `preset` and `copy`; omit `plan`. The composed plan carries the preset's full structure (workflows, templates with complete send-ready copy token-filled from the brief, staff users, calendars with the brief's appointment lengths, pipeline stages, notification targets) and then flows through the exact same validate \u2192 dry_run/execute path as a passed plan, including being saved on execute. The dry_run response lists copySlots \u2014 the template refs whose subject/body the model MAY refine via `copy` on the execute call. If the preset cannot cover the brief the tool answers phase:\"compose\" with the reasons; only then author a plan by hand."),
51462
- brief: import_zod80.z.record(import_zod80.z.string(), import_zod80.z.unknown()).optional().describe("fromPreset only: the \xA74 Brief (from normalize_submission_to_brief, or assembled from the cockpit intake answers)."),
51463
- preset: import_zod80.z.string().optional().describe("fromPreset only: which preset to compose from \u2014 a preset id (med_spa, clinic, coach, local_service, ecom, generic), an alias (dental, medspa, home_services\u2026), or an industry slug (med-spa, local-service, ecommerce\u2026). Defaults to the brief's own preset field; an unknown value falls back to the generic preset (noted in the response)."),
51464
- copy: import_zod80.z.array(
51465
- import_zod80.z.object({
51466
- ref: import_zod80.z.string().describe("email_template.* or sms_template.* ref from the composed plan"),
51467
- subject: import_zod80.z.string().optional(),
51468
- html: import_zod80.z.string().optional(),
51469
- body: import_zod80.z.string().optional()
51610
+ plan: import_zod81.z.record(import_zod81.z.string(), import_zod81.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."),
51611
+ fromPreset: import_zod81.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."),
51612
+ brief: import_zod81.z.record(import_zod81.z.string(), import_zod81.z.unknown()).optional().describe("fromPreset only: the \xA74 Brief (from normalize_submission_to_brief, or assembled from the cockpit intake answers)."),
51613
+ preset: import_zod81.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)."),
51614
+ copy: import_zod81.z.array(
51615
+ import_zod81.z.object({
51616
+ ref: import_zod81.z.string().describe("email_template.* or sms_template.* ref from the composed plan"),
51617
+ subject: import_zod81.z.string().optional(),
51618
+ html: import_zod81.z.string().optional(),
51619
+ body: import_zod81.z.string().optional()
51470
51620
  })
51471
51621
  ).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."),
51472
- useSavedPlan: import_zod80.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."),
51473
- replaceSavedPlan: import_zod80.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."),
51474
- mode: import_zod80.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes. Run dry_run FIRST, show the operator the report, and run execute only on their go."),
51475
- locationId: import_zod80.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
51476
- metHandoffs: import_zod80.z.array(import_zod80.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.'),
51477
- publishWorkflows: import_zod80.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
51478
- onConflict: import_zod80.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
51622
+ useSavedPlan: import_zod81.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."),
51623
+ replaceSavedPlan: import_zod81.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."),
51624
+ mode: import_zod81.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes. Run dry_run FIRST, show the operator the report, and run execute only on their go."),
51625
+ locationId: import_zod81.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first)."),
51626
+ metHandoffs: import_zod81.z.array(import_zod81.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.'),
51627
+ publishWorkflows: import_zod81.z.boolean().optional().describe("If true, ungated workflows would be published instead of left DRAFT. Default false (DRAFT)."),
51628
+ onConflict: import_zod81.z.enum(["skip", "abort"]).optional().describe("skip (default) = bind same-named existing objects and continue. abort = report conflicts as a halt.")
51479
51629
  };
51480
- const buildPlanArgs = import_zod80.z.object(buildPlanSchema2);
51630
+ const buildPlanArgs = import_zod81.z.object(buildPlanSchema2);
51481
51631
  const runBuildPlan = async ({ plan: planArgIn, fromPreset, brief: briefArg, preset: presetArg, copy: copyArg, useSavedPlan, replaceSavedPlan, mode, locationId: locationId2, metHandoffs, publishWorkflows, onConflict }, persistPreview = false) => {
51482
51632
  try {
51483
51633
  const resolvedMode = mode ?? "dry_run";
@@ -51795,7 +51945,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
51795
51945
  server2.tool(
51796
51946
  "validate_build_plan",
51797
51947
  "Validate a Build Plan and its references. With preview:true, compose from a brief or resolve a plan using the same executable planner as Build, read existing account assets, and save the complete preview locally. This tool cannot execute or write to the account. Approve the saved plan before running Build.",
51798
- { ...buildPlanSchema2, mode: import_zod80.z.literal("dry_run").optional(), preview: import_zod80.z.boolean().optional().describe("Prepare and save the executable preview using account reads only.") },
51948
+ { ...buildPlanSchema2, mode: import_zod81.z.literal("dry_run").optional(), preview: import_zod81.z.boolean().optional().describe("Prepare and save the executable preview using account reads only.") },
51799
51949
  async ({ preview, ...args }) => {
51800
51950
  try {
51801
51951
  return preview ? await runBuildPlan({ ...args, mode: "dry_run", publishWorkflows: false }, true) : jsonResponse(validateBuildPlan(args.plan));
@@ -51808,9 +51958,9 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
51808
51958
  "revert_build",
51809
51959
  `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.`,
51810
51960
  {
51811
- runId: import_zod80.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.'),
51812
- confirm: import_zod80.z.literal("DELETE").optional().describe('Omit for a preview (no writes at all). Pass "DELETE" to actually remove the objects the preview listed.'),
51813
- locationId: import_zod80.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first).")
51961
+ runId: import_zod81.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.'),
51962
+ confirm: import_zod81.z.literal("DELETE").optional().describe('Omit for a preview (no writes at all). Pass "DELETE" to actually remove the objects the preview listed.'),
51963
+ locationId: import_zod81.z.string().optional().describe("Target sub-account. MUST match the active location; if it differs the tool refuses (confirm/switch first).")
51814
51964
  },
51815
51965
  async ({ runId, confirm: confirm2, locationId: locationId2 }) => {
51816
51966
  try {
@@ -51988,10 +52138,10 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
51988
52138
  "install_intake_form",
51989
52139
  `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 intake interview asks \u2014 so no cockpit answer is left without a form field; omit it for the base set. Returns {formId, formUrl, fieldMap} \u2014 formUrl is the public link to send the client; keep fieldMap. NOTE: the answers come back on a CONTACT tagged "blueprint-intake", not as a form submission (GoHighLevel reports no submissions for forms created through the API), so read the contact and assemble the Brief from its fields. 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.`,
51990
52140
  {
51991
- dryRun: import_zod80.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
51992
- formId: import_zod80.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
51993
- formName: import_zod80.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`),
51994
- industry: import_zod80.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.`)
52141
+ dryRun: import_zod81.z.boolean().optional().describe("Preview the fields/form that would be created without writing anything."),
52142
+ formId: import_zod81.z.string().optional().describe("Update this existing form in place instead of creating a new one."),
52143
+ formName: import_zod81.z.string().optional().describe(`Form name. Defaults to "${INTAKE_FORM_NAME}".`),
52144
+ industry: import_zod81.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.`)
51995
52145
  },
51996
52146
  async ({ dryRun, formId, formName, industry }) => {
51997
52147
  try {
@@ -52110,10 +52260,10 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
52110
52260
  "normalize_submission_to_brief",
52111
52261
  `Read an intake form submission and normalize it into a \xA74 Brief (briefSource:"intake_form"). ONLY works for a form whose submissions GoHighLevel actually reports, which in practice means a form built in GHL's own editor \u2014 for an intake form installed by install_intake_form it will answer "No submissions found" even after the client has filled it in, and the answers should be read from the contact tagged "blueprint-intake" instead. 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).`,
52112
52262
  {
52113
- formId: import_zod80.z.string().describe("The intake form ID (from install_intake_form)."),
52114
- submissionId: import_zod80.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
52115
- fieldMap: import_zod80.z.record(import_zod80.z.string(), import_zod80.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
52116
- preset: import_zod80.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
52263
+ formId: import_zod81.z.string().describe("The intake form ID (from install_intake_form)."),
52264
+ submissionId: import_zod81.z.string().optional().describe("Specific submission to normalize. Defaults to the most recent."),
52265
+ fieldMap: import_zod81.z.record(import_zod81.z.string(), import_zod81.z.string()).optional().describe("intakeKey -> customFieldId map from install_intake_form. Reconstructed from the form if omitted."),
52266
+ preset: import_zod81.z.string().optional().describe("Override the preset. Defaults to one derived from business_type.")
52117
52267
  },
52118
52268
  async ({ formId, submissionId, fieldMap, preset }) => {
52119
52269
  try {
@@ -52142,7 +52292,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
52142
52292
  const formFull = await formApiRequest(bc, "GET", `/${formId}?locationId=${locationId2}`);
52143
52293
  resolvedMap = buildFieldMapFromFormFields(extractFormFields(formFull));
52144
52294
  }
52145
- const presetSchema2 = import_zod80.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
52295
+ const presetSchema2 = import_zod81.z.enum(["generic", "med_spa", "clinic_launch_a2p", "coach", "ecom", "agency"]).optional();
52146
52296
  const presetParsed = presetSchema2.safeParse(preset);
52147
52297
  const brief = normalizeSubmissionToBrief({
52148
52298
  others,
@@ -52168,7 +52318,7 @@ function intakeFormMatches(expected, actual) {
52168
52318
  const keys = ["id", "tag", "type", "hiddenFieldQueryKey", "required", "label", "options", "picklistOptions"];
52169
52319
  return expected.every((field, i) => keys.every((key) => field[key] === void 0 || JSON.stringify(field[key]) === JSON.stringify(actual[i]?.[key])));
52170
52320
  }
52171
- var import_zod80, customFieldItemSchema, sleep6;
52321
+ var import_zod81, customFieldItemSchema, sleep6;
52172
52322
  var init_intake_to_build = __esm({
52173
52323
  "src/tools/intake-to-build.ts"() {
52174
52324
  "use strict";
@@ -52176,7 +52326,7 @@ var init_intake_to_build = __esm({
52176
52326
  init_workflow_repair_approval();
52177
52327
  init_build_contract();
52178
52328
  init_build_obligations();
52179
- import_zod80 = require("zod");
52329
+ import_zod81 = require("zod");
52180
52330
  init_workflow_builder_client();
52181
52331
  init_tool_helpers();
52182
52332
  init_form_builder();
@@ -52200,16 +52350,16 @@ var init_intake_to_build = __esm({
52200
52350
  init_plan_from_preset();
52201
52351
  init_executor();
52202
52352
  init_execute();
52203
- customFieldItemSchema = import_zod80.z.object({
52204
- id: import_zod80.z.string(),
52205
- name: import_zod80.z.string(),
52206
- fieldKey: import_zod80.z.string(),
52207
- dataType: import_zod80.z.string(),
52208
- model: import_zod80.z.string().optional(),
52209
- parentId: import_zod80.z.string().optional(),
52210
- position: import_zod80.z.number().optional(),
52211
- dateAdded: import_zod80.z.string().optional(),
52212
- picklistOptions: import_zod80.z.array(import_zod80.z.string()).optional()
52353
+ customFieldItemSchema = import_zod81.z.object({
52354
+ id: import_zod81.z.string(),
52355
+ name: import_zod81.z.string(),
52356
+ fieldKey: import_zod81.z.string(),
52357
+ dataType: import_zod81.z.string(),
52358
+ model: import_zod81.z.string().optional(),
52359
+ parentId: import_zod81.z.string().optional(),
52360
+ position: import_zod81.z.number().optional(),
52361
+ dateAdded: import_zod81.z.string().optional(),
52362
+ picklistOptions: import_zod81.z.array(import_zod81.z.string()).optional()
52213
52363
  }).passthrough();
52214
52364
  sleep6 = (ms) => new Promise((r) => setTimeout(r, ms));
52215
52365
  }
@@ -52594,28 +52744,34 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
52594
52744
  const outDir = str3("out") ?? (0, import_node_path8.join)((0, import_node_os2.tmpdir)(), `design-run-${brand}-${medium}-${Date.now()}`);
52595
52745
  const model = str3("model") ?? DEFAULT_MODEL;
52596
52746
  io.out(`design-run: ${medium} for ${brand} on your Claude (${model}), up to ${passes} pass(es) \u2192 ${outDir}`);
52597
- const r = await runDesign({
52598
- brand,
52599
- brief: {
52600
- medium,
52601
- content: { headline: headline3, points: points.split("|").map((s) => s.trim()).filter(Boolean), audience: str3("audience"), job: str3("job"), ctaLabel: str3("cta"), proof: str3("proof") },
52602
- options: {
52603
- ...str3("mode") && { mode: str3("mode") },
52604
- ...a.slides && { slides: Number(a.slides) },
52605
- theme: str3("theme") ?? "both",
52606
- ...conversion ? { pageMode: "conversion" } : {}
52607
- },
52608
- ...conversion ? { conversion } : {}
52609
- },
52610
- ...gate2 ? { gate: gate2 } : {},
52611
- outDir,
52612
- model,
52613
- maxPasses: passes,
52614
- headless: !a.headed,
52615
- onPass: (p) => io.out(` pass ${p.pass}: ${p.lint ? p.verdict === "PASS" ? "PASS" : `${p.verdict}${p.failed.length ? " \u2014 " + p.failed.join(" | ") : ""}` : `contract: ${p.lintProblems.join("; ")}`} ($${p.costUsd.toFixed(3)}, ${Math.round(p.ms / 1e3)}s)`),
52616
- // Two verdicts, never one: craft is measured, copy is judged.
52617
- ...str3("grade-copy") ? { gradeCopyFor: str3("grade-copy") } : {}
52618
- });
52747
+ let r;
52748
+ try {
52749
+ r = await runDesign({
52750
+ brand,
52751
+ brief: {
52752
+ medium,
52753
+ content: { headline: headline3, points: points.split("|").map((s) => s.trim()).filter(Boolean), audience: str3("audience"), job: str3("job"), ctaLabel: str3("cta"), proof: str3("proof") },
52754
+ options: {
52755
+ ...str3("mode") && { mode: str3("mode") },
52756
+ ...a.slides && { slides: Number(a.slides) },
52757
+ theme: str3("theme") ?? "both",
52758
+ ...conversion ? { pageMode: "conversion" } : {}
52759
+ },
52760
+ ...conversion ? { conversion } : {}
52761
+ },
52762
+ ...gate2 ? { gate: gate2 } : {},
52763
+ outDir,
52764
+ model,
52765
+ maxPasses: passes,
52766
+ headless: !a.headed,
52767
+ onPass: (p) => io.out(` pass ${p.pass}: ${p.lint ? p.verdict === "PASS" ? "PASS" : `${p.verdict}${p.failed.length ? " \u2014 " + p.failed.join(" | ") : ""}` : `contract: ${p.lintProblems.join("; ")}`} ($${p.costUsd.toFixed(3)}, ${Math.round(p.ms / 1e3)}s)`),
52768
+ // Two verdicts, never one: craft is measured, copy is judged.
52769
+ ...str3("grade-copy") ? { gradeCopyFor: str3("grade-copy") } : {}
52770
+ });
52771
+ } catch (error) {
52772
+ io.err(error instanceof import_zod83.z.ZodError ? designValidationMessage(error) : error instanceof Error ? error.message : "The design run could not finish. Check the design details before trying again.");
52773
+ return 2;
52774
+ }
52619
52775
  io.out(summariseRun(r));
52620
52776
  if (r.file) io.out(`file: ${r.file}`);
52621
52777
  if (r.report) io.out(`report: ${r.report}`);
@@ -52625,10 +52781,12 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
52625
52781
  }
52626
52782
  return r.ok ? 0 : 1;
52627
52783
  }
52628
- var import_node_child_process6, import_node_path8, import_node_os2, USAGE2;
52784
+ var import_zod83, import_node_child_process6, import_node_path8, import_node_os2, USAGE2;
52629
52785
  var init_cli2 = __esm({
52630
52786
  "src/command-os/design/cli.ts"() {
52631
52787
  "use strict";
52788
+ import_zod83 = require("zod");
52789
+ init_brief_validation();
52632
52790
  import_node_child_process6 = require("node:child_process");
52633
52791
  import_node_path8 = require("node:path");
52634
52792
  import_node_os2 = require("node:os");
@@ -52725,7 +52883,7 @@ init_credentials_store();
52725
52883
  init_setup_tool();
52726
52884
 
52727
52885
  // src/tools/skills.ts
52728
- var import_zod81 = require("zod");
52886
+ var import_zod82 = require("zod");
52729
52887
 
52730
52888
  // src/skill-installer.ts
52731
52889
  var import_node_crypto12 = require("node:crypto");
@@ -52861,7 +53019,7 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
52861
53019
  "install_skills",
52862
53020
  "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.",
52863
53021
  {
52864
- targetDir: import_zod81.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
53022
+ targetDir: import_zod82.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
52865
53023
  },
52866
53024
  async ({ targetDir }) => {
52867
53025
  try {