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