@elitedcs/ghl-mcp 3.88.2 → 3.88.4

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
@@ -446,13 +446,13 @@ function setProtectedAccount(locationId2, name, file2 = accountRulesPath()) {
446
446
  function isWriteMethod(method) {
447
447
  return WRITE_METHODS.has(method.toUpperCase());
448
448
  }
449
- function locationIdsInRequest(path44, opts = {}) {
449
+ function locationIdsInRequest(path45, opts = {}) {
450
450
  const found = /* @__PURE__ */ new Set();
451
451
  const add2 = (v) => {
452
452
  if (typeof v === "string" && v.trim()) found.add(v.trim());
453
453
  else if (Array.isArray(v)) v.forEach(add2);
454
454
  };
455
- const seg = /\/locations\/([^/?#]+)/i.exec(path44);
455
+ const seg = /\/locations\/([^/?#]+)/i.exec(path45);
456
456
  if (seg) add2(decodeURIComponent(seg[1]));
457
457
  for (const src of [opts.params, opts.body]) {
458
458
  if (!src || typeof src !== "object") continue;
@@ -479,7 +479,7 @@ function protectedRefusalFor(locationId2, rulesFile) {
479
479
  const name = rules.protected[id];
480
480
  return name ? protectedRefusalMessage(name, false) : null;
481
481
  }
482
- function assertWriteAllowed(method, path44, opts = {}) {
482
+ function assertWriteAllowed(method, path45, opts = {}) {
483
483
  if (!isWriteMethod(method)) return;
484
484
  let rules;
485
485
  try {
@@ -488,7 +488,7 @@ function assertWriteAllowed(method, path44, opts = {}) {
488
488
  throw new Error(UNREADABLE_RULES_MSG);
489
489
  }
490
490
  if (Object.keys(rules.protected).length === 0) return;
491
- for (const id of locationIdsInRequest(path44, opts)) {
491
+ for (const id of locationIdsInRequest(path45, opts)) {
492
492
  const name = rules.protected[id];
493
493
  if (name) throw new Error(protectedRefusalMessage(name, false));
494
494
  }
@@ -646,8 +646,8 @@ var init_ghl_client = __esm({
646
646
  Version: version || GHL_API_VERSION
647
647
  };
648
648
  }
649
- buildUrl(path44, params) {
650
- const url = new URL(path44, GHL_BASE_URL);
649
+ buildUrl(path45, params) {
650
+ const url = new URL(path45, GHL_BASE_URL);
651
651
  if (params) {
652
652
  for (const [key, value] of Object.entries(params)) {
653
653
  if (value !== void 0 && value !== null) {
@@ -657,19 +657,19 @@ var init_ghl_client = __esm({
657
657
  }
658
658
  return url.toString();
659
659
  }
660
- async request(method, path44, options = {}, attempt = 0) {
660
+ async request(method, path45, options = {}, attempt = 0) {
661
661
  this.signal?.throwIfAborted();
662
662
  if (inBuildRequestScope() && method === "GET" && !inAccountReadAttempt()) {
663
- return retryAccountRead(() => this.request(method, path44, options, attempt), `${method} ${path44}`);
663
+ return retryAccountRead(() => this.request(method, path45, options, attempt), `${method} ${path45}`);
664
664
  }
665
665
  if (attempt === 0) {
666
- assertWriteAllowed(method, path44, {
666
+ assertWriteAllowed(method, path45, {
667
667
  params: options.params,
668
668
  body: options.body,
669
669
  defaultLocationId: this.defaultLocationId
670
670
  });
671
671
  }
672
- const url = this.buildUrl(path44, options.params);
672
+ const url = this.buildUrl(path45, options.params);
673
673
  const headers = this.buildHeaders(options.version);
674
674
  const fetchOptions = {
675
675
  method,
@@ -690,14 +690,14 @@ var init_ghl_client = __esm({
690
690
  clearTimeout(timeout);
691
691
  this.signal?.throwIfAborted();
692
692
  if (error instanceof Error && error.name === "AbortError") {
693
- throw requestError(new Error(`Request timeout (30s): ${method} ${path44}`));
693
+ throw requestError(new Error(`Request timeout (30s): ${method} ${path45}`));
694
694
  }
695
695
  if (!inBuildRequestScope() && !options.noRetry && attempt < MAX_RETRIES) {
696
696
  const delay5 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
697
- process.stderr.write(`[ghl-mcp] Network error on ${method} ${path44}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
697
+ process.stderr.write(`[ghl-mcp] Network error on ${method} ${path45}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
698
698
  `);
699
699
  await new Promise((r) => setTimeout(r, delay5));
700
- return this.request(method, path44, options, attempt + 1);
700
+ return this.request(method, path45, options, attempt + 1);
701
701
  }
702
702
  throw requestError(error);
703
703
  } finally {
@@ -705,10 +705,10 @@ var init_ghl_client = __esm({
705
705
  }
706
706
  if (!inBuildRequestScope() && !options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
707
707
  const delay5 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
708
- process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path44}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
708
+ process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path45}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
709
709
  `);
710
710
  await new Promise((r) => setTimeout(r, delay5));
711
- return this.request(method, path44, options, attempt + 1);
711
+ return this.request(method, path45, options, attempt + 1);
712
712
  }
713
713
  if (!response.ok) {
714
714
  let errorBody = "";
@@ -717,7 +717,7 @@ var init_ghl_client = __esm({
717
717
  } catch {
718
718
  }
719
719
  throw requestError(new Error(
720
- `GHL API Error ${response.status} ${response.statusText}: ${method} ${path44}
720
+ `GHL API Error ${response.status} ${response.statusText}: ${method} ${path45}
721
721
  ${errorBody}`
722
722
  ));
723
723
  }
@@ -738,20 +738,20 @@ ${errorBody}`
738
738
  combined?.dispose();
739
739
  }
740
740
  }
741
- async get(path44, options) {
742
- return this.request("GET", path44, options);
741
+ async get(path45, options) {
742
+ return this.request("GET", path45, options);
743
743
  }
744
- async post(path44, options) {
745
- return this.request("POST", path44, options);
744
+ async post(path45, options) {
745
+ return this.request("POST", path45, options);
746
746
  }
747
- async put(path44, options) {
748
- return this.request("PUT", path44, options);
747
+ async put(path45, options) {
748
+ return this.request("PUT", path45, options);
749
749
  }
750
- async patch(path44, options) {
751
- return this.request("PATCH", path44, options);
750
+ async patch(path45, options) {
751
+ return this.request("PATCH", path45, options);
752
752
  }
753
- async delete(path44, options) {
754
- return this.request("DELETE", path44, options);
753
+ async delete(path45, options) {
754
+ return this.request("DELETE", path45, options);
755
755
  }
756
756
  /**
757
757
  * Helper: resolves locationId from args or falls back to default
@@ -3470,8 +3470,8 @@ function textResponse(text2) {
3470
3470
  content: [{ type: "text", text: text2 }]
3471
3471
  };
3472
3472
  }
3473
- function escapeRegex(str8) {
3474
- return str8.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3473
+ function escapeRegex(str9) {
3474
+ return str9.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3475
3475
  }
3476
3476
  function errorMessage(error) {
3477
3477
  return error instanceof Error ? error.message : String(error);
@@ -7109,11 +7109,11 @@ function registerEmailTools(server2, client) {
7109
7109
  function registerEmailBuilderInternalTools(server2, builderClient, publicClient) {
7110
7110
  if (!builderClient) return;
7111
7111
  const client = builderClient;
7112
- async function builderRequest2(method, path44, body) {
7113
- const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path44}`, body ? { body } : {});
7112
+ async function builderRequest2(method, path45, body) {
7113
+ const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path45}`, body ? { body } : {});
7114
7114
  if (!response.ok) {
7115
7115
  const text3 = await response.text();
7116
- throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path44}
7116
+ throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path45}
7117
7117
  ${text3}`);
7118
7118
  }
7119
7119
  const text2 = await response.text();
@@ -9273,27 +9273,27 @@ function actionNameProblem(name) {
9273
9273
  function voiceActionBodyProblem(type, p) {
9274
9274
  const gap = undefinedPath2(p);
9275
9275
  if (gap) return `${gap} is undefined. JSON would drop that key silently and GoHighLevel would see a different body than you wrote \u2014 leave the key out or give it a value.`;
9276
- const str8 = (k) => typeof p[k] === "string" && p[k].trim().length > 0;
9276
+ const str9 = (k) => typeof p[k] === "string" && p[k].trim().length > 0;
9277
9277
  const num3 = (k) => typeof p[k] === "number" && Number.isFinite(p[k]);
9278
- const needTrigger = () => !str8("triggerPrompt") ? `${type} needs triggerPrompt: when, in plain words, the agent should do this ("When the caller asks for a person").` : !str8("triggerMessage") ? `${type} needs triggerMessage: what the agent says as it does it ("Let me get someone for you").` : null;
9278
+ const needTrigger = () => !str9("triggerPrompt") ? `${type} needs triggerPrompt: when, in plain words, the agent should do this ("When the caller asks for a person").` : !str9("triggerMessage") ? `${type} needs triggerMessage: what the agent says as it does it ("Let me get someone for you").` : null;
9279
9279
  switch (type) {
9280
9280
  case "CALL_TRANSFER": {
9281
9281
  const t = needTrigger();
9282
9282
  if (t) return t;
9283
9283
  if (p.transferToType !== "number") return 'CALL_TRANSFER needs transferToType: "number" (the only type GoHighLevel supports today).';
9284
- if (!str8("transferToValue") || !TRANSFER_NUMBER.test(p.transferToValue)) return "CALL_TRANSFER needs transferToValue as a full number with country code and no punctuation, e.g. +16025550100 (GoHighLevel refuses anything else with a 422).";
9284
+ if (!str9("transferToValue") || !TRANSFER_NUMBER.test(p.transferToValue)) return "CALL_TRANSFER needs transferToValue as a full number with country code and no punctuation, e.g. +16025550100 (GoHighLevel refuses anything else with a 422).";
9285
9285
  return null;
9286
9286
  }
9287
9287
  case "SMS": {
9288
9288
  const t = needTrigger();
9289
9289
  if (t) return t;
9290
- if (!str8("messageBody")) return "SMS needs messageBody: the text the caller receives.";
9290
+ if (!str9("messageBody")) return "SMS needs messageBody: the text the caller receives.";
9291
9291
  return null;
9292
9292
  }
9293
9293
  case "DATA_EXTRACTION":
9294
9294
  case "IN_CALL_DATA_EXTRACTION": {
9295
- if (!str8("contactFieldId")) return `${type} needs contactFieldId: the id of the contact field to fill (get_custom_fields). GoHighLevel refuses an unknown id with a 400 naming it.`;
9296
- if (!str8("description")) return `${type} needs description: what to listen for, in plain words.`;
9295
+ if (!str9("contactFieldId")) return `${type} needs contactFieldId: the id of the contact field to fill (get_custom_fields). GoHighLevel refuses an unknown id with a 400 naming it.`;
9296
+ if (!str9("description")) return `${type} needs description: what to listen for, in plain words.`;
9297
9297
  const ex = p.examples;
9298
9298
  if (!Array.isArray(ex) || ex.length < 1 || ex.length > 5 || ex.some((x) => typeof x !== "string" || !x.trim())) return `${type} needs examples: 1 to 5 example values (GoHighLevel refuses an empty list with a 422).`;
9299
9299
  return null;
@@ -9301,19 +9301,19 @@ function voiceActionBodyProblem(type, p) {
9301
9301
  case "WORKFLOW_TRIGGER": {
9302
9302
  const t = needTrigger();
9303
9303
  if (t) return t;
9304
- if (!str8("workflowId")) return "WORKFLOW_TRIGGER needs workflowId. It must be PUBLISHED \u2014 GoHighLevel refuses a draft or unknown id with a 400.";
9304
+ if (!str9("workflowId")) return "WORKFLOW_TRIGGER needs workflowId. It must be PUBLISHED \u2014 GoHighLevel refuses a draft or unknown id with a 400.";
9305
9305
  return null;
9306
9306
  }
9307
9307
  case "APPOINTMENT_BOOKING": {
9308
- if (!str8("calendarId")) return "APPOINTMENT_BOOKING needs calendarId (get_calendars). GoHighLevel refuses an unknown id with a 400 naming it.";
9308
+ if (!str9("calendarId")) return "APPOINTMENT_BOOKING needs calendarId (get_calendars). GoHighLevel refuses an unknown id with a 400 naming it.";
9309
9309
  for (const k of ["daysOfOfferingDates", "slotsPerDay", "hoursBetweenSlots"]) {
9310
9310
  if (!num3(k) || p[k] < 1) return `APPOINTMENT_BOOKING needs ${k} as a positive number (how many days ahead to offer, how many slots a day, hours between slots).`;
9311
9311
  }
9312
9312
  return null;
9313
9313
  }
9314
9314
  case "KNOWLEDGE_BASE": {
9315
- if (!str8("triggerPrompt")) return 'KNOWLEDGE_BASE needs triggerPrompt: when the agent should look something up ("When the caller asks about services, hours or pricing").';
9316
- if (!str8("knowledgeBaseId")) return "KNOWLEDGE_BASE needs knowledgeBaseId from THIS sub-account (list_knowledge_bases). GoHighLevel refuses an unknown id with a 400.";
9315
+ if (!str9("triggerPrompt")) return 'KNOWLEDGE_BASE needs triggerPrompt: when the agent should look something up ("When the caller asks about services, hours or pricing").';
9316
+ if (!str9("knowledgeBaseId")) return "KNOWLEDGE_BASE needs knowledgeBaseId from THIS sub-account (list_knowledge_bases). GoHighLevel refuses an unknown id with a 400.";
9317
9317
  return null;
9318
9318
  }
9319
9319
  case "CUSTOM_ACTION": {
@@ -10913,9 +10913,440 @@ var init_workflow_builder = __esm({
10913
10913
  }
10914
10914
  });
10915
10915
 
10916
+ // src/command-os/prospect/net-guard.ts
10917
+ function ipv6Bytes(raw) {
10918
+ let s = raw.toLowerCase().replace(/^\[|\]$/g, "").replace(/%.*$/, "");
10919
+ const tail = [];
10920
+ const v4 = s.match(/:(\d{1,3}(?:\.\d{1,3}){3})$/);
10921
+ if (v4) {
10922
+ const parts = v4[1].split(".").map(Number);
10923
+ if (parts.some((n) => n > 255)) return void 0;
10924
+ tail.push(...parts);
10925
+ s = s.slice(0, s.length - v4[1].length) + "0:0";
10926
+ }
10927
+ const halves = s.split("::");
10928
+ if (halves.length > 2) return void 0;
10929
+ const groups = (h) => h === "" ? [] : h.split(":");
10930
+ const head4 = groups(halves[0]);
10931
+ const rest = halves.length === 2 ? groups(halves[1]) : [];
10932
+ const fill = halves.length === 2 ? 8 - head4.length - rest.length : 0;
10933
+ if (fill < 0 || halves.length === 1 && head4.length !== 8) return void 0;
10934
+ const all = [...head4, ...Array(fill).fill("0"), ...rest];
10935
+ if (all.length !== 8 || all.some((g) => !/^[0-9a-f]{1,4}$/.test(g))) return void 0;
10936
+ const bytes = all.flatMap((g) => {
10937
+ const n = parseInt(g, 16);
10938
+ return [n >> 8, n & 255];
10939
+ });
10940
+ if (tail.length) bytes.splice(12, 4, ...tail);
10941
+ return bytes;
10942
+ }
10943
+ function privateV4(b) {
10944
+ const [a, c, d] = b;
10945
+ return a === 0 || a === 10 || a === 100 && c >= 64 && c <= 127 || a === 127 || a === 169 && c === 254 || a === 172 && c >= 16 && c <= 31 || a === 192 && c === 0 && d === 0 || a === 192 && c === 0 && d === 2 || a === 192 && c === 88 && d === 99 || a === 192 && c === 168 || a === 198 && (c === 18 || c === 19) || a === 198 && c === 51 && d === 100 || a === 203 && c === 0 && d === 113 || a >= 224;
10946
+ }
10947
+ function isPrivateAddress(ip) {
10948
+ const bare = ip.replace(/^\[|\]$/g, "").replace(/%.*$/, "");
10949
+ const v = import_node_net.default.isIP(bare);
10950
+ if (v === 4) return privateV4(bare.split(".").map(Number));
10951
+ if (v !== 6) return true;
10952
+ const b = ipv6Bytes(bare);
10953
+ if (!b) return true;
10954
+ const zeroTo = (n) => b.slice(0, n).every((x) => x === 0);
10955
+ if (zeroTo(16)) return true;
10956
+ if (zeroTo(15) && b[15] === 1) return true;
10957
+ if (zeroTo(10) && b[10] === 255 && b[11] === 255) return privateV4(b.slice(12));
10958
+ if (zeroTo(12)) return true;
10959
+ if (zeroTo(8) && b[8] === 255 && b[9] === 255 && b[10] === 0 && b[11] === 0) return true;
10960
+ if (b[0] === 32 && b[1] === 2) return privateV4(b.slice(2, 6));
10961
+ if ((b[0] & 224) !== 32) return true;
10962
+ if (b[0] === 32 && b[1] === 1 && (b[2] & 254) === 0) return true;
10963
+ if (b[0] === 32 && b[1] === 1 && b[2] === 13 && b[3] === 184) return true;
10964
+ if (b[0] === 63 && b[1] === 255 && (b[2] & 240) === 0) return true;
10965
+ return false;
10966
+ }
10967
+ function withDeadline(p, ms, why, signal) {
10968
+ return new Promise((resolve13, reject) => {
10969
+ if (signal?.aborted) {
10970
+ p.catch(() => void 0);
10971
+ reject(new Error("aborted"));
10972
+ return;
10973
+ }
10974
+ const t = setTimeout(() => reject(new Error(why)), ms);
10975
+ const onAbort = () => {
10976
+ clearTimeout(t);
10977
+ reject(new Error("aborted"));
10978
+ };
10979
+ signal?.addEventListener("abort", onAbort, { once: true });
10980
+ p.then((v) => {
10981
+ clearTimeout(t);
10982
+ signal?.removeEventListener("abort", onAbort);
10983
+ resolve13(v);
10984
+ }, (e) => {
10985
+ clearTimeout(t);
10986
+ signal?.removeEventListener("abort", onAbort);
10987
+ reject(e);
10988
+ });
10989
+ });
10990
+ }
10991
+ async function publicAddressProblem(raw, lookup = import_node_dns.default.promises.lookup) {
10992
+ let u;
10993
+ try {
10994
+ u = new URL(raw);
10995
+ } catch {
10996
+ return "not a web address";
10997
+ }
10998
+ if (u.protocol !== "http:" && u.protocol !== "https:") return `only web addresses are opened (this one is ${u.protocol})`;
10999
+ if (u.username || u.password) return "an address carrying a login is not opened";
11000
+ const host = u.hostname.replace(/^\[|\]$/g, "");
11001
+ if (LOCAL_NAME.test(host)) return "a local or private address is never opened";
11002
+ if (import_node_net.default.isIP(host)) return isPrivateAddress(host) ? "a local or private address is never opened" : void 0;
11003
+ try {
11004
+ const all = await withDeadline(lookup(host, { all: true, verbatim: true }), 8e3, "the address lookup took too long");
11005
+ if (!all.length) return "the address does not resolve";
11006
+ if (all.some((a) => isPrivateAddress(a.address))) return "the address resolves to a local or private network, so it is never opened";
11007
+ } catch {
11008
+ return "the address does not resolve";
11009
+ }
11010
+ return void 0;
11011
+ }
11012
+ function publicOnlyLookup(hostname2, options, callback) {
11013
+ import_node_dns.default.lookup(hostname2, { ...options, all: true, verbatim: true }, (err, addresses) => {
11014
+ if (err) return callback(err, "", 0);
11015
+ const list3 = addresses;
11016
+ const bad = list3.find((a) => isPrivateAddress(a.address));
11017
+ if (!list3.length || bad) {
11018
+ const e = new Error("the address resolves to a local or private network, so it is never opened");
11019
+ e.code = "EPRIVATE";
11020
+ return callback(e, "", 0);
11021
+ }
11022
+ if (options.all) return callback(null, list3);
11023
+ return callback(null, list3[0].address, list3[0].family);
11024
+ });
11025
+ }
11026
+ function transferBudget(totalBytes = 25e7) {
11027
+ const opts = { keepAlive: true, maxSockets: 8, lookup: publicOnlyLookup };
11028
+ return { bytesLeft: totalBytes, agents: { http: new import_node_http.default.Agent(opts), https: new import_node_https.default.Agent(opts) } };
11029
+ }
11030
+ function closeTransferBudget(b) {
11031
+ b.agents.http.destroy();
11032
+ b.agents.https.destroy();
11033
+ }
11034
+ function guardedTransfer(url, init) {
11035
+ return new Promise((resolve13, reject) => {
11036
+ let u;
11037
+ try {
11038
+ u = new URL(url);
11039
+ } catch {
11040
+ reject(new Error("not a web address"));
11041
+ return;
11042
+ }
11043
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
11044
+ reject(new Error("only web addresses are opened"));
11045
+ return;
11046
+ }
11047
+ if (u.username || u.password) {
11048
+ reject(new Error("an address carrying a login is not opened"));
11049
+ return;
11050
+ }
11051
+ if (refusedHost(u.hostname)) {
11052
+ reject(new Error("a local or private address is never opened"));
11053
+ return;
11054
+ }
11055
+ let settled = false;
11056
+ let cleanup = () => void 0;
11057
+ const settle2 = (f) => {
11058
+ if (settled) return;
11059
+ settled = true;
11060
+ init.signal.removeEventListener("abort", onAbort);
11061
+ cleanup();
11062
+ f();
11063
+ };
11064
+ const onAbort = () => settle2(() => {
11065
+ req.destroy();
11066
+ reject(new Error("aborted"));
11067
+ });
11068
+ if (init.signal.aborted) {
11069
+ reject(new Error("aborted"));
11070
+ return;
11071
+ }
11072
+ init.signal.addEventListener("abort", onAbort, { once: true });
11073
+ const isHttps = u.protocol === "https:";
11074
+ const headers = {};
11075
+ for (const [k, v] of Object.entries(init.headers)) if (!/^(?:host|connection|keep-alive|proxy-.*|transfer-encoding|upgrade|te|trailer|content-length|:.*)$/i.test(k)) headers[k] = v;
11076
+ headers["accept-encoding"] = "gzip, deflate, br";
11077
+ const req = (isHttps ? import_node_https.default : import_node_http.default).request(u, { method: init.method, headers, agent: isHttps ? init.budget.agents.https : init.budget.agents.http, lookup: publicOnlyLookup }, (res) => {
11078
+ const out = [];
11079
+ for (let i = 0; i < res.rawHeaders.length; i += 2) out.push([res.rawHeaders[i], res.rawHeaders[i + 1]]);
11080
+ const enc = (res.headers["content-encoding"] ?? "").toString().toLowerCase();
11081
+ const unzip = enc.includes("br") ? import_node_zlib.default.createBrotliDecompress() : enc.includes("gzip") ? import_node_zlib.default.createGunzip() : enc.includes("deflate") ? import_node_zlib.default.createInflate() : void 0;
11082
+ const stream = unzip ? res.pipe(unzip) : res;
11083
+ cleanup = () => {
11084
+ unzip?.destroy();
11085
+ res.destroy();
11086
+ };
11087
+ const kept = unzip ? out.filter(([k]) => !/^(?:content-encoding|content-length)$/i.test(k)) : out;
11088
+ const chunks = [];
11089
+ let size = 0;
11090
+ const finish3 = (incomplete) => settle2(() => resolve13({ status: res.statusCode ?? 0, headers: kept, body: Buffer.concat(chunks), incomplete }));
11091
+ stream.on("data", (c) => {
11092
+ size += c.length;
11093
+ init.budget.bytesLeft -= c.length;
11094
+ if (size > init.maxBytes) finish3(`the file is larger than ${Math.round(init.maxBytes / 1e6)} MB, so it was not loaded`);
11095
+ else if (init.budget.bytesLeft < 0) finish3("the run's limit on how much it downloads was reached");
11096
+ else chunks.push(c);
11097
+ });
11098
+ stream.on("end", () => finish3(res.complete ? void 0 : "the file did not arrive whole"));
11099
+ stream.on("error", () => finish3("the file did not arrive whole"));
11100
+ res.on("aborted", () => finish3("the file did not arrive whole"));
11101
+ res.on("error", () => finish3("the file did not arrive whole"));
11102
+ res.on("close", () => {
11103
+ if (!res.complete) finish3("the file did not arrive whole");
11104
+ else if (!unzip) finish3();
11105
+ });
11106
+ if (unzip) unzip.on("close", () => finish3(res.complete ? void 0 : "the file did not arrive whole"));
11107
+ });
11108
+ req.setTimeout(3e4, () => req.destroy(new Error("no answer within 30 seconds")));
11109
+ req.on("error", (e) => settle2(() => reject(e)));
11110
+ req.end();
11111
+ });
11112
+ }
11113
+ async function guardedFetchLike(url, init = {}) {
11114
+ const budget = transferBudget(6e7);
11115
+ try {
11116
+ let cur = url;
11117
+ const signal = init.signal ?? AbortSignal.timeout(2e4);
11118
+ for (let hop = 0; hop <= 5; hop++) {
11119
+ if (signal.aborted) throw new Error("aborted");
11120
+ const problem = await withDeadline(publicAddressProblem(cur), 9e3, "the address lookup took too long", signal);
11121
+ if (problem && problem !== "the address does not resolve") throw new Error(problem);
11122
+ const r = await guardedTransfer(cur, { method: "GET", headers: init.headers ?? {}, signal, maxBytes: init.statusOnly ? 65536 : Math.min(init.maxBytes ?? 15e6, 6e7), budget });
11123
+ const cutBySize = init.statusOnly && r.incomplete?.startsWith("the file is larger than");
11124
+ if (r.incomplete && !cutBySize) throw new Error(r.incomplete);
11125
+ const loc = r.headers.find(([k]) => k.toLowerCase() === "location")?.[1];
11126
+ if (r.status >= 300 && r.status < 400 && loc && init.redirect !== "manual") {
11127
+ cur = new URL(loc, cur).toString();
11128
+ continue;
11129
+ }
11130
+ const body = r.body;
11131
+ const get = (n) => r.headers.find(([k]) => k.toLowerCase() === n.toLowerCase())?.[1] ?? null;
11132
+ return { ok: r.status >= 200 && r.status < 300, status: r.status, url: cur, text: async () => body.toString("utf8"), arrayBuffer: async () => body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength), headers: { get } };
11133
+ }
11134
+ throw new Error("more than 5 redirects");
11135
+ } finally {
11136
+ closeTransferBudget(budget);
11137
+ }
11138
+ }
11139
+ async function guardedPost(url, body, headers, timeoutMs = 2e4) {
11140
+ const started = Date.now();
11141
+ const problem = await withDeadline(publicAddressProblem(url), timeoutMs, `no answer within ${Math.round(timeoutMs / 1e3)} seconds`);
11142
+ if (problem && problem !== "the address does not resolve") throw new Error(problem);
11143
+ const left = Math.max(1, timeoutMs - (Date.now() - started));
11144
+ const u = new URL(url);
11145
+ const mod = u.protocol === "https:" ? import_node_https.default : import_node_http.default;
11146
+ return new Promise((resolve13, reject) => {
11147
+ let settled = false;
11148
+ const chunks = [];
11149
+ let size = 0;
11150
+ const settle2 = (f) => {
11151
+ if (settled) return;
11152
+ settled = true;
11153
+ clearTimeout(timer);
11154
+ f();
11155
+ };
11156
+ const req = mod.request(u, { method: "POST", headers: { ...headers, "content-length": String(Buffer.byteLength(body)) }, lookup: publicOnlyLookup }, (res) => {
11157
+ const done = () => settle2(() => resolve13({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") }));
11158
+ res.on("data", (c) => {
11159
+ size += c.length;
11160
+ if (size <= 1e6) chunks.push(c);
11161
+ else {
11162
+ res.destroy();
11163
+ done();
11164
+ }
11165
+ });
11166
+ res.on("end", done);
11167
+ res.on("error", done);
11168
+ res.on("close", done);
11169
+ });
11170
+ const timer = setTimeout(() => settle2(() => {
11171
+ req.destroy();
11172
+ reject(new Error(`no answer within ${Math.round(timeoutMs / 1e3)} seconds`));
11173
+ }), left);
11174
+ req.on("error", (e) => settle2(() => reject(e)));
11175
+ req.end(body);
11176
+ });
11177
+ }
11178
+ function refusedHost(host) {
11179
+ const bare = host.replace(/^\[|\]$/g, "");
11180
+ if (!bare || LOCAL_NAME.test(bare)) return true;
11181
+ if (import_node_net.default.isIP(bare.replace(/%.*$/, ""))) return isPrivateAddress(bare);
11182
+ return /^(?:0x[0-9a-f]*|\d+)(?:\.(?:0x[0-9a-f]*|\d+)){0,3}\.?$/i.test(bare) || bare.includes(":");
11183
+ }
11184
+ async function startGuardProxy(opts = {}) {
11185
+ const max = opts.maxConnections ?? 8e3;
11186
+ const maxOpen = opts.maxOpen ?? 256;
11187
+ const idleMs = opts.idleMs ?? 3e4;
11188
+ const username = "shopper";
11189
+ const password = (0, import_node_crypto3.randomBytes)(18).toString("hex");
11190
+ const expected = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
11191
+ const stats = { connections: 0, refused: 0, open: 0 };
11192
+ const sockets = /* @__PURE__ */ new Set();
11193
+ const track = (s) => {
11194
+ sockets.add(s);
11195
+ s.once("close", () => sockets.delete(s));
11196
+ };
11197
+ const authorised = (h) => {
11198
+ const got = Buffer.from(String(h ?? ""));
11199
+ const want = Buffer.from(expected);
11200
+ return got.length === want.length && (0, import_node_crypto3.timingSafeEqual)(got, want);
11201
+ };
11202
+ const server2 = import_node_http.default.createServer((req, res) => {
11203
+ if (!authorised(req.headers["proxy-authorization"])) {
11204
+ stats.refused++;
11205
+ res.writeHead(407, { "proxy-authenticate": 'Basic realm="shopper"' }).end();
11206
+ return;
11207
+ }
11208
+ let u;
11209
+ try {
11210
+ u = new URL(req.url ?? "");
11211
+ } catch {
11212
+ res.writeHead(400).end();
11213
+ return;
11214
+ }
11215
+ if (u.protocol !== "http:" || !["GET", "HEAD"].includes(req.method ?? "") || req.headers.upgrade) {
11216
+ stats.refused++;
11217
+ res.writeHead(405).end();
11218
+ return;
11219
+ }
11220
+ if (++stats.connections > max) {
11221
+ stats.refused++;
11222
+ res.writeHead(429).end();
11223
+ return;
11224
+ }
11225
+ const port2 = Number(u.port || 80);
11226
+ if (!WEB_PORTS.has(port2) || refusedHost(u.hostname)) {
11227
+ stats.refused++;
11228
+ res.writeHead(403).end();
11229
+ return;
11230
+ }
11231
+ const headers = { ...req.headers };
11232
+ delete headers["proxy-connection"];
11233
+ delete headers["proxy-authorization"];
11234
+ const out = import_node_http.default.request({ host: u.hostname.replace(/^\[|\]$/g, ""), port: port2, path: u.pathname + u.search, method: req.method, headers, lookup: publicOnlyLookup, timeout: idleMs }, (up) => {
11235
+ res.writeHead(up.statusCode ?? 502, up.headers);
11236
+ up.pipe(res);
11237
+ });
11238
+ out.on("socket", (s) => track(s));
11239
+ out.on("error", () => {
11240
+ stats.refused++;
11241
+ if (!res.headersSent) res.writeHead(502);
11242
+ res.end();
11243
+ });
11244
+ out.on("timeout", () => out.destroy());
11245
+ out.end();
11246
+ });
11247
+ server2.on("connection", (s) => {
11248
+ s.on("error", () => void 0);
11249
+ if (sockets.size >= maxOpen) {
11250
+ stats.refused++;
11251
+ s.destroy();
11252
+ return;
11253
+ }
11254
+ track(s);
11255
+ stats.open = Math.max(stats.open, sockets.size);
11256
+ s.setTimeout(idleMs, () => s.destroy());
11257
+ });
11258
+ server2.headersTimeout = Math.min(idleMs, 1e4);
11259
+ server2.requestTimeout = idleMs;
11260
+ server2.on("connect", (req, client, head4) => {
11261
+ client.on("error", () => void 0);
11262
+ if (!authorised(req.headers["proxy-authorization"])) {
11263
+ stats.refused++;
11264
+ client.end('HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="shopper"\r\nContent-Length: 0\r\n\r\n');
11265
+ return;
11266
+ }
11267
+ const [host, portStr] = (req.url ?? "").split(/:(?=\d+$)/);
11268
+ const port2 = Number(portStr || 443);
11269
+ if (++stats.connections > max || !WEB_PORTS.has(port2) || refusedHost(host ?? "")) {
11270
+ stats.refused++;
11271
+ client.end("HTTP/1.1 403 Forbidden\r\n\r\n");
11272
+ return;
11273
+ }
11274
+ const up = import_node_net.default.connect({ host: (host ?? "").replace(/^\[|\]$/g, ""), port: port2, lookup: publicOnlyLookup, timeout: idleMs }, () => {
11275
+ client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
11276
+ if (head4.length) up.write(head4);
11277
+ up.pipe(client);
11278
+ client.pipe(up);
11279
+ });
11280
+ track(up);
11281
+ up.on("error", () => {
11282
+ stats.refused++;
11283
+ client.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
11284
+ });
11285
+ up.on("timeout", () => {
11286
+ up.destroy();
11287
+ client.destroy();
11288
+ });
11289
+ client.on("error", () => up.destroy());
11290
+ client.on("close", () => up.destroy());
11291
+ });
11292
+ server2.on("clientError", (_e, s) => s.destroy());
11293
+ await new Promise((resolve13, reject) => {
11294
+ const fail = (e) => {
11295
+ server2.removeListener("listening", ok);
11296
+ reject(e);
11297
+ };
11298
+ const ok = () => {
11299
+ server2.removeListener("error", fail);
11300
+ resolve13();
11301
+ };
11302
+ server2.once("error", fail);
11303
+ server2.once("listening", ok);
11304
+ try {
11305
+ server2.listen(0, "127.0.0.1");
11306
+ } catch (e) {
11307
+ server2.removeListener("error", fail);
11308
+ server2.removeListener("listening", ok);
11309
+ reject(e);
11310
+ }
11311
+ });
11312
+ server2.on("error", () => void 0);
11313
+ const port = server2.address().port;
11314
+ return {
11315
+ url: `http://127.0.0.1:${port}`,
11316
+ username,
11317
+ password,
11318
+ stats,
11319
+ // Every socket is destroyed and the close never waits more than two seconds.
11320
+ close: () => new Promise((r) => {
11321
+ const t = setTimeout(r, 2e3);
11322
+ server2.close(() => {
11323
+ clearTimeout(t);
11324
+ r();
11325
+ });
11326
+ for (const s of sockets) s.destroy();
11327
+ server2.closeAllConnections?.();
11328
+ })
11329
+ };
11330
+ }
11331
+ var import_node_dns, import_node_http, import_node_https, import_node_net, import_node_zlib, import_node_crypto3, LOCAL_NAME, WEB_PORTS;
11332
+ var init_net_guard = __esm({
11333
+ "src/command-os/prospect/net-guard.ts"() {
11334
+ "use strict";
11335
+ import_node_dns = __toESM(require("node:dns"));
11336
+ import_node_http = __toESM(require("node:http"));
11337
+ import_node_https = __toESM(require("node:https"));
11338
+ import_node_net = __toESM(require("node:net"));
11339
+ import_node_zlib = __toESM(require("node:zlib"));
11340
+ import_node_crypto3 = require("node:crypto");
11341
+ LOCAL_NAME = /^(?:localhost|localhost\.localdomain|ip6-localhost|ip6-loopback)\.?$|\.(?:localhost|local|internal|lan|home|corp|intranet|localdomain|home\.arpa)\.?$/i;
11342
+ WEB_PORTS = /* @__PURE__ */ new Set([80, 443, 8080, 8443]);
11343
+ }
11344
+ });
11345
+
10916
11346
  // src/page-reads.ts
10917
11347
  var page_reads_exports = {};
10918
11348
  __export(page_reads_exports, {
11349
+ pageReadsNet: () => pageReadsNet,
10919
11350
  readPageContent: () => readPageContent,
10920
11351
  readPageFull: () => readPageFull
10921
11352
  });
@@ -10923,11 +11354,11 @@ async function readPageFull(client, pageId, fetcher = fetch) {
10923
11354
  const headers = await client.buildHeaders();
10924
11355
  headers.Origin = "https://app.gohighlevel.com";
10925
11356
  headers.Referer = "https://app.gohighlevel.com/";
10926
- const path44 = `/page/${pageId}?locationId=${client.locationId}`;
10927
- const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path44}`, { method: "GET", headers });
11357
+ const path45 = `/page/${pageId}?locationId=${client.locationId}`;
11358
+ const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path45}`, { method: "GET", headers });
10928
11359
  if (!response.ok) {
10929
11360
  const text3 = await response.text();
10930
- throw new Error(`Funnel API Error ${response.status}: GET ${path44}
11361
+ throw new Error(`Funnel API Error ${response.status}: GET ${path45}
10931
11362
  ${text3}`);
10932
11363
  }
10933
11364
  const text2 = await response.text();
@@ -10938,20 +11369,24 @@ ${text3}`);
10938
11369
  return JSON.parse(text2.replace(/[\x00-\x1F\x7F]/g, ""));
10939
11370
  }
10940
11371
  }
10941
- async function readPageContent(pageDataDownloadUrl, fetcher = fetch) {
11372
+ async function readPageContent(pageDataDownloadUrl, fetcher = (url) => pageReadsNet.read(url)) {
10942
11373
  const parsedUrl = new URL(pageDataDownloadUrl);
10943
11374
  const allowedHosts = ["firebasestorage.googleapis.com", "storage.googleapis.com", "backend.leadconnectorhq.com"];
10944
- if (!allowedHosts.some((h) => parsedUrl.hostname === h || parsedUrl.hostname.endsWith("." + h))) {
10945
- throw new Error("URL must be from GHL storage (firebasestorage.googleapis.com). Got: " + parsedUrl.hostname);
11375
+ if (parsedUrl.protocol !== "https:" || !allowedHosts.some((h) => parsedUrl.hostname === h || parsedUrl.hostname.endsWith("." + h))) {
11376
+ throw new Error("URL must be an https address from GHL storage (firebasestorage.googleapis.com). Got: " + parsedUrl.protocol + "//" + parsedUrl.hostname);
10946
11377
  }
10947
11378
  const response = await fetcher(pageDataDownloadUrl);
10948
11379
  if (!response.ok) throw new Error(`Failed to download page data: ${response.status}`);
10949
- const data = await response.json();
10950
- return data;
11380
+ return JSON.parse(await response.text());
10951
11381
  }
11382
+ var pageReadsNet;
10952
11383
  var init_page_reads = __esm({
10953
11384
  "src/page-reads.ts"() {
10954
11385
  "use strict";
11386
+ init_net_guard();
11387
+ pageReadsNet = {
11388
+ read: (url) => guardedFetchLike(url, { signal: AbortSignal.timeout(3e4), maxBytes: 5e7 })
11389
+ };
10955
11390
  }
10956
11391
  });
10957
11392
 
@@ -10986,20 +11421,20 @@ function registerFunnelBuilderTools(server2, builderClient) {
10986
11421
  return;
10987
11422
  }
10988
11423
  const client = builderClient;
10989
- async function internalGet(path44) {
10990
- return client.request("GET", path44);
11424
+ async function internalGet(path45) {
11425
+ return client.request("GET", path45);
10991
11426
  }
10992
- async function internalPost(path44, body) {
10993
- return client.request("POST", path44, body);
11427
+ async function internalPost(path45, body) {
11428
+ return client.request("POST", path45, body);
10994
11429
  }
10995
- async function internalPut(path44, body) {
10996
- return client.request("PUT", path44, body);
11430
+ async function internalPut(path45, body) {
11431
+ return client.request("PUT", path45, body);
10997
11432
  }
10998
- async function internalDelete(path44) {
10999
- return client.request("DELETE", path44);
11433
+ async function internalDelete(path45) {
11434
+ return client.request("DELETE", path45);
11000
11435
  }
11001
- async function funnelRequest(method, path44, body) {
11002
- const url = `https://backend.leadconnectorhq.com/funnels${path44}`;
11436
+ async function funnelRequest(method, path45, body) {
11437
+ const url = `https://backend.leadconnectorhq.com/funnels${path45}`;
11003
11438
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
11004
11439
  const response = await builderRequest(client, method, url, {
11005
11440
  ...sendsBody ? { body } : {},
@@ -11012,7 +11447,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
11012
11447
  });
11013
11448
  if (!response.ok) {
11014
11449
  const text3 = await response.text();
11015
- throw new Error(`Funnel API Error ${response.status}: ${method} ${path44}
11450
+ throw new Error(`Funnel API Error ${response.status}: ${method} ${path45}
11016
11451
  ${text3}`);
11017
11452
  }
11018
11453
  const text2 = await response.text();
@@ -11603,17 +12038,17 @@ function requireCompany(api) {
11603
12038
  }
11604
12039
  return company;
11605
12040
  }
11606
- async function call(api, path44, init) {
12041
+ async function call(api, path45, init) {
11607
12042
  const headers = { ...await api.headers(), Origin: "https://app.gohighlevel.com", Referer: "https://app.gohighlevel.com/" };
11608
- if (MUTATING_PATHS.some((mutating) => path44.startsWith(mutating))) {
11609
- assertWriteAllowed("POST", SVC + path44, { defaultLocationId: api.locationId() });
12043
+ if (MUTATING_PATHS.some((mutating) => path45.startsWith(mutating))) {
12044
+ assertWriteAllowed("POST", SVC + path45, { defaultLocationId: api.locationId() });
11610
12045
  }
11611
12046
  const doFetch = api.fetch ?? fetch;
11612
- const response = await doFetch(SVC + path44, { ...init, headers });
12047
+ const response = await doFetch(SVC + path45, { ...init, headers });
11613
12048
  const text2 = await response.text();
11614
12049
  if (!response.ok) {
11615
12050
  const hint = response.status === 401 && /not authorized to access this company/i.test(text2) ? " \u2014 the Firebase session is authenticated as a DIFFERENT company than the one that owns this sub-account. Run switch_location for this sub-account and try again." : response.status === 401 ? " \u2014 the template library needs the workflow-builder Firebase session, not the API key alone. Run enable_workflow_builder for this sub-account." : "";
11616
- throw new Error(`GoHighLevel template library ${response.status} on ${path44}${hint}
12051
+ throw new Error(`GoHighLevel template library ${response.status} on ${path45}${hint}
11617
12052
  ${text2.slice(0, 300)}`);
11618
12053
  }
11619
12054
  return text2 ? JSON.parse(text2) : {};
@@ -11792,7 +12227,7 @@ async function confirmPreviewUrl(pageId, timeoutMs = 2e4) {
11792
12227
  const controller = new AbortController();
11793
12228
  const timer = setTimeout(() => controller.abort(), timeoutMs);
11794
12229
  try {
11795
- const response = await fetch(url, { redirect: "follow", signal: controller.signal, headers: { Accept: "text/html,*/*" } });
12230
+ const response = await guardedFetchLike(url, { signal: controller.signal, headers: { Accept: "text/html,*/*" } });
11796
12231
  const body = await response.text();
11797
12232
  const bytes = body.length;
11798
12233
  const reachable = response.ok && bytes > 0;
@@ -11896,6 +12331,7 @@ var init_ghl_page = __esm({
11896
12331
  "src/command-os/design/ghl-page.ts"() {
11897
12332
  "use strict";
11898
12333
  init_theme();
12334
+ init_net_guard();
11899
12335
  GHL_PREVIEW_HOST = "https://sites.leadconnectorhq.com";
11900
12336
  SLOT_LABEL = {
11901
12337
  primary: { id: "E1", label: "brand colour" },
@@ -11970,12 +12406,12 @@ function normaliseColour(value) {
11970
12406
  function renderedPalette(envelope) {
11971
12407
  const defs = pageVariables(envelope);
11972
12408
  const paints = [];
11973
- const walk = (node, path44, prop) => {
11974
- if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${path44}[${i}]`, prop));
12409
+ const walk = (node, path45, prop) => {
12410
+ if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${path45}[${i}]`, prop));
11975
12411
  if (isRecord8(node)) {
11976
12412
  for (const [k, x] of Object.entries(node)) {
11977
12413
  if (DEFINITION_KEYS.has(k) || k.startsWith("--")) continue;
11978
- walk(x, `${path44}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
12414
+ walk(x, `${path45}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
11979
12415
  }
11980
12416
  return;
11981
12417
  }
@@ -11985,7 +12421,7 @@ function renderedPalette(envelope) {
11985
12421
  const resolved2 = resolveValue(raw, defs);
11986
12422
  const colour = normaliseColour(resolved2);
11987
12423
  if (colour === null && resolved2 !== null) return;
11988
- paints.push({ path: path44, prop, kind: kindOf(prop), colour });
12424
+ paints.push({ path: path45, prop, kind: kindOf(prop), colour });
11989
12425
  };
11990
12426
  walk(envelope, "", "");
11991
12427
  const face = (name, settingsKey) => {
@@ -12080,7 +12516,7 @@ var init_rendered_palette = __esm({
12080
12516
  VISIBLE_CHANGE = 0.05;
12081
12517
  BARELY_SHARE = 0.5;
12082
12518
  CARRIES_COLOUR = 0.02;
12083
- paintGroup = (path44) => /^\.(sections|popups)\[\d+\]/.exec(path44)?.[0]?.slice(1) ?? "page";
12519
+ paintGroup = (path45) => /^\.(sections|popups)\[\d+\]/.exec(path45)?.[0]?.slice(1) ?? "page";
12084
12520
  looksDifferent = (a, b) => {
12085
12521
  if (a === b) return false;
12086
12522
  if (a === "transparent" || b === "transparent" || a.startsWith("rgba") || b.startsWith("rgba")) return a !== b;
@@ -15348,7 +15784,7 @@ function colorsIn(src) {
15348
15784
  return [...hex, ...rgb, ...hsl];
15349
15785
  }
15350
15786
  async function extractStatic(url, opts = {}) {
15351
- const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
15787
+ const fetchImpl = opts.fetchImpl ?? guardedFetchLike;
15352
15788
  const warnings = [];
15353
15789
  const page = await fetchText(url, fetchImpl);
15354
15790
  if (!page.ok || !page.text) throw new Error(`Could not fetch ${url} (status ${page.status}). Nothing was extracted.`);
@@ -15777,6 +16213,7 @@ var init_extract = __esm({
15777
16213
  init_schema();
15778
16214
  init_store();
15779
16215
  init_svg_sanitize();
16216
+ init_net_guard();
15780
16217
  UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36 GHLCommand-DesignStudio/0.1";
15781
16218
  rank2 = (arr) => {
15782
16219
  const m = /* @__PURE__ */ new Map();
@@ -16655,9 +17092,9 @@ function removalHoldBack(opts) {
16655
17092
  if (opts.multiPageStepNames.length > 0) return `${opts.multiPageStepNames.length} step(s) carry more than one page (${opts.multiPageStepNames.join(", ")}) and only the first of each was rebuilt`;
16656
17093
  return null;
16657
17094
  }
16658
- function brandedPath(path44, name, taken) {
17095
+ function brandedPath(path45, name, taken) {
16659
17096
  const normalize2 = (s) => s.replace(/^\/+|\/+$/g, "").toLowerCase();
16660
- const base = normalize2(path44 ?? "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
17097
+ const base = normalize2(path45 ?? "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
16661
17098
  const used = new Set([...taken].map(normalize2));
16662
17099
  let candidate = `${base}-branded`;
16663
17100
  for (let n = 2; used.has(candidate); n++) candidate = `${base}-branded-${n}`;
@@ -16685,9 +17122,9 @@ function rewriteTemplateLinks(envelope, destinations) {
16685
17122
  representation = "page";
16686
17123
  matches = destinations.filter((d2) => d2.pageId === preview[2]);
16687
17124
  } else if (!/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(from) && !from.startsWith("#")) {
16688
- const [path44] = from.split(/[?#]/);
16689
- suffix = from.slice(path44.length);
16690
- matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(path44));
17125
+ const [path45] = from.split(/[?#]/);
17126
+ suffix = from.slice(path45.length);
17127
+ matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(path45));
16691
17128
  }
16692
17129
  }
16693
17130
  if (matches.length !== 1) {
@@ -16770,8 +17207,8 @@ function registerPageStudioTools(server2, builderClient) {
16770
17207
  }
16771
17208
  return { map: out, truncated };
16772
17209
  }
16773
- async function funnelRequest(method, path44, body) {
16774
- const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path44}`, {
17210
+ async function funnelRequest(method, path45, body) {
17211
+ const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path45}`, {
16775
17212
  ...body ? { body } : {},
16776
17213
  headers: {
16777
17214
  // Internal-API calls require the app origin (autosave needs the
@@ -16780,7 +17217,7 @@ function registerPageStudioTools(server2, builderClient) {
16780
17217
  Referer: "https://app.gohighlevel.com/"
16781
17218
  }
16782
17219
  });
16783
- if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path44}
17220
+ if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path45}
16784
17221
  ${await response.text()}`);
16785
17222
  const text2 = await response.text();
16786
17223
  return text2 ? JSON.parse(text2) : {};
@@ -17018,9 +17455,9 @@ ${await response.text()}`);
17018
17455
  if (!step.pageId) throw new Error("this step has no page to rebuild");
17019
17456
  const meta = await funnelRequest("GET", `/page/${step.pageId}?locationId=${client.locationId}`);
17020
17457
  if (typeof meta.pageDataDownloadUrl !== "string" || !meta.pageDataDownloadUrl) throw new Error("GoHighLevel has no saved content for this page yet");
17021
- const response = await fetch(meta.pageDataDownloadUrl);
17458
+ const response = await pageStudioNet.read(meta.pageDataDownloadUrl, { signal: AbortSignal.timeout(3e4), maxBytes: 5e7 });
17022
17459
  if (!response.ok) throw new Error(`page content read failed (${response.status})`);
17023
- loaded.push({ ...step, envelope: await response.json() });
17460
+ loaded.push({ ...step, envelope: JSON.parse(await response.text()) });
17024
17461
  } catch (e) {
17025
17462
  throw new Error(`${step.name}: ${e instanceof Error ? e.message : String(e)}`);
17026
17463
  }
@@ -17029,7 +17466,8 @@ ${await response.text()}`);
17029
17466
  const controller = new AbortController();
17030
17467
  const timer = setTimeout(() => controller.abort(), 3e4);
17031
17468
  try {
17032
- return await fetch(url, { signal: controller.signal, headers: { "Cache-Control": "no-cache" } });
17469
+ const r = await pageStudioNet.read(url, { signal: controller.signal, headers: { "Cache-Control": "no-cache" }, maxBytes: 5e7 });
17470
+ return { ok: r.ok, status: r.status, text: () => r.text(), json: async () => JSON.parse(await r.text()) };
17033
17471
  } finally {
17034
17472
  clearTimeout(timer);
17035
17473
  }
@@ -17126,9 +17564,9 @@ ${await response.text()}`);
17126
17564
  await new Promise((r) => setTimeout(r, 1200));
17127
17565
  }
17128
17566
  if (typeof after.pageDataDownloadUrl !== "string" || !after.pageDataDownloadUrl) throw new Error("the fresh page saved but GoHighLevel is not serving it");
17129
- const response = await fetch(after.pageDataDownloadUrl);
17567
+ const response = await pageStudioNet.read(after.pageDataDownloadUrl, { signal: AbortSignal.timeout(3e4), maxBytes: 5e7 });
17130
17568
  if (!response.ok) throw new Error(`saved content read failed (${response.status})`);
17131
- const saved = await response.json();
17569
+ const saved = JSON.parse(await response.text());
17132
17570
  storedByPage.set(step.newPageId, saved);
17133
17571
  const savedText = JSON.stringify(saved).toLowerCase();
17134
17572
  if (formId && forms.length) {
@@ -17545,7 +17983,7 @@ ${await response.text()}`);
17545
17983
  }
17546
17984
  );
17547
17985
  }
17548
- var import_zod43, crypto4, valueCardSchema;
17986
+ var import_zod43, crypto4, pageStudioNet, valueCardSchema;
17549
17987
  var init_page_studio = __esm({
17550
17988
  "src/tools/page-studio.ts"() {
17551
17989
  "use strict";
@@ -17562,6 +18000,8 @@ var init_page_studio = __esm({
17562
18000
  init_tool_helpers();
17563
18001
  init_compose();
17564
18002
  init_website();
18003
+ init_net_guard();
18004
+ pageStudioNet = { read: guardedFetchLike };
17565
18005
  valueCardSchema = import_zod43.z.object({
17566
18006
  title: import_zod43.z.string().describe("Card title (short, uppercase reads best)."),
17567
18007
  bodyHtml: import_zod43.z.string().describe("Card body as HTML (<p>\u2026</p>). May use {{location.*}} merge fields."),
@@ -17649,13 +18089,13 @@ function cleanFormDataForSave(formData) {
17649
18089
  const cleaned = dropRedundantCaseCollidingKeys(formData);
17650
18090
  return isJsonObject(cleaned) ? cleaned : formData;
17651
18091
  }
17652
- async function formApiRequest(client, method, path44, body) {
17653
- const url = `https://backend.leadconnectorhq.com/forms${path44}`;
18092
+ async function formApiRequest(client, method, path45, body) {
18093
+ const url = `https://backend.leadconnectorhq.com/forms${path45}`;
17654
18094
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
17655
18095
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
17656
18096
  if (!response.ok) {
17657
18097
  const text3 = await response.text();
17658
- throw new Error(`Form API Error ${response.status}: ${method} ${path44}
18098
+ throw new Error(`Form API Error ${response.status}: ${method} ${path45}
17659
18099
  ${text3}`);
17660
18100
  }
17661
18101
  const text2 = await response.text();
@@ -17697,7 +18137,7 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
17697
18137
  );
17698
18138
  return;
17699
18139
  }
17700
- const formRequest = (method, path44, body) => formApiRequest(client, method, path44, body);
18140
+ const formRequest = (method, path45, body) => formApiRequest(client, method, path45, body);
17701
18141
  server2.tool(
17702
18142
  "get_form_full",
17703
18143
  "Get a form with full builder data: all fields (labels, types, IDs, validation), conditional logic, auto-responder config, email notification settings, styling, and version history. This is the internal API \u2014 it returns what the form builder UI shows, with one exception: where GoHighLevel stores the same value under two spellings of one key that differ only in case (it writes custom fields with both `Id` and `id`), only the lowercase one is returned. No value is lost, and the result can be edited and passed straight back to update_form.",
@@ -17831,10 +18271,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
17831
18271
  },
17832
18272
  async ({ formId, limit, skip }) => {
17833
18273
  try {
17834
- let path44 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
17835
- if (formId) path44 += `&formId=${formId}`;
17836
- if (skip) path44 += `&skip=${skip}`;
17837
- const result = await formRequest("GET", path44);
18274
+ let path45 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
18275
+ if (formId) path45 += `&formId=${formId}`;
18276
+ if (skip) path45 += `&skip=${skip}`;
18277
+ const result = await formRequest("GET", path45);
17838
18278
  return {
17839
18279
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
17840
18280
  };
@@ -17854,6 +18294,51 @@ var init_form_builder = __esm({
17854
18294
  }
17855
18295
  });
17856
18296
 
18297
+ // src/test-contacts.ts
18298
+ function carrierOf(raw) {
18299
+ const c = asObj(raw);
18300
+ const id = str(c.id) ?? str(c._id);
18301
+ if (!id) return void 0;
18302
+ const name = str(c.contactName) ?? [str(c.firstName), str(c.lastName)].filter(Boolean).join(" ");
18303
+ return { id, name: name ?? "", phone: str(c.phone) ?? "", email: (str(c.email) ?? "").toLowerCase(), created: str(c.dateAdded) ?? "" };
18304
+ }
18305
+ function carrierText(contacts, partial, former = []) {
18306
+ const head4 = partial ? `${CARRIER_PARTIAL} ` : "";
18307
+ const was = former.map((f) => ` ${f.id}: carried the test address during the run; now shows ${f.now || "no email"}.`).join("");
18308
+ if (!contacts.length) return `${head4}No contact carrying this run's test address was found.${was}`;
18309
+ const one = (c) => `${c.id} (name: ${c.name || "none"}; phone: ${c.phone || "none"}; email: ${c.email || "none"}; created: ${c.created || "unknown"})`;
18310
+ return `${head4}${CARRIER_HEADING}: ${contacts.map(one).join(", ")}. ${CARRIER_WARNING}${was}`;
18311
+ }
18312
+ async function readContactPages(get, params, cap = CONTACT_PAGE_CAP) {
18313
+ const limit = typeof params.limit === "number" ? params.limit : 20;
18314
+ const rows2 = [];
18315
+ let paging = {};
18316
+ for (let page = 0; page < cap; page++) {
18317
+ const raw = asObj(await get("/contacts/", { params: { ...params, ...paging } }));
18318
+ if (!Array.isArray(raw.contacts)) return void 0;
18319
+ rows2.push(...raw.contacts);
18320
+ const meta = asObj(raw.meta);
18321
+ const total = typeof meta.total === "number" ? meta.total : void 0;
18322
+ if (total !== void 0 && rows2.length >= total) return { rows: rows2, complete: true };
18323
+ const next = meta.startAfterId !== void 0 && meta.startAfterId !== null && meta.startAfter !== void 0 && meta.startAfter !== null ? { startAfterId: meta.startAfterId, startAfter: meta.startAfter } : typeof meta.nextPage === "number" ? { page: meta.nextPage } : void 0;
18324
+ if (!next || raw.contacts.length === 0) return { rows: rows2, complete: total === void 0 ? raw.contacts.length < limit : rows2.length >= total };
18325
+ paging = next;
18326
+ }
18327
+ return { rows: rows2, complete: false };
18328
+ }
18329
+ var CARRIER_HEADING, CARRIER_WARNING, CARRIER_PARTIAL, asObj, str, CONTACT_PAGE_CAP;
18330
+ var init_test_contacts = __esm({
18331
+ "src/test-contacts.ts"() {
18332
+ "use strict";
18333
+ CARRIER_HEADING = "Contacts that now carry this run's test address";
18334
+ CARRIER_WARNING = "If your form matches leads by phone, some of these may be existing customers whose email was replaced. Check each before deleting; to delete, ask Claude to delete specific contacts by id.";
18335
+ CARRIER_PARTIAL = "Partial, a search could not be read.";
18336
+ asObj = (v) => v && typeof v === "object" ? v : {};
18337
+ str = (v) => typeof v === "string" && v.trim() ? v : void 0;
18338
+ CONTACT_PAGE_CAP = 5;
18339
+ }
18340
+ });
18341
+
17857
18342
  // src/tools/funnel-qa.ts
17858
18343
  function rollupVerdict(checks) {
17859
18344
  const fail = checks.filter((c) => c.status === "fail");
@@ -17864,52 +18349,66 @@ function rollupVerdict(checks) {
17864
18349
  return { ok: fail.length === 0, verdict, summary };
17865
18350
  }
17866
18351
  function parseContact(raw) {
17867
- const c = asObj(asObj(raw).contact);
17868
- const id = str(c.id) ?? str(c._id);
18352
+ const c = asObj2(asObj2(raw).contact);
18353
+ const id = str2(c.id) ?? str2(c._id);
17869
18354
  if (!id) return void 0;
17870
18355
  const custom = /* @__PURE__ */ new Map();
17871
18356
  for (const f of asArr(c.customFields)) {
17872
- const fo = asObj(f);
17873
- const fid = str(fo.id) ?? str(fo.customFieldId) ?? str(fo.field_id);
18357
+ const fo = asObj2(f);
18358
+ const fid = str2(fo.id) ?? str2(fo.customFieldId) ?? str2(fo.field_id);
17874
18359
  if (!fid) continue;
17875
18360
  const val = fo.value ?? fo.field_value ?? fo.fieldValue ?? "";
17876
18361
  custom.set(fid, Array.isArray(val) ? val.join(",") : String(val));
17877
18362
  }
17878
18363
  return {
17879
18364
  id,
17880
- email: str(c.email)?.toLowerCase(),
17881
- phone: str(c.phone),
17882
- firstName: str(c.firstName),
17883
- lastName: str(c.lastName),
17884
- name: str(c.name),
18365
+ email: str2(c.email)?.toLowerCase(),
18366
+ phone: str2(c.phone),
18367
+ firstName: str2(c.firstName),
18368
+ lastName: str2(c.lastName),
18369
+ name: str2(c.name),
17885
18370
  tags: asArr(c.tags).map((t) => String(t).toLowerCase()),
17886
- source: str(c.source),
17887
- assignedTo: str(c.assignedTo),
18371
+ source: str2(c.source),
18372
+ assignedTo: str2(c.assignedTo),
17888
18373
  custom
17889
18374
  };
17890
18375
  }
17891
- async function findContactsByEmail(client, locationId2, email) {
17892
- const raw = await client.get("/contacts/", { params: { locationId: locationId2, query: email, limit: 20 } });
18376
+ async function searchByEmail(client, locationId2, email) {
18377
+ const paged = await readContactPages((path45, o) => client.get(path45, o), { locationId: locationId2, query: email, limit: 20 });
18378
+ if (!paged) throw new SearchUnreadable("the contact search answered in a shape that could not be read");
17893
18379
  const want = email.toLowerCase();
17894
18380
  const ids = [];
17895
- for (const c of asArr(asObj(raw).contacts)) {
17896
- const co = asObj(c);
17897
- if (str(co.email)?.toLowerCase() === want) {
17898
- const id = str(co.id) ?? str(co._id);
17899
- if (id) ids.push(id);
18381
+ const matches = [];
18382
+ const rows2 = [];
18383
+ for (const c of paged.rows) {
18384
+ const co = asObj2(c);
18385
+ const id = str2(co.id) ?? str2(co._id);
18386
+ if (!id) continue;
18387
+ ids.push(id);
18388
+ const row = carrierOf(co);
18389
+ if (row) rows2.push(row);
18390
+ if (str2(co.email)?.toLowerCase() === want) {
18391
+ const t = Date.parse(str2(co.dateAdded) ?? "");
18392
+ const name = str2(co.contactName) ?? ([str2(co.firstName), str2(co.lastName)].filter(Boolean).join(" ") || void 0);
18393
+ matches.push({ id, email: want, added: Number.isFinite(t) ? t : void 0, name, carrier: carrierOf(co) });
17900
18394
  }
17901
18395
  }
17902
- return ids;
18396
+ return { ids, matches, rows: rows2, complete: paged.complete };
18397
+ }
18398
+ async function findContactsByEmail(client, locationId2, email, onRead) {
18399
+ const read2 = await searchByEmail(client, locationId2, email);
18400
+ onRead?.(read2);
18401
+ return read2.matches.map((m) => m.id);
17903
18402
  }
17904
18403
  async function getContact(client, contactId) {
17905
18404
  return parseContact(await client.get(`/contacts/${contactId}`));
17906
18405
  }
17907
- async function settledEmailIds(client, locationId2, email, opts = {}) {
18406
+ async function settledEmailIds(client, locationId2, email, opts = {}, onRead) {
17908
18407
  const tries = opts.tries ?? 6;
17909
18408
  const delayMs = opts.delayMs ?? 3500;
17910
18409
  const seen = /* @__PURE__ */ new Set();
17911
18410
  for (let i = 1; i <= tries; i++) {
17912
- for (const id of await findContactsByEmail(client, locationId2, email)) seen.add(id);
18411
+ for (const id of await findContactsByEmail(client, locationId2, email, onRead)) seen.add(id);
17913
18412
  if (seen.size >= 2) break;
17914
18413
  if (i < tries) await sleep3(delayMs);
17915
18414
  }
@@ -17919,21 +18418,21 @@ async function hasSmsCapability(client, locationId2) {
17919
18418
  let numbers = 0;
17920
18419
  let pools = 0;
17921
18420
  try {
17922
- numbers = asArr(asObj(await client.get("/phone-system/numbers/", { params: { locationId: locationId2 } })).phoneNumbers).length;
18421
+ numbers = asArr(asObj2(await client.get("/phone-system/numbers/", { params: { locationId: locationId2 } })).phoneNumbers).length;
17923
18422
  } catch {
17924
18423
  }
17925
18424
  try {
17926
- pools = asArr(asObj(await client.get("/phone-system/number-pools/", { params: { locationId: locationId2 } })).pools).length;
18425
+ pools = asArr(asObj2(await client.get("/phone-system/number-pools/", { params: { locationId: locationId2 } })).pools).length;
17927
18426
  } catch {
17928
18427
  }
17929
18428
  return { numbers, pools };
17930
18429
  }
17931
18430
  async function latestOutboundMs(client, locationId2, contactId) {
17932
18431
  const convRaw = await client.get("/conversations/search", { params: { locationId: locationId2, contactId, limit: 20 } });
17933
- const conversations = asArr(asObj(convRaw).conversations);
18432
+ const conversations = asArr(asObj2(convRaw).conversations);
17934
18433
  let best;
17935
18434
  for (const c of conversations) {
17936
- const convId = str(asObj(c).id) ?? str(asObj(c)._id);
18435
+ const convId = str2(asObj2(c).id) ?? str2(asObj2(c)._id);
17937
18436
  if (!convId) continue;
17938
18437
  let msgsRaw;
17939
18438
  try {
@@ -17941,45 +18440,41 @@ async function latestOutboundMs(client, locationId2, contactId) {
17941
18440
  } catch {
17942
18441
  continue;
17943
18442
  }
17944
- const mo = asObj(msgsRaw);
17945
- const list3 = asArr(mo.messages).length ? asArr(mo.messages) : asArr(asObj(mo.messages).messages);
18443
+ const mo = asObj2(msgsRaw);
18444
+ const list3 = asArr(mo.messages).length ? asArr(mo.messages) : asArr(asObj2(mo.messages).messages);
17946
18445
  for (const m of list3) {
17947
- const msg3 = asObj(m);
17948
- const dir = str(msg3.direction)?.toLowerCase();
18446
+ const msg3 = asObj2(m);
18447
+ const dir = str2(msg3.direction)?.toLowerCase();
17949
18448
  if (dir !== "outbound") continue;
17950
- const at = Date.parse(str(msg3.dateAdded) ?? str(msg3.dateUpdated) ?? "");
18449
+ const at = Date.parse(str2(msg3.dateAdded) ?? str2(msg3.dateUpdated) ?? "");
17951
18450
  const atMs = Number.isNaN(at) ? void 0 : at;
17952
- if (!best || atMs && (!best.atMs || atMs > best.atMs)) best = { type: str(msg3.type), atMs };
18451
+ if (!best || atMs && (!best.atMs || atMs > best.atMs)) best = { type: str2(msg3.type), atMs };
17953
18452
  }
17954
18453
  }
17955
18454
  return best ? { found: true, ...best } : { found: false };
17956
18455
  }
17957
18456
  async function readWorkflow(bc, workflowId) {
17958
18457
  try {
17959
- const full = asObj(await bc.getWorkflow(workflowId));
17960
- const status = str(full.status);
17961
- const templates = asArr(asObj(full.workflowData).templates);
17962
- const sendsSms = templates.some((t) => str(asObj(t).type) === "sms");
18458
+ const full = asObj2(await bc.getWorkflow(workflowId));
18459
+ const status = str2(full.status);
18460
+ const templates = asArr(asObj2(full.workflowData).templates);
18461
+ const sendsSms = templates.some((t) => str2(asObj2(t).type) === "sms");
17963
18462
  return { status, sendsSms };
17964
18463
  } catch {
17965
18464
  return void 0;
17966
18465
  }
17967
18466
  }
17968
18467
  async function submit(url, body) {
17969
- const res = await fetch(url, {
17970
- method: "POST",
17971
- headers: { "Content-Type": "application/json", Accept: "application/json" },
17972
- body: JSON.stringify(body)
17973
- });
17974
- const text2 = (await res.text().catch(() => "")).slice(0, 300);
18468
+ const res = await guardedPost(url, JSON.stringify(body), { "Content-Type": "application/json", Accept: "application/json" });
18469
+ const text2 = res.text.slice(0, 300);
17975
18470
  return { httpStatus: res.status, blockedByCaptcha: res.status === 403 && /captcha|turnstile/i.test(text2), text: text2 };
17976
18471
  }
17977
18472
  function registerFunnelQaTools(server2, client, builderClient) {
17978
18473
  server2.tool(
17979
18474
  "verify_funnel",
17980
- "Prove a live public funnel actually captures a lead AND the automation acts on it \u2014 the runtime companion to audit_workflows. Submits a sentinel lead to funnelUrl, then reads GHL back to verify what REALLY happened (the UI/thank-you page is never proof): (1) backend contact truth \u2014 the contact is actually created (search_contacts), with submit\u2192appear latency; (2) field-value fidelity \u2014 each expectCustom value actually PERSISTED on the contact (a wrong field id makes upsert succeed while silently dropping the value); (3) the triggerTag landed (else the workflow can't fire); (4) SMS/A2P pre-check \u2014 flags if the account has no SMS-capable number so any SMS step would silently fail; (5) workflow status \u2014 flags a DRAFT workflow (never fires on real leads); (6) outreach fired \u2014 an OUTBOUND message was actually logged for the contact (proves enrollment + send, not just a green log), with latency; (7) consent recorded when messaging fires (TCPA/CAN-SPAM); (8) duplicate-contact retest \u2014 a second identical submit dedups to ONE contact, not a duplicate/false-failure; (9) opportunity/routing (expectOpportunity/expectAssigned/expectSource); (10) multi-surface (extraUrls) + concurrent double-submit race. Booking can't be faked server-side, so a real burner appointment is surfaced as a manual step. WARNING: this creates a real (clearly-marked 'blueprint-qa') contact and MAY fire real automation + internal notifications; it deletes the sentinel afterward by default (cleanup). Run it on the REAL production URL.",
18475
+ "Prove a live public funnel actually captures a lead AND the automation acts on it \u2014 the runtime companion to audit_workflows. Submits a sentinel lead to funnelUrl, then reads GHL back to verify what REALLY happened (the UI/thank-you page is never proof): (1) backend contact truth \u2014 a contact carrying the test address actually appears (search_contacts), with submit\u2192appear latency; (2) field-value fidelity \u2014 each expectCustom value actually PERSISTED on the contact (a wrong field id makes upsert succeed while silently dropping the value); (3) the triggerTag landed (else the workflow can't fire); (4) SMS/A2P pre-check \u2014 flags if the account has no SMS-capable number so any SMS step would silently fail; (5) workflow status \u2014 flags a DRAFT workflow (never fires on real leads); (6) outreach fired \u2014 an OUTBOUND message was actually logged for the contact (proves enrollment + send, not just a green log), with latency; (7) consent recorded when messaging fires (TCPA/CAN-SPAM); (8) duplicate-contact retest \u2014 a second identical submit dedups to ONE contact, not a duplicate/false-failure; (9) opportunity/routing (expectOpportunity/expectAssigned/expectSource); (10) multi-surface (extraUrls) + concurrent double-submit race. Booking can't be faked server-side, so a real burner appointment is surfaced as a manual step. WARNING: this submits a real (clearly-marked 'blueprint-qa') test lead, which GoHighLevel may turn into a contact, and MAY fire real automation + internal notifications; it never deletes anything: the result lists every contact that now carries the run's test address (id, name, phone, email, created date), for you to check before asking Claude to delete any. Run it on the REAL production URL.",
17981
18476
  {
17982
- funnelUrl: import_zod45.z.string().describe("The live URL the test submission POSTs to (your external lead-bridge Worker URL, or any endpoint that creates the GHL contact). POSTed as JSON."),
18477
+ funnelUrl: import_zod45.z.string().describe("The live URL the test submission POSTs to (your external lead-bridge Worker URL, or any endpoint that sends the lead into GHL). POSTed as JSON."),
17983
18478
  sentinelEmail: import_zod45.z.string().optional().describe("Override the minted sentinel email. Default: a unique blueprint-qa.<id>@example.com (won't deliver; used only to find the contact)."),
17984
18479
  testPhone: import_zod45.z.string().optional().describe("Include a phone on the submission (enables SMS-path checks). Must be a test-safe number you control \u2014 real SMS may send + bill."),
17985
18480
  submitBody: import_zod45.z.record(import_zod45.z.string(), import_zod45.z.unknown()).optional().describe("Full JSON body to POST (advanced; for non-bridge endpoints). The sentinel email is injected if absent. Default body is the lead-bridge contract {first_name,last_name,email,phone,custom,_hp}."),
@@ -17995,7 +18490,7 @@ function registerFunnelQaTools(server2, client, builderClient) {
17995
18490
  expectSource: import_zod45.z.boolean().optional().describe("Assert a lead source/attribution was captured (reporting works)."),
17996
18491
  extraUrls: import_zod45.z.array(import_zod45.z.string()).optional().describe("Additional capture surfaces to test the same way (each gets its own sentinel) \u2014 popup/inline/exit-intent forms."),
17997
18492
  testConcurrentDuplicate: import_zod45.z.boolean().optional().describe("Fire two simultaneous submits with a fresh email \u2192 assert exactly ONE contact (concurrent dedup race)."),
17998
- cleanup: import_zod45.z.boolean().optional().describe("Delete the sentinel contact(s) after verifying. Default true."),
18493
+ cleanup: import_zod45.z.boolean().optional().describe("No longer used: verify_funnel never deletes anything. The contacts that now carry the run's test address are listed in the result for you to check."),
17999
18494
  locationId: import_zod45.z.string().optional().describe("Target sub-account. Must match the active location.")
18000
18495
  },
18001
18496
  async (args) => {
@@ -18005,8 +18500,6 @@ function registerFunnelQaTools(server2, client, builderClient) {
18005
18500
  return jsonResponse({ ok: false, error: `Location mismatch: active is ${locationId2}, you passed ${args.locationId}. switch_location + confirm first.` });
18006
18501
  }
18007
18502
  const checks = [];
18008
- const cleanup = args.cleanup !== false;
18009
- const sentinels = [];
18010
18503
  const token2 = crypto.randomUUID().slice(0, 8);
18011
18504
  const sentinelEmail = (args.sentinelEmail ?? `blueprint-qa.${token2}@example.com`).toLowerCase();
18012
18505
  const custom = {};
@@ -18015,11 +18508,58 @@ function registerFunnelQaTools(server2, client, builderClient) {
18015
18508
  if (typeof body.email !== "string" || !body.email) body.email = sentinelEmail;
18016
18509
  const searchEmail = String(body.email).toLowerCase();
18017
18510
  if (args.testPhone && !body.phone) body.phone = args.testPhone;
18018
- const submitMs = Date.now();
18511
+ const addressProblem = await publicAddressProblem(args.funnelUrl);
18512
+ if (addressProblem && /local or private/i.test(addressProblem)) {
18513
+ const sentence = "Not checked: the address is private, so no test lead was sent.";
18514
+ return jsonResponse({ ok: false, verdict: "NOT_CHECKED", summary: sentence, checks: [{ id: "submit", label: "Submit reached the endpoint", status: "skip", detail: sentence }] });
18515
+ }
18516
+ const raceEmail = `blueprint-qa.${crypto.randomUUID().slice(0, 8)}@example.com`;
18517
+ const surfaceEmails = (args.extraUrls ?? []).map(() => `blueprint-qa.${crypto.randomUUID().slice(0, 8)}@example.com`);
18518
+ const runEmails = [searchEmail, ...args.testConcurrentDuplicate ? [raceEmail] : [], ...surfaceEmails];
18519
+ const notRun = (sentence) => jsonResponse({ ok: false, verdict: "NOT_RUN", summary: sentence, checks: [{ id: "preflight", label: "Test address is new", status: "fail", detail: sentence }] });
18520
+ const runStart = Date.now();
18521
+ const everSeen = /* @__PURE__ */ new Set();
18522
+ const found = /* @__PURE__ */ new Map();
18523
+ const latest = /* @__PURE__ */ new Map();
18524
+ let partial = false;
18525
+ const onRead = (r) => {
18526
+ if (!r.complete) partial = true;
18527
+ for (const id of r.ids) everSeen.add(id);
18528
+ for (const row of r.rows) latest.set(row.id, row);
18529
+ for (const m of r.matches) found.set(m.id, m);
18530
+ };
18531
+ for (const email of runEmails) {
18532
+ let read2;
18533
+ try {
18534
+ read2 = await searchByEmail(client, locationId2, email);
18535
+ } catch (e) {
18536
+ return notRun(`Not run: the contact search could not be read (${e instanceof Error ? e.message : String(e)}), so no test lead was sent.`);
18537
+ }
18538
+ onRead(read2);
18539
+ if (read2.matches.length) return notRun(`Not run: ${email} already belongs to a contact in this account; use a test address.`);
18540
+ if (!read2.complete) return notRun(`Not run: the contact search for ${email} could not be read in full, so whether the address is free was not checked and no test lead was sent.`);
18541
+ }
18542
+ const memberChosen = Boolean(args.sentinelEmail) || typeof args.submitBody?.email === "string" && args.submitBody.email !== "";
18543
+ const ownEmails = /* @__PURE__ */ new Set([...memberChosen ? [] : [searchEmail], raceEmail, ...surfaceEmails]);
18544
+ const beginSubmit = (email) => ({ email, start: Date.now(), before: new Set(everSeen) });
18545
+ const landed = (f, sub) => ownEmails.has(sub.email) && f.email === sub.email && f.added !== void 0 && f.added >= sub.start - 6e4 && !sub.before.has(f.id);
18546
+ const searchFailed = (e) => {
18547
+ partial = true;
18548
+ return `Not checked: a contact search could not be read (${e instanceof Error ? e.message.split("\n")[0] : String(e)}), so whether this submit's lead arrived was not checked.`;
18549
+ };
18550
+ const notLanded = (f, sub) => !ownEmails.has(sub.email) ? `A contact with the address you chose now exists (id ${f.id}); because that address is not the run's own, whether this run created it was not checked.` : f.added === void 0 ? `Not checked: when contact ${f.id} was added could not be read, so whether this run's lead landed was not checked.` : `Not checked: the contact found for ${sub.email} (id ${f.id}) is dated before this submit by the server clock, so whether this run's lead landed was not checked.`;
18551
+ const main2 = beginSubmit(searchEmail);
18552
+ const submitMs = main2.start;
18019
18553
  let httpInfo = "";
18554
+ const REDIRECTED = "Not checked: the form's endpoint answered with a redirect, which is not followed, so whether a lead landed was not checked.";
18555
+ let redirected = false;
18020
18556
  try {
18021
18557
  const r = await submit(args.funnelUrl, body);
18022
18558
  httpInfo = `HTTP ${r.httpStatus}`;
18559
+ if (r.httpStatus >= 300 && r.httpStatus < 400) {
18560
+ redirected = true;
18561
+ checks.push({ id: "submit", label: "Submit reached the endpoint", status: "skip", detail: `The endpoint answered ${httpInfo}, a redirect, which is not followed.` });
18562
+ }
18023
18563
  if (r.blockedByCaptcha) {
18024
18564
  checks.push({ id: "spam_gate", label: "Spam gate (Turnstile)", status: "warn", detail: `The funnel's Turnstile gate blocked this automated submit (${httpInfo}). That's correct protection \u2014 but it means verify_funnel can't drive this path. Test it manually, or temporarily allow a QA bypass.` });
18025
18565
  }
@@ -18027,27 +18567,39 @@ function registerFunnelQaTools(server2, client, builderClient) {
18027
18567
  checks.push({ id: "submit", label: "Submit reached the endpoint", status: "fail", detail: `POST to funnelUrl failed: ${e instanceof Error ? e.message : String(e)}` });
18028
18568
  }
18029
18569
  let contactId;
18030
- let cleanedUp = false;
18031
18570
  try {
18032
18571
  let appearMs;
18572
+ let seenForMain = [];
18573
+ let mainReadFailed;
18033
18574
  const backendTries = 12;
18034
- for (let i = 1; i <= backendTries; i++) {
18035
- const ids = await findContactsByEmail(client, locationId2, searchEmail);
18036
- if (ids.length) {
18037
- contactId = ids[0];
18038
- appearMs = Date.now();
18039
- break;
18575
+ try {
18576
+ for (let i = 1; i <= backendTries; i++) {
18577
+ const ids = await findContactsByEmail(client, locationId2, searchEmail, onRead);
18578
+ if (ids.length) {
18579
+ seenForMain = ids.map((id) => found.get(id)).filter((f) => !!f);
18580
+ appearMs = Date.now();
18581
+ break;
18582
+ }
18583
+ if (i < backendTries) await sleep3(2e3 + i * 500);
18040
18584
  }
18041
- if (i < backendTries) await sleep3(2e3 + i * 500);
18585
+ } catch (e) {
18586
+ mainReadFailed = searchFailed(e);
18042
18587
  }
18588
+ const mainLanded = new Set(seenForMain.filter((f) => landed(f, main2)).map((f) => f.id));
18589
+ if (mainLanded.size) contactId = [...mainLanded][0];
18043
18590
  const sms = await hasSmsCapability(client, locationId2);
18044
18591
  const wf = args.workflowId && builderClient ? await readWorkflow(builderClient, args.workflowId) : void 0;
18045
- if (!contactId) {
18592
+ if (mainReadFailed) {
18593
+ checks.push({ id: "backend_truth", label: "Lead actually landed (backend)", status: "skip", detail: mainReadFailed });
18594
+ } else if (!contactId && seenForMain.length) {
18595
+ checks.push({ id: "backend_truth", label: "Lead actually landed (backend)", status: "skip", detail: notLanded(seenForMain[0], main2) });
18596
+ } else if (!contactId && redirected) {
18597
+ checks.push({ id: "backend_truth", label: "Lead actually landed (backend)", status: "skip", detail: REDIRECTED });
18598
+ } else if (!contactId) {
18046
18599
  checks.push({ id: "backend_truth", label: "Lead actually landed (backend)", status: "fail", detail: `No contact with ${searchEmail} appeared in GHL after ~${Math.round(backendTries * 2.5)}s (${httpInfo || "submit failed"}). The funnel is a paper shredder \u2014 leads evaporate even if the page says thanks.` });
18047
18600
  } else {
18048
- sentinels.push(contactId);
18049
18601
  const latencySec = appearMs ? Math.round((appearMs - submitMs) / 1e3) : void 0;
18050
- checks.push({ id: "backend_truth", label: "Lead actually landed (backend)", status: "pass", detail: `Contact ${contactId} created (${httpInfo}). Submit\u2192appear ~${latencySec}s.` });
18602
+ checks.push({ id: "backend_truth", label: "Lead actually landed (backend)", status: "pass", detail: `A contact carrying this run's test address (id ${contactId}) appeared after the submit (${httpInfo}). Submit\u2192appear ~${latencySec}s.` });
18051
18603
  checks.push(latencySec !== void 0 && latencySec > 60 ? { id: "latency", label: "Speed-to-lead latency", status: "warn", detail: `Lead took ~${latencySec}s to land \u2014 slow capture loses leads. Investigate the bridge/retry path.` } : { id: "latency", label: "Speed-to-lead latency", status: "pass", detail: `~${latencySec ?? "<1"}s submit\u2192contact.` });
18052
18604
  const contact = await getContact(client, contactId);
18053
18605
  if (!contact) {
@@ -18065,11 +18617,11 @@ function registerFunnelQaTools(server2, client, builderClient) {
18065
18617
  else if (got2.trim() !== f.value.trim()) probs.push(`custom ${f.label ?? f.fieldId}: saved "${got2}" \u2260 sent "${f.value}"`);
18066
18618
  }
18067
18619
  const submitted = {
18068
- email: str(body.email)?.toLowerCase(),
18069
- phone: str(body.phone),
18070
- first_name: str(body.first_name),
18071
- last_name: str(body.last_name),
18072
- name: str(body.name)
18620
+ email: str2(body.email)?.toLowerCase(),
18621
+ phone: str2(body.phone),
18622
+ first_name: str2(body.first_name),
18623
+ last_name: str2(body.last_name),
18624
+ name: str2(body.name)
18073
18625
  };
18074
18626
  const got = {
18075
18627
  email: contact.email,
@@ -18113,7 +18665,7 @@ function registerFunnelQaTools(server2, client, builderClient) {
18113
18665
  if (args.workflowId) {
18114
18666
  if (!builderClient) checks.push({ id: "workflow_status", label: "Workflow is published", status: "skip", detail: "Workflow status needs the workflow-builder (Firebase) client, which isn't configured on this install." });
18115
18667
  else if (!wf) checks.push({ id: "workflow_status", label: "Workflow is published", status: "warn", detail: `Could not read workflow ${args.workflowId} (id wrong, or not readable).` });
18116
- else if (str(wf.status)?.toLowerCase() === "draft") checks.push({ id: "workflow_status", label: "Workflow is published", status: "fail", detail: `Workflow ${args.workflowId} is DRAFT \u2014 it will NOT fire on real leads. Publish it.` });
18668
+ else if (str2(wf.status)?.toLowerCase() === "draft") checks.push({ id: "workflow_status", label: "Workflow is published", status: "fail", detail: `Workflow ${args.workflowId} is DRAFT \u2014 it will NOT fire on real leads. Publish it.` });
18117
18669
  else checks.push({ id: "workflow_status", label: "Workflow is published", status: "pass", detail: `Workflow status: ${wf.status}.` });
18118
18670
  }
18119
18671
  const wantOutreach = args.expectOutreach ?? !!(args.triggerTag || args.workflowId);
@@ -18138,13 +18690,13 @@ function registerFunnelQaTools(server2, client, builderClient) {
18138
18690
  if (args.expectOpportunity) {
18139
18691
  try {
18140
18692
  const oppRaw = await client.get("/opportunities/search", { params: { location_id: locationId2, contact_id: contactId } });
18141
- const opps = asArr(asObj(oppRaw).opportunities).map(asObj);
18693
+ const opps = asArr(asObj2(oppRaw).opportunities).map(asObj2);
18142
18694
  if (opps.length === 0) {
18143
18695
  checks.push({ id: "opportunity", label: "Opportunity created", status: "fail", detail: "No opportunity exists for the contact \u2014 the create-opportunity step didn't run (often a dead pipeline/stage id silently kills it)." });
18144
18696
  } else {
18145
18697
  const want = args.expectOpportunity;
18146
- const match = opps.find((o) => (!want.pipelineId || str(o.pipelineId) === want.pipelineId) && (!want.stageId || str(o.pipelineStageId) === want.stageId));
18147
- checks.push(match ? { id: "opportunity", label: "Opportunity created", status: "pass", detail: `Opportunity in the expected pipeline/stage.` } : { id: "opportunity", label: "Opportunity created", status: "fail", detail: `An opportunity exists but not in the expected pipeline/stage (got pipeline ${str(opps[0].pipelineId)}, stage ${str(opps[0].pipelineStageId)}).` });
18698
+ const match = opps.find((o) => (!want.pipelineId || str2(o.pipelineId) === want.pipelineId) && (!want.stageId || str2(o.pipelineStageId) === want.stageId));
18699
+ checks.push(match ? { id: "opportunity", label: "Opportunity created", status: "pass", detail: `Opportunity in the expected pipeline/stage.` } : { id: "opportunity", label: "Opportunity created", status: "fail", detail: `An opportunity exists but not in the expected pipeline/stage (got pipeline ${str2(opps[0].pipelineId)}, stage ${str2(opps[0].pipelineStageId)}).` });
18148
18700
  }
18149
18701
  } catch (e) {
18150
18702
  checks.push({ id: "opportunity", label: "Opportunity created", status: "warn", detail: `Could not read opportunities: ${e instanceof Error ? e.message : String(e)}` });
@@ -18158,78 +18710,135 @@ function registerFunnelQaTools(server2, client, builderClient) {
18158
18710
  if (!contact) checks.push({ id: "attribution", label: "Lead source captured", status: "fail", detail: "Could not read the contact to verify source." });
18159
18711
  else checks.push(contact.source ? { id: "attribution", label: "Lead source captured", status: "pass", detail: `source = "${contact.source}".` } : { id: "attribution", label: "Lead source captured", status: "warn", detail: "No source/attribution on the contact \u2014 reporting can't tell which funnel/ad produced it." });
18160
18712
  }
18713
+ const dup = beginSubmit(searchEmail);
18161
18714
  await submit(args.funnelUrl, body).catch(() => void 0);
18162
- const dupIds = await settledEmailIds(client, locationId2, searchEmail, { tries: 6, delayMs: 3e3 });
18163
- for (const id of dupIds) if (!sentinels.includes(id)) sentinels.push(id);
18164
- checks.push(dupIds.length === 1 ? { id: "duplicate", label: "Duplicate-contact dedup", status: "pass", detail: "A second identical submit deduped to ONE contact (upsert by email)." } : dupIds.length === 0 ? { id: "duplicate", label: "Duplicate-contact dedup", status: "warn", detail: `Could not observe the contact for ${searchEmail} on re-read (search lag) \u2014 dedup not conclusively verified.` } : { id: "duplicate", label: "Duplicate-contact dedup", status: "fail", detail: `A second submit produced ${dupIds.length} contacts for ${searchEmail} \u2014 dedup is broken (real returning visitors will duplicate or false-fail).` });
18715
+ let dupMatches = [];
18716
+ let dupFailed;
18717
+ try {
18718
+ dupMatches = (await settledEmailIds(client, locationId2, searchEmail, { tries: 6, delayMs: 3e3 }, onRead)).map((id) => found.get(id)).filter((f) => !!f);
18719
+ } catch (e) {
18720
+ dupFailed = searchFailed(e);
18721
+ }
18722
+ const dupKnown = dupMatches.filter((f) => mainLanded.has(f.id) || landed(f, dup));
18723
+ const dupOther = dupMatches.filter((f) => !mainLanded.has(f.id) && !landed(f, dup));
18724
+ checks.push(dupFailed ? { id: "duplicate", label: "Duplicate-contact dedup", status: "skip", detail: dupFailed } : dupOther.length ? { id: "duplicate", label: "Duplicate-contact dedup", status: "skip", detail: `Not checked: a contact listed before the second submit now carries the address (id ${dupOther[0].id}), so whether the second submit made a duplicate was not checked.` } : dupKnown.length === 1 ? { id: "duplicate", label: "Duplicate-contact dedup", status: "pass", detail: "After a second identical submit, still ONE contact carries the address (upsert by email)." } : dupKnown.length === 0 ? { id: "duplicate", label: "Duplicate-contact dedup", status: "warn", detail: `Could not observe the contact for ${searchEmail} on re-read (search lag) \u2014 dedup not conclusively verified.` } : { id: "duplicate", label: "Duplicate-contact dedup", status: "fail", detail: `After a second submit, ${dupKnown.length} contacts carry ${searchEmail} \u2014 dedup is broken (real returning visitors will duplicate or false-fail).` });
18165
18725
  if (args.testConcurrentDuplicate) {
18166
- const raceToken = crypto.randomUUID().slice(0, 8);
18167
- const raceEmail = `blueprint-qa.${raceToken}@example.com`;
18168
- const raceBody = { ...body, email: raceEmail, last_name: `QA race ${raceToken}` };
18169
- await Promise.allSettled([submit(args.funnelUrl, raceBody), submit(args.funnelUrl, raceBody)]);
18170
- const raceIds = await settledEmailIds(client, locationId2, raceEmail, { tries: 6, delayMs: 3e3 });
18171
- for (const id of raceIds) if (!sentinels.includes(id)) sentinels.push(id);
18172
- checks.push(raceIds.length === 1 ? { id: "concurrent_race", label: "Concurrent double-submit", status: "pass", detail: `Two simultaneous submits \u2192 1 contact (no race duplicate).` } : raceIds.length === 0 ? { id: "concurrent_race", label: "Concurrent double-submit", status: "warn", detail: "Could not observe the race result (search lag, or neither submit landed)." } : { id: "concurrent_race", label: "Concurrent double-submit", status: "fail", detail: `Two simultaneous submits created ${raceIds.length} contacts \u2014 a concurrency race duplicates leads under real traffic.` });
18726
+ let raceBefore;
18727
+ try {
18728
+ await findContactsByEmail(client, locationId2, raceEmail, onRead);
18729
+ } catch (e) {
18730
+ raceBefore = searchFailed(e);
18731
+ }
18732
+ if (raceBefore) checks.push({ id: "concurrent_race", label: "Concurrent double-submit", status: "skip", detail: `${raceBefore} The concurrent pair was not sent.` });
18733
+ else {
18734
+ const race = beginSubmit(raceEmail);
18735
+ const raceBody = { ...body, email: raceEmail, last_name: `QA race ${raceEmail.slice(13, 21)}` };
18736
+ await Promise.allSettled([submit(args.funnelUrl, raceBody), submit(args.funnelUrl, raceBody)]);
18737
+ let raceMine = [];
18738
+ let raceFailed;
18739
+ try {
18740
+ raceMine = (await settledEmailIds(client, locationId2, raceEmail, { tries: 6, delayMs: 3e3 }, onRead)).map((id) => found.get(id)).filter((f) => !!f && landed(f, race));
18741
+ } catch (e) {
18742
+ raceFailed = searchFailed(e);
18743
+ }
18744
+ checks.push(raceFailed ? { id: "concurrent_race", label: "Concurrent double-submit", status: "skip", detail: raceFailed } : raceMine.length === 1 ? { id: "concurrent_race", label: "Concurrent double-submit", status: "pass", detail: `Two simultaneous submits \u2192 1 contact carrying the race address appeared (no race duplicate).` } : raceMine.length === 0 ? { id: "concurrent_race", label: "Concurrent double-submit", status: "warn", detail: "Could not observe the race result (search lag, or neither submit landed)." } : { id: "concurrent_race", label: "Concurrent double-submit", status: "fail", detail: `Two simultaneous submits \u2192 ${raceMine.length} contacts carrying the race address appeared \u2014 a concurrency race duplicates leads under real traffic.` });
18745
+ }
18173
18746
  }
18174
- for (const url of args.extraUrls ?? []) {
18175
- const sToken = crypto.randomUUID().slice(0, 8);
18176
- const sEmail = `blueprint-qa.${sToken}@example.com`;
18177
- const sBody = { ...body, email: sEmail, last_name: `QA ${sToken}` };
18747
+ for (const [k, url] of (args.extraUrls ?? []).entries()) {
18748
+ const sEmail = surfaceEmails[k];
18749
+ const sBody = { ...body, email: sEmail, last_name: `QA ${sEmail.slice(13, 21)}` };
18750
+ let sBefore;
18751
+ try {
18752
+ await findContactsByEmail(client, locationId2, sEmail, onRead);
18753
+ } catch (e) {
18754
+ sBefore = searchFailed(e);
18755
+ }
18756
+ if (sBefore) {
18757
+ checks.push({ id: `surface:${url}`, label: `Capture surface ${url}`, status: "skip", detail: `${sBefore} This surface's test lead was not sent.` });
18758
+ continue;
18759
+ }
18760
+ const surf = beginSubmit(sEmail);
18178
18761
  let ids = [];
18762
+ let sRedirected = false;
18763
+ let sFailed;
18179
18764
  try {
18180
- await submit(url, sBody);
18181
- ids = await settledEmailIds(client, locationId2, sEmail, { tries: 8, delayMs: 2500 });
18765
+ const r = await submit(url, sBody);
18766
+ sRedirected = r.httpStatus >= 300 && r.httpStatus < 400;
18767
+ try {
18768
+ ids = await settledEmailIds(client, locationId2, sEmail, { tries: 8, delayMs: 2500 }, onRead);
18769
+ } catch (e) {
18770
+ sFailed = searchFailed(e);
18771
+ }
18182
18772
  } catch {
18183
18773
  }
18184
- for (const id of ids) if (!sentinels.includes(id)) sentinels.push(id);
18185
- checks.push(ids.length >= 1 ? { id: `surface:${url}`, label: `Capture surface ${url}`, status: "pass", detail: `Submission landed ${ids.length} contact(s).` } : { id: `surface:${url}`, label: `Capture surface ${url}`, status: "fail", detail: "Submission did NOT land a contact \u2014 this surface is a paper shredder." });
18774
+ const seenHere = ids.map((id) => found.get(id)).filter((f) => !!f);
18775
+ const sMine = seenHere.filter((f) => landed(f, surf));
18776
+ checks.push(sFailed ? { id: `surface:${url}`, label: `Capture surface ${url}`, status: "skip", detail: sFailed } : sMine.length >= 1 ? { id: `surface:${url}`, label: `Capture surface ${url}`, status: "pass", detail: `${sMine.length} contact(s) carrying this surface's test address appeared after the submit.` } : seenHere.length ? { id: `surface:${url}`, label: `Capture surface ${url}`, status: "skip", detail: notLanded(seenHere[0], surf) } : sRedirected ? { id: `surface:${url}`, label: `Capture surface ${url}`, status: "skip", detail: REDIRECTED } : { id: `surface:${url}`, label: `Capture surface ${url}`, status: "fail", detail: "Submission did NOT land a contact \u2014 this surface is a paper shredder." });
18186
18777
  }
18187
18778
  }
18188
18779
  checks.push({ id: "booking", label: "Booking (manual)", status: "skip", detail: "A reachable calendar URL is not proof. Book ONE real test ('burner') appointment through the live booking widget and confirm it lands in GHL (Carlos #6). This step can't be verified server-side." });
18189
18780
  } catch (e) {
18190
- checks.push({ id: "error", label: "verify_funnel run error", status: "fail", detail: e instanceof Error ? e.message : String(e) });
18191
- } finally {
18192
- const uniq = [...new Set(sentinels)];
18193
- if (!cleanup) {
18194
- checks.push({ id: "cleanup", label: "Sentinel cleanup", status: "skip", detail: `cleanup:false \u2014 ${uniq.length} "blueprint-qa" sentinel contact(s) left in the account. Delete them so QA doesn't pollute reporting.` });
18195
- } else if (uniq.length === 0) {
18196
- cleanedUp = true;
18197
- checks.push({ id: "cleanup", label: "Sentinel cleanup", status: "skip", detail: "No sentinel contacts were created." });
18198
- } else {
18199
- let deleted = 0;
18200
- for (const id of uniq) {
18201
- try {
18202
- await client.delete(`/contacts/${id}`);
18203
- deleted++;
18204
- } catch {
18205
- }
18206
- }
18207
- cleanedUp = deleted === uniq.length;
18208
- checks.push({ id: "cleanup", label: "Sentinel cleanup", status: cleanedUp ? "pass" : "warn", detail: cleanedUp ? `Deleted ${deleted} sentinel contact(s).` : `Deleted ${deleted}/${uniq.length} sentinel contact(s) \u2014 remove any leftover "blueprint-qa" contacts manually.` });
18781
+ partial = true;
18782
+ const msg3 = e instanceof Error ? e.message.split("\n")[0] : String(e);
18783
+ const bt2 = checks.find((c) => c.id === "backend_truth");
18784
+ if (bt2?.status === "pass") {
18785
+ bt2.status = "skip";
18786
+ bt2.detail = `Not checked: a read after the lead appeared could not be completed (${msg3}), so this submit was not fully checked.`;
18787
+ for (let i = checks.length - 1; i >= 0; i--) if (checks[i].id === "latency") checks.splice(i, 1);
18788
+ checks.push({ id: "error", label: "verify_funnel read error", status: "skip", detail: msg3 });
18789
+ } else checks.push({ id: "error", label: "verify_funnel run error", status: "fail", detail: e instanceof Error ? e.message : String(e) });
18790
+ }
18791
+ for (const f of found.values()) {
18792
+ if (f.added !== void 0 && f.added < runStart - 6e4) checks.push({ id: `upsert:${f.id}`, label: "Test email on an existing contact", status: "warn", detail: `Contact ${f.id} carrying the test address is dated before this submit by the server clock; it may be an existing contact the endpoint re-used. Check that contact's email.` });
18793
+ }
18794
+ const runAddresses = /* @__PURE__ */ new Set([searchEmail, ...ownEmails]);
18795
+ for (const id of [...found.keys()].slice(0, 25)) {
18796
+ try {
18797
+ const now = carrierOf(asObj2(await client.get(`/contacts/${id}`)).contact);
18798
+ if (now) latest.set(id, now);
18799
+ } catch (e) {
18800
+ if (/\b404\b|not found/i.test(e instanceof Error ? e.message : String(e))) latest.set(id, { id, name: "", phone: "", email: "", created: "" });
18801
+ else partial = true;
18209
18802
  }
18210
18803
  }
18211
- return finish2(checks, { contactId, sentinelEmail: searchEmail, cleanedUp });
18804
+ if (found.size > 25) partial = true;
18805
+ const carriers = [];
18806
+ const former = [];
18807
+ for (const f of found.values()) {
18808
+ const now = latest.get(f.id);
18809
+ if (now && !runAddresses.has(now.email)) former.push({ id: f.id, now: now.email || "no contact by that id" });
18810
+ else carriers.push(now ?? f.carrier ?? { id: f.id, name: f.name ?? "", phone: "", email: f.email, created: "" });
18811
+ }
18812
+ checks.push({ id: "test_contacts", label: CARRIER_HEADING, status: "skip", detail: carrierText(carriers, partial, former) });
18813
+ const bt = checks.find((c) => c.id === "backend_truth");
18814
+ const notChecked = bt?.status === "skip" && !checks.some((c) => c.status === "fail") ? bt.detail : void 0;
18815
+ return finish2(checks, { contactId, sentinelEmail: searchEmail, contactsCarryingTestAddress: carriers, formerCarriers: former, contactsListPartial: partial }, carriers.length > 0, notChecked);
18212
18816
  } catch (error) {
18213
18817
  return errorResponse(error);
18214
18818
  }
18215
18819
  }
18216
18820
  );
18217
18821
  }
18218
- function finish2(checks, extra) {
18219
- const { ok, verdict, summary } = rollupVerdict(checks);
18220
- const note = "WARNING: verify_funnel created a real (clearly-marked 'blueprint-qa') contact and may have fired real automation + internal notifications. Run on the real production URL; the form/thank-you page is never proof \u2014 only the backend is.";
18822
+ function finish2(checks, extra, createdAny, notChecked) {
18823
+ const rolled = rollupVerdict(checks);
18824
+ const { ok, verdict, summary } = notChecked ? { ok: false, verdict: "NOT_CHECKED", summary: notChecked } : rolled;
18825
+ const note = createdAny ? "WARNING: verify_funnel submitted a real (clearly-marked 'blueprint-qa') test lead and may have fired real automation + internal notifications. It never deletes anything: the contacts that now carry this run's test address are listed, for you to check before deleting any. Run on the real production URL; the form/thank-you page is never proof \u2014 only the backend is." : "No contact carrying this run's test address was found.";
18221
18826
  return jsonResponse({ ok, verdict, summary, checks, note, ...extra });
18222
18827
  }
18223
- var import_zod45, asObj, asArr, str, sleep3, customFieldInput;
18828
+ var import_zod45, asObj2, asArr, str2, sleep3, SearchUnreadable, customFieldInput;
18224
18829
  var init_funnel_qa = __esm({
18225
18830
  "src/tools/funnel-qa.ts"() {
18226
18831
  "use strict";
18227
18832
  import_zod45 = require("zod");
18228
18833
  init_tool_helpers();
18229
- asObj = (v) => v && typeof v === "object" ? v : {};
18834
+ init_test_contacts();
18835
+ init_net_guard();
18836
+ asObj2 = (v) => v && typeof v === "object" ? v : {};
18230
18837
  asArr = (v) => Array.isArray(v) ? v : [];
18231
- str = (v) => typeof v === "string" ? v : void 0;
18838
+ str2 = (v) => typeof v === "string" ? v : void 0;
18232
18839
  sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
18840
+ SearchUnreadable = class extends Error {
18841
+ };
18233
18842
  customFieldInput = import_zod45.z.object({
18234
18843
  fieldId: import_zod45.z.string().describe("The VERIFIED GHL custom-field id the form sends (from apply_build_plan's wiring bundle)."),
18235
18844
  value: import_zod45.z.string().describe("The value the form sends for this field \u2014 asserted to PERSIST on the contact."),
@@ -18242,13 +18851,13 @@ var init_funnel_qa = __esm({
18242
18851
  function registerPipelineBuilderTools(server2, builderClient) {
18243
18852
  if (!builderClient) return;
18244
18853
  const client = builderClient;
18245
- async function pipelineRequest(method, path44, body) {
18246
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path44}`;
18854
+ async function pipelineRequest(method, path45, body) {
18855
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path45}`;
18247
18856
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT" || method === "PATCH");
18248
18857
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
18249
18858
  if (!response.ok) {
18250
18859
  const text3 = await response.text();
18251
- throw new Error(`Pipeline API Error ${response.status}: ${method} ${path44}
18860
+ throw new Error(`Pipeline API Error ${response.status}: ${method} ${path45}
18252
18861
  ${text3}`);
18253
18862
  }
18254
18863
  const text2 = await response.text();
@@ -18449,13 +19058,13 @@ var init_sub_account_errors = __esm({
18449
19058
  });
18450
19059
 
18451
19060
  // src/field-sanitize.ts
18452
- var MAX_STR, MAX_LIST, str2, num, list, drop, isoDate;
19061
+ var MAX_STR, MAX_LIST, str3, num, list, drop, isoDate;
18453
19062
  var init_field_sanitize = __esm({
18454
19063
  "src/field-sanitize.ts"() {
18455
19064
  "use strict";
18456
19065
  MAX_STR = 200;
18457
19066
  MAX_LIST = 50;
18458
- str2 = (x, max = MAX_STR) => {
19067
+ str3 = (x, max = MAX_STR) => {
18459
19068
  if (typeof x !== "string") return void 0;
18460
19069
  const t = x.trim();
18461
19070
  return t ? t.slice(0, max) : void 0;
@@ -18468,7 +19077,7 @@ var init_field_sanitize = __esm({
18468
19077
  };
18469
19078
  drop = (o) => Object.values(o).some((v) => v !== void 0) ? o : void 0;
18470
19079
  isoDate = (x) => {
18471
- const s = str2(x, 40);
19080
+ const s = str3(x, 40);
18472
19081
  if (!s) return void 0;
18473
19082
  const m = /^(\d{4})-(\d{2})-(\d{2})(?:[T ].*)?$/.exec(s);
18474
19083
  if (!m) return void 0;
@@ -18580,7 +19189,7 @@ function safeParse(raw) {
18580
19189
  }
18581
19190
  }
18582
19191
  function safeUrl2(x, max = MAX_URL) {
18583
- const raw = str2(x, max);
19192
+ const raw = str3(x, max);
18584
19193
  if (!raw) return void 0;
18585
19194
  let u;
18586
19195
  try {
@@ -18611,68 +19220,68 @@ function sanitizeProfile(raw) {
18611
19220
  v: 1,
18612
19221
  canCreateSubAccounts: typeof r.canCreateSubAccounts === "boolean" ? r.canCreateSubAccounts : void 0,
18613
19222
  ghlPlanEvidence: sanitizePlanEvidence(r.ghlPlanEvidence),
18614
- agencyName: str2(r.agencyName, 80),
18615
- legalName: str2(r.legalName),
19223
+ agencyName: str3(r.agencyName, 80),
19224
+ legalName: str3(r.legalName),
18616
19225
  accent: normalizeAccent(r.accent),
18617
19226
  logoUrl: safeUrl2(r.logoUrl),
18618
19227
  website: safeUrl2(r.website),
18619
19228
  sender: drop({
18620
- fromName: str2(sender?.fromName, 80),
18621
- replyTo: str2(sender?.replyTo),
18622
- phone: str2(sender?.phone, 32)
19229
+ fromName: str3(sender?.fromName, 80),
19230
+ replyTo: str3(sender?.replyTo),
19231
+ phone: str3(sender?.phone, 32)
18623
19232
  }),
18624
19233
  offers: list(r.offers, (o) => {
18625
19234
  const v = o ?? {};
18626
- const name = str2(v.name, 120);
19235
+ const name = str3(v.name, 120);
18627
19236
  if (!name) return void 0;
18628
19237
  return {
18629
19238
  name,
18630
19239
  priceFloor: num(v.priceFloor),
18631
19240
  cadence: v.cadence === "monthly" || v.cadence === "one-time" ? v.cadence : void 0,
18632
- notes: str2(v.notes, 500)
19241
+ notes: str3(v.notes, 500)
18633
19242
  };
18634
19243
  }),
18635
19244
  packages: (() => {
18636
19245
  const seen = /* @__PURE__ */ new Set();
18637
19246
  return list(r.packages, (pk) => {
18638
19247
  const v = pk ?? {};
18639
- const name = str2(v.name, 80);
19248
+ const name = str3(v.name, 80);
18640
19249
  if (!name) return void 0;
18641
- const idRaw = str2(v.id, 60) || name;
19250
+ const idRaw = str3(v.id, 60) || name;
18642
19251
  const id = idRaw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "package";
18643
19252
  if (seen.has(id)) return void 0;
18644
19253
  seen.add(id);
18645
19254
  const features = Array.isArray(v.features) ? [...new Set(v.features.filter(isFeatureId))] : [];
18646
- return { id, name, price: str2(v.price, 60), features };
19255
+ return { id, name, price: str3(v.price, 60), features };
18647
19256
  });
18648
19257
  })(),
18649
19258
  pricing: drop({
18650
19259
  minMonthly: num(pricing?.minMonthly),
18651
19260
  minSetup: num(pricing?.minSetup),
18652
- currency: str2(pricing?.currency, 8)
19261
+ currency: str3(pricing?.currency, 8)
18653
19262
  }),
18654
19263
  delivery: drop({
18655
- sops: list(delivery?.sops, (s) => str2(s, MAX_SOP)),
19264
+ sops: list(delivery?.sops, (s) => str3(s, MAX_SOP)),
18656
19265
  hoursToDeliverTarget: num(delivery?.hoursToDeliverTarget)
18657
19266
  }),
18658
19267
  staff: list(r.staff, (s) => {
18659
19268
  const v = s ?? {};
18660
- const name = str2(v.name, 80);
19269
+ const name = str3(v.name, 80);
18661
19270
  if (!name) return void 0;
18662
19271
  return {
18663
19272
  name,
18664
- role: str2(v.role),
18665
- email: str2(v.email),
18666
- mayApprove: list(v.mayApprove, (a) => str2(a, 40))
19273
+ role: str3(v.role),
19274
+ email: str3(v.email),
19275
+ mayApprove: list(v.mayApprove, (a) => str3(a, 40))
18667
19276
  };
18668
19277
  }),
18669
19278
  defaults: drop({
18670
- timezone: str2(defaults?.timezone, 64),
18671
- industry: str2(defaults?.industry, 64)
19279
+ timezone: str3(defaults?.timezone, 64),
19280
+ industry: str3(defaults?.industry, 64)
18672
19281
  }),
18673
19282
  agencyOs: sanitizeAgencyOs(r.agencyOs),
18674
19283
  clientFilesDir: safeDir(r.clientFilesDir),
18675
- updated_at: str2(r.updated_at, 40)
19284
+ updated_at: str3(r.updated_at, 40)
18676
19285
  };
18677
19286
  }
18678
19287
  function sanitizeAgencyOs(raw) {
@@ -18680,16 +19289,16 @@ function sanitizeAgencyOs(raw) {
18680
19289
  const r = raw;
18681
19290
  const ica = r.ica ?? {};
18682
19291
  const offer = r.offer ?? {};
18683
- const lines = (x) => list(x, (s) => str2(s, MAX_STR * 2));
19292
+ const lines = (x) => list(x, (s) => str3(s, MAX_STR * 2));
18684
19293
  const out = drop({
18685
- story: str2(r.story, MAX_PARA),
18686
- method: str2(r.method, MAX_PARA),
18687
- voice: str2(r.voice, 400),
19294
+ story: str3(r.story, MAX_PARA),
19295
+ method: str3(r.method, MAX_PARA),
19296
+ voice: str3(r.voice, 400),
18688
19297
  values: lines(r.values),
18689
- goals: str2(r.goals, 400),
19298
+ goals: str3(r.goals, 400),
18690
19299
  proof: lines(r.proof),
18691
19300
  ica: drop({
18692
- who: str2(ica.who, 400),
19301
+ who: str3(ica.who, 400),
18693
19302
  pains: lines(ica.pains),
18694
19303
  desires: lines(ica.desires),
18695
19304
  objections: lines(ica.objections),
@@ -18699,11 +19308,11 @@ function sanitizeAgencyOs(raw) {
18699
19308
  villains: lines(ica.villains)
18700
19309
  }),
18701
19310
  offer: drop({
18702
- name: str2(offer.name, 120),
18703
- promise: str2(offer.promise, 400),
19311
+ name: str3(offer.name, 120),
19312
+ promise: str3(offer.promise, 400),
18704
19313
  valueStack: lines(offer.valueStack),
18705
- price: str2(offer.price, 120),
18706
- guarantee: str2(offer.guarantee, 600),
19314
+ price: str3(offer.price, 120),
19315
+ guarantee: str3(offer.guarantee, 600),
18707
19316
  accelerators: lines(offer.accelerators)
18708
19317
  })
18709
19318
  });
@@ -18712,7 +19321,7 @@ function sanitizeAgencyOs(raw) {
18712
19321
  function legacyBranding(profileFile) {
18713
19322
  try {
18714
19323
  const raw = JSON.parse(fs9.readFileSync(path8.join(path8.dirname(profileFile), "cockpit-state.json"), "utf8"));
18715
- return { agencyName: str2(raw?.agencyName, 80), accent: normalizeAccent(raw?.agencyAccent) };
19324
+ return { agencyName: str3(raw?.agencyName, 80), accent: normalizeAccent(raw?.agencyAccent) };
18716
19325
  } catch {
18717
19326
  return {};
18718
19327
  }
@@ -18822,7 +19431,7 @@ function demoRailProfile(p = readAgencyProfile(), ghl, demoDays = 7) {
18822
19431
  if (p.accent) rec.accent = p.accent;
18823
19432
  if (p.logoUrl) rec.logoUrl = p.logoUrl;
18824
19433
  if (p.sender) rec.senderIdentity = p.sender;
18825
- const g = drop({ locationId: str2(ghl?.locationId), triggerTag: str2(ghl?.triggerTag, 60) });
19434
+ const g = drop({ locationId: str3(ghl?.locationId), triggerTag: str3(ghl?.triggerTag, 60) });
18826
19435
  if (g) rec.ghl = g;
18827
19436
  rec.demoDays = num(demoDays) ?? 7;
18828
19437
  return rec;
@@ -19064,9 +19673,6 @@ var init_telemetry_transport = __esm({
19064
19673
  });
19065
19674
 
19066
19675
  // src/interactive-capture.ts
19067
- function chromeProfileDir() {
19068
- return path9.join(appDataDir(), "chrome-profile");
19069
- }
19070
19676
  function captureStatePath() {
19071
19677
  return path9.join(appDataDir(), "interactive-capture.json");
19072
19678
  }
@@ -19235,9 +19841,9 @@ function planFromPayload(payload) {
19235
19841
  return payload?.plan === "command-os" ? "command-os" : "mcp";
19236
19842
  }
19237
19843
  function legacyDeviceFingerprint() {
19238
- const os8 = require("node:os");
19844
+ const os9 = require("node:os");
19239
19845
  const crypto9 = require("node:crypto");
19240
- const raw = `${os8.hostname()}:${os8.userInfo().username}:${os8.platform()}:${os8.arch()}`;
19846
+ const raw = `${os9.hostname()}:${os9.userInfo().username}:${os9.platform()}:${os9.arch()}`;
19241
19847
  return crypto9.createHash("sha256").update(raw).digest("hex").slice(0, 16);
19242
19848
  }
19243
19849
  function deviceFingerprint(opts) {
@@ -19277,7 +19883,7 @@ function base64urlDecode(s) {
19277
19883
  async function getPublicKey() {
19278
19884
  if (cachedPublicKey) return cachedPublicKey;
19279
19885
  const raw = Buffer.from(ATTESTATION_PUBLIC_KEY_B64, "base64");
19280
- const key = await import_node_crypto3.webcrypto.subtle.importKey(
19886
+ const key = await import_node_crypto4.webcrypto.subtle.importKey(
19281
19887
  "raw",
19282
19888
  raw,
19283
19889
  { name: "Ed25519" },
@@ -19325,7 +19931,7 @@ async function verifyAttestation(token2, bindings, now = /* @__PURE__ */ new Dat
19325
19931
  let signatureValid;
19326
19932
  try {
19327
19933
  const key = await getPublicKey();
19328
- signatureValid = await import_node_crypto3.webcrypto.subtle.verify({ name: "Ed25519" }, key, signatureBytes, payloadBytes);
19934
+ signatureValid = await import_node_crypto4.webcrypto.subtle.verify({ name: "Ed25519" }, key, signatureBytes, payloadBytes);
19329
19935
  } catch {
19330
19936
  return { ok: false, reason: "bad-signature" };
19331
19937
  }
@@ -19357,11 +19963,11 @@ function shouldRenew(payload, now = /* @__PURE__ */ new Date()) {
19357
19963
  const remainingMs = expiry - now.getTime();
19358
19964
  return remainingMs < 7 * 24 * 60 * 60 * 1e3;
19359
19965
  }
19360
- var import_node_crypto3, ATTESTATION_PUBLIC_KEY_B64, ATTESTATION_VERSION, ATTESTATION_TTL_DAYS, ATTESTATION_GRACE_DAYS, cachedPublicKey;
19966
+ var import_node_crypto4, ATTESTATION_PUBLIC_KEY_B64, ATTESTATION_VERSION, ATTESTATION_TTL_DAYS, ATTESTATION_GRACE_DAYS, cachedPublicKey;
19361
19967
  var init_attestation = __esm({
19362
19968
  "src/attestation.ts"() {
19363
19969
  "use strict";
19364
- import_node_crypto3 = require("node:crypto");
19970
+ import_node_crypto4 = require("node:crypto");
19365
19971
  ATTESTATION_PUBLIC_KEY_B64 = "8KJo8V3Z7ngeToIQfOXpKdlr/EA34hXJVEIpW0A7wqs=";
19366
19972
  ATTESTATION_VERSION = 1;
19367
19973
  ATTESTATION_TTL_DAYS = 30;
@@ -20694,23 +21300,23 @@ function moneySummary(subs, invoices, charges, products, now = /* @__PURE__ */ n
20694
21300
  }
20695
21301
  }
20696
21302
  const nowS = now.getTime() / 1e3;
20697
- const within = (t, days) => t <= nowS && nowS - t <= days * 86400;
21303
+ const within2 = (t, days) => t <= nowS && nowS - t <= days * 86400;
20698
21304
  const r7 = /* @__PURE__ */ new Map(), r30 = /* @__PURE__ */ new Map();
20699
21305
  let c7 = 0, c30 = 0, failed30 = 0;
20700
21306
  for (const c of charges) {
20701
- if (!within(c.created, 30)) continue;
21307
+ if (!within2(c.created, 30)) continue;
20702
21308
  if (c.status === "failed") {
20703
21309
  failed30++;
20704
21310
  continue;
20705
21311
  }
20706
21312
  if (c.status !== "succeeded") continue;
20707
21313
  const currency = upper(c.currency);
20708
- const net = fromMinor(c.amount - (c.amount_refunded ?? 0), currency) ?? 0;
20709
- if (net <= 0) continue;
20710
- add(r30, currency, net);
21314
+ const net2 = fromMinor(c.amount - (c.amount_refunded ?? 0), currency) ?? 0;
21315
+ if (net2 <= 0) continue;
21316
+ add(r30, currency, net2);
20711
21317
  c30++;
20712
- if (within(c.created, 7)) {
20713
- add(r7, currency, net);
21318
+ if (within2(c.created, 7)) {
21319
+ add(r7, currency, net2);
20714
21320
  c7++;
20715
21321
  }
20716
21322
  }
@@ -20718,7 +21324,7 @@ function moneySummary(subs, invoices, charges, products, now = /* @__PURE__ */ n
20718
21324
  for (const i of invoices) {
20719
21325
  const paidAt = i.status_transitions?.paid_at;
20720
21326
  const subId = i.parent?.subscription_details?.subscription ?? i.subscription;
20721
- if (i.status !== "paid" || !paidAt || !within(paidAt, 30) || !subId) continue;
21327
+ if (i.status !== "paid" || !paidAt || !within2(paidAt, 30) || !subId) continue;
20722
21328
  const currency = upper(i.currency);
20723
21329
  const paid = fromMinor(i.amount_paid, currency);
20724
21330
  if (currency && paid) add(recurring, currency, paid);
@@ -20789,7 +21395,7 @@ function saveCalendarSecret(secret) {
20789
21395
  const validated = SecretSchema.parse(secret);
20790
21396
  const dir = ensureAppDataDir();
20791
21397
  if (process.platform !== "win32") fs12.chmodSync(dir, 448);
20792
- tmp = `${googleCalendarPath()}.${(0, import_node_crypto4.randomUUID)()}.tmp`;
21398
+ tmp = `${googleCalendarPath()}.${(0, import_node_crypto5.randomUUID)()}.tmp`;
20793
21399
  fs12.writeFileSync(tmp, JSON.stringify(validated) + "\n", { mode: 384, flag: "wx" });
20794
21400
  fs12.renameSync(tmp, googleCalendarPath());
20795
21401
  } catch {
@@ -20812,13 +21418,13 @@ function removeCalendarSecret() {
20812
21418
  throw new Error("The Google Calendar sign-in could not be removed from this machine.");
20813
21419
  }
20814
21420
  }
20815
- var fs12, path11, import_node_crypto4, import_zod51, CALENDAR_SCOPE, SecretSchema, googleCalendarPath;
21421
+ var fs12, path11, import_node_crypto5, import_zod51, CALENDAR_SCOPE, SecretSchema, googleCalendarPath;
20816
21422
  var init_google_calendar_store = __esm({
20817
21423
  "src/command-os/connectors/google-calendar-store.ts"() {
20818
21424
  "use strict";
20819
21425
  fs12 = __toESM(require("node:fs"));
20820
21426
  path11 = __toESM(require("node:path"));
20821
- import_node_crypto4 = require("node:crypto");
21427
+ import_node_crypto5 = require("node:crypto");
20822
21428
  import_zod51 = require("zod");
20823
21429
  init_credentials_store();
20824
21430
  CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
@@ -20951,16 +21557,16 @@ function createGoogleCalendar(deps = {}) {
20951
21557
  requireOnlyCalendar(result.scope);
20952
21558
  return result.access_token;
20953
21559
  }
20954
- async function pages(path44, access, item, query = {}) {
21560
+ async function pages(path45, access, item, query = {}) {
20955
21561
  const result = [];
20956
21562
  const seen = /* @__PURE__ */ new Set();
20957
21563
  let pageToken;
20958
21564
  for (let page = 0; page < 100; page++) {
20959
- const url = new URL(`${API}${path44}`);
21565
+ const url = new URL(`${API}${path45}`);
20960
21566
  for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
20961
21567
  if (pageToken) url.searchParams.set("pageToken", pageToken);
20962
21568
  const data = parsed(
20963
- import_zod52.z.object({ items: import_zod52.z.array(item).optional(), nextPageToken: import_zod52.z.string().optional(), kind: import_zod52.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path44.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
21569
+ import_zod52.z.object({ items: import_zod52.z.array(item).optional(), nextPageToken: import_zod52.z.string().optional(), kind: import_zod52.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path45.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
20964
21570
  await request(url.href, { headers: { Authorization: `Bearer ${access}` } })
20965
21571
  );
20966
21572
  result.push(...data.items ?? []);
@@ -20978,9 +21584,9 @@ function createGoogleCalendar(deps = {}) {
20978
21584
  async function start() {
20979
21585
  const app = appConfig();
20980
21586
  clearPending2();
20981
- const state = (0, import_node_crypto5.randomBytes)(32).toString("base64url");
20982
- const verifier = (0, import_node_crypto5.randomBytes)(32).toString("base64url");
20983
- const server2 = (deps.createServer ?? import_node_http.createServer)((req, res) => {
21587
+ const state = (0, import_node_crypto6.randomBytes)(32).toString("base64url");
21588
+ const verifier = (0, import_node_crypto6.randomBytes)(32).toString("base64url");
21589
+ const server2 = (deps.createServer ?? import_node_http2.createServer)((req, res) => {
20984
21590
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
20985
21591
  res.setHeader("Cache-Control", "no-store");
20986
21592
  res.setHeader("Referrer-Policy", "no-referrer");
@@ -21033,7 +21639,7 @@ function createGoogleCalendar(deps = {}) {
21033
21639
  response_type: "code",
21034
21640
  scope: CALENDAR_SCOPE,
21035
21641
  state,
21036
- code_challenge: (0, import_node_crypto5.createHash)("sha256").update(verifier).digest("base64url"),
21642
+ code_challenge: (0, import_node_crypto6.createHash)("sha256").update(verifier).digest("base64url"),
21037
21643
  code_challenge_method: "S256",
21038
21644
  access_type: "offline",
21039
21645
  prompt: "consent"
@@ -21163,13 +21769,13 @@ function createGoogleCalendar(deps = {}) {
21163
21769
  close: clearPending2
21164
21770
  };
21165
21771
  }
21166
- var import_node_http, import_node_crypto5, import_node_child_process, import_zod52, TOKEN_URL, REVOKE_URL, API, tokenSchema, calendarSchema, eventSchema;
21772
+ var import_node_http2, import_node_crypto6, import_node_child_process, import_zod52, TOKEN_URL, REVOKE_URL, API, tokenSchema, calendarSchema, eventSchema;
21167
21773
  var init_google_calendar = __esm({
21168
21774
  "src/command-os/connectors/google-calendar.ts"() {
21169
21775
  "use strict";
21170
21776
  init_any_signal();
21171
- import_node_http = require("node:http");
21172
- import_node_crypto5 = require("node:crypto");
21777
+ import_node_http2 = require("node:http");
21778
+ import_node_crypto6 = require("node:crypto");
21173
21779
  import_node_child_process = require("node:child_process");
21174
21780
  import_zod52 = require("zod");
21175
21781
  init_credentials_store();
@@ -21211,8 +21817,8 @@ function redactConnectionKey(value, key) {
21211
21817
  return value;
21212
21818
  }
21213
21819
  function stripeFetcher(key, signal) {
21214
- return async (path44, query) => {
21215
- const url = new URL(`https://api.stripe.com${path44}`);
21820
+ return async (path45, query) => {
21821
+ const url = new URL(`https://api.stripe.com${path45}`);
21216
21822
  for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
21217
21823
  const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, signal });
21218
21824
  if (!res.ok) {
@@ -21301,11 +21907,11 @@ var init_handlers = __esm({
21301
21907
  });
21302
21908
 
21303
21909
  // src/command-os/connectors/stripe-read.ts
21304
- async function listAll(f, path44, schema, extra = {}) {
21910
+ async function listAll(f, path45, schema, extra = {}) {
21305
21911
  const out = [];
21306
21912
  let after;
21307
21913
  for (let page = 0; page < 20; page++) {
21308
- const result = import_zod53.z.object({ data: import_zod53.z.array(schema), has_more: import_zod53.z.boolean().optional() }).parse(await f(path44, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
21914
+ const result = import_zod53.z.object({ data: import_zod53.z.array(schema), has_more: import_zod53.z.boolean().optional() }).parse(await f(path45, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
21309
21915
  out.push(...result.data);
21310
21916
  if (!result.has_more) return out;
21311
21917
  const last = import_zod53.z.object({ id: import_zod53.z.string().min(1) }).parse(result.data.at(-1));
@@ -21406,25 +22012,25 @@ function sanitizeEngagement(raw) {
21406
22012
  // "client" downstream. Coercing a typo to `software` would silently take a
21407
22013
  // real client off the build board — the failure that costs work.
21408
22014
  kind: ENGAGEMENT_KINDS.includes(r.kind) ? r.kind : void 0,
21409
- clientName: str2(r.clientName, 120),
21410
- offer: str2(r.offer, 120),
22015
+ clientName: str3(r.clientName, 120),
22016
+ offer: str3(r.offer, 120),
21411
22017
  price: num(r.price),
21412
22018
  cadence: r.cadence === "monthly" || r.cadence === "one-time" ? r.cadence : void 0,
21413
- currency: str2(r.currency, 8),
22019
+ currency: str3(r.currency, 8),
21414
22020
  startedOn: isoDate(r.startedOn),
21415
22021
  renewsOn: isoDate(r.renewsOn),
21416
22022
  status: ENGAGEMENT_STATUSES.includes(r.status) ? r.status : void 0,
21417
- owner: str2(r.owner, 80),
21418
- sops: list(r.sops, (s) => str2(s, MAX_SOP2)),
22023
+ owner: str3(r.owner, 80),
22024
+ sops: list(r.sops, (s) => str3(s, MAX_SOP2)),
21419
22025
  emails: list(r.emails, (s) => {
21420
- const v = str2(s, 254)?.trim().toLowerCase();
22026
+ const v = str3(s, 254)?.trim().toLowerCase();
21421
22027
  return isEmailAddress(v) ? v : void 0;
21422
22028
  }),
21423
- notes: str2(r.notes, MAX_NOTES)
22029
+ notes: str3(r.notes, MAX_NOTES)
21424
22030
  };
21425
22031
  const { kind: _classification, ...content } = rec;
21426
22032
  if (!drop(content)) return void 0;
21427
- const stamped = str2(r.updated_at, 40);
22033
+ const stamped = str3(r.updated_at, 40);
21428
22034
  return stamped ? { ...rec, updated_at: stamped } : rec;
21429
22035
  }
21430
22036
  function sanitizeEngagements(raw) {
@@ -21433,13 +22039,13 @@ function sanitizeEngagements(raw) {
21433
22039
  const clients = /* @__PURE__ */ Object.create(null);
21434
22040
  if (src && typeof src === "object" && !Array.isArray(src)) {
21435
22041
  for (const [id, rec] of Object.entries(src).slice(0, MAX_CLIENTS)) {
21436
- const key = str2(id, 64);
22042
+ const key = str3(id, 64);
21437
22043
  if (!key || FORBIDDEN_KEY.has(key)) continue;
21438
22044
  const clean2 = sanitizeEngagement(rec);
21439
22045
  if (clean2) clients[key] = clean2;
21440
22046
  }
21441
22047
  }
21442
- return { v: 1, clients: { ...clients }, updated_at: str2(r.updated_at, 40) };
22048
+ return { v: 1, clients: { ...clients }, updated_at: str3(r.updated_at, 40) };
21443
22049
  }
21444
22050
  function readClientEngagements(file2 = clientEngagementsPath()) {
21445
22051
  try {
@@ -21466,7 +22072,7 @@ function mergeEngagement(existing, patch) {
21466
22072
  return sanitizeEngagement({ ...merged });
21467
22073
  }
21468
22074
  function writeClientEngagement(locationId2, patch, file2 = clientEngagementsPath()) {
21469
- const id = str2(locationId2, 64);
22075
+ const id = str3(locationId2, 64);
21470
22076
  if (!id || FORBIDDEN_KEY.has(id)) throw new Error("An id is required.");
21471
22077
  const stamp = () => {
21472
22078
  try {
@@ -25248,7 +25854,9 @@ function stageBlockedBy(stages, stage) {
25248
25854
  return null;
25249
25855
  }
25250
25856
  function verifyReadbackPrompt(ctx) {
25251
- return "\nAccount read-back has already run in code. These records decide existence, regardless of RESULT ok or dependencyChecks: " + JSON.stringify(ctx?.readback ?? []) + "\nDo not contradict any record. Keep dependencyChecks for evidence you establish yourself. The final report replaces the standing Checked and Not checked headings with the deterministic roll-call. Use the live URL on the funnel or page record, and the triggerTags and id on the speed-to-lead workflow record, for the existing fake-client check. Do not invent a URL or a trigger tag. Before calling verify_funnel, disclose that it submits a marked test lead and may fire this client's automations once. Preserve the existing verify_funnel submission and cleanup. If the funnel has no working page, report the reason that live proof could not run. Add liveProof:{ok,evidence} to RESULT. Set liveProof.ok true only when verify_funnel reports that the marked lead arrived and the test contact was deleted. Quote that result's evidence, including its contact id and cleanup result. Otherwise set liveProof.ok false with the check's own failure or the reason it could not run.\n";
25857
+ return "\nAccount read-back has already run in code. These records decide existence, regardless of RESULT ok or dependencyChecks: " + JSON.stringify(ctx?.readback ?? []) + `
25858
+ Do not contradict any record. Keep dependencyChecks for evidence you establish yourself. The final report replaces the standing Checked and Not checked headings with the deterministic roll-call. Use the live URL on the funnel or page record, and the triggerTags and id on the speed-to-lead workflow record, for the existing fake-client check. Do not invent a URL or a trigger tag. Before calling verify_funnel, disclose that it submits a marked test lead and may fire this client's automations once. Preserve the existing verify_funnel submission. verify_funnel never deletes anything: report the contacts it lists as carrying the test address, for the member to check before deleting any. If the funnel has no working page, report the reason that live proof could not run. Add liveProof:{ok,evidence} to RESULT. Set liveProof.ok true only when verify_funnel's "Lead actually landed" check passed (a contact carrying the run's test address appeared after the submit). Quote that result's evidence, including its contact id and the contacts it lists as carrying the test address. Otherwise set liveProof.ok false with the check's own failure or the reason it could not run.
25859
+ `;
25252
25860
  }
25253
25861
  function buildClaudeArgs(spec, locationId2, locationName, ctx) {
25254
25862
  return [
@@ -28562,7 +29170,7 @@ function remember(id, readAt) {
28562
29170
  const times = readTimes();
28563
29171
  times[id] = readAt;
28564
29172
  ensureAppDataDir();
28565
- const tmp = `${timesPath()}.${(0, import_node_crypto6.randomUUID)()}.tmp`;
29173
+ const tmp = `${timesPath()}.${(0, import_node_crypto7.randomUUID)()}.tmp`;
28566
29174
  try {
28567
29175
  fs21.writeFileSync(tmp, JSON.stringify(times) + "\n", { mode: 384, flag: "wx" });
28568
29176
  fs21.renameSync(tmp, timesPath());
@@ -28615,14 +29223,14 @@ function money2(rows2) {
28615
29223
  async function readBriefPanels(deps) {
28616
29224
  return Promise.all(CONNECTOR_REGISTRY.flatMap((entry) => entry.panel ? [entry.panel(briefPanelDeps(deps))] : []));
28617
29225
  }
28618
- var fs21, path19, import_node_crypto6, import_zod57, MoneySchema, TotalsSchema, SummarySchema, BriefPanelSchema, TimesSchema, timesPath, NotConnected, safeLink, amountText, stripePanel, slackPanel, calendarPanel;
29226
+ var fs21, path19, import_node_crypto7, import_zod57, MoneySchema, TotalsSchema, SummarySchema, BriefPanelSchema, TimesSchema, timesPath, NotConnected, safeLink, amountText, stripePanel, slackPanel, calendarPanel;
28619
29227
  var init_brief_panels = __esm({
28620
29228
  "src/command-os/connectors/brief-panels.ts"() {
28621
29229
  "use strict";
28622
29230
  init_brief_deadline();
28623
29231
  fs21 = __toESM(require("node:fs"));
28624
29232
  path19 = __toESM(require("node:path"));
28625
- import_node_crypto6 = require("node:crypto");
29233
+ import_node_crypto7 = require("node:crypto");
28626
29234
  import_zod57 = require("zod");
28627
29235
  init_credentials_store();
28628
29236
  init_brief_row();
@@ -28840,7 +29448,7 @@ var init_client_capabilities = __esm({
28840
29448
  { key: "reputation", label: "Reputation and content", blurb: "Reviews, social posts and the blog." },
28841
29449
  { key: "health", label: "Health and handoff", blurb: "Proving it works, keeping it clean, and handing it over." }
28842
29450
  ];
28843
- N = (label, path44, params, key, nameField, version) => ({ label, path: path44, params, key, nameField, version });
29451
+ N = (label, path45, params, key, nameField, version) => ({ label, path: path45, params, key, nameField, version });
28844
29452
  W = (read2, extra) => ({ ...read2, ...extra });
28845
29453
  AI2 = "2021-04-15";
28846
29454
  CAPABILITIES = [
@@ -29130,7 +29738,7 @@ var require_package = __commonJS({
29130
29738
  "package.json"(exports2, module2) {
29131
29739
  module2.exports = {
29132
29740
  name: "@elitedcs/ghl-mcp",
29133
- version: "3.88.2",
29741
+ version: "3.88.4",
29134
29742
  mcpName: "io.github.drjerryrelth/ghl-command",
29135
29743
  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.",
29136
29744
  main: "dist/index.js",
@@ -29658,7 +30266,7 @@ function resultHtml(run, now, clientName, action) {
29658
30266
  if (run.status === "failed") {
29659
30267
  return {
29660
30268
  head: pageHead("The Workflow Audit Pro could not run", esc2(run.error ?? "Unknown error"), action),
29661
- rest: callout("warn", "", esc2(run.cleanup?.detail ?? "Nothing was created in the account."))
30269
+ rest: callout("warn", "", esc2(run.cleanup?.detail ?? "No test lead was sent."))
29662
30270
  };
29663
30271
  }
29664
30272
  const f = run.findings;
@@ -29764,7 +30372,7 @@ function startForm(d, inlineSubmit) {
29764
30372
  <div class="cds-field"><label for="owner-email">Your email</label><div class="hint" id="owner-email-hint">The test lead uses it, so every message comes to you.</div><input class="cds-input" id="owner-email" name="ownerEmail" type="email" required value="${esc2(d.ownerEmail ?? "")}" autocomplete="email" aria-describedby="owner-email-hint"></div>
29765
30373
  <div class="cds-field"><label for="test-phone">Test phone, optional</label><div class="hint" id="test-phone-hint">A phone you own; texts will really send to it.</div><input class="cds-input" id="test-phone" name="testPhone" type="tel" autocomplete="tel" placeholder="+1 555 010 0100" aria-describedby="test-phone-hint"></div>
29766
30374
  <div class="cds-field"><label for="watch">Watch for</label><select class="cds-select" id="watch" name="watchMinutes"><option value="auto" selected>Automatic: until the last planned message, plus 2 minutes</option><option value="10">10 minutes</option><option value="30">30 minutes</option><option value="60">1 hour</option><option value="240">4 hours</option><option value="1440">24 hours</option></select></div>
29767
- <label class="cds-check"><input type="checkbox" id="keep" name="keepLead"><span>Keep the test lead afterwards so I can look at it in GoHighLevel</span></label>
30375
+
29768
30376
  <label class="cds-check"><input type="checkbox" id="ff" name="fastForward"><span>Fast-forward: test long workflows in minutes.<small>A temporary copy with every step kept and every wait cut to 1 minute; staff alerts, tasks and lead assignments go to one person you choose, marked TEST; the copy is deleted afterwards.</small></span></label>
29769
30377
  <div class="cds-field" id="alert-to-wrap" hidden><label for="alert-to">Staff alerts, tasks and assignments go to</label><div class="hint" id="alert-to-hint">Instead of your staff, marked TEST.</div><select class="cds-select" id="alert-to" name="alertUserId" aria-describedby="alert-to-hint"></select></div>
29770
30378
  <fieldset class="cds-field" id="hooks" hidden><legend class="lbl">Outside services these workflows send the lead to</legend><div class="wf" id="hooks-list"></div></fieldset>
@@ -29772,7 +30380,7 @@ ${callout("warn", "", "The test lead has no first name, on purpose. The workflow
29772
30380
  ${inlineSubmit ? `<div class="cds-row"><button class="cds-btn ghost" type="submit" id="start-btn" disabled>Start the check</button></div>` : ""}
29773
30381
  <p class="cds-callout result" id="start-result" role="status" aria-live="polite"></p>
29774
30382
  </form>`;
29775
- return panel("New check", `${d.run?.status === "done" ? "Run another check" : "Run the Workflow Audit Pro"} on ${esc2(d.client.name)}`, "Writes one test contact, then removes it", form, ` id="start"`, "cds-no-print");
30383
+ return panel("New check", `${d.run?.status === "done" ? "Run another check" : "Run the Workflow Audit Pro"} on ${esc2(d.client.name)}`, "Sends one test lead, then lists the contacts that carry its address", form, ` id="start"`, "cds-no-print");
29776
30384
  }
29777
30385
  function shopperPageHtml(d, seatToken, seatHeader) {
29778
30386
  const picker = `<form class="picker cds-no-print" method="get" action="/shopper"><div class="cds-field"><label for="client">Client account</label><select class="cds-select" id="client" name="client" onchange="this.form.submit()"><option value="">Choose a client\u2026</option>${d.accounts.map((a) => `<option value="${esc2(a.locationId)}"${d.client?.locationId === a.locationId ? " selected" : ""}>${esc2(a.name)}</option>`).join("")}</select></div><noscript><button class="cds-btn ghost" type="submit">Open</button></noscript></form>`;
@@ -29818,7 +30426,7 @@ ${COCKPIT_TYPE_JS}
29818
30426
  if(document.getElementById("ff").checked&&form.querySelector('[data-hook-blocked]')){say("One of these workflows sends the lead to an address built from a merge field, so fast-forward cannot test it yet. Untick that workflow, or run without fast-forward.");return}
29819
30427
  var btn=document.getElementById("start-btn");btn.disabled=true;say("Starting\u2026");
29820
30428
  var hosts=[].slice.call(form.querySelectorAll('input[name="hook"]:checked')).map(function(i){return i.value});
29821
- post("/api/shopper/start",{client:client,workflowIds:ids,ownerEmail:document.getElementById("owner-email").value,testPhone:document.getElementById("test-phone").value||undefined,watchMinutes:(document.getElementById("watch").value==="auto"?undefined:Number(document.getElementById("watch").value)),keepLead:document.getElementById("keep").checked,fastForward:document.getElementById("ff").checked,allowedHosts:hosts,alertUserId:(document.getElementById("alert-to").value||undefined)})
30429
+ post("/api/shopper/start",{client:client,workflowIds:ids,ownerEmail:document.getElementById("owner-email").value,testPhone:document.getElementById("test-phone").value||undefined,watchMinutes:(document.getElementById("watch").value==="auto"?undefined:Number(document.getElementById("watch").value)),fastForward:document.getElementById("ff").checked,allowedHosts:hosts,alertUserId:(document.getElementById("alert-to").value||undefined)})
29822
30430
  .then(function(r){if(r.ok){location.reload()}else{btn.disabled=false;say(r.error||"That did not start.")}})
29823
30431
  .catch(function(){btn.disabled=false;say("That did not start. Check that Command OS is still running.")});
29824
30432
  })}
@@ -30158,8 +30766,26 @@ async function checkLinks(urls, ownHosts, fetcher, timeoutMs = 1e4) {
30158
30766
  const ctrl = new AbortController();
30159
30767
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
30160
30768
  try {
30161
- const r = await fetcher(url, { method: "GET", redirect: "follow", signal: ctrl.signal, headers: { "User-Agent": "Mozilla/5.0 (Command OS secret shopper)" } });
30162
- results.push(r.status < 400 ? { url, state: "ok", detail: `Opened (HTTP ${r.status}).` } : { url, state: "broken", detail: `The page answered HTTP ${r.status}.` });
30769
+ let cur = url;
30770
+ let r = await fetcher(cur, { method: "GET", redirect: "manual", signal: ctrl.signal, headers: { "User-Agent": "Mozilla/5.0 (Command OS secret shopper)" } });
30771
+ let stopped = null;
30772
+ for (let hop = 0; r.status >= 300 && r.status < 400 && r.location; hop++) {
30773
+ let next;
30774
+ try {
30775
+ next = new URL(r.location, cur).toString();
30776
+ } catch {
30777
+ stopped = "It redirects to an address that is not readable, so it was not followed.";
30778
+ break;
30779
+ }
30780
+ const why = hop >= MAX_LINK_HOPS ? "it redirects more than 5 times" : skipReason(next, ownHosts)?.replace(/^Not opened: /, "") ?? null;
30781
+ if (why) {
30782
+ stopped = `It redirects to ${next}, which was not opened: ${why}`;
30783
+ break;
30784
+ }
30785
+ cur = next;
30786
+ r = await fetcher(cur, { method: "GET", redirect: "manual", signal: ctrl.signal, headers: { "User-Agent": "Mozilla/5.0 (Command OS secret shopper)" } });
30787
+ }
30788
+ results.push(stopped ? { url, state: "skipped", detail: stopped } : r.status < 400 ? { url, state: "ok", detail: `Opened (HTTP ${r.status}).` } : { url, state: "broken", detail: `The page answered HTTP ${r.status}.` });
30163
30789
  } catch (e) {
30164
30790
  const why = ctrl.signal.aborted ? `It did not answer within ${Math.round(timeoutMs / 1e3)} seconds.` : `It could not be reached (${e instanceof Error ? e.message : String(e)}).`;
30165
30791
  results.push({ url, state: "broken", detail: why });
@@ -30173,7 +30799,7 @@ function htmlToText(body) {
30173
30799
  if (!/<(?:p|div|br|table|td|span|a|html|body|h\d|li|style|strong|em|b|i)(?:\s+[a-z][a-z0-9:-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'<>=]+))?)*\s*\/?>/i.test(body)) return body;
30174
30800
  return body.replace(/<(style|script|head)\b[\s\S]*?<\/\1>/gi, " ").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(?:p|div|tr|li|h\d|table)>/gi, "\n").replace(/<\/(?:td|th)>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, "\xA0").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;|&apos;/gi, "'").replace(/&#x([0-9a-f]+);/gi, (_m, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_m, d) => String.fromCodePoint(Number(d))).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
30175
30801
  }
30176
- var URL_RE, HREF_RE, TRACKING_PATH, UNSUB;
30802
+ var URL_RE, HREF_RE, TRACKING_PATH, UNSUB, MAX_LINK_HOPS;
30177
30803
  var init_links = __esm({
30178
30804
  "src/command-os/shopper/links.ts"() {
30179
30805
  "use strict";
@@ -30181,6 +30807,7 @@ var init_links = __esm({
30181
30807
  HREF_RE = /\bhref\s*=\s*["']([^"']+)["']/gi;
30182
30808
  TRACKING_PATH = /\/(?:c|o|l|ls|tl|t|r|e|wf|track|click|open)\/|\/ls\/click|[?&](?:upn|trigger_link|utm_id|mc_cid|_hs(?:enc|mi))=/i;
30183
30809
  UNSUB = /unsubscribe|opt-?out|\/preferences|email-preferences|unsubscribe-view|\/emails\/builder\//i;
30810
+ MAX_LINK_HOPS = 5;
30184
30811
  }
30185
30812
  });
30186
30813
 
@@ -30213,9 +30840,9 @@ var init_mask = __esm({
30213
30840
 
30214
30841
  // src/command-os/shopper/steps.ts
30215
30842
  function reasonOf(meta) {
30216
- const resp = asObj2(asObj2(meta.actionFrom).response);
30217
- const data = asObj2(meta.data);
30218
- const cands = [str3(resp.msg), str3(meta.error), str3(meta.message), str3(data.message), str3(data.msg), str3(meta.reason)];
30843
+ const resp = asObj3(asObj3(meta.actionFrom).response);
30844
+ const data = asObj3(meta.data);
30845
+ const cands = [str4(resp.msg), str4(meta.error), str4(meta.message), str4(data.message), str4(data.msg), str4(meta.reason)];
30219
30846
  return maskSecrets((cands.find((c) => c && c.trim()) ?? "").replace(/(Webhook Action error: )+/g, "Webhook error: ").trim());
30220
30847
  }
30221
30848
  function translate(type, outcome, reason3, retryAt) {
@@ -30241,22 +30868,22 @@ function translate(type, outcome, reason3, retryAt) {
30241
30868
  };
30242
30869
  }
30243
30870
  function judgeSteps(rawRows, contactId, workflowNames) {
30244
- const mine = rawRows.map(asObj2).filter((r) => str3(r.contactId) === contactId).sort((a, b) => (Date.parse(str3(a.timestamp) ?? str3(a.createdAt) ?? "") || 0) - (Date.parse(str3(b.timestamp) ?? str3(b.createdAt) ?? "") || 0) || (str3(b.status) === "added_to_workflow" ? 1 : 0) - (str3(a.status) === "added_to_workflow" ? 1 : 0));
30871
+ const mine = rawRows.map(asObj3).filter((r) => str4(r.contactId) === contactId).sort((a, b) => (Date.parse(str4(a.timestamp) ?? str4(a.createdAt) ?? "") || 0) - (Date.parse(str4(b.timestamp) ?? str4(b.createdAt) ?? "") || 0) || (str4(b.status) === "added_to_workflow" ? 1 : 0) - (str4(a.status) === "added_to_workflow" ? 1 : 0));
30245
30872
  const rows2 = [];
30246
30873
  const passOf = /* @__PURE__ */ new Map();
30247
30874
  for (const r of mine) {
30248
- const status = (str3(r.status) ?? "").toLowerCase();
30249
- const type = str3(r.type) ?? "";
30875
+ const status = (str4(r.status) ?? "").toLowerCase();
30876
+ const type = str4(r.type) ?? "";
30250
30877
  if (status === "retry") continue;
30251
- const wfId = str3(r.workflowId) ?? "";
30878
+ const wfId = str4(r.workflowId) ?? "";
30252
30879
  if (status === "added_to_workflow") passOf.set(wfId, (passOf.get(wfId) ?? 0) + 1);
30253
30880
  const known = STATUS_OUTCOME[status];
30254
30881
  const outcome = type === "condition" && status === "success" ? "branch" : known ?? "unclear";
30255
- const at = Date.parse(str3(r.timestamp) ?? str3(r.createdAt) ?? "") || 0;
30256
- const retryAt = Date.parse(str3(r.nextExecutionAt) ?? "") || void 0;
30257
- const reason3 = reasonOf(asObj2(r.meta));
30882
+ const at = Date.parse(str4(r.timestamp) ?? str4(r.createdAt) ?? "") || 0;
30883
+ const retryAt = Date.parse(str4(r.nextExecutionAt) ?? "") || void 0;
30884
+ const reason3 = reasonOf(asObj3(r.meta));
30258
30885
  const gaveUp = status === "failed_retry_limit_reached";
30259
- const base = { workflowId: wfId, workflowName: workflowNames[wfId] ?? "Workflow", stepId: str3(r.stepId), stepName: str3(r.stepName) ?? type, type, outcome, at, pass: passOf.get(wfId) ?? 1 };
30886
+ const base = { workflowId: wfId, workflowName: workflowNames[wfId] ?? "Workflow", stepId: str4(r.stepId), stepName: str4(r.stepName) ?? type, type, outcome, at, pass: passOf.get(wfId) ?? 1 };
30260
30887
  let detail = "";
30261
30888
  if (outcome === "branch") detail = `Took the "${base.stepName}" path.`;
30262
30889
  else if (outcome === "entered") detail = "The test lead entered this workflow.";
@@ -30361,13 +30988,13 @@ async function auditSteps(reader, workflowIds, workflowNames, contactId, fromMs,
30361
30988
  const judged = judgeSteps(all, contactId, workflowNames);
30362
30989
  return { ...judged, ...unread.length ? { unreadWorkflows: unread } : {}, ...partial.length ? { partialWorkflows: partial } : {} };
30363
30990
  }
30364
- var asObj2, str3, STATUS_OUTCOME, localTime, STEP_LOG_LIMIT;
30991
+ var asObj3, str4, STATUS_OUTCOME, localTime, STEP_LOG_LIMIT;
30365
30992
  var init_steps = __esm({
30366
30993
  "src/command-os/shopper/steps.ts"() {
30367
30994
  "use strict";
30368
30995
  init_mask();
30369
- asObj2 = (v) => v && typeof v === "object" ? v : {};
30370
- str3 = (v) => typeof v === "string" ? v : void 0;
30996
+ asObj3 = (v) => v && typeof v === "object" ? v : {};
30997
+ str4 = (v) => typeof v === "string" ? v : void 0;
30371
30998
  STATUS_OUTCOME = {
30372
30999
  success: "ran",
30373
31000
  executed: "ran",
@@ -30409,24 +31036,24 @@ __export(fastforward_exports, {
30409
31036
  webhookDestinations: () => webhookDestinations
30410
31037
  });
30411
31038
  function windowText(raw) {
30412
- const w = asObj3(raw);
31039
+ const w = asObj4(raw);
30413
31040
  if (!w.start || !w.end) return void 0;
30414
31041
  const days = asArr2(w.days).map((d) => typeof d === "number" ? DAY_NAMES[d] ?? String(d) : String(d));
30415
- const cond = str4(w.condition);
30416
- return `runs only ${str4(w.start)}\u2013${str4(w.end)}${days.length ? ` on ${days.join(", ")}` : ""}${cond ? ` (${cond})` : ""}`;
31042
+ const cond = str5(w.condition);
31043
+ return `runs only ${str5(w.start)}\u2013${str5(w.end)}${days.length ? ` on ${days.join(", ")}` : ""}${cond ? ` (${cond})` : ""}`;
30417
31044
  }
30418
31045
  function rerouteAlert(attrs, owner) {
30419
- const channel = str4(attrs.type) ?? "notification";
31046
+ const channel = str5(attrs.type) ?? "notification";
30420
31047
  if (channel === "notification") {
30421
- const n = asObj3(attrs.notification);
30422
- attrs.notification = { body: n.body ?? "", title: `TEST: ${str4(n.title) ?? ""}`.trim(), userType: "user", redirectPage: n.redirectPage ?? "contact", type: n.type ?? "send_notification", selectedUser: owner };
31048
+ const n = asObj4(attrs.notification);
31049
+ attrs.notification = { body: n.body ?? "", title: `TEST: ${str5(n.title) ?? ""}`.trim(), userType: "user", redirectPage: n.redirectPage ?? "contact", type: n.type ?? "send_notification", selectedUser: owner };
30423
31050
  delete attrs.email;
30424
31051
  delete attrs.sms;
30425
31052
  return void 0;
30426
31053
  }
30427
31054
  if (channel === "email") {
30428
- const e = asObj3(attrs.email);
30429
- attrs.email = { isCloned: false, html: e.html ?? "", subject: `TEST: ${str4(e.subject) ?? ""}`.trim(), selectedUser: [owner], userType: "user", attachments: [] };
31055
+ const e = asObj4(attrs.email);
31056
+ attrs.email = { isCloned: false, html: e.html ?? "", subject: `TEST: ${str5(e.subject) ?? ""}`.trim(), selectedUser: [owner], userType: "user", attachments: [] };
30430
31057
  delete attrs.notification;
30431
31058
  delete attrs.sms;
30432
31059
  return void 0;
@@ -30434,65 +31061,65 @@ function rerouteAlert(attrs, owner) {
30434
31061
  return channel;
30435
31062
  }
30436
31063
  function staffRecipients(step) {
30437
- const type = str4(step.type) ?? "";
30438
- const a = asObj3(step.attributes);
31064
+ const type = str5(step.type) ?? "";
31065
+ const a = asObj4(step.attributes);
30439
31066
  if (type === "internal_notification") {
30440
31067
  const out = [];
30441
31068
  for (const k of ["notification", "email", "sms"]) {
30442
- const ch = asObj3(a[k]);
31069
+ const ch = asObj4(a[k]);
30443
31070
  if (!Object.keys(ch).length) continue;
30444
- if (str4(ch.userType) !== "user") out.push(`everyone (${k})`);
31071
+ if (str5(ch.userType) !== "user") out.push(`everyone (${k})`);
30445
31072
  for (const u of Array.isArray(ch.selectedUser) ? ch.selectedUser : [ch.selectedUser]) if (typeof u === "string") out.push(u);
30446
31073
  }
30447
31074
  return out;
30448
31075
  }
30449
- if (type === "task-notification") return [str4(a.assignedTo) ?? "nobody"];
31076
+ if (type === "task-notification") return [str5(a.assignedTo) ?? "nobody"];
30450
31077
  if (type === "assign_user") return [...asArr2(a.user_list), ...asArr2(a.customUserList)].map((u) => typeof u === "string" ? u : JSON.stringify(u));
30451
31078
  if (type === "internal_create_opportunity" || type === "internal_update_opportunity") {
30452
- return [...a.assignedTo !== void 0 ? [String(a.assignedTo)] : [], ...asArr2(a.__customInputFields__).map(asObj3).filter((f) => /assign/i.test(str4(f.filterField) ?? "")).map((f) => String(f.value))];
31079
+ return [...a.assignedTo !== void 0 ? [String(a.assignedTo)] : [], ...asArr2(a.__customInputFields__).map(asObj4).filter((f) => /assign/i.test(str5(f.filterField) ?? "")).map((f) => String(f.value))];
30453
31080
  }
30454
31081
  return [];
30455
31082
  }
30456
31083
  function waitLength(a) {
30457
- const s = asObj3(a.startAfter);
31084
+ const s = asObj4(a.startAfter);
30458
31085
  const v = Number(s.value);
30459
- const unit = (str4(s.type) ?? "").toLowerCase();
31086
+ const unit = (str5(s.type) ?? "").toLowerCase();
30460
31087
  const word = unit.startsWith("min") ? "minute" : unit.startsWith("hour") ? "hour" : unit.startsWith("day") ? "day" : unit;
30461
31088
  return `${v} ${word}${v === 1 ? "" : "s"}`;
30462
31089
  }
30463
31090
  function webhookDestinations(sources) {
30464
31091
  const out = [];
30465
31092
  for (const s of sources) {
30466
- for (const n of s.templates.map(asObj3)) {
30467
- if (str4(n.type) !== "webhook") continue;
30468
- const url = str4(asObj3(n.attributes).url) ?? "";
31093
+ for (const n of s.templates.map(asObj4)) {
31094
+ if (str5(n.type) !== "webhook") continue;
31095
+ const url = str5(asObj4(n.attributes).url) ?? "";
30469
31096
  const h = hostOf(url);
30470
31097
  const merged = /\{\{|%7b/i.test(url.split(/[/?#]/).slice(0, 3).join("/")) || h !== void 0 && /[{}]|%7b|%7d/i.test(h);
30471
31098
  const problem = merged ? "merge_field" : !h ? "no_address" : void 0;
30472
- out.push({ ...problem ? { problem } : { host: h }, stepName: str4(n.name) ?? "Webhook", workflowName: s.name });
31099
+ out.push({ ...problem ? { problem } : { host: h }, stepName: str5(n.name) ?? "Webhook", workflowName: s.name });
30473
31100
  }
30474
31101
  }
30475
31102
  return out;
30476
31103
  }
30477
31104
  function planCopy(src, opts) {
30478
31105
  const allowed = opts.allowedHosts.map(normalizeHost).filter(Boolean);
30479
- const nodes = src.templates.map(asObj3);
31106
+ const nodes = src.templates.map(asObj4);
30480
31107
  if (nodes.length === 0) return { ok: false, refusal: `"${src.name}" has no steps to test.` };
30481
31108
  if (nodes.length > 40) return { ok: false, refusal: `"${src.name}" has ${nodes.length} steps; fast-forward copies at most 40.` };
30482
- if (str4(src.status)?.toLowerCase() === "draft") return { ok: false, refusal: `"${src.name}" is still a draft, so real leads never go through it. Publish it first, or test it as it is.` };
31109
+ if (str5(src.status)?.toLowerCase() === "draft") return { ok: false, refusal: `"${src.name}" is still a draft, so real leads never go through it. Publish it first, or test it as it is.` };
30483
31110
  const idMap = /* @__PURE__ */ new Map();
30484
31111
  for (const n of nodes) {
30485
- const id = str4(n.id);
30486
- if (id) idMap.set(id, import_node_crypto7.default.randomUUID());
31112
+ const id = str5(n.id);
31113
+ if (id) idMap.set(id, import_node_crypto8.default.randomUUID());
30487
31114
  }
30488
31115
  const remap = (id) => typeof id === "string" ? idMap.get(id) ?? id : id;
30489
31116
  const waits = [];
30490
31117
  const hosts = /* @__PURE__ */ new Set();
30491
31118
  const actions = [];
30492
31119
  for (const n of nodes) {
30493
- const type = str4(n.type) ?? "";
30494
- const name = str4(n.name) ?? type;
30495
- if (type === "if_else" || str4(n.nodeType)?.startsWith("branch") || str4(n.cat) === "conditions") {
31120
+ const type = str5(n.type) ?? "";
31121
+ const name = str5(n.name) ?? type;
31122
+ if (type === "if_else" || str5(n.nodeType)?.startsWith("branch") || str5(n.cat) === "conditions") {
30496
31123
  return { ok: false, refusal: `"${src.name}" has an if/else branch ("${name}"). Fast-forward handles straight-line workflows for now; branches come next, with test leads that book, reply or stay silent.` };
30497
31124
  }
30498
31125
  if (type === "add_to_workflow" || type === "goto") {
@@ -30501,9 +31128,9 @@ function planCopy(src, opts) {
30501
31128
  if (!ALLOWED.has(type)) {
30502
31129
  return { ok: false, refusal: `"${src.name}" has a "${type}" step ("${name}") that fast-forward does not know how to copy safely yet, so nothing was copied.` };
30503
31130
  }
30504
- const attrs = { ...asObj3(n.attributes) };
31131
+ const attrs = { ...asObj4(n.attributes) };
30505
31132
  if (type === "wait") {
30506
- if (str4(attrs.type) !== "time" || !asObj3(attrs.startAfter).type) {
31133
+ if (str5(attrs.type) !== "time" || !asObj4(attrs.startAfter).type) {
30507
31134
  return { ok: false, refusal: `"${src.name}" has a wait that is not a plain time wait ("${name}"). Fast-forward handles time waits only for now.` };
30508
31135
  }
30509
31136
  waits.push(`${name}: ${waitLength(attrs)} in real life`);
@@ -30521,20 +31148,20 @@ function planCopy(src, opts) {
30521
31148
  Object.assign(attrs, { user_list: [o], traffic_split: "equally", traffic_weightage: { [o]: 1 }, traffic_index: [{ id: o, indexes: [1] }], total_index: 1 });
30522
31149
  }
30523
31150
  if (type === "internal_create_opportunity" || type === "internal_update_opportunity") {
30524
- const assigns = attrs.assignedTo !== void 0 || asArr2(attrs.__customInputFields__).map(asObj3).some((f) => /assign/i.test(str4(f.filterField) ?? ""));
31151
+ const assigns = attrs.assignedTo !== void 0 || asArr2(attrs.__customInputFields__).map(asObj4).some((f) => /assign/i.test(str5(f.filterField) ?? ""));
30525
31152
  if (assigns) {
30526
31153
  if (!opts.ownerUserId) return { ok: false, refusal: 'During a fast-forward check, cards are assigned to one person instead of your staff, but nobody was chosen and your email is not a user in this account. Choose who should get them (on the Workflow Audit Pro screen, "Staff alerts, tasks and assignments go to"; in chat, name the person).' };
30527
31154
  if (attrs.assignedTo !== void 0) attrs.assignedTo = opts.ownerUserId;
30528
- attrs.__customInputFields__ = asArr2(attrs.__customInputFields__).map(asObj3).map((f) => /assign/i.test(str4(f.filterField) ?? "") ? { ...f, value: opts.ownerUserId } : f);
31155
+ attrs.__customInputFields__ = asArr2(attrs.__customInputFields__).map(asObj4).map((f) => /assign/i.test(str5(f.filterField) ?? "") ? { ...f, value: opts.ownerUserId } : f);
30529
31156
  }
30530
31157
  }
30531
31158
  if (type === "task-notification") {
30532
31159
  if (!opts.ownerUserId) return { ok: false, refusal: 'During a fast-forward check, tasks go to one person instead of your staff, but nobody was chosen and your email is not a user in this account. Choose who should get them (on the Workflow Audit Pro screen, "Staff alerts, tasks and assignments go to"; in chat, name the person).' };
30533
31160
  attrs.assignedTo = opts.ownerUserId;
30534
- attrs.title = `TEST: ${str4(attrs.title) ?? ""}`.trim();
31161
+ attrs.title = `TEST: ${str5(attrs.title) ?? ""}`.trim();
30535
31162
  }
30536
31163
  if (type === "webhook") {
30537
- const url = str4(attrs.url) ?? "";
31164
+ const url = str5(attrs.url) ?? "";
30538
31165
  if (/^[^/]*\/\/[^/?#]*\{\{/.test(url) || /^\{\{/.test(url)) return { ok: false, refusal: `"${src.name}" has a webhook ("${name}") whose address is filled in from a merge field, so where the test lead would go cannot be checked. Nothing was copied.` };
30539
31166
  const h = hostOf(url);
30540
31167
  if (h && /[{}]|%7b|%7d/i.test(h)) return { ok: false, refusal: `"${src.name}" has a webhook ("${name}") whose address is filled in from a merge field, so where the test lead would go cannot be checked. Nothing was copied.` };
@@ -30551,7 +31178,7 @@ function planCopy(src, opts) {
30551
31178
  if (Array.isArray(n.sibling)) copy.sibling = n.sibling.map(remap);
30552
31179
  actions.push(copy);
30553
31180
  }
30554
- const w = asObj3(src.window);
31181
+ const w = asObj4(src.window);
30555
31182
  return {
30556
31183
  ok: true,
30557
31184
  plan: {
@@ -30605,12 +31232,12 @@ function openCopies(locationId2, base) {
30605
31232
  return [...byName.values()].filter((x) => !x.deleted).map(({ deleted: _d, ...x }) => x);
30606
31233
  }
30607
31234
  function toSource(id, raw) {
30608
- const wf = asObj3(raw);
31235
+ const wf = asObj4(raw);
30609
31236
  return {
30610
31237
  id,
30611
- name: str4(wf.name) ?? id,
30612
- status: str4(wf.status),
30613
- templates: asArr2(asObj3(wf.workflowData).templates),
31238
+ name: str5(wf.name) ?? id,
31239
+ status: str5(wf.status),
31240
+ templates: asArr2(asObj4(wf.workflowData).templates),
30614
31241
  triggers: asArr2(wf.triggers),
30615
31242
  stopOnResponse: typeof wf.stopOnResponse === "boolean" ? wf.stopOnResponse : void 0,
30616
31243
  allowMultiple: typeof wf.allowMultiple === "boolean" ? wf.allowMultiple : void 0,
@@ -30647,13 +31274,13 @@ async function makeCopies(admin, locationId2, sourceIds, opts) {
30647
31274
  const { copyId, copyName } = copies[i];
30648
31275
  try {
30649
31276
  await admin.save(copyId, { actions: remapWorkflows(p.actions, sourceToCopy), triggers: [], status: "draft", window: null, ...p.settings });
30650
- const back = asObj3(await admin.get(copyId));
30651
- const steps = asArr2(asObj3(back.workflowData).templates).map(asObj3);
30652
- const longWait = steps.find((s) => str4(s.type) === "wait" && Number(asObj3(asObj3(s.attributes).startAfter).value) !== 1);
31277
+ const back = asObj4(await admin.get(copyId));
31278
+ const steps = asArr2(asObj4(back.workflowData).templates).map(asObj4);
31279
+ const longWait = steps.find((s) => str5(s.type) === "wait" && Number(asObj4(asObj4(s.attributes).startAfter).value) !== 1);
30653
31280
  if (asArr2(back.triggers).length !== 0) throw new Error(`the copy of "${p.sourceName}" came back with a trigger, so it was not switched on.`);
30654
31281
  if (steps.length !== p.actions.length) throw new Error(`the copy of "${p.sourceName}" came back with ${steps.length} steps instead of ${p.actions.length}.`);
30655
31282
  if (longWait) throw new Error(`a wait in the copy of "${p.sourceName}" did not shorten.`);
30656
- if (!(str4(back.name) ?? "").startsWith(tokenPrefix(copyName))) throw new Error("the copy's name did not save.");
31283
+ if (!(str5(back.name) ?? "").startsWith(tokenPrefix(copyName))) throw new Error("the copy's name did not save.");
30657
31284
  const stray = steps.flatMap(staffRecipients).find((u) => u !== opts.ownerUserId);
30658
31285
  if (stray) throw new Error(`a staff step in the copy of "${p.sourceName}" still reaches ${stray} instead of the person chosen, so it was not switched on.`);
30659
31286
  if (steps.some((s) => sourceIds.some((id) => JSON.stringify(s).includes(`"${id}"`)))) throw new Error(`a step in the copy of "${p.sourceName}" still points at an original workflow, so it was not switched on.`);
@@ -30669,8 +31296,8 @@ async function removeCopies(admin, locationId2, copies, base, now = Date.now) {
30669
31296
  for (const c of copies) {
30670
31297
  if (!c.copyId) continue;
30671
31298
  try {
30672
- const wf = asObj3(await admin.get(c.copyId));
30673
- const name = str4(wf.name) ?? "";
31299
+ const wf = asObj4(await admin.get(c.copyId));
31300
+ const name = str5(wf.name) ?? "";
30674
31301
  const ours = TOKEN_NAME.test(c.name) && (c.matchedByName ? name === c.name : name.startsWith(tokenPrefix(c.name)));
30675
31302
  if (!ours) {
30676
31303
  problems.push(`Left alone: workflow ${c.copyId} is not a fast-forward copy this check made (named "${name}").`);
@@ -30717,18 +31344,18 @@ function builderAdmin(b) {
30717
31344
  list: async () => {
30718
31345
  const out = [];
30719
31346
  for (let skip = 0; skip < 1e3; skip += 100) {
30720
- const rows2 = asArr2(asObj3(await b.listWorkflows(100, skip)).rows).map(asObj3);
31347
+ const rows2 = asArr2(asObj4(await b.listWorkflows(100, skip)).rows).map(asObj4);
30721
31348
  for (const r of rows2) {
30722
- const id = str4(r._id) ?? str4(r.id);
30723
- if (id) out.push({ id, name: str4(r.name) ?? "", createdAt: str4(r.createdAt) });
31349
+ const id = str5(r._id) ?? str5(r.id);
31350
+ if (id) out.push({ id, name: str5(r.name) ?? "", createdAt: str5(r.createdAt) });
30724
31351
  }
30725
31352
  if (rows2.length < 100) break;
30726
31353
  }
30727
31354
  return out;
30728
31355
  },
30729
31356
  create: async (name) => {
30730
- const w = asObj3(await b.createWorkflow(name));
30731
- return { id: str4(w._id) ?? str4(w.id) ?? "" };
31357
+ const w = asObj4(await b.createWorkflow(name));
31358
+ return { id: str5(w._id) ?? str5(w.id) ?? "" };
30732
31359
  },
30733
31360
  save: (id, body) => b.updateWorkflow(id, body),
30734
31361
  publish: (id) => b.publishWorkflow(id),
@@ -30736,17 +31363,17 @@ function builderAdmin(b) {
30736
31363
  remove: (id, ack) => b.deleteWorkflow(id, ack ? { acknowledgeContacts: ack } : void 0)
30737
31364
  };
30738
31365
  }
30739
- var import_node_fs2, import_node_path2, import_node_crypto7, asObj3, asArr2, str4, FF_PREFIX, ALLOWED, hostOf, normalizeHost, newToken, TOKEN_NAME, tokenPrefix, DAY_NAMES, remapSelf, journalFile, SWEEP_MIN_AGE_MS;
31366
+ var import_node_fs2, import_node_path2, import_node_crypto8, asObj4, asArr2, str5, FF_PREFIX, ALLOWED, hostOf, normalizeHost, newToken, TOKEN_NAME, tokenPrefix, DAY_NAMES, remapSelf, journalFile, SWEEP_MIN_AGE_MS;
30740
31367
  var init_fastforward = __esm({
30741
31368
  "src/command-os/shopper/fastforward.ts"() {
30742
31369
  "use strict";
30743
31370
  import_node_fs2 = __toESM(require("node:fs"));
30744
31371
  import_node_path2 = __toESM(require("node:path"));
30745
- import_node_crypto7 = __toESM(require("node:crypto"));
31372
+ import_node_crypto8 = __toESM(require("node:crypto"));
30746
31373
  init_run();
30747
- asObj3 = (v) => v && typeof v === "object" ? v : {};
31374
+ asObj4 = (v) => v && typeof v === "object" ? v : {};
30748
31375
  asArr2 = (v) => Array.isArray(v) ? v : [];
30749
- str4 = (v) => typeof v === "string" ? v : void 0;
31376
+ str5 = (v) => typeof v === "string" ? v : void 0;
30750
31377
  FF_PREFIX = "ZZ TEST FF";
30751
31378
  ALLOWED = /* @__PURE__ */ new Set([
30752
31379
  "email",
@@ -30778,7 +31405,7 @@ var init_fastforward = __esm({
30778
31405
  };
30779
31406
  newToken = () => {
30780
31407
  let t = "";
30781
- while (t.length < 6) t += (import_node_crypto7.default.randomBytes(1)[0] % 36).toString(36);
31408
+ while (t.length < 6) t += (import_node_crypto8.default.randomBytes(1)[0] % 36).toString(36);
30782
31409
  return t;
30783
31410
  };
30784
31411
  TOKEN_NAME = new RegExp(`^${FF_PREFIX} [0-9a-z]{6} `);
@@ -30863,7 +31490,7 @@ function clampMinutes(n) {
30863
31490
  return Math.min(1440, Math.max(1, Math.round(n)));
30864
31491
  }
30865
31492
  function channelOf(msg3) {
30866
- const mt = (str5(msg3.messageType) ?? "").toUpperCase();
31493
+ const mt = (str6(msg3.messageType) ?? "").toUpperCase();
30867
31494
  if (mt === "TYPE_SMS") return "sms";
30868
31495
  if (mt === "TYPE_EMAIL") return "email";
30869
31496
  if (!mt) {
@@ -30874,30 +31501,30 @@ function channelOf(msg3) {
30874
31501
  return null;
30875
31502
  }
30876
31503
  async function readSentMessages(client, locationId2, contactId, since) {
30877
- const convs = asArr3(asObj4(await client.get("/conversations/search", { params: { locationId: locationId2, contactId, limit: 20 } })).conversations);
31504
+ const convs = asArr3(asObj5(await client.get("/conversations/search", { params: { locationId: locationId2, contactId, limit: 20 } })).conversations);
30878
31505
  const out = [];
30879
31506
  for (const c of convs) {
30880
- const convId = str5(asObj4(c).id);
31507
+ const convId = str6(asObj5(c).id);
30881
31508
  if (!convId) continue;
30882
- const raw = asObj4(await client.get(`/conversations/${convId}/messages`, { params: { limit: 100 } }));
30883
- const list3 = asArr3(raw.messages).length ? asArr3(raw.messages) : asArr3(asObj4(raw.messages).messages);
31509
+ const raw = asObj5(await client.get(`/conversations/${convId}/messages`, { params: { limit: 100 } }));
31510
+ const list3 = asArr3(raw.messages).length ? asArr3(raw.messages) : asArr3(asObj5(raw.messages).messages);
30884
31511
  for (const item of list3) {
30885
- const m = asObj4(item);
30886
- if (str5(m.direction)?.toLowerCase() !== "outbound") continue;
31512
+ const m = asObj5(item);
31513
+ if (str6(m.direction)?.toLowerCase() !== "outbound") continue;
30887
31514
  const channel = channelOf(m);
30888
31515
  if (!channel) continue;
30889
- const at = Date.parse(str5(m.dateAdded) ?? "");
31516
+ const at = Date.parse(str6(m.dateAdded) ?? "");
30890
31517
  if (Number.isNaN(at) || at < since - 5e3) continue;
30891
- const id = str5(m.id);
31518
+ const id = str6(m.id);
30892
31519
  if (!id) continue;
30893
31520
  out.push({
30894
31521
  id,
30895
31522
  channel,
30896
31523
  at,
30897
- subject: str5(asObj4(asObj4(m.meta).email).subject),
30898
- body: htmlToText(str5(m.body) ?? ""),
30899
- raw: str5(m.body) ?? "",
30900
- source: str5(m.source)
31524
+ subject: str6(asObj5(asObj5(m.meta).email).subject),
31525
+ body: htmlToText(str6(m.body) ?? ""),
31526
+ raw: str6(m.body) ?? "",
31527
+ source: str6(m.source)
30901
31528
  });
30902
31529
  }
30903
31530
  }
@@ -30905,16 +31532,16 @@ async function readSentMessages(client, locationId2, contactId, since) {
30905
31532
  }
30906
31533
  async function findContactIdByEmail(client, locationId2, email) {
30907
31534
  const raw = await client.get("/contacts/", { params: { locationId: locationId2, query: email, limit: 10 } });
30908
- for (const c of asArr3(asObj4(raw).contacts)) {
30909
- const co = asObj4(c);
30910
- if (str5(co.email)?.toLowerCase() === email) return str5(co.id);
31535
+ for (const c of asArr3(asObj5(raw).contacts)) {
31536
+ const co = asObj5(c);
31537
+ if (str6(co.email)?.toLowerCase() === email) return str6(co.id);
30911
31538
  }
30912
31539
  return void 0;
30913
31540
  }
30914
31541
  async function startShopper(input, deps) {
30915
31542
  const now = deps.now ?? Date.now;
30916
31543
  const sleep7 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
30917
- const token2 = import_node_crypto8.default.randomUUID().replace(/-/g, "").slice(0, 8);
31544
+ const token2 = import_node_crypto9.default.randomUUID().replace(/-/g, "").slice(0, 8);
30918
31545
  const leadEmail = plusAddress(input.ownerEmail, token2);
30919
31546
  if (!SAFE_ID.test(input.locationId)) throw new Error("That client id is not valid.");
30920
31547
  if (input.entry.kind === "workflows" && input.entry.workflowIds.length === 0) throw new Error("Choose at least one workflow for the test lead to enter.");
@@ -30928,9 +31555,9 @@ async function startShopper(input, deps) {
30928
31555
  const refused = protectedRefusalFor(input.locationId);
30929
31556
  if (refused) throw new Error(refused);
30930
31557
  }
30931
- const loc = asObj4(asObj4(await deps.client.get(`/locations/${input.locationId}`)).location);
30932
- const businessName = str5(loc.name) ?? "";
30933
- const websiteHost = siteHost(str5(loc.website));
31558
+ const loc = asObj5(asObj5(await deps.client.get(`/locations/${input.locationId}`)).location);
31559
+ const businessName = str6(loc.name) ?? "";
31560
+ const websiteHost = siteHost(str6(loc.website));
30934
31561
  const testPhone = input.testPhone?.trim() || void 0;
30935
31562
  let expect = input.expect ?? [];
30936
31563
  const planNote = { ...input.planNote ?? { source: expect.length ? "given" : "none" } };
@@ -30945,7 +31572,7 @@ async function startShopper(input, deps) {
30945
31572
  const watchMinutes = auto ? auto.minutes : clampMinutes(input.watchMinutes);
30946
31573
  const watchReason = auto ? auto.reason : `You chose ${watchMinutes} minute${watchMinutes === 1 ? "" : "s"}.`;
30947
31574
  const firstName = (input.firstName ?? "").trim();
30948
- const entryNote = input.fastForward?.length ? `Fast-forward: added to temporary copies of ${input.fastForward.length} workflow${input.fastForward.length === 1 ? "" : "s"} with every wait cut to 1 minute and every step kept; the originals were only read. Staff alerts, tasks and lead assignments came to you, marked TEST. The sign-up form itself was not submitted.` : input.entry.kind === "workflows" ? `Added straight to ${input.entry.workflowIds.length} workflow${input.entry.workflowIds.length === 1 ? "" : "s"}. The sign-up form itself was not submitted.` : input.entry.kind === "tag" ? `Created with the tag "${input.entry.tag}", which starts the workflows that listen for it. The sign-up form itself was not submitted.` : "Sent in through the lead-capture address, the way a form sends it.";
31575
+ const entryNote = input.fastForward?.length ? `Fast-forward: added to temporary copies of ${input.fastForward.length} workflow${input.fastForward.length === 1 ? "" : "s"} with every wait cut to 1 minute and every step kept; the originals were only read. Staff alerts, tasks and lead assignments came to you, marked TEST. The sign-up form itself was not submitted.` : input.entry.kind === "workflows" ? `Added straight to ${input.entry.workflowIds.length} workflow${input.entry.workflowIds.length === 1 ? "" : "s"}. The sign-up form itself was not submitted.` : input.entry.kind === "tag" ? `Sent with the tag "${input.entry.tag}", which starts the workflows that listen for it. The sign-up form itself was not submitted.` : "Sent in through the lead-capture address, the way a form sends it.";
30949
31576
  const run = {
30950
31577
  v: 1,
30951
31578
  runId: `${new Date(now()).toISOString().replace(/[-:T]/g, "").slice(0, 14)}-${token2}`,
@@ -30996,9 +31623,9 @@ async function startShopper(input, deps) {
30996
31623
  };
30997
31624
  if (firstName) body.firstName = firstName;
30998
31625
  if (testPhone) body.phone = testPhone;
30999
- const created = asObj4(await deps.client.post("/contacts/", { body, noRetry: true }));
31000
- run.contactId = str5(asObj4(created.contact).id) ?? str5(created.id);
31001
- if (!run.contactId) throw new Error("GoHighLevel did not return the new test lead's id.");
31626
+ const created = asObj5(await deps.client.post("/contacts/", { body, noRetry: true }));
31627
+ run.contactId = str6(asObj5(created.contact).id) ?? str6(created.id);
31628
+ if (!run.contactId) throw new Error("GoHighLevel did not return the test lead's id.");
31002
31629
  if (input.entry.kind === "workflows") {
31003
31630
  for (const wid of input.entry.workflowIds) {
31004
31631
  await deps.client.post(`/contacts/${run.contactId}/workflow/${wid}`, { body: {}, noRetry: true });
@@ -31011,41 +31638,38 @@ async function startShopper(input, deps) {
31011
31638
  run.status = "failed";
31012
31639
  run.error = e instanceof Error ? e.message : String(e);
31013
31640
  run.finishedAt = now();
31014
- if (run.contactId && !run.keepLead) {
31015
- run.cleanup = await removeOwnLead(deps.client, run);
31016
- } else if (!run.contactId) {
31017
- run.cleanup = { removed: false, detail: `If the test contact named "Secret Shopper" with the address ${leadEmail} appears in the account, delete it. It is the test lead from this check.` };
31018
- }
31641
+ run.cleanup = await listLeadCarriers(deps.client, run);
31019
31642
  saveRun(run, deps.base);
31020
31643
  return run;
31021
31644
  }
31022
31645
  }
31023
31646
  async function postJson(url, body) {
31024
- const r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(body) });
31647
+ const r = await guardedPost(url, JSON.stringify(body), { "Content-Type": "application/json", Accept: "application/json" });
31025
31648
  return r.status;
31026
31649
  }
31027
- async function removeOwnLead(client, run) {
31028
- const id = run.contactId;
31029
- if (!id) return { removed: false, detail: "No test lead was recorded for this check." };
31030
- let contact;
31650
+ async function listLeadCarriers(client, run) {
31651
+ const carriers = /* @__PURE__ */ new Map();
31652
+ let partial = false;
31031
31653
  try {
31032
- contact = asObj4(asObj4(await client.get(`/contacts/${id}`)).contact);
31033
- } catch (e) {
31034
- if (notFound(e)) return { removed: true, detail: "The test lead is already gone from the account." };
31035
- return { removed: false, detail: `The test lead ${run.leadEmail} could not be checked before removal (${e instanceof Error ? e.message.split("\n")[0] : String(e)}). Delete it in GoHighLevel.` };
31036
- }
31037
- const email = str5(contact.email)?.toLowerCase();
31038
- const added = Date.parse(str5(contact.dateAdded) ?? "");
31039
- if (email !== run.leadEmail || !Number.isNaN(added) && added < run.startedAt - 10 * 6e4) {
31040
- return { removed: false, detail: `Left alone on purpose: the contact this check ended up with is not the test lead it created (its address or creation time does not match ${run.leadEmail}). Look for the test contact named "Secret Shopper" in GoHighLevel.` };
31654
+ const paged = await readContactPages((path45, o) => client.get(path45, o), { locationId: run.locationId, query: run.leadEmail, limit: 20 });
31655
+ if (!paged || !paged.complete) partial = true;
31656
+ for (const c of paged?.rows ?? []) {
31657
+ const k = carrierOf(c);
31658
+ if (k && k.email === run.leadEmail) carriers.set(k.id, k);
31659
+ }
31660
+ } catch {
31661
+ partial = true;
31041
31662
  }
31042
- try {
31043
- await client.delete(`/contacts/${id}`);
31044
- } catch (e) {
31045
- if (notFound(e)) return { removed: true, detail: "The test lead is already gone from the account." };
31046
- return { removed: false, detail: `The test lead ${run.leadEmail} could not be removed (${e instanceof Error ? e.message.split("\n")[0] : String(e)}). Delete it in GoHighLevel.` };
31663
+ if (run.contactId && !carriers.has(run.contactId)) {
31664
+ try {
31665
+ const k = carrierOf(asObj5(await client.get(`/contacts/${run.contactId}`)).contact);
31666
+ if (k && k.email === run.leadEmail) carriers.set(k.id, k);
31667
+ } catch (e) {
31668
+ if (!notFound(e)) partial = true;
31669
+ }
31047
31670
  }
31048
- return { removed: true, detail: "The test lead was removed from the account after the check." };
31671
+ const contacts = [...carriers.values()];
31672
+ return { removed: false, detail: carrierText(contacts, partial), contacts, partial };
31049
31673
  }
31050
31674
  function pidAlive(pid) {
31051
31675
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -31059,7 +31683,7 @@ function pidAlive(pid) {
31059
31683
  function claimRun(locationId2, runId, base) {
31060
31684
  const lock = `${runPath(locationId2, runId, base)}.lock`;
31061
31685
  import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(lock), { recursive: true, mode: 448 });
31062
- const token2 = import_node_crypto8.default.randomUUID();
31686
+ const token2 = import_node_crypto9.default.randomUUID();
31063
31687
  const mine = JSON.stringify({ pid: process.pid, token: token2 });
31064
31688
  for (let attempt = 0; attempt < 3; attempt++) {
31065
31689
  try {
@@ -31113,7 +31737,7 @@ async function finishShopper(locationId2, runId, deps) {
31113
31737
  const t = now();
31114
31738
  const closed = t >= run.closesAt;
31115
31739
  const raw = await readSentMessages(deps.client, run.locationId, run.contactId, run.startedAt);
31116
- const fetcher = deps.fetcher ?? ((url, init) => fetch(url, init).then((r) => ({ status: r.status })));
31740
+ const fetcher = deps.fetcher ?? ((url, init) => guardedFetchLike(url, { headers: init.headers, signal: init.signal, redirect: "manual", statusOnly: true }).then((r) => ({ status: r.status, location: r.headers.get("location") })));
31117
31741
  const urls = [...new Set(raw.flatMap((m) => extractUrls(m.body, m.raw !== m.body ? m.raw : void 0)))];
31118
31742
  const linkResults = (await checkLinks(urls, run.websiteHost ? [run.websiteHost] : [], fetcher)).map((l) => ({ ...l, url: maskSecrets(l.url) }));
31119
31743
  const messages = raw.map((m) => ({ id: m.id, channel: m.channel, at: m.at, source: m.source, body: maskSecrets(m.body), subject: m.subject ? maskSecrets(m.subject) : void 0 }));
@@ -31151,7 +31775,7 @@ async function finishShopper(locationId2, runId, deps) {
31151
31775
  if (closed) {
31152
31776
  run.status = "done";
31153
31777
  run.finishedAt = t;
31154
- run.cleanup = run.keepLead ? { removed: false, detail: `Kept on request: the test lead ${run.leadEmail} is still in the account for you to look at. Delete it when you're done.` } : await removeOwnLead(deps.client, run);
31778
+ run.cleanup = await listLeadCarriers(deps.client, run);
31155
31779
  if (run.fastForward && deps.ffAdmin) {
31156
31780
  const { removeCopies: removeCopies2 } = await Promise.resolve().then(() => (init_fastforward(), fastforward_exports));
31157
31781
  const problems = await removeCopies2(deps.ffAdmin, run.locationId, run.fastForward.copies.map((c) => ({ copyId: c.copyId, name: c.copyName })), deps.base, now);
@@ -31189,13 +31813,13 @@ function summarizeRun(run) {
31189
31813
  if (run.status === "watching") return `So far: ${head4}${stepNote} Still watching until ${new Date(run.closesAt).toISOString()}.`;
31190
31814
  return gradeLine + head4 + stepNote;
31191
31815
  }
31192
- var import_node_fs3, import_node_path3, import_node_crypto8, MAX_EXTEND_MINUTES, asObj4, asArr3, str5, SAFE_ID, notFound;
31816
+ var import_node_fs3, import_node_path3, import_node_crypto9, MAX_EXTEND_MINUTES, asObj5, asArr3, str6, SAFE_ID, notFound;
31193
31817
  var init_run = __esm({
31194
31818
  "src/command-os/shopper/run.ts"() {
31195
31819
  "use strict";
31196
31820
  import_node_fs3 = __toESM(require("node:fs"));
31197
31821
  import_node_path3 = __toESM(require("node:path"));
31198
- import_node_crypto8 = __toESM(require("node:crypto"));
31822
+ import_node_crypto9 = __toESM(require("node:crypto"));
31199
31823
  init_credentials_store();
31200
31824
  init_analyze();
31201
31825
  init_links();
@@ -31203,10 +31827,12 @@ var init_run = __esm({
31203
31827
  init_account_rules();
31204
31828
  init_score();
31205
31829
  init_steps();
31830
+ init_net_guard();
31831
+ init_test_contacts();
31206
31832
  MAX_EXTEND_MINUTES = 10;
31207
- asObj4 = (v) => v && typeof v === "object" ? v : {};
31833
+ asObj5 = (v) => v && typeof v === "object" ? v : {};
31208
31834
  asArr3 = (v) => Array.isArray(v) ? v : [];
31209
- str5 = (v) => typeof v === "string" ? v : void 0;
31835
+ str6 = (v) => typeof v === "string" ? v : void 0;
31210
31836
  SAFE_ID = /^[A-Za-z0-9_-]{1,64}$/;
31211
31837
  notFound = (e) => {
31212
31838
  const m = e instanceof Error ? e.message : String(e);
@@ -31443,12 +32069,12 @@ mark{background:var(--surface-sunk);color:var(--bad);font-weight:700;border-radi
31443
32069
 
31444
32070
  // src/command-os/shopper/plan.ts
31445
32071
  function waitMinutes(node) {
31446
- const a = asObj5(node.attributes);
31447
- if (str6(a.type) && str6(a.type) !== "time") return null;
31448
- const s = asObj5(a.startAfter);
32072
+ const a = asObj6(node.attributes);
32073
+ if (str7(a.type) && str7(a.type) !== "time") return null;
32074
+ const s = asObj6(a.startAfter);
31449
32075
  const v = Number(s.value);
31450
32076
  if (!Number.isFinite(v) || v < 0) return null;
31451
- const unit = (str6(s.type) ?? "").toLowerCase();
32077
+ const unit = (str7(s.type) ?? "").toLowerCase();
31452
32078
  if (unit.startsWith("min")) return v;
31453
32079
  if (unit.startsWith("hour")) return v * 60;
31454
32080
  if (unit.startsWith("day")) return v * 1440;
@@ -31466,27 +32092,27 @@ function plannedMessages(workflows) {
31466
32092
  for (const wf of workflows) {
31467
32093
  const nodes = /* @__PURE__ */ new Map();
31468
32094
  for (const t of wf.templates) {
31469
- const n = asObj5(t);
31470
- const id = str6(n.id);
32095
+ const n = asObj6(t);
32096
+ const id = str7(n.id);
31471
32097
  if (id) nodes.set(id, n);
31472
32098
  }
31473
32099
  for (const node of nodes.values()) {
31474
- const type = str6(node.type);
32100
+ const type = str7(node.type);
31475
32101
  if (type !== "sms" && type !== "email") continue;
31476
32102
  let minutes = 0;
31477
- let conditional = Boolean(str6(node.parent) && nodes.get(str6(node.parent) ?? "") && isBranch(nodes.get(str6(node.parent) ?? "")));
32103
+ let conditional = Boolean(str7(node.parent) && nodes.get(str7(node.parent) ?? "") && isBranch(nodes.get(str7(node.parent) ?? "")));
31478
32104
  const seen = /* @__PURE__ */ new Set();
31479
32105
  const afterWaits = [];
31480
- let cur = str6(node.parentKey);
32106
+ let cur = str7(node.parentKey);
31481
32107
  while (cur && !conditional && !seen.has(cur)) {
31482
32108
  seen.add(cur);
31483
32109
  const p = nodes.get(cur);
31484
32110
  if (!p) break;
31485
- if (isBranch(p) || str6(p.parent) && isBranch(nodes.get(str6(p.parent) ?? "") ?? {})) {
32111
+ if (isBranch(p) || str7(p.parent) && isBranch(nodes.get(str7(p.parent) ?? "") ?? {})) {
31486
32112
  conditional = true;
31487
32113
  break;
31488
32114
  }
31489
- if (str6(p.type) === "wait") {
32115
+ if (str7(p.type) === "wait") {
31490
32116
  const m = waitMinutes(p);
31491
32117
  if (m === null) {
31492
32118
  conditional = true;
@@ -31495,20 +32121,20 @@ function plannedMessages(workflows) {
31495
32121
  minutes += m;
31496
32122
  afterWaits.push(cur);
31497
32123
  }
31498
- cur = str6(p.parentKey);
32124
+ cur = str7(p.parentKey);
31499
32125
  }
31500
32126
  if (conditional) {
31501
32127
  leftOut.conditional++;
31502
32128
  continue;
31503
32129
  }
31504
- const a = asObj5(node.attributes);
31505
- const key = type === "email" ? wordingKey(str6(a.subject) ?? "") || wordingKey(str6(a.html) ?? "") : wordingKey(str6(a.body) ?? "");
32130
+ const a = asObj6(node.attributes);
32131
+ const key = type === "email" ? wordingKey(str7(a.subject) ?? "") || wordingKey(str7(a.html) ?? "") : wordingKey(str7(a.body) ?? "");
31506
32132
  if (!key) {
31507
32133
  leftOut.noWording++;
31508
32134
  continue;
31509
32135
  }
31510
- const stepName = str6(node.name) || (type === "sms" ? "text" : "email");
31511
- expect.push({ label: `${wf.name}: ${stepName}`, channel: type, match: key, byMinute: minutes + 1, stepName, ...wf.id ? { workflowId: wf.id } : {}, ...str6(node.id) ? { stepId: str6(node.id) } : {}, afterWaits });
32136
+ const stepName = str7(node.name) || (type === "sms" ? "text" : "email");
32137
+ expect.push({ label: `${wf.name}: ${stepName}`, channel: type, match: key, byMinute: minutes + 1, stepName, ...wf.id ? { workflowId: wf.id } : {}, ...str7(node.id) ? { stepId: str7(node.id) } : {}, afterWaits });
31512
32138
  }
31513
32139
  }
31514
32140
  return { expect, leftOut };
@@ -31521,45 +32147,45 @@ async function planFromWorkflowIds(reader, ids) {
31521
32147
  const draftIds = [];
31522
32148
  for (const id of ids) {
31523
32149
  try {
31524
- const wf = asObj5(await reader.getWorkflow(id));
31525
- const name = str6(wf.name) ?? id;
32150
+ const wf = asObj6(await reader.getWorkflow(id));
32151
+ const name = str7(wf.name) ?? id;
31526
32152
  names[id] = name;
31527
- if (str6(wf.status)?.toLowerCase() === "draft") {
32153
+ if (str7(wf.status)?.toLowerCase() === "draft") {
31528
32154
  drafts.push(name);
31529
32155
  draftIds.push(id);
31530
32156
  }
31531
- workflows.push({ id, name, templates: asArr4(asObj5(wf.workflowData).templates) });
32157
+ workflows.push({ id, name, templates: asArr4(asObj6(wf.workflowData).templates) });
31532
32158
  } catch {
31533
32159
  unread.push(id);
31534
32160
  }
31535
32161
  }
31536
32162
  return { ...plannedMessages(workflows), unread, names, drafts, draftIds };
31537
32163
  }
31538
- var asObj5, asArr4, str6, isBranch;
32164
+ var asObj6, asArr4, str7, isBranch;
31539
32165
  var init_plan2 = __esm({
31540
32166
  "src/command-os/shopper/plan.ts"() {
31541
32167
  "use strict";
31542
32168
  init_links();
31543
- asObj5 = (v) => v && typeof v === "object" ? v : {};
32169
+ asObj6 = (v) => v && typeof v === "object" ? v : {};
31544
32170
  asArr4 = (v) => Array.isArray(v) ? v : [];
31545
- str6 = (v) => typeof v === "string" ? v : void 0;
31546
- isBranch = (n) => /^branch-/.test(str6(n.nodeType) ?? "") || str6(n.type) === "if_else";
32171
+ str7 = (v) => typeof v === "string" ? v : void 0;
32172
+ isBranch = (n) => /^branch-/.test(str7(n.nodeType) ?? "") || str7(n.type) === "if_else";
31547
32173
  }
31548
32174
  });
31549
32175
 
31550
32176
  // src/command-os/shopper/ff-start.ts
31551
32177
  async function ownerUserId(client, locationId2, ownerEmail) {
31552
- const raw = asObj6(await client.get("/users/", { params: { locationId: locationId2 } }));
32178
+ const raw = asObj7(await client.get("/users/", { params: { locationId: locationId2 } }));
31553
32179
  const want = baseEmail(ownerEmail);
31554
- for (const u of asArr5(raw.users).map(asObj6)) if (str7(u.email) && baseEmail(str7(u.email)) === want) return str7(u.id);
32180
+ for (const u of asArr5(raw.users).map(asObj7)) if (str8(u.email) && baseEmail(str8(u.email)) === want) return str8(u.id);
31555
32181
  return void 0;
31556
32182
  }
31557
32183
  async function accountUsers(client, locationId2) {
31558
- const raw = asObj6(await client.get("/users/", { params: { locationId: locationId2 } }));
31559
- return asArr5(raw.users).map(asObj6).filter((u) => str7(u.id) && !u.deleted).map((u) => ({
31560
- id: str7(u.id),
31561
- name: str7(u.name) ?? ([str7(u.firstName), str7(u.lastName)].filter(Boolean).join(" ") || str7(u.email) || str7(u.id)),
31562
- email: str7(u.email)
32184
+ const raw = asObj7(await client.get("/users/", { params: { locationId: locationId2 } }));
32185
+ return asArr5(raw.users).map(asObj7).filter((u) => str8(u.id) && !u.deleted).map((u) => ({
32186
+ id: str8(u.id),
32187
+ name: str8(u.name) ?? ([str8(u.firstName), str8(u.lastName)].filter(Boolean).join(" ") || str8(u.email) || str8(u.id)),
32188
+ email: str8(u.email)
31563
32189
  }));
31564
32190
  }
31565
32191
  async function sweepLeftovers(admin, locationId2, base, now) {
@@ -31609,16 +32235,16 @@ async function startFastForward(s) {
31609
32235
  return { ok: false, error: e instanceof Error ? e.message.split("\n")[0] : String(e) };
31610
32236
  }
31611
32237
  }
31612
- var asObj6, asArr5, str7, baseEmail;
32238
+ var asObj7, asArr5, str8, baseEmail;
31613
32239
  var init_ff_start = __esm({
31614
32240
  "src/command-os/shopper/ff-start.ts"() {
31615
32241
  "use strict";
31616
32242
  init_run();
31617
32243
  init_fastforward();
31618
32244
  init_plan2();
31619
- asObj6 = (v) => v && typeof v === "object" ? v : {};
32245
+ asObj7 = (v) => v && typeof v === "object" ? v : {};
31620
32246
  asArr5 = (v) => Array.isArray(v) ? v : [];
31621
- str7 = (v) => typeof v === "string" ? v : void 0;
32247
+ str8 = (v) => typeof v === "string" ? v : void 0;
31622
32248
  baseEmail = (e) => {
31623
32249
  const [l, d] = e.trim().toLowerCase().split("@");
31624
32250
  return `${(l ?? "").split("+")[0]}@${d ?? ""}`;
@@ -32402,7 +33028,7 @@ var init_page2 = __esm({
32402
33028
  nextStep = (c) => c.state === "proven" || c.id === "google-calendar" && c.setup === "needs-setup" ? "" : c.id === "google-calendar" ? "Next: press Connect and sign in with Google." : c.state === "saved-unproven" ? `Next: paste the ${keyWord(c.id)} again and press Connect, so we can check it works.` : `Next: paste your ${keyWord(c.id)} below and press Connect.`;
32403
33029
  chip3 = (c, needs) => c.readError ? { tone: "warn", word: "Could not read" } : c.state === "saved-unproven" ? { tone: "warn", word: "Not proven" } : c.state === "not-connected" ? { tone: "", word: "Not connected" } : needs ? { tone: "warn", word: "Needs you today" } : c.state === "proven" ? { tone: "ok", word: "Connected" } : { tone: "", word: "Not connected" };
32404
33030
  head = (c, chips) => `<div class="cds-panel-head"><div class="txt"><h2 class="cds-panel-title">${esc4(c.name)}</h2></div><div class="cds-row">${chips}</div></div>`;
32405
- how = (inner, id, hidden) => `<details class="how-it-works cds-details" id="instructions-${id}"${hidden ? " hidden" : ""}><summary>How this connection works</summary><div class="connector-instructions">${inner}</div></details>`;
33031
+ how = (inner, id, hidden2) => `<details class="how-it-works cds-details" id="instructions-${id}"${hidden2 ? " hidden" : ""}><summary>How this connection works</summary><div class="connector-instructions">${inner}</div></details>`;
32406
33032
  needsRow = (c, ask) => `<li><a class="ttl" href="#${c.id}">${esc4(c.name)}<span class="sub">${esc4(ask)}</span></a></li>`;
32407
33033
  }
32408
33034
  });
@@ -34142,7 +34768,7 @@ var init_client_status_placement = __esm({
34142
34768
  // src/build-contract.ts
34143
34769
  function contentHash(value) {
34144
34770
  const canonical2 = (v) => Array.isArray(v) ? v.map(canonical2) : v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().filter((k) => v[k] !== void 0).map((k) => [k, canonical2(v[k])])) : v;
34145
- return (0, import_node_crypto9.createHash)("sha256").update(JSON.stringify(canonical2(value)) ?? "null").digest("hex");
34771
+ return (0, import_node_crypto10.createHash)("sha256").update(JSON.stringify(canonical2(value)) ?? "null").digest("hex");
34146
34772
  }
34147
34773
  function assertBuildApproval(approval, inputHash) {
34148
34774
  if (!approval?.at) throw new Error("Approve building before running Build.");
@@ -34317,11 +34943,11 @@ function executablePlanLines(saved) {
34317
34943
  for (const w of preview?.workflows ?? []) for (const m of [...w.manual ?? [], ...w.needsContent ?? []]) lines.push(`Unresolved: ${w.name || names.get(w.ref) || "Unnamed workflow"}. ${m.reason}`);
34318
34944
  return lines.map(sentence);
34319
34945
  }
34320
- var import_node_crypto9, import_node_fs5, import_node_path5, INTAKE_CHANGED;
34946
+ var import_node_crypto10, import_node_fs5, import_node_path5, INTAKE_CHANGED;
34321
34947
  var init_build_contract = __esm({
34322
34948
  "src/build-contract.ts"() {
34323
34949
  "use strict";
34324
- import_node_crypto9 = require("node:crypto");
34950
+ import_node_crypto10 = require("node:crypto");
34325
34951
  import_node_fs5 = require("node:fs");
34326
34952
  import_node_path5 = require("node:path");
34327
34953
  init_credentials_store();
@@ -34400,7 +35026,7 @@ async function approvedStatusWriter(registry2, locationId2) {
34400
35026
  return saved;
34401
35027
  },
34402
35028
  async readPublishedPage(url) {
34403
- const result = await fetch(url, { signal: AbortSignal.timeout(15e3) });
35029
+ const result = await guardedFetchLike(url, { signal: AbortSignal.timeout(15e3) });
34404
35030
  if (!result.ok) throw new Error("The published handoff address could not be reached.");
34405
35031
  return result.text();
34406
35032
  }
@@ -34416,6 +35042,7 @@ var init_approved_status_writer = __esm({
34416
35042
  init_funnel_builder();
34417
35043
  init_client_status_placement();
34418
35044
  init_build_contract();
35045
+ init_net_guard();
34419
35046
  }
34420
35047
  });
34421
35048
 
@@ -34900,7 +35527,7 @@ function engineRepair(version) {
34900
35527
  return !version ? CLAUDE_REPAIR_MESSAGES[0] : !meetsFloor(version) ? CLAUDE_REPAIR_MESSAGES[1] : CLAUDE_REPAIR_MESSAGES[2];
34901
35528
  }
34902
35529
  function keyFingerprint(key) {
34903
- return (0, import_node_crypto10.createHash)("sha256").update(key).digest("hex");
35530
+ return (0, import_node_crypto11.createHash)("sha256").update(key).digest("hex");
34904
35531
  }
34905
35532
  async function clientPreflight(registry2, locationId2) {
34906
35533
  const lines = [];
@@ -34967,7 +35594,7 @@ async function agencyConnectionReady(registry2, companyId) {
34967
35594
  return false;
34968
35595
  }
34969
35596
  }
34970
- var import_node_crypto10, CLAUDE_REPAIR_MESSAGES;
35597
+ var import_node_crypto11, CLAUDE_REPAIR_MESSAGES;
34971
35598
  var init_client_preflight = __esm({
34972
35599
  "src/client-preflight.ts"() {
34973
35600
  "use strict";
@@ -34977,7 +35604,7 @@ var init_client_preflight = __esm({
34977
35604
  init_stage_runner();
34978
35605
  init_tool_filter();
34979
35606
  init_credentials_store();
34980
- import_node_crypto10 = require("node:crypto");
35607
+ import_node_crypto11 = require("node:crypto");
34981
35608
  CLAUDE_REPAIR_MESSAGES = [
34982
35609
  "Install Claude Code from Your agency before reading this source.",
34983
35610
  "Update Claude Code to version 2.1.0 or newer from Your agency.",
@@ -35020,25 +35647,25 @@ var init_brief_validation = __esm({
35020
35647
  // src/command-os/design/run-jobs.ts
35021
35648
  function validateRunRequest(input) {
35022
35649
  const a = input ?? {};
35023
- const str8 = (k) => typeof a[k] === "string" ? a[k].trim() : "";
35024
- const brand = str8("brand");
35650
+ const str9 = (k) => typeof a[k] === "string" ? a[k].trim() : "";
35651
+ const brand = str9("brand");
35025
35652
  if (!SLUG2.test(brand)) return { ok: false, error: "brand must be a stored brand's slug (lowercase letters, digits and dashes)." };
35026
- const medium = str8("medium");
35653
+ const medium = str9("medium");
35027
35654
  if (!MEDIA.includes(medium)) return { ok: false, error: `medium must be one of ${MEDIA.join(", ")}.` };
35028
- const headline3 = str8("headline");
35655
+ const headline3 = str9("headline");
35029
35656
  if (headline3.length < 3 || headline3.length > 300) return { ok: false, error: "headline must be between 3 and 300 characters." };
35030
35657
  const points = Array.isArray(a.points) ? a.points.filter((p) => typeof p === "string" && p.trim().length > 0).map((p) => p.trim()) : [];
35031
35658
  if (points.length === 0) return { ok: false, error: "points must be a non-empty list." };
35032
35659
  if (points.length > DESIGN_FORM_LIMITS.pointsMax) return { ok: false, error: POINTS_MESSAGE };
35033
35660
  if (points.some((p) => p.includes("|"))) return { ok: false, error: "a point may not contain the | character; it separates them on the command line." };
35034
35661
  if (points.some((p) => p.length > 400)) return { ok: false, error: "a point may be 400 characters at most." };
35035
- const model = str8("model");
35662
+ const model = str9("model");
35036
35663
  if (model && !MODEL.test(model)) return { ok: false, error: "model must be a plain engine name." };
35037
35664
  const slides = a.slides;
35038
35665
  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 };
35039
35666
  let refusal = null;
35040
35667
  const optional2 = (k, max = 400) => {
35041
- const v = str8(k);
35668
+ const v = str9(k);
35042
35669
  if (!v) return void 0;
35043
35670
  if (v.length > max) {
35044
35671
  if (refusal === null) refusal = `${k} must be ${max} characters at most.`;
@@ -35084,7 +35711,7 @@ function startDesignRun(request, opts = {}) {
35084
35711
  const checked = validateRunRequest(request);
35085
35712
  if (!checked.ok) throw new Error(checked.error);
35086
35713
  request = checked.value;
35087
- const id = (0, import_node_crypto11.randomUUID)();
35714
+ const id = (0, import_node_crypto12.randomUUID)();
35088
35715
  const outDir = (0, import_node_path7.join)(designRunsDir(), id);
35089
35716
  (0, import_node_fs7.mkdirSync)(outDir, { recursive: true });
35090
35717
  const job = { id, status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), outDir, request, tail: [] };
@@ -35151,14 +35778,14 @@ function cancelDesignRun(id) {
35151
35778
  }
35152
35779
  return true;
35153
35780
  }
35154
- var import_node_child_process3, import_node_fs7, import_node_path7, import_node_crypto11, SLUG2, MODEL, JOINED_FLAG, jobs, children;
35781
+ var import_node_child_process3, import_node_fs7, import_node_path7, import_node_crypto12, SLUG2, MODEL, JOINED_FLAG, jobs, children;
35155
35782
  var init_run_jobs = __esm({
35156
35783
  "src/command-os/design/run-jobs.ts"() {
35157
35784
  "use strict";
35158
35785
  import_node_child_process3 = require("node:child_process");
35159
35786
  import_node_fs7 = require("node:fs");
35160
35787
  import_node_path7 = require("node:path");
35161
- import_node_crypto11 = require("node:crypto");
35788
+ import_node_crypto12 = require("node:crypto");
35162
35789
  init_credentials_store();
35163
35790
  init_schema();
35164
35791
  init_brief_validation();
@@ -36037,9 +36664,9 @@ function presetForBusinessType(type) {
36037
36664
  return "generic";
36038
36665
  }
36039
36666
  }
36040
- function setPath(target, path44, value) {
36667
+ function setPath(target, path45, value) {
36041
36668
  if (value === void 0) return;
36042
- const parts = path44.split(".");
36669
+ const parts = path45.split(".");
36043
36670
  let node = target;
36044
36671
  for (let i = 0; i < parts.length - 1; i++) {
36045
36672
  const k = parts[i];
@@ -38162,9 +38789,9 @@ function recorderAllowPatterns(mcpListOutput) {
38162
38789
  function prefillTools(source, recorderPatterns = []) {
38163
38790
  const deny = "Bash,Write,Edit,MultiEdit,NotebookEdit,Task";
38164
38791
  if (source.kind === "text") return { allow: "", deny: deny + ",WebFetch,WebSearch,Read,Glob,Grep" };
38165
- if (source.kind === "url") return { allow: "WebFetch", deny: deny + ",WebSearch,Read,Glob,Grep" };
38792
+ if (source.kind === "url") return { allow: "", deny: deny + ",WebFetch,WebSearch,Read,Glob,Grep" };
38166
38793
  if (source.kind === "file") return { allow: "Read", deny: deny + ",WebFetch,WebSearch,Glob,Grep" };
38167
- return { allow: recorderPatterns.join(","), deny: deny + ",Read,Glob,Grep" };
38794
+ return { allow: recorderPatterns.join(","), deny: deny + ",WebFetch,WebSearch,Read,Glob,Grep" };
38168
38795
  }
38169
38796
  function sourcePreamble(source) {
38170
38797
  if (source.kind === "file") {
@@ -38211,6 +38838,7 @@ async function prefillFromSource(spawnClaude, labels, source, recorderPatterns =
38211
38838
  if (source.kind === "file" && !/[.](txt|md|csv|vtt|srt)$/i.test(source.name)) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "Use plain text, .vtt or .srt." };
38212
38839
  const empty = source.kind === "text" && !source.text.trim() || source.kind === "url" && !source.url.trim() || source.kind === "recorder" && !source.instruction.trim() || source.kind === "file" && !source.path.trim();
38213
38840
  if (empty) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "nothing to read" };
38841
+ if (source.kind === "url") return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: LINK_READING_OFF_MESSAGE };
38214
38842
  if (source.kind === "text" && source.text.length > 6e4) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "The limit is 60,000 characters. Paste an excerpt or shorten this text file." };
38215
38843
  const result = await runPrefill(() => spawnClaude(prefillPromptForSource(labels, source) + "\nQuestion schema (use only these choices; multiple choices use an array in the cited answer object\u2019s value):\n" + JSON.stringify(questions), prefillTools(source, recorderPatterns)), labels, source.kind === "text" ? source.text : void 0);
38216
38844
  result.answers = validatePrefillAnswers(questions, result.answers);
@@ -38269,7 +38897,7 @@ function validatePrefillAnswers(questions, proposed) {
38269
38897
  }
38270
38898
  return result;
38271
38899
  }
38272
- var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, RECORDER_HINTS;
38900
+ var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, RECORDER_HINTS, LINK_READING_OFF_MESSAGE;
38273
38901
  var init_client_intake = __esm({
38274
38902
  "src/client-intake.ts"() {
38275
38903
  "use strict";
@@ -38280,6 +38908,7 @@ var init_client_intake = __esm({
38280
38908
  INTAKE_FORM_NAME2 = "GHL Command Blueprint \u2014 Client Intake";
38281
38909
  INTAKE_CONTACT_TAG = "blueprint-intake";
38282
38910
  RECORDER_HINTS = ["plaud", "otter", "fireflies", "grain", "fathom", "tldv", "granola", "zoom", "read.ai", "readai", "supernormal", "avoma"];
38911
+ LINK_READING_OFF_MESSAGE = "Reading a link isn't available in this version. Open the page, copy its text, and paste it into Paste a transcript.";
38283
38912
  }
38284
38913
  });
38285
38914
 
@@ -38343,7 +38972,7 @@ var init_plan_store = __esm({
38343
38972
  // src/agency-profile-page.ts
38344
38973
  function agencyProfilePageHtml(p, clients = [], seatToken = "") {
38345
38974
  const offers = (p.offers ?? []).map((o) => `${o.name}${o.priceFloor ? ` - ${o.priceFloor}${o.cadence === "monthly" ? "/mo" : ""}` : ""}`).join("\n");
38346
- const os8 = p.agencyOs ?? {};
38975
+ const os9 = p.agencyOs ?? {};
38347
38976
  const lines = (xs) => (xs ?? []).join("\n");
38348
38977
  const sops = (p.delivery?.sops ?? []).join("\n");
38349
38978
  const chatOn = readAccountAdminSettings().chatCreation;
@@ -38481,36 +39110,36 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
38481
39110
  <div class="cds-panel-head"><div class="txt"><div class="cds-panel-label">Your offer and method</div><h2 class="cds-panel-title">Who you are, who you serve, what you promise, how you deliver</h2></div></div>
38482
39111
  <p>Filled in once, in your own words. Used on the assessment report, in proposals, in every Claude session's opening brief, and by every Command OS build.</p>
38483
39112
  <ul class="cds-list">
38484
- ${section2("Your brand: who you are", progress([os8.story, os8.method, os8.voice, os8.goals, os8.values, os8.proof]), `
38485
- ${area("story", "Your story: why this agency exists, in two or three sentences", os8.story, { placeholder: "I ran a clinic for twelve years and watched good practices lose patients to voicemail\u2026" })}
38486
- ${area("method", "Your method: how you get results, and what you call it", os8.method, { placeholder: "The Full Room method: one weekend, a full calendar, a system that keeps booking after we leave" })}
39113
+ ${section2("Your brand: who you are", progress([os9.story, os9.method, os9.voice, os9.goals, os9.values, os9.proof]), `
39114
+ ${area("story", "Your story: why this agency exists, in two or three sentences", os9.story, { placeholder: "I ran a clinic for twelve years and watched good practices lose patients to voicemail\u2026" })}
39115
+ ${area("method", "Your method: how you get results, and what you call it", os9.method, { placeholder: "The Full Room method: one weekend, a full calendar, a system that keeps booking after we leave" })}
38487
39116
  <div class="cds-grid">
38488
- ${area("voice", "How you sound: three words, then a sentence", os8.voice, { placeholder: "Direct, warm, clinical. Specifics over hype." })}
38489
- ${area("goals", "Your goal for the next twelve months", os8.goals, { placeholder: "$50k a month from 51 subscribers" })}
39117
+ ${area("voice", "How you sound: three words, then a sentence", os9.voice, { placeholder: "Direct, warm, clinical. Specifics over hype." })}
39118
+ ${area("goals", "Your goal for the next twelve months", os9.goals, { placeholder: "$50k a month from 51 subscribers" })}
38490
39119
  </div>
38491
- ${area("values", "What you stand for and what you refuse to do: one per line", lines(os8.values), { placeholder: "Never a false all-clear&#10;No cold texting anyone who did not opt in" })}
38492
- ${area("proof", "Proof you may state, with permission: one per line", lines(os8.proof), { placeholder: "Radiance Med Spa: 34 consults booked in one weekend (permission on file)" })}`)}
38493
- ${section2("Ideal client: who you serve", progress([os8.ica?.who, os8.ica?.pains, os8.ica?.desires, os8.ica?.objections, os8.ica?.triggers, os8.ica?.vocabulary, os8.ica?.hangouts, os8.ica?.villains]), `
38494
- ${area("icaWho", "Who they are: role, business, size, where", os8.ica?.who, { placeholder: "The owner of a two-provider med spa in a mid-size US city, doing $40k a month and answering the phone herself" })}
39120
+ ${area("values", "What you stand for and what you refuse to do: one per line", lines(os9.values), { placeholder: "Never a false all-clear&#10;No cold texting anyone who did not opt in" })}
39121
+ ${area("proof", "Proof you may state, with permission: one per line", lines(os9.proof), { placeholder: "Radiance Med Spa: 34 consults booked in one weekend (permission on file)" })}`)}
39122
+ ${section2("Ideal client: who you serve", progress([os9.ica?.who, os9.ica?.pains, os9.ica?.desires, os9.ica?.objections, os9.ica?.triggers, os9.ica?.vocabulary, os9.ica?.hangouts, os9.ica?.villains]), `
39123
+ ${area("icaWho", "Who they are: role, business, size, where", os9.ica?.who, { placeholder: "The owner of a two-provider med spa in a mid-size US city, doing $40k a month and answering the phone herself" })}
38495
39124
  <div class="cds-grid">
38496
- ${area("icaPains", "Their pains, worst first: one per line", lines(os8.ica?.pains))}
38497
- ${area("icaDesires", "What they want, most first: one per line", lines(os8.ica?.desires))}
38498
- ${area("icaObjections", "Why they say no: one per line", lines(os8.ica?.objections))}
38499
- ${area("icaTriggers", "What makes them buy now: one per line", lines(os8.ica?.triggers))}
38500
- ${area("icaVocabulary", "Their own words for the problem: one per line", lines(os8.ica?.vocabulary))}
38501
- ${area("icaHangouts", "Where they read, listen and gather: one per line", lines(os8.ica?.hangouts))}
39125
+ ${area("icaPains", "Their pains, worst first: one per line", lines(os9.ica?.pains))}
39126
+ ${area("icaDesires", "What they want, most first: one per line", lines(os9.ica?.desires))}
39127
+ ${area("icaObjections", "Why they say no: one per line", lines(os9.ica?.objections))}
39128
+ ${area("icaTriggers", "What makes them buy now: one per line", lines(os9.ica?.triggers))}
39129
+ ${area("icaVocabulary", "Their own words for the problem: one per line", lines(os9.ica?.vocabulary))}
39130
+ ${area("icaHangouts", "Where they read, listen and gather: one per line", lines(os9.ica?.hangouts))}
38502
39131
  </div>
38503
- ${area("icaVillains", "What they have tried and blame: one per line", lines(os8.ica?.villains), { placeholder: "The last agency that charged $3,000 a month for two posts a week" })}`)}
38504
- ${section2("The offer: what you promise", progress([os8.offer?.name, os8.offer?.price, os8.offer?.promise, os8.offer?.valueStack, os8.offer?.guarantee, os8.offer?.accelerators]), `
39132
+ ${area("icaVillains", "What they have tried and blame: one per line", lines(os9.ica?.villains), { placeholder: "The last agency that charged $3,000 a month for two posts a week" })}`)}
39133
+ ${section2("The offer: what you promise", progress([os9.offer?.name, os9.offer?.price, os9.offer?.promise, os9.offer?.valueStack, os9.offer?.guarantee, os9.offer?.accelerators]), `
38505
39134
  <div class="cds-grid">
38506
- ${input("offerName", "The offer's name", os8.offer?.name, { placeholder: "The Full Room" })}
38507
- ${input("offerPrice", "Price, and what it is per", os8.offer?.price, { placeholder: "$5,000 per event plus 30% of revenue" })}
39135
+ ${input("offerName", "The offer's name", os9.offer?.name, { placeholder: "The Full Room" })}
39136
+ ${input("offerPrice", "Price, and what it is per", os9.offer?.price, { placeholder: "$5,000 per event plus 30% of revenue" })}
38508
39137
  </div>
38509
- ${area("offerPromise", "The promise: one sentence a prospect could repeat", os8.offer?.promise, { placeholder: "A full room in one weekend, and a system that keeps booking after we leave" })}
38510
- ${area("offerStack", "What is in it: one per line", lines(os8.offer?.valueStack), { placeholder: "The campaign page&#10;The AI receptionist&#10;The 30-day nurture" })}
39138
+ ${area("offerPromise", "The promise: one sentence a prospect could repeat", os9.offer?.promise, { placeholder: "A full room in one weekend, and a system that keeps booking after we leave" })}
39139
+ ${area("offerStack", "What is in it: one per line", lines(os9.offer?.valueStack), { placeholder: "The campaign page&#10;The AI receptionist&#10;The 30-day nurture" })}
38511
39140
  <div class="cds-grid">
38512
- ${area("offerGuarantee", "The guarantee, exactly as you would say it", os8.offer?.guarantee)}
38513
- ${area("offerAccelerators", "Why now: bonuses, deadlines, scarcity, one per line", lines(os8.offer?.accelerators))}
39141
+ ${area("offerGuarantee", "The guarantee, exactly as you would say it", os9.offer?.guarantee)}
39142
+ ${area("offerAccelerators", "Why now: bonuses, deadlines, scarcity, one per line", lines(os9.offer?.accelerators))}
38514
39143
  </div>`)}
38515
39144
  ${section2("How you deliver", progress([p.delivery?.sops, p.staff]), `
38516
39145
  ${area("sops", "Delivery SOPs: one per line", sops, { placeholder: "Every build gets a rollback journal before go-live" })}
@@ -40417,7 +41046,7 @@ function writeVerifiedBackup(configPath, originalBytes) {
40417
41046
  return backupPath;
40418
41047
  }
40419
41048
  function sha256(bytes) {
40420
- return (0, import_node_crypto12.createHash)("sha256").update(bytes).digest("hex");
41049
+ return (0, import_node_crypto13.createHash)("sha256").update(bytes).digest("hex");
40421
41050
  }
40422
41051
  function baselineOf(bytes, stat) {
40423
41052
  return { size: stat.size, mtimeMs: stat.mtimeMs, hash: sha256(bytes) };
@@ -40443,7 +41072,7 @@ function assertNotStale(configPath, baseline) {
40443
41072
  }
40444
41073
  async function atomicReplace(opts) {
40445
41074
  const dir = path31.dirname(opts.configPath);
40446
- const tempPath = path31.join(dir, `.${path31.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto12.randomBytes)(4).toString("hex")}`);
41075
+ const tempPath = path31.join(dir, `.${path31.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto13.randomBytes)(4).toString("hex")}`);
40447
41076
  const fd = fs33.openSync(tempPath, "w");
40448
41077
  try {
40449
41078
  fs33.writeFileSync(fd, opts.newBytes);
@@ -40593,14 +41222,14 @@ async function runInstall(argv, ioOverride) {
40593
41222
  return EXIT_ABORTED;
40594
41223
  }
40595
41224
  }
40596
- var fs33, os6, path31, import_node_crypto12, import_node_util, import_json5, SERVER_KEY, DESIRED_ENTRY, EXIT_OK, EXIT_USAGE, EXIT_REFUSED, EXIT_ABORTED, RENAME_RETRY_DELAYS_MS, LOCK_ERROR_CODES, BOM_UTF8, InstallStop;
41225
+ var fs33, os6, path31, import_node_crypto13, import_node_util, import_json5, SERVER_KEY, DESIRED_ENTRY, EXIT_OK, EXIT_USAGE, EXIT_REFUSED, EXIT_ABORTED, RENAME_RETRY_DELAYS_MS, LOCK_ERROR_CODES, BOM_UTF8, InstallStop;
40597
41226
  var init_config_installer = __esm({
40598
41227
  "src/config-installer.ts"() {
40599
41228
  "use strict";
40600
41229
  fs33 = __toESM(require("node:fs"));
40601
41230
  os6 = __toESM(require("node:os"));
40602
41231
  path31 = __toESM(require("node:path"));
40603
- import_node_crypto12 = require("node:crypto");
41232
+ import_node_crypto13 = require("node:crypto");
40604
41233
  import_node_util = require("node:util");
40605
41234
  import_json5 = __toESM(require("json5"));
40606
41235
  init_npx_cache_check();
@@ -41295,71 +41924,122 @@ for(const s of document.querySelectorAll("[data-ink]")){const v=s.dataset.ink;if
41295
41924
  }
41296
41925
  });
41297
41926
 
41298
- // src/command-os/design/browser.ts
41299
- async function launchBrowser(opts = {}) {
41927
+ // src/command-os/prospect/guarded-browser.ts
41928
+ async function openBrowser2() {
41300
41929
  let chromium;
41301
41930
  try {
41302
41931
  ({ chromium } = await import("playwright-core"));
41303
41932
  } catch (e) {
41304
- return { ok: false, reason: `playwright-core could not be loaded: ${e.message}` };
41933
+ return { reason: `the browser library could not be loaded (${e.message.split("\n")[0]})` };
41305
41934
  }
41306
- const profile = opts.profileDir ?? chromeProfileDir();
41307
- const channels = ["chrome", "msedge"];
41935
+ let proxy;
41936
+ try {
41937
+ proxy = await startGuardProxy();
41938
+ } catch (e) {
41939
+ return { reason: `the browser's network gatekeeper could not start (${e.message.split("\n")[0]})` };
41940
+ }
41941
+ const args = ["--disable-quic", "--force-webrtc-ip-handling-policy=disable_non_proxied_udp", "--webrtc-ip-handling-policy=disable_non_proxied_udp", "--disable-features=IsolateOrigins,site-per-process", "--disable-site-isolation-trials", "--proxy-bypass-list=<-loopback>", "--no-pings", "--disable-background-networking"];
41308
41942
  let last = "";
41309
- for (const channel of channels) {
41943
+ for (const channel of ["chrome", "msedge"]) {
41310
41944
  try {
41311
- const context = await chromium.launchPersistentContext(profile, {
41312
- channel,
41313
- headless: opts.headless ?? true,
41314
- viewport: { width: 1440, height: 900 },
41315
- deviceScaleFactor: 1,
41316
- args: ["--hide-scrollbars"]
41317
- });
41318
- return { ok: true, browser: { context, close: () => context.close().catch(() => void 0) } };
41945
+ return { browser: await chromium.launch({ channel, headless: true, chromiumSandbox: true, args, proxy: { server: proxy.url, bypass: "<-loopback>", username: proxy.username, password: proxy.password } }), proxy };
41319
41946
  } catch (e) {
41320
- last = e.message;
41947
+ last = e.message.split("\n")[0];
41321
41948
  }
41322
41949
  }
41323
- return { ok: false, reason: `No Chrome or Edge could be launched on the automation profile (${last.split("\n")[0]}). If the cockpit's capture is running it holds the profile; close it and retry.` };
41950
+ await proxy.close();
41951
+ return { reason: `Chrome or Edge could not be opened on this computer (${last})` };
41324
41952
  }
41325
- async function evaluateSource(page, source, arg) {
41326
- const fn = new Function("arg", `return (${source})(arg);`);
41327
- return await page.evaluate(fn, arg);
41953
+ async function openGuardedBrowser(viewport = { width: 1440, height: 900 }) {
41954
+ const opened = await openBrowser2().catch((e) => ({ reason: e.message }));
41955
+ if ("reason" in opened) return { ok: false, reason: opened.reason };
41956
+ const { browser, proxy } = opened;
41957
+ try {
41958
+ const context = await browser.newContext({ viewport, serviceWorkers: "block", acceptDownloads: false, proxy: { server: proxy.url, username: proxy.username, password: proxy.password } });
41959
+ await context.route("**/*", async (route) => {
41960
+ const m = route.request().method();
41961
+ if (m !== "GET" && m !== "HEAD") {
41962
+ await route.abort("blockedbyclient").catch(() => void 0);
41963
+ return;
41964
+ }
41965
+ await route.continue().catch(() => void 0);
41966
+ });
41967
+ await context.routeWebSocket(/.*/, (ws) => ws.close({ code: 1008, reason: "read-only" }));
41968
+ context.on("page", (p) => {
41969
+ if (context.pages().length > 1) void p.close().catch(() => void 0);
41970
+ });
41971
+ return { ok: true, context, close: () => closeQuietly(browser, proxy) };
41972
+ } catch (e) {
41973
+ await closeQuietly(browser, proxy);
41974
+ return { ok: false, reason: e.message.split("\n")[0] };
41975
+ }
41328
41976
  }
41329
- var init_browser = __esm({
41330
- "src/command-os/design/browser.ts"() {
41977
+ async function closeQuietly(browser, proxy) {
41978
+ await Promise.race([
41979
+ (async () => {
41980
+ await browser.close().catch(() => void 0);
41981
+ await proxy.close().catch(() => void 0);
41982
+ })(),
41983
+ new Promise((r) => setTimeout(r, 1e4))
41984
+ ]);
41985
+ void proxy.close().catch(() => void 0);
41986
+ }
41987
+ var init_guarded_browser = __esm({
41988
+ "src/command-os/prospect/guarded-browser.ts"() {
41331
41989
  "use strict";
41332
- init_interactive_capture();
41990
+ init_net_guard();
41333
41991
  }
41334
41992
  });
41335
41993
 
41336
41994
  // src/command-os/design/extract-browser.ts
41337
41995
  async function browserPass(url, opts = {}) {
41338
- const launched = await launchBrowser({ headless: true });
41339
- if (!launched.ok) return launched;
41340
- const { context, close } = launched.browser;
41341
- try {
41342
- const page = context.pages()[0] ?? await context.newPage();
41996
+ const navMs = opts.timeoutMs ?? 3e4;
41997
+ const deadlineMs = navMs * 2 + 2e4;
41998
+ let timer;
41999
+ let expiredAt = false;
42000
+ let closeOnce;
42001
+ const expired = new Promise((resolve13) => {
42002
+ timer = setTimeout(() => {
42003
+ expiredAt = true;
42004
+ resolve13({ ok: false, reason: `The rendered pass took longer than ${Math.round(deadlineMs / 1e3)} seconds and was stopped.` });
42005
+ }, deadlineMs);
42006
+ });
42007
+ const work = (async () => {
42008
+ const problem = await publicAddressProblem(url);
42009
+ if (problem && problem !== "the address does not resolve") return { ok: false, reason: `The rendered pass did not open ${url}: ${problem}.` };
42010
+ const launched = await openGuardedBrowser({ width: 1440, height: 900 });
42011
+ if (!launched.ok) return { ok: false, reason: `The rendered pass could not start a guarded browser: ${launched.reason}` };
42012
+ let closing;
42013
+ closeOnce = () => closing ??= launched.close();
42014
+ if (expiredAt) {
42015
+ await closeOnce();
42016
+ return { ok: false, reason: "The rendered pass was stopped at its deadline." };
42017
+ }
42018
+ const page = await launched.context.newPage();
41343
42019
  await page.setViewportSize({ width: 1440, height: 900 });
41344
- await page.goto(url, { waitUntil: "networkidle", timeout: opts.timeoutMs ?? 3e4 }).catch(async () => {
41345
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: opts.timeoutMs ?? 3e4 });
42020
+ await page.goto(url, { waitUntil: "networkidle", timeout: navMs }).catch(async () => {
42021
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: navMs });
41346
42022
  });
41347
42023
  await page.waitForTimeout(800);
41348
42024
  const facts = await page.evaluate(BROWSER_FACTS_JS);
41349
42025
  const out = { ...facts, logoSvg: facts.logoSvg ?? void 0, logoSrc: facts.logoSrc ?? void 0 };
41350
42026
  if (opts.screenshot !== false) out.screenshotPng = await page.screenshot({ type: "png", fullPage: false });
41351
42027
  return { ok: true, facts: out };
41352
- } catch (e) {
41353
- return { ok: false, reason: `The rendered pass failed: ${e.message.split("\n")[0]}` };
42028
+ })().catch((e) => ({ ok: false, reason: `The rendered pass failed: ${e.message.split("\n")[0]}` }));
42029
+ try {
42030
+ return await Promise.race([work, expired]);
41354
42031
  } finally {
41355
- await close();
42032
+ clearTimeout(timer);
42033
+ expiredAt = true;
42034
+ if (closeOnce) await closeOnce();
41356
42035
  }
41357
42036
  }
41358
42037
  var BROWSER_FACTS_JS;
41359
42038
  var init_extract_browser = __esm({
41360
42039
  "src/command-os/design/extract-browser.ts"() {
41361
42040
  "use strict";
41362
- init_browser();
42041
+ init_guarded_browser();
42042
+ init_net_guard();
41363
42043
  BROWSER_FACTS_JS = `(() => {
41364
42044
  const cs = (el) => el ? getComputedStyle(el) : null;
41365
42045
  const rgbOf = (v) => (v && v !== "rgba(0, 0, 0, 0)" && v !== "transparent") ? v : null;
@@ -42855,7 +43535,7 @@ function registerAssessmentTools(server2, client) {
42855
43535
  const dir = args.outDir ?? (0, import_node_path8.join)((0, import_node_os.homedir)(), "Documents", "assessments");
42856
43536
  (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
42857
43537
  const slug3 = (state.biz || "assessment").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
42858
- const file2 = (0, import_node_path8.join)(dir, `${slug3}-${(0, import_node_crypto13.randomBytes)(4).toString("hex")}.html`);
43538
+ const file2 = (0, import_node_path8.join)(dir, `${slug3}-${(0, import_node_crypto14.randomBytes)(4).toString("hex")}.html`);
42859
43539
  (0, import_node_fs8.writeFileSync)(file2, html2, "utf8");
42860
43540
  return {
42861
43541
  written: file2,
@@ -42878,7 +43558,7 @@ function registerAssessmentTools(server2, client) {
42878
43558
  }
42879
43559
  );
42880
43560
  }
42881
- var import_node_util3, import_zod61, import_node_crypto13, import_node_fs8, import_node_os, import_node_path8, QUESTION_UNITS, AREA_TITLE, ASK, AREA_OF, ASSUMPTION_LABEL, LOOKUP, StateSchema;
43561
+ var import_node_util3, import_zod61, import_node_crypto14, import_node_fs8, import_node_os, import_node_path8, QUESTION_UNITS, AREA_TITLE, ASK, AREA_OF, ASSUMPTION_LABEL, LOOKUP, StateSchema;
42882
43562
  var init_assessment = __esm({
42883
43563
  "src/tools/assessment.ts"() {
42884
43564
  "use strict";
@@ -42891,7 +43571,7 @@ var init_assessment = __esm({
42891
43571
  init_store3();
42892
43572
  init_engine();
42893
43573
  init_report();
42894
- import_node_crypto13 = require("node:crypto");
43574
+ import_node_crypto14 = require("node:crypto");
42895
43575
  import_node_fs8 = require("node:fs");
42896
43576
  import_node_os = require("node:os");
42897
43577
  import_node_path8 = require("node:path");
@@ -43439,7 +44119,7 @@ function notify(message) {
43439
44119
  }
43440
44120
  }
43441
44121
  }
43442
- function openBrowser2(port) {
44122
+ function openBrowser3(port) {
43443
44123
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
43444
44124
  const openArgs = process.platform === "win32" ? ["/c", "start", "", `http://localhost:${port}`] : [`http://localhost:${port}`];
43445
44125
  try {
@@ -43952,8 +44632,9 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
43952
44632
  note.textContent="Command OS cannot verify this account\u2019s sender. This button records a manual task to confirm the sender and send the questionnaire in GoHighLevel.";w.appendChild(note);box.appendChild(w)}
43953
44633
  if(mode==="transcript"){const w=document.createElement("div");w.className="cds-stack";
43954
44634
  const tabs=document.createElement("div");tabs.className="cds-tabs ichoice";tabs.setAttribute("role","group");tabs.setAttribute("aria-label","Where the material comes from");
43955
- iSub[id]=iSub[id]||"paste";
43956
- for(const [k,lab] of [["paste","Paste a transcript"],["doc","A text document"],["url","From a link"],["recorder","From my recorder"]]){
44635
+ /* "From a link" is off in 3.88.3 (the browser/fetch inventory, 2026-10-09): the tab is gone and a remembered choice falls back to Paste. */
44636
+ if(!iSub[id]||iSub[id]==="url")iSub[id]="paste";
44637
+ for(const [k,lab] of [["paste","Paste a transcript"],["doc","A text document"],["recorder","From my recorder"]]){
43957
44638
  const tb=document.createElement("button");tb.className="cds-tab";tb.textContent=lab;if(iSub[id]===k){tb.className="cds-tab on";tb.setAttribute("aria-current","page")}
43958
44639
  tb.onclick=()=>{iSub[id]=k;render(lastState,lastRun)};tabs.appendChild(tb)}
43959
44640
  w.appendChild(tabs);
@@ -45111,7 +45792,7 @@ function cockpitHeader(req, name) {
45111
45792
  return Array.isArray(value) ? value.join(",") : value ?? "";
45112
45793
  }
45113
45794
  function headerFields(req, name) {
45114
- if (req instanceof http.IncomingMessage || "rawHeaders" in req) {
45795
+ if (req instanceof http2.IncomingMessage || "rawHeaders" in req) {
45115
45796
  const raw = req.rawHeaders;
45116
45797
  if (!Array.isArray(raw)) return [];
45117
45798
  const values = [];
@@ -45308,7 +45989,7 @@ async function runDashboard(argv) {
45308
45989
  return 0;
45309
45990
  }
45310
45991
  if (await cockpitAlreadyRunning(port)) {
45311
- openBrowser2(port);
45992
+ openBrowser3(port);
45312
45993
  process.stderr.write(`
45313
45994
  Command OS is already running. Opening localhost:${port}
45314
45995
 
@@ -45652,7 +46333,7 @@ async function runDashboard(argv) {
45652
46333
  };
45653
46334
  const connectionsRoutes = createConnectionsRoutes(seatToken, port, gate2.plan);
45654
46335
  const briefRoutes = createBriefRoutes(seatToken, port, gate2.plan, registry2);
45655
- const server2 = http.createServer(guarded(seatToken, port, (req, res) => {
46336
+ const server2 = http2.createServer(guarded(seatToken, port, (req, res) => {
45656
46337
  const url = cockpitPath(req);
45657
46338
  if (connectionsRoutes(req, res)) return;
45658
46339
  if (briefRoutes(req, res)) return;
@@ -46224,6 +46905,11 @@ ${opportunityNoteFailure?.detail ?? ""}`);
46224
46905
  return;
46225
46906
  }
46226
46907
  if (url === "/api/intake-prefill") {
46908
+ if (args.url && !args.file) {
46909
+ res.writeHead(200, { "Content-Type": "application/json" });
46910
+ res.end(JSON.stringify({ ok: false, answers: {}, unknown: args.labels ?? [], error: LINK_READING_OFF_MESSAGE }));
46911
+ return;
46912
+ }
46227
46913
  let bin;
46228
46914
  try {
46229
46915
  bin = claudeBin();
@@ -46529,7 +47215,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
46529
47215
  const address = new URL(String(args.url || ""));
46530
47216
  if (!["http:", "https:"].includes(address.protocol)) throw new Error("Enter the public website address.");
46531
47217
  const before = readStatusRecord(locationId2);
46532
- const response = await fetch(address.href, { signal: AbortSignal.timeout(15e3) });
47218
+ const response = await guardedFetchLike(address.href, { signal: AbortSignal.timeout(15e3) });
46533
47219
  const html2 = await response.text();
46534
47220
  const now = readStatusRecord(locationId2);
46535
47221
  if (now.preparedStamp !== before.preparedStamp || !response.ok || !verifyPublicationContent(html2, before.preparedStamp || "", before.preparedSentences || [])) throw new Error("Prepared, not yet published. This address does not show the latest handoff. Publish the prepared page, then verify it again.");
@@ -46561,7 +47247,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
46561
47247
  if (stale.length) throw new Error(stale[0].reason);
46562
47248
  const publication = readStatusRecord(locationId2);
46563
47249
  if (!publication.lastPublishedAt || publication.lastError || !publication.lastUrl) throw new Error("The handoff is not yet published. Publish it and verify the public address before sending.");
46564
- const response = await fetch(publication.lastUrl, { signal: AbortSignal.timeout(15e3) });
47250
+ const response = await guardedFetchLike(publication.lastUrl, { signal: AbortSignal.timeout(15e3) });
46565
47251
  const html2 = await response.text();
46566
47252
  const snapshot = readHandoffSnapshot(locationId2);
46567
47253
  if (!response.ok || !snapshot?.lines.length || !verifyPublicationContent(html2, publication.preparedStamp || "", publication.preparedSentences || [])) throw new Error("The public address does not show the current handoff. Publish the latest page before sending.");
@@ -47080,7 +47766,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
47080
47766
  ${roster.length} client${roster.length === 1 ? "" : "s"} on the board. Ctrl-C to stop.
47081
47767
 
47082
47768
  `);
47083
- if (!argv.includes("--no-open")) openBrowser2(port);
47769
+ if (!argv.includes("--no-open")) openBrowser3(port);
47084
47770
  {
47085
47771
  const { state: fixed, recovered } = recoverStuckStages(readCockpitState());
47086
47772
  if (recovered) {
@@ -47099,7 +47785,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
47099
47785
  process.on("SIGTERM", stop2);
47100
47786
  });
47101
47787
  }
47102
- var import_zod62, import_node_util4, fs37, path35, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest;
47788
+ var import_zod62, import_node_util4, fs37, path35, http2, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest;
47103
47789
  var init_dashboard = __esm({
47104
47790
  "src/dashboard.ts"() {
47105
47791
  "use strict";
@@ -47138,7 +47824,7 @@ var init_dashboard = __esm({
47138
47824
  init_build_obligations();
47139
47825
  fs37 = __toESM(require("fs"));
47140
47826
  path35 = __toESM(require("path"));
47141
- http = __toESM(require("http"));
47827
+ http2 = __toESM(require("http"));
47142
47828
  import_child_process4 = require("child_process");
47143
47829
  init_credentials_store();
47144
47830
  init_attestation();
@@ -47200,6 +47886,7 @@ var init_dashboard = __esm({
47200
47886
  osmod = __toESM(require("os"));
47201
47887
  init_script_json();
47202
47888
  init_svg_sanitize();
47889
+ init_net_guard();
47203
47890
  STAGES2 = [
47204
47891
  "Intake",
47205
47892
  "Brief",
@@ -49006,13 +49693,13 @@ function contactServiceNote(payload) {
49006
49693
  function registerSmartListTools(server2, builderClient) {
49007
49694
  if (!builderClient) return;
49008
49695
  const client = builderClient;
49009
- async function smartListRequest(method, path44, body) {
49010
- const url = `${SMARTLIST_BASE}${path44}`;
49696
+ async function smartListRequest(method, path45, body) {
49697
+ const url = `${SMARTLIST_BASE}${path45}`;
49011
49698
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
49012
49699
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
49013
49700
  if (!response.ok) {
49014
49701
  const text3 = await response.text();
49015
- throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path44}
49702
+ throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path45}
49016
49703
  ${text3}`);
49017
49704
  }
49018
49705
  const text2 = await response.text();
@@ -49257,11 +49944,11 @@ var init_smart_lists = __esm({
49257
49944
  function registerReputationTools(server2, builderClient) {
49258
49945
  if (!builderClient) return;
49259
49946
  const client = builderClient;
49260
- async function reputationRequest(method, path44) {
49261
- const response = await builderRequest(client, method, `${REPUTATION_BASE}${path44}`);
49947
+ async function reputationRequest(method, path45) {
49948
+ const response = await builderRequest(client, method, `${REPUTATION_BASE}${path45}`);
49262
49949
  if (!response.ok) {
49263
49950
  const text3 = await response.text();
49264
- throw new Error(`Reputation API Error ${response.status}: ${method} ${path44}
49951
+ throw new Error(`Reputation API Error ${response.status}: ${method} ${path45}
49265
49952
  ${text3}`);
49266
49953
  }
49267
49954
  const text2 = await response.text();
@@ -49399,11 +50086,11 @@ var init_email_campaigns = __esm({
49399
50086
  function registerMembershipTools(server2, builderClient) {
49400
50087
  if (!builderClient) return;
49401
50088
  const client = builderClient;
49402
- async function membershipRequest(path44, method = "GET", body) {
49403
- const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path44}`, body ? { body } : {});
50089
+ async function membershipRequest(path45, method = "GET", body) {
50090
+ const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path45}`, body ? { body } : {});
49404
50091
  if (!response.ok) {
49405
50092
  const text3 = await response.text();
49406
- throw new Error(`Membership API Error ${response.status}: ${method} ${path44}
50093
+ throw new Error(`Membership API Error ${response.status}: ${method} ${path45}
49407
50094
  ${text3}`);
49408
50095
  }
49409
50096
  const text2 = await response.text();
@@ -50422,10 +51109,10 @@ async function confirm(run) {
50422
51109
  async function runProbes(client, loc) {
50423
51110
  return Promise.all(
50424
51111
  PROBES.map((probe) => confirm(async () => {
50425
- const path44 = probe.path.replace("{loc}", loc);
51112
+ const path45 = probe.path.replace("{loc}", loc);
50426
51113
  const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
50427
51114
  try {
50428
- const raw = await client.get(path44, { params, noRetry: true });
51115
+ const raw = await client.get(path45, { params, noRetry: true });
50429
51116
  const count2 = countRows(raw, probe.listKeys);
50430
51117
  return {
50431
51118
  key: probe.key,
@@ -51243,6 +51930,161 @@ var init_probe = __esm({
51243
51930
  }
51244
51931
  });
51245
51932
 
51933
+ // src/command-os/design/browser.ts
51934
+ async function evaluateSource(page, source, arg) {
51935
+ const fn = new Function("arg", `return (${source})(arg);`);
51936
+ return await page.evaluate(fn, arg);
51937
+ }
51938
+ var init_browser = __esm({
51939
+ "src/command-os/design/browser.ts"() {
51940
+ "use strict";
51941
+ init_interactive_capture();
51942
+ }
51943
+ });
51944
+
51945
+ // src/command-os/design/local-target.ts
51946
+ function realDir(p) {
51947
+ if (!p || !p.trim()) return null;
51948
+ try {
51949
+ const r = fs40.realpathSync(path38.resolve(p));
51950
+ return fs40.statSync(r).isDirectory() ? r : null;
51951
+ } catch {
51952
+ return null;
51953
+ }
51954
+ }
51955
+ function within(root, p) {
51956
+ return p === root || p.startsWith(root.endsWith(path38.sep) ? root : root + path38.sep);
51957
+ }
51958
+ function safeDesignRoot(cwd) {
51959
+ const r = realDir(cwd);
51960
+ if (!r) return null;
51961
+ const home = realDir(os7.homedir());
51962
+ if (r === path38.parse(r).root) return null;
51963
+ if (home && (r === home || within(r, home))) return null;
51964
+ const depth = (p) => path38.relative(path38.parse(p).root, p).split(path38.sep).filter(Boolean).length;
51965
+ if ([path38.resolve(cwd), r].some((p) => depth(p) < 2)) return null;
51966
+ if (SYSTEM_DIRS.some((d) => within(d, r))) return null;
51967
+ if (windowsSystemDirs().some((d) => within(d, r))) return null;
51968
+ const own = home ? ["Documents", "Downloads", "Desktop", "Movies", "Music", "Pictures", "Public", "OneDrive"].map((n) => path38.join(home, n)) : [];
51969
+ const shared = ["/Users/Shared", "/tmp", "/private/tmp", "/var/tmp", "/private/var/tmp", "/var/folders", "/private/var/folders", realDir(os7.tmpdir()) ?? os7.tmpdir()];
51970
+ if ([...own, ...shared].some((d) => r === d || r === realDir(d))) return null;
51971
+ const appData = home ? [path38.join(home, "Library"), path38.join(home, "AppData"), process.env.APPDATA, process.env.LOCALAPPDATA] : [];
51972
+ for (const d of appData) if (d && (within(realDir(d) ?? path38.resolve(d), r) || within(path38.resolve(d), r))) return null;
51973
+ if (home && within(home, r) && path38.relative(home, r).split(path38.sep)[0].startsWith(".")) return null;
51974
+ return r;
51975
+ }
51976
+ function windowsSystemDirs() {
51977
+ if (process.platform !== "win32") return [];
51978
+ return [process.env.SystemRoot, process.env.ProgramFiles, process.env["ProgramFiles(x86)"], process.env.ProgramData].filter((d) => !!d).map((d) => realDir(d) ?? path38.resolve(d));
51979
+ }
51980
+ function designRoots(env2 = process.env, cwd = process.cwd()) {
51981
+ const out = [];
51982
+ const named = env2[DESIGN_DIR_ENV]?.trim() ? safeDesignRoot(env2[DESIGN_DIR_ENV]) : null;
51983
+ const moved = env2[BRANDS_ENV]?.trim();
51984
+ const brands = moved ? safeDesignRoot(moved) : realDir(brandsDir());
51985
+ for (const r of [named, safeDesignRoot(cwd), brands]) if (r && !out.includes(r)) out.push(r);
51986
+ return out;
51987
+ }
51988
+ function urlFor(root, file2) {
51989
+ return LOCAL_ORIGIN + "/" + path38.relative(root, file2).split(path38.sep).map(encodeURIComponent).join("/");
51990
+ }
51991
+ function hidden(rel) {
51992
+ return rel.split(path38.sep).some((s) => s.startsWith("."));
51993
+ }
51994
+ function checkLocalTarget(target, roots = designRoots()) {
51995
+ const abs = path38.resolve(target);
51996
+ let file2;
51997
+ try {
51998
+ file2 = fs40.realpathSync(abs);
51999
+ } catch {
52000
+ throw new Error(`No file at ${abs}.`);
52001
+ }
52002
+ const ext = path38.extname(file2).toLowerCase();
52003
+ if (ext !== ".html" && ext !== ".htm") throw new Error(`verify_design renders .html files only; ${abs} is not one.`);
52004
+ const st = fs40.statSync(file2);
52005
+ if (!st.isFile()) throw new Error(`${abs} is not a file.`);
52006
+ if (st.size > MAX_PAGE_BYTES) throw new Error(`${abs} is larger than 10 MB, so it is not rendered.`);
52007
+ const root = roots.find((r) => within(r, file2) && !hidden(path38.relative(r, file2)));
52008
+ if (!root) {
52009
+ const where = roots.length ? roots.join(", ") : "none is set up on this computer";
52010
+ throw new Error(`verify_design only renders pages inside the design folders (${where}); ${abs} is outside them. Move the page into your project or brand folder, or set ${DESIGN_DIR_ENV} to the folder that holds it.`);
52011
+ }
52012
+ return { file: file2, root, url: urlFor(root, file2), html: fs40.readFileSync(file2, "utf8") };
52013
+ }
52014
+ function localFileFor(root, raw) {
52015
+ let u;
52016
+ try {
52017
+ u = new URL(raw);
52018
+ } catch {
52019
+ return null;
52020
+ }
52021
+ if (u.origin !== LOCAL_ORIGIN || u.username || u.password) return null;
52022
+ const segs = [];
52023
+ for (const part of u.pathname.split("/").slice(1)) {
52024
+ let s;
52025
+ try {
52026
+ s = decodeURIComponent(part);
52027
+ } catch {
52028
+ return null;
52029
+ }
52030
+ if (!s || s.startsWith(".") || /[\\/\0]/.test(s) || s.includes(":")) return null;
52031
+ segs.push(s);
52032
+ }
52033
+ if (!segs.length) return null;
52034
+ let file2;
52035
+ try {
52036
+ file2 = fs40.realpathSync(path38.join(root, ...segs));
52037
+ } catch {
52038
+ return null;
52039
+ }
52040
+ if (!within(root, file2) || file2 === root || hidden(path38.relative(root, file2))) return null;
52041
+ const contentType = SERVED[path38.extname(file2).toLowerCase()];
52042
+ if (!contentType) return null;
52043
+ try {
52044
+ const st = fs40.statSync(file2);
52045
+ if (!st.isFile() || st.size > MAX_FILE_BYTES) return null;
52046
+ } catch {
52047
+ return null;
52048
+ }
52049
+ return { file: file2, contentType };
52050
+ }
52051
+ var fs40, os7, path38, DESIGN_DIR_ENV, LOCAL_ORIGIN, MAX_PAGE_BYTES, MAX_FILE_BYTES, SERVED, SYSTEM_DIRS;
52052
+ var init_local_target = __esm({
52053
+ "src/command-os/design/local-target.ts"() {
52054
+ "use strict";
52055
+ fs40 = __toESM(require("node:fs"));
52056
+ os7 = __toESM(require("node:os"));
52057
+ path38 = __toESM(require("node:path"));
52058
+ init_store();
52059
+ DESIGN_DIR_ENV = "GHL_MCP_DESIGN_DIR";
52060
+ LOCAL_ORIGIN = "http://verify.invalid";
52061
+ MAX_PAGE_BYTES = 10 * 1024 * 1024;
52062
+ MAX_FILE_BYTES = 25 * 1024 * 1024;
52063
+ SERVED = {
52064
+ ".html": "text/html; charset=utf-8",
52065
+ ".htm": "text/html; charset=utf-8",
52066
+ ".css": "text/css; charset=utf-8",
52067
+ ".js": "text/javascript; charset=utf-8",
52068
+ ".mjs": "text/javascript; charset=utf-8",
52069
+ ".png": "image/png",
52070
+ ".jpg": "image/jpeg",
52071
+ ".jpeg": "image/jpeg",
52072
+ ".gif": "image/gif",
52073
+ ".webp": "image/webp",
52074
+ ".avif": "image/avif",
52075
+ ".svg": "image/svg+xml",
52076
+ ".ico": "image/x-icon",
52077
+ ".woff": "font/woff",
52078
+ ".woff2": "font/woff2",
52079
+ ".ttf": "font/ttf",
52080
+ ".otf": "font/otf",
52081
+ ".mp4": "video/mp4",
52082
+ ".webm": "video/webm"
52083
+ };
52084
+ SYSTEM_DIRS = ["/etc", "/private/etc", "/usr", "/bin", "/sbin", "/System", "/Library", "/opt", "/dev", "/proc", "/sys"];
52085
+ }
52086
+ });
52087
+
51246
52088
  // src/command-os/design/png.ts
51247
52089
  function decodePng(buf) {
51248
52090
  const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
@@ -51272,7 +52114,7 @@ function decodePng(buf) {
51272
52114
  if (interlace !== 0) throw new Error("interlaced PNG is not supported");
51273
52115
  const channels = colorType === 0 ? 1 : colorType === 2 ? 3 : colorType === 3 ? 1 : colorType === 4 ? 2 : colorType === 6 ? 4 : 0;
51274
52116
  if (!channels) throw new Error(`PNG colour type ${colorType} is not supported`);
51275
- const raw = (0, import_node_zlib.inflateSync)(Buffer.concat(idat));
52117
+ const raw = (0, import_node_zlib2.inflateSync)(Buffer.concat(idat));
51276
52118
  const bpp = channels;
51277
52119
  const stride = width * bpp;
51278
52120
  const out = new Uint8Array(width * height * 4);
@@ -51357,11 +52199,11 @@ function pixelAt(png, x, y) {
51357
52199
  const o = (yi * png.width + xi) * 4;
51358
52200
  return { r: png.rgba[o], g: png.rgba[o + 1], b: png.rgba[o + 2] };
51359
52201
  }
51360
- var import_node_zlib, SIG;
52202
+ var import_node_zlib2, SIG;
51361
52203
  var init_png = __esm({
51362
52204
  "src/command-os/design/png.ts"() {
51363
52205
  "use strict";
51364
- import_node_zlib = require("node:zlib");
52206
+ import_node_zlib2 = require("node:zlib");
51365
52207
  SIG = [137, 80, 78, 71, 13, 10, 26, 10];
51366
52208
  }
51367
52209
  });
@@ -52990,11 +53832,14 @@ async function settle(page) {
52990
53832
  await page.evaluate(() => document.fonts?.ready).catch(() => void 0);
52991
53833
  await page.waitForTimeout(250);
52992
53834
  }
52993
- async function readTarget(target) {
52994
- if (isUrl(target)) return { html: null, url: target };
52995
- const abs = path38.resolve(target);
52996
- if (!fs40.existsSync(abs)) throw new Error(`No file at ${abs}.`);
52997
- return { html: fs40.readFileSync(abs, "utf8"), url: (0, import_node_url.pathToFileURL)(abs).toString() };
53835
+ async function readTarget(target, roots) {
53836
+ if (isUrl(target)) {
53837
+ const problem = await publicAddressProblem(target);
53838
+ if (problem) throw new Error(`verify_design did not open ${target}: ${problem}.`);
53839
+ return { html: null, url: target, root: null };
53840
+ }
53841
+ const t = checkLocalTarget(target, roots);
53842
+ return { html: t.html, url: t.url, root: t.root };
52998
53843
  }
52999
53844
  function slidePixels(index, png, file2, sec, brand) {
53000
53845
  let img;
@@ -53038,11 +53883,35 @@ function slidePixels(index, png, file2, sec, brand) {
53038
53883
  });
53039
53884
  return { index, screenshot: file2, bgShare, accentShare, hueBuckets, overImage };
53040
53885
  }
53886
+ async function serveLocalPage(context, root, url, html2) {
53887
+ const notServed = { status: 404, contentType: "text/plain", body: "not served" };
53888
+ await context.route(`${LOCAL_ORIGIN}/**`, async (route) => {
53889
+ const req = route.request();
53890
+ if (req.method() !== "GET" && req.method() !== "HEAD") {
53891
+ await route.abort("blockedbyclient").catch(() => void 0);
53892
+ return;
53893
+ }
53894
+ if (req.url() === url) {
53895
+ await route.fulfill({ status: 200, contentType: "text/html; charset=utf-8", body: html2 }).catch(() => void 0);
53896
+ return;
53897
+ }
53898
+ const hit = localFileFor(root, req.url());
53899
+ let body = null;
53900
+ if (hit) {
53901
+ try {
53902
+ body = fs41.readFileSync(hit.file);
53903
+ } catch {
53904
+ body = null;
53905
+ }
53906
+ }
53907
+ await route.fulfill(hit && body ? { status: 200, contentType: hit.contentType, body } : notServed).catch(() => void 0);
53908
+ });
53909
+ }
53041
53910
  async function verifyDesign(opts) {
53042
- const { html: html2, url } = await readTarget(opts.target);
53911
+ const { html: html2, url, root } = await readTarget(opts.target, opts.roots);
53043
53912
  const placement = opts.placement ?? "html";
53044
53913
  const medium = opts.medium;
53045
- fs40.mkdirSync(opts.outDir, { recursive: true });
53914
+ fs41.mkdirSync(opts.outDir, { recursive: true });
53046
53915
  const files = [];
53047
53916
  const screenshots = [];
53048
53917
  const at = (/* @__PURE__ */ new Date()).toISOString();
@@ -53064,10 +53933,16 @@ async function verifyDesign(opts) {
53064
53933
  if (!conformance.ok) {
53065
53934
  rules = [];
53066
53935
  } else {
53067
- const launched = opts.noBrowser ? { ok: false, reason: "browser disabled for this run" } : await launchBrowser({ headless: opts.headless ?? true, profileDir: opts.profileDir });
53936
+ const launched = opts.noBrowser ? { ok: false, reason: "browser disabled for this run" } : await openGuardedBrowser();
53068
53937
  if (launched.ok) {
53069
- const { context, close } = launched.browser;
53070
- try {
53938
+ const { context, close } = launched;
53939
+ let timer;
53940
+ const expired = new Promise((_, reject) => {
53941
+ const limit = opts.deadlineMs ?? VERIFY_DEADLINE_MS;
53942
+ timer = setTimeout(() => reject(new Error(`verify_design stopped: the page did not finish rendering within ${Math.round(limit / 1e3)} seconds, so nothing was graded.`)), limit);
53943
+ });
53944
+ const work = (async () => {
53945
+ if (root !== null && html2 !== null) await serveLocalPage(context, root, url, html2);
53071
53946
  const page = context.pages()[0] ?? await context.newPage();
53072
53947
  if (medium === "deck") {
53073
53948
  await page.setViewportSize({ width: 1920, height: 1080 });
@@ -53087,8 +53962,8 @@ async function verifyDesign(opts) {
53087
53962
  try {
53088
53963
  await loc.scrollIntoViewIfNeeded({ timeout: 5e3 });
53089
53964
  const png = await loc.screenshot({ type: "png", timeout: 15e3 });
53090
- fs40.writeFileSync(path38.join(opts.outDir, file2), png);
53091
- files.push(path38.join(opts.outDir, file2));
53965
+ fs41.writeFileSync(path39.join(opts.outDir, file2), png);
53966
+ files.push(path39.join(opts.outDir, file2));
53092
53967
  screenshots.push(file2);
53093
53968
  pixels.push(slidePixels(sec.index, png, file2, sec, opts.brand));
53094
53969
  } catch {
@@ -53120,8 +53995,8 @@ async function verifyDesign(opts) {
53120
53995
  const shot = "page-1440.png";
53121
53996
  try {
53122
53997
  const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
53123
- fs40.writeFileSync(path38.join(opts.outDir, shot), png);
53124
- files.push(path38.join(opts.outDir, shot));
53998
+ fs41.writeFileSync(path39.join(opts.outDir, shot), png);
53999
+ files.push(path39.join(opts.outDir, shot));
53125
54000
  screenshots.push(shot);
53126
54001
  } catch {
53127
54002
  }
@@ -53129,8 +54004,8 @@ async function verifyDesign(opts) {
53129
54004
  await page.setViewportSize({ width: 390, height: 844 });
53130
54005
  await page.evaluate(SETTLE_SCROLL_JS).catch(() => void 0);
53131
54006
  const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
53132
- fs40.writeFileSync(path38.join(opts.outDir, "page-390.png"), png);
53133
- files.push(path38.join(opts.outDir, "page-390.png"));
54007
+ fs41.writeFileSync(path39.join(opts.outDir, "page-390.png"), png);
54008
+ files.push(path39.join(opts.outDir, "page-390.png"));
53134
54009
  screenshots.push("page-390.png");
53135
54010
  } catch {
53136
54011
  }
@@ -53149,7 +54024,12 @@ async function verifyDesign(opts) {
53149
54024
  rules.push(...checkConversion(conv, { brief: opts.conversion, gate: opts.gate ?? "ship", copyAxes: opts.copyAxes ?? null }, Boolean(conv)));
53150
54025
  }
53151
54026
  }
54027
+ })();
54028
+ work.catch(() => void 0);
54029
+ try {
54030
+ await Promise.race([work, expired]);
53152
54031
  } finally {
54032
+ clearTimeout(timer);
53153
54033
  await close();
53154
54034
  }
53155
54035
  } else {
@@ -53203,25 +54083,27 @@ async function verifyDesign(opts) {
53203
54083
  screenshots
53204
54084
  };
53205
54085
  const checked = VerificationSchema.parse(report3);
53206
- const jsonPath = path38.join(opts.outDir, "verification.json");
53207
- fs40.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
54086
+ const jsonPath = path39.join(opts.outDir, "verification.json");
54087
+ fs41.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
53208
54088
  files.push(jsonPath);
53209
54089
  const reportHtml = renderVerificationHtml(checked, opts.branding);
53210
- const htmlPath = path38.join(opts.outDir, "verification.html");
53211
- fs40.writeFileSync(htmlPath, reportHtml);
54090
+ const htmlPath = path39.join(opts.outDir, "verification.html");
54091
+ fs41.writeFileSync(htmlPath, reportHtml);
53212
54092
  files.push(htmlPath);
53213
54093
  return { report: checked, files, reportHtml };
53214
54094
  }
53215
- var fs40, path38, import_node_url, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS;
54095
+ var fs41, path39, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS, VERIFY_DEADLINE_MS;
53216
54096
  var init_verify = __esm({
53217
54097
  "src/command-os/design/verify.ts"() {
53218
54098
  "use strict";
53219
- fs40 = __toESM(require("node:fs"));
53220
- path38 = __toESM(require("node:path"));
53221
- import_node_url = require("node:url");
54099
+ fs41 = __toESM(require("node:fs"));
54100
+ path39 = __toESM(require("node:path"));
53222
54101
  init_lint();
53223
54102
  init_probe();
53224
54103
  init_browser();
54104
+ init_guarded_browser();
54105
+ init_net_guard();
54106
+ init_local_target();
53225
54107
  init_png();
53226
54108
  init_slides2();
53227
54109
  init_web2();
@@ -53259,6 +54141,7 @@ var init_verify = __esm({
53259
54141
  }
53260
54142
  return out;
53261
54143
  }`;
54144
+ VERIFY_DEADLINE_MS = 3e5;
53262
54145
  }
53263
54146
  });
53264
54147
 
@@ -53470,17 +54353,15 @@ Fix exactly those and reply with ONLY the complete corrected HTML in one \`\`\`h
53470
54353
  ${html2}
53471
54354
  \`\`\``;
53472
54355
  }
53473
- async function defaultVerify(file2, medium, brand, outDir, mode, headless = true, conversion) {
53474
- const { mkdtempSync } = await import("node:fs");
53475
- const { tmpdir: tmpdir2 } = await import("node:os");
54356
+ async function defaultVerify(file2, medium, brand, outDir, mode, conversion) {
54357
+ const own = safeDesignRoot((0, import_node_path9.dirname)(file2));
53476
54358
  const r = await verifyDesign({
53477
54359
  target: file2,
53478
54360
  medium,
53479
54361
  brand,
53480
54362
  outDir,
53481
54363
  mode,
53482
- headless,
53483
- profileDir: mkdtempSync((0, import_node_path9.join)(tmpdir2(), "ds-run-profile-")),
54364
+ roots: own ? [own, ...designRoots()] : designRoots(),
53484
54365
  ...conversion ? { pageMode: "conversion", conversion: conversion.brief, gate: conversion.gate } : {}
53485
54366
  });
53486
54367
  if (r.report.renderedWith !== "browser") {
@@ -53504,7 +54385,7 @@ async function runDesign(opts) {
53504
54385
  const maxPasses = Math.max(1, Math.min(3, opts.maxPasses ?? 3));
53505
54386
  const generate = opts.generate ?? claudeGenerate(opts.model ?? DEFAULT_MODEL);
53506
54387
  const conversionRun = brief.options.pageMode === "conversion" && brief.conversion ? { brief: brief.conversion, gate: opts.gate ?? "ship" } : void 0;
53507
- const verify = opts.verify ?? ((f, m, b, o, mode2) => defaultVerify(f, m, b, o, mode2, opts.headless ?? true, conversionRun));
54388
+ const verify = opts.verify ?? ((f, m, b, o, mode2) => defaultVerify(f, m, b, o, mode2, conversionRun));
53508
54389
  const mode = medium === "deck" ? brief.options.mode : void 0;
53509
54390
  const result = { ok: false, greenAt: null, passes: [], costUsd: 0, ms: 0, model: null, outDir, brandSlug: dna.slug, medium, stoppedBecause: "" };
53510
54391
  let instruction = `${pack.markdown}
@@ -53589,6 +54470,7 @@ var init_run2 = __esm({
53589
54470
  init_copy_grade();
53590
54471
  init_lint();
53591
54472
  init_verify();
54473
+ init_local_target();
53592
54474
  init_looks();
53593
54475
  init_store();
53594
54476
  init_brief_validation();
@@ -53636,7 +54518,7 @@ function registerDesignStudioTools(server2) {
53636
54518
  const { dna, logoSvg, logoPng } = deriveBrandDna(raw, { name, slug: slug3, rights: { declaration: a.rights, at: (/* @__PURE__ */ new Date()).toISOString() }, browser, firecrawl: fc });
53637
54519
  if (a.rights === "none" && dna.logo) notes.push("Rights declared as none: the logo is stored for reference only; do not place it on a client-facing page.");
53638
54520
  const written = writeBrand(dna, { logoSvg, logoPng, screenshotPng: browser?.screenshotPng });
53639
- return { ok: true, brand: summarise2(dna), passes: { static: true, browser: Boolean(browser), firecrawl: Boolean(fc) }, spaShell: raw.spaShell, dir: written.dir, files: written.files.map((f) => path39.basename(f)), notes };
54521
+ return { ok: true, brand: summarise2(dna), passes: { static: true, browser: Boolean(browser), firecrawl: Boolean(fc) }, spaShell: raw.spaShell, dir: written.dir, files: written.files.map((f) => path40.basename(f)), notes };
53640
54522
  }
53641
54523
  );
53642
54524
  safeTool(
@@ -53803,18 +54685,18 @@ function registerDesignStudioTools(server2) {
53803
54685
  "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.",
53804
54686
  { file: import_zod76.z.string().describe("Path to the .html file."), medium: import_zod76.z.enum(MEDIA) },
53805
54687
  async ({ file: file2, medium }) => {
53806
- const abs = path39.resolve(file2);
53807
- if (!fs41.existsSync(abs)) throw new Error(`No file at ${abs}.`);
53808
- const r = lintDesignContract(fs41.readFileSync(abs, "utf8"), medium);
54688
+ const abs = path40.resolve(file2);
54689
+ if (!fs42.existsSync(abs)) throw new Error(`No file at ${abs}.`);
54690
+ const r = lintDesignContract(fs42.readFileSync(abs, "utf8"), medium);
53809
54691
  return { ok: r.ok, problems: r.problems, warnings: r.warnings, facts: r.facts };
53810
54692
  }
53811
54693
  );
53812
54694
  safeTool(
53813
54695
  server2,
53814
54696
  "verify_design",
53815
- "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.",
54697
+ "Render a deck or page (a local .html inside your project or brand folder, or a public web address) in a fresh, guarded browser (no saved logins, public addresses only) 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.",
53816
54698
  {
53817
- target: import_zod76.z.string().describe("A local .html path or an http(s) URL."),
54699
+ target: import_zod76.z.string().describe("A local .html path inside the project folder, the brand folder or GHL_MCP_DESIGN_DIR, or a public http(s) URL."),
53818
54700
  medium: import_zod76.z.enum(MEDIA),
53819
54701
  brandSlug: import_zod76.z.string().optional().describe("The brand to measure against (accent share, logo, faces)."),
53820
54702
  outDir: import_zod76.z.string().optional().describe("Where to write the report. Default: <file dir>/verify-<name>/ or ./verify-<host>/ for URLs."),
@@ -53826,7 +54708,7 @@ function registerDesignStudioTools(server2) {
53826
54708
  async (a) => {
53827
54709
  const brand = a.brandSlug ? houseLooks().find((d) => d.slug === a.brandSlug) ?? readBrand(slugify(a.brandSlug)) : void 0;
53828
54710
  const isUrl2 = /^https?:\/\//i.test(a.target);
53829
- const outDir = a.outDir ?? (isUrl2 ? path39.resolve(`verify-${new URL(a.target).hostname.replace(/[^a-z0-9.-]/gi, "_")}`) : path39.join(path39.dirname(path39.resolve(a.target)), `verify-${path39.basename(a.target, ".html")}`));
54711
+ const outDir = a.outDir ?? (isUrl2 ? path40.resolve(`verify-${new URL(a.target).hostname.replace(/[^a-z0-9.-]/gi, "_")}`) : path40.join(path40.dirname(path40.resolve(a.target)), `verify-${path40.basename(a.target, ".html")}`));
53830
54712
  const r = await verifyDesign({ target: a.target, medium: a.medium, brand, outDir, placement: a.placement, mode: a.mode, ratio: a.ratio, logo: a.logo, branding: readAgencyBranding() });
53831
54713
  const v = r.report;
53832
54714
  const failed = v.rules.filter((x) => x.state === "MEASURED-FAIL").map((x) => `${x.ruleId} ${x.name}: ${x.measured ?? ""}`);
@@ -53841,7 +54723,7 @@ function registerDesignStudioTools(server2) {
53841
54723
  judgments: v.rules.filter((x) => x.state === "ASSERTED").map((x) => `${x.ruleId} ${x.name}: ${x.detail ?? ""}`),
53842
54724
  notApplicable: v.rules.filter((x) => x.state === "N-A").map((x) => `${x.ruleId}: ${x.detail ?? ""}`),
53843
54725
  brandMatch: v.brandMatch,
53844
- report: path39.join(outDir, "verification.html"),
54726
+ report: path40.join(outDir, "verification.html"),
53845
54727
  files: r.files,
53846
54728
  ...a.placement === "ghl" && { builderOwned: builderOwnedRules().map((b) => `${b.rule.id} ${b.rule.name}: ${b.why}`) }
53847
54729
  };
@@ -53869,11 +54751,20 @@ function registerDesignStudioTools(server2) {
53869
54751
  if (!a.pageDataDownloadUrl && !a.file) throw new Error("Pass pageDataDownloadUrl (from get_page_full) or file.");
53870
54752
  let page;
53871
54753
  if (a.file) {
53872
- const abs = path39.resolve(a.file);
53873
- if (!fs41.existsSync(abs)) throw new Error(`No file at ${abs}.`);
53874
- page = JSON.parse(fs41.readFileSync(abs, "utf8"));
54754
+ const abs = path40.resolve(a.file);
54755
+ if (!fs42.existsSync(abs)) throw new Error(`No file at ${abs}.`);
54756
+ page = JSON.parse(fs42.readFileSync(abs, "utf8"));
53875
54757
  } else {
53876
- const response = await fetch(a.pageDataDownloadUrl, { headers: { Accept: "application/json,*/*" } });
54758
+ const host = (() => {
54759
+ try {
54760
+ const u = new URL(a.pageDataDownloadUrl);
54761
+ return u.protocol === "https:" ? u.hostname : "";
54762
+ } catch {
54763
+ return "";
54764
+ }
54765
+ })();
54766
+ if (!GHL_PAGE_DATA_HOSTS.some((h) => host === h || host.endsWith("." + h))) throw new Error("pageDataDownloadUrl must be the https address get_page_full returned (GoHighLevel's storage).");
54767
+ const response = await guardedFetchLike(a.pageDataDownloadUrl, { headers: { Accept: "application/json,*/*" }, signal: AbortSignal.timeout(3e4), maxBytes: 5e7 });
53877
54768
  if (!response.ok) throw new Error(`Could not download the page data: HTTP ${response.status}. Re-read get_page_full \u2014 the download URL expires.`);
53878
54769
  page = JSON.parse(await response.text());
53879
54770
  }
@@ -53912,9 +54803,9 @@ function registerDesignStudioTools(server2) {
53912
54803
  if (!a.file && !a.text) throw new Error("Pass file or text.");
53913
54804
  let html2 = a.text ?? "";
53914
54805
  if (a.file) {
53915
- const abs = path39.resolve(a.file);
53916
- if (!fs41.existsSync(abs)) throw new Error(`No file at ${abs}.`);
53917
- html2 = fs41.readFileSync(abs, "utf8");
54806
+ const abs = path40.resolve(a.file);
54807
+ if (!fs42.existsSync(abs)) throw new Error(`No file at ${abs}.`);
54808
+ html2 = fs42.readFileSync(abs, "utf8");
53918
54809
  }
53919
54810
  const r = await gradeCopy(html2, claudeGenerate(a.model), { icp: a.icp });
53920
54811
  if (!r.grade) {
@@ -53949,13 +54840,13 @@ function registerDesignStudioTools(server2) {
53949
54840
  }
53950
54841
  );
53951
54842
  }
53952
- var import_zod76, fs41, path39, summarise2;
54843
+ var import_zod76, fs42, path40, GHL_PAGE_DATA_HOSTS, summarise2;
53953
54844
  var init_design_studio = __esm({
53954
54845
  "src/tools/design-studio.ts"() {
53955
54846
  "use strict";
53956
54847
  import_zod76 = require("zod");
53957
- fs41 = __toESM(require("node:fs"));
53958
- path39 = __toESM(require("node:path"));
54848
+ fs42 = __toESM(require("node:fs"));
54849
+ path40 = __toESM(require("node:path"));
53959
54850
  init_tool_helpers();
53960
54851
  init_extract();
53961
54852
  init_extract_browser();
@@ -53974,6 +54865,8 @@ var init_design_studio = __esm({
53974
54865
  init_color();
53975
54866
  init_branding();
53976
54867
  init_svg_sanitize();
54868
+ init_net_guard();
54869
+ GHL_PAGE_DATA_HOSTS = ["firebasestorage.googleapis.com", "storage.googleapis.com", "backend.leadconnectorhq.com"];
53977
54870
  summarise2 = (d) => ({
53978
54871
  slug: d.slug,
53979
54872
  name: d.name,
@@ -54679,11 +55572,11 @@ var init_audit = __esm({
54679
55572
  // src/command-os/delivery-audit/store.ts
54680
55573
  function deliveryAuditPath() {
54681
55574
  const override = process.env[DELIVERY_AUDIT_ENV];
54682
- return override && override.trim() ? override : path40.join(appDataDir(), "delivery-audit.json");
55575
+ return override && override.trim() ? override : path41.join(appDataDir(), "delivery-audit.json");
54683
55576
  }
54684
55577
  function readDeliveryAudit() {
54685
55578
  try {
54686
- const parsed2 = JSON.parse(fs42.readFileSync(deliveryAuditPath(), "utf-8"));
55579
+ const parsed2 = JSON.parse(fs43.readFileSync(deliveryAuditPath(), "utf-8"));
54687
55580
  if (!parsed2 || typeof parsed2 !== "object" || !parsed2.answers || typeof parsed2.answers !== "object") return { ...EMPTY3 };
54688
55581
  return { v: 1, answers: parsed2.answers, updated_at: parsed2.updated_at };
54689
55582
  } catch {
@@ -54694,7 +55587,7 @@ function readForWrite2() {
54694
55587
  const target = deliveryAuditPath();
54695
55588
  let raw;
54696
55589
  try {
54697
- raw = fs42.readFileSync(target, "utf-8");
55590
+ raw = fs43.readFileSync(target, "utf-8");
54698
55591
  } catch (e) {
54699
55592
  if (e?.code === "ENOENT") return { ...EMPTY3 };
54700
55593
  throw new Error("Your saved audit could not be read, so nothing was changed. Nothing was lost.");
@@ -54706,7 +55599,7 @@ function readForWrite2() {
54706
55599
  } catch {
54707
55600
  const kept = `${target}.unreadable-${Date.now()}`;
54708
55601
  try {
54709
- fs42.copyFileSync(target, kept);
55602
+ fs43.copyFileSync(target, kept);
54710
55603
  } catch {
54711
55604
  }
54712
55605
  throw new Error(`Your saved audit is damaged, so nothing was written over it. A copy is at ${kept}. Start again with reset:true when you are ready.`);
@@ -54718,13 +55611,13 @@ function writeFile2(f) {
54718
55611
  const tmp = `${target}.${process.pid}.tmp`;
54719
55612
  let renamed = false;
54720
55613
  try {
54721
- fs42.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
54722
- fs42.renameSync(tmp, target);
55614
+ fs43.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
55615
+ fs43.renameSync(tmp, target);
54723
55616
  renamed = true;
54724
55617
  } finally {
54725
55618
  if (!renamed) {
54726
55619
  try {
54727
- fs42.unlinkSync(tmp);
55620
+ fs43.unlinkSync(tmp);
54728
55621
  } catch {
54729
55622
  }
54730
55623
  }
@@ -54745,12 +55638,12 @@ function resetDeliveryAudit(now = () => /* @__PURE__ */ new Date()) {
54745
55638
  writeFile2(next);
54746
55639
  return next;
54747
55640
  }
54748
- var fs42, path40, EMPTY3, DELIVERY_AUDIT_ENV;
55641
+ var fs43, path41, EMPTY3, DELIVERY_AUDIT_ENV;
54749
55642
  var init_store4 = __esm({
54750
55643
  "src/command-os/delivery-audit/store.ts"() {
54751
55644
  "use strict";
54752
- fs42 = __toESM(require("node:fs"));
54753
- path40 = __toESM(require("node:path"));
55645
+ fs43 = __toESM(require("node:fs"));
55646
+ path41 = __toESM(require("node:path"));
54754
55647
  init_credentials_store();
54755
55648
  EMPTY3 = { v: 1, answers: {} };
54756
55649
  DELIVERY_AUDIT_ENV = "GHL_MCP_DELIVERY_AUDIT";
@@ -54975,7 +55868,7 @@ function planFingerprint(goal, steps) {
54975
55868
  goal,
54976
55869
  steps: steps.map((s) => ({ id: s.id, tool: s.tool, targets: s.targets, creates: s.creates ?? [], args: s.args ?? {} }))
54977
55870
  });
54978
- return (0, import_node_crypto14.createHash)("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 16);
55871
+ return (0, import_node_crypto15.createHash)("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 16);
54979
55872
  }
54980
55873
  function checkPlan(goal, findings, steps, now = /* @__PURE__ */ new Date(), knownTools) {
54981
55874
  const refusals = [];
@@ -55192,11 +56085,11 @@ function abandon(a, why) {
55192
56085
  const stillApplied = position(a).undo;
55193
56086
  return { ok: true, automation: { ...a, phase: "abandoned", goal: a.goal, abandonedBecause: why }, stillApplied };
55194
56087
  }
55195
- var import_node_crypto14, isExisting, FINDINGS_STALE_AFTER_MINUTES;
56088
+ var import_node_crypto15, isExisting, FINDINGS_STALE_AFTER_MINUTES;
55196
56089
  var init_automation = __esm({
55197
56090
  "src/command-os/build/automation.ts"() {
55198
56091
  "use strict";
55199
- import_node_crypto14 = require("node:crypto");
56092
+ import_node_crypto15 = require("node:crypto");
55200
56093
  init_verify_findings();
55201
56094
  isExisting = (t) => "existing" in t;
55202
56095
  FINDINGS_STALE_AFTER_MINUTES = 60;
@@ -55206,16 +56099,16 @@ var init_automation = __esm({
55206
56099
  // src/command-os/build/store.ts
55207
56100
  function automationsPath() {
55208
56101
  const override = process.env[AUTOMATIONS_ENV];
55209
- return override && override.trim() ? override : path41.join(appDataDir(), "automations.json");
56102
+ return override && override.trim() ? override : path42.join(appDataDir(), "automations.json");
55210
56103
  }
55211
- function parse4(raw) {
56104
+ function parse5(raw) {
55212
56105
  const p = JSON.parse(raw);
55213
56106
  if (!p || typeof p !== "object" || !p.builds || typeof p.builds !== "object") throw new Error("shape");
55214
56107
  return { v: 1, builds: p.builds };
55215
56108
  }
55216
56109
  function readAutomations() {
55217
56110
  try {
55218
- return parse4(fs43.readFileSync(automationsPath(), "utf-8")).builds;
56111
+ return parse5(fs44.readFileSync(automationsPath(), "utf-8")).builds;
55219
56112
  } catch {
55220
56113
  return { ...EMPTY4.builds };
55221
56114
  }
@@ -55224,17 +56117,17 @@ function readForWrite3() {
55224
56117
  const target = automationsPath();
55225
56118
  let raw;
55226
56119
  try {
55227
- raw = fs43.readFileSync(target, "utf-8");
56120
+ raw = fs44.readFileSync(target, "utf-8");
55228
56121
  } catch (e) {
55229
56122
  if (e?.code === "ENOENT") return { v: 1, builds: {} };
55230
56123
  throw new Error("Your builds file could not be read, so nothing was changed. Nothing was lost.");
55231
56124
  }
55232
56125
  try {
55233
- return parse4(raw);
56126
+ return parse5(raw);
55234
56127
  } catch {
55235
56128
  const kept = `${target}.unreadable-${Date.now()}`;
55236
56129
  try {
55237
- fs43.copyFileSync(target, kept);
56130
+ fs44.copyFileSync(target, kept);
55238
56131
  } catch {
55239
56132
  }
55240
56133
  throw new Error(
@@ -55248,13 +56141,13 @@ function writeFile3(f) {
55248
56141
  const tmp = `${target}.${process.pid}.tmp`;
55249
56142
  let renamed = false;
55250
56143
  try {
55251
- fs43.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
55252
- fs43.renameSync(tmp, target);
56144
+ fs44.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
56145
+ fs44.renameSync(tmp, target);
55253
56146
  renamed = true;
55254
56147
  } finally {
55255
56148
  if (!renamed) {
55256
56149
  try {
55257
- fs43.unlinkSync(tmp);
56150
+ fs44.unlinkSync(tmp);
55258
56151
  } catch {
55259
56152
  }
55260
56153
  }
@@ -55280,12 +56173,12 @@ function newAutomationId(now = () => /* @__PURE__ */ new Date()) {
55280
56173
  }
55281
56174
  return id;
55282
56175
  }
55283
- var fs43, path41, AUTOMATIONS_ENV, EMPTY4;
56176
+ var fs44, path42, AUTOMATIONS_ENV, EMPTY4;
55284
56177
  var init_store5 = __esm({
55285
56178
  "src/command-os/build/store.ts"() {
55286
56179
  "use strict";
55287
- fs43 = __toESM(require("node:fs"));
55288
- path41 = __toESM(require("node:path"));
56180
+ fs44 = __toESM(require("node:fs"));
56181
+ path42 = __toESM(require("node:path"));
55289
56182
  init_credentials_store();
55290
56183
  AUTOMATIONS_ENV = "GHL_MCP_AUTOMATIONS";
55291
56184
  EMPTY4 = { v: 1, builds: {} };
@@ -55604,7 +56497,7 @@ function registerAgencyBuildTools(server2, knownTools, verify, activeLocation) {
55604
56497
  }
55605
56498
  const refKinds = new Map(plan.flatMap((s) => (s.creates ?? []).map((c) => [c.ref, normaliseKind(c.kind)])));
55606
56499
  const seenKeys = new Set(found.map((f) => `${normaliseKind(f.kind)}:${f.id}`));
55607
- const hidden = [];
56500
+ const hidden2 = [];
55608
56501
  const argRefusals = [];
55609
56502
  for (const { step: s, scan } of scans) {
55610
56503
  if (scan.incomplete) argRefusals.push({ because: `Step "${s.id}": ${scan.incomplete}`, finding: { kind: "arguments", id: s.id } });
@@ -55619,7 +56512,7 @@ function registerAgencyBuildTools(server2, knownTools, verify, activeLocation) {
55619
56512
  const key = `${kind}:${id}`;
55620
56513
  if (seenKeys.has(key)) continue;
55621
56514
  seenKeys.add(key);
55622
- hidden.push({ kind, id, observedAt: now.toISOString() });
56515
+ hidden2.push({ kind, id, observedAt: now.toISOString() });
55623
56516
  }
55624
56517
  for (const { kind, id } of scan.ids) {
55625
56518
  const refKind = refKinds.get(id);
@@ -55635,13 +56528,13 @@ function registerAgencyBuildTools(server2, knownTools, verify, activeLocation) {
55635
56528
  const key = `${kind}:${id}`;
55636
56529
  if (seenKeys.has(key)) continue;
55637
56530
  seenKeys.add(key);
55638
- hidden.push({ kind, id, observedAt: now.toISOString() });
56531
+ hidden2.push({ kind, id, observedAt: now.toISOString() });
55639
56532
  }
55640
56533
  }
55641
56534
  if (argRefusals.length) {
55642
56535
  return { planned: false, refusals: argRefusals, note: "Nothing was saved. These arguments carry ids that cannot be checked as they stand." };
55643
56536
  }
55644
- const toVerify = [...found, ...hidden];
56537
+ const toVerify = [...found, ...hidden2];
55645
56538
  boundAccount = account2;
55646
56539
  const refusals = toVerify.length === 0 ? [] : verify ? await verify(account2, toVerify) : toVerify.map((f) => ({ finding: f, because: `"${f.id}" could not be checked against any account, so it is refused rather than trusted.` }));
55647
56540
  if (refusals.length) {
@@ -56694,7 +57587,7 @@ function registerSecretShopperTools(server2, client, registry2) {
56694
57587
  safeTool(
56695
57588
  server2,
56696
57589
  "run_secret_shopper",
56697
- `Workflow Audit Pro: an audit of a client account you manage. Signs up as a clearly marked TEST lead on the ACTIVE sub-account, waits, then reads every text and email the account actually sent that lead, word for word, and reports what a real customer would have seen wrong: a blank name ("Hi , "), template code left showing ({{\u2026}}), another business's name left over from a template, a link that doesn't open, a message sent twice, and a planned message that never came. Two halves: action "start" creates the lead and returns a runId and the time the watch window closes (default 10 minutes); action "finish" reads, judges and removes the test lead (finishing early gives a partial look and keeps watching); action "status" shows the latest run. WRITES: start creates one contact (the owner's own email, plus-addressed, never a customer's) and enters it into the chosen workflows, which will really send to that address and may notify staff; finish deletes that contact unless keepLead is true. The client's sign-up form itself is not submitted: entry is by adding the lead to named workflows, by a tag, or through a lead-capture https address. Tell the owner all of this before starting. Call it Workflow Audit Pro to the owner; an owner who says "secret shopper" (its old name) means this tool.`,
57590
+ `Workflow Audit Pro: an audit of a client account you manage. Signs up as a clearly marked TEST lead on the ACTIVE sub-account, waits, then reads every text and email the account actually sent that lead, word for word, and reports what a real customer would have seen wrong: a blank name ("Hi , "), template code left showing ({{\u2026}}), another business's name left over from a template, a link that doesn't open, a message sent twice, and a planned message that never came. Two halves: action "start" sends the test lead and returns a runId and the time the watch window closes (default 10 minutes); action "finish" reads, judges and lists every contact that now carries the test address (it never deletes one; you check them before deleting any) (finishing early gives a partial look and keeps watching); action "status" shows the latest run. WRITES: start sends one test lead (the owner's own email, plus-addressed, never a customer's) and enters it into the chosen workflows, which will really send to that address and may notify staff; nothing is ever deleted. The client's sign-up form itself is not submitted: entry is by adding the lead to named workflows, by a tag, or through a lead-capture https address. Tell the owner all of this before starting. Call it Workflow Audit Pro to the owner; an owner who says "secret shopper" (its old name) means this tool.`,
56698
57591
  {
56699
57592
  action: import_zod83.z.enum(["start", "finish", "status"]),
56700
57593
  workflowIds: import_zod83.z.array(import_zod83.z.string()).optional().describe("start: the workflows the test lead enters directly (usually the speed-to-lead and welcome workflows). Use list_workflows_full to find ids."),
@@ -56705,7 +57598,7 @@ function registerSecretShopperTools(server2, client, registry2) {
56705
57598
  firstName: import_zod83.z.string().optional().describe("start: leave empty (the default) to test what happens when a lead gives no first name."),
56706
57599
  watchMinutes: import_zod83.z.number().optional().describe("start: how long to watch, 1 to 1440 minutes. Leave it out to watch until the last planned message is due plus 2 minutes (10 when no plan is known)."),
56707
57600
  expect: import_zod83.z.array(expectation).optional().describe("start: messages the build plan says the lead should get, so a missing one is reported."),
56708
- keepLead: import_zod83.z.boolean().optional().describe("Keep the test lead in the account after finishing, so the owner can look at it. Default false."),
57601
+ keepLead: import_zod83.z.boolean().optional().describe("No longer used: the test lead is never removed. The result lists every contact that now carries the test address, for you to check before deleting any."),
56709
57602
  fastForward: import_zod83.z.boolean().optional().describe("start with workflowIds: test long workflows in minutes. Makes a temporary 'ZZ TEST FF' copy of each (every step kept, every time wait cut to 1 minute, no trigger), runs the test lead through the copies, then deletes them; the originals are only read. Staff alerts, tasks and lead assignments go to one chosen user (alertUserId) marked TEST. Straight-line workflows with time waits only for now (if/else, hand-offs to other workflows and other waits are refused with the reason). Tell the owner before starting."),
56710
57603
  alertUserId: import_zod83.z.string().optional().describe("fastForward: the GoHighLevel user (get_users) who receives the staff alerts, tasks and lead assignments instead of the staff. Defaults to the user whose email matches ownerEmail; if none matches, ask the owner who should get them."),
56711
57604
  allowWebhookHosts: import_zod83.z.array(import_zod83.z.string()).optional().describe("fastForward: website hosts (e.g. hooks.zapier.com) the owner allows the test lead to be sent to by webhook steps. Any other webhook host makes the check refuse and name it."),
@@ -58821,13 +59714,13 @@ ${text3}`);
58821
59714
  };
58822
59715
  }
58823
59716
  function makePipelineApi(builderClient) {
58824
- return async (method, path44, body) => {
58825
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path44}`;
59717
+ return async (method, path45, body) => {
59718
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path45}`;
58826
59719
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
58827
59720
  const response = await builderRequest(builderClient, method, url, sendsBody ? { body } : {});
58828
59721
  if (!response.ok) {
58829
59722
  const text3 = await response.text();
58830
- throw new Error(`Pipeline API ${response.status}: ${method} ${path44}
59723
+ throw new Error(`Pipeline API ${response.status}: ${method} ${path45}
58831
59724
  ${text3}`);
58832
59725
  }
58833
59726
  const text2 = await response.text();
@@ -58840,8 +59733,8 @@ ${text3}`);
58840
59733
  };
58841
59734
  }
58842
59735
  function makeFunnelApi(builderClient) {
58843
- return async (method, path44, body) => {
58844
- const url = `https://backend.leadconnectorhq.com/funnels${path44}`;
59736
+ return async (method, path45, body) => {
59737
+ const url = `https://backend.leadconnectorhq.com/funnels${path45}`;
58845
59738
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
58846
59739
  const response = await builderRequest(builderClient, method, url, {
58847
59740
  ...sendsBody ? { body } : {},
@@ -58854,7 +59747,7 @@ function makeFunnelApi(builderClient) {
58854
59747
  });
58855
59748
  if (!response.ok) {
58856
59749
  const text3 = await response.text();
58857
- throw new Error(`Funnel API ${response.status}: ${method} ${path44}
59750
+ throw new Error(`Funnel API ${response.status}: ${method} ${path45}
58858
59751
  ${text3.slice(0, 300)}`);
58859
59752
  }
58860
59753
  const text2 = await response.text();
@@ -59019,8 +59912,8 @@ function makeExecuteDeps(client, builderClient, locationId2, registry2) {
59019
59912
  },
59020
59913
  fetchEmailTemplatePreview: async (previewUrl) => {
59021
59914
  if (!/^https:\/\//i.test(previewUrl)) throw new Error(`preview URL is not https (${previewUrl.slice(0, 60)})`);
59022
- const response = await fetch(previewUrl, { headers: { Accept: "text/html,*/*" } });
59023
- if (!response.ok) throw new Error(`preview fetch HTTP ${response.status}: ${await response.text()}`);
59915
+ const response = await buildNet.read(previewUrl, { headers: { Accept: "text/html,*/*" }, signal: AbortSignal.timeout(3e4) });
59916
+ if (!response.ok) throw new Error(`preview fetch HTTP ${response.status}`);
59024
59917
  return response.text();
59025
59918
  },
59026
59919
  deleteEmailTemplate: async (templateId) => {
@@ -60071,7 +60964,7 @@ function intakeFormMatches(expected, actual) {
60071
60964
  const keys = ["id", "tag", "type", "hiddenFieldQueryKey", "required", "label", "options", "picklistOptions"];
60072
60965
  return expected.every((field, i) => keys.every((key) => field[key] === void 0 || JSON.stringify(field[key]) === JSON.stringify(actual[i]?.[key])));
60073
60966
  }
60074
- var import_zod87, customFieldItemSchema, sleep6, NOT_CONFIRMED_SCOPE, WHOLE_PRESET_NOT_KEPT, notConfirmedAsk;
60967
+ var import_zod87, buildNet, customFieldItemSchema, sleep6, NOT_CONFIRMED_SCOPE, WHOLE_PRESET_NOT_KEPT, notConfirmedAsk;
60075
60968
  var init_intake_to_build = __esm({
60076
60969
  "src/tools/intake-to-build.ts"() {
60077
60970
  "use strict";
@@ -60105,6 +60998,8 @@ var init_intake_to_build = __esm({
60105
60998
  init_features();
60106
60999
  init_executor();
60107
61000
  init_execute();
61001
+ init_net_guard();
61002
+ buildNet = { read: guardedFetchLike };
60108
61003
  customFieldItemSchema = import_zod87.z.object({
60109
61004
  id: import_zod87.z.string(),
60110
61005
  name: import_zod87.z.string(),
@@ -60169,7 +61064,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env2 = process
60169
61064
  return findings.map((finding) => ({ finding, because: `"${finding.id}" cannot be checked: no saved key for sub-account ${loc}. Register it (register_location) or point the server at it first.` }));
60170
61065
  }
60171
61066
  const reader = token2 ? new GHLClient({ apiKey: token2.apiKey, locationId: loc }) : client;
60172
- return refuseUnseenFindings2((path44, params) => reader.get(path44, params ? { params } : void 0), loc, findings);
61067
+ return refuseUnseenFindings2((path45, params) => reader.get(path45, params ? { params } : void 0), loc, findings);
60173
61068
  }, () => client.defaultLocationId);
60174
61069
  registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
60175
61070
  registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
@@ -60484,8 +61379,8 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
60484
61379
  io.out(USAGE2);
60485
61380
  return argv.length === 0 ? 2 : 0;
60486
61381
  }
60487
- const str8 = (k) => typeof a[k] === "string" ? a[k] : void 0;
60488
- const brand = str8("brand"), medium = str8("medium"), headline3 = str8("headline"), points = str8("points");
61382
+ const str9 = (k) => typeof a[k] === "string" ? a[k] : void 0;
61383
+ const brand = str9("brand"), medium = str9("medium"), headline3 = str9("headline"), points = str9("points");
60489
61384
  if (!brand || !medium || !headline3 || !points) {
60490
61385
  io.err("design-run: --brand, --medium, --headline and --points are required\n" + USAGE2);
60491
61386
  return 2;
@@ -60499,7 +61394,7 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
60499
61394
  return 2;
60500
61395
  }
60501
61396
  let conversion;
60502
- const convFile = str8("conversion");
61397
+ const convFile = str9("conversion");
60503
61398
  if (convFile) {
60504
61399
  try {
60505
61400
  conversion = JSON.parse(require("node:fs").readFileSync(convFile, "utf8"));
@@ -60512,7 +61407,7 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
60512
61407
  return 2;
60513
61408
  }
60514
61409
  }
60515
- const gate2 = str8("gate");
61410
+ const gate2 = str9("gate");
60516
61411
  if (gate2 && gate2 !== "draft" && gate2 !== "ship") {
60517
61412
  io.err("design-run: --gate must be draft or ship");
60518
61413
  return 2;
@@ -60522,8 +61417,8 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
60522
61417
  io.err("design-run: --passes must be 1, 2 or 3");
60523
61418
  return 2;
60524
61419
  }
60525
- const outDir = str8("out") ?? (0, import_node_path10.join)((0, import_node_os2.tmpdir)(), `design-run-${brand}-${medium}-${Date.now()}`);
60526
- const model = str8("model") ?? DEFAULT_MODEL;
61420
+ const outDir = str9("out") ?? (0, import_node_path10.join)((0, import_node_os2.tmpdir)(), `design-run-${brand}-${medium}-${Date.now()}`);
61421
+ const model = str9("model") ?? DEFAULT_MODEL;
60527
61422
  io.out(`design-run: ${medium} for ${brand} on your Claude (${model}), up to ${passes} pass(es) \u2192 ${outDir}`);
60528
61423
  let r;
60529
61424
  try {
@@ -60531,11 +61426,11 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
60531
61426
  brand,
60532
61427
  brief: {
60533
61428
  medium,
60534
- content: { headline: headline3, points: points.split("|").map((s) => s.trim()).filter(Boolean), audience: str8("audience"), job: str8("job"), ctaLabel: str8("cta"), proof: str8("proof") },
61429
+ content: { headline: headline3, points: points.split("|").map((s) => s.trim()).filter(Boolean), audience: str9("audience"), job: str9("job"), ctaLabel: str9("cta"), proof: str9("proof") },
60535
61430
  options: {
60536
- ...str8("mode") && { mode: str8("mode") },
61431
+ ...str9("mode") && { mode: str9("mode") },
60537
61432
  ...a.slides && { slides: Number(a.slides) },
60538
- theme: str8("theme") ?? "both",
61433
+ theme: str9("theme") ?? "both",
60539
61434
  ...conversion ? { pageMode: "conversion" } : {}
60540
61435
  },
60541
61436
  ...conversion ? { conversion } : {}
@@ -60547,7 +61442,7 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
60547
61442
  headless: !a.headed,
60548
61443
  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)`),
60549
61444
  // Two verdicts, never one: craft is measured, copy is judged.
60550
- ...str8("grade-copy") ? { gradeCopyFor: str8("grade-copy") } : {}
61445
+ ...str9("grade-copy") ? { gradeCopyFor: str9("grade-copy") } : {}
60551
61446
  });
60552
61447
  } catch (error) {
60553
61448
  io.err(error instanceof import_zod89.z.ZodError ? designValidationMessage(error) : error instanceof Error ? error.message : "The design run could not finish. Check the design details before trying again.");
@@ -60595,8 +61490,8 @@ __export(index_exports, {
60595
61490
  });
60596
61491
  module.exports = __toCommonJS(index_exports);
60597
61492
  var dotenv2 = __toESM(require("dotenv"));
60598
- var path43 = __toESM(require("path"));
60599
- var fs45 = __toESM(require("fs"));
61493
+ var path44 = __toESM(require("path"));
61494
+ var fs46 = __toESM(require("fs"));
60600
61495
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
60601
61496
 
60602
61497
  // src/version-compare.ts
@@ -60667,12 +61562,12 @@ init_setup_tool();
60667
61562
  var import_zod88 = require("zod");
60668
61563
 
60669
61564
  // src/skill-installer.ts
60670
- var import_node_crypto15 = require("node:crypto");
60671
- var fs44 = __toESM(require("node:fs"));
60672
- var path42 = __toESM(require("node:path"));
60673
- var os7 = __toESM(require("node:os"));
61565
+ var import_node_crypto16 = require("node:crypto");
61566
+ var fs45 = __toESM(require("node:fs"));
61567
+ var path43 = __toESM(require("node:path"));
61568
+ var os8 = __toESM(require("node:os"));
60674
61569
  function sha2562(buf) {
60675
- return (0, import_node_crypto15.createHash)("sha256").update(buf).digest("hex");
61570
+ return (0, import_node_crypto16.createHash)("sha256").update(buf).digest("hex");
60676
61571
  }
60677
61572
  function isOlderRelease(candidate, installed) {
60678
61573
  const parts = (v) => v.trim().split(".").map((n) => Number.parseInt(n, 10));
@@ -60684,9 +61579,9 @@ function isOlderRelease(candidate, installed) {
60684
61579
  return false;
60685
61580
  }
60686
61581
  function bundledSkillsDir(baseDir) {
60687
- const candidate = path42.resolve(baseDir, "..", "skills");
61582
+ const candidate = path43.resolve(baseDir, "..", "skills");
60688
61583
  try {
60689
- return fs44.statSync(candidate).isDirectory() ? candidate : null;
61584
+ return fs45.statSync(candidate).isDirectory() ? candidate : null;
60690
61585
  } catch {
60691
61586
  return null;
60692
61587
  }
@@ -60694,10 +61589,10 @@ function bundledSkillsDir(baseDir) {
60694
61589
  function listFiles(root) {
60695
61590
  const out = [];
60696
61591
  const walk = (dir) => {
60697
- for (const entry of fs44.readdirSync(dir, { withFileTypes: true })) {
60698
- const full = path42.join(dir, entry.name);
61592
+ for (const entry of fs45.readdirSync(dir, { withFileTypes: true })) {
61593
+ const full = path43.join(dir, entry.name);
60699
61594
  if (entry.isDirectory()) walk(full);
60700
- else if (entry.isFile()) out.push(path42.relative(root, full));
61595
+ else if (entry.isFile()) out.push(path43.relative(root, full));
60701
61596
  }
60702
61597
  };
60703
61598
  walk(root);
@@ -60705,7 +61600,7 @@ function listFiles(root) {
60705
61600
  }
60706
61601
  function readMarker(markerPath) {
60707
61602
  try {
60708
- const parsed2 = JSON.parse(fs44.readFileSync(markerPath, "utf8"));
61603
+ const parsed2 = JSON.parse(fs45.readFileSync(markerPath, "utf8"));
60709
61604
  return parsed2 && typeof parsed2 === "object" && parsed2.files ? parsed2 : null;
60710
61605
  } catch {
60711
61606
  return null;
@@ -60719,35 +61614,35 @@ function installBundledSkills(opts) {
60719
61614
  skippedOlderPackage: [],
60720
61615
  unchanged: [],
60721
61616
  errors: [],
60722
- targetDir: opts.targetDir ?? path42.join(os7.homedir(), ".claude", "skills"),
61617
+ targetDir: opts.targetDir ?? path43.join(os8.homedir(), ".claude", "skills"),
60723
61618
  bundledDir: null
60724
61619
  };
60725
61620
  const bundled = bundledSkillsDir(opts.baseDir);
60726
61621
  result.bundledDir = bundled;
60727
61622
  if (!bundled) return result;
60728
61623
  try {
60729
- fs44.mkdirSync(result.targetDir, { recursive: true });
61624
+ fs45.mkdirSync(result.targetDir, { recursive: true });
60730
61625
  } catch (e) {
60731
61626
  result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
60732
61627
  return result;
60733
61628
  }
60734
- const markerPath = path42.join(result.targetDir, ".ghl-command-skills.json");
61629
+ const markerPath = path43.join(result.targetDir, ".ghl-command-skills.json");
60735
61630
  const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
60736
61631
  const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
60737
61632
  for (const rel of listFiles(bundled)) {
60738
61633
  try {
60739
- const src = fs44.readFileSync(path42.join(bundled, rel));
60740
- const dest = path42.join(result.targetDir, rel);
61634
+ const src = fs45.readFileSync(path43.join(bundled, rel));
61635
+ const dest = path43.join(result.targetDir, rel);
60741
61636
  const srcHash = sha2562(src);
60742
61637
  let destBuf = null;
60743
61638
  try {
60744
- destBuf = fs44.readFileSync(dest);
61639
+ destBuf = fs45.readFileSync(dest);
60745
61640
  } catch {
60746
61641
  destBuf = null;
60747
61642
  }
60748
61643
  if (destBuf === null) {
60749
- fs44.mkdirSync(path42.dirname(dest), { recursive: true });
60750
- fs44.writeFileSync(dest, src);
61644
+ fs45.mkdirSync(path43.dirname(dest), { recursive: true });
61645
+ fs45.writeFileSync(dest, src);
60751
61646
  newMarker.files[rel] = srcHash;
60752
61647
  result.installed.push(rel);
60753
61648
  continue;
@@ -60764,7 +61659,7 @@ function installBundledSkills(opts) {
60764
61659
  newMarker.packageVersion = marker.packageVersion;
60765
61660
  result.skippedOlderPackage.push(rel);
60766
61661
  } else if (lastWritten && destHash === lastWritten) {
60767
- fs44.writeFileSync(dest, src);
61662
+ fs45.writeFileSync(dest, src);
60768
61663
  newMarker.files[rel] = srcHash;
60769
61664
  result.updated.push(rel);
60770
61665
  } else {
@@ -60775,7 +61670,7 @@ function installBundledSkills(opts) {
60775
61670
  }
60776
61671
  }
60777
61672
  try {
60778
- fs44.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
61673
+ fs45.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
60779
61674
  } catch (e) {
60780
61675
  result.errors.push(`marker write failed: ${String(e)}`);
60781
61676
  }
@@ -60870,11 +61765,11 @@ function agencyPrimer(p) {
60870
61765
  if (staff.length) lines.push(`Their team: ${staff.join("; ")}.`);
60871
61766
  const sops = (p.delivery?.sops ?? []).filter(Boolean);
60872
61767
  if (sops.length) lines.push(`How they deliver, in their words: ${sops.map((s) => `"${s}"`).join(" ")}`);
60873
- const os8 = p.agencyOs;
60874
- if (os8?.method) lines.push(`Their method: ${os8.method}`);
60875
- if (os8?.voice) lines.push(`How they sound, and how you should sound for them: ${os8.voice}`);
60876
- if (os8?.ica?.who) lines.push(`Who they serve: ${os8.ica.who}`);
60877
- if (os8?.offer?.name || os8?.offer?.promise) lines.push(`Their core offer: ${[os8.offer?.name, os8.offer?.promise].filter(Boolean).join(" \u2014 ")}`);
61768
+ const os9 = p.agencyOs;
61769
+ if (os9?.method) lines.push(`Their method: ${os9.method}`);
61770
+ if (os9?.voice) lines.push(`How they sound, and how you should sound for them: ${os9.voice}`);
61771
+ if (os9?.ica?.who) lines.push(`Who they serve: ${os9.ica.who}`);
61772
+ if (os9?.offer?.name || os9?.offer?.promise) lines.push(`Their core offer: ${[os9.offer?.name, os9.offer?.promise].filter(Boolean).join(" \u2014 ")}`);
60878
61773
  if (!lines.length) return void 0;
60879
61774
  lines.push("These are the agency's own details, not any client's. Client-by-client records are a tool call away, not in here.");
60880
61775
  const text2 = lines.join("\n");
@@ -60931,7 +61826,7 @@ dotenv2.config();
60931
61826
  setPkgVersion(pkg.version);
60932
61827
  {
60933
61828
  const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
60934
- if (configDirOverride && !path43.isAbsolute(configDirOverride)) {
61829
+ if (configDirOverride && !path44.isAbsolute(configDirOverride)) {
60935
61830
  process.stderr.write(
60936
61831
  `[ghl-mcp] GHL_MCP_CONFIG_DIR must be an absolute path (got "${configDirOverride}"). Use e.g. /data/ghl-mcp in a container, with a volume mounted at /data.
60937
61832
  `
@@ -60944,20 +61839,20 @@ process.on("unhandledRejection", (reason3) => {
60944
61839
  `);
60945
61840
  });
60946
61841
  function hardenSecretFilePerms() {
60947
- const repoDir = path43.resolve(__dirname, "..");
61842
+ const repoDir = path44.resolve(__dirname, "..");
60948
61843
  const candidates = [
60949
- { file: path43.join(repoDir, "start-mcp.sh"), mode: 448 },
61844
+ { file: path44.join(repoDir, "start-mcp.sh"), mode: 448 },
60950
61845
  // Legacy registry location (pre-migration); new location lives in app-data.
60951
- { file: path43.join(repoDir, ".ghl-tokens.json"), mode: 384 },
61846
+ { file: path44.join(repoDir, ".ghl-tokens.json"), mode: 384 },
60952
61847
  { file: tokenRegistryPath(), mode: 384 }
60953
61848
  ];
60954
61849
  for (const { file: file2, mode } of candidates) {
60955
61850
  let current;
60956
61851
  try {
60957
- if (!fs45.existsSync(file2)) continue;
60958
- current = fs45.statSync(file2).mode & 511;
61852
+ if (!fs46.existsSync(file2)) continue;
61853
+ current = fs46.statSync(file2).mode & 511;
60959
61854
  if (current !== mode) {
60960
- fs45.chmodSync(file2, mode);
61855
+ fs46.chmodSync(file2, mode);
60961
61856
  }
60962
61857
  } catch (error) {
60963
61858
  const message = error instanceof Error ? error.message : String(error);