@elitedcs/ghl-mcp 3.88.2 → 3.88.3

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
@@ -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();
@@ -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
  };
@@ -17966,12 +18406,8 @@ async function readWorkflow(bc, workflowId) {
17966
18406
  }
17967
18407
  }
17968
18408
  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);
18409
+ const res = await guardedPost(url, JSON.stringify(body), { "Content-Type": "application/json", Accept: "application/json" });
18410
+ const text2 = res.text.slice(0, 300);
17975
18411
  return { httpStatus: res.status, blockedByCaptcha: res.status === 403 && /captcha|turnstile/i.test(text2), text: text2 };
17976
18412
  }
17977
18413
  function registerFunnelQaTools(server2, client, builderClient) {
@@ -18226,6 +18662,7 @@ var init_funnel_qa = __esm({
18226
18662
  "use strict";
18227
18663
  import_zod45 = require("zod");
18228
18664
  init_tool_helpers();
18665
+ init_net_guard();
18229
18666
  asObj = (v) => v && typeof v === "object" ? v : {};
18230
18667
  asArr = (v) => Array.isArray(v) ? v : [];
18231
18668
  str = (v) => typeof v === "string" ? v : void 0;
@@ -18242,13 +18679,13 @@ var init_funnel_qa = __esm({
18242
18679
  function registerPipelineBuilderTools(server2, builderClient) {
18243
18680
  if (!builderClient) return;
18244
18681
  const client = builderClient;
18245
- async function pipelineRequest(method, path44, body) {
18246
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path44}`;
18682
+ async function pipelineRequest(method, path45, body) {
18683
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path45}`;
18247
18684
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT" || method === "PATCH");
18248
18685
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
18249
18686
  if (!response.ok) {
18250
18687
  const text3 = await response.text();
18251
- throw new Error(`Pipeline API Error ${response.status}: ${method} ${path44}
18688
+ throw new Error(`Pipeline API Error ${response.status}: ${method} ${path45}
18252
18689
  ${text3}`);
18253
18690
  }
18254
18691
  const text2 = await response.text();
@@ -19064,9 +19501,6 @@ var init_telemetry_transport = __esm({
19064
19501
  });
19065
19502
 
19066
19503
  // src/interactive-capture.ts
19067
- function chromeProfileDir() {
19068
- return path9.join(appDataDir(), "chrome-profile");
19069
- }
19070
19504
  function captureStatePath() {
19071
19505
  return path9.join(appDataDir(), "interactive-capture.json");
19072
19506
  }
@@ -19235,9 +19669,9 @@ function planFromPayload(payload) {
19235
19669
  return payload?.plan === "command-os" ? "command-os" : "mcp";
19236
19670
  }
19237
19671
  function legacyDeviceFingerprint() {
19238
- const os8 = require("node:os");
19672
+ const os9 = require("node:os");
19239
19673
  const crypto9 = require("node:crypto");
19240
- const raw = `${os8.hostname()}:${os8.userInfo().username}:${os8.platform()}:${os8.arch()}`;
19674
+ const raw = `${os9.hostname()}:${os9.userInfo().username}:${os9.platform()}:${os9.arch()}`;
19241
19675
  return crypto9.createHash("sha256").update(raw).digest("hex").slice(0, 16);
19242
19676
  }
19243
19677
  function deviceFingerprint(opts) {
@@ -19277,7 +19711,7 @@ function base64urlDecode(s) {
19277
19711
  async function getPublicKey() {
19278
19712
  if (cachedPublicKey) return cachedPublicKey;
19279
19713
  const raw = Buffer.from(ATTESTATION_PUBLIC_KEY_B64, "base64");
19280
- const key = await import_node_crypto3.webcrypto.subtle.importKey(
19714
+ const key = await import_node_crypto4.webcrypto.subtle.importKey(
19281
19715
  "raw",
19282
19716
  raw,
19283
19717
  { name: "Ed25519" },
@@ -19325,7 +19759,7 @@ async function verifyAttestation(token2, bindings, now = /* @__PURE__ */ new Dat
19325
19759
  let signatureValid;
19326
19760
  try {
19327
19761
  const key = await getPublicKey();
19328
- signatureValid = await import_node_crypto3.webcrypto.subtle.verify({ name: "Ed25519" }, key, signatureBytes, payloadBytes);
19762
+ signatureValid = await import_node_crypto4.webcrypto.subtle.verify({ name: "Ed25519" }, key, signatureBytes, payloadBytes);
19329
19763
  } catch {
19330
19764
  return { ok: false, reason: "bad-signature" };
19331
19765
  }
@@ -19357,11 +19791,11 @@ function shouldRenew(payload, now = /* @__PURE__ */ new Date()) {
19357
19791
  const remainingMs = expiry - now.getTime();
19358
19792
  return remainingMs < 7 * 24 * 60 * 60 * 1e3;
19359
19793
  }
19360
- var import_node_crypto3, ATTESTATION_PUBLIC_KEY_B64, ATTESTATION_VERSION, ATTESTATION_TTL_DAYS, ATTESTATION_GRACE_DAYS, cachedPublicKey;
19794
+ var import_node_crypto4, ATTESTATION_PUBLIC_KEY_B64, ATTESTATION_VERSION, ATTESTATION_TTL_DAYS, ATTESTATION_GRACE_DAYS, cachedPublicKey;
19361
19795
  var init_attestation = __esm({
19362
19796
  "src/attestation.ts"() {
19363
19797
  "use strict";
19364
- import_node_crypto3 = require("node:crypto");
19798
+ import_node_crypto4 = require("node:crypto");
19365
19799
  ATTESTATION_PUBLIC_KEY_B64 = "8KJo8V3Z7ngeToIQfOXpKdlr/EA34hXJVEIpW0A7wqs=";
19366
19800
  ATTESTATION_VERSION = 1;
19367
19801
  ATTESTATION_TTL_DAYS = 30;
@@ -20694,23 +21128,23 @@ function moneySummary(subs, invoices, charges, products, now = /* @__PURE__ */ n
20694
21128
  }
20695
21129
  }
20696
21130
  const nowS = now.getTime() / 1e3;
20697
- const within = (t, days) => t <= nowS && nowS - t <= days * 86400;
21131
+ const within2 = (t, days) => t <= nowS && nowS - t <= days * 86400;
20698
21132
  const r7 = /* @__PURE__ */ new Map(), r30 = /* @__PURE__ */ new Map();
20699
21133
  let c7 = 0, c30 = 0, failed30 = 0;
20700
21134
  for (const c of charges) {
20701
- if (!within(c.created, 30)) continue;
21135
+ if (!within2(c.created, 30)) continue;
20702
21136
  if (c.status === "failed") {
20703
21137
  failed30++;
20704
21138
  continue;
20705
21139
  }
20706
21140
  if (c.status !== "succeeded") continue;
20707
21141
  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);
21142
+ const net2 = fromMinor(c.amount - (c.amount_refunded ?? 0), currency) ?? 0;
21143
+ if (net2 <= 0) continue;
21144
+ add(r30, currency, net2);
20711
21145
  c30++;
20712
- if (within(c.created, 7)) {
20713
- add(r7, currency, net);
21146
+ if (within2(c.created, 7)) {
21147
+ add(r7, currency, net2);
20714
21148
  c7++;
20715
21149
  }
20716
21150
  }
@@ -20718,7 +21152,7 @@ function moneySummary(subs, invoices, charges, products, now = /* @__PURE__ */ n
20718
21152
  for (const i of invoices) {
20719
21153
  const paidAt = i.status_transitions?.paid_at;
20720
21154
  const subId = i.parent?.subscription_details?.subscription ?? i.subscription;
20721
- if (i.status !== "paid" || !paidAt || !within(paidAt, 30) || !subId) continue;
21155
+ if (i.status !== "paid" || !paidAt || !within2(paidAt, 30) || !subId) continue;
20722
21156
  const currency = upper(i.currency);
20723
21157
  const paid = fromMinor(i.amount_paid, currency);
20724
21158
  if (currency && paid) add(recurring, currency, paid);
@@ -20789,7 +21223,7 @@ function saveCalendarSecret(secret) {
20789
21223
  const validated = SecretSchema.parse(secret);
20790
21224
  const dir = ensureAppDataDir();
20791
21225
  if (process.platform !== "win32") fs12.chmodSync(dir, 448);
20792
- tmp = `${googleCalendarPath()}.${(0, import_node_crypto4.randomUUID)()}.tmp`;
21226
+ tmp = `${googleCalendarPath()}.${(0, import_node_crypto5.randomUUID)()}.tmp`;
20793
21227
  fs12.writeFileSync(tmp, JSON.stringify(validated) + "\n", { mode: 384, flag: "wx" });
20794
21228
  fs12.renameSync(tmp, googleCalendarPath());
20795
21229
  } catch {
@@ -20812,13 +21246,13 @@ function removeCalendarSecret() {
20812
21246
  throw new Error("The Google Calendar sign-in could not be removed from this machine.");
20813
21247
  }
20814
21248
  }
20815
- var fs12, path11, import_node_crypto4, import_zod51, CALENDAR_SCOPE, SecretSchema, googleCalendarPath;
21249
+ var fs12, path11, import_node_crypto5, import_zod51, CALENDAR_SCOPE, SecretSchema, googleCalendarPath;
20816
21250
  var init_google_calendar_store = __esm({
20817
21251
  "src/command-os/connectors/google-calendar-store.ts"() {
20818
21252
  "use strict";
20819
21253
  fs12 = __toESM(require("node:fs"));
20820
21254
  path11 = __toESM(require("node:path"));
20821
- import_node_crypto4 = require("node:crypto");
21255
+ import_node_crypto5 = require("node:crypto");
20822
21256
  import_zod51 = require("zod");
20823
21257
  init_credentials_store();
20824
21258
  CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
@@ -20951,16 +21385,16 @@ function createGoogleCalendar(deps = {}) {
20951
21385
  requireOnlyCalendar(result.scope);
20952
21386
  return result.access_token;
20953
21387
  }
20954
- async function pages(path44, access, item, query = {}) {
21388
+ async function pages(path45, access, item, query = {}) {
20955
21389
  const result = [];
20956
21390
  const seen = /* @__PURE__ */ new Set();
20957
21391
  let pageToken;
20958
21392
  for (let page = 0; page < 100; page++) {
20959
- const url = new URL(`${API}${path44}`);
21393
+ const url = new URL(`${API}${path45}`);
20960
21394
  for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
20961
21395
  if (pageToken) url.searchParams.set("pageToken", pageToken);
20962
21396
  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")),
21397
+ 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
21398
  await request(url.href, { headers: { Authorization: `Bearer ${access}` } })
20965
21399
  );
20966
21400
  result.push(...data.items ?? []);
@@ -20978,9 +21412,9 @@ function createGoogleCalendar(deps = {}) {
20978
21412
  async function start() {
20979
21413
  const app = appConfig();
20980
21414
  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) => {
21415
+ const state = (0, import_node_crypto6.randomBytes)(32).toString("base64url");
21416
+ const verifier = (0, import_node_crypto6.randomBytes)(32).toString("base64url");
21417
+ const server2 = (deps.createServer ?? import_node_http2.createServer)((req, res) => {
20984
21418
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
20985
21419
  res.setHeader("Cache-Control", "no-store");
20986
21420
  res.setHeader("Referrer-Policy", "no-referrer");
@@ -21033,7 +21467,7 @@ function createGoogleCalendar(deps = {}) {
21033
21467
  response_type: "code",
21034
21468
  scope: CALENDAR_SCOPE,
21035
21469
  state,
21036
- code_challenge: (0, import_node_crypto5.createHash)("sha256").update(verifier).digest("base64url"),
21470
+ code_challenge: (0, import_node_crypto6.createHash)("sha256").update(verifier).digest("base64url"),
21037
21471
  code_challenge_method: "S256",
21038
21472
  access_type: "offline",
21039
21473
  prompt: "consent"
@@ -21163,13 +21597,13 @@ function createGoogleCalendar(deps = {}) {
21163
21597
  close: clearPending2
21164
21598
  };
21165
21599
  }
21166
- var import_node_http, import_node_crypto5, import_node_child_process, import_zod52, TOKEN_URL, REVOKE_URL, API, tokenSchema, calendarSchema, eventSchema;
21600
+ var import_node_http2, import_node_crypto6, import_node_child_process, import_zod52, TOKEN_URL, REVOKE_URL, API, tokenSchema, calendarSchema, eventSchema;
21167
21601
  var init_google_calendar = __esm({
21168
21602
  "src/command-os/connectors/google-calendar.ts"() {
21169
21603
  "use strict";
21170
21604
  init_any_signal();
21171
- import_node_http = require("node:http");
21172
- import_node_crypto5 = require("node:crypto");
21605
+ import_node_http2 = require("node:http");
21606
+ import_node_crypto6 = require("node:crypto");
21173
21607
  import_node_child_process = require("node:child_process");
21174
21608
  import_zod52 = require("zod");
21175
21609
  init_credentials_store();
@@ -21211,8 +21645,8 @@ function redactConnectionKey(value, key) {
21211
21645
  return value;
21212
21646
  }
21213
21647
  function stripeFetcher(key, signal) {
21214
- return async (path44, query) => {
21215
- const url = new URL(`https://api.stripe.com${path44}`);
21648
+ return async (path45, query) => {
21649
+ const url = new URL(`https://api.stripe.com${path45}`);
21216
21650
  for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
21217
21651
  const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, signal });
21218
21652
  if (!res.ok) {
@@ -21301,11 +21735,11 @@ var init_handlers = __esm({
21301
21735
  });
21302
21736
 
21303
21737
  // src/command-os/connectors/stripe-read.ts
21304
- async function listAll(f, path44, schema, extra = {}) {
21738
+ async function listAll(f, path45, schema, extra = {}) {
21305
21739
  const out = [];
21306
21740
  let after;
21307
21741
  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 } : {} }));
21742
+ 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
21743
  out.push(...result.data);
21310
21744
  if (!result.has_more) return out;
21311
21745
  const last = import_zod53.z.object({ id: import_zod53.z.string().min(1) }).parse(result.data.at(-1));
@@ -28562,7 +28996,7 @@ function remember(id, readAt) {
28562
28996
  const times = readTimes();
28563
28997
  times[id] = readAt;
28564
28998
  ensureAppDataDir();
28565
- const tmp = `${timesPath()}.${(0, import_node_crypto6.randomUUID)()}.tmp`;
28999
+ const tmp = `${timesPath()}.${(0, import_node_crypto7.randomUUID)()}.tmp`;
28566
29000
  try {
28567
29001
  fs21.writeFileSync(tmp, JSON.stringify(times) + "\n", { mode: 384, flag: "wx" });
28568
29002
  fs21.renameSync(tmp, timesPath());
@@ -28615,14 +29049,14 @@ function money2(rows2) {
28615
29049
  async function readBriefPanels(deps) {
28616
29050
  return Promise.all(CONNECTOR_REGISTRY.flatMap((entry) => entry.panel ? [entry.panel(briefPanelDeps(deps))] : []));
28617
29051
  }
28618
- var fs21, path19, import_node_crypto6, import_zod57, MoneySchema, TotalsSchema, SummarySchema, BriefPanelSchema, TimesSchema, timesPath, NotConnected, safeLink, amountText, stripePanel, slackPanel, calendarPanel;
29052
+ var fs21, path19, import_node_crypto7, import_zod57, MoneySchema, TotalsSchema, SummarySchema, BriefPanelSchema, TimesSchema, timesPath, NotConnected, safeLink, amountText, stripePanel, slackPanel, calendarPanel;
28619
29053
  var init_brief_panels = __esm({
28620
29054
  "src/command-os/connectors/brief-panels.ts"() {
28621
29055
  "use strict";
28622
29056
  init_brief_deadline();
28623
29057
  fs21 = __toESM(require("node:fs"));
28624
29058
  path19 = __toESM(require("node:path"));
28625
- import_node_crypto6 = require("node:crypto");
29059
+ import_node_crypto7 = require("node:crypto");
28626
29060
  import_zod57 = require("zod");
28627
29061
  init_credentials_store();
28628
29062
  init_brief_row();
@@ -28840,7 +29274,7 @@ var init_client_capabilities = __esm({
28840
29274
  { key: "reputation", label: "Reputation and content", blurb: "Reviews, social posts and the blog." },
28841
29275
  { key: "health", label: "Health and handoff", blurb: "Proving it works, keeping it clean, and handing it over." }
28842
29276
  ];
28843
- N = (label, path44, params, key, nameField, version) => ({ label, path: path44, params, key, nameField, version });
29277
+ N = (label, path45, params, key, nameField, version) => ({ label, path: path45, params, key, nameField, version });
28844
29278
  W = (read2, extra) => ({ ...read2, ...extra });
28845
29279
  AI2 = "2021-04-15";
28846
29280
  CAPABILITIES = [
@@ -29130,7 +29564,7 @@ var require_package = __commonJS({
29130
29564
  "package.json"(exports2, module2) {
29131
29565
  module2.exports = {
29132
29566
  name: "@elitedcs/ghl-mcp",
29133
- version: "3.88.2",
29567
+ version: "3.88.3",
29134
29568
  mcpName: "io.github.drjerryrelth/ghl-command",
29135
29569
  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
29570
  main: "dist/index.js",
@@ -30158,8 +30592,26 @@ async function checkLinks(urls, ownHosts, fetcher, timeoutMs = 1e4) {
30158
30592
  const ctrl = new AbortController();
30159
30593
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
30160
30594
  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}.` });
30595
+ let cur = url;
30596
+ let r = await fetcher(cur, { method: "GET", redirect: "manual", signal: ctrl.signal, headers: { "User-Agent": "Mozilla/5.0 (Command OS secret shopper)" } });
30597
+ let stopped = null;
30598
+ for (let hop = 0; r.status >= 300 && r.status < 400 && r.location; hop++) {
30599
+ let next;
30600
+ try {
30601
+ next = new URL(r.location, cur).toString();
30602
+ } catch {
30603
+ stopped = "It redirects to an address that is not readable, so it was not followed.";
30604
+ break;
30605
+ }
30606
+ const why = hop >= MAX_LINK_HOPS ? "it redirects more than 5 times" : skipReason(next, ownHosts)?.replace(/^Not opened: /, "") ?? null;
30607
+ if (why) {
30608
+ stopped = `It redirects to ${next}, which was not opened: ${why}`;
30609
+ break;
30610
+ }
30611
+ cur = next;
30612
+ r = await fetcher(cur, { method: "GET", redirect: "manual", signal: ctrl.signal, headers: { "User-Agent": "Mozilla/5.0 (Command OS secret shopper)" } });
30613
+ }
30614
+ 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
30615
  } catch (e) {
30164
30616
  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
30617
  results.push({ url, state: "broken", detail: why });
@@ -30173,7 +30625,7 @@ function htmlToText(body) {
30173
30625
  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
30626
  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
30627
  }
30176
- var URL_RE, HREF_RE, TRACKING_PATH, UNSUB;
30628
+ var URL_RE, HREF_RE, TRACKING_PATH, UNSUB, MAX_LINK_HOPS;
30177
30629
  var init_links = __esm({
30178
30630
  "src/command-os/shopper/links.ts"() {
30179
30631
  "use strict";
@@ -30181,6 +30633,7 @@ var init_links = __esm({
30181
30633
  HREF_RE = /\bhref\s*=\s*["']([^"']+)["']/gi;
30182
30634
  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
30635
  UNSUB = /unsubscribe|opt-?out|\/preferences|email-preferences|unsubscribe-view|\/emails\/builder\//i;
30636
+ MAX_LINK_HOPS = 5;
30184
30637
  }
30185
30638
  });
30186
30639
 
@@ -30483,7 +30936,7 @@ function planCopy(src, opts) {
30483
30936
  const idMap = /* @__PURE__ */ new Map();
30484
30937
  for (const n of nodes) {
30485
30938
  const id = str4(n.id);
30486
- if (id) idMap.set(id, import_node_crypto7.default.randomUUID());
30939
+ if (id) idMap.set(id, import_node_crypto8.default.randomUUID());
30487
30940
  }
30488
30941
  const remap = (id) => typeof id === "string" ? idMap.get(id) ?? id : id;
30489
30942
  const waits = [];
@@ -30736,13 +31189,13 @@ function builderAdmin(b) {
30736
31189
  remove: (id, ack) => b.deleteWorkflow(id, ack ? { acknowledgeContacts: ack } : void 0)
30737
31190
  };
30738
31191
  }
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;
31192
+ var import_node_fs2, import_node_path2, import_node_crypto8, asObj3, asArr2, str4, FF_PREFIX, ALLOWED, hostOf, normalizeHost, newToken, TOKEN_NAME, tokenPrefix, DAY_NAMES, remapSelf, journalFile, SWEEP_MIN_AGE_MS;
30740
31193
  var init_fastforward = __esm({
30741
31194
  "src/command-os/shopper/fastforward.ts"() {
30742
31195
  "use strict";
30743
31196
  import_node_fs2 = __toESM(require("node:fs"));
30744
31197
  import_node_path2 = __toESM(require("node:path"));
30745
- import_node_crypto7 = __toESM(require("node:crypto"));
31198
+ import_node_crypto8 = __toESM(require("node:crypto"));
30746
31199
  init_run();
30747
31200
  asObj3 = (v) => v && typeof v === "object" ? v : {};
30748
31201
  asArr2 = (v) => Array.isArray(v) ? v : [];
@@ -30778,7 +31231,7 @@ var init_fastforward = __esm({
30778
31231
  };
30779
31232
  newToken = () => {
30780
31233
  let t = "";
30781
- while (t.length < 6) t += (import_node_crypto7.default.randomBytes(1)[0] % 36).toString(36);
31234
+ while (t.length < 6) t += (import_node_crypto8.default.randomBytes(1)[0] % 36).toString(36);
30782
31235
  return t;
30783
31236
  };
30784
31237
  TOKEN_NAME = new RegExp(`^${FF_PREFIX} [0-9a-z]{6} `);
@@ -30914,7 +31367,7 @@ async function findContactIdByEmail(client, locationId2, email) {
30914
31367
  async function startShopper(input, deps) {
30915
31368
  const now = deps.now ?? Date.now;
30916
31369
  const sleep7 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
30917
- const token2 = import_node_crypto8.default.randomUUID().replace(/-/g, "").slice(0, 8);
31370
+ const token2 = import_node_crypto9.default.randomUUID().replace(/-/g, "").slice(0, 8);
30918
31371
  const leadEmail = plusAddress(input.ownerEmail, token2);
30919
31372
  if (!SAFE_ID.test(input.locationId)) throw new Error("That client id is not valid.");
30920
31373
  if (input.entry.kind === "workflows" && input.entry.workflowIds.length === 0) throw new Error("Choose at least one workflow for the test lead to enter.");
@@ -31021,7 +31474,7 @@ async function startShopper(input, deps) {
31021
31474
  }
31022
31475
  }
31023
31476
  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) });
31477
+ const r = await guardedPost(url, JSON.stringify(body), { "Content-Type": "application/json", Accept: "application/json" });
31025
31478
  return r.status;
31026
31479
  }
31027
31480
  async function removeOwnLead(client, run) {
@@ -31059,7 +31512,7 @@ function pidAlive(pid) {
31059
31512
  function claimRun(locationId2, runId, base) {
31060
31513
  const lock = `${runPath(locationId2, runId, base)}.lock`;
31061
31514
  import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(lock), { recursive: true, mode: 448 });
31062
- const token2 = import_node_crypto8.default.randomUUID();
31515
+ const token2 = import_node_crypto9.default.randomUUID();
31063
31516
  const mine = JSON.stringify({ pid: process.pid, token: token2 });
31064
31517
  for (let attempt = 0; attempt < 3; attempt++) {
31065
31518
  try {
@@ -31113,7 +31566,7 @@ async function finishShopper(locationId2, runId, deps) {
31113
31566
  const t = now();
31114
31567
  const closed = t >= run.closesAt;
31115
31568
  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 })));
31569
+ 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
31570
  const urls = [...new Set(raw.flatMap((m) => extractUrls(m.body, m.raw !== m.body ? m.raw : void 0)))];
31118
31571
  const linkResults = (await checkLinks(urls, run.websiteHost ? [run.websiteHost] : [], fetcher)).map((l) => ({ ...l, url: maskSecrets(l.url) }));
31119
31572
  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 }));
@@ -31189,13 +31642,13 @@ function summarizeRun(run) {
31189
31642
  if (run.status === "watching") return `So far: ${head4}${stepNote} Still watching until ${new Date(run.closesAt).toISOString()}.`;
31190
31643
  return gradeLine + head4 + stepNote;
31191
31644
  }
31192
- var import_node_fs3, import_node_path3, import_node_crypto8, MAX_EXTEND_MINUTES, asObj4, asArr3, str5, SAFE_ID, notFound;
31645
+ var import_node_fs3, import_node_path3, import_node_crypto9, MAX_EXTEND_MINUTES, asObj4, asArr3, str5, SAFE_ID, notFound;
31193
31646
  var init_run = __esm({
31194
31647
  "src/command-os/shopper/run.ts"() {
31195
31648
  "use strict";
31196
31649
  import_node_fs3 = __toESM(require("node:fs"));
31197
31650
  import_node_path3 = __toESM(require("node:path"));
31198
- import_node_crypto8 = __toESM(require("node:crypto"));
31651
+ import_node_crypto9 = __toESM(require("node:crypto"));
31199
31652
  init_credentials_store();
31200
31653
  init_analyze();
31201
31654
  init_links();
@@ -31203,6 +31656,7 @@ var init_run = __esm({
31203
31656
  init_account_rules();
31204
31657
  init_score();
31205
31658
  init_steps();
31659
+ init_net_guard();
31206
31660
  MAX_EXTEND_MINUTES = 10;
31207
31661
  asObj4 = (v) => v && typeof v === "object" ? v : {};
31208
31662
  asArr3 = (v) => Array.isArray(v) ? v : [];
@@ -32402,7 +32856,7 @@ var init_page2 = __esm({
32402
32856
  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
32857
  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
32858
  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>`;
32859
+ 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
32860
  needsRow = (c, ask) => `<li><a class="ttl" href="#${c.id}">${esc4(c.name)}<span class="sub">${esc4(ask)}</span></a></li>`;
32407
32861
  }
32408
32862
  });
@@ -34142,7 +34596,7 @@ var init_client_status_placement = __esm({
34142
34596
  // src/build-contract.ts
34143
34597
  function contentHash(value) {
34144
34598
  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");
34599
+ return (0, import_node_crypto10.createHash)("sha256").update(JSON.stringify(canonical2(value)) ?? "null").digest("hex");
34146
34600
  }
34147
34601
  function assertBuildApproval(approval, inputHash) {
34148
34602
  if (!approval?.at) throw new Error("Approve building before running Build.");
@@ -34317,11 +34771,11 @@ function executablePlanLines(saved) {
34317
34771
  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
34772
  return lines.map(sentence);
34319
34773
  }
34320
- var import_node_crypto9, import_node_fs5, import_node_path5, INTAKE_CHANGED;
34774
+ var import_node_crypto10, import_node_fs5, import_node_path5, INTAKE_CHANGED;
34321
34775
  var init_build_contract = __esm({
34322
34776
  "src/build-contract.ts"() {
34323
34777
  "use strict";
34324
- import_node_crypto9 = require("node:crypto");
34778
+ import_node_crypto10 = require("node:crypto");
34325
34779
  import_node_fs5 = require("node:fs");
34326
34780
  import_node_path5 = require("node:path");
34327
34781
  init_credentials_store();
@@ -34400,7 +34854,7 @@ async function approvedStatusWriter(registry2, locationId2) {
34400
34854
  return saved;
34401
34855
  },
34402
34856
  async readPublishedPage(url) {
34403
- const result = await fetch(url, { signal: AbortSignal.timeout(15e3) });
34857
+ const result = await guardedFetchLike(url, { signal: AbortSignal.timeout(15e3) });
34404
34858
  if (!result.ok) throw new Error("The published handoff address could not be reached.");
34405
34859
  return result.text();
34406
34860
  }
@@ -34416,6 +34870,7 @@ var init_approved_status_writer = __esm({
34416
34870
  init_funnel_builder();
34417
34871
  init_client_status_placement();
34418
34872
  init_build_contract();
34873
+ init_net_guard();
34419
34874
  }
34420
34875
  });
34421
34876
 
@@ -34900,7 +35355,7 @@ function engineRepair(version) {
34900
35355
  return !version ? CLAUDE_REPAIR_MESSAGES[0] : !meetsFloor(version) ? CLAUDE_REPAIR_MESSAGES[1] : CLAUDE_REPAIR_MESSAGES[2];
34901
35356
  }
34902
35357
  function keyFingerprint(key) {
34903
- return (0, import_node_crypto10.createHash)("sha256").update(key).digest("hex");
35358
+ return (0, import_node_crypto11.createHash)("sha256").update(key).digest("hex");
34904
35359
  }
34905
35360
  async function clientPreflight(registry2, locationId2) {
34906
35361
  const lines = [];
@@ -34967,7 +35422,7 @@ async function agencyConnectionReady(registry2, companyId) {
34967
35422
  return false;
34968
35423
  }
34969
35424
  }
34970
- var import_node_crypto10, CLAUDE_REPAIR_MESSAGES;
35425
+ var import_node_crypto11, CLAUDE_REPAIR_MESSAGES;
34971
35426
  var init_client_preflight = __esm({
34972
35427
  "src/client-preflight.ts"() {
34973
35428
  "use strict";
@@ -34977,7 +35432,7 @@ var init_client_preflight = __esm({
34977
35432
  init_stage_runner();
34978
35433
  init_tool_filter();
34979
35434
  init_credentials_store();
34980
- import_node_crypto10 = require("node:crypto");
35435
+ import_node_crypto11 = require("node:crypto");
34981
35436
  CLAUDE_REPAIR_MESSAGES = [
34982
35437
  "Install Claude Code from Your agency before reading this source.",
34983
35438
  "Update Claude Code to version 2.1.0 or newer from Your agency.",
@@ -35084,7 +35539,7 @@ function startDesignRun(request, opts = {}) {
35084
35539
  const checked = validateRunRequest(request);
35085
35540
  if (!checked.ok) throw new Error(checked.error);
35086
35541
  request = checked.value;
35087
- const id = (0, import_node_crypto11.randomUUID)();
35542
+ const id = (0, import_node_crypto12.randomUUID)();
35088
35543
  const outDir = (0, import_node_path7.join)(designRunsDir(), id);
35089
35544
  (0, import_node_fs7.mkdirSync)(outDir, { recursive: true });
35090
35545
  const job = { id, status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), outDir, request, tail: [] };
@@ -35151,14 +35606,14 @@ function cancelDesignRun(id) {
35151
35606
  }
35152
35607
  return true;
35153
35608
  }
35154
- var import_node_child_process3, import_node_fs7, import_node_path7, import_node_crypto11, SLUG2, MODEL, JOINED_FLAG, jobs, children;
35609
+ var import_node_child_process3, import_node_fs7, import_node_path7, import_node_crypto12, SLUG2, MODEL, JOINED_FLAG, jobs, children;
35155
35610
  var init_run_jobs = __esm({
35156
35611
  "src/command-os/design/run-jobs.ts"() {
35157
35612
  "use strict";
35158
35613
  import_node_child_process3 = require("node:child_process");
35159
35614
  import_node_fs7 = require("node:fs");
35160
35615
  import_node_path7 = require("node:path");
35161
- import_node_crypto11 = require("node:crypto");
35616
+ import_node_crypto12 = require("node:crypto");
35162
35617
  init_credentials_store();
35163
35618
  init_schema();
35164
35619
  init_brief_validation();
@@ -36037,9 +36492,9 @@ function presetForBusinessType(type) {
36037
36492
  return "generic";
36038
36493
  }
36039
36494
  }
36040
- function setPath(target, path44, value) {
36495
+ function setPath(target, path45, value) {
36041
36496
  if (value === void 0) return;
36042
- const parts = path44.split(".");
36497
+ const parts = path45.split(".");
36043
36498
  let node = target;
36044
36499
  for (let i = 0; i < parts.length - 1; i++) {
36045
36500
  const k = parts[i];
@@ -38162,9 +38617,9 @@ function recorderAllowPatterns(mcpListOutput) {
38162
38617
  function prefillTools(source, recorderPatterns = []) {
38163
38618
  const deny = "Bash,Write,Edit,MultiEdit,NotebookEdit,Task";
38164
38619
  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" };
38620
+ if (source.kind === "url") return { allow: "", deny: deny + ",WebFetch,WebSearch,Read,Glob,Grep" };
38166
38621
  if (source.kind === "file") return { allow: "Read", deny: deny + ",WebFetch,WebSearch,Glob,Grep" };
38167
- return { allow: recorderPatterns.join(","), deny: deny + ",Read,Glob,Grep" };
38622
+ return { allow: recorderPatterns.join(","), deny: deny + ",WebFetch,WebSearch,Read,Glob,Grep" };
38168
38623
  }
38169
38624
  function sourcePreamble(source) {
38170
38625
  if (source.kind === "file") {
@@ -38211,6 +38666,7 @@ async function prefillFromSource(spawnClaude, labels, source, recorderPatterns =
38211
38666
  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
38667
  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
38668
  if (empty) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "nothing to read" };
38669
+ if (source.kind === "url") return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: LINK_READING_OFF_MESSAGE };
38214
38670
  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
38671
  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
38672
  result.answers = validatePrefillAnswers(questions, result.answers);
@@ -38269,7 +38725,7 @@ function validatePrefillAnswers(questions, proposed) {
38269
38725
  }
38270
38726
  return result;
38271
38727
  }
38272
- var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, RECORDER_HINTS;
38728
+ var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, RECORDER_HINTS, LINK_READING_OFF_MESSAGE;
38273
38729
  var init_client_intake = __esm({
38274
38730
  "src/client-intake.ts"() {
38275
38731
  "use strict";
@@ -38280,6 +38736,7 @@ var init_client_intake = __esm({
38280
38736
  INTAKE_FORM_NAME2 = "GHL Command Blueprint \u2014 Client Intake";
38281
38737
  INTAKE_CONTACT_TAG = "blueprint-intake";
38282
38738
  RECORDER_HINTS = ["plaud", "otter", "fireflies", "grain", "fathom", "tldv", "granola", "zoom", "read.ai", "readai", "supernormal", "avoma"];
38739
+ 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
38740
  }
38284
38741
  });
38285
38742
 
@@ -38343,7 +38800,7 @@ var init_plan_store = __esm({
38343
38800
  // src/agency-profile-page.ts
38344
38801
  function agencyProfilePageHtml(p, clients = [], seatToken = "") {
38345
38802
  const offers = (p.offers ?? []).map((o) => `${o.name}${o.priceFloor ? ` - ${o.priceFloor}${o.cadence === "monthly" ? "/mo" : ""}` : ""}`).join("\n");
38346
- const os8 = p.agencyOs ?? {};
38803
+ const os9 = p.agencyOs ?? {};
38347
38804
  const lines = (xs) => (xs ?? []).join("\n");
38348
38805
  const sops = (p.delivery?.sops ?? []).join("\n");
38349
38806
  const chatOn = readAccountAdminSettings().chatCreation;
@@ -38481,36 +38938,36 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
38481
38938
  <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
38939
  <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
38940
  <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" })}
38941
+ ${section2("Your brand: who you are", progress([os9.story, os9.method, os9.voice, os9.goals, os9.values, os9.proof]), `
38942
+ ${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" })}
38943
+ ${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
38944
  <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" })}
38945
+ ${area("voice", "How you sound: three words, then a sentence", os9.voice, { placeholder: "Direct, warm, clinical. Specifics over hype." })}
38946
+ ${area("goals", "Your goal for the next twelve months", os9.goals, { placeholder: "$50k a month from 51 subscribers" })}
38490
38947
  </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" })}
38948
+ ${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" })}
38949
+ ${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)" })}`)}
38950
+ ${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]), `
38951
+ ${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
38952
  <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))}
38953
+ ${area("icaPains", "Their pains, worst first: one per line", lines(os9.ica?.pains))}
38954
+ ${area("icaDesires", "What they want, most first: one per line", lines(os9.ica?.desires))}
38955
+ ${area("icaObjections", "Why they say no: one per line", lines(os9.ica?.objections))}
38956
+ ${area("icaTriggers", "What makes them buy now: one per line", lines(os9.ica?.triggers))}
38957
+ ${area("icaVocabulary", "Their own words for the problem: one per line", lines(os9.ica?.vocabulary))}
38958
+ ${area("icaHangouts", "Where they read, listen and gather: one per line", lines(os9.ica?.hangouts))}
38502
38959
  </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]), `
38960
+ ${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" })}`)}
38961
+ ${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
38962
  <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" })}
38963
+ ${input("offerName", "The offer's name", os9.offer?.name, { placeholder: "The Full Room" })}
38964
+ ${input("offerPrice", "Price, and what it is per", os9.offer?.price, { placeholder: "$5,000 per event plus 30% of revenue" })}
38508
38965
  </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" })}
38966
+ ${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" })}
38967
+ ${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
38968
  <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))}
38969
+ ${area("offerGuarantee", "The guarantee, exactly as you would say it", os9.offer?.guarantee)}
38970
+ ${area("offerAccelerators", "Why now: bonuses, deadlines, scarcity, one per line", lines(os9.offer?.accelerators))}
38514
38971
  </div>`)}
38515
38972
  ${section2("How you deliver", progress([p.delivery?.sops, p.staff]), `
38516
38973
  ${area("sops", "Delivery SOPs: one per line", sops, { placeholder: "Every build gets a rollback journal before go-live" })}
@@ -40417,7 +40874,7 @@ function writeVerifiedBackup(configPath, originalBytes) {
40417
40874
  return backupPath;
40418
40875
  }
40419
40876
  function sha256(bytes) {
40420
- return (0, import_node_crypto12.createHash)("sha256").update(bytes).digest("hex");
40877
+ return (0, import_node_crypto13.createHash)("sha256").update(bytes).digest("hex");
40421
40878
  }
40422
40879
  function baselineOf(bytes, stat) {
40423
40880
  return { size: stat.size, mtimeMs: stat.mtimeMs, hash: sha256(bytes) };
@@ -40443,7 +40900,7 @@ function assertNotStale(configPath, baseline) {
40443
40900
  }
40444
40901
  async function atomicReplace(opts) {
40445
40902
  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")}`);
40903
+ const tempPath = path31.join(dir, `.${path31.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto13.randomBytes)(4).toString("hex")}`);
40447
40904
  const fd = fs33.openSync(tempPath, "w");
40448
40905
  try {
40449
40906
  fs33.writeFileSync(fd, opts.newBytes);
@@ -40593,14 +41050,14 @@ async function runInstall(argv, ioOverride) {
40593
41050
  return EXIT_ABORTED;
40594
41051
  }
40595
41052
  }
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;
41053
+ 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
41054
  var init_config_installer = __esm({
40598
41055
  "src/config-installer.ts"() {
40599
41056
  "use strict";
40600
41057
  fs33 = __toESM(require("node:fs"));
40601
41058
  os6 = __toESM(require("node:os"));
40602
41059
  path31 = __toESM(require("node:path"));
40603
- import_node_crypto12 = require("node:crypto");
41060
+ import_node_crypto13 = require("node:crypto");
40604
41061
  import_node_util = require("node:util");
40605
41062
  import_json5 = __toESM(require("json5"));
40606
41063
  init_npx_cache_check();
@@ -41295,71 +41752,122 @@ for(const s of document.querySelectorAll("[data-ink]")){const v=s.dataset.ink;if
41295
41752
  }
41296
41753
  });
41297
41754
 
41298
- // src/command-os/design/browser.ts
41299
- async function launchBrowser(opts = {}) {
41755
+ // src/command-os/prospect/guarded-browser.ts
41756
+ async function openBrowser2() {
41300
41757
  let chromium;
41301
41758
  try {
41302
41759
  ({ chromium } = await import("playwright-core"));
41303
41760
  } catch (e) {
41304
- return { ok: false, reason: `playwright-core could not be loaded: ${e.message}` };
41761
+ return { reason: `the browser library could not be loaded (${e.message.split("\n")[0]})` };
41762
+ }
41763
+ let proxy;
41764
+ try {
41765
+ proxy = await startGuardProxy();
41766
+ } catch (e) {
41767
+ return { reason: `the browser's network gatekeeper could not start (${e.message.split("\n")[0]})` };
41305
41768
  }
41306
- const profile = opts.profileDir ?? chromeProfileDir();
41307
- const channels = ["chrome", "msedge"];
41769
+ 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
41770
  let last = "";
41309
- for (const channel of channels) {
41771
+ for (const channel of ["chrome", "msedge"]) {
41310
41772
  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) } };
41773
+ 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
41774
  } catch (e) {
41320
- last = e.message;
41775
+ last = e.message.split("\n")[0];
41321
41776
  }
41322
41777
  }
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.` };
41778
+ await proxy.close();
41779
+ return { reason: `Chrome or Edge could not be opened on this computer (${last})` };
41324
41780
  }
41325
- async function evaluateSource(page, source, arg) {
41326
- const fn = new Function("arg", `return (${source})(arg);`);
41327
- return await page.evaluate(fn, arg);
41781
+ async function openGuardedBrowser(viewport = { width: 1440, height: 900 }) {
41782
+ const opened = await openBrowser2().catch((e) => ({ reason: e.message }));
41783
+ if ("reason" in opened) return { ok: false, reason: opened.reason };
41784
+ const { browser, proxy } = opened;
41785
+ try {
41786
+ const context = await browser.newContext({ viewport, serviceWorkers: "block", acceptDownloads: false, proxy: { server: proxy.url, username: proxy.username, password: proxy.password } });
41787
+ await context.route("**/*", async (route) => {
41788
+ const m = route.request().method();
41789
+ if (m !== "GET" && m !== "HEAD") {
41790
+ await route.abort("blockedbyclient").catch(() => void 0);
41791
+ return;
41792
+ }
41793
+ await route.continue().catch(() => void 0);
41794
+ });
41795
+ await context.routeWebSocket(/.*/, (ws) => ws.close({ code: 1008, reason: "read-only" }));
41796
+ context.on("page", (p) => {
41797
+ if (context.pages().length > 1) void p.close().catch(() => void 0);
41798
+ });
41799
+ return { ok: true, context, close: () => closeQuietly(browser, proxy) };
41800
+ } catch (e) {
41801
+ await closeQuietly(browser, proxy);
41802
+ return { ok: false, reason: e.message.split("\n")[0] };
41803
+ }
41328
41804
  }
41329
- var init_browser = __esm({
41330
- "src/command-os/design/browser.ts"() {
41805
+ async function closeQuietly(browser, proxy) {
41806
+ await Promise.race([
41807
+ (async () => {
41808
+ await browser.close().catch(() => void 0);
41809
+ await proxy.close().catch(() => void 0);
41810
+ })(),
41811
+ new Promise((r) => setTimeout(r, 1e4))
41812
+ ]);
41813
+ void proxy.close().catch(() => void 0);
41814
+ }
41815
+ var init_guarded_browser = __esm({
41816
+ "src/command-os/prospect/guarded-browser.ts"() {
41331
41817
  "use strict";
41332
- init_interactive_capture();
41818
+ init_net_guard();
41333
41819
  }
41334
41820
  });
41335
41821
 
41336
41822
  // src/command-os/design/extract-browser.ts
41337
41823
  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();
41824
+ const navMs = opts.timeoutMs ?? 3e4;
41825
+ const deadlineMs = navMs * 2 + 2e4;
41826
+ let timer;
41827
+ let expiredAt = false;
41828
+ let closeOnce;
41829
+ const expired = new Promise((resolve13) => {
41830
+ timer = setTimeout(() => {
41831
+ expiredAt = true;
41832
+ resolve13({ ok: false, reason: `The rendered pass took longer than ${Math.round(deadlineMs / 1e3)} seconds and was stopped.` });
41833
+ }, deadlineMs);
41834
+ });
41835
+ const work = (async () => {
41836
+ const problem = await publicAddressProblem(url);
41837
+ if (problem && problem !== "the address does not resolve") return { ok: false, reason: `The rendered pass did not open ${url}: ${problem}.` };
41838
+ const launched = await openGuardedBrowser({ width: 1440, height: 900 });
41839
+ if (!launched.ok) return { ok: false, reason: `The rendered pass could not start a guarded browser: ${launched.reason}` };
41840
+ let closing;
41841
+ closeOnce = () => closing ??= launched.close();
41842
+ if (expiredAt) {
41843
+ await closeOnce();
41844
+ return { ok: false, reason: "The rendered pass was stopped at its deadline." };
41845
+ }
41846
+ const page = await launched.context.newPage();
41343
41847
  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 });
41848
+ await page.goto(url, { waitUntil: "networkidle", timeout: navMs }).catch(async () => {
41849
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: navMs });
41346
41850
  });
41347
41851
  await page.waitForTimeout(800);
41348
41852
  const facts = await page.evaluate(BROWSER_FACTS_JS);
41349
41853
  const out = { ...facts, logoSvg: facts.logoSvg ?? void 0, logoSrc: facts.logoSrc ?? void 0 };
41350
41854
  if (opts.screenshot !== false) out.screenshotPng = await page.screenshot({ type: "png", fullPage: false });
41351
41855
  return { ok: true, facts: out };
41352
- } catch (e) {
41353
- return { ok: false, reason: `The rendered pass failed: ${e.message.split("\n")[0]}` };
41856
+ })().catch((e) => ({ ok: false, reason: `The rendered pass failed: ${e.message.split("\n")[0]}` }));
41857
+ try {
41858
+ return await Promise.race([work, expired]);
41354
41859
  } finally {
41355
- await close();
41860
+ clearTimeout(timer);
41861
+ expiredAt = true;
41862
+ if (closeOnce) await closeOnce();
41356
41863
  }
41357
41864
  }
41358
41865
  var BROWSER_FACTS_JS;
41359
41866
  var init_extract_browser = __esm({
41360
41867
  "src/command-os/design/extract-browser.ts"() {
41361
41868
  "use strict";
41362
- init_browser();
41869
+ init_guarded_browser();
41870
+ init_net_guard();
41363
41871
  BROWSER_FACTS_JS = `(() => {
41364
41872
  const cs = (el) => el ? getComputedStyle(el) : null;
41365
41873
  const rgbOf = (v) => (v && v !== "rgba(0, 0, 0, 0)" && v !== "transparent") ? v : null;
@@ -42855,7 +43363,7 @@ function registerAssessmentTools(server2, client) {
42855
43363
  const dir = args.outDir ?? (0, import_node_path8.join)((0, import_node_os.homedir)(), "Documents", "assessments");
42856
43364
  (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
42857
43365
  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`);
43366
+ const file2 = (0, import_node_path8.join)(dir, `${slug3}-${(0, import_node_crypto14.randomBytes)(4).toString("hex")}.html`);
42859
43367
  (0, import_node_fs8.writeFileSync)(file2, html2, "utf8");
42860
43368
  return {
42861
43369
  written: file2,
@@ -42878,7 +43386,7 @@ function registerAssessmentTools(server2, client) {
42878
43386
  }
42879
43387
  );
42880
43388
  }
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;
43389
+ 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
43390
  var init_assessment = __esm({
42883
43391
  "src/tools/assessment.ts"() {
42884
43392
  "use strict";
@@ -42891,7 +43399,7 @@ var init_assessment = __esm({
42891
43399
  init_store3();
42892
43400
  init_engine();
42893
43401
  init_report();
42894
- import_node_crypto13 = require("node:crypto");
43402
+ import_node_crypto14 = require("node:crypto");
42895
43403
  import_node_fs8 = require("node:fs");
42896
43404
  import_node_os = require("node:os");
42897
43405
  import_node_path8 = require("node:path");
@@ -43439,7 +43947,7 @@ function notify(message) {
43439
43947
  }
43440
43948
  }
43441
43949
  }
43442
- function openBrowser2(port) {
43950
+ function openBrowser3(port) {
43443
43951
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
43444
43952
  const openArgs = process.platform === "win32" ? ["/c", "start", "", `http://localhost:${port}`] : [`http://localhost:${port}`];
43445
43953
  try {
@@ -43952,8 +44460,9 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
43952
44460
  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
44461
  if(mode==="transcript"){const w=document.createElement("div");w.className="cds-stack";
43954
44462
  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"]]){
44463
+ /* "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. */
44464
+ if(!iSub[id]||iSub[id]==="url")iSub[id]="paste";
44465
+ for(const [k,lab] of [["paste","Paste a transcript"],["doc","A text document"],["recorder","From my recorder"]]){
43957
44466
  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
44467
  tb.onclick=()=>{iSub[id]=k;render(lastState,lastRun)};tabs.appendChild(tb)}
43959
44468
  w.appendChild(tabs);
@@ -45111,7 +45620,7 @@ function cockpitHeader(req, name) {
45111
45620
  return Array.isArray(value) ? value.join(",") : value ?? "";
45112
45621
  }
45113
45622
  function headerFields(req, name) {
45114
- if (req instanceof http.IncomingMessage || "rawHeaders" in req) {
45623
+ if (req instanceof http2.IncomingMessage || "rawHeaders" in req) {
45115
45624
  const raw = req.rawHeaders;
45116
45625
  if (!Array.isArray(raw)) return [];
45117
45626
  const values = [];
@@ -45308,7 +45817,7 @@ async function runDashboard(argv) {
45308
45817
  return 0;
45309
45818
  }
45310
45819
  if (await cockpitAlreadyRunning(port)) {
45311
- openBrowser2(port);
45820
+ openBrowser3(port);
45312
45821
  process.stderr.write(`
45313
45822
  Command OS is already running. Opening localhost:${port}
45314
45823
 
@@ -45652,7 +46161,7 @@ async function runDashboard(argv) {
45652
46161
  };
45653
46162
  const connectionsRoutes = createConnectionsRoutes(seatToken, port, gate2.plan);
45654
46163
  const briefRoutes = createBriefRoutes(seatToken, port, gate2.plan, registry2);
45655
- const server2 = http.createServer(guarded(seatToken, port, (req, res) => {
46164
+ const server2 = http2.createServer(guarded(seatToken, port, (req, res) => {
45656
46165
  const url = cockpitPath(req);
45657
46166
  if (connectionsRoutes(req, res)) return;
45658
46167
  if (briefRoutes(req, res)) return;
@@ -46224,6 +46733,11 @@ ${opportunityNoteFailure?.detail ?? ""}`);
46224
46733
  return;
46225
46734
  }
46226
46735
  if (url === "/api/intake-prefill") {
46736
+ if (args.url && !args.file) {
46737
+ res.writeHead(200, { "Content-Type": "application/json" });
46738
+ res.end(JSON.stringify({ ok: false, answers: {}, unknown: args.labels ?? [], error: LINK_READING_OFF_MESSAGE }));
46739
+ return;
46740
+ }
46227
46741
  let bin;
46228
46742
  try {
46229
46743
  bin = claudeBin();
@@ -46529,7 +47043,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
46529
47043
  const address = new URL(String(args.url || ""));
46530
47044
  if (!["http:", "https:"].includes(address.protocol)) throw new Error("Enter the public website address.");
46531
47045
  const before = readStatusRecord(locationId2);
46532
- const response = await fetch(address.href, { signal: AbortSignal.timeout(15e3) });
47046
+ const response = await guardedFetchLike(address.href, { signal: AbortSignal.timeout(15e3) });
46533
47047
  const html2 = await response.text();
46534
47048
  const now = readStatusRecord(locationId2);
46535
47049
  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 +47075,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
46561
47075
  if (stale.length) throw new Error(stale[0].reason);
46562
47076
  const publication = readStatusRecord(locationId2);
46563
47077
  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) });
47078
+ const response = await guardedFetchLike(publication.lastUrl, { signal: AbortSignal.timeout(15e3) });
46565
47079
  const html2 = await response.text();
46566
47080
  const snapshot = readHandoffSnapshot(locationId2);
46567
47081
  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 +47594,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
47080
47594
  ${roster.length} client${roster.length === 1 ? "" : "s"} on the board. Ctrl-C to stop.
47081
47595
 
47082
47596
  `);
47083
- if (!argv.includes("--no-open")) openBrowser2(port);
47597
+ if (!argv.includes("--no-open")) openBrowser3(port);
47084
47598
  {
47085
47599
  const { state: fixed, recovered } = recoverStuckStages(readCockpitState());
47086
47600
  if (recovered) {
@@ -47099,7 +47613,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
47099
47613
  process.on("SIGTERM", stop2);
47100
47614
  });
47101
47615
  }
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;
47616
+ 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
47617
  var init_dashboard = __esm({
47104
47618
  "src/dashboard.ts"() {
47105
47619
  "use strict";
@@ -47138,7 +47652,7 @@ var init_dashboard = __esm({
47138
47652
  init_build_obligations();
47139
47653
  fs37 = __toESM(require("fs"));
47140
47654
  path35 = __toESM(require("path"));
47141
- http = __toESM(require("http"));
47655
+ http2 = __toESM(require("http"));
47142
47656
  import_child_process4 = require("child_process");
47143
47657
  init_credentials_store();
47144
47658
  init_attestation();
@@ -47200,6 +47714,7 @@ var init_dashboard = __esm({
47200
47714
  osmod = __toESM(require("os"));
47201
47715
  init_script_json();
47202
47716
  init_svg_sanitize();
47717
+ init_net_guard();
47203
47718
  STAGES2 = [
47204
47719
  "Intake",
47205
47720
  "Brief",
@@ -49006,13 +49521,13 @@ function contactServiceNote(payload) {
49006
49521
  function registerSmartListTools(server2, builderClient) {
49007
49522
  if (!builderClient) return;
49008
49523
  const client = builderClient;
49009
- async function smartListRequest(method, path44, body) {
49010
- const url = `${SMARTLIST_BASE}${path44}`;
49524
+ async function smartListRequest(method, path45, body) {
49525
+ const url = `${SMARTLIST_BASE}${path45}`;
49011
49526
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
49012
49527
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
49013
49528
  if (!response.ok) {
49014
49529
  const text3 = await response.text();
49015
- throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path44}
49530
+ throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path45}
49016
49531
  ${text3}`);
49017
49532
  }
49018
49533
  const text2 = await response.text();
@@ -49257,11 +49772,11 @@ var init_smart_lists = __esm({
49257
49772
  function registerReputationTools(server2, builderClient) {
49258
49773
  if (!builderClient) return;
49259
49774
  const client = builderClient;
49260
- async function reputationRequest(method, path44) {
49261
- const response = await builderRequest(client, method, `${REPUTATION_BASE}${path44}`);
49775
+ async function reputationRequest(method, path45) {
49776
+ const response = await builderRequest(client, method, `${REPUTATION_BASE}${path45}`);
49262
49777
  if (!response.ok) {
49263
49778
  const text3 = await response.text();
49264
- throw new Error(`Reputation API Error ${response.status}: ${method} ${path44}
49779
+ throw new Error(`Reputation API Error ${response.status}: ${method} ${path45}
49265
49780
  ${text3}`);
49266
49781
  }
49267
49782
  const text2 = await response.text();
@@ -49399,11 +49914,11 @@ var init_email_campaigns = __esm({
49399
49914
  function registerMembershipTools(server2, builderClient) {
49400
49915
  if (!builderClient) return;
49401
49916
  const client = builderClient;
49402
- async function membershipRequest(path44, method = "GET", body) {
49403
- const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path44}`, body ? { body } : {});
49917
+ async function membershipRequest(path45, method = "GET", body) {
49918
+ const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path45}`, body ? { body } : {});
49404
49919
  if (!response.ok) {
49405
49920
  const text3 = await response.text();
49406
- throw new Error(`Membership API Error ${response.status}: ${method} ${path44}
49921
+ throw new Error(`Membership API Error ${response.status}: ${method} ${path45}
49407
49922
  ${text3}`);
49408
49923
  }
49409
49924
  const text2 = await response.text();
@@ -50422,10 +50937,10 @@ async function confirm(run) {
50422
50937
  async function runProbes(client, loc) {
50423
50938
  return Promise.all(
50424
50939
  PROBES.map((probe) => confirm(async () => {
50425
- const path44 = probe.path.replace("{loc}", loc);
50940
+ const path45 = probe.path.replace("{loc}", loc);
50426
50941
  const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
50427
50942
  try {
50428
- const raw = await client.get(path44, { params, noRetry: true });
50943
+ const raw = await client.get(path45, { params, noRetry: true });
50429
50944
  const count2 = countRows(raw, probe.listKeys);
50430
50945
  return {
50431
50946
  key: probe.key,
@@ -51243,6 +51758,161 @@ var init_probe = __esm({
51243
51758
  }
51244
51759
  });
51245
51760
 
51761
+ // src/command-os/design/browser.ts
51762
+ async function evaluateSource(page, source, arg) {
51763
+ const fn = new Function("arg", `return (${source})(arg);`);
51764
+ return await page.evaluate(fn, arg);
51765
+ }
51766
+ var init_browser = __esm({
51767
+ "src/command-os/design/browser.ts"() {
51768
+ "use strict";
51769
+ init_interactive_capture();
51770
+ }
51771
+ });
51772
+
51773
+ // src/command-os/design/local-target.ts
51774
+ function realDir(p) {
51775
+ if (!p || !p.trim()) return null;
51776
+ try {
51777
+ const r = fs40.realpathSync(path38.resolve(p));
51778
+ return fs40.statSync(r).isDirectory() ? r : null;
51779
+ } catch {
51780
+ return null;
51781
+ }
51782
+ }
51783
+ function within(root, p) {
51784
+ return p === root || p.startsWith(root.endsWith(path38.sep) ? root : root + path38.sep);
51785
+ }
51786
+ function safeDesignRoot(cwd) {
51787
+ const r = realDir(cwd);
51788
+ if (!r) return null;
51789
+ const home = realDir(os7.homedir());
51790
+ if (r === path38.parse(r).root) return null;
51791
+ if (home && (r === home || within(r, home))) return null;
51792
+ const depth = (p) => path38.relative(path38.parse(p).root, p).split(path38.sep).filter(Boolean).length;
51793
+ if ([path38.resolve(cwd), r].some((p) => depth(p) < 2)) return null;
51794
+ if (SYSTEM_DIRS.some((d) => within(d, r))) return null;
51795
+ if (windowsSystemDirs().some((d) => within(d, r))) return null;
51796
+ const own = home ? ["Documents", "Downloads", "Desktop", "Movies", "Music", "Pictures", "Public", "OneDrive"].map((n) => path38.join(home, n)) : [];
51797
+ const shared = ["/Users/Shared", "/tmp", "/private/tmp", "/var/tmp", "/private/var/tmp", "/var/folders", "/private/var/folders", realDir(os7.tmpdir()) ?? os7.tmpdir()];
51798
+ if ([...own, ...shared].some((d) => r === d || r === realDir(d))) return null;
51799
+ const appData = home ? [path38.join(home, "Library"), path38.join(home, "AppData"), process.env.APPDATA, process.env.LOCALAPPDATA] : [];
51800
+ for (const d of appData) if (d && (within(realDir(d) ?? path38.resolve(d), r) || within(path38.resolve(d), r))) return null;
51801
+ if (home && within(home, r) && path38.relative(home, r).split(path38.sep)[0].startsWith(".")) return null;
51802
+ return r;
51803
+ }
51804
+ function windowsSystemDirs() {
51805
+ if (process.platform !== "win32") return [];
51806
+ return [process.env.SystemRoot, process.env.ProgramFiles, process.env["ProgramFiles(x86)"], process.env.ProgramData].filter((d) => !!d).map((d) => realDir(d) ?? path38.resolve(d));
51807
+ }
51808
+ function designRoots(env2 = process.env, cwd = process.cwd()) {
51809
+ const out = [];
51810
+ const named = env2[DESIGN_DIR_ENV]?.trim() ? safeDesignRoot(env2[DESIGN_DIR_ENV]) : null;
51811
+ const moved = env2[BRANDS_ENV]?.trim();
51812
+ const brands = moved ? safeDesignRoot(moved) : realDir(brandsDir());
51813
+ for (const r of [named, safeDesignRoot(cwd), brands]) if (r && !out.includes(r)) out.push(r);
51814
+ return out;
51815
+ }
51816
+ function urlFor(root, file2) {
51817
+ return LOCAL_ORIGIN + "/" + path38.relative(root, file2).split(path38.sep).map(encodeURIComponent).join("/");
51818
+ }
51819
+ function hidden(rel) {
51820
+ return rel.split(path38.sep).some((s) => s.startsWith("."));
51821
+ }
51822
+ function checkLocalTarget(target, roots = designRoots()) {
51823
+ const abs = path38.resolve(target);
51824
+ let file2;
51825
+ try {
51826
+ file2 = fs40.realpathSync(abs);
51827
+ } catch {
51828
+ throw new Error(`No file at ${abs}.`);
51829
+ }
51830
+ const ext = path38.extname(file2).toLowerCase();
51831
+ if (ext !== ".html" && ext !== ".htm") throw new Error(`verify_design renders .html files only; ${abs} is not one.`);
51832
+ const st = fs40.statSync(file2);
51833
+ if (!st.isFile()) throw new Error(`${abs} is not a file.`);
51834
+ if (st.size > MAX_PAGE_BYTES) throw new Error(`${abs} is larger than 10 MB, so it is not rendered.`);
51835
+ const root = roots.find((r) => within(r, file2) && !hidden(path38.relative(r, file2)));
51836
+ if (!root) {
51837
+ const where = roots.length ? roots.join(", ") : "none is set up on this computer";
51838
+ 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.`);
51839
+ }
51840
+ return { file: file2, root, url: urlFor(root, file2), html: fs40.readFileSync(file2, "utf8") };
51841
+ }
51842
+ function localFileFor(root, raw) {
51843
+ let u;
51844
+ try {
51845
+ u = new URL(raw);
51846
+ } catch {
51847
+ return null;
51848
+ }
51849
+ if (u.origin !== LOCAL_ORIGIN || u.username || u.password) return null;
51850
+ const segs = [];
51851
+ for (const part of u.pathname.split("/").slice(1)) {
51852
+ let s;
51853
+ try {
51854
+ s = decodeURIComponent(part);
51855
+ } catch {
51856
+ return null;
51857
+ }
51858
+ if (!s || s.startsWith(".") || /[\\/\0]/.test(s) || s.includes(":")) return null;
51859
+ segs.push(s);
51860
+ }
51861
+ if (!segs.length) return null;
51862
+ let file2;
51863
+ try {
51864
+ file2 = fs40.realpathSync(path38.join(root, ...segs));
51865
+ } catch {
51866
+ return null;
51867
+ }
51868
+ if (!within(root, file2) || file2 === root || hidden(path38.relative(root, file2))) return null;
51869
+ const contentType = SERVED[path38.extname(file2).toLowerCase()];
51870
+ if (!contentType) return null;
51871
+ try {
51872
+ const st = fs40.statSync(file2);
51873
+ if (!st.isFile() || st.size > MAX_FILE_BYTES) return null;
51874
+ } catch {
51875
+ return null;
51876
+ }
51877
+ return { file: file2, contentType };
51878
+ }
51879
+ var fs40, os7, path38, DESIGN_DIR_ENV, LOCAL_ORIGIN, MAX_PAGE_BYTES, MAX_FILE_BYTES, SERVED, SYSTEM_DIRS;
51880
+ var init_local_target = __esm({
51881
+ "src/command-os/design/local-target.ts"() {
51882
+ "use strict";
51883
+ fs40 = __toESM(require("node:fs"));
51884
+ os7 = __toESM(require("node:os"));
51885
+ path38 = __toESM(require("node:path"));
51886
+ init_store();
51887
+ DESIGN_DIR_ENV = "GHL_MCP_DESIGN_DIR";
51888
+ LOCAL_ORIGIN = "http://verify.invalid";
51889
+ MAX_PAGE_BYTES = 10 * 1024 * 1024;
51890
+ MAX_FILE_BYTES = 25 * 1024 * 1024;
51891
+ SERVED = {
51892
+ ".html": "text/html; charset=utf-8",
51893
+ ".htm": "text/html; charset=utf-8",
51894
+ ".css": "text/css; charset=utf-8",
51895
+ ".js": "text/javascript; charset=utf-8",
51896
+ ".mjs": "text/javascript; charset=utf-8",
51897
+ ".png": "image/png",
51898
+ ".jpg": "image/jpeg",
51899
+ ".jpeg": "image/jpeg",
51900
+ ".gif": "image/gif",
51901
+ ".webp": "image/webp",
51902
+ ".avif": "image/avif",
51903
+ ".svg": "image/svg+xml",
51904
+ ".ico": "image/x-icon",
51905
+ ".woff": "font/woff",
51906
+ ".woff2": "font/woff2",
51907
+ ".ttf": "font/ttf",
51908
+ ".otf": "font/otf",
51909
+ ".mp4": "video/mp4",
51910
+ ".webm": "video/webm"
51911
+ };
51912
+ SYSTEM_DIRS = ["/etc", "/private/etc", "/usr", "/bin", "/sbin", "/System", "/Library", "/opt", "/dev", "/proc", "/sys"];
51913
+ }
51914
+ });
51915
+
51246
51916
  // src/command-os/design/png.ts
51247
51917
  function decodePng(buf) {
51248
51918
  const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
@@ -51272,7 +51942,7 @@ function decodePng(buf) {
51272
51942
  if (interlace !== 0) throw new Error("interlaced PNG is not supported");
51273
51943
  const channels = colorType === 0 ? 1 : colorType === 2 ? 3 : colorType === 3 ? 1 : colorType === 4 ? 2 : colorType === 6 ? 4 : 0;
51274
51944
  if (!channels) throw new Error(`PNG colour type ${colorType} is not supported`);
51275
- const raw = (0, import_node_zlib.inflateSync)(Buffer.concat(idat));
51945
+ const raw = (0, import_node_zlib2.inflateSync)(Buffer.concat(idat));
51276
51946
  const bpp = channels;
51277
51947
  const stride = width * bpp;
51278
51948
  const out = new Uint8Array(width * height * 4);
@@ -51357,11 +52027,11 @@ function pixelAt(png, x, y) {
51357
52027
  const o = (yi * png.width + xi) * 4;
51358
52028
  return { r: png.rgba[o], g: png.rgba[o + 1], b: png.rgba[o + 2] };
51359
52029
  }
51360
- var import_node_zlib, SIG;
52030
+ var import_node_zlib2, SIG;
51361
52031
  var init_png = __esm({
51362
52032
  "src/command-os/design/png.ts"() {
51363
52033
  "use strict";
51364
- import_node_zlib = require("node:zlib");
52034
+ import_node_zlib2 = require("node:zlib");
51365
52035
  SIG = [137, 80, 78, 71, 13, 10, 26, 10];
51366
52036
  }
51367
52037
  });
@@ -52990,11 +53660,14 @@ async function settle(page) {
52990
53660
  await page.evaluate(() => document.fonts?.ready).catch(() => void 0);
52991
53661
  await page.waitForTimeout(250);
52992
53662
  }
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() };
53663
+ async function readTarget(target, roots) {
53664
+ if (isUrl(target)) {
53665
+ const problem = await publicAddressProblem(target);
53666
+ if (problem) throw new Error(`verify_design did not open ${target}: ${problem}.`);
53667
+ return { html: null, url: target, root: null };
53668
+ }
53669
+ const t = checkLocalTarget(target, roots);
53670
+ return { html: t.html, url: t.url, root: t.root };
52998
53671
  }
52999
53672
  function slidePixels(index, png, file2, sec, brand) {
53000
53673
  let img;
@@ -53038,11 +53711,35 @@ function slidePixels(index, png, file2, sec, brand) {
53038
53711
  });
53039
53712
  return { index, screenshot: file2, bgShare, accentShare, hueBuckets, overImage };
53040
53713
  }
53714
+ async function serveLocalPage(context, root, url, html2) {
53715
+ const notServed = { status: 404, contentType: "text/plain", body: "not served" };
53716
+ await context.route(`${LOCAL_ORIGIN}/**`, async (route) => {
53717
+ const req = route.request();
53718
+ if (req.method() !== "GET" && req.method() !== "HEAD") {
53719
+ await route.abort("blockedbyclient").catch(() => void 0);
53720
+ return;
53721
+ }
53722
+ if (req.url() === url) {
53723
+ await route.fulfill({ status: 200, contentType: "text/html; charset=utf-8", body: html2 }).catch(() => void 0);
53724
+ return;
53725
+ }
53726
+ const hit = localFileFor(root, req.url());
53727
+ let body = null;
53728
+ if (hit) {
53729
+ try {
53730
+ body = fs41.readFileSync(hit.file);
53731
+ } catch {
53732
+ body = null;
53733
+ }
53734
+ }
53735
+ await route.fulfill(hit && body ? { status: 200, contentType: hit.contentType, body } : notServed).catch(() => void 0);
53736
+ });
53737
+ }
53041
53738
  async function verifyDesign(opts) {
53042
- const { html: html2, url } = await readTarget(opts.target);
53739
+ const { html: html2, url, root } = await readTarget(opts.target, opts.roots);
53043
53740
  const placement = opts.placement ?? "html";
53044
53741
  const medium = opts.medium;
53045
- fs40.mkdirSync(opts.outDir, { recursive: true });
53742
+ fs41.mkdirSync(opts.outDir, { recursive: true });
53046
53743
  const files = [];
53047
53744
  const screenshots = [];
53048
53745
  const at = (/* @__PURE__ */ new Date()).toISOString();
@@ -53064,10 +53761,16 @@ async function verifyDesign(opts) {
53064
53761
  if (!conformance.ok) {
53065
53762
  rules = [];
53066
53763
  } else {
53067
- const launched = opts.noBrowser ? { ok: false, reason: "browser disabled for this run" } : await launchBrowser({ headless: opts.headless ?? true, profileDir: opts.profileDir });
53764
+ const launched = opts.noBrowser ? { ok: false, reason: "browser disabled for this run" } : await openGuardedBrowser();
53068
53765
  if (launched.ok) {
53069
- const { context, close } = launched.browser;
53070
- try {
53766
+ const { context, close } = launched;
53767
+ let timer;
53768
+ const expired = new Promise((_, reject) => {
53769
+ const limit = opts.deadlineMs ?? VERIFY_DEADLINE_MS;
53770
+ 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);
53771
+ });
53772
+ const work = (async () => {
53773
+ if (root !== null && html2 !== null) await serveLocalPage(context, root, url, html2);
53071
53774
  const page = context.pages()[0] ?? await context.newPage();
53072
53775
  if (medium === "deck") {
53073
53776
  await page.setViewportSize({ width: 1920, height: 1080 });
@@ -53087,8 +53790,8 @@ async function verifyDesign(opts) {
53087
53790
  try {
53088
53791
  await loc.scrollIntoViewIfNeeded({ timeout: 5e3 });
53089
53792
  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));
53793
+ fs41.writeFileSync(path39.join(opts.outDir, file2), png);
53794
+ files.push(path39.join(opts.outDir, file2));
53092
53795
  screenshots.push(file2);
53093
53796
  pixels.push(slidePixels(sec.index, png, file2, sec, opts.brand));
53094
53797
  } catch {
@@ -53120,8 +53823,8 @@ async function verifyDesign(opts) {
53120
53823
  const shot = "page-1440.png";
53121
53824
  try {
53122
53825
  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));
53826
+ fs41.writeFileSync(path39.join(opts.outDir, shot), png);
53827
+ files.push(path39.join(opts.outDir, shot));
53125
53828
  screenshots.push(shot);
53126
53829
  } catch {
53127
53830
  }
@@ -53129,8 +53832,8 @@ async function verifyDesign(opts) {
53129
53832
  await page.setViewportSize({ width: 390, height: 844 });
53130
53833
  await page.evaluate(SETTLE_SCROLL_JS).catch(() => void 0);
53131
53834
  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"));
53835
+ fs41.writeFileSync(path39.join(opts.outDir, "page-390.png"), png);
53836
+ files.push(path39.join(opts.outDir, "page-390.png"));
53134
53837
  screenshots.push("page-390.png");
53135
53838
  } catch {
53136
53839
  }
@@ -53149,7 +53852,12 @@ async function verifyDesign(opts) {
53149
53852
  rules.push(...checkConversion(conv, { brief: opts.conversion, gate: opts.gate ?? "ship", copyAxes: opts.copyAxes ?? null }, Boolean(conv)));
53150
53853
  }
53151
53854
  }
53855
+ })();
53856
+ work.catch(() => void 0);
53857
+ try {
53858
+ await Promise.race([work, expired]);
53152
53859
  } finally {
53860
+ clearTimeout(timer);
53153
53861
  await close();
53154
53862
  }
53155
53863
  } else {
@@ -53203,25 +53911,27 @@ async function verifyDesign(opts) {
53203
53911
  screenshots
53204
53912
  };
53205
53913
  const checked = VerificationSchema.parse(report3);
53206
- const jsonPath = path38.join(opts.outDir, "verification.json");
53207
- fs40.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
53914
+ const jsonPath = path39.join(opts.outDir, "verification.json");
53915
+ fs41.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
53208
53916
  files.push(jsonPath);
53209
53917
  const reportHtml = renderVerificationHtml(checked, opts.branding);
53210
- const htmlPath = path38.join(opts.outDir, "verification.html");
53211
- fs40.writeFileSync(htmlPath, reportHtml);
53918
+ const htmlPath = path39.join(opts.outDir, "verification.html");
53919
+ fs41.writeFileSync(htmlPath, reportHtml);
53212
53920
  files.push(htmlPath);
53213
53921
  return { report: checked, files, reportHtml };
53214
53922
  }
53215
- var fs40, path38, import_node_url, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS;
53923
+ var fs41, path39, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS, VERIFY_DEADLINE_MS;
53216
53924
  var init_verify = __esm({
53217
53925
  "src/command-os/design/verify.ts"() {
53218
53926
  "use strict";
53219
- fs40 = __toESM(require("node:fs"));
53220
- path38 = __toESM(require("node:path"));
53221
- import_node_url = require("node:url");
53927
+ fs41 = __toESM(require("node:fs"));
53928
+ path39 = __toESM(require("node:path"));
53222
53929
  init_lint();
53223
53930
  init_probe();
53224
53931
  init_browser();
53932
+ init_guarded_browser();
53933
+ init_net_guard();
53934
+ init_local_target();
53225
53935
  init_png();
53226
53936
  init_slides2();
53227
53937
  init_web2();
@@ -53259,6 +53969,7 @@ var init_verify = __esm({
53259
53969
  }
53260
53970
  return out;
53261
53971
  }`;
53972
+ VERIFY_DEADLINE_MS = 3e5;
53262
53973
  }
53263
53974
  });
53264
53975
 
@@ -53470,17 +54181,15 @@ Fix exactly those and reply with ONLY the complete corrected HTML in one \`\`\`h
53470
54181
  ${html2}
53471
54182
  \`\`\``;
53472
54183
  }
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");
54184
+ async function defaultVerify(file2, medium, brand, outDir, mode, conversion) {
54185
+ const own = safeDesignRoot((0, import_node_path9.dirname)(file2));
53476
54186
  const r = await verifyDesign({
53477
54187
  target: file2,
53478
54188
  medium,
53479
54189
  brand,
53480
54190
  outDir,
53481
54191
  mode,
53482
- headless,
53483
- profileDir: mkdtempSync((0, import_node_path9.join)(tmpdir2(), "ds-run-profile-")),
54192
+ roots: own ? [own, ...designRoots()] : designRoots(),
53484
54193
  ...conversion ? { pageMode: "conversion", conversion: conversion.brief, gate: conversion.gate } : {}
53485
54194
  });
53486
54195
  if (r.report.renderedWith !== "browser") {
@@ -53504,7 +54213,7 @@ async function runDesign(opts) {
53504
54213
  const maxPasses = Math.max(1, Math.min(3, opts.maxPasses ?? 3));
53505
54214
  const generate = opts.generate ?? claudeGenerate(opts.model ?? DEFAULT_MODEL);
53506
54215
  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));
54216
+ const verify = opts.verify ?? ((f, m, b, o, mode2) => defaultVerify(f, m, b, o, mode2, conversionRun));
53508
54217
  const mode = medium === "deck" ? brief.options.mode : void 0;
53509
54218
  const result = { ok: false, greenAt: null, passes: [], costUsd: 0, ms: 0, model: null, outDir, brandSlug: dna.slug, medium, stoppedBecause: "" };
53510
54219
  let instruction = `${pack.markdown}
@@ -53589,6 +54298,7 @@ var init_run2 = __esm({
53589
54298
  init_copy_grade();
53590
54299
  init_lint();
53591
54300
  init_verify();
54301
+ init_local_target();
53592
54302
  init_looks();
53593
54303
  init_store();
53594
54304
  init_brief_validation();
@@ -53636,7 +54346,7 @@ function registerDesignStudioTools(server2) {
53636
54346
  const { dna, logoSvg, logoPng } = deriveBrandDna(raw, { name, slug: slug3, rights: { declaration: a.rights, at: (/* @__PURE__ */ new Date()).toISOString() }, browser, firecrawl: fc });
53637
54347
  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
54348
  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 };
54349
+ 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
54350
  }
53641
54351
  );
53642
54352
  safeTool(
@@ -53803,18 +54513,18 @@ function registerDesignStudioTools(server2) {
53803
54513
  "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
54514
  { file: import_zod76.z.string().describe("Path to the .html file."), medium: import_zod76.z.enum(MEDIA) },
53805
54515
  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);
54516
+ const abs = path40.resolve(file2);
54517
+ if (!fs42.existsSync(abs)) throw new Error(`No file at ${abs}.`);
54518
+ const r = lintDesignContract(fs42.readFileSync(abs, "utf8"), medium);
53809
54519
  return { ok: r.ok, problems: r.problems, warnings: r.warnings, facts: r.facts };
53810
54520
  }
53811
54521
  );
53812
54522
  safeTool(
53813
54523
  server2,
53814
54524
  "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.",
54525
+ "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
54526
  {
53817
- target: import_zod76.z.string().describe("A local .html path or an http(s) URL."),
54527
+ 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
54528
  medium: import_zod76.z.enum(MEDIA),
53819
54529
  brandSlug: import_zod76.z.string().optional().describe("The brand to measure against (accent share, logo, faces)."),
53820
54530
  outDir: import_zod76.z.string().optional().describe("Where to write the report. Default: <file dir>/verify-<name>/ or ./verify-<host>/ for URLs."),
@@ -53826,7 +54536,7 @@ function registerDesignStudioTools(server2) {
53826
54536
  async (a) => {
53827
54537
  const brand = a.brandSlug ? houseLooks().find((d) => d.slug === a.brandSlug) ?? readBrand(slugify(a.brandSlug)) : void 0;
53828
54538
  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")}`));
54539
+ 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
54540
  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
54541
  const v = r.report;
53832
54542
  const failed = v.rules.filter((x) => x.state === "MEASURED-FAIL").map((x) => `${x.ruleId} ${x.name}: ${x.measured ?? ""}`);
@@ -53841,7 +54551,7 @@ function registerDesignStudioTools(server2) {
53841
54551
  judgments: v.rules.filter((x) => x.state === "ASSERTED").map((x) => `${x.ruleId} ${x.name}: ${x.detail ?? ""}`),
53842
54552
  notApplicable: v.rules.filter((x) => x.state === "N-A").map((x) => `${x.ruleId}: ${x.detail ?? ""}`),
53843
54553
  brandMatch: v.brandMatch,
53844
- report: path39.join(outDir, "verification.html"),
54554
+ report: path40.join(outDir, "verification.html"),
53845
54555
  files: r.files,
53846
54556
  ...a.placement === "ghl" && { builderOwned: builderOwnedRules().map((b) => `${b.rule.id} ${b.rule.name}: ${b.why}`) }
53847
54557
  };
@@ -53869,11 +54579,20 @@ function registerDesignStudioTools(server2) {
53869
54579
  if (!a.pageDataDownloadUrl && !a.file) throw new Error("Pass pageDataDownloadUrl (from get_page_full) or file.");
53870
54580
  let page;
53871
54581
  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"));
54582
+ const abs = path40.resolve(a.file);
54583
+ if (!fs42.existsSync(abs)) throw new Error(`No file at ${abs}.`);
54584
+ page = JSON.parse(fs42.readFileSync(abs, "utf8"));
53875
54585
  } else {
53876
- const response = await fetch(a.pageDataDownloadUrl, { headers: { Accept: "application/json,*/*" } });
54586
+ const host = (() => {
54587
+ try {
54588
+ const u = new URL(a.pageDataDownloadUrl);
54589
+ return u.protocol === "https:" ? u.hostname : "";
54590
+ } catch {
54591
+ return "";
54592
+ }
54593
+ })();
54594
+ 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).");
54595
+ const response = await guardedFetchLike(a.pageDataDownloadUrl, { headers: { Accept: "application/json,*/*" }, signal: AbortSignal.timeout(3e4), maxBytes: 5e7 });
53877
54596
  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
54597
  page = JSON.parse(await response.text());
53879
54598
  }
@@ -53912,9 +54631,9 @@ function registerDesignStudioTools(server2) {
53912
54631
  if (!a.file && !a.text) throw new Error("Pass file or text.");
53913
54632
  let html2 = a.text ?? "";
53914
54633
  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");
54634
+ const abs = path40.resolve(a.file);
54635
+ if (!fs42.existsSync(abs)) throw new Error(`No file at ${abs}.`);
54636
+ html2 = fs42.readFileSync(abs, "utf8");
53918
54637
  }
53919
54638
  const r = await gradeCopy(html2, claudeGenerate(a.model), { icp: a.icp });
53920
54639
  if (!r.grade) {
@@ -53949,13 +54668,13 @@ function registerDesignStudioTools(server2) {
53949
54668
  }
53950
54669
  );
53951
54670
  }
53952
- var import_zod76, fs41, path39, summarise2;
54671
+ var import_zod76, fs42, path40, GHL_PAGE_DATA_HOSTS, summarise2;
53953
54672
  var init_design_studio = __esm({
53954
54673
  "src/tools/design-studio.ts"() {
53955
54674
  "use strict";
53956
54675
  import_zod76 = require("zod");
53957
- fs41 = __toESM(require("node:fs"));
53958
- path39 = __toESM(require("node:path"));
54676
+ fs42 = __toESM(require("node:fs"));
54677
+ path40 = __toESM(require("node:path"));
53959
54678
  init_tool_helpers();
53960
54679
  init_extract();
53961
54680
  init_extract_browser();
@@ -53974,6 +54693,8 @@ var init_design_studio = __esm({
53974
54693
  init_color();
53975
54694
  init_branding();
53976
54695
  init_svg_sanitize();
54696
+ init_net_guard();
54697
+ GHL_PAGE_DATA_HOSTS = ["firebasestorage.googleapis.com", "storage.googleapis.com", "backend.leadconnectorhq.com"];
53977
54698
  summarise2 = (d) => ({
53978
54699
  slug: d.slug,
53979
54700
  name: d.name,
@@ -54679,11 +55400,11 @@ var init_audit = __esm({
54679
55400
  // src/command-os/delivery-audit/store.ts
54680
55401
  function deliveryAuditPath() {
54681
55402
  const override = process.env[DELIVERY_AUDIT_ENV];
54682
- return override && override.trim() ? override : path40.join(appDataDir(), "delivery-audit.json");
55403
+ return override && override.trim() ? override : path41.join(appDataDir(), "delivery-audit.json");
54683
55404
  }
54684
55405
  function readDeliveryAudit() {
54685
55406
  try {
54686
- const parsed2 = JSON.parse(fs42.readFileSync(deliveryAuditPath(), "utf-8"));
55407
+ const parsed2 = JSON.parse(fs43.readFileSync(deliveryAuditPath(), "utf-8"));
54687
55408
  if (!parsed2 || typeof parsed2 !== "object" || !parsed2.answers || typeof parsed2.answers !== "object") return { ...EMPTY3 };
54688
55409
  return { v: 1, answers: parsed2.answers, updated_at: parsed2.updated_at };
54689
55410
  } catch {
@@ -54694,7 +55415,7 @@ function readForWrite2() {
54694
55415
  const target = deliveryAuditPath();
54695
55416
  let raw;
54696
55417
  try {
54697
- raw = fs42.readFileSync(target, "utf-8");
55418
+ raw = fs43.readFileSync(target, "utf-8");
54698
55419
  } catch (e) {
54699
55420
  if (e?.code === "ENOENT") return { ...EMPTY3 };
54700
55421
  throw new Error("Your saved audit could not be read, so nothing was changed. Nothing was lost.");
@@ -54706,7 +55427,7 @@ function readForWrite2() {
54706
55427
  } catch {
54707
55428
  const kept = `${target}.unreadable-${Date.now()}`;
54708
55429
  try {
54709
- fs42.copyFileSync(target, kept);
55430
+ fs43.copyFileSync(target, kept);
54710
55431
  } catch {
54711
55432
  }
54712
55433
  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 +55439,13 @@ function writeFile2(f) {
54718
55439
  const tmp = `${target}.${process.pid}.tmp`;
54719
55440
  let renamed = false;
54720
55441
  try {
54721
- fs42.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
54722
- fs42.renameSync(tmp, target);
55442
+ fs43.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
55443
+ fs43.renameSync(tmp, target);
54723
55444
  renamed = true;
54724
55445
  } finally {
54725
55446
  if (!renamed) {
54726
55447
  try {
54727
- fs42.unlinkSync(tmp);
55448
+ fs43.unlinkSync(tmp);
54728
55449
  } catch {
54729
55450
  }
54730
55451
  }
@@ -54745,12 +55466,12 @@ function resetDeliveryAudit(now = () => /* @__PURE__ */ new Date()) {
54745
55466
  writeFile2(next);
54746
55467
  return next;
54747
55468
  }
54748
- var fs42, path40, EMPTY3, DELIVERY_AUDIT_ENV;
55469
+ var fs43, path41, EMPTY3, DELIVERY_AUDIT_ENV;
54749
55470
  var init_store4 = __esm({
54750
55471
  "src/command-os/delivery-audit/store.ts"() {
54751
55472
  "use strict";
54752
- fs42 = __toESM(require("node:fs"));
54753
- path40 = __toESM(require("node:path"));
55473
+ fs43 = __toESM(require("node:fs"));
55474
+ path41 = __toESM(require("node:path"));
54754
55475
  init_credentials_store();
54755
55476
  EMPTY3 = { v: 1, answers: {} };
54756
55477
  DELIVERY_AUDIT_ENV = "GHL_MCP_DELIVERY_AUDIT";
@@ -54975,7 +55696,7 @@ function planFingerprint(goal, steps) {
54975
55696
  goal,
54976
55697
  steps: steps.map((s) => ({ id: s.id, tool: s.tool, targets: s.targets, creates: s.creates ?? [], args: s.args ?? {} }))
54977
55698
  });
54978
- return (0, import_node_crypto14.createHash)("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 16);
55699
+ return (0, import_node_crypto15.createHash)("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 16);
54979
55700
  }
54980
55701
  function checkPlan(goal, findings, steps, now = /* @__PURE__ */ new Date(), knownTools) {
54981
55702
  const refusals = [];
@@ -55192,11 +55913,11 @@ function abandon(a, why) {
55192
55913
  const stillApplied = position(a).undo;
55193
55914
  return { ok: true, automation: { ...a, phase: "abandoned", goal: a.goal, abandonedBecause: why }, stillApplied };
55194
55915
  }
55195
- var import_node_crypto14, isExisting, FINDINGS_STALE_AFTER_MINUTES;
55916
+ var import_node_crypto15, isExisting, FINDINGS_STALE_AFTER_MINUTES;
55196
55917
  var init_automation = __esm({
55197
55918
  "src/command-os/build/automation.ts"() {
55198
55919
  "use strict";
55199
- import_node_crypto14 = require("node:crypto");
55920
+ import_node_crypto15 = require("node:crypto");
55200
55921
  init_verify_findings();
55201
55922
  isExisting = (t) => "existing" in t;
55202
55923
  FINDINGS_STALE_AFTER_MINUTES = 60;
@@ -55206,16 +55927,16 @@ var init_automation = __esm({
55206
55927
  // src/command-os/build/store.ts
55207
55928
  function automationsPath() {
55208
55929
  const override = process.env[AUTOMATIONS_ENV];
55209
- return override && override.trim() ? override : path41.join(appDataDir(), "automations.json");
55930
+ return override && override.trim() ? override : path42.join(appDataDir(), "automations.json");
55210
55931
  }
55211
- function parse4(raw) {
55932
+ function parse5(raw) {
55212
55933
  const p = JSON.parse(raw);
55213
55934
  if (!p || typeof p !== "object" || !p.builds || typeof p.builds !== "object") throw new Error("shape");
55214
55935
  return { v: 1, builds: p.builds };
55215
55936
  }
55216
55937
  function readAutomations() {
55217
55938
  try {
55218
- return parse4(fs43.readFileSync(automationsPath(), "utf-8")).builds;
55939
+ return parse5(fs44.readFileSync(automationsPath(), "utf-8")).builds;
55219
55940
  } catch {
55220
55941
  return { ...EMPTY4.builds };
55221
55942
  }
@@ -55224,17 +55945,17 @@ function readForWrite3() {
55224
55945
  const target = automationsPath();
55225
55946
  let raw;
55226
55947
  try {
55227
- raw = fs43.readFileSync(target, "utf-8");
55948
+ raw = fs44.readFileSync(target, "utf-8");
55228
55949
  } catch (e) {
55229
55950
  if (e?.code === "ENOENT") return { v: 1, builds: {} };
55230
55951
  throw new Error("Your builds file could not be read, so nothing was changed. Nothing was lost.");
55231
55952
  }
55232
55953
  try {
55233
- return parse4(raw);
55954
+ return parse5(raw);
55234
55955
  } catch {
55235
55956
  const kept = `${target}.unreadable-${Date.now()}`;
55236
55957
  try {
55237
- fs43.copyFileSync(target, kept);
55958
+ fs44.copyFileSync(target, kept);
55238
55959
  } catch {
55239
55960
  }
55240
55961
  throw new Error(
@@ -55248,13 +55969,13 @@ function writeFile3(f) {
55248
55969
  const tmp = `${target}.${process.pid}.tmp`;
55249
55970
  let renamed = false;
55250
55971
  try {
55251
- fs43.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
55252
- fs43.renameSync(tmp, target);
55972
+ fs44.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
55973
+ fs44.renameSync(tmp, target);
55253
55974
  renamed = true;
55254
55975
  } finally {
55255
55976
  if (!renamed) {
55256
55977
  try {
55257
- fs43.unlinkSync(tmp);
55978
+ fs44.unlinkSync(tmp);
55258
55979
  } catch {
55259
55980
  }
55260
55981
  }
@@ -55280,12 +56001,12 @@ function newAutomationId(now = () => /* @__PURE__ */ new Date()) {
55280
56001
  }
55281
56002
  return id;
55282
56003
  }
55283
- var fs43, path41, AUTOMATIONS_ENV, EMPTY4;
56004
+ var fs44, path42, AUTOMATIONS_ENV, EMPTY4;
55284
56005
  var init_store5 = __esm({
55285
56006
  "src/command-os/build/store.ts"() {
55286
56007
  "use strict";
55287
- fs43 = __toESM(require("node:fs"));
55288
- path41 = __toESM(require("node:path"));
56008
+ fs44 = __toESM(require("node:fs"));
56009
+ path42 = __toESM(require("node:path"));
55289
56010
  init_credentials_store();
55290
56011
  AUTOMATIONS_ENV = "GHL_MCP_AUTOMATIONS";
55291
56012
  EMPTY4 = { v: 1, builds: {} };
@@ -55604,7 +56325,7 @@ function registerAgencyBuildTools(server2, knownTools, verify, activeLocation) {
55604
56325
  }
55605
56326
  const refKinds = new Map(plan.flatMap((s) => (s.creates ?? []).map((c) => [c.ref, normaliseKind(c.kind)])));
55606
56327
  const seenKeys = new Set(found.map((f) => `${normaliseKind(f.kind)}:${f.id}`));
55607
- const hidden = [];
56328
+ const hidden2 = [];
55608
56329
  const argRefusals = [];
55609
56330
  for (const { step: s, scan } of scans) {
55610
56331
  if (scan.incomplete) argRefusals.push({ because: `Step "${s.id}": ${scan.incomplete}`, finding: { kind: "arguments", id: s.id } });
@@ -55619,7 +56340,7 @@ function registerAgencyBuildTools(server2, knownTools, verify, activeLocation) {
55619
56340
  const key = `${kind}:${id}`;
55620
56341
  if (seenKeys.has(key)) continue;
55621
56342
  seenKeys.add(key);
55622
- hidden.push({ kind, id, observedAt: now.toISOString() });
56343
+ hidden2.push({ kind, id, observedAt: now.toISOString() });
55623
56344
  }
55624
56345
  for (const { kind, id } of scan.ids) {
55625
56346
  const refKind = refKinds.get(id);
@@ -55635,13 +56356,13 @@ function registerAgencyBuildTools(server2, knownTools, verify, activeLocation) {
55635
56356
  const key = `${kind}:${id}`;
55636
56357
  if (seenKeys.has(key)) continue;
55637
56358
  seenKeys.add(key);
55638
- hidden.push({ kind, id, observedAt: now.toISOString() });
56359
+ hidden2.push({ kind, id, observedAt: now.toISOString() });
55639
56360
  }
55640
56361
  }
55641
56362
  if (argRefusals.length) {
55642
56363
  return { planned: false, refusals: argRefusals, note: "Nothing was saved. These arguments carry ids that cannot be checked as they stand." };
55643
56364
  }
55644
- const toVerify = [...found, ...hidden];
56365
+ const toVerify = [...found, ...hidden2];
55645
56366
  boundAccount = account2;
55646
56367
  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
56368
  if (refusals.length) {
@@ -58821,13 +59542,13 @@ ${text3}`);
58821
59542
  };
58822
59543
  }
58823
59544
  function makePipelineApi(builderClient) {
58824
- return async (method, path44, body) => {
58825
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path44}`;
59545
+ return async (method, path45, body) => {
59546
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path45}`;
58826
59547
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
58827
59548
  const response = await builderRequest(builderClient, method, url, sendsBody ? { body } : {});
58828
59549
  if (!response.ok) {
58829
59550
  const text3 = await response.text();
58830
- throw new Error(`Pipeline API ${response.status}: ${method} ${path44}
59551
+ throw new Error(`Pipeline API ${response.status}: ${method} ${path45}
58831
59552
  ${text3}`);
58832
59553
  }
58833
59554
  const text2 = await response.text();
@@ -58840,8 +59561,8 @@ ${text3}`);
58840
59561
  };
58841
59562
  }
58842
59563
  function makeFunnelApi(builderClient) {
58843
- return async (method, path44, body) => {
58844
- const url = `https://backend.leadconnectorhq.com/funnels${path44}`;
59564
+ return async (method, path45, body) => {
59565
+ const url = `https://backend.leadconnectorhq.com/funnels${path45}`;
58845
59566
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
58846
59567
  const response = await builderRequest(builderClient, method, url, {
58847
59568
  ...sendsBody ? { body } : {},
@@ -58854,7 +59575,7 @@ function makeFunnelApi(builderClient) {
58854
59575
  });
58855
59576
  if (!response.ok) {
58856
59577
  const text3 = await response.text();
58857
- throw new Error(`Funnel API ${response.status}: ${method} ${path44}
59578
+ throw new Error(`Funnel API ${response.status}: ${method} ${path45}
58858
59579
  ${text3.slice(0, 300)}`);
58859
59580
  }
58860
59581
  const text2 = await response.text();
@@ -59019,8 +59740,8 @@ function makeExecuteDeps(client, builderClient, locationId2, registry2) {
59019
59740
  },
59020
59741
  fetchEmailTemplatePreview: async (previewUrl) => {
59021
59742
  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()}`);
59743
+ const response = await buildNet.read(previewUrl, { headers: { Accept: "text/html,*/*" }, signal: AbortSignal.timeout(3e4) });
59744
+ if (!response.ok) throw new Error(`preview fetch HTTP ${response.status}`);
59024
59745
  return response.text();
59025
59746
  },
59026
59747
  deleteEmailTemplate: async (templateId) => {
@@ -60071,7 +60792,7 @@ function intakeFormMatches(expected, actual) {
60071
60792
  const keys = ["id", "tag", "type", "hiddenFieldQueryKey", "required", "label", "options", "picklistOptions"];
60072
60793
  return expected.every((field, i) => keys.every((key) => field[key] === void 0 || JSON.stringify(field[key]) === JSON.stringify(actual[i]?.[key])));
60073
60794
  }
60074
- var import_zod87, customFieldItemSchema, sleep6, NOT_CONFIRMED_SCOPE, WHOLE_PRESET_NOT_KEPT, notConfirmedAsk;
60795
+ var import_zod87, buildNet, customFieldItemSchema, sleep6, NOT_CONFIRMED_SCOPE, WHOLE_PRESET_NOT_KEPT, notConfirmedAsk;
60075
60796
  var init_intake_to_build = __esm({
60076
60797
  "src/tools/intake-to-build.ts"() {
60077
60798
  "use strict";
@@ -60105,6 +60826,8 @@ var init_intake_to_build = __esm({
60105
60826
  init_features();
60106
60827
  init_executor();
60107
60828
  init_execute();
60829
+ init_net_guard();
60830
+ buildNet = { read: guardedFetchLike };
60108
60831
  customFieldItemSchema = import_zod87.z.object({
60109
60832
  id: import_zod87.z.string(),
60110
60833
  name: import_zod87.z.string(),
@@ -60169,7 +60892,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env2 = process
60169
60892
  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
60893
  }
60171
60894
  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);
60895
+ return refuseUnseenFindings2((path45, params) => reader.get(path45, params ? { params } : void 0), loc, findings);
60173
60896
  }, () => client.defaultLocationId);
60174
60897
  registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
60175
60898
  registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
@@ -60595,8 +61318,8 @@ __export(index_exports, {
60595
61318
  });
60596
61319
  module.exports = __toCommonJS(index_exports);
60597
61320
  var dotenv2 = __toESM(require("dotenv"));
60598
- var path43 = __toESM(require("path"));
60599
- var fs45 = __toESM(require("fs"));
61321
+ var path44 = __toESM(require("path"));
61322
+ var fs46 = __toESM(require("fs"));
60600
61323
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
60601
61324
 
60602
61325
  // src/version-compare.ts
@@ -60667,12 +61390,12 @@ init_setup_tool();
60667
61390
  var import_zod88 = require("zod");
60668
61391
 
60669
61392
  // 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"));
61393
+ var import_node_crypto16 = require("node:crypto");
61394
+ var fs45 = __toESM(require("node:fs"));
61395
+ var path43 = __toESM(require("node:path"));
61396
+ var os8 = __toESM(require("node:os"));
60674
61397
  function sha2562(buf) {
60675
- return (0, import_node_crypto15.createHash)("sha256").update(buf).digest("hex");
61398
+ return (0, import_node_crypto16.createHash)("sha256").update(buf).digest("hex");
60676
61399
  }
60677
61400
  function isOlderRelease(candidate, installed) {
60678
61401
  const parts = (v) => v.trim().split(".").map((n) => Number.parseInt(n, 10));
@@ -60684,9 +61407,9 @@ function isOlderRelease(candidate, installed) {
60684
61407
  return false;
60685
61408
  }
60686
61409
  function bundledSkillsDir(baseDir) {
60687
- const candidate = path42.resolve(baseDir, "..", "skills");
61410
+ const candidate = path43.resolve(baseDir, "..", "skills");
60688
61411
  try {
60689
- return fs44.statSync(candidate).isDirectory() ? candidate : null;
61412
+ return fs45.statSync(candidate).isDirectory() ? candidate : null;
60690
61413
  } catch {
60691
61414
  return null;
60692
61415
  }
@@ -60694,10 +61417,10 @@ function bundledSkillsDir(baseDir) {
60694
61417
  function listFiles(root) {
60695
61418
  const out = [];
60696
61419
  const walk = (dir) => {
60697
- for (const entry of fs44.readdirSync(dir, { withFileTypes: true })) {
60698
- const full = path42.join(dir, entry.name);
61420
+ for (const entry of fs45.readdirSync(dir, { withFileTypes: true })) {
61421
+ const full = path43.join(dir, entry.name);
60699
61422
  if (entry.isDirectory()) walk(full);
60700
- else if (entry.isFile()) out.push(path42.relative(root, full));
61423
+ else if (entry.isFile()) out.push(path43.relative(root, full));
60701
61424
  }
60702
61425
  };
60703
61426
  walk(root);
@@ -60705,7 +61428,7 @@ function listFiles(root) {
60705
61428
  }
60706
61429
  function readMarker(markerPath) {
60707
61430
  try {
60708
- const parsed2 = JSON.parse(fs44.readFileSync(markerPath, "utf8"));
61431
+ const parsed2 = JSON.parse(fs45.readFileSync(markerPath, "utf8"));
60709
61432
  return parsed2 && typeof parsed2 === "object" && parsed2.files ? parsed2 : null;
60710
61433
  } catch {
60711
61434
  return null;
@@ -60719,35 +61442,35 @@ function installBundledSkills(opts) {
60719
61442
  skippedOlderPackage: [],
60720
61443
  unchanged: [],
60721
61444
  errors: [],
60722
- targetDir: opts.targetDir ?? path42.join(os7.homedir(), ".claude", "skills"),
61445
+ targetDir: opts.targetDir ?? path43.join(os8.homedir(), ".claude", "skills"),
60723
61446
  bundledDir: null
60724
61447
  };
60725
61448
  const bundled = bundledSkillsDir(opts.baseDir);
60726
61449
  result.bundledDir = bundled;
60727
61450
  if (!bundled) return result;
60728
61451
  try {
60729
- fs44.mkdirSync(result.targetDir, { recursive: true });
61452
+ fs45.mkdirSync(result.targetDir, { recursive: true });
60730
61453
  } catch (e) {
60731
61454
  result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
60732
61455
  return result;
60733
61456
  }
60734
- const markerPath = path42.join(result.targetDir, ".ghl-command-skills.json");
61457
+ const markerPath = path43.join(result.targetDir, ".ghl-command-skills.json");
60735
61458
  const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
60736
61459
  const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
60737
61460
  for (const rel of listFiles(bundled)) {
60738
61461
  try {
60739
- const src = fs44.readFileSync(path42.join(bundled, rel));
60740
- const dest = path42.join(result.targetDir, rel);
61462
+ const src = fs45.readFileSync(path43.join(bundled, rel));
61463
+ const dest = path43.join(result.targetDir, rel);
60741
61464
  const srcHash = sha2562(src);
60742
61465
  let destBuf = null;
60743
61466
  try {
60744
- destBuf = fs44.readFileSync(dest);
61467
+ destBuf = fs45.readFileSync(dest);
60745
61468
  } catch {
60746
61469
  destBuf = null;
60747
61470
  }
60748
61471
  if (destBuf === null) {
60749
- fs44.mkdirSync(path42.dirname(dest), { recursive: true });
60750
- fs44.writeFileSync(dest, src);
61472
+ fs45.mkdirSync(path43.dirname(dest), { recursive: true });
61473
+ fs45.writeFileSync(dest, src);
60751
61474
  newMarker.files[rel] = srcHash;
60752
61475
  result.installed.push(rel);
60753
61476
  continue;
@@ -60764,7 +61487,7 @@ function installBundledSkills(opts) {
60764
61487
  newMarker.packageVersion = marker.packageVersion;
60765
61488
  result.skippedOlderPackage.push(rel);
60766
61489
  } else if (lastWritten && destHash === lastWritten) {
60767
- fs44.writeFileSync(dest, src);
61490
+ fs45.writeFileSync(dest, src);
60768
61491
  newMarker.files[rel] = srcHash;
60769
61492
  result.updated.push(rel);
60770
61493
  } else {
@@ -60775,7 +61498,7 @@ function installBundledSkills(opts) {
60775
61498
  }
60776
61499
  }
60777
61500
  try {
60778
- fs44.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
61501
+ fs45.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
60779
61502
  } catch (e) {
60780
61503
  result.errors.push(`marker write failed: ${String(e)}`);
60781
61504
  }
@@ -60870,11 +61593,11 @@ function agencyPrimer(p) {
60870
61593
  if (staff.length) lines.push(`Their team: ${staff.join("; ")}.`);
60871
61594
  const sops = (p.delivery?.sops ?? []).filter(Boolean);
60872
61595
  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 ")}`);
61596
+ const os9 = p.agencyOs;
61597
+ if (os9?.method) lines.push(`Their method: ${os9.method}`);
61598
+ if (os9?.voice) lines.push(`How they sound, and how you should sound for them: ${os9.voice}`);
61599
+ if (os9?.ica?.who) lines.push(`Who they serve: ${os9.ica.who}`);
61600
+ if (os9?.offer?.name || os9?.offer?.promise) lines.push(`Their core offer: ${[os9.offer?.name, os9.offer?.promise].filter(Boolean).join(" \u2014 ")}`);
60878
61601
  if (!lines.length) return void 0;
60879
61602
  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
61603
  const text2 = lines.join("\n");
@@ -60931,7 +61654,7 @@ dotenv2.config();
60931
61654
  setPkgVersion(pkg.version);
60932
61655
  {
60933
61656
  const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
60934
- if (configDirOverride && !path43.isAbsolute(configDirOverride)) {
61657
+ if (configDirOverride && !path44.isAbsolute(configDirOverride)) {
60935
61658
  process.stderr.write(
60936
61659
  `[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
61660
  `
@@ -60944,20 +61667,20 @@ process.on("unhandledRejection", (reason3) => {
60944
61667
  `);
60945
61668
  });
60946
61669
  function hardenSecretFilePerms() {
60947
- const repoDir = path43.resolve(__dirname, "..");
61670
+ const repoDir = path44.resolve(__dirname, "..");
60948
61671
  const candidates = [
60949
- { file: path43.join(repoDir, "start-mcp.sh"), mode: 448 },
61672
+ { file: path44.join(repoDir, "start-mcp.sh"), mode: 448 },
60950
61673
  // Legacy registry location (pre-migration); new location lives in app-data.
60951
- { file: path43.join(repoDir, ".ghl-tokens.json"), mode: 384 },
61674
+ { file: path44.join(repoDir, ".ghl-tokens.json"), mode: 384 },
60952
61675
  { file: tokenRegistryPath(), mode: 384 }
60953
61676
  ];
60954
61677
  for (const { file: file2, mode } of candidates) {
60955
61678
  let current;
60956
61679
  try {
60957
- if (!fs45.existsSync(file2)) continue;
60958
- current = fs45.statSync(file2).mode & 511;
61680
+ if (!fs46.existsSync(file2)) continue;
61681
+ current = fs46.statSync(file2).mode & 511;
60959
61682
  if (current !== mode) {
60960
- fs45.chmodSync(file2, mode);
61683
+ fs46.chmodSync(file2, mode);
60961
61684
  }
60962
61685
  } catch (error) {
60963
61686
  const message = error instanceof Error ? error.message : String(error);