@elitedcs/ghl-mcp 3.93.0 → 3.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +15 -10
- package/dist/index.js +1433 -534
- package/guide/guide.html +3 -8
- package/package.json +1 -1
- package/skills/blueprint/examples/validate-plan.cjs +9 -0
- package/skills/blueprint/presets/clinic.preset.json +49 -5
- package/skills/blueprint/presets/coach.preset.json +48 -4
- package/skills/blueprint/presets/ecommerce.preset.json +46 -2
- package/skills/blueprint/presets/generic-client.preset.json +47 -3
- package/skills/blueprint/presets/local-service.preset.json +48 -4
- package/skills/blueprint/presets/med-spa.preset.json +48 -4
- package/skills/blueprint/presets/wellness-clinic.preset.json +122 -12
package/dist/index.js
CHANGED
|
@@ -156,7 +156,7 @@ var init_account_read_retry = __esm({
|
|
|
156
156
|
inBuildRequestScope = () => buildScope.getStore() !== void 0;
|
|
157
157
|
inAccountReadAttempt = () => readScope.getStore() === true;
|
|
158
158
|
withBuildRequestScope = (run, wait = sleep) => buildScope.run({ wait }, run);
|
|
159
|
-
sleep = (ms) => new Promise((
|
|
159
|
+
sleep = (ms) => new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
160
160
|
buildRequestWait = (ms) => (buildScope.getStore()?.wait ?? sleep)(ms);
|
|
161
161
|
}
|
|
162
162
|
});
|
|
@@ -250,9 +250,9 @@ function planFromPayload(payload) {
|
|
|
250
250
|
}
|
|
251
251
|
function legacyDeviceFingerprint() {
|
|
252
252
|
const os12 = require("node:os");
|
|
253
|
-
const
|
|
253
|
+
const crypto16 = require("node:crypto");
|
|
254
254
|
const raw = `${os12.hostname()}:${os12.userInfo().username}:${os12.platform()}:${os12.arch()}`;
|
|
255
|
-
return
|
|
255
|
+
return crypto16.createHash("sha256").update(raw).digest("hex").slice(0, 16);
|
|
256
256
|
}
|
|
257
257
|
function registerDeviceIdResolver(fn) {
|
|
258
258
|
deviceIdResolver = fn;
|
|
@@ -263,7 +263,7 @@ function deviceFingerprint(opts) {
|
|
|
263
263
|
opts.persist?.(opts.cached);
|
|
264
264
|
return opts.cached;
|
|
265
265
|
}
|
|
266
|
-
const
|
|
266
|
+
const crypto16 = require("node:crypto");
|
|
267
267
|
if (!opts && deviceIdResolver) {
|
|
268
268
|
try {
|
|
269
269
|
return deviceIdResolver();
|
|
@@ -277,7 +277,7 @@ function deviceFingerprint(opts) {
|
|
|
277
277
|
const creds = store2.readCredentials();
|
|
278
278
|
if (creds?.device_id) return creds.device_id;
|
|
279
279
|
const adopted = creds?.signed_attestation ? parseAttestationPayload(creds.signed_attestation)?.device_fingerprint : void 0;
|
|
280
|
-
const id2 = adopted ||
|
|
280
|
+
const id2 = adopted || crypto16.randomBytes(8).toString("hex");
|
|
281
281
|
if (creds) {
|
|
282
282
|
try {
|
|
283
283
|
store2.updateCredentials(creds, { device_id: id2 });
|
|
@@ -289,7 +289,7 @@ function deviceFingerprint(opts) {
|
|
|
289
289
|
return legacyDeviceFingerprint();
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
-
const fresh =
|
|
292
|
+
const fresh = crypto16.randomBytes(8).toString("hex");
|
|
293
293
|
opts.persist?.(fresh);
|
|
294
294
|
return fresh;
|
|
295
295
|
}
|
|
@@ -811,13 +811,13 @@ function setProtectedAccount(locationId2, name2, file3 = accountRulesPath()) {
|
|
|
811
811
|
function isWriteMethod(method) {
|
|
812
812
|
return WRITE_METHODS.has(method.toUpperCase());
|
|
813
813
|
}
|
|
814
|
-
function locationIdsInRequest(
|
|
814
|
+
function locationIdsInRequest(path67, opts = {}) {
|
|
815
815
|
const found = /* @__PURE__ */ new Set();
|
|
816
816
|
const add2 = (v) => {
|
|
817
817
|
if (typeof v === "string" && v.trim()) found.add(v.trim());
|
|
818
818
|
else if (Array.isArray(v)) v.forEach(add2);
|
|
819
819
|
};
|
|
820
|
-
const seg = /\/locations\/([^/?#]+)/i.exec(
|
|
820
|
+
const seg = /\/locations\/([^/?#]+)/i.exec(path67);
|
|
821
821
|
if (seg) add2(decodeURIComponent(seg[1]));
|
|
822
822
|
for (const src of [opts.params, opts.body]) {
|
|
823
823
|
if (!src || typeof src !== "object") continue;
|
|
@@ -844,7 +844,7 @@ function protectedRefusalFor(locationId2, rulesFile) {
|
|
|
844
844
|
const name2 = rules.protected[id2];
|
|
845
845
|
return name2 ? protectedRefusalMessage(name2, false) : null;
|
|
846
846
|
}
|
|
847
|
-
function assertWriteAllowed(method,
|
|
847
|
+
function assertWriteAllowed(method, path67, opts = {}) {
|
|
848
848
|
if (!isWriteMethod(method)) return;
|
|
849
849
|
let rules;
|
|
850
850
|
try {
|
|
@@ -853,7 +853,7 @@ function assertWriteAllowed(method, path66, opts = {}) {
|
|
|
853
853
|
throw new Error(UNREADABLE_RULES_MSG);
|
|
854
854
|
}
|
|
855
855
|
if (Object.keys(rules.protected).length === 0) return;
|
|
856
|
-
for (const id2 of locationIdsInRequest(
|
|
856
|
+
for (const id2 of locationIdsInRequest(path67, opts)) {
|
|
857
857
|
const name2 = rules.protected[id2];
|
|
858
858
|
if (name2) throw new Error(protectedRefusalMessage(name2, false));
|
|
859
859
|
}
|
|
@@ -1013,8 +1013,8 @@ var init_ghl_client = __esm({
|
|
|
1013
1013
|
Version: version || GHL_API_VERSION
|
|
1014
1014
|
};
|
|
1015
1015
|
}
|
|
1016
|
-
buildUrl(
|
|
1017
|
-
const url = new URL(
|
|
1016
|
+
buildUrl(path67, params) {
|
|
1017
|
+
const url = new URL(path67, GHL_BASE_URL);
|
|
1018
1018
|
if (params) {
|
|
1019
1019
|
for (const [key2, value] of Object.entries(params)) {
|
|
1020
1020
|
if (value !== void 0 && value !== null) {
|
|
@@ -1024,19 +1024,19 @@ var init_ghl_client = __esm({
|
|
|
1024
1024
|
}
|
|
1025
1025
|
return url.toString();
|
|
1026
1026
|
}
|
|
1027
|
-
async request(method,
|
|
1027
|
+
async request(method, path67, options = {}, attempt2 = 0) {
|
|
1028
1028
|
this.signal?.throwIfAborted();
|
|
1029
1029
|
if (inBuildRequestScope() && method === "GET" && !inAccountReadAttempt()) {
|
|
1030
|
-
return retryAccountRead(() => this.request(method,
|
|
1030
|
+
return retryAccountRead(() => this.request(method, path67, options, attempt2), `${method} ${path67}`);
|
|
1031
1031
|
}
|
|
1032
1032
|
if (attempt2 === 0) {
|
|
1033
|
-
assertWriteAllowed(method,
|
|
1033
|
+
assertWriteAllowed(method, path67, {
|
|
1034
1034
|
params: options.params,
|
|
1035
1035
|
body: options.body,
|
|
1036
1036
|
defaultLocationId: this.defaultLocationId
|
|
1037
1037
|
});
|
|
1038
1038
|
}
|
|
1039
|
-
const url = this.buildUrl(
|
|
1039
|
+
const url = this.buildUrl(path67, options.params);
|
|
1040
1040
|
const headers = this.buildHeaders(options.version);
|
|
1041
1041
|
if (options.form) delete headers["Content-Type"];
|
|
1042
1042
|
if (this.beforeAttempt) {
|
|
@@ -1064,14 +1064,14 @@ var init_ghl_client = __esm({
|
|
|
1064
1064
|
clearTimeout(timeout);
|
|
1065
1065
|
this.signal?.throwIfAborted();
|
|
1066
1066
|
if (error instanceof Error && error.name === "AbortError") {
|
|
1067
|
-
throw requestError(new Error(`Request timeout (30s): ${method} ${
|
|
1067
|
+
throw requestError(new Error(`Request timeout (30s): ${method} ${path67}`));
|
|
1068
1068
|
}
|
|
1069
1069
|
if (!inBuildRequestScope() && !options.noRetry && !options.form && attempt2 < MAX_RETRIES) {
|
|
1070
1070
|
const delay5 = computeRetryDelay(null, attempt2, BASE_DELAY_MS);
|
|
1071
|
-
process.stderr.write(`[ghl-mcp] Network error on ${method} ${
|
|
1071
|
+
process.stderr.write(`[ghl-mcp] Network error on ${method} ${path67}, retry ${attempt2 + 1}/${MAX_RETRIES} in ${delay5}ms
|
|
1072
1072
|
`);
|
|
1073
1073
|
await new Promise((r) => setTimeout(r, delay5));
|
|
1074
|
-
return this.request(method,
|
|
1074
|
+
return this.request(method, path67, options, attempt2 + 1);
|
|
1075
1075
|
}
|
|
1076
1076
|
throw requestError(error);
|
|
1077
1077
|
} finally {
|
|
@@ -1079,10 +1079,10 @@ var init_ghl_client = __esm({
|
|
|
1079
1079
|
}
|
|
1080
1080
|
if (!inBuildRequestScope() && !options.noRetry && !options.form && (response.status === 429 || response.status >= 500) && attempt2 < MAX_RETRIES) {
|
|
1081
1081
|
const delay5 = computeRetryDelay(response.headers.get("Retry-After"), attempt2, BASE_DELAY_MS);
|
|
1082
|
-
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${
|
|
1082
|
+
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path67}, retry ${attempt2 + 1}/${MAX_RETRIES} in ${delay5}ms
|
|
1083
1083
|
`);
|
|
1084
1084
|
await new Promise((r) => setTimeout(r, delay5));
|
|
1085
|
-
return this.request(method,
|
|
1085
|
+
return this.request(method, path67, options, attempt2 + 1);
|
|
1086
1086
|
}
|
|
1087
1087
|
if (!response.ok) {
|
|
1088
1088
|
let errorBody = "";
|
|
@@ -1091,7 +1091,7 @@ var init_ghl_client = __esm({
|
|
|
1091
1091
|
} catch {
|
|
1092
1092
|
}
|
|
1093
1093
|
throw requestError(new Error(
|
|
1094
|
-
`GHL API Error ${response.status} ${response.statusText}: ${method} ${
|
|
1094
|
+
`GHL API Error ${response.status} ${response.statusText}: ${method} ${path67}
|
|
1095
1095
|
${errorBody}`
|
|
1096
1096
|
));
|
|
1097
1097
|
}
|
|
@@ -1113,20 +1113,20 @@ ${errorBody}`
|
|
|
1113
1113
|
combined?.dispose();
|
|
1114
1114
|
}
|
|
1115
1115
|
}
|
|
1116
|
-
async get(
|
|
1117
|
-
return this.request("GET",
|
|
1116
|
+
async get(path67, options) {
|
|
1117
|
+
return this.request("GET", path67, options);
|
|
1118
1118
|
}
|
|
1119
|
-
async post(
|
|
1120
|
-
return this.request("POST",
|
|
1119
|
+
async post(path67, options) {
|
|
1120
|
+
return this.request("POST", path67, options);
|
|
1121
1121
|
}
|
|
1122
|
-
async put(
|
|
1123
|
-
return this.request("PUT",
|
|
1122
|
+
async put(path67, options) {
|
|
1123
|
+
return this.request("PUT", path67, options);
|
|
1124
1124
|
}
|
|
1125
|
-
async patch(
|
|
1126
|
-
return this.request("PATCH",
|
|
1125
|
+
async patch(path67, options) {
|
|
1126
|
+
return this.request("PATCH", path67, options);
|
|
1127
1127
|
}
|
|
1128
|
-
async delete(
|
|
1129
|
-
return this.request("DELETE",
|
|
1128
|
+
async delete(path67, options) {
|
|
1129
|
+
return this.request("DELETE", path67, options);
|
|
1130
1130
|
}
|
|
1131
1131
|
/**
|
|
1132
1132
|
* Helper: resolves locationId from args or falls back to default
|
|
@@ -1703,26 +1703,26 @@ function decodeName(name2) {
|
|
|
1703
1703
|
return name2;
|
|
1704
1704
|
}
|
|
1705
1705
|
}
|
|
1706
|
-
function maskCredential(value, rec2,
|
|
1706
|
+
function maskCredential(value, rec2, path67, component = "=") {
|
|
1707
1707
|
if (!value.trim() || MASK_PRESENT_RE.test(value) || value.includes("[MASKED") || isMergeOnly(value)) return value;
|
|
1708
1708
|
const m = CREDENTIAL_SCHEME_RE.exec(value);
|
|
1709
1709
|
const real = m ? m[2] : value;
|
|
1710
1710
|
const token2 = maskToken(real);
|
|
1711
|
-
rec2.pairs.push({ path: pathKey(
|
|
1711
|
+
rec2.pairs.push({ path: pathKey(path67), group: groupKey(path67), component, token: token2, real });
|
|
1712
1712
|
return m ? `${m[1]}${token2}` : token2;
|
|
1713
1713
|
}
|
|
1714
1714
|
function looksLikeCredential(value) {
|
|
1715
1715
|
return CREDENTIAL_SCHEME_RE.test(value) && !isMergeOnly(value);
|
|
1716
1716
|
}
|
|
1717
|
-
function maskUrl(url, rec2 = newRecorder(),
|
|
1718
|
-
let out = url.replace(USERINFO_RE, (_m, pre, pass, at) => `${pre}${maskCredential(pass, rec2,
|
|
1717
|
+
function maskUrl(url, rec2 = newRecorder(), path67 = []) {
|
|
1718
|
+
let out = url.replace(USERINFO_RE, (_m, pre, pass, at) => `${pre}${maskCredential(pass, rec2, path67, "@")}${at}`);
|
|
1719
1719
|
const seen = /* @__PURE__ */ new Map();
|
|
1720
1720
|
out = out.replace(QUERY_RE, (m, sep6, name2, val) => {
|
|
1721
1721
|
const decoded2 = decodeName(name2);
|
|
1722
1722
|
const n = seen.get(decoded2) ?? 0;
|
|
1723
1723
|
seen.set(decoded2, n + 1);
|
|
1724
1724
|
if (!val || !isSensitiveName(decoded2)) return m;
|
|
1725
|
-
return `${sep6}${name2}=${maskCredential(val, rec2,
|
|
1725
|
+
return `${sep6}${name2}=${maskCredential(val, rec2, path67, `?${decoded2}#${n}`)}`;
|
|
1726
1726
|
});
|
|
1727
1727
|
return out;
|
|
1728
1728
|
}
|
|
@@ -1746,11 +1746,11 @@ function segmentsFor(items) {
|
|
|
1746
1746
|
return `[${index}]`;
|
|
1747
1747
|
});
|
|
1748
1748
|
}
|
|
1749
|
-
function maskWebhookBag(node, rec2,
|
|
1749
|
+
function maskWebhookBag(node, rec2, path67) {
|
|
1750
1750
|
if (Array.isArray(node)) {
|
|
1751
1751
|
const segs = segmentsFor(node);
|
|
1752
1752
|
node.forEach((item, i) => {
|
|
1753
|
-
const at = [...
|
|
1753
|
+
const at = [...path67, segs[i]];
|
|
1754
1754
|
if (typeof item === "string") {
|
|
1755
1755
|
if (looksLikeCredential(item)) node[i] = maskCredential(item, rec2, at);
|
|
1756
1756
|
} else maskWebhookBag(item, rec2, at);
|
|
@@ -1762,7 +1762,7 @@ function maskWebhookBag(node, rec2, path66) {
|
|
|
1762
1762
|
const label2 = labelProp ? node[labelProp] : void 0;
|
|
1763
1763
|
for (const [prop, val] of Object.entries(node)) {
|
|
1764
1764
|
if (prop === labelProp) continue;
|
|
1765
|
-
const at = [...
|
|
1765
|
+
const at = [...path67, prop];
|
|
1766
1766
|
if (typeof val === "string") {
|
|
1767
1767
|
if (prop === "value" && typeof label2 === "string") {
|
|
1768
1768
|
if (isSensitiveName(label2) || looksLikeCredential(val)) node[prop] = maskCredential(val, rec2, at);
|
|
@@ -1782,21 +1782,21 @@ function isWebhookNode(o) {
|
|
|
1782
1782
|
function isWebhookSummary(o) {
|
|
1783
1783
|
return (Array.isArray(o.headers) || isRecord2(o.headers)) && (typeof o.url === "string" || typeof o.method === "string");
|
|
1784
1784
|
}
|
|
1785
|
-
function walk(node, rec2,
|
|
1785
|
+
function walk(node, rec2, path67) {
|
|
1786
1786
|
if (Array.isArray(node)) {
|
|
1787
|
-
node.forEach((item, i) => walk(item, rec2, [...
|
|
1787
|
+
node.forEach((item, i) => walk(item, rec2, [...path67, `[${i}]`]));
|
|
1788
1788
|
return;
|
|
1789
1789
|
}
|
|
1790
1790
|
if (!isRecord2(node)) return;
|
|
1791
1791
|
if (isWebhookNode(node)) {
|
|
1792
|
-
maskWebhookBag(node.attributes, rec2, [...
|
|
1792
|
+
maskWebhookBag(node.attributes, rec2, [...path67, "attributes"]);
|
|
1793
1793
|
return;
|
|
1794
1794
|
}
|
|
1795
1795
|
if (isWebhookSummary(node)) {
|
|
1796
|
-
maskWebhookBag(node, rec2,
|
|
1796
|
+
maskWebhookBag(node, rec2, path67);
|
|
1797
1797
|
return;
|
|
1798
1798
|
}
|
|
1799
|
-
for (const [k, val] of Object.entries(node)) walk(val, rec2, [...
|
|
1799
|
+
for (const [k, val] of Object.entries(node)) walk(val, rec2, [...path67, k]);
|
|
1800
1800
|
}
|
|
1801
1801
|
function maskWorkflowSecrets(data) {
|
|
1802
1802
|
const copy = structuredClone(data);
|
|
@@ -1954,11 +1954,11 @@ function containsMask(value) {
|
|
|
1954
1954
|
function maskedWriteRefusal(where, token2) {
|
|
1955
1955
|
return `Refused: ${where} still carries a masked secret (${token2}). GHL Command hides webhook keys when it shows a workflow, and saving the placeholder would replace the real key and break that webhook. A mask is kept only in the same field of the same step it was read from (the real key is then kept for you); anywhere else, type the real value. Nothing was saved.`;
|
|
1956
1956
|
}
|
|
1957
|
-
function mapStrings(node,
|
|
1957
|
+
function mapStrings(node, path67, fn) {
|
|
1958
1958
|
if (Array.isArray(node)) {
|
|
1959
1959
|
const segs = segmentsFor(node);
|
|
1960
1960
|
node.forEach((item, i) => {
|
|
1961
|
-
const at = [...
|
|
1961
|
+
const at = [...path67, segs[i]];
|
|
1962
1962
|
if (typeof item === "string") node[i] = fn(at, item);
|
|
1963
1963
|
else mapStrings(item, at, fn);
|
|
1964
1964
|
});
|
|
@@ -1966,15 +1966,15 @@ function mapStrings(node, path66, fn) {
|
|
|
1966
1966
|
}
|
|
1967
1967
|
if (!isRecord2(node)) return;
|
|
1968
1968
|
for (const [k, v] of Object.entries(node)) {
|
|
1969
|
-
if (typeof v === "string") node[k] = fn([...
|
|
1970
|
-
else if (v !== null && typeof v === "object") mapStrings(v, [...
|
|
1969
|
+
if (typeof v === "string") node[k] = fn([...path67, k], v);
|
|
1970
|
+
else if (v !== null && typeof v === "object") mapStrings(v, [...path67, k], fn);
|
|
1971
1971
|
}
|
|
1972
1972
|
}
|
|
1973
1973
|
function stringsByGroup(node) {
|
|
1974
1974
|
const out = { byPath: /* @__PURE__ */ new Map(), byGroup: /* @__PURE__ */ new Map() };
|
|
1975
|
-
mapStrings(structuredClone(node), [], (
|
|
1976
|
-
out.byPath.set(pathKey(
|
|
1977
|
-
const g = groupKey(
|
|
1975
|
+
mapStrings(structuredClone(node), [], (path67, str16) => {
|
|
1976
|
+
out.byPath.set(pathKey(path67), str16);
|
|
1977
|
+
const g = groupKey(path67);
|
|
1978
1978
|
out.byGroup.set(g, [...out.byGroup.get(g) ?? [], str16]);
|
|
1979
1979
|
return str16;
|
|
1980
1980
|
});
|
|
@@ -2000,10 +2000,10 @@ function restoreMaskedSecrets(incoming, current) {
|
|
|
2000
2000
|
const shown = typeof action.id === "string" ? shownById.get(action.id) : void 0;
|
|
2001
2001
|
const sent = stringsByGroup(action);
|
|
2002
2002
|
const copy = structuredClone(action);
|
|
2003
|
-
mapStrings(copy, [], (
|
|
2003
|
+
mapStrings(copy, [], (path67, str16) => {
|
|
2004
2004
|
if (!MASK_PRESENT_RE.test(str16)) return str16;
|
|
2005
|
-
const pk = pathKey(
|
|
2006
|
-
const gk = groupKey(
|
|
2005
|
+
const pk = pathKey(path67);
|
|
2006
|
+
const gk = groupKey(path67);
|
|
2007
2007
|
const pick3 = (component, token2) => {
|
|
2008
2008
|
const cg = componentGroup(component);
|
|
2009
2009
|
const reals = new Set(pairs.filter((p) => p.group === gk && componentGroup(p.component) === cg && p.token === token2).map((p) => p.real));
|
|
@@ -2056,8 +2056,8 @@ var init_secret_mask = __esm({
|
|
|
2056
2056
|
SENSITIVE_NAME_RE = /auth|key|token|secret|signature|signing|passw|pwd|credential|cookie|session|hmac|private|^sig$/i;
|
|
2057
2057
|
CREDENTIAL_SCHEME_RE = /^(\s*(?:bearer|basic|token)\s+)(\S[\s\S]*)$/i;
|
|
2058
2058
|
MERGE_ONLY_RE = /^\s*(?:\{\{[^{}]+\}\}\s*)+$/;
|
|
2059
|
-
pathKey = (
|
|
2060
|
-
groupKey = (
|
|
2059
|
+
pathKey = (path67) => JSON.stringify(path67);
|
|
2060
|
+
groupKey = (path67) => JSON.stringify(path67.map((seg) => seg.replace(/#\d+$/, "")));
|
|
2061
2061
|
componentGroup = (c) => c.replace(/#\d+$/, "");
|
|
2062
2062
|
TOKEN_SRC = String.raw`\[MASKED(?: …[^\]\s]{1,4})?\]`;
|
|
2063
2063
|
USERINFO_RE = new RegExp(String.raw`^(\s*[a-z][a-z0-9+.-]*:\/\/[^/@:?#\s]+:)((?:${TOKEN_SRC}|[^/@?#\s])+)(@)`, "i");
|
|
@@ -4954,8 +4954,8 @@ function buildEvent(event, opts) {
|
|
|
4954
4954
|
}
|
|
4955
4955
|
function ephemeralId() {
|
|
4956
4956
|
if (!processEphemeralId) {
|
|
4957
|
-
const
|
|
4958
|
-
processEphemeralId =
|
|
4957
|
+
const crypto16 = require("node:crypto");
|
|
4958
|
+
processEphemeralId = crypto16.randomBytes(16).toString("hex");
|
|
4959
4959
|
}
|
|
4960
4960
|
return processEphemeralId;
|
|
4961
4961
|
}
|
|
@@ -8760,7 +8760,7 @@ function pinnedLookup(address, family) {
|
|
|
8760
8760
|
};
|
|
8761
8761
|
}
|
|
8762
8762
|
function realRequest(url, address, family, signal) {
|
|
8763
|
-
return new Promise((
|
|
8763
|
+
return new Promise((resolve15, reject) => {
|
|
8764
8764
|
const req = https.request(url, {
|
|
8765
8765
|
method: "GET",
|
|
8766
8766
|
signal,
|
|
@@ -8772,7 +8772,7 @@ function realRequest(url, address, family, signal) {
|
|
|
8772
8772
|
lookup: pinnedLookup(address, family)
|
|
8773
8773
|
}, (res) => {
|
|
8774
8774
|
const len = Number(res.headers["content-length"]);
|
|
8775
|
-
|
|
8775
|
+
resolve15({
|
|
8776
8776
|
status: res.statusCode ?? 0,
|
|
8777
8777
|
location: typeof res.headers.location === "string" ? res.headers.location : void 0,
|
|
8778
8778
|
contentType: typeof res.headers["content-type"] === "string" ? res.headers["content-type"] : void 0,
|
|
@@ -9773,11 +9773,11 @@ function registerEmailTools(server2, client) {
|
|
|
9773
9773
|
function registerEmailBuilderInternalTools(server2, builderClient, publicClient) {
|
|
9774
9774
|
if (!builderClient) return;
|
|
9775
9775
|
const client = builderClient;
|
|
9776
|
-
async function builderRequest2(method,
|
|
9777
|
-
const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${
|
|
9776
|
+
async function builderRequest2(method, path67, body) {
|
|
9777
|
+
const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path67}`, body ? { body } : {});
|
|
9778
9778
|
if (!response.ok) {
|
|
9779
9779
|
const text4 = await response.text();
|
|
9780
|
-
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${
|
|
9780
|
+
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path67}
|
|
9781
9781
|
${text4}`);
|
|
9782
9782
|
}
|
|
9783
9783
|
const text3 = await response.text();
|
|
@@ -12075,9 +12075,9 @@ function bindingClash(number, agents, exceptAgentId, poolObjects = []) {
|
|
|
12075
12075
|
const pool = poolBinding(a);
|
|
12076
12076
|
if (pool) {
|
|
12077
12077
|
const obj2 = poolObjects.find((p) => poolId(p) === pool);
|
|
12078
|
-
const
|
|
12079
|
-
if (
|
|
12080
|
-
if (
|
|
12078
|
+
const held2 = obj2 ? poolNumbers(obj2) : [];
|
|
12079
|
+
if (held2.includes(number)) return `${number} is in number pool ${pool}, which agent "${String(a.agentName)}" (${String(a.id)}) answers through. Binding it here would take those calls away. Unbind the pool there first, or choose another number.`;
|
|
12080
|
+
if (held2.length === 0) return `Agent "${String(a.agentName)}" (${String(a.id)}) answers through number pool ${pool}, and this API does not say which numbers that pool holds, so ${number} cannot be called free. Check the pool in GoHighLevel (Settings \u2192 Phone Numbers \u2192 Number Pools); if ${number} is not in it, unbind nothing and bind by hand there, or move that agent off the pool first.`;
|
|
12081
12081
|
}
|
|
12082
12082
|
}
|
|
12083
12083
|
return null;
|
|
@@ -12128,9 +12128,9 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
12128
12128
|
const poolBound = agents.filter((a) => poolBinding(a) !== null).map((a) => {
|
|
12129
12129
|
const pool = poolBinding(a);
|
|
12130
12130
|
const obj2 = poolObjects.find((p) => poolId(p) === pool);
|
|
12131
|
-
const
|
|
12132
|
-
for (const n of
|
|
12133
|
-
return { agentId: String(a.id), agentName: String(a.agentName), poolId: pool, poolNumbers:
|
|
12131
|
+
const held2 = obj2 ? poolNumbers(obj2) : [];
|
|
12132
|
+
for (const n of held2) if (!holders.has(n)) holders.set(n, { agentId: String(a.id), agentName: String(a.agentName), via: `pool ${pool}` });
|
|
12133
|
+
return { agentId: String(a.id), agentName: String(a.agentName), poolId: pool, poolNumbers: held2.length ? held2 : "unknown \u2014 this API does not list the pool's numbers" };
|
|
12134
12134
|
});
|
|
12135
12135
|
const poolRouting = poolBound.filter((p) => Array.isArray(p.poolNumbers) && p.poolNumbers.length > 0);
|
|
12136
12136
|
const poolUnproven = poolBound.filter((p) => typeof p.poolNumbers === "string");
|
|
@@ -12325,11 +12325,11 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
12325
12325
|
const holder = others.find((a) => poolBinding(a) === numberPoolId);
|
|
12326
12326
|
if (holder) throw new Error(`Number pool ${numberPoolId} already answers for agent "${String(holder.agentName)}" (${String(holder.id)}). Binding it here would take those calls away. Unbind it there first (update_voice_agent with numberPoolId: null), or choose another pool.`);
|
|
12327
12327
|
const target = poolObjects.find((p) => poolId(p) === numberPoolId);
|
|
12328
|
-
const
|
|
12328
|
+
const held2 = target ? poolNumbers(target) : [];
|
|
12329
12329
|
const othersBound = others.filter((a) => boundNumbers(a).length > 0);
|
|
12330
|
-
if (
|
|
12330
|
+
if (held2.length) {
|
|
12331
12331
|
for (const a of othersBound) {
|
|
12332
|
-
const overlap = boundNumbers(a).filter((n) =>
|
|
12332
|
+
const overlap = boundNumbers(a).filter((n) => held2.includes(n));
|
|
12333
12333
|
if (overlap.length) throw new Error(`Number pool ${numberPoolId} holds ${overlap.join(", ")}, which already answers for agent "${String(a.agentName)}" (${String(a.id)}) by inboundNumber. Binding the pool here would take those calls away. Unbind there first, or choose another pool.`);
|
|
12334
12334
|
}
|
|
12335
12335
|
} else if (othersBound.length) {
|
|
@@ -15472,7 +15472,7 @@ function isPrivateAddress(ip) {
|
|
|
15472
15472
|
return false;
|
|
15473
15473
|
}
|
|
15474
15474
|
function withDeadline(p, ms, why, signal) {
|
|
15475
|
-
return new Promise((
|
|
15475
|
+
return new Promise((resolve15, reject) => {
|
|
15476
15476
|
if (signal?.aborted) {
|
|
15477
15477
|
p.catch(() => void 0);
|
|
15478
15478
|
reject(new Error("aborted"));
|
|
@@ -15487,7 +15487,7 @@ function withDeadline(p, ms, why, signal) {
|
|
|
15487
15487
|
p.then((v) => {
|
|
15488
15488
|
clearTimeout(t);
|
|
15489
15489
|
signal?.removeEventListener("abort", onAbort);
|
|
15490
|
-
|
|
15490
|
+
resolve15(v);
|
|
15491
15491
|
}, (e) => {
|
|
15492
15492
|
clearTimeout(t);
|
|
15493
15493
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -15539,7 +15539,7 @@ function closeTransferBudget(b) {
|
|
|
15539
15539
|
b.agents.https.destroy();
|
|
15540
15540
|
}
|
|
15541
15541
|
function guardedTransfer(url, init) {
|
|
15542
|
-
return new Promise((
|
|
15542
|
+
return new Promise((resolve15, reject) => {
|
|
15543
15543
|
let u;
|
|
15544
15544
|
try {
|
|
15545
15545
|
u = new URL(url);
|
|
@@ -15594,7 +15594,7 @@ function guardedTransfer(url, init) {
|
|
|
15594
15594
|
const kept = unzip ? out.filter(([k]) => !/^(?:content-encoding|content-length)$/i.test(k)) : out;
|
|
15595
15595
|
const chunks = [];
|
|
15596
15596
|
let size2 = 0;
|
|
15597
|
-
const finish3 = (incomplete) => settle2(() =>
|
|
15597
|
+
const finish3 = (incomplete) => settle2(() => resolve15({ status: res.statusCode ?? 0, headers: kept, body: Buffer.concat(chunks), incomplete }));
|
|
15598
15598
|
stream.on("data", (c) => {
|
|
15599
15599
|
size2 += c.length;
|
|
15600
15600
|
init.budget.bytesLeft -= c.length;
|
|
@@ -15650,7 +15650,7 @@ async function guardedPost(url, body, headers, timeoutMs = 2e4) {
|
|
|
15650
15650
|
const left = Math.max(1, timeoutMs - (Date.now() - started));
|
|
15651
15651
|
const u = new URL(url);
|
|
15652
15652
|
const mod = u.protocol === "https:" ? import_node_https.default : import_node_http.default;
|
|
15653
|
-
return new Promise((
|
|
15653
|
+
return new Promise((resolve15, reject) => {
|
|
15654
15654
|
let settled = false;
|
|
15655
15655
|
const chunks = [];
|
|
15656
15656
|
let size2 = 0;
|
|
@@ -15661,7 +15661,7 @@ async function guardedPost(url, body, headers, timeoutMs = 2e4) {
|
|
|
15661
15661
|
f();
|
|
15662
15662
|
};
|
|
15663
15663
|
const req = mod.request(u, { method: "POST", headers: { ...headers, "content-length": String(Buffer.byteLength(body)) }, lookup: publicOnlyLookup }, (res) => {
|
|
15664
|
-
const done = () => settle2(() =>
|
|
15664
|
+
const done = () => settle2(() => resolve15({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") }));
|
|
15665
15665
|
res.on("data", (c) => {
|
|
15666
15666
|
size2 += c.length;
|
|
15667
15667
|
if (size2 <= 1e6) chunks.push(c);
|
|
@@ -15797,14 +15797,14 @@ async function startGuardProxy(opts = {}) {
|
|
|
15797
15797
|
client.on("close", () => up.destroy());
|
|
15798
15798
|
});
|
|
15799
15799
|
server2.on("clientError", (_e, s) => s.destroy());
|
|
15800
|
-
await new Promise((
|
|
15800
|
+
await new Promise((resolve15, reject) => {
|
|
15801
15801
|
const fail = (e) => {
|
|
15802
15802
|
server2.removeListener("listening", ok);
|
|
15803
15803
|
reject(e);
|
|
15804
15804
|
};
|
|
15805
15805
|
const ok = () => {
|
|
15806
15806
|
server2.removeListener("error", fail);
|
|
15807
|
-
|
|
15807
|
+
resolve15();
|
|
15808
15808
|
};
|
|
15809
15809
|
server2.once("error", fail);
|
|
15810
15810
|
server2.once("listening", ok);
|
|
@@ -15862,12 +15862,12 @@ async function readPageFull(client, pageId, fetcher = fetch) {
|
|
|
15862
15862
|
const usedIdToken = headers["token-id"];
|
|
15863
15863
|
headers.Origin = "https://app.gohighlevel.com";
|
|
15864
15864
|
headers.Referer = "https://app.gohighlevel.com/";
|
|
15865
|
-
const
|
|
15866
|
-
const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${
|
|
15865
|
+
const path67 = `/page/${pageId}?locationId=${client.locationId}`;
|
|
15866
|
+
const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path67}`, { method: "GET", headers });
|
|
15867
15867
|
if (!response.ok) {
|
|
15868
15868
|
if (response.status === 401) client.invalidateIdToken?.(usedIdToken);
|
|
15869
15869
|
const text4 = await response.text();
|
|
15870
|
-
throw new Error(`Funnel API Error ${response.status}: GET ${
|
|
15870
|
+
throw new Error(`Funnel API Error ${response.status}: GET ${path67}
|
|
15871
15871
|
${text4}`);
|
|
15872
15872
|
}
|
|
15873
15873
|
const text3 = await response.text();
|
|
@@ -15930,20 +15930,20 @@ function registerFunnelBuilderTools(server2, builderClient) {
|
|
|
15930
15930
|
return;
|
|
15931
15931
|
}
|
|
15932
15932
|
const client = builderClient;
|
|
15933
|
-
async function internalGet(
|
|
15934
|
-
return client.request("GET",
|
|
15933
|
+
async function internalGet(path67) {
|
|
15934
|
+
return client.request("GET", path67);
|
|
15935
15935
|
}
|
|
15936
|
-
async function internalPost(
|
|
15937
|
-
return client.request("POST",
|
|
15936
|
+
async function internalPost(path67, body) {
|
|
15937
|
+
return client.request("POST", path67, body);
|
|
15938
15938
|
}
|
|
15939
|
-
async function internalPut(
|
|
15940
|
-
return client.request("PUT",
|
|
15939
|
+
async function internalPut(path67, body) {
|
|
15940
|
+
return client.request("PUT", path67, body);
|
|
15941
15941
|
}
|
|
15942
|
-
async function internalDelete(
|
|
15943
|
-
return client.request("DELETE",
|
|
15942
|
+
async function internalDelete(path67) {
|
|
15943
|
+
return client.request("DELETE", path67);
|
|
15944
15944
|
}
|
|
15945
|
-
async function funnelRequest(method,
|
|
15946
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
15945
|
+
async function funnelRequest(method, path67, body) {
|
|
15946
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path67}`;
|
|
15947
15947
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
15948
15948
|
const response = await builderRequest(client, method, url, {
|
|
15949
15949
|
...sendsBody ? { body } : {},
|
|
@@ -15956,7 +15956,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
|
|
|
15956
15956
|
});
|
|
15957
15957
|
if (!response.ok) {
|
|
15958
15958
|
const text4 = await response.text();
|
|
15959
|
-
throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
15959
|
+
throw new Error(`Funnel API Error ${response.status}: ${method} ${path67}
|
|
15960
15960
|
${text4}`);
|
|
15961
15961
|
}
|
|
15962
15962
|
const text3 = await response.text();
|
|
@@ -16547,17 +16547,17 @@ function requireCompany(api) {
|
|
|
16547
16547
|
}
|
|
16548
16548
|
return company;
|
|
16549
16549
|
}
|
|
16550
|
-
async function call(api,
|
|
16550
|
+
async function call(api, path67, init) {
|
|
16551
16551
|
const headers = { ...await api.headers(), Origin: "https://app.gohighlevel.com", Referer: "https://app.gohighlevel.com/" };
|
|
16552
|
-
if (MUTATING_PATHS.some((mutating) =>
|
|
16553
|
-
assertWriteAllowed("POST", SVC +
|
|
16552
|
+
if (MUTATING_PATHS.some((mutating) => path67.startsWith(mutating))) {
|
|
16553
|
+
assertWriteAllowed("POST", SVC + path67, { defaultLocationId: api.locationId() });
|
|
16554
16554
|
}
|
|
16555
16555
|
const doFetch = api.fetch ?? fetch;
|
|
16556
|
-
const response = await doFetch(SVC +
|
|
16556
|
+
const response = await doFetch(SVC + path67, { ...init, headers });
|
|
16557
16557
|
const text3 = await response.text();
|
|
16558
16558
|
if (!response.ok) {
|
|
16559
16559
|
const hint = response.status === 401 && /not authorized to access this company/i.test(text3) ? " \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." : "";
|
|
16560
|
-
throw new Error(`GoHighLevel template library ${response.status} on ${
|
|
16560
|
+
throw new Error(`GoHighLevel template library ${response.status} on ${path67}${hint}
|
|
16561
16561
|
${text3.slice(0, 300)}`);
|
|
16562
16562
|
}
|
|
16563
16563
|
return text3 ? JSON.parse(text3) : {};
|
|
@@ -16915,12 +16915,12 @@ function normaliseColour(value) {
|
|
|
16915
16915
|
function renderedPalette(envelope) {
|
|
16916
16916
|
const defs = pageVariables(envelope);
|
|
16917
16917
|
const paints = [];
|
|
16918
|
-
const walk2 = (node,
|
|
16919
|
-
if (Array.isArray(node)) return void node.forEach((x, i) => walk2(x, `${
|
|
16918
|
+
const walk2 = (node, path67, prop) => {
|
|
16919
|
+
if (Array.isArray(node)) return void node.forEach((x, i) => walk2(x, `${path67}[${i}]`, prop));
|
|
16920
16920
|
if (isRecord11(node)) {
|
|
16921
16921
|
for (const [k, x] of Object.entries(node)) {
|
|
16922
16922
|
if (DEFINITION_KEYS.has(k) || k.startsWith("--")) continue;
|
|
16923
|
-
walk2(x, `${
|
|
16923
|
+
walk2(x, `${path67}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
|
|
16924
16924
|
}
|
|
16925
16925
|
return;
|
|
16926
16926
|
}
|
|
@@ -16930,7 +16930,7 @@ function renderedPalette(envelope) {
|
|
|
16930
16930
|
const resolved2 = resolveValue(raw, defs);
|
|
16931
16931
|
const colour = normaliseColour(resolved2);
|
|
16932
16932
|
if (colour === null && resolved2 !== null) return;
|
|
16933
|
-
paints.push({ path:
|
|
16933
|
+
paints.push({ path: path67, prop, kind: kindOf(prop), colour });
|
|
16934
16934
|
};
|
|
16935
16935
|
walk2(envelope, "", "");
|
|
16936
16936
|
const face = (name2, settingsKey) => {
|
|
@@ -17025,7 +17025,7 @@ var init_rendered_palette = __esm({
|
|
|
17025
17025
|
VISIBLE_CHANGE = 0.05;
|
|
17026
17026
|
BARELY_SHARE = 0.5;
|
|
17027
17027
|
CARRIES_COLOUR = 0.02;
|
|
17028
|
-
paintGroup = (
|
|
17028
|
+
paintGroup = (path67) => /^\.(sections|popups)\[\d+\]/.exec(path67)?.[0]?.slice(1) ?? "page";
|
|
17029
17029
|
looksDifferent = (a, b) => {
|
|
17030
17030
|
if (a === b) return false;
|
|
17031
17031
|
if (a === "transparent" || b === "transparent" || a.startsWith("rgba") || b.startsWith("rgba")) return a !== b;
|
|
@@ -22272,9 +22272,9 @@ function removalHoldBack(opts) {
|
|
|
22272
22272
|
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`;
|
|
22273
22273
|
return null;
|
|
22274
22274
|
}
|
|
22275
|
-
function brandedPath(
|
|
22275
|
+
function brandedPath(path67, name2, taken) {
|
|
22276
22276
|
const normalize2 = (s) => s.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
22277
|
-
const base = normalize2(
|
|
22277
|
+
const base = normalize2(path67 ?? "") || name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
|
|
22278
22278
|
const used = new Set([...taken].map(normalize2));
|
|
22279
22279
|
let candidate = `${base}-branded`;
|
|
22280
22280
|
for (let n = 2; used.has(candidate); n++) candidate = `${base}-branded-${n}`;
|
|
@@ -22302,9 +22302,9 @@ function rewriteTemplateLinks(envelope, destinations) {
|
|
|
22302
22302
|
representation = "page";
|
|
22303
22303
|
matches = destinations.filter((d2) => d2.pageId === preview[2]);
|
|
22304
22304
|
} else if (!/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(from) && !from.startsWith("#")) {
|
|
22305
|
-
const [
|
|
22306
|
-
suffix = from.slice(
|
|
22307
|
-
matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(
|
|
22305
|
+
const [path67] = from.split(/[?#]/);
|
|
22306
|
+
suffix = from.slice(path67.length);
|
|
22307
|
+
matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(path67));
|
|
22308
22308
|
}
|
|
22309
22309
|
}
|
|
22310
22310
|
if (matches.length !== 1) {
|
|
@@ -22387,8 +22387,8 @@ function registerPageStudioTools(server2, builderClient) {
|
|
|
22387
22387
|
}
|
|
22388
22388
|
return { map: out, truncated };
|
|
22389
22389
|
}
|
|
22390
|
-
async function funnelRequest(method,
|
|
22391
|
-
const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${
|
|
22390
|
+
async function funnelRequest(method, path67, body) {
|
|
22391
|
+
const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path67}`, {
|
|
22392
22392
|
...body ? { body } : {},
|
|
22393
22393
|
headers: {
|
|
22394
22394
|
// Internal-API calls require the app origin (autosave needs the
|
|
@@ -22397,7 +22397,7 @@ function registerPageStudioTools(server2, builderClient) {
|
|
|
22397
22397
|
Referer: "https://app.gohighlevel.com/"
|
|
22398
22398
|
}
|
|
22399
22399
|
});
|
|
22400
|
-
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
22400
|
+
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path67}
|
|
22401
22401
|
${await response.text()}`);
|
|
22402
22402
|
const text3 = await response.text();
|
|
22403
22403
|
return text3 ? JSON.parse(text3) : {};
|
|
@@ -22905,7 +22905,7 @@ ${await response.text()}`);
|
|
|
22905
22905
|
}
|
|
22906
22906
|
const confirmed = /* @__PURE__ */ new Set();
|
|
22907
22907
|
for (let attempt2 = 0; accepted.length && attempt2 < 5; attempt2++) {
|
|
22908
|
-
if (attempt2) await new Promise((
|
|
22908
|
+
if (attempt2) await new Promise((resolve15) => setTimeout(resolve15, attempt2 * 1e3));
|
|
22909
22909
|
try {
|
|
22910
22910
|
const listed = await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=${FUNNEL_PAGE}`);
|
|
22911
22911
|
const current = listed.funnels?.find((item) => item._id === funnel._id);
|
|
@@ -23269,13 +23269,13 @@ function cleanFormDataForSave(formData) {
|
|
|
23269
23269
|
const cleaned = dropRedundantCaseCollidingKeys(formData);
|
|
23270
23270
|
return isJsonObject(cleaned) ? cleaned : formData;
|
|
23271
23271
|
}
|
|
23272
|
-
async function formApiRequest(client, method,
|
|
23273
|
-
const url = `https://backend.leadconnectorhq.com/forms${
|
|
23272
|
+
async function formApiRequest(client, method, path67, body) {
|
|
23273
|
+
const url = `https://backend.leadconnectorhq.com/forms${path67}`;
|
|
23274
23274
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
23275
23275
|
const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
|
|
23276
23276
|
if (!response.ok) {
|
|
23277
23277
|
const text4 = await response.text();
|
|
23278
|
-
throw new Error(`Form API Error ${response.status}: ${method} ${
|
|
23278
|
+
throw new Error(`Form API Error ${response.status}: ${method} ${path67}
|
|
23279
23279
|
${text4}`);
|
|
23280
23280
|
}
|
|
23281
23281
|
const text3 = await response.text();
|
|
@@ -23317,7 +23317,7 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
|
|
|
23317
23317
|
);
|
|
23318
23318
|
return;
|
|
23319
23319
|
}
|
|
23320
|
-
const formRequest = (method,
|
|
23320
|
+
const formRequest = (method, path67, body) => formApiRequest(client, method, path67, body);
|
|
23321
23321
|
server2.tool(
|
|
23322
23322
|
"get_form_full",
|
|
23323
23323
|
"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.",
|
|
@@ -23451,10 +23451,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
|
|
|
23451
23451
|
},
|
|
23452
23452
|
async ({ formId, limit, skip }) => {
|
|
23453
23453
|
try {
|
|
23454
|
-
let
|
|
23455
|
-
if (formId)
|
|
23456
|
-
if (skip)
|
|
23457
|
-
const result = await formRequest("GET",
|
|
23454
|
+
let path67 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
|
|
23455
|
+
if (formId) path67 += `&formId=${formId}`;
|
|
23456
|
+
if (skip) path67 += `&skip=${skip}`;
|
|
23457
|
+
const result = await formRequest("GET", path67);
|
|
23458
23458
|
return {
|
|
23459
23459
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
23460
23460
|
};
|
|
@@ -23559,7 +23559,7 @@ function parseContact(raw) {
|
|
|
23559
23559
|
};
|
|
23560
23560
|
}
|
|
23561
23561
|
async function searchByEmail(client, locationId2, email) {
|
|
23562
|
-
const paged = await readContactPages((
|
|
23562
|
+
const paged = await readContactPages((path67, o) => client.get(path67, o), { locationId: locationId2, query: email, limit: 20 });
|
|
23563
23563
|
if (!paged) throw new SearchUnreadable("the contact search answered in a shape that could not be read");
|
|
23564
23564
|
const want = email.toLowerCase();
|
|
23565
23565
|
const ids = [];
|
|
@@ -24044,13 +24044,13 @@ var init_funnel_qa = __esm({
|
|
|
24044
24044
|
function registerPipelineBuilderTools(server2, builderClient) {
|
|
24045
24045
|
if (!builderClient) return;
|
|
24046
24046
|
const client = builderClient;
|
|
24047
|
-
async function pipelineRequest(method,
|
|
24048
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
24047
|
+
async function pipelineRequest(method, path67, body) {
|
|
24048
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path67}`;
|
|
24049
24049
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT" || method === "PATCH");
|
|
24050
24050
|
const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
|
|
24051
24051
|
if (!response.ok) {
|
|
24052
24052
|
const text4 = await response.text();
|
|
24053
|
-
throw new Error(`Pipeline API Error ${response.status}: ${method} ${
|
|
24053
|
+
throw new Error(`Pipeline API Error ${response.status}: ${method} ${path67}
|
|
24054
24054
|
${text4}`);
|
|
24055
24055
|
}
|
|
24056
24056
|
const text3 = await response.text();
|
|
@@ -24340,6 +24340,8 @@ var init_features = __esm({
|
|
|
24340
24340
|
BY_WORKFLOW_REF = {
|
|
24341
24341
|
"workflow.speed_to_lead": "speed_to_lead",
|
|
24342
24342
|
"workflow.missed_call": "missed_call",
|
|
24343
|
+
// F6 (2026-10-29): a missed caller with no card gets one. Goes with the text-back it hangs off.
|
|
24344
|
+
"workflow.missed_call_card": "missed_call",
|
|
24343
24345
|
"workflow.lead_nurture": "nurture",
|
|
24344
24346
|
"workflow.win_back": "winback",
|
|
24345
24347
|
"workflow.appointment_booked": "booking",
|
|
@@ -26084,10 +26086,14 @@ var init_registry = __esm({
|
|
|
26084
26086
|
whatItIs: "Google sign-in gives this machine permission to read your calendar without pasting a password here.",
|
|
26085
26087
|
checkedOn: null,
|
|
26086
26088
|
gives: "Adds your own day beside your clients' appointments as a separate part of the brief.",
|
|
26087
|
-
|
|
26088
|
-
|
|
26089
|
+
// ba's Hub audit (2026-10-29): the guide read as if this connects today, while CHANGELOG 3.92.2 and the card say
|
|
26090
|
+
// CALENDAR_SETUP_MESSAGE. It does not connect until GHL Command ships its own Google sign-in, so the guide says so
|
|
26091
|
+
// first and the steps below are what it will be.
|
|
26092
|
+
needs: ["A Google account with a calendar you can read.", "Google sign-in for GHL Command, which is not available yet."],
|
|
26093
|
+
before: `${CALENDAR_SETUP_MESSAGE} Choosing Connect says so and saves nothing. When Google sign-in arrives in an update, these are the steps:`,
|
|
26089
26094
|
steps: ["On the Google Calendar card on the Connections screen, choose Connect to open Google sign-in.", "Select your Google account and allow read-only calendar access.", "Return to the Google Calendar card and choose Finish. Your sign-in is saved only after your calendars can be read."],
|
|
26090
|
-
say: [
|
|
26095
|
+
say: [],
|
|
26096
|
+
// none until Google sign-in ships: each one would fail (ba audit 2026-10-29)
|
|
26091
26097
|
revoke: "Choose Disconnect on this system's Connections card to remove the saved sign-in and request revocation with Google. You can also remove the app in your Google Account's third-party connections at myaccount.google.com/connections.",
|
|
26092
26098
|
tools: { connect: "connect_google_calendar", disconnect: "connect_google_calendar", read: "read_my_calendar" }
|
|
26093
26099
|
},
|
|
@@ -26476,7 +26482,7 @@ function calendarSetupDetail() {
|
|
|
26476
26482
|
}
|
|
26477
26483
|
function openBrowser(url) {
|
|
26478
26484
|
const [file3, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
26479
|
-
return new Promise((
|
|
26485
|
+
return new Promise((resolve15) => (0, import_node_child_process.execFile)(file3, args, { timeout: 5e3 }, (error) => resolve15(!error)));
|
|
26480
26486
|
}
|
|
26481
26487
|
function parsed(schema, value) {
|
|
26482
26488
|
const result = schema.safeParse(value);
|
|
@@ -26573,16 +26579,16 @@ function createGoogleCalendar(deps = {}) {
|
|
|
26573
26579
|
requireOnlyCalendar(result.scope);
|
|
26574
26580
|
return result.access_token;
|
|
26575
26581
|
}
|
|
26576
|
-
async function pages(
|
|
26582
|
+
async function pages(path67, access, item, query = {}) {
|
|
26577
26583
|
const result = [];
|
|
26578
26584
|
const seen = /* @__PURE__ */ new Set();
|
|
26579
26585
|
let pageToken;
|
|
26580
26586
|
for (let page2 = 0; page2 < 100; page2++) {
|
|
26581
|
-
const url = new URL(`${API}${
|
|
26587
|
+
const url = new URL(`${API}${path67}`);
|
|
26582
26588
|
for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
|
|
26583
26589
|
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
|
26584
26590
|
const data = parsed(
|
|
26585
|
-
import_zod53.z.object({ items: import_zod53.z.array(item).optional(), nextPageToken: import_zod53.z.string().optional(), kind: import_zod53.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (
|
|
26591
|
+
import_zod53.z.object({ items: import_zod53.z.array(item).optional(), nextPageToken: import_zod53.z.string().optional(), kind: import_zod53.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path67.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
|
|
26586
26592
|
await request2(url.href, { headers: { Authorization: `Bearer ${access}` } })
|
|
26587
26593
|
);
|
|
26588
26594
|
result.push(...data.items ?? []);
|
|
@@ -26634,9 +26640,9 @@ function createGoogleCalendar(deps = {}) {
|
|
|
26634
26640
|
server2.requestTimeout = 5e3;
|
|
26635
26641
|
server2.headersTimeout = 5e3;
|
|
26636
26642
|
try {
|
|
26637
|
-
await new Promise((
|
|
26643
|
+
await new Promise((resolve15, reject) => {
|
|
26638
26644
|
server2.once("error", reject);
|
|
26639
|
-
server2.listen(0, "127.0.0.1",
|
|
26645
|
+
server2.listen(0, "127.0.0.1", resolve15);
|
|
26640
26646
|
});
|
|
26641
26647
|
} catch {
|
|
26642
26648
|
server2.close();
|
|
@@ -27561,7 +27567,7 @@ var init_supabase = __esm({
|
|
|
27561
27567
|
}
|
|
27562
27568
|
token;
|
|
27563
27569
|
fetchFn;
|
|
27564
|
-
async call(what,
|
|
27570
|
+
async call(what, path67, init = {}) {
|
|
27565
27571
|
const headers = { Authorization: `Bearer ${this.token}`, Accept: "application/json" };
|
|
27566
27572
|
let body;
|
|
27567
27573
|
if (init.form) body = init.form;
|
|
@@ -27571,7 +27577,7 @@ var init_supabase = __esm({
|
|
|
27571
27577
|
}
|
|
27572
27578
|
let res;
|
|
27573
27579
|
try {
|
|
27574
|
-
res = await this.fetchFn(`${MANAGEMENT_API}${
|
|
27580
|
+
res = await this.fetchFn(`${MANAGEMENT_API}${path67}`, { method: init.method ?? "GET", headers, body, redirect: "error" });
|
|
27575
27581
|
} catch {
|
|
27576
27582
|
throw new BrainApiError(`${what}: Supabase could not be reached. Nothing on your side changed.`, null);
|
|
27577
27583
|
}
|
|
@@ -28298,8 +28304,8 @@ function redactConnectionKey(value, key2) {
|
|
|
28298
28304
|
return value;
|
|
28299
28305
|
}
|
|
28300
28306
|
function stripeFetcher(key2, signal) {
|
|
28301
|
-
return async (
|
|
28302
|
-
const url = new URL(`https://api.stripe.com${
|
|
28307
|
+
return async (path67, query) => {
|
|
28308
|
+
const url = new URL(`https://api.stripe.com${path67}`);
|
|
28303
28309
|
for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
|
|
28304
28310
|
const res = await fetch(url, { headers: { Authorization: `Bearer ${key2}` }, signal });
|
|
28305
28311
|
if (!res.ok) {
|
|
@@ -28448,11 +28454,11 @@ var init_handlers = __esm({
|
|
|
28448
28454
|
});
|
|
28449
28455
|
|
|
28450
28456
|
// src/command-os/connectors/stripe-read.ts
|
|
28451
|
-
async function listAll(f,
|
|
28457
|
+
async function listAll(f, path67, schema, extra = {}) {
|
|
28452
28458
|
const out = [];
|
|
28453
28459
|
let after;
|
|
28454
28460
|
for (let page2 = 0; page2 < 20; page2++) {
|
|
28455
|
-
const result = import_zod58.z.object({ data: import_zod58.z.array(schema), has_more: import_zod58.z.boolean().optional() }).parse(await f(
|
|
28461
|
+
const result = import_zod58.z.object({ data: import_zod58.z.array(schema), has_more: import_zod58.z.boolean().optional() }).parse(await f(path67, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
|
|
28456
28462
|
out.push(...result.data);
|
|
28457
28463
|
if (!result.has_more) return out;
|
|
28458
28464
|
const last = import_zod58.z.object({ id: import_zod58.z.string().min(1) }).parse(result.data.at(-1));
|
|
@@ -32290,8 +32296,8 @@ function classifyBrainWorkflow(read2, functionUrl) {
|
|
|
32290
32296
|
return null;
|
|
32291
32297
|
}
|
|
32292
32298
|
if (u.origin !== base.origin || u.search !== "" || u.hash !== "") return null;
|
|
32293
|
-
const
|
|
32294
|
-
return
|
|
32299
|
+
const path67 = u.pathname.replace(/\/+$/, "");
|
|
32300
|
+
return path67 === `${base.pathname}/event` ? "/event" : path67 === `${base.pathname}/reply` ? "/reply" : null;
|
|
32295
32301
|
};
|
|
32296
32302
|
const urlOf = (a) => a?.attributes?.url;
|
|
32297
32303
|
const hits = steps.filter((s) => exact(urlOf(s)) !== null);
|
|
@@ -32390,8 +32396,8 @@ async function brainWireGhl(deps, args) {
|
|
|
32390
32396
|
return { ok: false, wrote: "nothing", error: e instanceof Error ? e.message : String(e) };
|
|
32391
32397
|
}
|
|
32392
32398
|
const specs = wiringSpecs({ testTag: args.testTag, leadTag });
|
|
32393
|
-
const
|
|
32394
|
-
const clean5 = (e) => scrub(e instanceof Error ? e.message : String(e),
|
|
32399
|
+
const held2 = [];
|
|
32400
|
+
const clean5 = (e) => scrub(e instanceof Error ? e.message : String(e), held2);
|
|
32395
32401
|
const survey = async () => {
|
|
32396
32402
|
const refuse4 = (error) => ({ refusal: { ok: false, wrote: "nothing", error } });
|
|
32397
32403
|
const meta = deps.meta();
|
|
@@ -32417,7 +32423,7 @@ async function brainWireGhl(deps, args) {
|
|
|
32417
32423
|
if (c.kind === "uncertain") return refuse4(`Workflow ${id2} reaches this brain, but ${c.why}, so the brain cannot tell what it is. Nothing was changed. ${RESTART}`);
|
|
32418
32424
|
if (c.kind === "brain") {
|
|
32419
32425
|
if (known && known !== c.signal) return refuse4(`Workflow ${id2} is saved on this computer as the brain's ${known} workflow but now reports ${c.signal}, so the brain cannot tell what it is. Nothing was changed. ${RESTART}`);
|
|
32420
|
-
|
|
32426
|
+
held2.push(c.key);
|
|
32421
32427
|
found.push({ id: id2, signal: c.signal, stepId: c.stepId, key: c.key, keyHash: await hashKey2(c.key) });
|
|
32422
32428
|
} else if (known && shape.kind === "empty") {
|
|
32423
32429
|
found.push({ id: id2, signal: known, stepId: null, key: null, keyHash: null });
|
|
@@ -32440,7 +32446,7 @@ async function brainWireGhl(deps, args) {
|
|
|
32440
32446
|
if (c.kind === "uncertain") return refuse4(`Workflow ${id2} reaches this brain, but ${c.why}, so the brain cannot tell what it is. Nothing was changed. ${RESTART}`);
|
|
32441
32447
|
if (c.kind === "brain") {
|
|
32442
32448
|
if (known !== c.signal) return refuse4(`Workflow ${id2} is saved on this computer as the brain's ${known} workflow but now reports ${c.signal}, so the brain cannot tell what it is. Nothing was changed. ${RESTART}`);
|
|
32443
|
-
|
|
32449
|
+
held2.push(c.key);
|
|
32444
32450
|
found.push({ id: id2, signal: c.signal, stepId: c.stepId, key: c.key, keyHash: await hashKey2(c.key) });
|
|
32445
32451
|
} else if (shape.kind === "empty") found.push({ id: id2, signal: known, stepId: null, key: null, keyHash: null });
|
|
32446
32452
|
else return refuse4(`Workflow ${id2} (${workflowToFind(known, id2)}) no longer holds the brain's one webhook step. Nothing was changed. ${RESTART}`);
|
|
@@ -32537,13 +32543,13 @@ async function brainWireGhl(deps, args) {
|
|
|
32537
32543
|
else if (now.keyState.wiredAt !== null) {
|
|
32538
32544
|
if (!onlyKnownKeyless([...bySignal.values()], now.keyState.hash)) return { ok: false, wrote: "nothing", error: NO_CURRENT_KEY };
|
|
32539
32545
|
writeKey = newKey("write");
|
|
32540
|
-
|
|
32546
|
+
held2.push(writeKey);
|
|
32541
32547
|
if (!await store2.replaceFirstWriteKey(client.id, now.keyState.hash, await hashKey2(writeKey), lease)) {
|
|
32542
32548
|
return { ok: false, wrote: "nothing", error: "The brain's key changed while this ran, or another operation took over, so nothing was changed. Run brain_wire_ghl again." };
|
|
32543
32549
|
}
|
|
32544
32550
|
} else {
|
|
32545
32551
|
writeKey = newKey("write");
|
|
32546
|
-
|
|
32552
|
+
held2.push(writeKey);
|
|
32547
32553
|
const leftovers = [...bySignal.values()].filter((w) => w.keyHash !== null).map((w) => [w.id, w.keyHash]);
|
|
32548
32554
|
if (leftovers.length) deps.saveMeta({ [leftoversKey]: JSON.stringify(leftovers) });
|
|
32549
32555
|
if (!await store2.setFirstWriteKey(client.id, now.keyState.hash, await hashKey2(writeKey), lease)) {
|
|
@@ -32679,12 +32685,12 @@ function keyOf(a) {
|
|
|
32679
32685
|
async function clearSetupLock(deps, api, ref, args) {
|
|
32680
32686
|
const table = await api.query(ref, "select to_regclass('brain.setup_lease') is not null as present;");
|
|
32681
32687
|
if (table[0]?.present !== true) return { ok: true, wrote: "nothing", note: "No setup lock is held on this project." };
|
|
32682
|
-
const
|
|
32683
|
-
if (!
|
|
32684
|
-
const plan = { mode: "clear_setup_lock", project: ref, lock: { takenAt:
|
|
32685
|
-
const g = gate2(plan, args, deps, { operation: "brain_retire:clear_setup_lock", projectRef: ref, clientId: "setup-lock", locationId:
|
|
32688
|
+
const held2 = import_zod63.z.array(import_zod63.z.object({ holder: import_zod63.z.string(), taken_at: import_zod63.z.string() })).parse(await api.query(ref, "select holder, taken_at::text as taken_at from brain.setup_lease;"));
|
|
32689
|
+
if (!held2.length) return { ok: true, wrote: "nothing", note: "No setup lock is held on this project." };
|
|
32690
|
+
const plan = { mode: "clear_setup_lock", project: ref, lock: { takenAt: held2[0].taken_at }, writes: "Deletes the setup lock row only. Nothing else in the project changes. Only do this if no brain_setup is running for this project right now." };
|
|
32691
|
+
const g = gate2(plan, args, deps, { operation: "brain_retire:clear_setup_lock", projectRef: ref, clientId: "setup-lock", locationId: held2[0].holder });
|
|
32686
32692
|
if (!g.go) return g.response;
|
|
32687
|
-
await api.query(ref, `delete from brain.setup_lease where holder = '${
|
|
32693
|
+
await api.query(ref, `delete from brain.setup_lease where holder = '${held2[0].holder.replace(/'/g, "''")}';`);
|
|
32688
32694
|
const left = await api.query(ref, "select 1 from brain.setup_lease;");
|
|
32689
32695
|
return { ok: left.length === 0, cleared: left.length === 0, next: "Run brain_setup again." };
|
|
32690
32696
|
}
|
|
@@ -33147,8 +33153,8 @@ function normUrl(u) {
|
|
|
33147
33153
|
try {
|
|
33148
33154
|
const url = new URL(scheme + t);
|
|
33149
33155
|
const host = url.hostname.replace(/\.$/, "").replace(/^www\./, "");
|
|
33150
|
-
const
|
|
33151
|
-
return `${host}${url.port ? `:${url.port}` : ""}${
|
|
33156
|
+
const path67 = url.pathname.replace(/\/+$/, "");
|
|
33157
|
+
return `${host}${url.port ? `:${url.port}` : ""}${path67}`.toLowerCase();
|
|
33152
33158
|
} catch {
|
|
33153
33159
|
return t.replace(/^https?:\/\//, "").replace(/^[^/@]*@/, "").replace(/^www\./, "").replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
33154
33160
|
}
|
|
@@ -33565,8 +33571,8 @@ async function fingerprint(body, firstName) {
|
|
|
33565
33571
|
let at = 0;
|
|
33566
33572
|
for (const l of linksIn(t)) {
|
|
33567
33573
|
out += t.slice(at, l.index);
|
|
33568
|
-
const
|
|
33569
|
-
out += ` link ${
|
|
33574
|
+
const path67 = l.canon.split("/").filter((seg) => seg && !/^(?=.*\d)[\p{L}\p{N}_-]{12,}$/u.test(seg) && !/^[0-9a-f-]{16,}$/i.test(seg));
|
|
33575
|
+
out += ` link ${path67.join(" ").replace(/[^\p{L}\p{N}]+/gu, " ")} `;
|
|
33570
33576
|
at = l.index + l.length;
|
|
33571
33577
|
}
|
|
33572
33578
|
t = (out + t.slice(at)).replace(/[\p{L}\p{N}._%+-]+@[\p{L}\p{N}.-]+\.[\p{L}]{2,}/gu, " ").replace(/\b\d{4}-\d{1,2}-\d{1,2}\b|\b\d{1,2}[/.-]\d{1,2}(?:[/.-]\d{2,4})?\b/g, " ").replace(/\b\d{1,2}(?::\d{2})?\s*(?:[ap]\.?\s?m\b\.?)|\b\d{1,2}:\d{2}\b/g, " ").replace(/(?:[$€£¥₹]|\b(?:usd|eur|gbp|aed|sar|cad|aud)\b)\s?\d[\d,]*(?:\.\d+)?|\b\d[\d,]*(?:\.\d+)?\s?(?:%|percent\b|dollars?\b|(?:usd|eur|gbp|aed|sar|cad|aud)\b)/g, " ").replace(/\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?\s+\d{1,2}(?:st|nd|rd|th)?\b(?:,?\s+\d{4}\b)?/g, " ").replace(/\b\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?(?:,?\s+\d{4}\b)?/g, " ").replace(/\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?,?\s+\d{4}\b/g, " ").replace(/\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?\b/g, " ").replace(/\b(?:mon|tue|tues|wed|thu|thur|thurs|fri|sat|sun)(?:day|sday|nesday|rsday|urday)?\b/g, " ").replace(/[^\p{L}\p{M}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
@@ -33864,15 +33870,15 @@ var init_cockpit_budget = __esm({
|
|
|
33864
33870
|
|
|
33865
33871
|
// src/command-os/brain/account-proposals.ts
|
|
33866
33872
|
function clientSettingsGet(client) {
|
|
33867
|
-
return (
|
|
33873
|
+
return (path67, params, opts) => client.get(path67, opts?.onText ? { params, onText: opts.onText } : { params });
|
|
33868
33874
|
}
|
|
33869
|
-
function settingsPathAllowed(
|
|
33870
|
-
return
|
|
33875
|
+
function settingsPathAllowed(path67, locationId2) {
|
|
33876
|
+
return path67 === "/calendars/" || path67 === "/products/" || path67 === "/users/" || /^\/products\/[A-Za-z0-9_-]{1,64}\/price$/.test(path67) || path67 === `/locations/${encodeURIComponent(locationId2)}`;
|
|
33871
33877
|
}
|
|
33872
33878
|
function settingsOnly(locationId2, get2) {
|
|
33873
|
-
return async (
|
|
33874
|
-
if (!settingsPathAllowed(
|
|
33875
|
-
return opts ? get2(
|
|
33879
|
+
return async (path67, params, opts) => {
|
|
33880
|
+
if (!settingsPathAllowed(path67, locationId2)) throw new NotASettingError(`${path67} is not an account setting, so it was not read`);
|
|
33881
|
+
return opts ? get2(path67, params, opts) : get2(path67, params);
|
|
33876
33882
|
};
|
|
33877
33883
|
}
|
|
33878
33884
|
function openHoursOf(raw) {
|
|
@@ -34076,10 +34082,10 @@ function continuationOf(raw) {
|
|
|
34076
34082
|
async function readAccountSettings(get2, locationId2) {
|
|
34077
34083
|
const out = { candidates: [], notRead: [], empty: [], held: [] };
|
|
34078
34084
|
const more = /* @__PURE__ */ new Map();
|
|
34079
|
-
const read2 = async (what, schema,
|
|
34085
|
+
const read2 = async (what, schema, path67, params, onText) => {
|
|
34080
34086
|
let raw;
|
|
34081
34087
|
try {
|
|
34082
|
-
raw = await (onText ? get2(
|
|
34088
|
+
raw = await (onText ? get2(path67, params, { onText }) : get2(path67, params));
|
|
34083
34089
|
} catch (e) {
|
|
34084
34090
|
const budget = budgetFailureText(e);
|
|
34085
34091
|
out.notRead.push(budget !== null ? `Not read, ${what}: the brain could not pay for the request, so nothing was sent to GoHighLevel.` : e instanceof Error && e.name === "NotASettingError" ? `Not read, ${what}: it is not an account setting, so the brain did not ask for it.` : `Could not read ${what}: GoHighLevel did not answer.`);
|
|
@@ -35791,20 +35797,20 @@ async function brainAudit(deps, args) {
|
|
|
35791
35797
|
const started = Date.now();
|
|
35792
35798
|
let calls = 0;
|
|
35793
35799
|
const notChecked = [];
|
|
35794
|
-
const read2 = async (what,
|
|
35800
|
+
const read2 = async (what, path67, params) => {
|
|
35795
35801
|
if (calls >= MAX_GHL_CALLS || Date.now() - started > TIME_BUDGET_MS) {
|
|
35796
35802
|
notChecked.push(what);
|
|
35797
35803
|
return { got: "skipped" };
|
|
35798
35804
|
}
|
|
35799
35805
|
calls += 1;
|
|
35800
35806
|
try {
|
|
35801
|
-
return { got: "data", data: await deps.ghlGet(
|
|
35807
|
+
return { got: "data", data: await deps.ghlGet(path67, params) };
|
|
35802
35808
|
} catch {
|
|
35803
35809
|
return { got: "error" };
|
|
35804
35810
|
}
|
|
35805
35811
|
};
|
|
35806
|
-
const ghl = async (what,
|
|
35807
|
-
const r = await read2(what,
|
|
35812
|
+
const ghl = async (what, path67, params) => {
|
|
35813
|
+
const r = await read2(what, path67, params);
|
|
35808
35814
|
if (r.got === "error") notChecked.push(`${what} (GoHighLevel could not be read)`);
|
|
35809
35815
|
return r.got === "data" ? r.data : void 0;
|
|
35810
35816
|
};
|
|
@@ -40647,6 +40653,7 @@ function journalObjectFor(b, parents) {
|
|
|
40647
40653
|
};
|
|
40648
40654
|
const parent = parents.get(b.ref);
|
|
40649
40655
|
if (parent) o.parentRef = parent;
|
|
40656
|
+
if (b.type === "calendar" && b.teamIds?.length) o.teamIds = [...b.teamIds];
|
|
40650
40657
|
return o;
|
|
40651
40658
|
}
|
|
40652
40659
|
function buildRunRecord(input) {
|
|
@@ -40823,6 +40830,32 @@ function describeRuns(locationId2, base) {
|
|
|
40823
40830
|
reverted: reverted.has(r.runId)
|
|
40824
40831
|
})).reverse();
|
|
40825
40832
|
}
|
|
40833
|
+
function calendarTeamsFromJournal(locationId2, base) {
|
|
40834
|
+
const out = {};
|
|
40835
|
+
for (const r of readJournal(locationId2, base)) {
|
|
40836
|
+
if (r.kind !== "build-object") continue;
|
|
40837
|
+
const o = r.object;
|
|
40838
|
+
if (o?.type === "calendar" && o.action === "created" && o.teamIds?.length) out[o.name.trim().toLowerCase()] = [...o.teamIds];
|
|
40839
|
+
}
|
|
40840
|
+
return out;
|
|
40841
|
+
}
|
|
40842
|
+
function madeByBuild(locationId2, planId, base) {
|
|
40843
|
+
const records = readJournal(locationId2, base);
|
|
40844
|
+
const planOf = /* @__PURE__ */ new Map();
|
|
40845
|
+
let since = "";
|
|
40846
|
+
for (const r of records) {
|
|
40847
|
+
if (r.kind === "build-start") planOf.set(r.runId, r.planId);
|
|
40848
|
+
if (r.kind === "revert" && (!planId || planOf.get(r.runId) === planId)) since = r.at > since ? r.at : since;
|
|
40849
|
+
}
|
|
40850
|
+
const out = /* @__PURE__ */ new Map();
|
|
40851
|
+
for (const r of records) {
|
|
40852
|
+
if (r.kind !== "build-object" || r.at <= since) continue;
|
|
40853
|
+
if (planId && planOf.get(r.runId) !== planId) continue;
|
|
40854
|
+
const o = r.object;
|
|
40855
|
+
if (o?.action === "created" && !out.has(o.ref)) out.set(o.ref, { ref: o.ref, type: o.type, name: o.name });
|
|
40856
|
+
}
|
|
40857
|
+
return [...out.values()];
|
|
40858
|
+
}
|
|
40826
40859
|
var fs24, path24, DIRECTORY_FLUSH_UNSUPPORTED;
|
|
40827
40860
|
var init_build_journal = __esm({
|
|
40828
40861
|
"src/build-journal.ts"() {
|
|
@@ -40863,15 +40896,15 @@ function shimTarget(shim) {
|
|
|
40863
40896
|
return null;
|
|
40864
40897
|
}
|
|
40865
40898
|
const refs3 = [...text3.matchAll(/"%~?dp0%?\\([^"\r\n]+)"/g)].map((m) => m[1]);
|
|
40866
|
-
const
|
|
40899
|
+
const resolve15 = (rel) => path25.join(path25.dirname(shim), ...rel.split(/[\\/]/));
|
|
40867
40900
|
const script = refs3.find((r) => /\.(?:js|cjs|mjs)$/i.test(r));
|
|
40868
40901
|
if (script) {
|
|
40869
|
-
const p =
|
|
40902
|
+
const p = resolve15(script);
|
|
40870
40903
|
return fs25.existsSync(p) ? { kind: "script", path: p } : null;
|
|
40871
40904
|
}
|
|
40872
40905
|
const program = refs3.find((r) => /\.exe$/i.test(r) && !/(^|[\\/])node\.exe$/i.test(r));
|
|
40873
40906
|
if (program) {
|
|
40874
|
-
const p =
|
|
40907
|
+
const p = resolve15(program);
|
|
40875
40908
|
return fs25.existsSync(p) ? { kind: "program", path: p } : null;
|
|
40876
40909
|
}
|
|
40877
40910
|
return null;
|
|
@@ -41110,12 +41143,12 @@ function noteObservedEngine(observed, file3 = enginePath()) {
|
|
|
41110
41143
|
return true;
|
|
41111
41144
|
}
|
|
41112
41145
|
function runSmoke(bin, timeoutMs = 9e4) {
|
|
41113
|
-
return new Promise((
|
|
41146
|
+
return new Promise((resolve15) => {
|
|
41114
41147
|
let settled = false;
|
|
41115
41148
|
const done = (r) => {
|
|
41116
41149
|
if (!settled) {
|
|
41117
41150
|
settled = true;
|
|
41118
|
-
|
|
41151
|
+
resolve15(r);
|
|
41119
41152
|
}
|
|
41120
41153
|
};
|
|
41121
41154
|
const cmd = claudeCommand(bin, [
|
|
@@ -41331,7 +41364,7 @@ What the saved plan says should exist:
|
|
|
41331
41364
|
${saved.summary}
|
|
41332
41365
|
1. switch_location to ${locationId2} (${locationName}); verify with get_current_location and stop if it is not that location.
|
|
41333
41366
|
2. Call apply_build_plan with useSavedPlan:true and mode:"dry_run" (no plan argument \u2014 the tool loads the saved plan itself). Read the report: same-named objects that already exist are bound, never duplicated.
|
|
41334
|
-
3. Call apply_build_plan with useSavedPlan:true and mode:"execute". Never delete or recreate anything that already exists.
|
|
41367
|
+
3. Call apply_build_plan with useSavedPlan:true and mode:"execute". Never delete or recreate anything that already exists. ${ONE_EXECUTE_RULE}
|
|
41335
41368
|
4. Read back what landed (get_pipelines, list_workflows_full, get_calendars) and compare it, name for name, with the saved plan above. ${BUILD_REPLY_RULE}
|
|
41336
41369
|
5. ${STAFF_NOTIFICATION_RULE} A notification step whose recipient is empty is listed in issues as waiting for a staff member, never left unmentioned.
|
|
41337
41370
|
6. ${MISSING_STEP_RULE} ${STAFF_PAUSE_RULE}
|
|
@@ -41471,19 +41504,19 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
41471
41504
|
const spec = opts.spec ?? STAGE_SPECS[stage];
|
|
41472
41505
|
if (!spec) return Promise.reject(new Error(`Stage ${stage} is not powered yet.`));
|
|
41473
41506
|
let engineAcct = {};
|
|
41474
|
-
return new Promise((
|
|
41507
|
+
return new Promise((resolve15) => {
|
|
41475
41508
|
void (async () => {
|
|
41476
41509
|
const bin = claudeBin();
|
|
41477
41510
|
if (!opts.preflightServer && !stageServerPinned()) {
|
|
41478
41511
|
const reason3 = "Command OS could not make a fixed copy of its own server beside it (the install folder may be read-only), so the build step was not started. Reinstall GHL Command in a folder you can write to, or ask for help.";
|
|
41479
41512
|
onEvent({ kind: "error", line: reason3 });
|
|
41480
|
-
|
|
41513
|
+
resolve15({ ok: false, error: reason3 });
|
|
41481
41514
|
return;
|
|
41482
41515
|
}
|
|
41483
41516
|
const preflight = await (opts.preflightServer ?? preflightPinnedServer)();
|
|
41484
41517
|
if (!preflight.ok) {
|
|
41485
41518
|
onEvent({ kind: "error", line: preflight.reason });
|
|
41486
|
-
|
|
41519
|
+
resolve15({ ok: false, error: preflight.reason });
|
|
41487
41520
|
return;
|
|
41488
41521
|
}
|
|
41489
41522
|
onEvent({ kind: "status", line: `Writing through GHL Command build ${ownBuildId()} from ${pinnedMcpConfig().mcpServers.ghl.args[0]}.` });
|
|
@@ -41491,7 +41524,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
41491
41524
|
engineAcct = { engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked };
|
|
41492
41525
|
if (!engineCheck.ok) {
|
|
41493
41526
|
onEvent({ kind: "error", line: engineCheck.reason });
|
|
41494
|
-
|
|
41527
|
+
resolve15({ ok: false, error: engineCheck.reason, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
|
|
41495
41528
|
return;
|
|
41496
41529
|
}
|
|
41497
41530
|
onEvent({ kind: "status", line: `Starting ${spec.name} for ${locationName}.` });
|
|
@@ -41507,7 +41540,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
41507
41540
|
if (!opts.preflightServer && buildIdOf(pinned2) !== ownBuildId()) {
|
|
41508
41541
|
const reason3 = `The build step's GHL Command server file changed after it was checked (it is now build ${buildIdOf(pinned2)}, this cockpit is build ${ownBuildId()}), so nothing was started. Restart the cockpit, then try again.`;
|
|
41509
41542
|
onEvent({ kind: "error", line: reason3 });
|
|
41510
|
-
|
|
41543
|
+
resolve15({ ok: false, error: reason3, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked });
|
|
41511
41544
|
return;
|
|
41512
41545
|
}
|
|
41513
41546
|
const cmd = claudeCommand(bin, buildClaudeArgs(spec, locationId2, locationName, opts.context));
|
|
@@ -41604,7 +41637,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
41604
41637
|
if (inventoryError) {
|
|
41605
41638
|
if (progressPoll) clearInterval(progressPoll);
|
|
41606
41639
|
onEvent({ kind: "error", line: inventoryError });
|
|
41607
|
-
|
|
41640
|
+
resolve15({ ok: false, error: inventoryError, engineVersion, engineModel, costUsd, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked });
|
|
41608
41641
|
return;
|
|
41609
41642
|
}
|
|
41610
41643
|
if (progressPoll) clearInterval(progressPoll);
|
|
@@ -41625,7 +41658,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
41625
41658
|
if (!timedOut && code !== 0 && texts.empty) {
|
|
41626
41659
|
const outcome2 = { ok: false, error: `claude exited ${code}: ${stderrTail.trim().slice(-200) || "no output"}`, ...engine };
|
|
41627
41660
|
onEvent({ kind: "error", line: outcome2.error });
|
|
41628
|
-
|
|
41661
|
+
resolve15(outcome2);
|
|
41629
41662
|
return;
|
|
41630
41663
|
}
|
|
41631
41664
|
const outcome = timedOut ? { ok: false, error: "the run timed out and was stopped before it finished", ...engine } : texts.failure ? { ok: false, error: `the run did not finish (${texts.failure})`, raw: texts.pick().slice(-400), ...engine } : { ...parseResultLine(texts.pick()), ...engine };
|
|
@@ -41639,7 +41672,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
41639
41672
|
const detail = outcome.error || (outcome.issues?.length ? outcome.issues.join(" \xB7 ") : "") || outcome.summary || "no detail reported";
|
|
41640
41673
|
const success = outcome.formId ? `Done: form "${outcome.formName}" (${outcome.formId})` : `Done: ${outcome.summary ?? "stage complete"}`;
|
|
41641
41674
|
onEvent({ kind: "result", line: outcome.ok ? success : `Not passed: ${detail}` });
|
|
41642
|
-
|
|
41675
|
+
resolve15(outcome);
|
|
41643
41676
|
});
|
|
41644
41677
|
child.on("error", (err) => {
|
|
41645
41678
|
spawnFailed = true;
|
|
@@ -41647,16 +41680,16 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
41647
41680
|
if (progressPoll) clearInterval(progressPoll);
|
|
41648
41681
|
const outcome = { ok: false, error: `could not start claude: ${err.message}`, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked };
|
|
41649
41682
|
onEvent({ kind: "error", line: outcome.error });
|
|
41650
|
-
|
|
41683
|
+
resolve15(outcome);
|
|
41651
41684
|
});
|
|
41652
41685
|
})().catch((err) => {
|
|
41653
41686
|
const line4 = err instanceof Error ? err.message : String(err);
|
|
41654
41687
|
onEvent({ kind: "error", line: line4 });
|
|
41655
|
-
|
|
41688
|
+
resolve15({ ok: false, error: line4, ...engineAcct });
|
|
41656
41689
|
});
|
|
41657
41690
|
});
|
|
41658
41691
|
}
|
|
41659
|
-
var import_child_process3, fs27, BRIEF_NEEDS_INTAKE, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, CONFIRM_PAUSE_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
|
|
41692
|
+
var import_child_process3, fs27, BRIEF_NEEDS_INTAKE, ONE_EXECUTE_RULE, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, CONFIRM_PAUSE_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
|
|
41660
41693
|
var init_stage_runner = __esm({
|
|
41661
41694
|
"src/stage-runner.ts"() {
|
|
41662
41695
|
"use strict";
|
|
@@ -41679,6 +41712,7 @@ var init_stage_runner = __esm({
|
|
|
41679
41712
|
init_client_record();
|
|
41680
41713
|
init_workflow_builder_client();
|
|
41681
41714
|
BRIEF_NEEDS_INTAKE = "First complete the intake. It fills most answers from a call or your notes.";
|
|
41715
|
+
ONE_EXECUTE_RULE = "Make ONE execute call and wait for its answer, however long it takes; never call apply_build_plan again while an earlier call has not answered. If it answers buildRunning, another build is writing to this account: do not call it again; finish with RESULT ok:false naming that.";
|
|
41682
41716
|
STAFF_NOTIFICATION_RULE = `Staff notifications ALWAYS stay in the workflow. Call get_users on THIS account and use only a user id it returns as the recipient. If get_users returns no users, leave the recipient EMPTY ("") \u2014 never use a user id from another account, from the agency, from an earlier conversation or from memory (a foreign id looks like it worked and notifies nobody). An empty recipient can make publish fail: then keep the workflow as a draft with the notification step still in it, and list that step in the RESULT issues as "waiting for a staff member: add one in ${GHL_ADD_STAFF_CLICK_PATH}, then pick them on the notification step and publish".`;
|
|
41683
41717
|
STAFF_PAUSE_RULE = `If apply_build_plan answers with staffPause, copy its reason into issues word for word and do not call those calendars built.`;
|
|
41684
41718
|
BUILD_REPLY_RULE = `A long apply_build_plan reply comes back shortened, with replyTrimmed on it; it still holds everything your report needs (summary, missingSteps, workflowsNotBuilt, staffPause, calendars, countsByType, notClean, templates, nextManualSteps, customValues and, on a dry run, composed.copySlots), so report from it. If you need a section it lists under sectionsNotShownInFull, read it with apply_build_plan detail:{of:"dry_run" or "execute",section:"<name>"}; that call builds nothing.`;
|
|
@@ -41818,7 +41852,7 @@ Use these authoritative normalized intake answers when provided: ${JSON.stringif
|
|
|
41818
41852
|
3. Call apply_build_plan with fromPreset:true, preset:"${presetForIndustry(ctx?.industry)}", brief:<the Brief from step 2>, mode:"dry_run". The server expands the industry preset with the client's staff, calendars (appointment lengths included), pipeline stages, offer and notification rules in under a second, validates it, and reports what would be built.
|
|
41819
41853
|
${BUILD_REPLY_RULE}
|
|
41820
41854
|
4. Read the dry-run report. Its composed.copySlots list the message templates; their copy is already filled from the brief and send-ready. ONLY where the brief gives client-specific material the preset copy misses (the client's signature line, a named offer or price), pass copy:[{ref:"email_template.<slug>", subject, html}] / [{ref:"sms_template.<slug>", body}] for JUST those templates on the next call. Never restructure; never rewrite every message.
|
|
41821
|
-
5. Call apply_build_plan again with the SAME fromPreset:true, preset and brief (plus your copy overrides, if any) and mode:"execute". Never delete or recreate anything that already exists.
|
|
41855
|
+
5. Call apply_build_plan again with the SAME fromPreset:true, preset and brief (plus your copy overrides, if any) and mode:"execute". Never delete or recreate anything that already exists. ${ONE_EXECUTE_RULE}
|
|
41822
41856
|
6. FALLBACK \u2014 only if apply_build_plan answered with phase:"compose" and errors that point at a genuine preset gap (not a fixable brief problem): author the plan yourself per the PLAN GUIDE below, check it with validate_build_plan, STOP if it does not validate, then apply_build_plan dry_run \u2192 execute with that plan.
|
|
41823
41857
|
7. Read back what landed (get_pipelines, list_workflows_full, get_calendars).
|
|
41824
41858
|
8. ${STAFF_NOTIFICATION_RULE} In a hand-authored fallback plan, a notification's "to" is that user id, or "" when the account has no users.
|
|
@@ -42911,13 +42945,13 @@ var init_handoff_pack = __esm({
|
|
|
42911
42945
|
|
|
42912
42946
|
// src/command-os/handoff-run.ts
|
|
42913
42947
|
function askNoTools(prompt, opts = {}) {
|
|
42914
|
-
return new Promise((
|
|
42948
|
+
return new Promise((resolve15) => {
|
|
42915
42949
|
let bin;
|
|
42916
42950
|
try {
|
|
42917
42951
|
bin = claudeBin();
|
|
42918
42952
|
} catch {
|
|
42919
42953
|
opts.onCost?.();
|
|
42920
|
-
|
|
42954
|
+
resolve15("");
|
|
42921
42955
|
return;
|
|
42922
42956
|
}
|
|
42923
42957
|
const cmd = claudeCommand(bin, ["-p", prompt, "--output-format", "stream-json", "--verbose", "--max-turns", String(opts.maxTurns ?? 3), "--allowedTools", "", "--disallowedTools", `${NO_TOOLS_DENY},${MCP_DENY}`, ...NO_MEMBER_HOOKS, ...NO_MCP_ARGS]);
|
|
@@ -42951,12 +42985,12 @@ function askNoTools(prompt, opts = {}) {
|
|
|
42951
42985
|
child.on("error", () => {
|
|
42952
42986
|
clearTimeout(timeout);
|
|
42953
42987
|
report3();
|
|
42954
|
-
|
|
42988
|
+
resolve15(texts.pick());
|
|
42955
42989
|
});
|
|
42956
42990
|
child.on("close", () => {
|
|
42957
42991
|
clearTimeout(timeout);
|
|
42958
42992
|
report3();
|
|
42959
|
-
|
|
42993
|
+
resolve15(texts.pick());
|
|
42960
42994
|
});
|
|
42961
42995
|
});
|
|
42962
42996
|
}
|
|
@@ -43505,6 +43539,7 @@ function goLiveTopic(text3) {
|
|
|
43505
43539
|
function jobTiming(text3) {
|
|
43506
43540
|
const t = String(text3).trim();
|
|
43507
43541
|
if (t === HANDOFF_NO_HOME_JOB) return "later";
|
|
43542
|
+
if (HELD_JOB.test(t)) return "later";
|
|
43508
43543
|
if (LEAD_MAGNET_PAGES_JOB.test(t)) return "later";
|
|
43509
43544
|
if (SWITCH_ON_JOB.test(t) || STILL_OFF_NOTE.test(t)) return "at-go-live";
|
|
43510
43545
|
if (LATER_JOB.test(t)) return "later";
|
|
@@ -43621,11 +43656,12 @@ function manualSignOffRefusal(tasks2, verifyHasRun) {
|
|
|
43621
43656
|
}
|
|
43622
43657
|
return "Everything is ticked, but the check has not confirmed it yet. Run the check again and this will clear on its own.";
|
|
43623
43658
|
}
|
|
43624
|
-
var SELF_REPORTED, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
|
|
43659
|
+
var SELF_REPORTED, HELD_JOB, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
|
|
43625
43660
|
var init_manual_tasks = __esm({
|
|
43626
43661
|
"src/manual-tasks.ts"() {
|
|
43627
43662
|
"use strict";
|
|
43628
43663
|
SELF_REPORTED = /^[^:]+: Bought, but not built automatically yet\./;
|
|
43664
|
+
HELD_JOB = /^(?:Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
|
|
43629
43665
|
SWITCH_ON_JOB = /^[^\n]+: This workflow is built and waiting for you to switch it on in GoHighLevel under Automation > Workflows\.$/;
|
|
43630
43666
|
LATER_JOB = /^Manual action: (?:[^:\n]*\b(?:after (?:a|the|each) (?:trial )?visit|before any bulk)\b[^:\n]*|How [^:\n]{1,80} starts): /;
|
|
43631
43667
|
LEAD_MAGNET_PAGES_JOB = /^[^:\n]+: Bought, but not built automatically yet\. The form is built\. The pages are separate work\.$/;
|
|
@@ -43640,6 +43676,7 @@ var init_manual_tasks = __esm({
|
|
|
43640
43676
|
{ key: "price", title: "Check the trial price, deposit and balance", re: /trial price|deposit amount|balance amount|trial_price/i }
|
|
43641
43677
|
];
|
|
43642
43678
|
NAMED_LATER = [
|
|
43679
|
+
HELD_JOB,
|
|
43643
43680
|
/^[^:\n]+: Bought, but not built automatically yet\. (?:Set up rebooking and recall|Payment setup is done|Set up referral requests|The form is built|It is not in the plan)/,
|
|
43644
43681
|
/^Manual action: Set up the trial balance link\b/,
|
|
43645
43682
|
/^Manual action: Limit the Trial Balance Paid workflow\b/,
|
|
@@ -44650,7 +44687,7 @@ function stopReasonSentence(reason3, fetched, cap2) {
|
|
|
44650
44687
|
}
|
|
44651
44688
|
}
|
|
44652
44689
|
function delay(ms) {
|
|
44653
|
-
return new Promise((
|
|
44690
|
+
return new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
44654
44691
|
}
|
|
44655
44692
|
async function fetchAllContacts(client, locationId2, options = {}) {
|
|
44656
44693
|
const cap2 = options.maxContacts ?? DEFAULT_MAX_CONTACTS;
|
|
@@ -45488,7 +45525,7 @@ var init_client_capabilities = __esm({
|
|
|
45488
45525
|
{ key: "reputation", label: "Reputation and content", blurb: "Reviews, social posts and the blog." },
|
|
45489
45526
|
{ key: "health", label: "Health and handoff", blurb: "Proving it works, keeping it clean, and handing it over." }
|
|
45490
45527
|
];
|
|
45491
|
-
N = (label2,
|
|
45528
|
+
N = (label2, path67, params, key2, nameField, version) => ({ label: label2, path: path67, params, key: key2, nameField, version });
|
|
45492
45529
|
W = (read2, extra) => ({ ...read2, ...extra });
|
|
45493
45530
|
AI2 = "2021-04-15";
|
|
45494
45531
|
CAPABILITIES = [
|
|
@@ -45616,7 +45653,10 @@ var init_client_capabilities = __esm({
|
|
|
45616
45653
|
hub: "command-os/ai-employee.html",
|
|
45617
45654
|
reads: [
|
|
45618
45655
|
W(N("knowledge bases", "/knowledge-bases/", { locationId: "{loc}", limit: "50" }, "knowledgeBases", "name", AI2), { one: "knowledge base" }),
|
|
45619
|
-
N("bots", "/conversation-ai/agents/search", { limit: "50" }, "agents", "name", AI2)
|
|
45656
|
+
N("bots", "/conversation-ai/agents/search", { limit: "50" }, "agents", "name", AI2),
|
|
45657
|
+
// Walk W42: Ready to answer reads the account live, the way Verify does.
|
|
45658
|
+
{ ...N("booking calendars", "/calendars/", { locationId: "{loc}" }, "calendars", "name"), activeOnly: true },
|
|
45659
|
+
N("phone numbers", "/phone-system/numbers/", { locationId: "{loc}" }, "phoneNumbers", "title")
|
|
45620
45660
|
]
|
|
45621
45661
|
},
|
|
45622
45662
|
{
|
|
@@ -45626,7 +45666,7 @@ var init_client_capabilities = __esm({
|
|
|
45626
45666
|
what: "GoHighLevel's name for the agent that answers the phone. Built without a number, given one on purpose, proven by the transcript.",
|
|
45627
45667
|
says: "Check whether a call can land on {client}'s account, then build a Voice AI agent that answers, books and takes a message. Leave it not yet connected to a phone number or calendar until I approve those connections. List a test conversation as still needed.",
|
|
45628
45668
|
hub: "modules/11-your-ai-employee/lesson-05-the-phone-agent.html",
|
|
45629
|
-
reads: [N("phone agents", "/voice-ai/agents", { locationId: "{loc}", page: "1", pageSize: "50" }, "agents", "agentName", AI2), N("phone numbers", "/phone-system/numbers/", { locationId: "{loc}" }, "phoneNumbers", "title")]
|
|
45669
|
+
reads: [N("phone agents", "/voice-ai/agents", { locationId: "{loc}", page: "1", pageSize: "50" }, "agents", "agentName", AI2), N("phone numbers", "/phone-system/numbers/", { locationId: "{loc}" }, "phoneNumbers", "title"), { ...N("booking calendars", "/calendars/", { locationId: "{loc}" }, "calendars", "name"), activeOnly: true }]
|
|
45630
45670
|
},
|
|
45631
45671
|
{
|
|
45632
45672
|
key: "demo",
|
|
@@ -45635,7 +45675,13 @@ var init_client_capabilities = __esm({
|
|
|
45635
45675
|
what: "One sentence about a prospect's business becomes a bot they can text, on your own demo account, and one command puts it back. It messages a real prospect, so it runs from your own Claude with you watching, never from a button.",
|
|
45636
45676
|
says: "Prepare an AI employee demo for {client} from their website and arm it so they can text it.",
|
|
45637
45677
|
hub: "modules/11-your-ai-employee/lesson-04-the-five-minute-demo.html",
|
|
45638
|
-
|
|
45678
|
+
// command-center 2026-10-29: read live, the same two lists the demo tools use (the demo bot, and one knowledge
|
|
45679
|
+
// base per prepared prospect, named "Demo: ..."). Whether a demo is armed is the bot's mode, which only
|
|
45680
|
+
// show_ai_employee_demo reads; this page does not claim it.
|
|
45681
|
+
reads: [
|
|
45682
|
+
N("bots", "/conversation-ai/agents/search", { limit: "50" }, "agents", "name", AI2),
|
|
45683
|
+
W(N("knowledge bases", "/knowledge-bases/", { locationId: "{loc}", limit: "50" }, "knowledgeBases", "name", AI2), { one: "knowledge base" })
|
|
45684
|
+
]
|
|
45639
45685
|
},
|
|
45640
45686
|
// ── Money and paperwork ─────────────────────────────────────────────────
|
|
45641
45687
|
{
|
|
@@ -45778,7 +45824,7 @@ var require_package = __commonJS({
|
|
|
45778
45824
|
"package.json"(exports2, module2) {
|
|
45779
45825
|
module2.exports = {
|
|
45780
45826
|
name: "@elitedcs/ghl-mcp",
|
|
45781
|
-
version: "3.
|
|
45827
|
+
version: "3.94.0",
|
|
45782
45828
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
45783
45829
|
description: "GoHighLevel MCP Server for Claude. 250 tools for CRM, automation and marketing, an account-wide workflow audit, live funnel-capture verification and a workflow builder, across client accounts.",
|
|
45784
45830
|
main: "dist/index.js",
|
|
@@ -46135,13 +46181,9 @@ function cockpitMoney(amount,currency){
|
|
|
46135
46181
|
var SAY=" (opens in a new tab)";
|
|
46136
46182
|
function mark(a){if(a.getAttribute("data-nt")||a.hidden)return;a.setAttribute("data-nt","1");if((a.textContent||"").indexOf("opens in a new tab")<0){var s=document.createElement("span");s.className="nt";s.textContent=SAY;a.appendChild(s)}}
|
|
46137
46183
|
function markAll(root){var list=(root||document).querySelectorAll?((root||document).querySelectorAll('a[target="_blank"]')):[];for(var i=0;i<list.length;i++)mark(list[i])}
|
|
46138
|
-
|
|
46139
|
-
|
|
46140
|
-
|
|
46141
|
-
if(a.protocol!=="http:"&&a.protocol!=="https:")return;
|
|
46142
|
-
/* "noopener" in the features makes window.open answer null even when it worked, so the opener is cut by hand. */
|
|
46143
|
-
e.preventDefault();var w=null;try{w=window.open(a.href,"_blank")}catch(x){w=null}
|
|
46144
|
-
if(w){try{w.opener=null;w.focus()}catch(x){}}else{var same=document.createElement("a");same.href=a.href;same.click()}},false);
|
|
46184
|
+
/* Walk W34 (2026-10-29, Safari): opening the page by hand inside the click fell back to this tab when Safari refused,
|
|
46185
|
+
and the cockpit was lost. The link is left to the browser: a real click on a target=_blank link opens a new tab in
|
|
46186
|
+
every browser. The redraw hold (clickInFlight) keeps the link under the pointer from being swapped mid-click. */
|
|
46145
46187
|
markAll(document);
|
|
46146
46188
|
try{new MutationObserver(function(ms){for(var i=0;i<ms.length;i++){var n=ms[i].addedNodes;for(var j=0;j<n.length;j++){var x=n[j];if(x.nodeType!==1)continue;if(x.matches&&x.matches('a[target="_blank"]'))mark(x);markAll(x)}}}).observe(document.documentElement,{childList:true,subtree:true})}catch(e){}
|
|
46147
46189
|
})();`;
|
|
@@ -47161,9 +47203,9 @@ var init_cockpit_cache = __esm({
|
|
|
47161
47203
|
*/
|
|
47162
47204
|
readNow(locationId2, page2, opts, priority = 1) {
|
|
47163
47205
|
const key2 = this.cache.key(locationId2, page2);
|
|
47164
|
-
const p = new Promise((
|
|
47206
|
+
const p = new Promise((resolve15) => {
|
|
47165
47207
|
const w = this.waiters.get(key2) ?? [];
|
|
47166
|
-
w.push(
|
|
47208
|
+
w.push(resolve15);
|
|
47167
47209
|
this.waiters.set(key2, w);
|
|
47168
47210
|
});
|
|
47169
47211
|
this.queue(locationId2, page2, priority, opts);
|
|
@@ -47187,7 +47229,7 @@ var init_cockpit_cache = __esm({
|
|
|
47187
47229
|
const key2 = this.cache.key(locationId2, page2);
|
|
47188
47230
|
const ticket = given ?? this.cache.ticket(locationId2, page2);
|
|
47189
47231
|
return new Promise((done) => {
|
|
47190
|
-
const
|
|
47232
|
+
const resolve15 = (v) => {
|
|
47191
47233
|
setTimeout(() => done(v.ok && !this.cache.stillValid(ticket) ? { ok: false, error: new StaleReadError() } : v.ok ? { ...v, ticket } : v), 0);
|
|
47192
47234
|
};
|
|
47193
47235
|
this.pacer.enqueue(`${key2}#live${++this.liveSeq}`, locationId2 === "agency" ? "agency" : locationId2, priority, 0, (gate3) => this.cache.inRegion(ticket, async () => {
|
|
@@ -47198,17 +47240,17 @@ var init_cockpit_cache = __esm({
|
|
|
47198
47240
|
const data = await Promise.race([opts.read((client) => pacedClient(client, gate3, count4), (fn) => !late && !gate3.expired?.() && this.cache.commitIfCurrent(ticket, fn)), gate3.deadline ?? NEVER]);
|
|
47199
47241
|
const kept = this.cache.putFor(ticket, data, { reach: opts.reach, calls: count4.calls, ghlMs: this.now() - started, holdSave: opts.holdSave });
|
|
47200
47242
|
if (kept === null) {
|
|
47201
|
-
|
|
47243
|
+
resolve15({ ok: false, error: new StaleReadError() });
|
|
47202
47244
|
return;
|
|
47203
47245
|
}
|
|
47204
47246
|
if (kept.changed) opts.onChange?.(data);
|
|
47205
|
-
|
|
47247
|
+
resolve15({ ok: true, data });
|
|
47206
47248
|
} catch (e) {
|
|
47207
47249
|
if (e instanceof PacerDeadlineError) late = true;
|
|
47208
47250
|
this.cache.failFor(ticket, e instanceof PacerCeilingError ? DAILY_CEILING_TEXT : e instanceof PacerDeadlineError ? DEADLINE_TEXT : isSlowDown(e) ? SLOW_DOWN_TEXT : opts.failureText?.(e) ?? "could not read: try Refresh");
|
|
47209
|
-
|
|
47251
|
+
resolve15({ ok: false, error: e });
|
|
47210
47252
|
}
|
|
47211
|
-
}), () =>
|
|
47253
|
+
}), () => resolve15({ ok: false, error: new StaleReadError() }));
|
|
47212
47254
|
});
|
|
47213
47255
|
}
|
|
47214
47256
|
queue(locationId2, page2, priority, opts) {
|
|
@@ -47807,8 +47849,8 @@ function scoreRun(run) {
|
|
|
47807
47849
|
const drafts = run.draftWorkflows ?? [];
|
|
47808
47850
|
const stepProblems = st?.read ? (st.problems ?? []).length : 0;
|
|
47809
47851
|
if (f.sent === 0 && judgedSteps === 0 && drafts.length === 0 && stepProblems === 0) return { graded: false, reason: "Nothing the test lead received and no workflow step could be judged, so there is nothing to grade. Check that the workflows are published, then run it again." };
|
|
47810
|
-
const
|
|
47811
|
-
if (
|
|
47852
|
+
const held2 = (f.notYetDue ?? []).filter((m) => m.heldByWait).length;
|
|
47853
|
+
if (held2) return { graded: false, reason: `GoHighLevel was still holding the test lead, so ${held2} planned message${held2 === 1 ? " was" : "s were"} not judged. A grade now would look better than the truth. Run the check again.` };
|
|
47812
47854
|
const deductions2 = [];
|
|
47813
47855
|
const add2 = (kind, area, text3) => {
|
|
47814
47856
|
const p = POINTS[kind];
|
|
@@ -48174,7 +48216,7 @@ async function listLeadCarriers(client, run) {
|
|
|
48174
48216
|
const carriers = /* @__PURE__ */ new Map();
|
|
48175
48217
|
let partial = false;
|
|
48176
48218
|
try {
|
|
48177
|
-
const paged = await readContactPages((
|
|
48219
|
+
const paged = await readContactPages((path67, o) => client.get(path67, o), { locationId: run.locationId, query: run.leadEmail, limit: 20 });
|
|
48178
48220
|
if (!paged || !paged.complete) partial = true;
|
|
48179
48221
|
for (const c of paged?.rows ?? []) {
|
|
48180
48222
|
const k = carrierOf(c);
|
|
@@ -48231,23 +48273,23 @@ function claimRun(locationId2, runId, base) {
|
|
|
48231
48273
|
};
|
|
48232
48274
|
} catch (e) {
|
|
48233
48275
|
if (e.code !== "EEXIST") throw e;
|
|
48234
|
-
let
|
|
48276
|
+
let held2 = "";
|
|
48235
48277
|
let age = 0;
|
|
48236
48278
|
try {
|
|
48237
|
-
|
|
48279
|
+
held2 = import_node_fs7.default.readFileSync(lock, "utf8");
|
|
48238
48280
|
age = Date.now() - import_node_fs7.default.statSync(lock).mtimeMs;
|
|
48239
48281
|
} catch {
|
|
48240
48282
|
continue;
|
|
48241
48283
|
}
|
|
48242
48284
|
let pid = 0;
|
|
48243
48285
|
try {
|
|
48244
|
-
pid = Number(JSON.parse(
|
|
48286
|
+
pid = Number(JSON.parse(held2).pid);
|
|
48245
48287
|
} catch {
|
|
48246
48288
|
}
|
|
48247
48289
|
const stale = age > 10 * 6e4 || pid > 0 && !pidAlive(pid);
|
|
48248
48290
|
if (!stale) return null;
|
|
48249
48291
|
try {
|
|
48250
|
-
if (import_node_fs7.default.readFileSync(lock, "utf8") ===
|
|
48292
|
+
if (import_node_fs7.default.readFileSync(lock, "utf8") === held2) import_node_fs7.default.unlinkSync(lock);
|
|
48251
48293
|
} catch {
|
|
48252
48294
|
}
|
|
48253
48295
|
}
|
|
@@ -48284,10 +48326,10 @@ async function finishShopper(locationId2, runId, deps) {
|
|
|
48284
48326
|
if (deps.stepLog && run.entry.kind === "workflows") {
|
|
48285
48327
|
run.steps = await auditSteps(deps.stepLog, run.entry.workflowIds, run.workflowNames ?? {}, run.contactId, run.startedAt, t, { hasPhone: Boolean(run.testPhone), hasEmail: Boolean(run.leadEmail), ...run.staffGaps?.length ? { staffGaps: run.staffGaps } : {} });
|
|
48286
48328
|
}
|
|
48287
|
-
const
|
|
48329
|
+
const held2 = closed && run.steps?.read ? heldWaits(run.steps.rows) : [];
|
|
48288
48330
|
const base = run.originalClosesAt ?? run.closesAt;
|
|
48289
48331
|
const cap2 = base + MAX_EXTEND_MINUTES * 6e4;
|
|
48290
|
-
const soon =
|
|
48332
|
+
const soon = held2.filter((h) => h.waitUntil !== void 0 && h.waitUntil <= t + 5 * 6e4);
|
|
48291
48333
|
if (soon.length && t < cap2) {
|
|
48292
48334
|
run.originalClosesAt = base;
|
|
48293
48335
|
run.closesAt = Math.min(cap2, Math.max(t, ...soon.map((h) => h.waitUntil)) + 3 * 6e4);
|
|
@@ -48295,7 +48337,7 @@ async function finishShopper(locationId2, runId, deps) {
|
|
|
48295
48337
|
saveRun(run, deps.base);
|
|
48296
48338
|
return run;
|
|
48297
48339
|
}
|
|
48298
|
-
const late =
|
|
48340
|
+
const late = held2.filter((h) => h.waitUntil !== void 0 && h.waitUntil <= t);
|
|
48299
48341
|
if (closed && late.length) {
|
|
48300
48342
|
const rows2 = run.steps.rows;
|
|
48301
48343
|
const stillHeld = run.findings.missing.filter((m) => m.workflowId && m.stepId && late.some((h) => h.workflowId === m.workflowId && h.stepId !== void 0 && (m.afterWaits ?? []).includes(h.stepId)) && !rows2.some((r) => r.workflowId === m.workflowId && r.stepId === m.stepId));
|
|
@@ -48462,7 +48504,6 @@ function planCopy(src, opts) {
|
|
|
48462
48504
|
const nodes = src.templates.map(asObj9);
|
|
48463
48505
|
if (nodes.length === 0) return { ok: false, refusal: `"${src.name}" has no steps to test.` };
|
|
48464
48506
|
if (nodes.length > 40) return { ok: false, refusal: `"${src.name}" has ${nodes.length} steps; fast-forward copies at most 40.` };
|
|
48465
|
-
if (str11(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.` };
|
|
48466
48507
|
const idMap = /* @__PURE__ */ new Map();
|
|
48467
48508
|
for (const n of nodes) {
|
|
48468
48509
|
const id2 = str11(n.id);
|
|
@@ -48861,17 +48902,17 @@ ${ringHtml(card.score, card.coverageLabel ?? card.coverage, true)}</div>`;
|
|
|
48861
48902
|
}
|
|
48862
48903
|
const minutesLeft = Math.max(0, Math.ceil((run.closesAt - now) / 6e4));
|
|
48863
48904
|
const planned = run.planNote.source !== "none" && run.expect.length > 0;
|
|
48864
|
-
const
|
|
48865
|
-
const title2 = !f ? "Watching what the test lead receives" : f.sent === 0 ? !watching && nothingArrivedHeading(run.steps) || `Nothing arrived in ${watching ? "the time so far" : `${run.watchMinutes} minutes`}` : f.problems.length || f.missing.length ? "What a new lead actually received" :
|
|
48905
|
+
const held2 = f?.notYetDue.filter((m) => m.heldByWait) ?? [];
|
|
48906
|
+
const title2 = !f ? "Watching what the test lead receives" : f.sent === 0 ? !watching && nothingArrivedHeading(run.steps) || `Nothing arrived in ${watching ? "the time so far" : `${run.watchMinutes} minutes`}` : f.problems.length || f.missing.length ? "What a new lead actually received" : held2.length ? "Not finished: GoHighLevel was still holding the test lead" : planned ? "A new lead got exactly what you planned" : "No problems in what the lead received";
|
|
48866
48907
|
const head4 = pageHead(title2, `${productName()} made a test lead, then read every text and email this account sent it, word for word. ${esc2(run.entryNote)}`, action);
|
|
48867
48908
|
const worst = f?.problems[0];
|
|
48868
48909
|
const quote2 = (tone, text3, cite) => `<div class="cds-callout quote${tone ? ` ${tone}` : ""}">${text3}<cite>${cite}</cite></div>`;
|
|
48869
|
-
const quoteHtml = f && f.sent === 0 ? quote2("warn", "The test lead received no text and no email.", esc2(nothingArrivedWhy(run.steps, run.draftWorkflows))) : worst ? quote2("bad", `${worst.kind === "blank_field" ? "A new lead with no first name on file got" : "A new lead got"} "${marked(worst.quote, "")}"`, `${esc2(worst.title)}${f && f.problems.length > 1 ? `. The first of ${f.problems.length}.` : "."}`) : f &&
|
|
48910
|
+
const quoteHtml = f && f.sent === 0 ? quote2("warn", "The test lead received no text and no email.", esc2(nothingArrivedWhy(run.steps, run.draftWorkflows))) : worst ? quote2("bad", `${worst.kind === "blank_field" ? "A new lead with no first name on file got" : "A new lead got"} "${marked(worst.quote, "")}"`, `${esc2(worst.title)}${f && f.problems.length > 1 ? `. The first of ${f.problems.length}.` : "."}`) : f && held2.length && !f.missing.length ? quote2("warn", `${held2.length} planned message${held2.length === 1 ? " was" : "s were"} still waiting behind a wait GoHighLevel had not let go of.`, "Not judged. Run the check again.") : f && f.missing.length ? quote2("warn", `${f.missing.length} planned message${f.missing.length === 1 ? " never" : "s never"} arrived.`, "Everything that did arrive read correctly.") : f ? quote2("", `Every message read correctly${planned ? " and arrived when planned" : ""}.`, `Checked for the first ${run.watchMinutes} minutes. Later messages are listed below as not yet checked.`) : "";
|
|
48870
48911
|
const stats = f ? `<div class="stats">
|
|
48871
48912
|
${statTile(f.sent, "Messages sent")}
|
|
48872
48913
|
${statTile(f.problems.length, "A customer would notice", f.problems.length ? "bad" : "")}
|
|
48873
48914
|
${run.steps?.read ? statTile(run.steps.rows.length ? run.steps.problems.length : "\u2013", "Steps failed, skipped or did nothing", run.steps.problems.length ? "bad" : run.steps.rows.length && !run.steps.unreadWorkflows?.length && !run.steps.partialWorkflows?.length && !run.steps.counts.unclear ? "" : "warn") : ""}
|
|
48874
|
-
${statTile(run.planNote.source === "none" ? "\u2013" : f.missing.length, "Planned, never sent", f.missing.length ||
|
|
48915
|
+
${statTile(run.planNote.source === "none" ? "\u2013" : f.missing.length, "Planned, never sent", f.missing.length || held2.length ? "warn" : "")}
|
|
48875
48916
|
${(() => {
|
|
48876
48917
|
const opened = f.links.total - f.links.skipped;
|
|
48877
48918
|
return opened ? statTile(`${f.links.ok} of ${opened}`, `Links open${f.links.skipped ? ` \xB7 ${f.links.skipped} not opened` : ""}`, f.links.broken ? "bad" : "") : statTile(0, `Links opened${f.links.skipped ? ` \xB7 ${f.links.skipped} not` : ""}`);
|
|
@@ -48949,7 +48990,7 @@ function startForm(d, inlineSubmit) {
|
|
|
48949
48990
|
<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>
|
|
48950
48991
|
<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>
|
|
48951
48992
|
${callout("warn", "", "The test lead has no first name, on purpose. The workflows you tick will really send to your address and may alert your staff once.")}
|
|
48952
|
-
|
|
48993
|
+
<div class="cds-row"><button class="cds-btn${inlineSubmit ? " ghost" : ""}" type="submit" id="start-btn" data-start="" disabled>Start the check</button></div>
|
|
48953
48994
|
<div id="start-ask"></div>
|
|
48954
48995
|
<p class="cds-callout result" id="start-result" role="status" aria-live="polite"></p>
|
|
48955
48996
|
</form>`;
|
|
@@ -48960,7 +49001,7 @@ function shopperPageHtml(d, seatToken, seatHeader) {
|
|
|
48960
49001
|
const run = d.run;
|
|
48961
49002
|
const graded2 = !!(run && run.status === "done" && run.findings && scoreRun(run).graded);
|
|
48962
49003
|
const canStart = !!d.client && run?.status !== "watching";
|
|
48963
|
-
const action = graded2 && run ? `<a class="cds-btn" href="${reportHref(run)}" target="_blank" rel="noopener">Open the report</a>` : canStart ? `<button class="cds-btn" type="submit" form="start-form" id="start-btn" disabled>Start the check</button>` : "";
|
|
49004
|
+
const action = graded2 && run ? `<a class="cds-btn" href="${reportHref(run)}" target="_blank" rel="noopener">Open the report</a>` : canStart ? `<button class="cds-btn" type="submit" form="start-form" id="start-btn-top" data-start="" disabled>Start the check</button>` : "";
|
|
48964
49005
|
let body;
|
|
48965
49006
|
if (!d.client) {
|
|
48966
49007
|
body = pageHead("See what a new lead actually receives", `Pick a client account. ${productName()} makes a test lead with your own email address, puts it through the workflows you choose, then reads every text and email the account sent it, word for word, and tells you what a real customer would have seen wrong.`) + picker;
|
|
@@ -48986,7 +49027,7 @@ ${COCKPIT_TYPE_JS}
|
|
|
48986
49027
|
function drawWorkflows(ws,keep){shownWorkflows=ws.map(function(w){return {id:w.id,name:w.name,status:w.status}});
|
|
48987
49028
|
if(!ws.length){list.innerHTML='<p class="err">This account has no workflows to test.</p>';return}
|
|
48988
49029
|
list.innerHTML=ws.map(function(w){return '<label class="cds-check"><input type="checkbox" name="wf" value="'+esc(w.id)+'"'+(keep&&keep[w.id]?' checked':'')+'><span>'+esc(w.name)+(w.status&&w.status!=="published"?' <span class="cds-chip warn">'+esc(w.status)+'</span>':'')+'</span></label>'}).join("");
|
|
48989
|
-
|
|
49030
|
+
[].forEach.call(document.querySelectorAll("[data-start]"),function(b){b.disabled=false})}
|
|
48990
49031
|
// The cockpit keeps what it has loaded (cockpit-cache.ts): the list says how old it is, with one Refresh; a stale
|
|
48991
49032
|
// list is re-read behind the page, and a look that found nothing new never redraws it.
|
|
48992
49033
|
${AGE_LINE_JS}
|
|
@@ -49015,12 +49056,30 @@ ${COCKPIT_TYPE_JS}
|
|
|
49015
49056
|
if(run&&run.status==="watching"&&run.due){fetch("/api/shopper/finish",{method:"POST",headers:hdrs({"Content-Type":"application/json"}),body:JSON.stringify({client:client,runId:run.runId})}).then(function(r){return r.json()}).then(function(r){if(r.ok)location.reload()})}
|
|
49016
49057
|
function post(path, body){return fetch(path,{method:"POST",headers:hdrs({"Content-Type":"application/json"}),body:JSON.stringify(body)}).then(function(r){return r.json()})}
|
|
49017
49058
|
var form=document.getElementById("start-form"), startConfirmed=false;
|
|
49059
|
+
function startBody(ids,hosts){return {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),workflowsShown:shownWorkflows}}
|
|
49060
|
+
/* Walk W39: the panel asks the start's own checks first (preview, nothing written) and never promises a start the
|
|
49061
|
+
server would refuse: a refusal is shown with no confirm button. */
|
|
49018
49062
|
function showStartAsk(ids){
|
|
49063
|
+
var ask=document.getElementById("start-ask");
|
|
49064
|
+
var hosts=[].slice.call(form.querySelectorAll('input[name="hook"]:checked')).map(function(i){return i.value});
|
|
49065
|
+
ask.innerHTML='<div class="cds-callout"><span class="ttl">Checking the workflows\u2026</span>Nothing is written yet.</div>';
|
|
49066
|
+
var body=startBody(ids,hosts);body.preview=true;
|
|
49067
|
+
post("/api/shopper/start",body).then(function(r){
|
|
49068
|
+
if(!r||!r.ok){ask.innerHTML='<div class="cds-callout warn"><span class="ttl">This check cannot start</span>'+esc((r&&r.error)||"The workflows could not be checked.")+'</div>';if(r&&r.stale&&r.workflows){var keep={};ids.forEach(function(id){keep[id]=true});drawWorkflows(r.workflows,keep)}return}
|
|
49069
|
+
if(r.refusals&&r.refusals.length){ask.innerHTML='<div class="cds-callout warn"><span class="ttl">This check cannot start as it is</span><ul>'+r.refusals.map(function(x){return '<li>'+esc(x)+'</li>'}).join("")+'</ul>Nothing was written.</div>';return}
|
|
49070
|
+
showStartPanel(ids)})
|
|
49071
|
+
.catch(function(){ask.innerHTML='<div class="cds-callout warn"><span class="ttl">This check cannot start</span>${productName()} is not answering on this computer.</div>'})}
|
|
49072
|
+
function showStartPanel(ids){
|
|
49019
49073
|
var names=ids.map(function(id){var w=(shownWorkflows||[]).filter(function(x){return x.id===id})[0];return w?w.name:id});
|
|
49020
49074
|
var ff=document.getElementById("ff").checked, email=document.getElementById("owner-email").value, phone=document.getElementById("test-phone").value;
|
|
49021
49075
|
var li=['A test contact named "Secret Shopper" with your email ('+esc(email)+')'+(phone?' and the phone '+esc(phone):'')+', and the tag command-os-secret-shopper. It stays in the account afterwards; delete it when you no longer need it.'];
|
|
49022
49076
|
if(ff)li.push('Temporary copies of '+names.length+' workflow'+(names.length===1?'':'s')+', switched on while the check runs: '+names.map(function(n){return esc("${FF_PREFIX} "+n)}).join(", ")+' (each name also carries a short code). The test lead goes into the copies, not the originals, and the copies are deleted when the check ends.');
|
|
49023
49077
|
else li.push('The test lead goes into '+names.map(esc).join(", ")+'. Those workflows really text and email you, and may alert your staff once.');
|
|
49078
|
+
// Walk W38: in the copies, staff alerts, tasks and assignments go to one person, marked TEST; say who, by name.
|
|
49079
|
+
if(ff){var at=document.getElementById("alert-to"),opt=at&&at.value?at.options[at.selectedIndex]:null,who=opt?String(opt.textContent||"").replace(/\\s*\\([^()]*@[^()]*\\)\\s*$/,""):"";
|
|
49080
|
+
li.push(who?'Staff alerts, tasks and assignments in the copies go to '+esc(who)+', marked TEST.':'Staff alerts, tasks and assignments in the copies go to your own login in this account, if your email matches one, marked TEST.');
|
|
49081
|
+
// Walk W40: the copy's safeguards, said before the click.
|
|
49082
|
+
li.push('The copies have no triggers, so only the test lead goes in and no real lead can enter them. The workflows themselves are not changed, and a draft stays a draft.')}
|
|
49024
49083
|
var ask=document.getElementById("start-ask");
|
|
49025
49084
|
ask.innerHTML='<div class="cds-callout"><span class="ttl">Before the check starts: this writes to '+esc(clientName)+'</span><ul>'+li.map(function(x){return '<li>'+x+'</li>'}).join("")+'</ul><div class="cds-actions"><button class="cds-btn" type="button" id="start-yes">Start the check in '+esc(clientName)+'</button><button class="cds-btn ghost" type="button" id="start-no">Cancel</button></div></div>';
|
|
49026
49085
|
document.getElementById("start-yes").onclick=function(){startConfirmed=true;form.dispatchEvent(new Event("submit",{cancelable:true}))};
|
|
@@ -49039,15 +49098,15 @@ ${COCKPIT_TYPE_JS}
|
|
|
49039
49098
|
// Walk 2026-10-28 W8: before anything is written, say what the check writes, in names, in this account.
|
|
49040
49099
|
if(!startConfirmed){showStartAsk(ids);return}
|
|
49041
49100
|
startConfirmed=false;document.getElementById("start-ask").innerHTML="";
|
|
49042
|
-
var
|
|
49101
|
+
var btns=[].slice.call(document.querySelectorAll("[data-start]"));btns.forEach(function(b){b.disabled=true});say("Starting\u2026");
|
|
49043
49102
|
var hosts=[].slice.call(form.querySelectorAll('input[name="hook"]:checked')).map(function(i){return i.value});
|
|
49044
|
-
post("/api/shopper/start",
|
|
49103
|
+
post("/api/shopper/start",startBody(ids,hosts))
|
|
49045
49104
|
.then(function(r){if(r.ok){location.reload();return}
|
|
49046
49105
|
// Out of date: the list is drawn again as the account has it now, the ticks still there are kept, and the
|
|
49047
49106
|
// refusal says what changed. Nothing started; pressing Start again sends the list now shown.
|
|
49048
49107
|
if(r.stale&&r.workflows){var keep={};ids.forEach(function(id){keep[id]=true});drawWorkflows(r.workflows,keep)}
|
|
49049
|
-
|
|
49050
|
-
.catch(function(){
|
|
49108
|
+
btns.forEach(function(b){b.disabled=false});say(r.error||"That did not start.")})
|
|
49109
|
+
.catch(function(){btns.forEach(function(b){b.disabled=false});say("That did not start. Check that ${productName()} is still running.")});
|
|
49051
49110
|
})}
|
|
49052
49111
|
// Fast-forward sends the test lead to the workflows' outside services for real:
|
|
49053
49112
|
// read them from the chosen workflows and let the owner tick each one.
|
|
@@ -49205,6 +49264,68 @@ function websiteHost(url) {
|
|
|
49205
49264
|
return void 0;
|
|
49206
49265
|
}
|
|
49207
49266
|
}
|
|
49267
|
+
function smallCopyPaths(file3, st) {
|
|
49268
|
+
let dir;
|
|
49269
|
+
try {
|
|
49270
|
+
dir = path32.join(appDataDir(), "logo-copies");
|
|
49271
|
+
} catch {
|
|
49272
|
+
return void 0;
|
|
49273
|
+
}
|
|
49274
|
+
const key2 = crypto11.createHash("sha256").update(`${path32.resolve(file3)}|${st.size}|${st.mtimeMs}`).digest("hex").slice(0, 24);
|
|
49275
|
+
return [{ px: 600, format: "png", mime: "image/png" }, { px: 400, format: "png", mime: "image/png" }, { px: 600, format: "jpeg", mime: "image/jpeg" }].map((t) => ({ ...t, out: path32.join(dir, `${key2}-${t.px}.${t.format === "png" ? "png" : "jpg"}`) }));
|
|
49276
|
+
}
|
|
49277
|
+
function smallLogoCopy(file3, st, maxBytes) {
|
|
49278
|
+
for (const t of smallCopyPaths(file3, st) ?? []) {
|
|
49279
|
+
try {
|
|
49280
|
+
const buf = fs33.readFileSync(t.out);
|
|
49281
|
+
if (buf.length <= maxBytes && RASTER_TYPES.some((r) => r.mime === t.mime && r.test(buf))) return { mime: t.mime, buf };
|
|
49282
|
+
} catch {
|
|
49283
|
+
}
|
|
49284
|
+
}
|
|
49285
|
+
return void 0;
|
|
49286
|
+
}
|
|
49287
|
+
function prepareSmallLogo(file3, maxBytes = LOGO_MAX_BYTES) {
|
|
49288
|
+
if (!file3 || process.platform !== "darwin") return Promise.resolve();
|
|
49289
|
+
const abs = path32.resolve(file3);
|
|
49290
|
+
const running = preparing.get(abs);
|
|
49291
|
+
if (running) return running;
|
|
49292
|
+
const job = (async () => {
|
|
49293
|
+
let st;
|
|
49294
|
+
try {
|
|
49295
|
+
st = fs33.statSync(abs);
|
|
49296
|
+
} catch {
|
|
49297
|
+
return;
|
|
49298
|
+
}
|
|
49299
|
+
if (!st.isFile() || st.size <= maxBytes || st.size > LOGO_SOURCE_MAX_BYTES) return;
|
|
49300
|
+
if (smallLogoCopy(abs, st, maxBytes)) return;
|
|
49301
|
+
const tries = smallCopyPaths(abs, st);
|
|
49302
|
+
if (!tries) return;
|
|
49303
|
+
try {
|
|
49304
|
+
ensureAppDataDir();
|
|
49305
|
+
fs33.mkdirSync(path32.dirname(tries[0].out), { recursive: true, mode: 448 });
|
|
49306
|
+
} catch {
|
|
49307
|
+
return;
|
|
49308
|
+
}
|
|
49309
|
+
for (const t of tries) {
|
|
49310
|
+
const tmp = `${t.out}.tmp-${crypto11.randomUUID()}.${t.format === "png" ? "png" : "jpg"}`;
|
|
49311
|
+
const ok = await new Promise((done) => (0, import_node_child_process3.execFile)("/usr/bin/sips", ["-Z", String(t.px), "-s", "format", t.format, ...t.format === "jpeg" ? ["-s", "formatOptions", "80"] : [], abs, "--out", tmp], { timeout: 2e4 }, (e) => done(!e)));
|
|
49312
|
+
try {
|
|
49313
|
+
if (ok && fs33.statSync(tmp).size <= maxBytes) {
|
|
49314
|
+
fs33.renameSync(tmp, t.out);
|
|
49315
|
+
return;
|
|
49316
|
+
}
|
|
49317
|
+
} catch {
|
|
49318
|
+
}
|
|
49319
|
+
try {
|
|
49320
|
+
fs33.rmSync(tmp, { force: true });
|
|
49321
|
+
} catch {
|
|
49322
|
+
}
|
|
49323
|
+
}
|
|
49324
|
+
notSmallEnough.add(fileKey(abs, st));
|
|
49325
|
+
})().finally(() => preparing.delete(abs));
|
|
49326
|
+
preparing.set(abs, job);
|
|
49327
|
+
return job;
|
|
49328
|
+
}
|
|
49208
49329
|
function logoFileDataUri(file3, maxBytes = LOGO_MAX_BYTES) {
|
|
49209
49330
|
const name2 = path32.basename(file3);
|
|
49210
49331
|
let st;
|
|
@@ -49214,7 +49335,7 @@ function logoFileDataUri(file3, maxBytes = LOGO_MAX_BYTES) {
|
|
|
49214
49335
|
return { problem: `The logo file ${name2} was not found on this computer.` };
|
|
49215
49336
|
}
|
|
49216
49337
|
if (!st.isFile()) return { problem: `The logo path ${name2} is not a file.` };
|
|
49217
|
-
if (st.size >
|
|
49338
|
+
if (st.size > LOGO_SOURCE_MAX_BYTES) return { problem: `The logo file ${name2} is ${Math.round(st.size / 1024 / 1024)} MB, more than the ${LOGO_SOURCE_MAX_BYTES / 1024 / 1024} MB a logo file can be.` };
|
|
49218
49339
|
let buf;
|
|
49219
49340
|
try {
|
|
49220
49341
|
buf = fs33.readFileSync(file3);
|
|
@@ -49222,7 +49343,16 @@ function logoFileDataUri(file3, maxBytes = LOGO_MAX_BYTES) {
|
|
|
49222
49343
|
return { problem: `The logo file ${name2} could not be read.` };
|
|
49223
49344
|
}
|
|
49224
49345
|
const raster = RASTER_TYPES.find((t) => t.test(buf));
|
|
49225
|
-
if (raster) return { src: `data:${raster.mime};base64,${buf.toString("base64")}` };
|
|
49346
|
+
if (raster && buf.length <= maxBytes) return { src: `data:${raster.mime};base64,${buf.toString("base64")}` };
|
|
49347
|
+
if (raster) {
|
|
49348
|
+
const small = smallLogoCopy(file3, st, maxBytes);
|
|
49349
|
+
if (small) return { src: `data:${small.mime};base64,${small.buf.toString("base64")}` };
|
|
49350
|
+
if (process.platform !== "darwin") return { problem: `The logo file ${name2} is large, and making a small copy of it needs a Mac. The logo link from Your agency works instead.` };
|
|
49351
|
+
if (notSmallEnough.has(fileKey(path32.resolve(file3), st))) return { problem: `The logo file ${name2} could not be made small enough for a report. The logo link from Your agency works instead.` };
|
|
49352
|
+
if (!preparing.has(path32.resolve(file3))) void prepareSmallLogo(file3, maxBytes);
|
|
49353
|
+
return { problem: `The small copy of the logo file ${name2} for reports is being made. It shows from the next report; the logo link from Your agency works meanwhile.` };
|
|
49354
|
+
}
|
|
49355
|
+
if (st.size > maxBytes) return { problem: `The logo file ${name2} is not a PNG, JPEG, WebP or GIF image, and is too large to embed as an SVG drawing.` };
|
|
49226
49356
|
const text3 = buf.toString("utf8").replace(/^\ufeff/, "");
|
|
49227
49357
|
if (/<svg[\s>]/i.test(text3)) {
|
|
49228
49358
|
let svg;
|
|
@@ -49359,10 +49489,12 @@ function brandChooserHtml(b, form) {
|
|
|
49359
49489
|
const save = form.save ? `<p class="rp-soon" id="rp-choose-status" role="status" hidden></p><button type="button" class="cds-btn ghost" id="rp-choose-retry" hidden>Try again</button><script>${brandChooserSaveJs(form.save)}</script>` : "";
|
|
49360
49490
|
return `<form class="rp-choose" method="get" action="${esc3(form.action)}"><span>Branding on this report</span>${hidden2}<span class="cds-seg">${opt("agency", !b.agencyAvailable)}${opt("neutral")}${opt("standard")}</span></form>${save}${notes}`;
|
|
49361
49491
|
}
|
|
49362
|
-
var fs33, path32, REPORT_BRAND_MODES, REPORT_BRAND_LABELS, LEGACY_MODES, MIN_ACCENT_CONTRAST, TEXT_BACKGROUNDS, GHL_FOOTER, LOGO_MAX_BYTES, RASTER_TYPES, LOGO_DATA_URI, REPORT_BRAND_CHOICES_ENV, LOCATION_ID, esc3, BRAND_LAYOUT_CSS, BRAND_LOGO_JS, BRAND_SAVE_UNREACHABLE;
|
|
49492
|
+
var crypto11, import_node_child_process3, fs33, path32, REPORT_BRAND_MODES, REPORT_BRAND_LABELS, LEGACY_MODES, MIN_ACCENT_CONTRAST, TEXT_BACKGROUNDS, GHL_FOOTER, LOGO_MAX_BYTES, LOGO_SOURCE_MAX_BYTES, preparing, notSmallEnough, fileKey, RASTER_TYPES, LOGO_DATA_URI, REPORT_BRAND_CHOICES_ENV, LOCATION_ID, esc3, BRAND_LAYOUT_CSS, BRAND_LOGO_JS, BRAND_SAVE_UNREACHABLE;
|
|
49363
49493
|
var init_report_brand = __esm({
|
|
49364
49494
|
"src/command-os/report-brand.ts"() {
|
|
49365
49495
|
"use strict";
|
|
49496
|
+
crypto11 = __toESM(require("node:crypto"));
|
|
49497
|
+
import_node_child_process3 = require("node:child_process");
|
|
49366
49498
|
fs33 = __toESM(require("node:fs"));
|
|
49367
49499
|
path32 = __toESM(require("node:path"));
|
|
49368
49500
|
init_agency_profile();
|
|
@@ -49379,6 +49511,10 @@ var init_report_brand = __esm({
|
|
|
49379
49511
|
TEXT_BACKGROUNDS = ["surface", "bg", "surface-sunk"];
|
|
49380
49512
|
GHL_FOOTER = "Made with GHL Command \xB7 ghlcommand.com";
|
|
49381
49513
|
LOGO_MAX_BYTES = 256 * 1024;
|
|
49514
|
+
LOGO_SOURCE_MAX_BYTES = 20 * 1024 * 1024;
|
|
49515
|
+
preparing = /* @__PURE__ */ new Map();
|
|
49516
|
+
notSmallEnough = /* @__PURE__ */ new Set();
|
|
49517
|
+
fileKey = (abs, st) => `${abs}|${st.size}|${st.mtimeMs}`;
|
|
49382
49518
|
RASTER_TYPES = [
|
|
49383
49519
|
{ mime: "image/png", test: (b) => b.length > 8 && b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) },
|
|
49384
49520
|
{ mime: "image/jpeg", test: (b) => b.length > 3 && b[0] === 255 && b[1] === 216 && b[2] === 255 },
|
|
@@ -50107,7 +50243,7 @@ function planFindCard(nodes, offer, ctx) {
|
|
|
50107
50243
|
if (tailIds.has(str12(n.id) ?? "")) continue;
|
|
50108
50244
|
after.push(n);
|
|
50109
50245
|
}
|
|
50110
|
-
const at = (id2,
|
|
50246
|
+
const at = (id2, path67, want, what, mode) => ({ nodeId: id2, path: path67, want, what, ...mode ? { mode } : {} });
|
|
50111
50247
|
const checks = [
|
|
50112
50248
|
at(findId, ["type"], "find_opportunity", "the new Find opportunity step"),
|
|
50113
50249
|
at(findId, ["attributes", "__customInputFields__", "[filterField=pipeline_id]", "secondValue"], pipelineId, "the pipeline the Find opportunity step looks in"),
|
|
@@ -50206,9 +50342,9 @@ function stored(node) {
|
|
|
50206
50342
|
a = normalizeInternalCreateOpportunityAction(a);
|
|
50207
50343
|
return asObj10(a);
|
|
50208
50344
|
}
|
|
50209
|
-
function pick2(node,
|
|
50345
|
+
function pick2(node, path67) {
|
|
50210
50346
|
let cur = node;
|
|
50211
|
-
for (const seg of
|
|
50347
|
+
for (const seg of path67) {
|
|
50212
50348
|
const sel = /^\[(\w+)=(.*)\]$/.exec(seg);
|
|
50213
50349
|
if (sel) {
|
|
50214
50350
|
cur = asArr6(cur).map(asObj10).find((x) => String(x[sel[1]]) === sel[2]);
|
|
@@ -50751,6 +50887,20 @@ async function accountUsers(client, locationId2) {
|
|
|
50751
50887
|
email: str15(u.email)
|
|
50752
50888
|
}));
|
|
50753
50889
|
}
|
|
50890
|
+
async function fastForwardRefusals(s) {
|
|
50891
|
+
if (s.workflowIds.length > 5) return ["A fast-forward check copies at most 5 workflows."];
|
|
50892
|
+
let owner;
|
|
50893
|
+
if (s.alertUserId) {
|
|
50894
|
+
if (!(await accountUsers(s.client, s.locationId)).some((u) => u.id === s.alertUserId)) return ["The person chosen to receive staff alerts is not a user of this account any more. Pick someone from the list again."];
|
|
50895
|
+
owner = s.alertUserId;
|
|
50896
|
+
} else owner = await ownerUserId(s.client, s.locationId, s.ownerEmail);
|
|
50897
|
+
const out = [];
|
|
50898
|
+
for (const id2 of s.workflowIds) {
|
|
50899
|
+
const r = planCopy(toSource(id2, await s.admin.get(id2)), { ownerUserId: owner, allowedHosts: s.allowedHosts });
|
|
50900
|
+
if (!r.ok) out.push(r.refusal);
|
|
50901
|
+
}
|
|
50902
|
+
return out;
|
|
50903
|
+
}
|
|
50754
50904
|
async function sweepLeftovers(admin, locationId2, base, now) {
|
|
50755
50905
|
const keep = listRuns(locationId2, base).filter((r) => r.status === "watching").flatMap((r) => r.fastForward?.copies.map((c) => c.copyId) ?? []);
|
|
50756
50906
|
return sweep(admin, locationId2, base, now, keep);
|
|
@@ -51804,7 +51954,7 @@ async function readVerifyAccount(saved, client, builderSource = () => workflowRe
|
|
|
51804
51954
|
} };
|
|
51805
51955
|
const plannedTags = new Set(planned.filter((p) => p.type === "tag").map((p) => p.name.trim().toLowerCase()));
|
|
51806
51956
|
const meantLive = meantLiveRefs(saved);
|
|
51807
|
-
const
|
|
51957
|
+
const held2 = heldDraftGates(saved);
|
|
51808
51958
|
await Promise.allSettled(records.filter((r) => r.type === "workflow" && r.id).map(async (record) => {
|
|
51809
51959
|
try {
|
|
51810
51960
|
const full = await builder.getWorkflow(record.id);
|
|
@@ -51839,8 +51989,8 @@ async function readVerifyAccount(saved, client, builderSource = () => workflowRe
|
|
|
51839
51989
|
const draft = full.status !== "published";
|
|
51840
51990
|
if (errors.length || goneTags.length || draft && full.status !== "draft") record.status = "invalid";
|
|
51841
51991
|
else if (unreadable.length) record.status = "unreadable";
|
|
51842
|
-
else if (full.status === "draft") record.status =
|
|
51843
|
-
const heldGates = full.status === "draft" && !errors.length && !goneTags.length && !unreadable.length ?
|
|
51992
|
+
else if (full.status === "draft") record.status = held2.has(record.ref) || meantLive.has(record.ref) ? "invalid" : "pending";
|
|
51993
|
+
const heldGates = full.status === "draft" && !errors.length && !goneTags.length && !unreadable.length ? held2.get(record.ref) : void 0;
|
|
51844
51994
|
if (heldGates) record.held = true;
|
|
51845
51995
|
record.detail = heldGates ? heldDraftDetail(heldGates) : [record.status === "pending" ? "This workflow is built and waiting for you to switch it on in GoHighLevel under Automation > Workflows." : draft && full.status === "draft" && meantLive.has(record.ref) && !errors.length ? SHOULD_BE_LIVE : draft ? `Workflow status is ${full.status}.` : "", ...errors.map((f) => f.message), ...goneTags.map((t) => `It uses the tag "${t}", which is no longer in the account. Run Build again to put it back.`), ...unreadable.map((f) => `${f.message} ${lists.get(f.category)?.error || ""}`)].filter(Boolean).join(" ");
|
|
51846
51996
|
const triggers = Array.isArray(full.triggers) ? full.triggers : [];
|
|
@@ -52589,8 +52739,8 @@ function executablePlanLines(saved) {
|
|
|
52589
52739
|
const refs3 = [...names.keys()].sort((a, b) => b.length - a.length);
|
|
52590
52740
|
const refPattern = refs3.length ? new RegExp(`(?<![\\w./:@])(?:${refs3.map(escapeRE).join("|")})(?![\\w]|\\.[\\w])`, "g") : void 0;
|
|
52591
52741
|
const missingRefPattern = /(?<![\w./:@])(user|pipeline|stage|field|tag|workflow|form|calendar|email_template|sms_template|email|sms|cv|handoff|funnel)\.[a-z0-9_]+(?![\w]|\.[\w])/g;
|
|
52592
|
-
const
|
|
52593
|
-
const renderRefs = (s) =>
|
|
52742
|
+
const resolve15 = (s) => s.replace(missingRefPattern, (ref) => names.get(ref) || `Unnamed ${kindOf2(ref.split(".")[0])}`);
|
|
52743
|
+
const renderRefs = (s) => resolve15(refPattern ? s.replace(refPattern, (ref) => names.get(ref)) : s);
|
|
52594
52744
|
const mergeLabel = (field2) => {
|
|
52595
52745
|
const [group, ...parts] = field2.trim().split(".");
|
|
52596
52746
|
const key2 = parts.join(".");
|
|
@@ -52903,7 +53053,7 @@ function compute(ans, skip, asm) {
|
|
|
52903
53053
|
function() {
|
|
52904
53054
|
return raw("calls_missed") * share("missed_never_return") * c * v;
|
|
52905
53055
|
},
|
|
52906
|
-
"Call answering
|
|
53056
|
+
"Call answering"
|
|
52907
53057
|
);
|
|
52908
53058
|
add2(
|
|
52909
53059
|
"waited",
|
|
@@ -53586,7 +53736,12 @@ function programBuiltLabel(name2) {
|
|
|
53586
53736
|
}
|
|
53587
53737
|
return `Built: Program follow-up${win ? ` (texts over the ${win} after the visit)` : ""}`;
|
|
53588
53738
|
}
|
|
53589
|
-
function
|
|
53739
|
+
function statusLabel(label2, feature, status) {
|
|
53740
|
+
const what = label2.replace(/^Built: /, "");
|
|
53741
|
+
if (!status?.made) return `Will build: ${what}`;
|
|
53742
|
+
return status.made.has(feature) ? `Built: ${what}` : `Not built yet: ${what}`;
|
|
53743
|
+
}
|
|
53744
|
+
function boughtItemsFor(offered, wording = {}, status) {
|
|
53590
53745
|
return ASSESSMENT_FEATURES.map((f) => {
|
|
53591
53746
|
const id2 = f.id;
|
|
53592
53747
|
const base = { id: id2, name: f.name, means: BOUGHT_MEANS[id2] };
|
|
@@ -53596,11 +53751,12 @@ function boughtItemsFor(offered, wording = {}) {
|
|
|
53596
53751
|
if (!offered.includes(feature)) {
|
|
53597
53752
|
return { ...base, kind: "not-in-plan", feature, ...next, delivery: `Not built for this client: ${FEATURES[feature].name.toLowerCase()} is not in the plan for this industry.` };
|
|
53598
53753
|
}
|
|
53599
|
-
const
|
|
53754
|
+
const label2 = id2 === "quotes" && wording.programFollowUp ? programBuiltLabel(wording.programFollowUp) : BUILT_LABEL[id2];
|
|
53755
|
+
const delivery = status?.held?.[id2] ?? statusLabel(label2, feature, status);
|
|
53600
53756
|
return { ...base, kind: "built", feature, ...next, delivery };
|
|
53601
53757
|
}
|
|
53602
53758
|
const other = OTHER_DELIVERY[id2];
|
|
53603
|
-
return { ...base, kind: other.kind, delivery: other.text };
|
|
53759
|
+
return { ...base, kind: other.kind, delivery: status?.held?.[id2] ?? other.text };
|
|
53604
53760
|
});
|
|
53605
53761
|
}
|
|
53606
53762
|
function featuresFromBought(ticks, offered) {
|
|
@@ -53646,13 +53802,19 @@ function assessmentGateRefusal(state2, rk, clientKey, name2) {
|
|
|
53646
53802
|
function approvalBindingHash(scopeHash2, state2, rk) {
|
|
53647
53803
|
const saved = state2.bought?.[rk];
|
|
53648
53804
|
const answer2 = assessmentAnswerOf(state2, rk);
|
|
53649
|
-
|
|
53805
|
+
const asks = asksBound(state2, rk);
|
|
53806
|
+
if (!Array.isArray(saved) && !answer2 && !asks) return scopeHash2;
|
|
53650
53807
|
const snapshot = state2.assessmentAttachments?.[rk];
|
|
53651
|
-
return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null });
|
|
53808
|
+
return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null, ...asks ? { asks } : {} });
|
|
53652
53809
|
}
|
|
53653
53810
|
function boughtBindingHash(scopeHash2, state2, rk) {
|
|
53654
53811
|
const saved = state2.bought?.[rk];
|
|
53655
|
-
|
|
53812
|
+
const asks = asksBound(state2, rk);
|
|
53813
|
+
return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, ...asks ? { asks } : {} });
|
|
53814
|
+
}
|
|
53815
|
+
function asksBound(state2, rk) {
|
|
53816
|
+
const a = state2.asks?.[rk];
|
|
53817
|
+
return a && Object.keys(a).length ? a : void 0;
|
|
53656
53818
|
}
|
|
53657
53819
|
function intakeChannelAnswers(answers) {
|
|
53658
53820
|
const get2 = (key2, label2) => {
|
|
@@ -53672,6 +53834,12 @@ function boughtJobs(ticks, offered, channels = {}) {
|
|
|
53672
53834
|
reason: item.kind === "not-in-plan" ? "Bought, but not built automatically yet. It is not in the plan for this client's industry." : `Bought, but not built automatically yet. ${OTHER_DELIVERY[item.id].job}`
|
|
53673
53835
|
}));
|
|
53674
53836
|
}
|
|
53837
|
+
function heldJobs(held2) {
|
|
53838
|
+
return held2.map((h) => {
|
|
53839
|
+
const at = h.line.indexOf(": ");
|
|
53840
|
+
return { ref: `bought:held-${h.id}`, type: BOUGHT_JOB_TYPE, name: h.line.slice(0, at), reason: h.line.slice(at + 2) };
|
|
53841
|
+
});
|
|
53842
|
+
}
|
|
53675
53843
|
function boughtButExcludedJobs(ticks, offered, scopeFeatures) {
|
|
53676
53844
|
if (scopeFeatures === "all") return [];
|
|
53677
53845
|
return boughtItemsFor(offered).filter((item) => ticks.includes(item.id) && item.kind === "built" && item.feature && !scopeFeatures.includes(item.feature)).map((item) => ({
|
|
@@ -53694,10 +53862,10 @@ function assessmentChoiceLine(answer2, attached = answer2 === "attached") {
|
|
|
53694
53862
|
if (answer2 === "run-first") return "The Assessment will be run first.";
|
|
53695
53863
|
return "Not answered yet: has the Assessment been run for this client?";
|
|
53696
53864
|
}
|
|
53697
|
-
function boughtViewFor(state2, rk, offered, wording = {}) {
|
|
53865
|
+
function boughtViewFor(state2, rk, offered, wording = {}, status) {
|
|
53698
53866
|
const { ticks, source } = ticksFor(state2, rk);
|
|
53699
53867
|
return {
|
|
53700
|
-
items: boughtItemsFor(offered, wording),
|
|
53868
|
+
items: boughtItemsFor(offered, wording, status),
|
|
53701
53869
|
ticks,
|
|
53702
53870
|
source,
|
|
53703
53871
|
answer: assessmentAnswerOf(state2, rk),
|
|
@@ -53706,17 +53874,20 @@ function boughtViewFor(state2, rk, offered, wording = {}) {
|
|
|
53706
53874
|
suggestions: source === "saved" ? claudeSuggestions(ticks) : []
|
|
53707
53875
|
};
|
|
53708
53876
|
}
|
|
53709
|
-
function boughtDocumentFor(state2, rk, offered, wording = {}) {
|
|
53877
|
+
function boughtDocumentFor(state2, rk, offered, wording = {}, status) {
|
|
53710
53878
|
const { ticks, source } = ticksFor(state2, rk);
|
|
53711
|
-
const items = boughtItemsFor(offered, wording);
|
|
53879
|
+
const items = boughtItemsFor(offered, wording, status);
|
|
53712
53880
|
const confirmed = source === "saved" || source === "scope";
|
|
53713
53881
|
const ticked = items.filter((i) => ticks.includes(i.id));
|
|
53714
53882
|
return {
|
|
53715
53883
|
confirmed,
|
|
53716
53884
|
assessment: assessmentChoiceLine(assessmentAnswerOf(state2, rk), Boolean(state2.assessmentAttachments?.[rk])),
|
|
53717
|
-
|
|
53885
|
+
// "No call goes unanswered" is a promise only an AI answering live keeps, and this version builds none: the client
|
|
53886
|
+
// reads what the build did for calls (promise-vs-build F4 wording, 2026-10-29).
|
|
53887
|
+
bought: ticked.map((i) => ({ name: i.name, means: i.id === "missed" ? status?.made?.has("missed_call") ? "Missed calls get a text back" : "How calls are answered" : i.means, delivery: clientDelivery(i) })),
|
|
53718
53888
|
// The client reads this: never the agency's job text, and never "with Claude" (review of ea3526f).
|
|
53719
53889
|
stillToDo: ticked.flatMap((i) => {
|
|
53890
|
+
if (HELD_LINE.test(i.delivery)) return [`${i.name}: ${clientDelivery(i).toLowerCase()}.`];
|
|
53720
53891
|
if (i.kind === "go-live") return [`${i.name}: being set up in your account.`];
|
|
53721
53892
|
if (i.kind === "separate") return [`${i.name}: the form is ready. The pages are a separate piece of work.`];
|
|
53722
53893
|
if (i.kind === "not-in-plan") return [`${i.name}: being set up separately from this build.`];
|
|
@@ -53726,13 +53897,15 @@ function boughtDocumentFor(state2, rk, offered, wording = {}) {
|
|
|
53726
53897
|
};
|
|
53727
53898
|
}
|
|
53728
53899
|
function clientDelivery(i) {
|
|
53900
|
+
const held2 = HELD_LINE.exec(i.delivery);
|
|
53901
|
+
if (held2) return held2[1] === "Waiting for your client's answer" ? "Waiting on your answer" : held2[1] === "Not built, your client said not yet" ? "Not set up, as you said not yet" : "Chosen, and coming in a later update";
|
|
53729
53902
|
if (i.kind === "go-live") return "Being set up in your account";
|
|
53730
53903
|
if (i.kind === "separate") return "The form is built; the pages are separate work";
|
|
53731
53904
|
if (i.kind === "not-in-plan") return "Not part of this build";
|
|
53732
53905
|
if (i.kind === "claude") return "A weekly report on what is working";
|
|
53733
53906
|
return i.delivery;
|
|
53734
53907
|
}
|
|
53735
|
-
var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE;
|
|
53908
|
+
var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE, HELD_LINE;
|
|
53736
53909
|
var init_bought_list = __esm({
|
|
53737
53910
|
"src/bought-list.ts"() {
|
|
53738
53911
|
"use strict";
|
|
@@ -53741,11 +53914,13 @@ var init_bought_list = __esm({
|
|
|
53741
53914
|
init_build_contract();
|
|
53742
53915
|
BOUGHT_IDS = ASSESSMENT_FEATURES.map((f) => f.id);
|
|
53743
53916
|
BOUGHT_MEANS = {
|
|
53744
|
-
|
|
53917
|
+
// rc.1 MED-1: true whatever the client chooses; nothing answers live until a voice agent is built.
|
|
53918
|
+
missed: "Callers get the answer your client chooses",
|
|
53745
53919
|
waited: "Every new lead hears back in seconds",
|
|
53746
53920
|
quotes: "No quote goes cold",
|
|
53747
53921
|
noshow: "Bookings confirmed, reminded and rescued",
|
|
53748
|
-
|
|
53922
|
+
// rc.1 MED-2: what Win-back reaches (leads who go cold from now on); an old list only once a campaign for it is built.
|
|
53923
|
+
reactivation: "Leads who go quiet are brought back",
|
|
53749
53924
|
nurture: "Leads kept warm until they are ready",
|
|
53750
53925
|
recall: "Clients come back on schedule",
|
|
53751
53926
|
deposits: "Fewer no-shows, cash up front",
|
|
@@ -53783,6 +53958,7 @@ var init_bought_list = __esm({
|
|
|
53783
53958
|
ATTACH_THE_ASSESSMENT = "Attach the Assessment in step 4, or choose Build without an Assessment there.";
|
|
53784
53959
|
RUN_THE_ASSESSMENT_FIRST = "You chose to run the Assessment first. Attach it in step 4 when it is done, or choose Build without an Assessment there.";
|
|
53785
53960
|
BOUGHT_JOB_TYPE = "bought-not-built";
|
|
53961
|
+
HELD_LINE = /^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
|
|
53786
53962
|
}
|
|
53787
53963
|
});
|
|
53788
53964
|
|
|
@@ -53840,6 +54016,23 @@ var init_offer_normalize = __esm({
|
|
|
53840
54016
|
}
|
|
53841
54017
|
});
|
|
53842
54018
|
|
|
54019
|
+
// src/intake-to-build/texting.ts
|
|
54020
|
+
function textingAnswer(brief) {
|
|
54021
|
+
const channels = brief && typeof brief === "object" ? brief.channels : void 0;
|
|
54022
|
+
const sms = channels && typeof channels === "object" ? channels.sms : void 0;
|
|
54023
|
+
if (sms === true) return "yes";
|
|
54024
|
+
if (sms === false) return "not_yet";
|
|
54025
|
+
return "unanswered";
|
|
54026
|
+
}
|
|
54027
|
+
function smsOn(brief) {
|
|
54028
|
+
return textingAnswer(brief) === "yes";
|
|
54029
|
+
}
|
|
54030
|
+
var init_texting = __esm({
|
|
54031
|
+
"src/intake-to-build/texting.ts"() {
|
|
54032
|
+
"use strict";
|
|
54033
|
+
}
|
|
54034
|
+
});
|
|
54035
|
+
|
|
53843
54036
|
// src/intake-to-build/brief.ts
|
|
53844
54037
|
function norm4(s) {
|
|
53845
54038
|
return s.trim().toLowerCase();
|
|
@@ -54598,9 +54791,9 @@ function distillProof(brief) {
|
|
|
54598
54791
|
if (quote2) proof.testimonials = [{ text: quote2, consent }];
|
|
54599
54792
|
if (Object.keys(proof).length === 0) delete brief.proof;
|
|
54600
54793
|
}
|
|
54601
|
-
function setPath(target,
|
|
54794
|
+
function setPath(target, path67, value) {
|
|
54602
54795
|
if (value === void 0) return;
|
|
54603
|
-
const parts =
|
|
54796
|
+
const parts = path67.split(".");
|
|
54604
54797
|
let node = target;
|
|
54605
54798
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
54606
54799
|
const k = parts[i];
|
|
@@ -54614,7 +54807,7 @@ function deriveFlags(brief) {
|
|
|
54614
54807
|
const channels = brief.channels ?? {};
|
|
54615
54808
|
const goal = brief.goal ?? {};
|
|
54616
54809
|
const a2p = typeof channels.a2pStatus === "string" ? channels.a2pStatus.toLowerCase() : "";
|
|
54617
|
-
if (
|
|
54810
|
+
if (smsOn(brief) && a2p !== "approved" && a2p !== "not needed") {
|
|
54618
54811
|
flags.push("needs_a2p");
|
|
54619
54812
|
}
|
|
54620
54813
|
if (channels.payment === "Stripe not connected" || channels.payment === void 0) flags.push("stripe_not_connected");
|
|
@@ -54622,7 +54815,7 @@ function deriveFlags(brief) {
|
|
|
54622
54815
|
flags.push("calendar_oauth_needed");
|
|
54623
54816
|
}
|
|
54624
54817
|
if (channels.email === false) flags.push("email_not_ready");
|
|
54625
|
-
if (
|
|
54818
|
+
if (smsOn(brief) && channels.hasPhoneNumber !== "yes") {
|
|
54626
54819
|
flags.push("phone_number_needed");
|
|
54627
54820
|
}
|
|
54628
54821
|
return flags;
|
|
@@ -54793,6 +54986,7 @@ var init_normalizer = __esm({
|
|
|
54793
54986
|
"src/intake-to-build/normalizer.ts"() {
|
|
54794
54987
|
"use strict";
|
|
54795
54988
|
init_offer_normalize();
|
|
54989
|
+
init_texting();
|
|
54796
54990
|
init_question_set();
|
|
54797
54991
|
init_brief2();
|
|
54798
54992
|
EMAIL_RE3 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -54893,10 +55087,10 @@ function livePending(pending, nowMs) {
|
|
|
54893
55087
|
return out;
|
|
54894
55088
|
}
|
|
54895
55089
|
function applyStaffPending(check, pending, nowMs) {
|
|
54896
|
-
const
|
|
54897
|
-
const waiting = check.missing.filter((m) =>
|
|
55090
|
+
const held2 = livePending(pending, nowMs);
|
|
55091
|
+
const waiting = check.missing.filter((m) => held2[normEmail(m.email)]);
|
|
54898
55092
|
if (!waiting.length) return check;
|
|
54899
|
-
const isHeld = (email) => Boolean(
|
|
55093
|
+
const isHeld = (email) => Boolean(held2[normEmail(email)]);
|
|
54900
55094
|
const lines = new Map(waiting.map((m) => [normEmail(m.email), staffWaitingLine(m.name)]));
|
|
54901
55095
|
return {
|
|
54902
55096
|
...check,
|
|
@@ -55373,7 +55567,8 @@ function context(src) {
|
|
|
55373
55567
|
const notify2 = clean3(src.roster?.notifyName) || intakeAnswer2(src.intake, "notify_name") || void 0;
|
|
55374
55568
|
const calls = clean3(src.roster?.callsName) || intakeAnswer2(src.intake, "calls_name") || void 0;
|
|
55375
55569
|
const notifyShown = notify2 ? teamNames(src).find((n) => sameName(n, notify2)) : void 0;
|
|
55376
|
-
|
|
55570
|
+
const callAnswering = src.recordKey && src.boughtState?.asks?.[src.recordKey]?.callAnswering || [];
|
|
55571
|
+
return { features, live: workflowsConfirmed(plan, src.lines), calendars, notify: notify2, ...notifyShown && { notifyShown }, calls, items, ticked: confirmedTicks ? ticks : [], callAnswering, confirmedTicks, programFollowUp: isProgramFollowUp(plan), review: reviewTimingOf(plan), noShowRescue: hasWorkflow(plan, "workflow.no_show"), reply: replyChannels(plan) };
|
|
55377
55572
|
}
|
|
55378
55573
|
function printableCalendars(ctx) {
|
|
55379
55574
|
return [...new Set(ctx.calendars.map((c) => c.name))].filter((n) => n !== "booking calendar");
|
|
@@ -55435,7 +55630,9 @@ function summaryOf2(ctx, plan) {
|
|
|
55435
55630
|
}
|
|
55436
55631
|
const extras = [
|
|
55437
55632
|
f("missed_call") ? "missed calls get a text back" : "",
|
|
55438
|
-
|
|
55633
|
+
// Win-back starts from a tag Lead Nurture or No-Show Rescue adds, so it reaches leads who go cold from now on,
|
|
55634
|
+
// not an old list (promise-vs-build F5, 2026-10-29).
|
|
55635
|
+
f("winback") ? "leads who go cold get a win-back message" : "",
|
|
55439
55636
|
f("quote_followup") ? ctx.programFollowUp ? PROGRAM_WORDS.quotes.summary : "every quote is followed up" : "",
|
|
55440
55637
|
f("review_requests") ? "happy clients are asked for a review" : ""
|
|
55441
55638
|
].filter(Boolean);
|
|
@@ -55552,7 +55749,9 @@ function boughtOf(ctx) {
|
|
|
55552
55749
|
if (built) working.push(boughtLabel(item, true, ctx));
|
|
55553
55750
|
else if (item.kind !== "built") next.push(boughtLabel(item, false, ctx));
|
|
55554
55751
|
else next.push(item.name);
|
|
55555
|
-
if (item.id === "missed" &&
|
|
55752
|
+
if (item.id === "missed" && ctx.ticked.includes("missed")) {
|
|
55753
|
+
for (const c of ctx.callAnswering) if (c !== "missed_text") next.push(c === "live_voice" ? "An AI that answers the phone live (chosen, coming in a later update)" : "An AI that replies by text or chat and books (chosen, coming in a later update)");
|
|
55754
|
+
}
|
|
55556
55755
|
}
|
|
55557
55756
|
return { workingLabel: ctx.live ? "Working now" : "Built, ready to switch on", working, next };
|
|
55558
55757
|
}
|
|
@@ -56554,10 +56753,10 @@ var init_build_obligations = __esm({
|
|
|
56554
56753
|
});
|
|
56555
56754
|
|
|
56556
56755
|
// src/command-os/cockpit-routes.ts
|
|
56557
|
-
function routeOf(method,
|
|
56756
|
+
function routeOf(method, path67) {
|
|
56558
56757
|
const m = method === "HEAD" ? "GET" : method;
|
|
56559
56758
|
const fits = (r) => r.method === "ANY" || r.method === m;
|
|
56560
|
-
return COCKPIT_ROUTES.find((r) => r.match === "exact" && r.path ===
|
|
56759
|
+
return COCKPIT_ROUTES.find((r) => r.match === "exact" && r.path === path67 && fits(r)) ?? COCKPIT_ROUTES.find((r) => r.match === "pattern" && r.pattern.test(path67) && fits(r)) ?? COCKPIT_ROUTES.find((r) => r.match === "prefix" && path67.startsWith(r.path) && path67.length > r.path.length && fits(r));
|
|
56561
56760
|
}
|
|
56562
56761
|
function routeAllows(route, plan) {
|
|
56563
56762
|
return route.plans.includes(plan ?? "");
|
|
@@ -56574,10 +56773,10 @@ var init_cockpit_routes = __esm({
|
|
|
56574
56773
|
OS = ["command-os"];
|
|
56575
56774
|
BOTH = ["pro", "command-os"];
|
|
56576
56775
|
PRO_ONLY = ["pro"];
|
|
56577
|
-
page = (
|
|
56578
|
-
get = (
|
|
56579
|
-
post = (
|
|
56580
|
-
any = (
|
|
56776
|
+
page = (path67, scope, plans, match = "exact", pattern) => ({ method: "GET", path: path67, match, pattern, scope, plans, write: false, kind: "page" });
|
|
56777
|
+
get = (path67, scope, plans) => ({ method: "GET", path: path67, match: "exact", scope, plans, write: false, kind: "api" });
|
|
56778
|
+
post = (path67, scope, plans) => ({ method: "POST", path: path67, match: "exact", scope, plans, write: true, kind: "api" });
|
|
56779
|
+
any = (path67, scope, plans, write, match = "exact", kind = "api") => ({ method: "ANY", path: path67, match, scope, plans, write, kind });
|
|
56581
56780
|
none = (r) => ({ ...r, client: { none: true } });
|
|
56582
56781
|
via = (r, client) => ({ ...r, client });
|
|
56583
56782
|
COCKPIT_ROUTES = [
|
|
@@ -56872,22 +57071,22 @@ function helperPresence(opts) {
|
|
|
56872
57071
|
}
|
|
56873
57072
|
};
|
|
56874
57073
|
}
|
|
56875
|
-
var
|
|
57074
|
+
var import_node_child_process4, import_node_crypto25, fs39, path38, import_zod79, execHelper, HelperOut, PinSchema, SIGN_TIMEOUT_MS, READ_TIMEOUT_MS, DIGEST_HEX;
|
|
56876
57075
|
var init_presence = __esm({
|
|
56877
57076
|
"src/command-os/brain/presence.ts"() {
|
|
56878
57077
|
"use strict";
|
|
56879
|
-
|
|
57078
|
+
import_node_child_process4 = require("node:child_process");
|
|
56880
57079
|
import_node_crypto25 = require("node:crypto");
|
|
56881
57080
|
fs39 = __toESM(require("node:fs"));
|
|
56882
57081
|
path38 = __toESM(require("node:path"));
|
|
56883
57082
|
import_zod79 = require("zod");
|
|
56884
57083
|
init_shapes2();
|
|
56885
|
-
execHelper = (file3, args, timeoutMs) => new Promise((
|
|
56886
|
-
(0,
|
|
57084
|
+
execHelper = (file3, args, timeoutMs) => new Promise((resolve15) => {
|
|
57085
|
+
(0, import_node_child_process4.execFile)(file3, args, { timeout: timeoutMs, maxBuffer: 64 * 1024, shell: false, windowsHide: true, encoding: "utf8" }, (err, stdout) => {
|
|
56887
57086
|
const code = err && "code" in err ? err.code : null;
|
|
56888
|
-
if (code === "ENOENT" || code === "EACCES") return
|
|
56889
|
-
if (err && "killed" in err && err.killed) return
|
|
56890
|
-
|
|
57087
|
+
if (code === "ENOENT" || code === "EACCES") return resolve15({ code: null, stdout: "", missing: true });
|
|
57088
|
+
if (err && "killed" in err && err.killed) return resolve15({ code: null, stdout: "", timedOut: true });
|
|
57089
|
+
resolve15({ code: err ? typeof code === "number" ? code : null : 0, stdout: typeof stdout === "string" ? stdout : "" });
|
|
56891
57090
|
});
|
|
56892
57091
|
});
|
|
56893
57092
|
HelperOut = import_zod79.z.object({
|
|
@@ -57387,10 +57586,10 @@ async function leadLabels(deps, locationId2, contactIds) {
|
|
|
57387
57586
|
await Promise.all(unique.map(async (id2) => {
|
|
57388
57587
|
const stop3 = new AbortController();
|
|
57389
57588
|
let timer;
|
|
57390
|
-
const late = new Promise((
|
|
57589
|
+
const late = new Promise((resolve15) => {
|
|
57391
57590
|
timer = setTimeout(() => {
|
|
57392
57591
|
stop3.abort(new Error("lead lookup timed out"));
|
|
57393
|
-
|
|
57592
|
+
resolve15(null);
|
|
57394
57593
|
}, LEAD_LOOKUP_MS);
|
|
57395
57594
|
});
|
|
57396
57595
|
try {
|
|
@@ -58819,8 +59018,8 @@ async function checkLead(deps, ghl, store2, args) {
|
|
|
58819
59018
|
const check = runPrecheck(args.text, precheckContext(briefNow, client, glossary, rules, optOutWhy({ brainOptedOut: b.brief.optedOut, contact, messages }), now));
|
|
58820
59019
|
const failed = check.checks.filter((c) => !c.pass);
|
|
58821
59020
|
if (failed.length) return stop("checks", `A check fails now: ${failed.map((c) => `${c.check} (${c.why ?? ""})`).join("; ")}`);
|
|
58822
|
-
const
|
|
58823
|
-
if (
|
|
59021
|
+
const held2 = await gateAgain(ghl, store2, { client, d, text: args.text, contactId, now, brief: briefNow, glossary, firstName: contact.firstName ?? null, ...live ? { live } : {} });
|
|
59022
|
+
if (held2) return held2;
|
|
58824
59023
|
return { channel, day: today2.date, messages, sentToday: sentToday2, contact };
|
|
58825
59024
|
}
|
|
58826
59025
|
async function gateAgain(ghl, store2, x) {
|
|
@@ -58895,15 +59094,15 @@ async function checkTextNow(deps, store2, x) {
|
|
|
58895
59094
|
const check = runPrecheck(x.text, precheckContext(b.brief, x.client, glossary, rules, optOutWhy({ brainOptedOut: b.brief.optedOut, contact }), x.now));
|
|
58896
59095
|
const found = findingsOf(check.checks.filter((c) => !c.pass)).map((f) => `${checkLabel(f.check)}: ${f.found}.`);
|
|
58897
59096
|
if (found.length) return { ok: true, found };
|
|
58898
|
-
const
|
|
58899
|
-
if (
|
|
59097
|
+
const held2 = await gateAgain(ghl, store2, { client: x.client, d: x.d, text: x.text, contactId, now: x.now, brief: b.brief, glossary, firstName: contact.firstName ?? null });
|
|
59098
|
+
if (held2 && !held2.ok && held2.sent === false) return { ok: true, found: [held2.why.replace(/^Not sent: /, "")] };
|
|
58900
59099
|
return { ok: true, found: [] };
|
|
58901
59100
|
}
|
|
58902
59101
|
function composeBody(channel, contactId, text3, businessName, emailTo) {
|
|
58903
59102
|
return channel === "SMS" ? { type: "SMS", contactId, message: text3 } : { type: "Email", contactId, subject: `Re: your message to ${businessName}`, html: `<p style="margin:0px;">${escHtml2(text3)}</p>`, ...emailTo ? { emailTo } : {} };
|
|
58904
59103
|
}
|
|
58905
|
-
function productionPath(deps,
|
|
58906
|
-
return
|
|
59104
|
+
function productionPath(deps, path67) {
|
|
59105
|
+
return path67 === "cockpit" && deps.liveSendEnabled?.() === true && deps.outOfBandReady?.() === true;
|
|
58907
59106
|
}
|
|
58908
59107
|
function sentNote(x) {
|
|
58909
59108
|
const who2 = x.path === "sandbox" ? "from the sandbox send command" : `approved by ${x.approvedBy ?? "the owner"} in the Command OS cockpit`;
|
|
@@ -59932,7 +60131,7 @@ function json2(res, status, body) {
|
|
|
59932
60131
|
res.end(JSON.stringify(body));
|
|
59933
60132
|
}
|
|
59934
60133
|
function readJson(req, limit) {
|
|
59935
|
-
return new Promise((
|
|
60134
|
+
return new Promise((resolve15, reject) => {
|
|
59936
60135
|
let size2 = 0;
|
|
59937
60136
|
const chunks = [];
|
|
59938
60137
|
req.on("data", (c) => {
|
|
@@ -59944,7 +60143,7 @@ function readJson(req, limit) {
|
|
|
59944
60143
|
});
|
|
59945
60144
|
req.on("end", () => {
|
|
59946
60145
|
try {
|
|
59947
|
-
|
|
60146
|
+
resolve15(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
59948
60147
|
} catch {
|
|
59949
60148
|
reject(new Error("not json"));
|
|
59950
60149
|
}
|
|
@@ -59955,24 +60154,24 @@ function readJson(req, limit) {
|
|
|
59955
60154
|
function createBrainRoutes(opts) {
|
|
59956
60155
|
return (req, res) => {
|
|
59957
60156
|
const u = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
59958
|
-
const
|
|
59959
|
-
if (
|
|
60157
|
+
const path67 = u.pathname;
|
|
60158
|
+
if (path67 !== "/brain" && path67 !== "/brain/handoff" && path67 !== MASTER_PAGE_PATH && !path67.startsWith("/api/brain/")) return false;
|
|
59960
60159
|
if (opts.plan !== "command-os") {
|
|
59961
60160
|
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" });
|
|
59962
60161
|
res.end(opts.upgradeMessage.trim());
|
|
59963
60162
|
return true;
|
|
59964
60163
|
}
|
|
59965
60164
|
const onBoard = (id2) => opts.accounts().find((a) => a.locationId === id2);
|
|
59966
|
-
if (req.method === "GET" &&
|
|
60165
|
+
if (req.method === "GET" && path67 === "/brain/handoff") {
|
|
59967
60166
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
59968
60167
|
res.end(brainHandoffPageHtml(opts.seatFor(req), opts.seatHeader));
|
|
59969
60168
|
return true;
|
|
59970
60169
|
}
|
|
59971
|
-
if (req.method === "GET" &&
|
|
60170
|
+
if (req.method === "GET" && path67 === "/api/brain/handoff") {
|
|
59972
60171
|
void handoffView(opts.handoffDeps()).then((v) => json2(res, 200, v)).catch(() => json2(res, 200, { ok: false, error: "Your brain could not be read. Check your internet connection and the Brain card in Connections, then Refresh." }));
|
|
59973
60172
|
return true;
|
|
59974
60173
|
}
|
|
59975
|
-
if (req.method === "POST" &&
|
|
60174
|
+
if (req.method === "POST" && path67 === "/api/brain/export") {
|
|
59976
60175
|
void exportBundle(opts.handoffDeps()).then((r) => {
|
|
59977
60176
|
if (!r.ok) {
|
|
59978
60177
|
json2(res, 200, r);
|
|
@@ -59985,7 +60184,7 @@ function createBrainRoutes(opts) {
|
|
|
59985
60184
|
});
|
|
59986
60185
|
return true;
|
|
59987
60186
|
}
|
|
59988
|
-
if (req.method === "POST" &&
|
|
60187
|
+
if (req.method === "POST" && path67 === "/api/brain/pause") {
|
|
59989
60188
|
void readJson(req, 2e3).then(async (body) => {
|
|
59990
60189
|
const parsed2 = PauseRequest.safeParse(body);
|
|
59991
60190
|
if (!parsed2.success) {
|
|
@@ -59998,13 +60197,13 @@ function createBrainRoutes(opts) {
|
|
|
59998
60197
|
});
|
|
59999
60198
|
return true;
|
|
60000
60199
|
}
|
|
60001
|
-
if (
|
|
60200
|
+
if (path67 === MASTER_PAGE_PATH || path67 === MASTER_API_PATH) {
|
|
60002
60201
|
if (req.method !== "GET") {
|
|
60003
60202
|
res.writeHead(405, { "Content-Type": "application/json", "Cache-Control": "no-store", Allow: "GET" });
|
|
60004
60203
|
res.end(JSON.stringify({ ok: false, error: "This page is read only." }));
|
|
60005
60204
|
return true;
|
|
60006
60205
|
}
|
|
60007
|
-
if (
|
|
60206
|
+
if (path67 === MASTER_PAGE_PATH) {
|
|
60008
60207
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
60009
60208
|
res.end(brainMasterPageHtml(opts.seatFor(req), opts.seatHeader));
|
|
60010
60209
|
return true;
|
|
@@ -60017,14 +60216,14 @@ function createBrainRoutes(opts) {
|
|
|
60017
60216
|
void Promise.resolve().then(() => masterView(md())).then((v) => json2(res, 200, v)).catch(() => json2(res, 200, { ok: false, error: "The brain could not be read. Check the Brain card in Connections, then Refresh." }));
|
|
60018
60217
|
return true;
|
|
60019
60218
|
}
|
|
60020
|
-
if (req.method === "GET" &&
|
|
60219
|
+
if (req.method === "GET" && path67 === "/brain") {
|
|
60021
60220
|
const accounts = [...opts.accounts()].sort((a, b) => a.name.localeCompare(b.name));
|
|
60022
|
-
const client = onBoard(u.searchParams.get("client") ?? "");
|
|
60221
|
+
const client = onBoard(u.searchParams.get("client") ?? "") ?? (accounts.length === 1 && !u.searchParams.get("client") ? onBoard(accounts[0].locationId) : void 0);
|
|
60023
60222
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
60024
60223
|
res.end(brainApprovePageHtml({ accounts, client }, opts.seatFor(req), opts.seatHeader));
|
|
60025
60224
|
return true;
|
|
60026
60225
|
}
|
|
60027
|
-
if (req.method === "GET" &&
|
|
60226
|
+
if (req.method === "GET" && path67 === BRAIN_PENDING_PATH) {
|
|
60028
60227
|
const client = onBoard(u.searchParams.get("client") ?? "");
|
|
60029
60228
|
if (!client) {
|
|
60030
60229
|
json2(res, 200, { ok: false, error: "That account is not on the board." });
|
|
@@ -60033,7 +60232,7 @@ function createBrainRoutes(opts) {
|
|
|
60033
60232
|
void listPending(opts.deps(), client.locationId).then((view2) => json2(res, 200, view2)).catch(() => json2(res, 200, { ok: false, error: "The brain could not be read. Check the Brain card in Connections, then Refresh." }));
|
|
60034
60233
|
return true;
|
|
60035
60234
|
}
|
|
60036
|
-
if (req.method === "GET" &&
|
|
60235
|
+
if (req.method === "GET" && path67 === BRAIN_SEND_LOG_PATH) {
|
|
60037
60236
|
const client = onBoard(u.searchParams.get("client") ?? "");
|
|
60038
60237
|
if (!client) {
|
|
60039
60238
|
json2(res, 200, { ok: false, error: "That account is not on the board." });
|
|
@@ -60043,7 +60242,7 @@ function createBrainRoutes(opts) {
|
|
|
60043
60242
|
json2(res, 200, rows2 === "unconfirmed" ? { ok: false, error: "The send log on this computer (brain-send-log.jsonl in the Command OS app-data folder) could not be read. Nothing can be sent until it is repaired or moved." } : { ok: true, client: client.name, rows: rows2 });
|
|
60044
60243
|
return true;
|
|
60045
60244
|
}
|
|
60046
|
-
if (req.method === "POST" && (
|
|
60245
|
+
if (req.method === "POST" && (path67 === "/api/brain/decide" || path67 === "/api/brain/send" || path67 === "/api/brain/send-mode" || path67 === "/api/brain/confirm-mappings")) {
|
|
60047
60246
|
void readJson(req, 2e4).then(async (body) => {
|
|
60048
60247
|
try {
|
|
60049
60248
|
await answer2(body);
|
|
@@ -60054,7 +60253,7 @@ function createBrainRoutes(opts) {
|
|
|
60054
60253
|
if (!res.headersSent) json2(res, 400, { ok: false, wrote: "nothing", error: "Send a valid request." });
|
|
60055
60254
|
});
|
|
60056
60255
|
const answer2 = async (body) => {
|
|
60057
|
-
if (
|
|
60256
|
+
if (path67 === "/api/brain/decide") {
|
|
60058
60257
|
const parsed3 = DecideRequest.safeParse(body);
|
|
60059
60258
|
if (!parsed3.success) {
|
|
60060
60259
|
json2(res, 400, { ok: false, wrote: "nothing", error: "That request is not a decision this screen makes." });
|
|
@@ -60067,7 +60266,7 @@ function createBrainRoutes(opts) {
|
|
|
60067
60266
|
json2(res, 200, parsed3.data.preview ? await previewDecision(opts.deps(), parsed3.data) : await decide(opts.deps(), parsed3.data));
|
|
60068
60267
|
return;
|
|
60069
60268
|
}
|
|
60070
|
-
if (
|
|
60269
|
+
if (path67 === "/api/brain/confirm-mappings") {
|
|
60071
60270
|
const parsed3 = ConfirmMappingsRequest.safeParse(body);
|
|
60072
60271
|
if (!parsed3.success) {
|
|
60073
60272
|
json2(res, 400, { ok: false, wrote: "nothing", error: "That request is not a confirmation this screen makes." });
|
|
@@ -60080,7 +60279,7 @@ function createBrainRoutes(opts) {
|
|
|
60080
60279
|
json2(res, 200, await confirmMappings(opts.deps(), parsed3.data));
|
|
60081
60280
|
return;
|
|
60082
60281
|
}
|
|
60083
|
-
if (
|
|
60282
|
+
if (path67 === "/api/brain/send-mode") {
|
|
60084
60283
|
const parsed3 = SendModeRequest.safeParse(body);
|
|
60085
60284
|
if (!parsed3.success) {
|
|
60086
60285
|
json2(res, 400, { ok: false, wrote: "nothing", error: "That request is not a send-mode change this screen makes." });
|
|
@@ -60821,11 +61020,11 @@ function defaultHasSavedSetup() {
|
|
|
60821
61020
|
return false;
|
|
60822
61021
|
}
|
|
60823
61022
|
}
|
|
60824
|
-
var
|
|
61023
|
+
var import_node_child_process5, import_node_fs14, GHL_SERVER_NAME, CLAUDE_CODE_ADD_ARGS, CLAUDE_CODE_INSTALL_LINE, CLAUDE_CODE_ADD_HELP, OURS, defaultRunner;
|
|
60825
61024
|
var init_claude_code_registration = __esm({
|
|
60826
61025
|
"src/claude-code-registration.ts"() {
|
|
60827
61026
|
"use strict";
|
|
60828
|
-
|
|
61027
|
+
import_node_child_process5 = require("node:child_process");
|
|
60829
61028
|
init_claude_locate();
|
|
60830
61029
|
init_running_channel();
|
|
60831
61030
|
import_node_fs14 = require("node:fs");
|
|
@@ -60836,7 +61035,7 @@ var init_claude_code_registration = __esm({
|
|
|
60836
61035
|
CLAUDE_CODE_INSTALL_LINE = "npx -y @elitedcs/ghl-mcp@latest cli install --claude-code";
|
|
60837
61036
|
CLAUDE_CODE_ADD_HELP = claudeCodeAddHelp("latest");
|
|
60838
61037
|
OURS = /ghl-mcp|start-mcp\.sh|dist[\\/]index\.js/i;
|
|
60839
|
-
defaultRunner = (command, args, env2, timeoutMs) => new Promise((
|
|
61038
|
+
defaultRunner = (command, args, env2, timeoutMs) => new Promise((resolve15) => {
|
|
60840
61039
|
let stdout = "";
|
|
60841
61040
|
let stderr = "";
|
|
60842
61041
|
let settled = false;
|
|
@@ -60845,12 +61044,12 @@ var init_claude_code_registration = __esm({
|
|
|
60845
61044
|
if (!settled) {
|
|
60846
61045
|
settled = true;
|
|
60847
61046
|
if (timer) clearTimeout(timer);
|
|
60848
|
-
|
|
61047
|
+
resolve15(r);
|
|
60849
61048
|
}
|
|
60850
61049
|
};
|
|
60851
61050
|
let child;
|
|
60852
61051
|
try {
|
|
60853
|
-
child = (0,
|
|
61052
|
+
child = (0, import_node_child_process5.spawn)(command, args, { stdio: ["ignore", "pipe", "pipe"], env: env2 });
|
|
60854
61053
|
} catch {
|
|
60855
61054
|
done({ code: null, stdout, stderr, failed: true });
|
|
60856
61055
|
return;
|
|
@@ -61099,7 +61298,7 @@ function startDesignRun(request2, opts = {}) {
|
|
|
61099
61298
|
const job = { id: id2, status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), outDir, request: request2, tail: [] };
|
|
61100
61299
|
jobs.set(id2, job);
|
|
61101
61300
|
const entry = opts.entry ?? process.argv[1];
|
|
61102
|
-
const spawner = opts.spawnFn ??
|
|
61301
|
+
const spawner = opts.spawnFn ?? import_node_child_process6.spawn;
|
|
61103
61302
|
const child = spawner(process.execPath, [entry, ...argvFor(request2, outDir)], {
|
|
61104
61303
|
cwd: outDir,
|
|
61105
61304
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -61160,11 +61359,11 @@ function cancelDesignRun(id2) {
|
|
|
61160
61359
|
}
|
|
61161
61360
|
return true;
|
|
61162
61361
|
}
|
|
61163
|
-
var
|
|
61362
|
+
var import_node_child_process6, import_node_fs15, import_node_path13, import_node_crypto29, SLUG2, MODEL, JOINED_FLAG, jobs, children;
|
|
61164
61363
|
var init_run_jobs = __esm({
|
|
61165
61364
|
"src/command-os/design/run-jobs.ts"() {
|
|
61166
61365
|
"use strict";
|
|
61167
|
-
|
|
61366
|
+
import_node_child_process6 = require("node:child_process");
|
|
61168
61367
|
import_node_fs15 = require("node:fs");
|
|
61169
61368
|
import_node_path13 = require("node:path");
|
|
61170
61369
|
import_node_crypto29 = require("node:crypto");
|
|
@@ -61252,14 +61451,14 @@ var init_run_cost = __esm({
|
|
|
61252
61451
|
// src/claude-call.ts
|
|
61253
61452
|
function callClaudeRecorded(bin, argv, context2, onCost = () => {
|
|
61254
61453
|
}) {
|
|
61255
|
-
return new Promise((
|
|
61454
|
+
return new Promise((resolve15, reject) => {
|
|
61256
61455
|
const run = new RunTexts();
|
|
61257
61456
|
let buffer = "";
|
|
61258
61457
|
let stderr = "";
|
|
61259
61458
|
let facts = {};
|
|
61260
61459
|
let settled = false;
|
|
61261
61460
|
const cmd = claudeCommand(bin, [...argv, "--output-format", "stream-json", "--verbose", ...NO_MEMBER_HOOKS]);
|
|
61262
|
-
const child = (0,
|
|
61461
|
+
const child = (0, import_node_child_process7.spawn)(cmd.command, cmd.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
61263
61462
|
const timer = setTimeout(() => child.kill("SIGKILL"), 6 * 60 * 1e3);
|
|
61264
61463
|
const finish3 = (ok) => {
|
|
61265
61464
|
if (settled) return;
|
|
@@ -61271,7 +61470,7 @@ function callClaudeRecorded(bin, argv, context2, onCost = () => {
|
|
|
61271
61470
|
onCost("The cost record could not be saved.");
|
|
61272
61471
|
}
|
|
61273
61472
|
onCost(`${context2.kind}: ${facts.costUsd === void 0 ? "cost not reported" : formatUsd(facts.costUsd)}.`);
|
|
61274
|
-
if (ok)
|
|
61473
|
+
if (ok) resolve15(run.pick());
|
|
61275
61474
|
else reject(new Error("Check your Claude authentication in Your agency before reading this source."));
|
|
61276
61475
|
};
|
|
61277
61476
|
const consume = (line4) => {
|
|
@@ -61300,11 +61499,11 @@ function callClaudeRecorded(bin, argv, context2, onCost = () => {
|
|
|
61300
61499
|
});
|
|
61301
61500
|
});
|
|
61302
61501
|
}
|
|
61303
|
-
var
|
|
61502
|
+
var import_node_child_process7;
|
|
61304
61503
|
var init_claude_call = __esm({
|
|
61305
61504
|
"src/claude-call.ts"() {
|
|
61306
61505
|
"use strict";
|
|
61307
|
-
|
|
61506
|
+
import_node_child_process7 = require("node:child_process");
|
|
61308
61507
|
init_run_cost();
|
|
61309
61508
|
init_claude_cli();
|
|
61310
61509
|
init_claude_locate();
|
|
@@ -61434,9 +61633,10 @@ function calendarStaff(h, facts, planned) {
|
|
|
61434
61633
|
}
|
|
61435
61634
|
return { done: true, teams };
|
|
61436
61635
|
}
|
|
61437
|
-
function withBuiltCalendarTeams(facts, saved) {
|
|
61438
|
-
const built =
|
|
61439
|
-
|
|
61636
|
+
function withBuiltCalendarTeams(facts, saved, journal2 = {}) {
|
|
61637
|
+
const built = { ...journal2 };
|
|
61638
|
+
for (const [name2, ids] of Object.entries(saved?.calendarTeamsBuilt ?? {})) built[name2] = [.../* @__PURE__ */ new Set([...built[name2] ?? [], ...ids])];
|
|
61639
|
+
return Object.keys(built).length ? { ...facts, calendarTeamsBuilt: built } : facts;
|
|
61440
61640
|
}
|
|
61441
61641
|
function mergeCalendarTeamsBuilt(kept, built) {
|
|
61442
61642
|
return { ...kept ?? {}, ...Object.fromEntries(built.filter((b) => b.type === "calendar" && b.teamIds?.length).map((b) => [b.name.trim().toLowerCase(), [...b.teamIds]])) };
|
|
@@ -61571,6 +61771,22 @@ function phoneInfo(facts) {
|
|
|
61571
61771
|
if (phones.some((p) => digits(p) === digits(biz))) return void 0;
|
|
61572
61772
|
return `Your texts come from ${phones[0]}; your business line is ${biz}. That is fine if the business line is the clinic's own number.`;
|
|
61573
61773
|
}
|
|
61774
|
+
function fitHandoff(h, all, facts) {
|
|
61775
|
+
let out = h;
|
|
61776
|
+
const booking = facts?.customValues?.find((v) => v.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_") === "booking_link")?.value;
|
|
61777
|
+
if (typeof booking === "string" && booking.trim() && out.instruction && /booking_link custom value/i.test(out.instruction)) {
|
|
61778
|
+
out = { ...out, instruction: out.instruction.replace(/\s*[^.]*\bbooking_link custom value\.?/i, "").trim() };
|
|
61779
|
+
}
|
|
61780
|
+
if (out.ref === "handoff.business_details" && all.some((x) => x?.ref === "handoff.phone_number") && /business_phone/.test(out.instruction ?? "")) {
|
|
61781
|
+
out = {
|
|
61782
|
+
...out,
|
|
61783
|
+
title: "Confirm the name messages are signed with",
|
|
61784
|
+
instruction: "Settings > Custom Values: set owner_first_name to the first name every email is signed with. The build pre-fills it from the intake when it can. The business phone is the phone number job above.",
|
|
61785
|
+
successCheck: "owner_first_name shows a real value, not a blank"
|
|
61786
|
+
};
|
|
61787
|
+
}
|
|
61788
|
+
return out;
|
|
61789
|
+
}
|
|
61574
61790
|
function catalogJobs(input) {
|
|
61575
61791
|
const out = [];
|
|
61576
61792
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -61583,11 +61799,12 @@ function catalogJobs(input) {
|
|
|
61583
61799
|
for (const h of input.handoffs) {
|
|
61584
61800
|
if (!h || typeof h.ref !== "string") continue;
|
|
61585
61801
|
const plannedCalendars = input.plannedCalendars ?? /* @__PURE__ */ new Map();
|
|
61586
|
-
const
|
|
61802
|
+
const fit = fitHandoff(h, input.handoffs, input.facts);
|
|
61803
|
+
const state2 = handoffInAccount(fit, input.facts ?? {}, input.readback ?? [], input.plannedUsers ?? [], openRecipientJobs, plannedCalendars);
|
|
61587
61804
|
const text3 = handoffJobText(h);
|
|
61588
61805
|
if (!(state2.readable && state2.done)) {
|
|
61589
|
-
const why = (state2.readable || h.ref === ASSIGN_CALENDAR_STAFF) && input.facts ? handoffReadReason(
|
|
61590
|
-
push({ id: h.ref, text: handoffShownText(
|
|
61806
|
+
const why = (state2.readable || h.ref === ASSIGN_CALENDAR_STAFF) && input.facts ? handoffReadReason(fit, input.facts, plannedCalendars) : void 0;
|
|
61807
|
+
push({ id: h.ref, text: handoffShownText(fit, input.facts), timing: jobTiming(text3), selfReport: !state2.readable, key: requirementsKey([text3]), ...why ? { why } : {} });
|
|
61591
61808
|
}
|
|
61592
61809
|
const part = HANDOFF_PARTS[h.ref];
|
|
61593
61810
|
if (part) {
|
|
@@ -62609,6 +62826,7 @@ function evalCondition(cond, ctx) {
|
|
|
62609
62826
|
const [, pathExpr, op, rawRhs] = cmp;
|
|
62610
62827
|
let lhs = getPath(ctx.brief, pathExpr);
|
|
62611
62828
|
if (pathExpr === "goal.bookingNeeded") lhs = ctx.bookingWanted;
|
|
62829
|
+
if (pathExpr === "channels.sms") lhs = smsOn(ctx.brief);
|
|
62612
62830
|
const rhs = rawRhs.trim().replace(/^'(.*)'$/, "$1").replace(/^"(.*)"$/, "$1");
|
|
62613
62831
|
let equal;
|
|
62614
62832
|
if (rhs === "true" || rhs === "false") equal = lhs === (rhs === "true");
|
|
@@ -63478,7 +63696,7 @@ function planFromPreset(input) {
|
|
|
63478
63696
|
const skTemplates = sk.templates ?? {};
|
|
63479
63697
|
const emailTemplates = keptByCondition(skTemplates.emails, ctx).map((t) => fillNode(t, ctx));
|
|
63480
63698
|
const gsm7 = typeof p.conventions?.gsm7 === "string";
|
|
63481
|
-
const smsTemplates = keptByCondition(skTemplates.sms, ctx).map((t) => {
|
|
63699
|
+
const smsTemplates = (smsOn(input.brief) ? keptByCondition(skTemplates.sms, ctx) : []).map((t) => {
|
|
63482
63700
|
const filled2 = fillNode(t, ctx);
|
|
63483
63701
|
if (gsm7 && typeof filled2.body === "string") filled2.body = toGsm7(filled2.body);
|
|
63484
63702
|
return filled2;
|
|
@@ -63890,6 +64108,7 @@ var init_plan_from_preset = __esm({
|
|
|
63890
64108
|
init_sms_rules();
|
|
63891
64109
|
init_business_hours();
|
|
63892
64110
|
init_plan_guide();
|
|
64111
|
+
init_texting();
|
|
63893
64112
|
init_claims_guard();
|
|
63894
64113
|
PRESET_TOKEN_ROOTS = /* @__PURE__ */ new Set(["business", "offer", "audience", "voice", "goal", "proof"]);
|
|
63895
64114
|
GSM7_REPLACEMENTS = [
|
|
@@ -64272,7 +64491,7 @@ function unitLooksLike(unit) {
|
|
|
64272
64491
|
function gateExtraction(transcript, proposed, questions) {
|
|
64273
64492
|
const knownIds = questions;
|
|
64274
64493
|
const accepted = [];
|
|
64275
|
-
const
|
|
64494
|
+
const held2 = [];
|
|
64276
64495
|
const rejected = [];
|
|
64277
64496
|
const patch = {};
|
|
64278
64497
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -64309,10 +64528,10 @@ function gateExtraction(transcript, proposed, questions) {
|
|
|
64309
64528
|
accepted.push(item);
|
|
64310
64529
|
patch[item.questionId] = item.value;
|
|
64311
64530
|
} else {
|
|
64312
|
-
|
|
64531
|
+
held2.push({ ...item, because: cite.because });
|
|
64313
64532
|
}
|
|
64314
64533
|
}
|
|
64315
|
-
return { accepted, held, rejected, patch };
|
|
64534
|
+
return { accepted, held: held2, rejected, patch };
|
|
64316
64535
|
}
|
|
64317
64536
|
function mergeAccepted(ans, result) {
|
|
64318
64537
|
return { ...ans, ...result.patch };
|
|
@@ -64641,10 +64860,10 @@ async function prefillFromSource(spawnClaude, labels, source, toolContext = { se
|
|
|
64641
64860
|
const byLabel = new Map(questions.map((q3) => [q3.label, q3]));
|
|
64642
64861
|
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, toolContext)), labels, source.kind === "text" ? source.text : void 0, byLabel);
|
|
64643
64862
|
result.answers = validatePrefillAnswers(questions, result.answers);
|
|
64644
|
-
for (const [label2,
|
|
64645
|
-
const checked = validatePrefillAnswers(questions, { [label2]:
|
|
64863
|
+
for (const [label2, held2] of Object.entries(result.held)) {
|
|
64864
|
+
const checked = validatePrefillAnswers(questions, { [label2]: held2.value });
|
|
64646
64865
|
if (!(label2 in checked)) delete result.held[label2];
|
|
64647
|
-
else
|
|
64866
|
+
else held2.value = checked[label2];
|
|
64648
64867
|
}
|
|
64649
64868
|
result.unverified = result.unverified.filter((label2) => label2 in result.answers);
|
|
64650
64869
|
result.unknown = labels.filter((label2) => !(label2 in result.answers) && !(label2 in result.held));
|
|
@@ -64738,7 +64957,7 @@ async function runPrefill(call2, labels, material, questions = /* @__PURE__ */ n
|
|
|
64738
64957
|
if (json3 === void 0) return fail("the model did not return a result line");
|
|
64739
64958
|
const parsed2 = JSON.parse(json3);
|
|
64740
64959
|
const answers = {};
|
|
64741
|
-
const
|
|
64960
|
+
const held2 = {};
|
|
64742
64961
|
const unverified = [];
|
|
64743
64962
|
for (const [k, raw] of Object.entries(parsed2.answers ?? {})) {
|
|
64744
64963
|
if (!labels.includes(k)) continue;
|
|
@@ -64756,10 +64975,10 @@ async function runPrefill(call2, labels, material, questions = /* @__PURE__ */ n
|
|
|
64756
64975
|
const checks = (Array.isArray(value) ? value : [value]).map((v) => intakeCitation(material, v, quote2, basis, q3 ? { label: k, options: q3.options } : void 0));
|
|
64757
64976
|
const cite = checks.find((c) => c.verdict === "refused") ?? checks.find((c) => c.verdict === "held") ?? checks[0];
|
|
64758
64977
|
if (cite.verdict === "ok") answers[k] = value;
|
|
64759
|
-
else if (cite.verdict === "held")
|
|
64978
|
+
else if (cite.verdict === "held") held2[k] = { value, because: cite.because, ...cite.claims ? { claims: true } : {} };
|
|
64760
64979
|
}
|
|
64761
|
-
const unknown = labels.filter((l) => !(l in answers) && !(l in
|
|
64762
|
-
return { ok: true, answers, unknown, held, unverified };
|
|
64980
|
+
const unknown = labels.filter((l) => !(l in answers) && !(l in held2));
|
|
64981
|
+
return { ok: true, answers, unknown, held: held2, unverified };
|
|
64763
64982
|
} catch (e) {
|
|
64764
64983
|
return fail(e instanceof Error ? e.message : String(e));
|
|
64765
64984
|
}
|
|
@@ -65225,7 +65444,7 @@ function bsSummary(n,c,state){const id=c.locationId;const acc=((state.accounts||
|
|
|
65225
65444
|
if(n===6){const sc=(state.staffChecks||{})[id];return sc&&sc.matched&&sc.matched.length?"In the account: "+sc.matched.map(m=>m.name).join(", ")+".":"Staff confirmed."}
|
|
65226
65445
|
if(n===7){const a=(state.buildApproved||{})[id];return a?"Approved "+niceDate(a,true)+".":"Approved."}
|
|
65227
65446
|
/* Run 9: the folded step said only "Built.". It says what was made, from the build's own record. */
|
|
65228
|
-
if(n===8){const
|
|
65447
|
+
if(n===8){const bv0=(((state.buildViews||{})[id])||{});const made=(bv0.made&&bv0.made.length)?bv0.made:(bv0.built||[]).filter(x=>x.status==="created");if(!made.length)return "Built.";
|
|
65229
65448
|
const by={};for(const x of made)by[x.type]=(by[x.type]||0)+1;const KIND={pipeline:"pipeline",stage:"pipeline stage",field:"custom field",tag:"tag",cv:"custom value",calendar:"calendar",form:"form",email_template:"email template",sms_template:"text template",workflow:"draft automation",user:"staff login"};
|
|
65230
65449
|
return "Built in GoHighLevel: "+Object.entries(by).map(([k,v])=>v+" "+plural(v,KIND[k]||k)).join(", ")+". Nothing is switched on."}
|
|
65231
65450
|
if(n===9){const oc=(state.outcomes||{})[id+":4"];const must=bsMustDo(c,state).length;
|
|
@@ -65285,7 +65504,7 @@ function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
|
|
|
65285
65504
|
if(bp&&!bp.confirmed&&!live){bsPause(c,state,bp.preview,box);return}
|
|
65286
65505
|
bsOutcome(c,state,3,box);
|
|
65287
65506
|
/* Run 8 finding 4: it ended on just "Built." The finish says what was made, counted from the build's own record. */
|
|
65288
|
-
if(!live&&(((state.clients||{})[id]||{}).stages||[])[3]==="done"){const
|
|
65507
|
+
if(!live&&(((state.clients||{})[id]||{}).stages||[])[3]==="done"){const bv0=(((state.buildViews||{})[id])||{});const made=(bv0.made&&bv0.made.length)?bv0.made:(bv0.built||[]).filter(x=>x.status==="created");
|
|
65289
65508
|
if(made.length){const by={};for(const x of made)by[x.type]=(by[x.type]||0)+1;const KIND={pipeline:"pipeline",stage:"pipeline stage",field:"custom field",tag:"tag",cv:"custom value",calendar:"calendar",form:"form",email_template:"email template",sms_template:"text template",workflow:"draft automation",user:"staff login"};
|
|
65290
65509
|
box.appendChild(mk("p","","Made in GoHighLevel this build: "+Object.entries(by).map(([k,n])=>n+" "+plural(n,KIND[k]||k)).join(", ")+". Every automation is a draft until you switch it on."))}}if(!live)bsSnippets(c,state,box);bsRun(c,state,run,3,(((state.clients[id]||{}).stages||[])[3]==="done")?"Build again":"Start the build",box);return}
|
|
65291
65510
|
if(n===9){bsOutcome(c,state,4,box);bsRun(c,state,run,4,(state.outcomes||{})[id+":4"]?"Run the check again":"Run the check",box);if((state.outcomes||{})[id+":4"])bsRebuild(c,state,run,box);return}
|
|
@@ -65620,7 +65839,14 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
|
|
|
65620
65839
|
if(view.approvedAt)d.appendChild(mk("p","","Building approved on "+niceDate(view.approvedAt,true)+"."));page.appendChild(d)}}
|
|
65621
65840
|
/* The step that just opened comes into view, once per change (and on first open), after the redraw keeps its scroll. */
|
|
65622
65841
|
if(bsSeen[id]!==pr.current){bsSeen[id]=pr.current;if(pr.current>0)setTimeout(()=>{const el=document.getElementById("step-"+(pr.current+1));if(el)try{el.scrollIntoView({block:"start",behavior:"smooth"})}catch(e){el.scrollIntoView()}},0)}
|
|
65623
|
-
if(pr.current<0)page.appendChild(mk("p","cds-callout info","All "+n+" steps are done. The build is signed off. Switching the client on is yours to do in GoHighLevel: see step 12's list."));
|
|
65842
|
+
if(pr.current<0){page.appendChild(mk("p","cds-callout info","All "+n+" steps are done. The build is signed off. Switching the client on is yours to do in GoHighLevel: see step 12's list."));
|
|
65843
|
+
/* Walk W26 (2026-10-29): after sign-off nothing could start the build again, and the page still said Built after its
|
|
65844
|
+
objects were deleted in GoHighLevel. The check can always run again; when it finds built objects gone, it says how
|
|
65845
|
+
many and Build again is offered beside it. */
|
|
65846
|
+
const after=mk("div","bs-after-signoff");const gone=((state.builtMissing||{})[id])||0;
|
|
65847
|
+
if(gone)after.appendChild(mk("p","cds-callout warn bs-gone","The last check found "+gone+" "+plural(gone,"thing")+" this build made missing from "+c.name+" in GoHighLevel. Build again to put "+(gone===1?"it":"them")+" back, then run the check."));
|
|
65848
|
+
else after.appendChild(mk("p","muted","Changed something in GoHighLevel since? Check the account again to see it as it is now."));
|
|
65849
|
+
bsRun(c,state,run,4,"Check the account again",after,!gone);if(gone)bsRebuild(c,state,run,after);page.appendChild(after)}
|
|
65624
65850
|
return page}
|
|
65625
65851
|
`;
|
|
65626
65852
|
BS_STAFF_WHY = "Everyone on your team needs their own login in this client's GoHighLevel account. The build only gives appointments, alerts and tasks to people who have one, so each person below must be in the account before the build runs.";
|
|
@@ -65740,7 +65966,7 @@ ${cockpitHeaderHtml("agency")}
|
|
|
65740
65966
|
<h1 class="cds-page-title">Your agency's own details, set once and used on every client</h1>
|
|
65741
65967
|
<p class="cds-page-sub">Filled in once. Reused on every client document, every proposal, and every demo page, so nothing has to ask you again.</p>
|
|
65742
65968
|
</div>
|
|
65743
|
-
<div class="ap-save"><span id="msgTop" role="status"></span><button type="submit" form="f" class="cds-btn">Save your agency details</button></div>
|
|
65969
|
+
<div class="ap-save"><span id="msgTop" role="status"></span><button type="submit" form="f" class="cds-btn" data-msg="msgTop">Save your agency details</button></div>
|
|
65744
65970
|
</div>
|
|
65745
65971
|
${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an agency name to use this on client documents.</span>Everything else can wait. The name is what every report and proposal carries.</div>
|
|
65746
65972
|
`}
|
|
@@ -65755,7 +65981,7 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
|
|
|
65755
65981
|
<div class="cds-field"><label for="accent">Brand color</label><input type="color" class="cds-input" id="accent" name="accent" value="${esc14(p.accent || "#b8934a")}"></div>
|
|
65756
65982
|
</div>
|
|
65757
65983
|
${input("logoUrl", "Logo URL", p.logoUrl, { placeholder: "https://youragency.com/logo.svg", hint: "Must start with https. A logo link that isn't https is refused, because this one goes on pages your prospects open." })}
|
|
65758
|
-
${input("logoFile", "Logo file on this computer (for client reports)", p.logoFile, { placeholder: "/Users/you/Documents/logo.png", hint: "Optional. The full path to a PNG, JPEG, WebP, GIF or SVG
|
|
65984
|
+
${input("logoFile", "Logo file on this computer (for client reports)", p.logoFile, { placeholder: "/Users/you/Documents/logo.png", hint: "Optional. The full path to a PNG, JPEG, WebP, GIF or SVG. Any size: we make a small copy for reports, and your file is not changed. Reports you hand clients carry it inside the page, so it prints and travels with a saved copy." })}
|
|
65759
65985
|
${input("bookingUrl", "Booking link", p.bookingUrl, { placeholder: "https://your-calendar-link", hint: "Your own calendar link, usually a GoHighLevel booking page. The Book the walkthrough button on a report you send a prospect opens it. Must start with https." })}
|
|
65760
65986
|
</section>
|
|
65761
65987
|
<section class="cds-panel">
|
|
@@ -65855,6 +66081,9 @@ ${SERVICE_IDS.map((id2) => ` <label class="cds-check"><input type="checkbox"
|
|
|
65855
66081
|
</ul>
|
|
65856
66082
|
</section>
|
|
65857
66083
|
|
|
66084
|
+
<!-- Jerry's rule (walk 2026-10-29): a long form's action sits at the bottom as well as the top; one submit, one result
|
|
66085
|
+
line, beside the Save that was pressed. -->
|
|
66086
|
+
<div class="ap-save ap-save-bottom"><button type="submit" class="cds-btn" data-msg="msgBottom">Save your agency details</button><span id="msgBottom" role="status"></span></div>
|
|
65858
66087
|
</form>
|
|
65859
66088
|
|
|
65860
66089
|
<div class="cds-grid two ap-start">
|
|
@@ -66003,6 +66232,9 @@ function settlePackages(profile,read){
|
|
|
66003
66232
|
const v=id=>document.getElementById(id).value.trim();
|
|
66004
66233
|
const lines=id=>v(id).split("\\n").map(s=>s.trim()).filter(Boolean);
|
|
66005
66234
|
document.getElementById("f").addEventListener("submit",async e=>{
|
|
66235
|
+
/* The one result line is beside the Save that was pressed; the other is cleared. */
|
|
66236
|
+
const msgId=(e.submitter&&e.submitter.getAttribute("data-msg"))||"msgTop";
|
|
66237
|
+
for(const id of ["msgTop","msgBottom"])if(id!==msgId){const x=document.getElementById(id);if(x)x.textContent=""}
|
|
66006
66238
|
e.preventDefault();
|
|
66007
66239
|
const offers=lines("offers").map(l=>{
|
|
66008
66240
|
const m=l.match(/^(.*?)\\s*[.-]\\s*\\$?([\\d,.]+)\\s*(\\/mo)?$/);
|
|
@@ -66010,7 +66242,7 @@ document.getElementById("f").addEventListener("submit",async e=>{
|
|
|
66010
66242
|
return{name:m[1].trim(),priceFloor:Number(m[2].replace(/,/g,""))||undefined,cadence:m[3]?"monthly":"one-time"};
|
|
66011
66243
|
});
|
|
66012
66244
|
const pkgRead=readPackages();
|
|
66013
|
-
if(pkgRead.clash){document.getElementById(
|
|
66245
|
+
if(pkgRead.clash){document.getElementById(msgId).textContent="Two packages, "+pkgRead.clash[0]+" and "+pkgRead.clash[1]+", would be saved as one. Give one of them a different name. Nothing was saved.";return}
|
|
66014
66246
|
const staff=lines("staff").map(l=>{const [name,role,email]=l.split("\xB7").map(s=>s.trim());return{name,role:role||undefined,email:email||undefined}});
|
|
66015
66247
|
/* EMPTY clears; UNREADABLE stops the save. undefined is the "stop" signal here. */
|
|
66016
66248
|
const num=id=>{const r=readNumber(v(id));return r.clear?null:(r.bad?undefined:r.value)};
|
|
@@ -66030,8 +66262,8 @@ document.getElementById("f").addEventListener("submit",async e=>{
|
|
|
66030
66262
|
vocabulary:lines("icaVocabulary").length?lines("icaVocabulary"):null,hangouts:lines("icaHangouts").length?lines("icaHangouts"):null,villains:lines("icaVillains").length?lines("icaVillains"):null},
|
|
66031
66263
|
offer:{name:v("offerName")||null,promise:v("offerPromise")||null,price:v("offerPrice")||null,guarantee:v("offerGuarantee")||null,
|
|
66032
66264
|
valueStack:lines("offerStack").length?lines("offerStack"):null,accelerators:lines("offerAccelerators").length?lines("offerAccelerators"):null}}};
|
|
66033
|
-
/* Said beside the
|
|
66034
|
-
const m={set textContent(t){document.getElementById(
|
|
66265
|
+
/* Said beside the Save that was pressed. */
|
|
66266
|
+
const m={set textContent(t){document.getElementById(msgId).textContent=t}};
|
|
66035
66267
|
const badNum=["minMonthly","minSetup"].find(id=>body.pricing[id]===undefined);
|
|
66036
66268
|
if(badNum){m.textContent="That is not a number I can read, so nothing was saved. Check the "+(badNum==="minMonthly"?"minimum monthly":"minimum setup fee")+".";return}
|
|
66037
66269
|
m.textContent="Saving\u2026";
|
|
@@ -66703,7 +66935,7 @@ function compute(ans, skip, asm) {
|
|
|
66703
66935
|
add("missed", "Calls you never get back to",
|
|
66704
66936
|
[{ id: "calls_missed" }, { id: "missed_never_return", share: true }], null,
|
|
66705
66937
|
function () { return raw("calls_missed") * share("missed_never_return") * c * v; },
|
|
66706
|
-
"Call answering
|
|
66938
|
+
"Call answering");
|
|
66707
66939
|
|
|
66708
66940
|
add("waited", "Enquiries that waited",
|
|
66709
66941
|
[{ id: "waited_month" }], "response_penalty",
|
|
@@ -69043,11 +69275,11 @@ var init_closed_roots = __esm({
|
|
|
69043
69275
|
READ_RESULT_MAX_CHARS = 16e6;
|
|
69044
69276
|
OVER_BUDGET_KEY = "__secretShopperOverBudget";
|
|
69045
69277
|
overBudget = (v) => !!v && typeof v === "object" && OVER_BUDGET_KEY in v;
|
|
69046
|
-
deadline = (p, ms, what) => new Promise((
|
|
69278
|
+
deadline = (p, ms, what) => new Promise((resolve15, reject) => {
|
|
69047
69279
|
const t = setTimeout(() => reject(new Error(`${what} took longer than ${ms} ms`)), ms);
|
|
69048
69280
|
p.then((v) => {
|
|
69049
69281
|
clearTimeout(t);
|
|
69050
|
-
|
|
69282
|
+
resolve15(v);
|
|
69051
69283
|
}, (e) => {
|
|
69052
69284
|
clearTimeout(t);
|
|
69053
69285
|
reject(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -69364,10 +69596,10 @@ async function browserPass(url, opts = {}) {
|
|
|
69364
69596
|
let timer;
|
|
69365
69597
|
let expiredAt = false;
|
|
69366
69598
|
let closeOnce;
|
|
69367
|
-
const expired2 = new Promise((
|
|
69599
|
+
const expired2 = new Promise((resolve15) => {
|
|
69368
69600
|
timer = setTimeout(() => {
|
|
69369
69601
|
expiredAt = true;
|
|
69370
|
-
|
|
69602
|
+
resolve15({ ok: false, reason: `The rendered pass took longer than ${Math.round(deadlineMs / 1e3)} seconds and was stopped.` });
|
|
69371
69603
|
}, deadlineMs);
|
|
69372
69604
|
});
|
|
69373
69605
|
const work = (async () => {
|
|
@@ -69486,6 +69718,14 @@ function reviewPrompt(input) {
|
|
|
69486
69718
|
"For each gap, state the cost using their own numbers when they gave any, and what we build to close it:",
|
|
69487
69719
|
...GAP_CHECKLIST.map((g) => ` \u2022 ${g}`),
|
|
69488
69720
|
"",
|
|
69721
|
+
...input.promises ? [
|
|
69722
|
+
"WHAT THIS BUILD MAKES. You do not list what we build or how each gap is fixed: the document takes that from this list itself. You write the diagnosis only. Your recommended offer and biggest lever may name a system or automation ONLY if it is on this list:",
|
|
69723
|
+
...input.promises.willBuild.length ? input.promises.willBuild.map((w) => ` \u2022 ${w}`) : [" \u2022 (nothing yet)"],
|
|
69724
|
+
...input.promises.held.length ? ["WAITING ON THE CLIENT'S ANSWER (never say we build these):", ...input.promises.held.map((h) => ` \u2022 ${h}`)] : [],
|
|
69725
|
+
'Never promise that calls are answered live, "day and night", "24/7" or "every call": nothing in this build answers the phone.',
|
|
69726
|
+
...input.promises.textingOn ? [] : ["This client is not texting: never promise a text, an SMS or a text-back anywhere."],
|
|
69727
|
+
""
|
|
69728
|
+
] : [],
|
|
69489
69729
|
"THE CLIENT'S INTAKE ANSWERS:",
|
|
69490
69730
|
answers || "(no answers provided)",
|
|
69491
69731
|
"",
|
|
@@ -69497,7 +69737,7 @@ function reviewPrompt(input) {
|
|
|
69497
69737
|
] : [],
|
|
69498
69738
|
'A gap about deposits or prepayment is never priority "now": deposits are set up later and never hold up sign-off.',
|
|
69499
69739
|
"Reply with ONE line of JSON and nothing else:",
|
|
69500
|
-
'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","
|
|
69740
|
+
'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","priority":"now"}],"unknowns":[""]}'
|
|
69501
69741
|
].join("\n");
|
|
69502
69742
|
}
|
|
69503
69743
|
function accountPrefixes(clientName) {
|
|
@@ -69553,6 +69793,97 @@ function strictRetryPrompt(input) {
|
|
|
69553
69793
|
"No preamble, no explanation, no markdown fences, no closing remarks."
|
|
69554
69794
|
].join("\n");
|
|
69555
69795
|
}
|
|
69796
|
+
function renderedLines(r) {
|
|
69797
|
+
const out = [];
|
|
69798
|
+
const add2 = (where, text3) => {
|
|
69799
|
+
if (typeof text3 === "string" && text3.trim()) out.push({ where, text: text3 });
|
|
69800
|
+
};
|
|
69801
|
+
add2("headline", r.headline);
|
|
69802
|
+
if (r.ica) {
|
|
69803
|
+
add2("ica.who", r.ica.who);
|
|
69804
|
+
add2("ica.buyingTrigger", r.ica.buyingTrigger);
|
|
69805
|
+
add2("ica.whereTheyAre", r.ica.whereTheyAre);
|
|
69806
|
+
(r.ica.painPoints ?? []).forEach((t, i) => add2(`ica.painPoints.${i}`, t));
|
|
69807
|
+
(r.ica.objections ?? []).forEach((t, i) => add2(`ica.objections.${i}`, t));
|
|
69808
|
+
}
|
|
69809
|
+
if (r.offer) for (const k of ["dreamOutcome", "believability", "speed", "effort", "biggestLever", "recommendedOffer"]) add2(`offer.${k}`, r.offer[k]);
|
|
69810
|
+
(r.gaps ?? []).forEach((g, i) => {
|
|
69811
|
+
add2(`gaps.${i}.title`, g.title);
|
|
69812
|
+
add2(`gaps.${i}.cost`, g.cost);
|
|
69813
|
+
});
|
|
69814
|
+
(r.unknowns ?? []).forEach((t, i) => add2(`unknowns.${i}`, t));
|
|
69815
|
+
return out;
|
|
69816
|
+
}
|
|
69817
|
+
function overPromises(r, p) {
|
|
69818
|
+
if (!p || !r.ok) return [];
|
|
69819
|
+
const made = p.willBuild.join(" | ");
|
|
69820
|
+
return renderedLines(r).filter((l) => LIVE_CALLS.test(l.text) || (!p.textingOn || p.held.some((h) => /texting/i.test(h))) && TEXTS.test(l.text) || TOPICS.some((t) => t.re.test(l.text) && !t.built.test(made))).map((l) => l.where);
|
|
69821
|
+
}
|
|
69822
|
+
function graderPrompt(r, p) {
|
|
69823
|
+
return [
|
|
69824
|
+
"You check a client document for promises. A line promises something if it says or implies that we will build, set up, run or deliver it for the client.",
|
|
69825
|
+
"WHAT THIS BUILD MAKES (the only things a line may promise):",
|
|
69826
|
+
...p.willBuild.length ? p.willBuild.map((w) => `- ${w}`) : ["- (nothing)"],
|
|
69827
|
+
...p.held.length ? ["NOT BUILT, WAITING ON THE CLIENT'S ANSWER (a line promising these is a violation):", ...p.held.map((h) => `- ${h}`)] : [],
|
|
69828
|
+
`Texting is ${p.textingOn ? "on" : "OFF: any promise of a text, SMS or text-back is a violation"}. Nothing in this build answers the phone live: any promise of answered calls is a violation.`,
|
|
69829
|
+
"Diagnosis, observations about the client and questions are not promises. Advice clearly marked as for later is not a promise.",
|
|
69830
|
+
"THE LINES (id: text):",
|
|
69831
|
+
...renderedLines(r).map((l) => `${l.where}: ${l.text}`),
|
|
69832
|
+
'Reply with ONE line of JSON and nothing else: RESULT: {"violations":["<id>", ...]}'
|
|
69833
|
+
].join("\n");
|
|
69834
|
+
}
|
|
69835
|
+
function parseGrader(text3, r) {
|
|
69836
|
+
const json3 = extractResultJson(text3);
|
|
69837
|
+
if (json3 === void 0) return null;
|
|
69838
|
+
try {
|
|
69839
|
+
const v = JSON.parse(json3).violations;
|
|
69840
|
+
if (!Array.isArray(v)) return null;
|
|
69841
|
+
const known = new Set(renderedLines(r).map((l) => l.where));
|
|
69842
|
+
return v.map(String).filter((x) => known.has(x));
|
|
69843
|
+
} catch {
|
|
69844
|
+
return null;
|
|
69845
|
+
}
|
|
69846
|
+
}
|
|
69847
|
+
function trimLines(r, bad) {
|
|
69848
|
+
if (!bad.length) return r;
|
|
69849
|
+
const set = new Set(bad);
|
|
69850
|
+
const trimmed = renderedLines(r).filter((l) => set.has(l.where)).map((l) => l.text);
|
|
69851
|
+
const keep = (where) => !set.has(where);
|
|
69852
|
+
const ica = r.ica && {
|
|
69853
|
+
...r.ica,
|
|
69854
|
+
who: keep("ica.who") ? r.ica.who : "",
|
|
69855
|
+
buyingTrigger: keep("ica.buyingTrigger") ? r.ica.buyingTrigger : "",
|
|
69856
|
+
whereTheyAre: keep("ica.whereTheyAre") ? r.ica.whereTheyAre : "",
|
|
69857
|
+
painPoints: (r.ica.painPoints ?? []).filter((_, i) => keep(`ica.painPoints.${i}`)),
|
|
69858
|
+
objections: (r.ica.objections ?? []).filter((_, i) => keep(`ica.objections.${i}`))
|
|
69859
|
+
};
|
|
69860
|
+
const offer = r.offer && Object.fromEntries(Object.entries(r.offer).map(([k, v]) => [k, typeof v === "string" && !keep(`offer.${k}`) ? "" : v]));
|
|
69861
|
+
return {
|
|
69862
|
+
...r,
|
|
69863
|
+
headline: keep("headline") ? r.headline : void 0,
|
|
69864
|
+
...ica ? { ica } : {},
|
|
69865
|
+
...offer ? { offer } : {},
|
|
69866
|
+
gaps: (r.gaps ?? []).filter((_, i) => keep(`gaps.${i}.title`) && keep(`gaps.${i}.cost`)),
|
|
69867
|
+
unknowns: (r.unknowns ?? []).filter((_, i) => keep(`unknowns.${i}`)),
|
|
69868
|
+
trimmed: [...r.trimmed ?? [], ...trimmed]
|
|
69869
|
+
};
|
|
69870
|
+
}
|
|
69871
|
+
async function violations(spawnClaude, r, p) {
|
|
69872
|
+
if (!p || !r.ok) return { ids: [], graded: true };
|
|
69873
|
+
const fast = overPromises(r, p);
|
|
69874
|
+
let found = null;
|
|
69875
|
+
for (let attempt2 = 0; attempt2 < 2 && found === null; attempt2++) {
|
|
69876
|
+
try {
|
|
69877
|
+
found = parseGrader(await spawnClaude(graderPrompt(r, p)), r);
|
|
69878
|
+
} catch {
|
|
69879
|
+
found = null;
|
|
69880
|
+
}
|
|
69881
|
+
}
|
|
69882
|
+
return { ids: [.../* @__PURE__ */ new Set([...fast, ...found ?? []])], graded: found !== null };
|
|
69883
|
+
}
|
|
69884
|
+
function structural(r, p) {
|
|
69885
|
+
return { ...r, build: [], gaps: (r.gaps ?? []).map((g) => ({ ...g, fix: "" })), ...p ? { promises: p } : {} };
|
|
69886
|
+
}
|
|
69556
69887
|
async function generateReview(spawnClaude, input) {
|
|
69557
69888
|
const answered = Object.values(input.answers).filter((v) => v && String(v).trim()).length;
|
|
69558
69889
|
if (answered < 5) {
|
|
@@ -69565,7 +69896,21 @@ async function generateReview(spawnClaude, input) {
|
|
|
69565
69896
|
lastRaw = raw;
|
|
69566
69897
|
const names = [input.clientName, ...Object.entries(input.answers).filter(([k]) => /business[ _]?name/i.test(k)).map(([, v]) => v)].join(" ");
|
|
69567
69898
|
const parsed2 = parseReview(raw, names);
|
|
69568
|
-
if (parsed2.ok)
|
|
69899
|
+
if (parsed2.ok) {
|
|
69900
|
+
const first2 = structural(parsed2, input.promises);
|
|
69901
|
+
const over2 = await violations(spawnClaude, first2, input.promises);
|
|
69902
|
+
if (!over2.ids.length) return { ...first2, graded: over2.graded };
|
|
69903
|
+
try {
|
|
69904
|
+
const again = parseReview(await spawnClaude([reviewPrompt(input), "", `IMPORTANT: your previous answer promised what this build does not make (${over2.ids.join(", ")}). Promise only what is listed under WHAT THIS BUILD MAKES.`].join("\n")), names);
|
|
69905
|
+
if (again.ok) {
|
|
69906
|
+
const second = structural(again, input.promises);
|
|
69907
|
+
const v = await violations(spawnClaude, second, input.promises);
|
|
69908
|
+
return { ...trimLines(second, v.ids), graded: v.graded };
|
|
69909
|
+
}
|
|
69910
|
+
} catch {
|
|
69911
|
+
}
|
|
69912
|
+
return { ...trimLines(first2, over2.ids), graded: over2.graded };
|
|
69913
|
+
}
|
|
69569
69914
|
} catch (e) {
|
|
69570
69915
|
lastRaw = e instanceof Error ? e.message : String(e);
|
|
69571
69916
|
}
|
|
@@ -69576,12 +69921,15 @@ async function generateReview(spawnClaude, input) {
|
|
|
69576
69921
|
raw: lastRaw.slice(-600)
|
|
69577
69922
|
};
|
|
69578
69923
|
}
|
|
69924
|
+
function clientHeldLine(line4) {
|
|
69925
|
+
return line4.replace(/^Waiting for your client's answer: /, "Waiting on your answer: ").replace(/^Not built, your client said not yet: /, "Not set up, as you said not yet: ").replace(/^Not built yet, chosen and coming in a later update: /, "Chosen, and coming in a later update: ");
|
|
69926
|
+
}
|
|
69579
69927
|
function reviewDocumentHtml(review, opts) {
|
|
69580
69928
|
const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
|
|
69581
69929
|
const agency = opts.agencyName?.trim() || "";
|
|
69582
69930
|
const esc18 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
69583
69931
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
|
|
69584
|
-
const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}
|
|
69932
|
+
const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}</div>`;
|
|
69585
69933
|
const o = review.offer;
|
|
69586
69934
|
const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
|
|
69587
69935
|
const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
|
|
@@ -69592,7 +69940,7 @@ function reviewDocumentHtml(review, opts) {
|
|
|
69592
69940
|
const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
|
|
69593
69941
|
const assessmentNext = opts.assessment && purchased ? assessmentFeatureLines(opts.assessment, [...purchased, ...represented]) : [];
|
|
69594
69942
|
const available = unbuilt.map((f) => `<div class="build"><b>${esc18(f.name)}</b><span>${esc18(f.what)}${f.figure ? ` <b>${esc18(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line4) => `<p>${esc18(line4)}</p>`).join("");
|
|
69595
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)}
|
|
69943
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)}: Growth Blueprint</title><style>
|
|
69596
69944
|
@page{margin:0.6in}
|
|
69597
69945
|
*{box-sizing:border-box}
|
|
69598
69946
|
body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
|
|
@@ -69647,7 +69995,7 @@ function reviewDocumentHtml(review, opts) {
|
|
|
69647
69995
|
${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc18(o.biggestLever)}</p>` : ""}
|
|
69648
69996
|
${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc18(o.recommendedOffer)}</div>` : ""}` : ""}
|
|
69649
69997
|
${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
|
|
69650
|
-
${review.
|
|
69998
|
+
${review.promises && (review.promises.willBuild.length || review.promises.held.length) ? `<div class="head">What we're building</div>${review.promises.willBuild.map((w) => `<div class="build"><b>${esc18(w)}</b></div>`).join("")}${review.promises.held.map((h) => `<div class="build"><b>${esc18(clientHeldLine(h))}</b></div>`).join("")}` : ""}
|
|
69651
69999
|
${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
|
|
69652
70000
|
<p>Not confirmed yet. The build waits until the features to build are confirmed.</p>
|
|
69653
70001
|
` : opts.scope ? `<div class="head">What this build includes</div>
|
|
@@ -69663,7 +70011,7 @@ function reviewDocumentHtml(review, opts) {
|
|
|
69663
70011
|
<footer><span>${agency ? esc18(agency) : "Growth Blueprint"}</span><span>${esc18(opts.clientName)} \xB7 ${date}</span></footer>
|
|
69664
70012
|
</div></body></html>`;
|
|
69665
70013
|
}
|
|
69666
|
-
var GAP_CHECKLIST;
|
|
70014
|
+
var GAP_CHECKLIST, LIVE_CALLS, TEXTS, TOPICS;
|
|
69667
70015
|
var init_blueprint_review = __esm({
|
|
69668
70016
|
"src/blueprint-review.ts"() {
|
|
69669
70017
|
"use strict";
|
|
@@ -69683,6 +70031,12 @@ var init_blueprint_review = __esm({
|
|
|
69683
70031
|
"SMS capability (A2P registered) if they want to text",
|
|
69684
70032
|
"Reporting they will actually look at"
|
|
69685
70033
|
];
|
|
70034
|
+
LIVE_CALLS = /(?:\bday\s*(?:and|&)\s*night\b|\b24\s*[\/\-\u2013]\s*7\b|\b24 hours\b|\b(?:a)?round[- ]the[- ]clock\b|\bafter[- ]hours?\b|\bevery (?:single )?(?:call|caller|enquiry|inquiry|ring)s?\b|\banswer(?:s|ed|ing)?\b[^.]{0,40}\b(?:phones?|calls?|rings?)\b|\b(?:phones?|calls?)\b[^.]{0,20}\banswer(?:s|ed|ing)?\b|\bcalls? (?:are |get )?answered\b|\bpick(?:s|ing)? up\b|\bsomeone rings\b|\bphones? (?:are |is )?covered\b|\b(?:call|phone) coverage\b|\bcoverage on (?:your |the )?phones?\b|\bno missed calls\b|\bnever miss(?:es)?\b[^.]{0,40}\b(?:calls?|leads?|phone)\b|\bno (?:call|caller) (?:goes|is) (?:unanswered|missed)\b|\blive (?:call |phone )?answering\b|\banswering 24\b|\b(?:ai|virtual) (?:receptionist|voice|phone)\b|\breceptionist\b)/i;
|
|
70035
|
+
TEXTS = /\b(?:texts?|texted|texting|sms|text-?backs?|whatsapp|by message on (?:their|your) (?:mobile|phone)|on their mobile|mobile messages?)\b/i;
|
|
70036
|
+
TOPICS = [
|
|
70037
|
+
{ re: /\b(?:missed[- ]calls?|text[- ]?backs?|callers?)\b/i, built: /missed-call text-back/i },
|
|
70038
|
+
{ re: /\b(?:reactivat\w*|win[- ]?backs?|past (?:clients|customers|patients)|old (?:leads|lists?|clients|customers|patients)|dormant|lapsed|bring (?:them|back)\b|brought back)\b/i, built: /win-back/i }
|
|
70039
|
+
];
|
|
69686
70040
|
}
|
|
69687
70041
|
});
|
|
69688
70042
|
|
|
@@ -70085,8 +70439,8 @@ function callDate(source, now) {
|
|
|
70085
70439
|
}
|
|
70086
70440
|
return { date: now.toISOString().slice(0, 10), fromRecording: false };
|
|
70087
70441
|
}
|
|
70088
|
-
function confirmList(
|
|
70089
|
-
return
|
|
70442
|
+
function confirmList(held2) {
|
|
70443
|
+
return held2.map((h) => {
|
|
70090
70444
|
const saved = /already has "([^"]*)" saved/.exec(h.because)?.[1];
|
|
70091
70445
|
return `${ASK.get(h.questionId) ?? h.questionId} \u2192 ${h.value} (their words: ${q2(h.quote)})${saved !== void 0 ? ` [replaces the saved "${saved}"]` : ""}`;
|
|
70092
70446
|
});
|
|
@@ -70367,8 +70721,8 @@ async function saveToContact(input, deps, gated, source) {
|
|
|
70367
70721
|
for (const c of input.confirmed ?? []) {
|
|
70368
70722
|
const id2 = String(c?.questionId), value = String(c?.value ?? "");
|
|
70369
70723
|
if (confirmedIds.has(id2)) continue;
|
|
70370
|
-
const
|
|
70371
|
-
out.confirmationsNotUsed.push({ questionId: id2, value, why:
|
|
70724
|
+
const held2 = gated.held.find((h) => h.questionId === id2);
|
|
70725
|
+
out.confirmationsNotUsed.push({ questionId: id2, value, why: held2 ? `the held answer is "${held2.value}", not "${value}". Propose "${value}" with its quote, then confirm it.` : gated.rejected.some((r) => r.questionId === id2) ? "that answer was thrown out: its quote is not in the transcript." : "no answer to this question is waiting for confirmation." });
|
|
70372
70726
|
}
|
|
70373
70727
|
const candidates = [...gated.accepted, ...gated.held.filter((h) => confirmedIds.has(h.questionId)).map(({ because: _b, ...a }) => a)];
|
|
70374
70728
|
const prior = record?.state.ans ?? {};
|
|
@@ -71412,13 +71766,13 @@ function briefFromAnswers(answers, questions, industry, briefId) {
|
|
|
71412
71766
|
if (cleared.length) brief.__cleared = cleared;
|
|
71413
71767
|
return brief;
|
|
71414
71768
|
}
|
|
71415
|
-
function getPath2(o,
|
|
71769
|
+
function getPath2(o, path67) {
|
|
71416
71770
|
let cur = o;
|
|
71417
|
-
for (const k of
|
|
71771
|
+
for (const k of path67.split(".")) cur = isRec2(cur) ? cur[k] : void 0;
|
|
71418
71772
|
return cur;
|
|
71419
71773
|
}
|
|
71420
|
-
function clearPath(o,
|
|
71421
|
-
const keys =
|
|
71774
|
+
function clearPath(o, path67) {
|
|
71775
|
+
const keys = path67.split(".");
|
|
71422
71776
|
let cur = o;
|
|
71423
71777
|
for (const k of keys.slice(0, -1)) cur = isRec2(cur) ? cur[k] : void 0;
|
|
71424
71778
|
if (isRec2(cur)) delete cur[keys[keys.length - 1]];
|
|
@@ -71551,12 +71905,12 @@ function withLocationLock(dir, locationId2, fn, opts = {}) {
|
|
|
71551
71905
|
return fn();
|
|
71552
71906
|
} finally {
|
|
71553
71907
|
if (fd !== void 0) fs52.closeSync(fd);
|
|
71554
|
-
let
|
|
71908
|
+
let held2 = "";
|
|
71555
71909
|
try {
|
|
71556
|
-
|
|
71910
|
+
held2 = fs52.readFileSync(lock, "utf8");
|
|
71557
71911
|
} catch {
|
|
71558
71912
|
}
|
|
71559
|
-
if (
|
|
71913
|
+
if (held2 === token2) fs52.rmSync(lock, { force: true });
|
|
71560
71914
|
}
|
|
71561
71915
|
}
|
|
71562
71916
|
function claimClinicPrefix(locationId2, prefix, clientKey, opts) {
|
|
@@ -71616,6 +71970,116 @@ var init_clinic_prefix_registry = __esm({
|
|
|
71616
71970
|
}
|
|
71617
71971
|
});
|
|
71618
71972
|
|
|
71973
|
+
// src/client-asks.ts
|
|
71974
|
+
function parseAsks(raw) {
|
|
71975
|
+
if (raw === void 0 || raw === null) return { ok: true, asks: {} };
|
|
71976
|
+
if (typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "The answers could not be read. Choose them again, then save." };
|
|
71977
|
+
const r = raw;
|
|
71978
|
+
const out = {};
|
|
71979
|
+
if (r.texting !== void 0) {
|
|
71980
|
+
if (r.texting !== "yes" && r.texting !== "not_yet") return { ok: false, error: "Texting is Yes or Not yet." };
|
|
71981
|
+
out.texting = r.texting;
|
|
71982
|
+
}
|
|
71983
|
+
if (r.callAnswering !== void 0) {
|
|
71984
|
+
if (!Array.isArray(r.callAnswering) || !r.callAnswering.every((c) => CALL_ANSWERING_CHOICES.includes(c))) {
|
|
71985
|
+
return { ok: false, error: "Call answering choices could not be read. Choose them again, then save." };
|
|
71986
|
+
}
|
|
71987
|
+
const chosen = [...new Set(r.callAnswering)];
|
|
71988
|
+
if (chosen.length) out.callAnswering = CALL_ANSWERING_CHOICES.filter((c) => chosen.includes(c));
|
|
71989
|
+
}
|
|
71990
|
+
if (r.reactivation !== void 0) {
|
|
71991
|
+
const x = r.reactivation;
|
|
71992
|
+
if (!x || typeof x !== "object") return { ok: false, error: "Reactivation answers could not be read. Choose them again, then save." };
|
|
71993
|
+
if (!REACTIVATION_LISTS.includes(x.list)) return { ok: false, error: "Choose who reactivation should reach." };
|
|
71994
|
+
const offer = x.offer;
|
|
71995
|
+
const text3 = typeof offer?.text === "string" ? offer.text.trim() : "";
|
|
71996
|
+
if (!offer || offer.from !== "brief" && offer.from !== "typed" || !text3) return { ok: false, error: "Choose the offer that brings them back, or type one." };
|
|
71997
|
+
if (text3.length > MAX_OFFER) return { ok: false, error: `Keep the offer under ${MAX_OFFER} characters.` };
|
|
71998
|
+
if (x.channel !== "email" && x.channel !== "email_and_text") return { ok: false, error: "Choose Email, or Email and text." };
|
|
71999
|
+
const a = { list: x.list, offer: { from: offer.from, text: text3 }, channel: x.channel };
|
|
72000
|
+
if (a.list === "in_account_not_booked") {
|
|
72001
|
+
const d = Number(x.notBookedDays);
|
|
72002
|
+
if (!Number.isInteger(d) || d < 1 || d > 3650) return { ok: false, error: "Not booked in how many days? Use a whole number from 1 to 3650." };
|
|
72003
|
+
a.notBookedDays = d;
|
|
72004
|
+
}
|
|
72005
|
+
if (x.textConsentConfirmed === true) a.textConsentConfirmed = true;
|
|
72006
|
+
out.reactivation = a;
|
|
72007
|
+
}
|
|
72008
|
+
return { ok: true, asks: out };
|
|
72009
|
+
}
|
|
72010
|
+
function effectiveTexting(asks, intakeSms) {
|
|
72011
|
+
if (asks?.texting === "yes") return "yes";
|
|
72012
|
+
if (asks?.texting === "not_yet") return "not_yet";
|
|
72013
|
+
if (intakeSms === true) return "yes";
|
|
72014
|
+
if (intakeSms === false) return "not_yet";
|
|
72015
|
+
return "unanswered";
|
|
72016
|
+
}
|
|
72017
|
+
function asksContradiction(asks, texting) {
|
|
72018
|
+
if (texting !== "yes" && asks.callAnswering?.includes("missed_text")) return "A text back after a missed call needs Texting: Yes.";
|
|
72019
|
+
const re = asks.reactivation;
|
|
72020
|
+
if (re?.channel === "email_and_text") {
|
|
72021
|
+
if (texting !== "yes") return "Email and text needs Texting: Yes.";
|
|
72022
|
+
if (!re.textConsentConfirmed) return "Email and text needs your client to confirm these people agreed to get texts.";
|
|
72023
|
+
}
|
|
72024
|
+
return null;
|
|
72025
|
+
}
|
|
72026
|
+
function heldItems(ticks, asks, texting) {
|
|
72027
|
+
const out = [];
|
|
72028
|
+
const wait = (id2, name2) => ({ id: id2, name: name2, reason: "unanswered", line: `Waiting for your client's answer: ${name2}` });
|
|
72029
|
+
const textsMatter = ticks.includes("sms") || ticks.includes("missed") || ticks.includes("reactivation");
|
|
72030
|
+
if (textsMatter && texting === "unanswered") out.push(wait("texting", "Texting"));
|
|
72031
|
+
if (ticks.includes("sms") && texting === "not_yet") out.push({ id: "texting", name: "Texting", reason: "not_yet", line: "Not built, your client said not yet: Texting" });
|
|
72032
|
+
if (ticks.includes("missed")) {
|
|
72033
|
+
const c = asks?.callAnswering ?? [];
|
|
72034
|
+
if (!c.length) out.push(wait("call_answering", "Call answering"));
|
|
72035
|
+
else {
|
|
72036
|
+
const later = c.filter((x) => x === "live_voice" || x === "chat_bot");
|
|
72037
|
+
if (later.length) out.push({ id: "call_answering", name: "Call answering", reason: "later_update", line: `Not built yet, chosen and coming in a later update: ${later.map((x) => x === "live_voice" ? "an AI that answers the phone live" : "an AI that replies by text or chat and books").join(" and ")}` });
|
|
72038
|
+
if (c.includes("missed_text") && texting !== "yes") out.push(wait("texting", "Texting (the text back after a missed call needs it)"));
|
|
72039
|
+
}
|
|
72040
|
+
}
|
|
72041
|
+
if (ticks.includes("reactivation")) {
|
|
72042
|
+
const re = asks?.reactivation;
|
|
72043
|
+
if (!re) out.push(wait("reactivation", "Reactivation (who it reaches, which offer)"));
|
|
72044
|
+
else if (re.list !== "cold_from_now") out.push({ id: "reactivation", name: "Reactivation", reason: "later_update", line: `Not built yet, chosen and coming in a later update: reactivation for ${re.list === "import" ? "a list your client will give you" : `contacts here with no booking in ${re.notBookedDays} days`}` });
|
|
72045
|
+
}
|
|
72046
|
+
return out.filter((h, i) => out.findIndex((o) => o.id === h.id && o.reason === h.reason) === i);
|
|
72047
|
+
}
|
|
72048
|
+
function featuresAllowedByAsks(features, ticks, asks) {
|
|
72049
|
+
return features.filter((f) => {
|
|
72050
|
+
if (f === "missed_call" && ticks.includes("missed")) return asks?.callAnswering?.includes("missed_text") === true;
|
|
72051
|
+
if (f === "winback" && ticks.includes("reactivation")) return asks?.reactivation?.list === "cold_from_now";
|
|
72052
|
+
return true;
|
|
72053
|
+
});
|
|
72054
|
+
}
|
|
72055
|
+
function briefWithAsks(brief, asks, ticks, presetWorkflows) {
|
|
72056
|
+
const ch = brief.channels && typeof brief.channels === "object" ? brief.channels : {};
|
|
72057
|
+
const texting = effectiveTexting(asks, typeof ch.sms === "boolean" ? ch.sms : void 0);
|
|
72058
|
+
const { sms: _was, ...rest } = ch;
|
|
72059
|
+
let out = { ...brief, channels: { ...rest, ...texting === "unanswered" ? {} : { sms: texting === "yes" } } };
|
|
72060
|
+
if (!ticks) return out;
|
|
72061
|
+
const scope = out.scope;
|
|
72062
|
+
const offered = [...new Set(presetWorkflows.map((w) => featureOfWorkflow(String(w.ref ?? ""), w.feature)).filter((f) => f !== null))];
|
|
72063
|
+
const current = scope ? Array.isArray(scope.features) ? scope.features.map(String) : [] : offered;
|
|
72064
|
+
const allowed = featuresAllowedByAsks(current, ticks, asks);
|
|
72065
|
+
if (allowed.length !== current.length) out = { ...out, scope: { ...scope?.package ? { package: scope.package } : {}, features: allowed } };
|
|
72066
|
+
return out;
|
|
72067
|
+
}
|
|
72068
|
+
function asksBindingHash(asks, ticks) {
|
|
72069
|
+
return contentHash2({ asks: asks && Object.keys(asks).length ? asks : null, ticks: ticks ? [...new Set(ticks)].sort() : null });
|
|
72070
|
+
}
|
|
72071
|
+
var CALL_ANSWERING_CHOICES, REACTIVATION_LISTS, MAX_OFFER;
|
|
72072
|
+
var init_client_asks = __esm({
|
|
72073
|
+
"src/client-asks.ts"() {
|
|
72074
|
+
"use strict";
|
|
72075
|
+
init_features();
|
|
72076
|
+
init_build_contract();
|
|
72077
|
+
CALL_ANSWERING_CHOICES = ["live_voice", "chat_bot", "missed_text"];
|
|
72078
|
+
REACTIVATION_LISTS = ["import", "in_account_not_booked", "cold_from_now"];
|
|
72079
|
+
MAX_OFFER = 300;
|
|
72080
|
+
}
|
|
72081
|
+
});
|
|
72082
|
+
|
|
71619
72083
|
// src/dashboard.ts
|
|
71620
72084
|
var dashboard_exports = {};
|
|
71621
72085
|
__export(dashboard_exports, {
|
|
@@ -71630,6 +72094,7 @@ __export(dashboard_exports, {
|
|
|
71630
72094
|
ASSESSMENT_CHANGED_SINCE_APPROVAL: () => ASSESSMENT_CHANGED_SINCE_APPROVAL,
|
|
71631
72095
|
ASSESSMENT_PIPELINE_MISSING: () => ASSESSMENT_PIPELINE_MISSING,
|
|
71632
72096
|
BRIEF_NEEDS_INTAKE: () => BRIEF_NEEDS_INTAKE2,
|
|
72097
|
+
BUILD_WHAT_WAS_WAITING: () => BUILD_WHAT_WAS_WAITING,
|
|
71633
72098
|
CHOOSE_BEFORE_BUILD: () => CHOOSE_BEFORE_BUILD,
|
|
71634
72099
|
CHOOSE_CLIENT: () => CHOOSE_CLIENT,
|
|
71635
72100
|
CHOOSE_WHAT_THEY_BOUGHT: () => CHOOSE_WHAT_THEY_BOUGHT,
|
|
@@ -71686,6 +72151,7 @@ __export(dashboard_exports, {
|
|
|
71686
72151
|
brandPagesForPro: () => brandPagesForPro,
|
|
71687
72152
|
briefReach: () => briefReach,
|
|
71688
72153
|
buildPreviewsFor: () => buildPreviewsFor,
|
|
72154
|
+
buildStatusFor: () => buildStatusFor,
|
|
71689
72155
|
callClaudeRecorded: () => callClaudeRecorded,
|
|
71690
72156
|
canonicalIndustry: () => canonicalIndustry,
|
|
71691
72157
|
capabilityJobs: () => capabilityJobs,
|
|
@@ -71727,6 +72193,7 @@ __export(dashboard_exports, {
|
|
|
71727
72193
|
guarded: () => guarded,
|
|
71728
72194
|
hasChosenScope: () => hasChosenScope,
|
|
71729
72195
|
insideDir: () => insideDir,
|
|
72196
|
+
intakeAskFacts: () => intakeAskFacts,
|
|
71730
72197
|
intakeErrorDetail: () => intakeErrorDetail,
|
|
71731
72198
|
intakeInput: () => intakeInput,
|
|
71732
72199
|
intakeMissingMessage: () => intakeMissingMessage,
|
|
@@ -71782,15 +72249,20 @@ __export(dashboard_exports, {
|
|
|
71782
72249
|
rereadQueueIntakes: () => rereadQueueIntakes,
|
|
71783
72250
|
resetCockpitStateRecovery: () => resetCockpitStateRecovery,
|
|
71784
72251
|
resolvePlan: () => resolvePlan2,
|
|
72252
|
+
reviewIsStale: () => reviewIsStale,
|
|
72253
|
+
reviewPromisesFor: () => reviewPromisesFor,
|
|
72254
|
+
reviewPromisesHash: () => reviewPromisesHash,
|
|
71785
72255
|
routeGate: () => routeGate,
|
|
71786
72256
|
runDashboard: () => runDashboard,
|
|
71787
72257
|
runForTests: () => runForTests,
|
|
71788
72258
|
runRefusal: () => runRefusal,
|
|
71789
72259
|
runStopReason: () => runStopReason,
|
|
71790
72260
|
safeDocName: () => safeDocName,
|
|
72261
|
+
savedPlanFitsAnswers: () => savedPlanFitsAnswers,
|
|
71791
72262
|
scanGate: () => scanGate,
|
|
71792
72263
|
scopeHashOf: () => scopeHashOf,
|
|
71793
72264
|
scopePrefillFor: () => scopePrefillFor,
|
|
72265
|
+
scopeWithAsks: () => scopeWithAsks,
|
|
71794
72266
|
secondClientInputHash: () => secondClientInputHash,
|
|
71795
72267
|
setAccountIndustry: () => setAccountIndustry,
|
|
71796
72268
|
setAccountType: () => setAccountType,
|
|
@@ -71981,7 +72453,7 @@ function withdrawIfStaffChanged(state2, locationId2) {
|
|
|
71981
72453
|
return { state: { ...setBuildApproval(state2, locationId2, null), planApprovals: approvals }, withdrawn: true, message: ap.staffHash === void 0 ? APPROVAL_WITHDRAWN_NO_TEAM : APPROVAL_WITHDRAWN_FOR_STAFF };
|
|
71982
72454
|
}
|
|
71983
72455
|
function withBoughtJobs(tasks2, lines) {
|
|
71984
|
-
const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text));
|
|
72456
|
+
const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text) && !HELD_JOB.test(t.text));
|
|
71985
72457
|
const prior = new Map(tasks2.map((t) => [t.id, t]));
|
|
71986
72458
|
return [...kept, ...lines.map((line4) => {
|
|
71987
72459
|
if (typeof line4 === "string") return prior.get(taskId(line4)) ?? { id: taskId(line4), text: line4 };
|
|
@@ -72081,6 +72553,71 @@ function presetWorkflowsFor(state2, recordKey) {
|
|
|
72081
72553
|
}
|
|
72082
72554
|
return null;
|
|
72083
72555
|
}
|
|
72556
|
+
function scopeWithAsks(state2, rk) {
|
|
72557
|
+
const current = state2.scope?.[rk];
|
|
72558
|
+
const ticks = state2.bought?.[rk];
|
|
72559
|
+
if (!current || !Array.isArray(ticks)) return current;
|
|
72560
|
+
const base = current.wholePreset ? offeredFeaturesFor(state2, rk) : current.features;
|
|
72561
|
+
const allowed = featuresAllowedByAsks(base, ticks, state2.asks?.[rk]);
|
|
72562
|
+
return allowed.length === base.length ? current : { features: allowed, setAt: current.setAt };
|
|
72563
|
+
}
|
|
72564
|
+
function intakeAskFacts(answers) {
|
|
72565
|
+
const get2 = (key2, label2) => {
|
|
72566
|
+
const v = answers?.[label2] ?? answers?.[key2];
|
|
72567
|
+
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
72568
|
+
};
|
|
72569
|
+
const { sms } = intakeChannelAnswers(answers);
|
|
72570
|
+
const phone = get2("has_phone_number", "Do you already have a phone number in GoHighLevel?")?.toLowerCase();
|
|
72571
|
+
const offer = get2("core_offer", "Core Offer");
|
|
72572
|
+
return { ...sms !== void 0 ? { sms } : {}, ...phone === "yes" || phone === "no" || phone === "unsure" ? { phone } : {}, ...offer ? { offer: offer.slice(0, 300) } : {} };
|
|
72573
|
+
}
|
|
72574
|
+
function buildStatusFor(st, rk, locationId2) {
|
|
72575
|
+
const ticks = st.bought?.[rk] ?? [];
|
|
72576
|
+
const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
|
|
72577
|
+
const held2 = {};
|
|
72578
|
+
const item = { texting: "sms", call_answering: "missed", reactivation: "reactivation" };
|
|
72579
|
+
for (const h of heldItems(ticks, st.asks?.[rk], effectiveTexting(st.asks?.[rk], intake.sms))) held2[item[h.id]] ??= h.line;
|
|
72580
|
+
const sp = readSavedPlan(rk);
|
|
72581
|
+
const ran = st.outcomes?.[`${rk}:3`]?.ok === true || (sp?.built?.length ?? 0) > 0;
|
|
72582
|
+
if (!ran) return { held: held2 };
|
|
72583
|
+
const featureOf = (ref) => {
|
|
72584
|
+
try {
|
|
72585
|
+
return featureOfWorkflow(ref);
|
|
72586
|
+
} catch {
|
|
72587
|
+
return null;
|
|
72588
|
+
}
|
|
72589
|
+
};
|
|
72590
|
+
const made = /* @__PURE__ */ new Set();
|
|
72591
|
+
for (const o of sp?.planId ? madeByBuild(locationId2, sp.planId) : []) if (o.type === "workflow") {
|
|
72592
|
+
const f = featureOf(o.ref);
|
|
72593
|
+
if (f) made.add(f);
|
|
72594
|
+
}
|
|
72595
|
+
for (const b of sp?.built ?? []) if (b.type === "workflow" && b.realId) {
|
|
72596
|
+
const f = featureOf(String(b.ref));
|
|
72597
|
+
if (f) made.add(f);
|
|
72598
|
+
}
|
|
72599
|
+
return { made, held: held2 };
|
|
72600
|
+
}
|
|
72601
|
+
function reviewPromisesFor(st, rk, locationId2) {
|
|
72602
|
+
const ticks = new Set(st.bought?.[rk] ?? []);
|
|
72603
|
+
const items = boughtItemsFor(offeredFeaturesFor(st, rk), boughtWordingFor(st, rk), buildStatusFor(st, rk, locationId2)).filter((i) => ticks.has(i.id));
|
|
72604
|
+
const willBuild = items.map((i) => /^(?:Will build|Built|Always built): (.+)$/.exec(i.delivery)?.[1]).filter((x) => Boolean(x));
|
|
72605
|
+
const held2 = items.map((i) => i.delivery).filter((d) => HELD_JOB.test(d));
|
|
72606
|
+
const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
|
|
72607
|
+
return { willBuild, held: held2, textingOn: effectiveTexting(st.asks?.[rk], intake.sms) === "yes" };
|
|
72608
|
+
}
|
|
72609
|
+
function reviewPromisesHash(st, rk, locationId2 = rk) {
|
|
72610
|
+
return contentHash2(reviewPromisesFor(st, rk, locationId2));
|
|
72611
|
+
}
|
|
72612
|
+
function reviewIsStale(st, rk, review) {
|
|
72613
|
+
return Boolean(review?.ok) && review.promisesHash !== reviewPromisesHash(st, rk);
|
|
72614
|
+
}
|
|
72615
|
+
function savedPlanFitsAnswers(sp, state2, rk) {
|
|
72616
|
+
if (!sp || !savedPlanMatchesScope(sp, scopeWithAsks(state2, rk))) return false;
|
|
72617
|
+
const asks = state2.asks?.[rk];
|
|
72618
|
+
if (!sp.asksHash) return !(asks && Object.keys(asks).length);
|
|
72619
|
+
return sp.asksHash === asksBindingHash(asks, state2.bought?.[rk]);
|
|
72620
|
+
}
|
|
72084
72621
|
function offeredFeaturesFor(state2, recordKey) {
|
|
72085
72622
|
const workflows = presetWorkflowsFor(state2, recordKey);
|
|
72086
72623
|
return workflows ? featuresInPreset(workflows) : FEATURE_IDS;
|
|
@@ -72201,12 +72738,12 @@ async function resolvePlan2(creds, opts = {}) {
|
|
|
72201
72738
|
}
|
|
72202
72739
|
return { error: "Could not verify the license (offline and no valid cached attestation)." };
|
|
72203
72740
|
}
|
|
72204
|
-
async function recheckCockpitLicense(
|
|
72741
|
+
async function recheckCockpitLicense(resolve15 = resolvePlan2, opts = {}) {
|
|
72205
72742
|
const id2 = pinnedLicenseIdentity();
|
|
72206
72743
|
if (!id2) return;
|
|
72207
72744
|
const seq = beginLicenseCheck(id2);
|
|
72208
72745
|
const startedWith = readCredentials();
|
|
72209
|
-
const r = await
|
|
72746
|
+
const r = await resolve15({ ...id2 }, { persist: false }).catch(() => ({ error: "unreachable" }));
|
|
72210
72747
|
if ("error" in r) {
|
|
72211
72748
|
if (r.reason) applyLicenseVerdict(seq, id2, r.reason === "team_limit" ? { ok: false, reason: "team_limit", detail: r.error, serverTime: r.serverTime } : { ok: false, reason: r.reason, serverTime: r.serverTime }, opts);
|
|
72212
72749
|
if (licenseSessionRefusal(Date.now(), "cockpit")) cockpitLicenceRefused("licence re-check refused");
|
|
@@ -72219,6 +72756,17 @@ function assertCockpitLicenseHolds(now = Date.now()) {
|
|
|
72219
72756
|
const refusal2 = licenseSessionRefusal(now, "cockpit");
|
|
72220
72757
|
if (refusal2) throw new Error(refusal2);
|
|
72221
72758
|
}
|
|
72759
|
+
function claimRunStart() {
|
|
72760
|
+
if (activeRun2?.running || runStarting) throw new Error(RUN_IN_PROGRESS);
|
|
72761
|
+
runStarting = true;
|
|
72762
|
+
let done = false;
|
|
72763
|
+
return () => {
|
|
72764
|
+
if (!done) {
|
|
72765
|
+
done = true;
|
|
72766
|
+
runStarting = false;
|
|
72767
|
+
}
|
|
72768
|
+
};
|
|
72769
|
+
}
|
|
72222
72770
|
function takeOneTimeLine(entry, runLocationId, l) {
|
|
72223
72771
|
if (l.locationId !== runLocationId) return false;
|
|
72224
72772
|
const k = l.email.trim().toLowerCase();
|
|
@@ -72279,7 +72827,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
72279
72827
|
// or before Your team decided) is made again, never built with someone the member was not shown.
|
|
72280
72828
|
savedPlan: (() => {
|
|
72281
72829
|
const sp = stage === 3 ? readSavedPlan(rk) : null;
|
|
72282
|
-
return sp && sp.inputHash === meta.inputHash &&
|
|
72830
|
+
return sp && sp.inputHash === meta.inputHash && savedPlanFitsAnswers(sp, state2, rk) && savedPlanTeamIsCurrent(sp, meta.normalizedAnswers, composed) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
|
|
72283
72831
|
})(),
|
|
72284
72832
|
textSnippets: Boolean(state2.textSnippets?.[rk])
|
|
72285
72833
|
};
|
|
@@ -72295,7 +72843,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
72295
72843
|
} catch {
|
|
72296
72844
|
runFacts = {};
|
|
72297
72845
|
}
|
|
72298
|
-
runFacts = withBuiltCalendarTeams(runFacts, readSavedPlan(rk));
|
|
72846
|
+
runFacts = withBuiltCalendarTeams(runFacts, readSavedPlan(rk), calendarTeamsFromJournal(locationId2));
|
|
72299
72847
|
} catch (e) {
|
|
72300
72848
|
context2.readback = plannedReadbacks(verificationPlan).map(({ ref, type, name: name2 }) => ({ ref, type, name: name2, status: "unreadable", detail: e instanceof Error ? e.message : String(e) }));
|
|
72301
72849
|
}
|
|
@@ -72330,7 +72878,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
72330
72878
|
const ticked = standingTicks(tasksNow, currentKeys);
|
|
72331
72879
|
const openRecipients = readBuildObligations(rk).filter((o) => o.type === "staff_pending" && o.ref.startsWith("workflow.") && !ticked.has(`${o.type}:${o.ref}`)).length;
|
|
72332
72880
|
const openHandoffs = planHandoffs.filter((h) => {
|
|
72333
|
-
const st = handoffInAccount(h, runFacts, readback, plannedUsers, openRecipients, plannedCalendars);
|
|
72881
|
+
const st = handoffInAccount(fitHandoff(h, planHandoffs, runFacts), runFacts, readback, plannedUsers, openRecipients, plannedCalendars);
|
|
72334
72882
|
const mainOpen = !(st.readable && st.done) && !ticked.has(h.ref);
|
|
72335
72883
|
const part = handoffPartId(h.ref);
|
|
72336
72884
|
return mainOpen || part !== void 0 && !ticked.has(part);
|
|
@@ -72713,7 +73261,7 @@ function mergeQueueIntakes(latest, reread, before) {
|
|
|
72713
73261
|
return { ...latest, intakeInputs: { ...latest.intakeInputs ?? {}, ...fresh } };
|
|
72714
73262
|
}
|
|
72715
73263
|
function gateReadBody(req) {
|
|
72716
|
-
return new Promise((
|
|
73264
|
+
return new Promise((resolve15) => {
|
|
72717
73265
|
let body = "", over2 = false;
|
|
72718
73266
|
req.on("data", (c) => {
|
|
72719
73267
|
if (over2) return;
|
|
@@ -72724,8 +73272,8 @@ function gateReadBody(req) {
|
|
|
72724
73272
|
req.resume?.();
|
|
72725
73273
|
}
|
|
72726
73274
|
});
|
|
72727
|
-
req.on("end", () =>
|
|
72728
|
-
req.on("error", () =>
|
|
73275
|
+
req.on("end", () => resolve15(over2 ? null : body));
|
|
73276
|
+
req.on("error", () => resolve15(null));
|
|
72729
73277
|
});
|
|
72730
73278
|
}
|
|
72731
73279
|
function readBody(req, res, max, onDone, tooLarge = "That request is too large.", sensitive = false) {
|
|
@@ -72739,10 +73287,10 @@ function readBody(req, res, max, onDone, tooLarge = "That request is too large."
|
|
|
72739
73287
|
} catch {
|
|
72740
73288
|
}
|
|
72741
73289
|
};
|
|
72742
|
-
const
|
|
72743
|
-
if (typeof
|
|
73290
|
+
const held2 = req[GATE_BODY];
|
|
73291
|
+
if (typeof held2 === "string") {
|
|
72744
73292
|
setImmediate(() => {
|
|
72745
|
-
if (
|
|
73293
|
+
if (held2.length > max) {
|
|
72746
73294
|
try {
|
|
72747
73295
|
res.writeHead(413, { "Content-Type": "application/json" });
|
|
72748
73296
|
res.end(JSON.stringify({ ok: false, error: tooLarge }));
|
|
@@ -72751,7 +73299,7 @@ function readBody(req, res, max, onDone, tooLarge = "That request is too large."
|
|
|
72751
73299
|
return;
|
|
72752
73300
|
}
|
|
72753
73301
|
try {
|
|
72754
|
-
const r = onDone(
|
|
73302
|
+
const r = onDone(held2);
|
|
72755
73303
|
if (r && typeof r.catch === "function") r.catch(fail);
|
|
72756
73304
|
} catch (e) {
|
|
72757
73305
|
fail(e);
|
|
@@ -74026,6 +74574,60 @@ async function profLoad(){if(profLoading)return;profLoading=true;try{profCache=(
|
|
|
74026
74574
|
waits for the Assessment question above the list. */
|
|
74027
74575
|
/* The ticks and package chosen but not yet saved, kept outside the block so a redraw keeps them (review of c09e946). */
|
|
74028
74576
|
let scopeDraft={},boughtNotice={};
|
|
74577
|
+
/* Ask first (Jerry 2026-10-29): texting, call answering and reactivation are asked of the client before Build. The
|
|
74578
|
+
answers save with "Save what they bought" (server: parseAsks, asksContradiction). Nothing is pre-selected: the
|
|
74579
|
+
client's own form answer shows with a Change link; a phone number is only a hint. Blank = held, never built. */
|
|
74580
|
+
let asksDraft={};
|
|
74581
|
+
function asksClean(a){const o={};if(a.texting)o.texting=a.texting;if(a.callAnswering&&a.callAnswering.length)o.callAnswering=[...a.callAnswering];
|
|
74582
|
+
/* W44: typed text counts whenever no other offer was chosen, whatever was clicked first. */
|
|
74583
|
+
const r=a.reactivation;if(r&&r.list){const off=r.offer&&r.offer.text?r.offer:(r._typed?{from:"typed",text:r._typed}:{});const x={list:r.list,offer:off,channel:r.channel};if(r.list==="in_account_not_booked")x.notBookedDays=Number(r.notBookedDays);if(r.textConsentConfirmed)x.textConsentConfirmed=true;o.reactivation=x}return o}
|
|
74584
|
+
function asksBox(id,v,getTicked,keep){
|
|
74585
|
+
const box=mk("div","cds-stack ask-first");box.id="asks-"+id;const ia=v.intakeAsks||{};const savedJson=JSON.stringify(v.asks||{});
|
|
74586
|
+
if(!asksDraft[id]||asksDraft[id].base!==savedJson)asksDraft[id]={base:savedJson,a:JSON.parse(savedJson),change:false};
|
|
74587
|
+
const d=asksDraft[id],a=d.a;
|
|
74588
|
+
const radio=(name,val,lab,on,dis,why,act)=>{const l=mk("label","cds-check");const r=document.createElement("input");r.type="radio";r.name=name;r.checked=on;r.disabled=!!dis;r.onchange=act;l.appendChild(r);const s=mk("span","");s.appendChild(mk("b","",lab));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);return l};
|
|
74589
|
+
function draw(){box.innerHTML="";const t=getTicked();
|
|
74590
|
+
if(!(t.has("sms")||t.has("missed")||t.has("reactivation"))){box.hidden=true;return}box.hidden=false;
|
|
74591
|
+
box.appendChild(mk("h4","cds-panel-label","Confirm with your client before Build"));
|
|
74592
|
+
box.appendChild(mk("p","","Ask your client these, then choose. Nothing is built for a question left blank; it waits for your client's answer."));
|
|
74593
|
+
box.appendChild(mk("p","muted","The answers save with Save what they bought. Changing one after the plan is approved means approving it again."));
|
|
74594
|
+
const texting=a.texting||(ia.sms===true?"yes":ia.sms===false?"not_yet":"");
|
|
74595
|
+
const tx=mk("div","cds-stack");tx.appendChild(mk("b","","Texting"));tx.appendChild(mk("p","","Can this business text leads?"));
|
|
74596
|
+
if(!a.texting&&ia.sms!==undefined&&!d.change){const p=mk("p","","Your client's form said: "+(ia.sms?"Yes":"Not yet")+" ");const ch=mk("button","cds-link","Change");ch.type="button";ch.onclick=()=>{d.change=true;draw()};p.appendChild(ch);tx.appendChild(p)}
|
|
74597
|
+
else{const row=mk("div","cds-row");row.appendChild(radio("tx-"+id,"yes","Yes",a.texting==="yes",false,"",()=>{a.texting="yes";keep();draw()}));row.appendChild(radio("tx-"+id,"not_yet","Not yet",a.texting==="not_yet",false,"",()=>{a.texting="not_yet";keep();draw()}));tx.appendChild(row)}
|
|
74598
|
+
if(ia.phone==="yes")tx.appendChild(mk("small","muted","Hint: your client's form said they have a phone number in GoHighLevel."));
|
|
74599
|
+
box.appendChild(tx);
|
|
74600
|
+
if(t.has("missed")){const ca=mk("div","cds-stack");ca.appendChild(mk("b","","Call answering"));ca.appendChild(mk("p","","What did your client pay for? Choose all that apply."));
|
|
74601
|
+
const cur=new Set(a.callAnswering||[]);
|
|
74602
|
+
const opt=(val,lab,sub,why,greyed)=>{const l=mk("label","cds-check");const c=document.createElement("input");c.type="checkbox";c.checked=cur.has(val);c.disabled=greyed&&!cur.has(val);c.onchange=()=>{if(c.checked)cur.add(val);else cur.delete(val);a.callAnswering=[...cur];keep();draw()};l.appendChild(c);const s=mk("span","");s.appendChild(mk("b","",lab));if(sub)s.appendChild(mk("small","",sub));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);ca.appendChild(l)};
|
|
74603
|
+
opt("live_voice","An AI answers the phone live","Your client connects their number to it afterwards. Once it is connected, GoHighLevel charges for every minute it talks, usually from your agency's wallet.","",false);
|
|
74604
|
+
opt("chat_bot","An AI replies by text or chat and books the visit","",texting==="yes"?"":"Texting replies need Texting: Yes.",false);
|
|
74605
|
+
opt("missed_text","A text back after a missed call","",texting==="yes"?"":"Needs texting: Yes",texting!=="yes");
|
|
74606
|
+
box.appendChild(ca)}
|
|
74607
|
+
if(t.has("reactivation")){const re=a.reactivation||(a.reactivation={});const rv=mk("div","cds-stack");rv.appendChild(mk("b","","Reactivation"));
|
|
74608
|
+
rv.appendChild(mk("p","","Who should it reach?"));
|
|
74609
|
+
rv.appendChild(radio("rl-"+id,"import","A list your client will give you to import",re.list==="import",false,"",()=>{re.list="import";keep();draw()}));
|
|
74610
|
+
/* rc.3 c6 hole 4: the number is in the sentence as text, and its box sits on its own labelled line. */
|
|
74611
|
+
const nDays=re.notBookedDays||90;
|
|
74612
|
+
rv.appendChild(radio("rl-"+id,"in_account_not_booked","Contacts already here who have not booked in "+nDays+" days",re.list==="in_account_not_booked",false,"",()=>{re.list="in_account_not_booked";if(!re.notBookedDays)re.notBookedDays=90;keep();draw()}));
|
|
74613
|
+
if(re.list==="in_account_not_booked"){const row=mk("label","cds-row");row.appendChild(mk("span","","Days with no booking: "));const days=document.createElement("input");days.type="number";days.min="1";days.max="3650";days.className="cds-input";days.style.width="6em";days.value=String(nDays);days.onchange=()=>{const n=Math.round(Number(days.value));if(n>=1&&n<=3650){re.notBookedDays=n;keep();draw()}else days.value=String(nDays)};row.appendChild(days);rv.appendChild(row);
|
|
74614
|
+
rv.appendChild(mk("small","muted","You will see how many people this reaches and approve it before anyone gets a message."))}
|
|
74615
|
+
rv.appendChild(radio("rl-"+id,"cold_from_now","Only leads who go cold from now on",re.list==="cold_from_now",false,"",()=>{re.list="cold_from_now";keep();draw()}));
|
|
74616
|
+
rv.appendChild(mk("p","","What offer brings them back?"));
|
|
74617
|
+
/* W44: with no offer in the brief, typing one is the only choice, so it starts chosen. */
|
|
74618
|
+
if(!ia.offer&&!re.offer)re.offer={from:"typed",text:""};
|
|
74619
|
+
if(ia.offer)rv.appendChild(radio("ro-"+id,"brief","From the brief: \\u201c"+ia.offer+"\\u201d",re.offer&&re.offer.from==="brief",false,"",()=>{re.offer={from:"brief",text:ia.offer};keep();draw()}));
|
|
74620
|
+
const typed=radio("ro-"+id,"typed","Type one:",re.offer&&re.offer.from==="typed",false,"",()=>{re.offer={from:"typed",text:ot.value.trim()};keep();draw()});
|
|
74621
|
+
/* W45 (Jerry, Safari): inside the label the box drew as a tiny oval. It is its own full-width field under its choice. */
|
|
74622
|
+
const ot=document.createElement("textarea");ot.rows=2;ot.maxLength=300;ot.className="cds-input ask-offer";ot.style.width="100%";ot.style.boxSizing="border-box";ot.placeholder="e.g. a free skin check";ot.value=re.offer&&re.offer.from==="typed"?re.offer.text:"";ot.setAttribute("aria-label","The offer, in your client's words");ot.oninput=()=>{re._typed=ot.value.trim();re.offer={from:"typed",text:re._typed};keep();typed.querySelector("input").checked=true};
|
|
74623
|
+
rv.appendChild(typed);rv.appendChild(ot);
|
|
74624
|
+
rv.appendChild(mk("p","","How?"));
|
|
74625
|
+
const canText=texting==="yes"&&re.textConsentConfirmed===true;
|
|
74626
|
+
rv.appendChild(radio("rc-"+id,"email","Email",re.channel==="email",false,"",()=>{re.channel="email";keep();draw()}));
|
|
74627
|
+
rv.appendChild(radio("rc-"+id,"email_and_text","Email and text",re.channel==="email_and_text",!canText&&re.channel!=="email_and_text",texting!=="yes"?"Needs texting: Yes":re.textConsentConfirmed?"":"Needs your client to confirm these people agreed to get texts",()=>{re.channel="email_and_text";keep();draw()}));
|
|
74628
|
+
const cl=mk("label","cds-check");const cc=document.createElement("input");cc.type="checkbox";cc.checked=re.textConsentConfirmed===true;cc.onchange=()=>{if(cc.checked)re.textConsentConfirmed=true;else{delete re.textConsentConfirmed;if(re.channel==="email_and_text")re.channel="email"}keep();draw()};cl.appendChild(cc);cl.appendChild(mk("span","","My client confirmed these people agreed to get texts"));rv.appendChild(cl);
|
|
74629
|
+
box.appendChild(rv)}}
|
|
74630
|
+
draw();box.redraw=draw;return box}
|
|
74029
74631
|
const BOUGHT_ITEMS=${scriptJson(boughtItemsFor(FEATURE_IDS))};
|
|
74030
74632
|
function scopeChosen(state,id){const s=(state.scope||{})[id];const v=(state.boughtViews||{})[id];return Boolean(s&&(s.wholePreset||(Array.isArray(s.features)&&s.features.length)))&&!(v&&v.ready===false)}
|
|
74031
74633
|
function openWhatTheyBought(id){rOpen[id]=true;rSeen[id]=true;const go=()=>{render(lastState,lastRun);requestAnimationFrame(()=>{const t=document.getElementById("wtb-"+id);if(t){t.scrollIntoView({behavior:"smooth",block:"start"});const f=t.querySelector("button,input,select");if(f)f.focus({preventScroll:true})}})};if(!rData[id])rLoad(id).then(go,go);else go()}
|
|
@@ -74048,7 +74650,7 @@ function scopeBlock(id,state){
|
|
|
74048
74650
|
const v=boughtView(state,id);
|
|
74049
74651
|
if(!bsHideAssess)sc.appendChild(assessmentQuestion(id,v));
|
|
74050
74652
|
sc.appendChild(mk("h4","cds-panel-label","What they bought"));
|
|
74051
|
-
sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked
|
|
74653
|
+
sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked Will build get built."));
|
|
74052
74654
|
if(!profCache){profLoad();const p=document.createElement("p");p.className="muted";p.textContent="Loading your packages\u2026";sc.appendChild(p);return sc}
|
|
74053
74655
|
const saved=(state.scope||{})[id]||null;const pk=(profCache.packages||[]);
|
|
74054
74656
|
const sel=document.createElement("select");sel.className="cds-select";sel.setAttribute("aria-label","Package they bought");
|
|
@@ -74059,17 +74661,17 @@ function scopeBlock(id,state){
|
|
|
74059
74661
|
const keepScope=()=>{scopeDraft[id]={ticked:[...ticked],pkg:sel.value,base:base};unsaved.hidden=false};
|
|
74060
74662
|
const everything=document.createElement("button");everything.className="cds-btn ghost";everything.textContent="Tick everything";
|
|
74061
74663
|
everything.title="Ticks all fourteen. Nothing is saved until you press Save what they bought.";
|
|
74062
|
-
everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs()};
|
|
74664
|
+
everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};
|
|
74063
74665
|
sc.appendChild(everything);
|
|
74064
74666
|
const boxes=document.createElement("div");boxes.className="cds-stack";
|
|
74065
74667
|
function drawBoxes(){boxes.innerHTML="";for(const it of v.items){const l=document.createElement("label");l.className="cds-check";
|
|
74066
|
-
l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs()};l.appendChild(c);
|
|
74668
|
+
l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs();if(askEl)askEl.redraw()};l.appendChild(c);
|
|
74067
74669
|
const t=document.createElement("span");const tb=document.createElement("b");tb.textContent=it.name;t.appendChild(tb);t.appendChild(mk("small","",it.means));
|
|
74068
|
-
t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next)t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
|
|
74670
|
+
t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next&&!(((asksDraft[id]&&asksDraft[id].a.callAnswering)||(v.asks||{}).callAnswering||[]).includes("live_voice")))t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
|
|
74069
74671
|
const figs=((state.assessmentFigures||{})[id])||{};
|
|
74070
74672
|
function drawFigs(){for(const l of boxes.querySelectorAll("label")){const it=v.items.find(i=>i.id===l.dataset.k);const old=l.querySelector("em");if(old)old.remove();if(it&&it.feature&&figs[it.feature]){const e=document.createElement("em");e.className="cds-chip"+(ticked.has(it.id)?"":" warn");e.textContent="Assessment: "+figs[it.feature]+(ticked.has(it.id)?".":". Not ticked.");l.querySelector("span").appendChild(e)}}}
|
|
74071
74673
|
/* A package names build features: it ticks their items and unticks the other built ones; the rest stay as they are. */
|
|
74072
|
-
sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs()};drawBoxes();drawFigs();
|
|
74674
|
+
sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};let askEl=null;drawBoxes();drawFigs();askEl=asksBox(id,v,()=>ticked,keepScope);
|
|
74073
74675
|
const note=document.createElement("p");note.className="muted";note.textContent="Always built, whatever is ticked: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing. A ticked item that is not built automatically becomes one of your jobs in the account.";
|
|
74074
74676
|
const sv=document.createElement("button");sv.className="cds-btn ghost";sv.textContent="Save what they bought";
|
|
74075
74677
|
const status=document.createElement("p");status.className="muted";status.setAttribute("role","status");
|
|
@@ -74077,14 +74679,15 @@ function scopeBlock(id,state){
|
|
|
74077
74679
|
if(!v.items.some(it=>it.kind==="built"&&ticked.has(it.id))){status.textContent="None of the ticked items is built for this client's industry. Tick at least one item marked Built.";return}
|
|
74078
74680
|
/* Run 8 finding 9: about 5 seconds with nothing showing it was working. */
|
|
74079
74681
|
sv.disabled=true;const was=sv.textContent;sv.textContent="Saving\u2026";status.textContent="Saving what they bought\u2026";
|
|
74080
|
-
const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;const r=await postJSON("/api/scope",body);
|
|
74081
|
-
if(r){unsaved.hidden=true;if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];status.textContent="Saved.";load()}
|
|
74682
|
+
const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;if(asksDraft[id])body.asks=asksClean(asksDraft[id].a);const r=await postJSON("/api/scope",body);
|
|
74683
|
+
if(r){unsaved.hidden=true;delete asksDraft[id];if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];status.textContent="Saved.";load()}
|
|
74082
74684
|
if(sv.isConnected){sv.disabled=false;sv.textContent=was}};
|
|
74083
74685
|
/* Audit 2026-10-28 #9: the always-built pipeline counts as built ("2 bought, 1 built" while it is always built). */
|
|
74084
|
-
|
|
74085
|
-
|
|
74686
|
+
/* rc.3 c6 hole 2: the count follows the labels: what will be built (or was) and what waits on your client's answer. */
|
|
74687
|
+
const tickedItems=v.items.filter(it=>v.ticks.includes(it.id));const nBought=v.ticks.length,nBuilt=tickedItems.filter(it=>/^(Will build|Built|Always built): /.test(it.delivery)).length,nHeld=tickedItems.filter(it=>/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /.test(it.delivery)).length;
|
|
74688
|
+
status.textContent=saved&&v.source==="saved"?"Saved: "+nBought+" of "+v.items.length+" bought. "+nBuilt+(nBuilt===1?" is":" are")+" built automatically"+(nHeld?", "+nHeld+(nHeld===1?" waits":" wait")+" for your client's answer":"")+(saved.package?(pk.find(x=>x.id===saved.package)?", from the "+pk.find(x=>x.id===saved.package).name+" package":", from a package no longer on Your agency"):"")+". The build follows this.":saved?(saved.wholePreset?"Saved: everything in the plan. The build follows this.":"Saved before this list: "+saved.features.length+" features. Save again to record what they bought."):"Not saved yet. Save what they bought before approving.";
|
|
74086
74689
|
const unsaved=mk("span","cds-chip warn","Not saved yet");unsaved.hidden=!scopeDraft[id];
|
|
74087
|
-
sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
|
|
74690
|
+
sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(askEl);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
|
|
74088
74691
|
if(boughtNotice[id]){const n=mk("p","cds-callout warn",boughtNotice[id]);n.setAttribute("role","alert");sc.appendChild(n)}
|
|
74089
74692
|
return sc}
|
|
74090
74693
|
async function rLoad(id){const tok=clientToken();const r=await(await fetch("/api/review?locationId="+id)).json();rData[id]=r;renderFor(tok)}
|
|
@@ -74113,6 +74716,10 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
74113
74716
|
rBusy[id]=false;load()};
|
|
74114
74717
|
if(busy){const p=document.createElement("p");p.className="muted";
|
|
74115
74718
|
p.textContent="Reading their answers and writing the review, usually 1 to 2 minutes. This panel updates itself when it is ready.";d.appendChild(p)}
|
|
74719
|
+
/* rc.3 c6 hole 1: a review written before the answers changed may promise what the build will not make. */
|
|
74720
|
+
if(r&&r.ok&&r.stale&&!busy){const w=mk("p","cds-callout warn","This review was written before your client's answers or what they bought changed, so it may promise what this build will not make. Press Regenerate before you share it; the client document waits for the new one.");w.setAttribute("role","alert");d.appendChild(w)}
|
|
74721
|
+
if(r&&r.ok&&!r.stale&&r.graded!==true&&!busy){const w=mk("p","cds-callout warn","Not checked for over-promises. Regenerate.");w.setAttribute("role","alert");d.appendChild(w)}
|
|
74722
|
+
if(r&&r.ok&&Array.isArray(r.trimmed)&&r.trimmed.length&&!busy)d.appendChild(mk("p","muted","Taken out because they promised more than this build makes: "+r.trimmed.join("; ")+"."));
|
|
74116
74723
|
if(!r||!r.ok){if(!busy){const p=document.createElement("p");if(r&&r.error&&r.error!=="not generated yet"){p.className="cds-callout warn";p.setAttribute("role","alert")}else p.className="muted";
|
|
74117
74724
|
p.textContent=r&&r.error?r.error:"Turn their intake answers into strategy: who they really sell to, whether the offer holds up, what the account is missing, and what to build first.";
|
|
74118
74725
|
d.appendChild(p)}
|
|
@@ -74139,15 +74746,16 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
74139
74746
|
if((r.gaps||[]).length){const s3=document.createElement("div");s3.className="cds-stack";const gh=document.createElement("h4");gh.className="cds-panel-label";gh.textContent="What's missing ("+r.gaps.length+")";s3.appendChild(gh);
|
|
74140
74747
|
const gl=mk("ul","cds-list gaps");
|
|
74141
74748
|
for(const g of r.gaps){const gg=mk("li","gap "+(g.priority||"soon"));
|
|
74142
|
-
const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(
|
|
74749
|
+
const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(String(g.cost||""))));/* rc.5: the fix is never the model's words; what we build is listed below, from the build */gg.appendChild(gt);
|
|
74143
74750
|
const ge=mk("span","end");ge.appendChild(chip(pri==="now"?"warn":"",pri==="now"?"Fix now":pri==="later"?"Later":"Soon"));gg.appendChild(ge);gl.appendChild(gg)}
|
|
74144
74751
|
s3.appendChild(gl);
|
|
74145
74752
|
d.appendChild(s3)}
|
|
74146
|
-
|
|
74753
|
+
/* rc.5 (c6 ruling): What we build is the build's own will-build list and held lines, never the model's prose. */
|
|
74754
|
+
{const pr=r.promises;if(pr&&((pr.willBuild||[]).length||(pr.held||[]).length)){const s4=document.createElement("div");s4.className="cds-stack";s4.innerHTML="<h4 class='cds-panel-label'>What we build</h4>";
|
|
74147
74755
|
const bl=document.createElement("ul");bl.className="cds-list";
|
|
74148
|
-
for(const
|
|
74149
|
-
|
|
74150
|
-
s4.appendChild(bl);d.appendChild(s4)}
|
|
74756
|
+
for(const w of (pr.willBuild||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl",String(w)));bl.appendChild(bb)}
|
|
74757
|
+
for(const h of (pr.held||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl muted",String(h)));bl.appendChild(bb)}
|
|
74758
|
+
s4.appendChild(bl);d.appendChild(s4)}}
|
|
74151
74759
|
if((r.unknowns||[]).length){const s5=document.createElement("div");s5.className="cds-stack";s5.innerHTML="<h4 class='cds-panel-label'>Ask them next</h4>";
|
|
74152
74760
|
const p=document.createElement("p");p.textContent=r.unknowns.join(" \xB7 ");s5.appendChild(p);d.appendChild(s5)}
|
|
74153
74761
|
d.appendChild(scopeBlock(id,state));
|
|
@@ -74256,6 +74864,8 @@ function unsavedTyping(){try{
|
|
|
74256
74864
|
return [...m.querySelectorAll("input,textarea")].some(el=>textField(el)&&!el.disabled&&typedKeys.has(fieldKey(el,m))&&String(el.value||"").trim())}catch(e){return false}}
|
|
74257
74865
|
/* A write in flight arms it too: a link that leaves the page would cut it off. */
|
|
74258
74866
|
window.addEventListener("beforeunload",e=>{if(liveWrites>0||unsavedTyping()){e.preventDefault();e.returnValue=""}});
|
|
74867
|
+
/* Walk W27: the folded Start a new client stays open across redraws once the member opened it. */
|
|
74868
|
+
let ncFoldOpen=false;
|
|
74259
74869
|
function newClientForm(){const f=document.createElement("div");f.className="cds-panel ncform";
|
|
74260
74870
|
f.innerHTML="<h2 class='cds-panel-title'>Create the sub-account</h2><p>GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and paste its key into Command OS.</p>";
|
|
74261
74871
|
const g=document.createElement("div");g.className="ncgrid";
|
|
@@ -75178,7 +75788,26 @@ function capEmpty(title,text){const e=mk("div","cds-empty");e.appendChild(mk("h3
|
|
|
75178
75788
|
/* Capability jobs that read and change nothing (audit 2026-10-28 #22). */
|
|
75179
75789
|
/* Payments makes catalog products from the approved plan's package names, and nothing without them (audit #18). */
|
|
75180
75790
|
function capBuildNote(){return CAP.key==="payments"?"Uses the package names in the approved plan. With none approved, it creates nothing and says so.":""}
|
|
75181
|
-
|
|
75791
|
+
/* Walk W41: what the main button does, in plain words ("Build this for <client>" alone said nothing). */
|
|
75792
|
+
const CAP_ACTION={pipelines:"build the pipelines and stages",calendars:"build the booking calendars",forms:"build the forms",pages:"build the pages and funnels",fields:"add the fields, values and tags",staff:"check the staff and users",phone:"check the phone numbers and A2P",workflows:"build the workflows as drafts",reactivation:"build the reactivation campaign as drafts",templates:"write the email and SMS templates",broadcasts:"draft the broadcast emails","conversation-ai":"build the Conversation AI bot","voice-ai":"build the Voice AI agent",payments:"prepare the three packages in the catalog",memberships:"build the course and membership",reviews:"build the review requests as drafts",blogs:"draft the blog post",checkup:"run the checkup",contacts:"find everyone who has not booked in 90 days",integrations:"check the integrations and webhooks"};
|
|
75793
|
+
function capBuildLabel(c){return "Have Claude "+(CAP_ACTION[CAP.key]||"do this for "+c.name)}
|
|
75794
|
+
/* The main button (top and bottom alike): opens the one before-panel beside itself. */
|
|
75795
|
+
function capMainButton(c,words,busy){const b=mk("button","cds-btn cap-build",capBuildLabel(c));b.type="button";if(busy)b.disabled=true;b.onclick=()=>capAsk(c,words,b);return b}
|
|
75796
|
+
/* The W8 before-panel, one at a time, beside the button pressed; nothing runs until Yes. */
|
|
75797
|
+
function capAsk(c,words,btn){for(const x of document.querySelectorAll(".cap-ask"))x.remove();
|
|
75798
|
+
const ask=mk("div","cds-callout cap-ask");ask.setAttribute("role","group");ask.appendChild(mk("span","ttl","Before Claude starts: this works in "+c.name));
|
|
75799
|
+
ask.appendChild(mk("p","",words));const before=String(CAP.before||"").split("{client}").join(c.name);if(before)ask.appendChild(mk("p","",before));
|
|
75800
|
+
const row=mk("div","cds-actions");const yes=mk("button","cds-btn","Yes, "+capBuildLabel(c).charAt(0).toLowerCase()+capBuildLabel(c).slice(1));yes.type="button";const no=mk("button","cds-btn ghost","Cancel");no.type="button";
|
|
75801
|
+
no.onclick=()=>ask.remove();yes.onclick=async()=>{yes.disabled=no.disabled=true;yes.textContent="Starting\u2026";await capStart(c,ask)};row.appendChild(yes);row.appendChild(no);ask.appendChild(row);
|
|
75802
|
+
const host=btn.parentNode;host.insertBefore(ask,btn.nextSibling);if(ask.scrollIntoView)try{ask.scrollIntoView({block:"nearest"})}catch(e){}}
|
|
75803
|
+
async function capStart(c,ask){const tok=clientToken();
|
|
75804
|
+
const shownReads=capReadsFor===c.locationId&&capReads&&capReads.ok&&Array.isArray(capReads.fingerprints)?capReads.fingerprints:null;
|
|
75805
|
+
const r=await(await fetch("/api/run-capability",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,key:CAP.key,confirmed:true,readsShown:shownReads})})).json();
|
|
75806
|
+
ask.remove();
|
|
75807
|
+
/* Out of date (safety release): the panel shows what the account has now, and the refusal says what changed. */
|
|
75808
|
+
if(r.stale&&Array.isArray(r.reads)){if(!stillFor(tok)){alertFor(c.locationId,r.error);return}capReadsFor=c.locationId;capReads={ok:true,key:CAP.key,locationId:c.locationId,reads:r.reads,fingerprints:r.fingerprints};render(lastState,lastRun);alertFor(c.locationId,r.error);return}
|
|
75809
|
+
if(!r.ok){alertFor(c.locationId,r.error||"Could not start");return}
|
|
75810
|
+
if(!stillFor(tok))return;capRun={locationId:c.locationId};pollCap()}
|
|
75182
75811
|
/* What one read counts, in the owner's words and the right number: "1 bot", "2 bots". */
|
|
75183
75812
|
function readLabel(label,n){if(Number(n)!==1)return label;const r=((CAP&&CAP.reads)||[]).find(x=>x.label===label);return (r&&r.one)||String(label).replace(/s$/,"")}
|
|
75184
75813
|
/* One row per thing the read found: its name, and the sub line the read gives (amount, status and date; N stages). */
|
|
@@ -75201,22 +75830,17 @@ function renderCapability(m,state,run){
|
|
|
75201
75830
|
sel.setAttribute("data-switch","");sel.onchange=()=>goClient(sel.value);pick.appendChild(sel);end.appendChild(pick);
|
|
75202
75831
|
const hasBuild=CAP.key!=="intake"&&CAP.key!=="handoff"&&!CAP.noBuild;
|
|
75203
75832
|
let bb=null;
|
|
75204
|
-
|
|
75205
|
-
|
|
75206
|
-
|
|
75207
|
-
|
|
75208
|
-
const shownReads=capReadsFor===c.locationId&&capReads&&capReads.ok&&Array.isArray(capReads.fingerprints)?capReads.fingerprints:null;
|
|
75209
|
-
const r=await(await fetch("/api/run-capability",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,key:CAP.key,confirmed:true,readsShown:shownReads})})).json();
|
|
75210
|
-
/* Out of date (safety release): the panel shows what the account has now, and the refusal says what changed. */
|
|
75211
|
-
if(r.stale&&Array.isArray(r.reads)){if(!stillFor(tok)){alertFor(c.locationId,r.error);return}capReadsFor=c.locationId;capReads={ok:true,key:CAP.key,locationId:c.locationId,reads:r.reads,fingerprints:r.fingerprints};render(lastState,lastRun);alertFor(c.locationId,r.error);return}
|
|
75212
|
-
if(!r.ok){alertFor(c.locationId,r.error||"Could not start");bb.disabled=false;bb.textContent=capBuildLabel(c);return}
|
|
75213
|
-
if(!stillFor(tok))return;capRun={locationId:c.locationId};pollCap()};
|
|
75214
|
-
end.appendChild(bb);if(capBuildNote())end.appendChild(mk("p","muted cap-note",capBuildNote()))}
|
|
75833
|
+
/* Walk W41 (2026-10-29, Jerry: "it is not clear at all"): the page's main action is the plain-worded build through
|
|
75834
|
+
the member's own Claude, at the top here and again at the bottom after Ready to answer; both open the one W8
|
|
75835
|
+
before-panel beside the button pressed (never a browser dialog). */
|
|
75836
|
+
if(hasBuild){bb=capMainButton(c,words,busy);end.appendChild(bb);if(capBuildNote())end.appendChild(mk("p","muted cap-note",capBuildNote()))}
|
|
75215
75837
|
/* Walk 2026-10-28 W9: the client's first page (where Your clients leads) has its build as a button. */
|
|
75216
75838
|
if(CAP.key==="intake"){const go=mk("a","cds-btn cap-build-account","Build this client's account");end.appendChild(asClientLink(go,"/new",c.locationId))}
|
|
75217
75839
|
m.appendChild(screenHead(CAP.label,CAP.what,CAP.groupLabel,end));
|
|
75218
75840
|
const sc=document.getElementById("side-client");if(sc)sc.textContent=c.name;
|
|
75219
|
-
|
|
75841
|
+
/* W43 (Jerry's watched walk, 2026-10-29): he saw no Build button on the page Your clients leads to. The header carries
|
|
75842
|
+
one; this puts the next step at the top of the page itself, where nobody can miss it. */
|
|
75843
|
+
if(CAP.key==="intake"){const nx=mk("div","cds-callout cap-next-build");nx.appendChild(mk("span","ttl","Next step: build this client's account"));nx.appendChild(mk("span","","Answers, approval and the build itself are on the build page."));const go=mk("a","cds-btn cap-build-account-main","Build this client's account");nx.appendChild(asClientLink(go,"/new",c.locationId));m.appendChild(nx);renderIntakeCapability(m,c);return}
|
|
75220
75844
|
const built=capBuiltItems(view,CAP.key);
|
|
75221
75845
|
const toCheck=built.filter(x=>x.status==="uncertain");
|
|
75222
75846
|
const health=HEALTH_PAGES.includes(CAP.key);
|
|
@@ -75257,7 +75881,25 @@ function renderCapability(m,state,run){
|
|
|
75257
75881
|
if(health)m.appendChild(capOwedPanel(owed,(state.jobCounts||{})[c.locationId],((view||{}).handoff||{}).lines))}
|
|
75258
75882
|
/* What this page reads live: one list for one read. With several reads, a read of 3 or fewer shows its rows under its own label,
|
|
75259
75883
|
and a longer one folds under a closed "See the N \u2026". A count read is one row: the number. */
|
|
75884
|
+
/* Sign-off and handoff (command-center 2026-10-29): read from this client's own build record, the same one the build
|
|
75885
|
+
steps show (F2): what is Built, what waits on the client (the held lines), and the jobs still open. Never GoHighLevel's
|
|
75886
|
+
words for it, and never a claim the record does not hold. */
|
|
75887
|
+
const HELD_RE=/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
|
|
75888
|
+
function capHandoffRecord(card,c){
|
|
75889
|
+
const v=((lastState&&lastState.boughtViews)||{})[c.locationId];
|
|
75890
|
+
if(!v){card.appendChild(mk("p","muted","Nothing is on record for this client yet."));return}
|
|
75891
|
+
const ticked=new Set(v.ticks||[]);const items=(v.items||[]).filter(i=>ticked.has(i.id));
|
|
75892
|
+
const built=items.filter(i=>/^Built: /.test(i.delivery)).map(i=>i.delivery.replace(/^Built: /,""));
|
|
75893
|
+
const waiting=items.filter(i=>HELD_RE.test(i.delivery)).map(i=>i.delivery);
|
|
75894
|
+
const open=(((lastState&&lastState.manualTasks)||{})[c.locationId]||[]).filter(t=>!t.claimed&&!HELD_RE.test(String(t.text))).map(t=>String(t.text));
|
|
75895
|
+
const sec=(title,list,none)=>{const b=mk("div","cap-read");b.appendChild(mk("b","",title));if(!list.length)b.appendChild(mk("p","muted",none));else{const u=mk("ul","");for(const x of list.slice(0,8))u.appendChild(mk("li","",x));if(list.length>8)u.appendChild(mk("li","muted","and "+(list.length-8)+" more"));b.appendChild(u)}card.appendChild(b)};
|
|
75896
|
+
card.appendChild(mk("p","muted","From this client's build record on this computer, the same record the build steps show."));
|
|
75897
|
+
sec("Built",built,"Nothing is recorded as built yet.");
|
|
75898
|
+
sec("Waiting on your client",waiting,"Nothing is waiting on your client.");
|
|
75899
|
+
sec("Still open",open,"No open jobs.");
|
|
75900
|
+
}
|
|
75260
75901
|
function capReadsInto(card,c,hasBuild,foldAll){
|
|
75902
|
+
if(CAP.key==="handoff"){capHandoffRecord(card,c);return}
|
|
75261
75903
|
if(!CAP.reads.length){card.appendChild(capEmpty("This page does not read the account yet","Say the words below to Claude, and it reads what this account has for this job, live."));return}
|
|
75262
75904
|
if(capReadsFor!==c.locationId){card.appendChild(mk("p","muted","Reading this account\u2026"));loadCapReads(c.locationId);return}
|
|
75263
75905
|
if(!capReads){card.appendChild(mk("p","muted","Reading this account\u2026"));return}
|
|
@@ -75267,7 +75909,7 @@ function capReadsInto(card,c,hasBuild,foldAll){
|
|
|
75267
75909
|
const stack=mk("div","cds-stack");const several=foldAll||capReads.reads.length>1;
|
|
75268
75910
|
for(const r of capReads.reads){const block=mk("div","cap-read");
|
|
75269
75911
|
if(r.status!=="ok"){const w=mk("div","cds-callout warn");w.appendChild(mk("span","ttl",r.countOnly?(capFirst(readLabel(r.label,1))+" count not read yet"):"Could not read the "+r.label));w.appendChild(mk("span","",String(r.reason||"GoHighLevel did not answer.").replace(/^could not read: /,"")));block.appendChild(w)}
|
|
75270
|
-
else if(!r.count)block.appendChild(capEmpty("No "+r.label+" yet",hasBuild?"Use
|
|
75912
|
+
else if(!r.count)block.appendChild(capEmpty("No "+r.label+" yet",hasBuild?"Use "+capBuildLabel(c)+" above, or ask Claude yourself with the words below.":"Ask Claude yourself with the words below."));
|
|
75271
75913
|
else if(r.countOnly){const ul=mk("ul","cds-list");ul.appendChild(capRow(Number(r.count).toLocaleString("en-US")+" "+readLabel(r.label,r.count),"Counted by GoHighLevel."));block.appendChild(ul)}
|
|
75272
75914
|
else if(!(r.names||[]).length&&!(r.items||[]).length)block.appendChild(mk("p","muted",r.count+" "+readLabel(r.label,r.count)+" found; GoHighLevel returned no names for them."));
|
|
75273
75915
|
else if(several&&!foldAll&&r.count<=3){block.appendChild(mk("div","cds-panel-label",r.count+" "+readLabel(r.label,r.count)));const ul=mk("ul","cds-list");readRows(r).forEach(x=>ul.appendChild(x));block.appendChild(ul)}
|
|
@@ -75293,21 +75935,44 @@ function capCheckupPanel(last,bb){const p=panel("Last checkup",last?"What the la
|
|
|
75293
75935
|
row.appendChild(mk("span","",last.summary||(last.ok?"The checkup finished.":(last.error||"The run did not finish."))));p.appendChild(row);
|
|
75294
75936
|
if(iss.length)p.appendChild(mk("p","muted","Each one is listed under Still owed, below."));
|
|
75295
75937
|
return p}
|
|
75938
|
+
/* Walk W42: Ready to answer, from the account as read now (the page's live reads) and the intake. */
|
|
75939
|
+
function capReadiness(c,state,last){const reads=capReadsFor===c.locationId&&capReads&&capReads.ok?capReads.reads:null;
|
|
75940
|
+
const fromRead=(label,none)=>{if(!reads)return {wait:true,text:"Reading the account\u2026"};const r=reads.find(x=>x.label===label);if(!r)return {wait:true,text:"Reading the account\u2026"};
|
|
75941
|
+
if(r.status!=="ok")return {text:"Could not read: "+(r.reason||"GoHighLevel did not answer")};
|
|
75942
|
+
const names=(r.names||[]).filter(Boolean);if(!r.count)return {text:none};
|
|
75943
|
+
return {ok:true,text:names.length?names.slice(0,2).join(", ")+(r.count>2?", and "+(r.count-2)+" more":""):r.count+" in this account"}};
|
|
75944
|
+
const ans=Object.assign({},((state.intakeDrafts||{})[c.locationId]||{}).answers||{},((state.intakeInputs||{})[c.locationId]||{}).answers||{},((state.intakeCompleted||{})[c.locationId]||{}).answers||{});
|
|
75945
|
+
const raw=ans.website??ans.Website;const site=String(raw&&typeof raw==="object"?(raw.value??""):(raw??"")).trim();
|
|
75946
|
+
const said=last&&last.readiness&&last.readiness["Test conversation"];
|
|
75947
|
+
return {Website:site?{ok:true,text:site}:{text:"No website in the intake yet. Add it on step 5, Intake."},
|
|
75948
|
+
"Booking calendar":fromRead("booking calendars","No booking calendar in this account yet."),
|
|
75949
|
+
"Phone number":fromRead("phone numbers","No phone number in this account yet."),
|
|
75950
|
+
"Test conversation":said?{ok:true,text:said}:{text:"Yours to do: have one test conversation with it."}}}
|
|
75296
75951
|
/* Do it now: the words, the ways to run them, and what the last run left. */
|
|
75297
75952
|
function capDoPanel(c,state,run,words,hasBuild,busy,last){
|
|
75298
|
-
const doc=panel("Do it now","
|
|
75953
|
+
const doc=panel("Do it now",hasBuild?capBuildLabel(c):"Optional help");doc.classList.add("cap-do");
|
|
75954
|
+
/* Walk W41: asking Claude yourself is the optional path, folded to one line; on a page with no build it is the only
|
|
75955
|
+
action, open, and still labelled as optional help. */
|
|
75956
|
+
const fold=mk("details","cds-details cap-words-fold");if(!hasBuild)fold.open=true;
|
|
75957
|
+
fold.appendChild(mk("summary","",hasBuild?"Prefer to ask Claude yourself? Copy the words":"Optional help: ask Claude yourself. Copy the words"));
|
|
75299
75958
|
const q=mk("div","cds-callout say cap-words");q.appendChild(mk("span","",words));
|
|
75300
75959
|
const acts=mk("div","cds-actions");
|
|
75301
75960
|
const cp=mk("button","cds-btn ghost","Copy the words");cp.onclick=async()=>{try{await navigator.clipboard.writeText(words);cp.textContent="Copied"}catch(e){cp.textContent="Select and copy the text above"}setTimeout(()=>{cp.textContent="Copy the words"},2000)};acts.appendChild(cp);
|
|
75302
75961
|
if(CAP.hub){const h=mk("a","cds-btn ghost","Step by step in the hub");h.href=HUB+CAP.hub;h.target="_blank";h.rel="noopener";acts.appendChild(h)}
|
|
75303
|
-
q.appendChild(acts);
|
|
75962
|
+
q.appendChild(acts);fold.appendChild(q);
|
|
75304
75963
|
const drafts=DRAFT_PAGES.includes(CAP.key);
|
|
75305
75964
|
doc.appendChild(mk("p","muted cap-note",CAP.noBuild?"This one messages a real person, so it has no Build button: paste the words into your own Claude and review the complete draft before approving any send or publication.":!hasBuild?"Paste the words into your own Claude and watch it work.":busy?"Another run is in progress, and Build this wakes up when it finishes.":drafts?"This runs through your own Claude account, leaves what it makes as drafts, and sends, posts or deletes nothing.":"This runs through your own Claude account and sends, posts or deletes nothing."));
|
|
75306
75965
|
if(hasBuild&&!busy){const d=mk("details","cds-details");d.appendChild(mk("summary","","Details"));d.appendChild(mk("p","","This runs through your own Claude account, with only the tools this job needs: the words above and what it reads from this account go to Claude, and nowhere else. "+(drafts?"Workflows and messages are left as drafts for your review; nothing sends until you turn it on, and activation is approved separately. ":"")+"Nothing is sent, posted or deleted; every id is read from this account first. Or paste the words into your own Claude and watch it work."));doc.appendChild(d)}
|
|
75307
75966
|
if(["conversation-ai","voice-ai"].includes(CAP.key)){const block=mk("div","cap-sub");block.appendChild(mk("div","cds-panel-label","Ready to answer"));
|
|
75308
|
-
|
|
75309
|
-
|
|
75310
|
-
|
|
75967
|
+
/* Walk W42 (2026-10-29): the checks said "Not verified" for a calendar and number Demo-on-Demand has. Each now says
|
|
75968
|
+
what the account holds, read live like Verify (the page's own reads), or exactly what is missing. The test
|
|
75969
|
+
conversation stays the member's to do. */
|
|
75970
|
+
const checks=["Website","Booking calendar","Phone number","Test conversation"];const found=capReadiness(c,state,last);const nOk=checks.filter(l=>found[l]&&found[l].ok).length;
|
|
75971
|
+
block.appendChild(mk("p","cap-ready-sum",nOk===checks.length?"All "+checks.length+" are in place.":nOk+" of the "+checks.length+" are in place."));
|
|
75972
|
+
const dl=mk("dl","cds-kv cap-ready");for(const l of checks){dl.appendChild(mk("dt","",l));const dd=mk("dd");const f=found[l];if(f.ok)dd.textContent=f.text;else{dd.appendChild(chip(f.wait?"":"warn",f.wait?"Reading":"Missing"));dd.appendChild(document.createTextNode(" "+f.text))}dl.appendChild(dd)}block.appendChild(dl);doc.appendChild(block)}
|
|
75973
|
+
/* Walk W41: the main action again at the bottom, after what it checks; then the optional copy-the-words fold. */
|
|
75974
|
+
if(hasBuild&&CAP.key!=="checkup"){const row=mk("div","cds-actions cap-main-bottom");row.appendChild(capMainButton(c,words,busy));doc.appendChild(row)}
|
|
75975
|
+
doc.appendChild(fold);
|
|
75311
75976
|
/* The run, live */
|
|
75312
75977
|
if(run&&run.capability===CAP.key&&run.locationId===c.locationId){const d=mk("details","cds-details cap-run");d.open=Boolean(run.running);d.appendChild(mk("summary","","What Claude is doing"));
|
|
75313
75978
|
const pre=mk("pre","cds-callout cap-log",(run.lines||[]).slice(-40).join(String.fromCharCode(10))||"Starting\u2026");d.appendChild(pre);doc.appendChild(d);
|
|
@@ -75325,8 +75990,11 @@ function capDoPanel(c,state,run,words,hasBuild,busy,last){
|
|
|
75325
75990
|
checked" while step 10 said "GoHighLevel confirms 81 items. You confirmed 6 yourself."). One sentence, every surface. */
|
|
75326
75991
|
/* Walk 2026-10-28 W18: "confirms 66 \u2026 covers 75" left nine items unexplained. The rest are named by state. */
|
|
75327
75992
|
function handoffCountLine(lines,counts){const done=lines.filter(l=>l.state==="CHECKED").length,mine=(counts&&counts.ticked)||0;
|
|
75328
|
-
const waiting=lines.filter(l=>l.state==="WAITING").length
|
|
75329
|
-
|
|
75993
|
+
const waiting=lines.filter(l=>l.state==="WAITING").length;
|
|
75994
|
+
/* Walk W33: the draft automations are built; they are off until the member switches them on, not unconfirmed. */
|
|
75995
|
+
const drafts=lines.filter(l=>l.state!=="CHECKED"&&l.state!=="WAITING"&&/draft/i.test(String(l.reason||""))).length;
|
|
75996
|
+
const unread=lines.length-done-waiting-drafts;
|
|
75997
|
+
const rest=[drafts?"the "+drafts+" "+plural(drafts,"automation")+" "+(drafts===1?"is":"are")+" built and switched off until you turn "+(drafts===1?"it":"them")+" on":"",unread?unread+" "+(unread===1?"was":"were")+" built but GoHighLevel could not confirm "+(unread===1?"it":"them")+" yet":"",waiting?waiting+" "+(waiting===1?"waits":"wait")+" on a step outside the account, such as an approval":""].filter(Boolean);
|
|
75330
75998
|
return "GoHighLevel confirms "+done+" "+plural(done,"item")+". You confirmed "+mine+" yourself. The handoff document covers "+lines.length+" "+plural(lines.length,"item")+"."+(rest.length?" Of the other "+(lines.length-done)+", "+rest.join(", and ")+".":"")}
|
|
75331
75999
|
/* The Health pages' own list: what is verified and what is still owed. */
|
|
75332
76000
|
/* Counts the same jobs, the same way, as step 11 (/api/state jobCounts), and the handoff document separately in
|
|
@@ -75499,12 +76167,15 @@ function renderPage(state,run){if(state.roster)mergeRoster(state.roster);syncRev
|
|
|
75499
76167
|
if(SCREEN==="new"){m.classList.add("ncflow");const head=screenHead(HEADS.new.title,HEADS.new.sub,HEADS.new.kicker);head.querySelector(".cds-page-sub").classList.add("nc-lead");m.appendChild(head);
|
|
75500
76168
|
const choices=ROSTER.filter(c=>["client","sandbox"].includes(typeOf(state,c.locationId)));
|
|
75501
76169
|
const c0=ROSTER.find(c=>c.locationId===newClientId);
|
|
75502
|
-
{const wc=bsWaitingCard(state,run,c0?c0.locationId:"");if(wc&&(!c0||bsWhere(c0,state,run).step<3))m.appendChild(wc)}
|
|
76170
|
+
let waiting=null;{const wc=bsWaitingCard(state,run,c0?c0.locationId:"");if(wc&&(!c0||bsWhere(c0,state,run).step<3)){m.appendChild(wc);waiting=wc}}
|
|
75503
76171
|
if(!c0){
|
|
75504
|
-
|
|
75505
|
-
|
|
75506
|
-
|
|
75507
|
-
|
|
76172
|
+
/* Walk W27 (2026-10-29): with a build in progress the page led with an open Create form, and Jerry nearly filled it
|
|
76173
|
+
in. The builds in progress come first; starting a new client is folded under them. */
|
|
76174
|
+
let host=m;if(waiting){const more=document.createElement("details");more.className="cds-more nc-new-fold";more.open=ncCreating||ncFoldOpen;more.addEventListener("toggle",()=>{ncFoldOpen=more.open});const s=document.createElement("summary");s.textContent="Start a new client account instead";more.appendChild(s);m.appendChild(more);host=more}
|
|
76175
|
+
if(canCreateSubAccounts===null)host.appendChild(agencyPlanQuestion());
|
|
76176
|
+
else if(canCreateSubAccounts){host.appendChild(newClientForm());const fold=document.createElement("details");fold.className="cds-more";const title=document.createElement("summary");title.textContent="Register a sub-account you created in GoHighLevel";fold.appendChild(title);fold.appendChild(pasteKeyCard(true));host.appendChild(fold)}
|
|
76177
|
+
else{host.appendChild(pasteKeyCard());const quiet=document.createElement("p");quiet.className="muted";
|
|
76178
|
+
quiet.innerHTML="Your agency is on GoHighLevel's Agency Pro plan? <a class='cds-link' href='/agency#canCreateSubAccounts'>Turn on creating sub-accounts here</a>.";host.appendChild(quiet)}}
|
|
75508
76179
|
if(planNotice){const note=document.createElement("p");note.className="cds-callout warn";note.setAttribute("role","alert");note.textContent=planNotice;m.appendChild(note)}
|
|
75509
76180
|
if(c0&®isterNotice){const note=document.createElement("p");note.className="cds-callout info";note.setAttribute("role","status");note.textContent=registerNotice;m.appendChild(note)}
|
|
75510
76181
|
const existing=document.createElement("a");existing.href="#existing-clients";existing.className="cds-link";existing.textContent=c0?"Work on a different client":"Already registered? Pick the client";
|
|
@@ -75914,6 +76585,14 @@ function createBriefRoutes(seatToken, port, plan, registry2, fromCache, viewer =
|
|
|
75914
76585
|
return true;
|
|
75915
76586
|
}
|
|
75916
76587
|
if (url === "/brief") {
|
|
76588
|
+
if (plan === "pro" && !new URL(req.url || "/", "http://x").searchParams.get("client")) {
|
|
76589
|
+
const only = registry2?.listLocations() ?? [];
|
|
76590
|
+
if (only.length === 1) {
|
|
76591
|
+
res.writeHead(302, { Location: `/brief?client=${encodeURIComponent(only[0].locationId)}` });
|
|
76592
|
+
res.end();
|
|
76593
|
+
return true;
|
|
76594
|
+
}
|
|
76595
|
+
}
|
|
75917
76596
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
75918
76597
|
const picked = plan === "pro" ? new URL(req.url || "/", "http://x").searchParams.get("client") ?? "" : "";
|
|
75919
76598
|
const clientName = picked ? registry2?.listLocations().find((l) => l.locationId === picked)?.name : void 0;
|
|
@@ -76290,7 +76969,11 @@ async function runDashboard(argv) {
|
|
|
76290
76969
|
}
|
|
76291
76970
|
return { label: r.label, status: "ok", count: total2, names: [], items: [], countOnly: true };
|
|
76292
76971
|
}
|
|
76293
|
-
const
|
|
76972
|
+
const all = findArray(raw, r.key);
|
|
76973
|
+
const items = all && r.activeOnly ? all.filter((i) => {
|
|
76974
|
+
const o = Object(i);
|
|
76975
|
+
return o.isActive !== false && o.deleted !== true;
|
|
76976
|
+
}) : all;
|
|
76294
76977
|
if (!items) {
|
|
76295
76978
|
logQuietly(`capability read ${cap2.key}/${r.label}: unrecognised shape from ${p}`);
|
|
76296
76979
|
return { label: r.label, status: "unavailable", count: 0, names: [], reason: "could not read: the answer had a shape this page does not know" };
|
|
@@ -76413,7 +77096,7 @@ async function runDashboard(argv) {
|
|
|
76413
77096
|
}
|
|
76414
77097
|
if (req.method === "GET" && url === "/shopper") {
|
|
76415
77098
|
const accounts = currentRoster().filter((r) => readableAccount(r.locationId)).map((r) => ({ locationId: r.locationId, name: r.name })).sort((a, b) => a.name.localeCompare(b.name));
|
|
76416
|
-
const client = accounts.find((a) => a.locationId === (u.searchParams.get("client") ?? ""));
|
|
77099
|
+
const client = accounts.find((a) => a.locationId === (u.searchParams.get("client") ?? "")) ?? (accounts.length === 1 && !u.searchParams.get("client") ? accounts[0] : void 0);
|
|
76417
77100
|
const run = client ? latestRun(client.locationId) : void 0;
|
|
76418
77101
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
76419
77102
|
res.end(shopperPageHtml({ accounts, client, run, ownerEmail: readAgencyProfile().sender?.replyTo, now: Date.now() }, mayServeSeat(req, port) ? seatToken : "", SEAT_TOKEN_HEADER));
|
|
@@ -76609,6 +77292,26 @@ async function runDashboard(argv) {
|
|
|
76609
77292
|
}
|
|
76610
77293
|
const ownerEmail = typeof input.ownerEmail === "string" ? input.ownerEmail : "";
|
|
76611
77294
|
const testPhone = typeof input.testPhone === "string" && input.testPhone.trim() ? input.testPhone.trim() : void 0;
|
|
77295
|
+
const draftsTicked = liveWorkflows.filter((w) => workflowIds.includes(w.id) && (w.status ?? "").toLowerCase() === "draft");
|
|
77296
|
+
const draftRefusals = input.fastForward === true ? [] : draftsTicked.map((w) => `"${w.name}" is a draft, and a draft never runs: tick Fast-forward to test it.`);
|
|
77297
|
+
if (input.preview === true) {
|
|
77298
|
+
let refusals = draftRefusals;
|
|
77299
|
+
if (input.fastForward === true) {
|
|
77300
|
+
const rawHosts = Array.isArray(input.allowedHosts) ? input.allowedHosts.filter((h) => typeof h === "string") : [];
|
|
77301
|
+
try {
|
|
77302
|
+
const b = workflowReadConnection(registry2, c);
|
|
77303
|
+
refusals = await fastForwardRefusals({ client: c, admin: builderAdmin(b, publishCheckFor(c, b, { locationId: locationId2 })), locationId: locationId2, ownerEmail, alertUserId: typeof input.alertUserId === "string" && input.alertUserId ? input.alertUserId : void 0, workflowIds, allowedHosts: rawHosts.map(normalizeHost).filter(Boolean) });
|
|
77304
|
+
} catch (e) {
|
|
77305
|
+
refusals = [e instanceof Error ? e.message.split("\n")[0] : "The workflows could not be checked for fast-forward."];
|
|
77306
|
+
}
|
|
77307
|
+
}
|
|
77308
|
+
shopperJson(res, 200, { ok: true, preview: true, written: false, refusals });
|
|
77309
|
+
return;
|
|
77310
|
+
}
|
|
77311
|
+
if (draftRefusals.length) {
|
|
77312
|
+
shopperJson(res, 200, { ok: false, written: false, error: draftRefusals.join(" ") });
|
|
77313
|
+
return;
|
|
77314
|
+
}
|
|
76612
77315
|
if (input.fastForward === true) {
|
|
76613
77316
|
const rawHosts = Array.isArray(input.allowedHosts) ? input.allowedHosts.filter((h) => typeof h === "string") : typeof input.allowedHosts === "string" ? input.allowedHosts.split(/[\s,]+/) : [];
|
|
76614
77317
|
const allowedHosts = rawHosts.map(normalizeHost).filter(Boolean);
|
|
@@ -76743,10 +77446,11 @@ async function runDashboard(argv) {
|
|
|
76743
77446
|
const hit = await pickIntakeContact(c, locationId2, byId2, snap.contactId, INTAKE_FORM_NAME2);
|
|
76744
77447
|
const completed = st0.intakeCompleted?.[locationId2]?.answers;
|
|
76745
77448
|
const answers = Object.fromEntries(Object.entries(completed ?? normalizedIntake({ answers: hit?.answers ?? {}, answeredAt: snap.answeredAt }, st0.intakeDrafts?.[locationId2])).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v]));
|
|
76746
|
-
|
|
77449
|
+
const review = await generateReview(
|
|
76747
77450
|
(prompt) => callClaudeRecorded(claudeBin(), ["-p", prompt, "--allowedTools", "", "--disallowedTools", `Bash,Write,Edit,WebFetch,WebSearch,Task,Read,Glob,Grep,${MCP_DENY}`, "--max-turns", "6", ...NO_MCP_ARGS], { locationId: locationId2, stage: 1, kind: "review" }),
|
|
76748
|
-
{ clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {} }
|
|
77451
|
+
{ clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {}, promises: reviewPromisesFor(st0, locationId2, locationId2) }
|
|
76749
77452
|
);
|
|
77453
|
+
return review.ok ? { ...review, promisesHash: contentHash2(review.promises ?? reviewPromisesFor(st0, locationId2, locationId2)) } : review;
|
|
76750
77454
|
};
|
|
76751
77455
|
const currentRoster = () => {
|
|
76752
77456
|
const registered = registry2.listLocations();
|
|
@@ -76989,12 +77693,13 @@ async function runDashboard(argv) {
|
|
|
76989
77693
|
}
|
|
76990
77694
|
if (req.method === "POST" && url === "/api/run-capability") {
|
|
76991
77695
|
readBody(req, res, 1e4, async (body) => {
|
|
77696
|
+
let releaseStart;
|
|
76992
77697
|
try {
|
|
76993
77698
|
const { locationId: locationId2, key: key2, confirmed, readsShown } = JSON.parse(body || "{}");
|
|
76994
77699
|
if (confirmed !== true) throw new Error("Review this action and approve it on the client page first.");
|
|
76995
77700
|
const cap2 = capabilityByKey(String(key2 ?? ""));
|
|
76996
77701
|
if (!cap2) throw new Error("no such capability");
|
|
76997
|
-
|
|
77702
|
+
releaseStart = claimRunStart();
|
|
76998
77703
|
const client = currentRoster().find((r) => r.locationId === locationId2);
|
|
76999
77704
|
if (!client) throw new Error("that account is not on the board");
|
|
77000
77705
|
if (cap2.reads.length) {
|
|
@@ -77047,6 +77752,8 @@ async function runDashboard(argv) {
|
|
|
77047
77752
|
} catch (e) {
|
|
77048
77753
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
77049
77754
|
res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : String(e) }));
|
|
77755
|
+
} finally {
|
|
77756
|
+
releaseStart?.();
|
|
77050
77757
|
}
|
|
77051
77758
|
});
|
|
77052
77759
|
return;
|
|
@@ -77397,7 +78104,12 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77397
78104
|
const assessmentFigures = {};
|
|
77398
78105
|
for (const loc of Object.keys(st.assessmentAttachments ?? {})) assessmentFigures[loc] = assessmentFiguresFor(st, loc);
|
|
77399
78106
|
const scopeOffered = Object.fromEntries(rosterNow.map((c) => [c.locationId, offeredFeaturesFor(st, c.locationId)]));
|
|
77400
|
-
const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId,
|
|
78107
|
+
const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId, {
|
|
78108
|
+
...boughtViewFor(st, c.locationId, scopeOffered[c.locationId], boughtWordingFor(st, c.locationId), buildStatusFor(st, c.locationId, c.locationId)),
|
|
78109
|
+
// Ask first (2026-10-29): the saved answers and what the client's own form said, for the questions box.
|
|
78110
|
+
asks: st.asks?.[c.locationId] ?? {},
|
|
78111
|
+
intakeAsks: intakeAskFacts(st.intakeCompleted?.[c.locationId]?.answers ?? st.intakeInputs?.[c.locationId]?.answers)
|
|
78112
|
+
}]));
|
|
77401
78113
|
const buildViews = Object.fromEntries(rosterNow.map((c) => {
|
|
77402
78114
|
const saved = readSavedPlan(c.locationId);
|
|
77403
78115
|
const approval = st.planApprovals?.[c.locationId];
|
|
@@ -77412,6 +78124,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77412
78124
|
handoff: readHandoffSnapshot(c.locationId),
|
|
77413
78125
|
publication: readStatusRecord(c.locationId),
|
|
77414
78126
|
delivery: st.handoffDeliveries?.[c.locationId],
|
|
78127
|
+
// Walk W29: everything this build made across its runs (the journal), not only the last run's share.
|
|
78128
|
+
made: saved?.planId ? madeByBuild(c.locationId, saved.planId) : [],
|
|
77415
78129
|
// The two pages exist only after Verify (cockpit audit 2026-10-28 #36), and go with an undo (#43).
|
|
77416
78130
|
prepared: fs53.existsSync(localCopyPath(c.locationId)),
|
|
77417
78131
|
detailPrepared: fs53.existsSync(detailCopyPath(c.locationId)),
|
|
@@ -77428,6 +78142,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77428
78142
|
approvedAt: approval?.at,
|
|
77429
78143
|
stale: stageFreshness(saved?.inputHash, saved, approval, st.outcomes?.[`${rk}:3`], st.outcomes?.[`${rk}:4`], st.outcomes?.[`${rk}:1`]),
|
|
77430
78144
|
built: saved?.built ?? [],
|
|
78145
|
+
made: saved?.planId ? madeByBuild(profile.locationId, saved.planId) : [],
|
|
77431
78146
|
repairs: [],
|
|
77432
78147
|
handoff: null,
|
|
77433
78148
|
publication: readStatusRecord(profile.locationId),
|
|
@@ -77471,6 +78186,9 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77471
78186
|
}).filter((t) => t.text)];
|
|
77472
78187
|
})),
|
|
77473
78188
|
// Walk W15: a job the check read as done leaves the list; step 11 says what it confirmed, by name.
|
|
78189
|
+
// Walk W26 (2026-10-29): built objects the latest check found gone from the account, per client, so a signed-off
|
|
78190
|
+
// build can say so and offer Build again.
|
|
78191
|
+
builtMissing: Object.fromEntries(Object.entries(st.jobEvidence ?? {}).map(([id2, ev]) => [id2, (ev?.readback ?? []).filter((r) => r.status === "missing").length]).filter(([, n]) => n > 0)),
|
|
77474
78192
|
confirmedJobs: Object.fromEntries(Object.keys(st.manualTasks ?? {}).map((id2) => {
|
|
77475
78193
|
const shape = planShape(readSavedPlan(id2)?.plan);
|
|
77476
78194
|
return [id2, confirmedHandoffLines(mergeHandoffDefs(st.jobHandoffs?.[id2] ?? [], shape.handoffs), st.jobEvidence?.[id2]?.facts, shape.calendars)];
|
|
@@ -77632,9 +78350,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77632
78350
|
}
|
|
77633
78351
|
if (req.method === "POST" && url === "/api/run") {
|
|
77634
78352
|
readBody(req, res, 1e4, async (body) => {
|
|
78353
|
+
let releaseStart;
|
|
77635
78354
|
try {
|
|
77636
78355
|
const { locationId: locationId2, stage, intakeConfirmed, clientKey, intakeShown, inputHashShown, planHashShown } = JSON.parse(body);
|
|
77637
|
-
|
|
78356
|
+
releaseStart = claimRunStart();
|
|
77638
78357
|
const who2 = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
|
|
77639
78358
|
const rk = who2.recordKey;
|
|
77640
78359
|
if (who2.clientKey && (stage === 0 || stage === 1)) throw new Error(SECOND_CLIENT_NO_INTAKE);
|
|
@@ -77664,7 +78383,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77664
78383
|
if (stage === 3) {
|
|
77665
78384
|
assertStaffUnchanged(st0.planApprovals?.[rk], st0.staffRoster?.[locationId2]);
|
|
77666
78385
|
const sp = readSavedPlan(rk);
|
|
77667
|
-
if (!sp || !
|
|
78386
|
+
if (!sp || !savedPlanFitsAnswers(sp, st0, rk)) throw new Error("The features selected for this client, or the answers about them, changed after its plan was prepared. Prepare the plan again, approve it, then build.");
|
|
77668
78387
|
}
|
|
77669
78388
|
if (stage === 4) {
|
|
77670
78389
|
const build = st0.outcomes?.[`${rk}:3`];
|
|
@@ -77771,6 +78490,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77771
78490
|
} catch (e) {
|
|
77772
78491
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
77773
78492
|
res.end(JSON.stringify(runRefusal(e)));
|
|
78493
|
+
} finally {
|
|
78494
|
+
releaseStart?.();
|
|
77774
78495
|
}
|
|
77775
78496
|
});
|
|
77776
78497
|
return;
|
|
@@ -77985,7 +78706,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77985
78706
|
const source = fileSource ? fileSource : args.url ? { kind: "url", url: String(args.url) } : args.instruction ? { kind: "recorder", instruction: String(args.instruction) } : { kind: "text", text: String(args.transcript ?? "") };
|
|
77986
78707
|
const toolContext = { servers: [] };
|
|
77987
78708
|
if (source.kind === "recorder") {
|
|
77988
|
-
const collect = (cmdArgs, done) => new Promise((
|
|
78709
|
+
const collect = (cmdArgs, done) => new Promise((resolve15) => {
|
|
77989
78710
|
const cmd = claudeCommand(claudeBin(), cmdArgs);
|
|
77990
78711
|
const child = (0, import_child_process5.spawn)(cmd.command, cmd.args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
77991
78712
|
let buf = "";
|
|
@@ -77994,7 +78715,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77994
78715
|
if (settled) return;
|
|
77995
78716
|
settled = true;
|
|
77996
78717
|
child.kill("SIGKILL");
|
|
77997
|
-
|
|
78718
|
+
resolve15({ out: buf, ok });
|
|
77998
78719
|
};
|
|
77999
78720
|
child.stdout.on("data", (d) => {
|
|
78000
78721
|
buf += d.toString();
|
|
@@ -78387,9 +79108,19 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78387
79108
|
res.end("No review generated for this client yet.");
|
|
78388
79109
|
return;
|
|
78389
79110
|
}
|
|
79111
|
+
if (reviewIsStale(st, locationId2, review)) {
|
|
79112
|
+
res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
|
|
79113
|
+
res.end("This client's Blueprint Review was written before their answers or what they bought changed. Press Regenerate on the review, then open the client document again.");
|
|
79114
|
+
return;
|
|
79115
|
+
}
|
|
79116
|
+
if (review.graded !== true) {
|
|
79117
|
+
res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
|
|
79118
|
+
res.end("This client's Blueprint Review was not checked for over-promises. Press Regenerate on the review, then open the client document again.");
|
|
79119
|
+
return;
|
|
79120
|
+
}
|
|
78390
79121
|
const name2 = clientDisplayName(st, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name || locationId2);
|
|
78391
79122
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
78392
|
-
res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
|
|
79123
|
+
res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2), buildStatusFor(st, locationId2, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
|
|
78393
79124
|
return;
|
|
78394
79125
|
}
|
|
78395
79126
|
if (req.method === "GET" && url === "/api/review") {
|
|
@@ -78397,7 +79128,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78397
79128
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
78398
79129
|
const stR = readCockpitState();
|
|
78399
79130
|
const businessName = clientDisplayName(stR, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name ?? "");
|
|
78400
|
-
|
|
79131
|
+
const savedReview = stR.reviews?.[locationId2];
|
|
79132
|
+
res.end(JSON.stringify({ ...savedReview ?? { ok: false, error: "not generated yet" }, ...businessName ? { businessName } : {}, ...reviewIsStale(stR, locationId2, savedReview) ? { stale: true } : {} }));
|
|
78401
79133
|
return;
|
|
78402
79134
|
}
|
|
78403
79135
|
if (req.method === "POST" && url === "/api/shared-adopt") {
|
|
@@ -78727,6 +79459,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78727
79459
|
if (!companyId || !apiKey2) throw new Error("Agency profile unavailable");
|
|
78728
79460
|
return new GHLClient({ apiKey: apiKey2 }).get(`/companies/${encodeURIComponent(companyId)}`, { noRetry: true });
|
|
78729
79461
|
});
|
|
79462
|
+
void prepareSmallLogo(saved.logoFile);
|
|
78730
79463
|
const st = readCockpitState();
|
|
78731
79464
|
writeCockpitState({ ...st, agencyName: saved.agencyName ?? st.agencyName, agencyAccent: saved.accent ?? st.agencyAccent });
|
|
78732
79465
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -78940,7 +79673,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78940
79673
|
if (req.method === "POST" && url === "/api/scope") {
|
|
78941
79674
|
readBody(req, res, 1e4, (body) => {
|
|
78942
79675
|
try {
|
|
78943
|
-
const { locationId: locationId2, clientKey, package: pkg2, features, bought } = JSON.parse(body);
|
|
79676
|
+
const { locationId: locationId2, clientKey, package: pkg2, features, bought, asks: asksIn } = JSON.parse(body);
|
|
78944
79677
|
const who2 = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
|
|
78945
79678
|
let ticks;
|
|
78946
79679
|
let derived;
|
|
@@ -78964,21 +79697,36 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78964
79697
|
else ticks = savedTicks[rk];
|
|
78965
79698
|
nextState = { ...nextState, bought: savedTicks };
|
|
78966
79699
|
if (!Object.keys(savedTicks).length) delete nextState.bought;
|
|
79700
|
+
const intakeSms = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers).sms;
|
|
79701
|
+
if (asksIn !== void 0) {
|
|
79702
|
+
const parsed2 = parseAsks(asksIn);
|
|
79703
|
+
if (!parsed2.ok) throw new Error(parsed2.error);
|
|
79704
|
+
const clash = asksContradiction(parsed2.asks, effectiveTexting(parsed2.asks, intakeSms));
|
|
79705
|
+
if (clash) throw new Error(clash);
|
|
79706
|
+
const all = { ...nextState.asks ?? {} };
|
|
79707
|
+
if (Object.keys(parsed2.asks).length) all[rk] = parsed2.asks;
|
|
79708
|
+
else delete all[rk];
|
|
79709
|
+
nextState = { ...nextState, asks: all };
|
|
79710
|
+
if (!Object.keys(all).length) delete nextState.asks;
|
|
79711
|
+
}
|
|
78967
79712
|
const { state: bound, withdrawn } = withdrawIfBindingChanged(nextState, rk);
|
|
78968
79713
|
nextState = bound;
|
|
78969
79714
|
{
|
|
78970
79715
|
const offered = offeredFeaturesFor(nextState, rk);
|
|
78971
79716
|
const channels = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers);
|
|
79717
|
+
const texting = effectiveTexting(nextState.asks?.[rk], channels.sms);
|
|
78972
79718
|
const scopeNow = nextState.scope?.[rk];
|
|
78973
79719
|
const excluded = ticks ? boughtButExcludedJobs(ticks, offered, !scopeNow || scopeNow.wholePreset ? "all" : scopeNow.features ?? []) : [];
|
|
78974
|
-
const
|
|
79720
|
+
const held2 = ticks ? heldJobs(heldItems(ticks, nextState.asks?.[rk], texting)) : [];
|
|
79721
|
+
const boughtList = ticks ? [...boughtJobs(ticks, offered, { ...channels, sms: texting === "yes" }), ...excluded, ...held2] : [];
|
|
78975
79722
|
replaceBuildObligationsOfType(rk, BOUGHT_JOB_TYPE, boughtList);
|
|
78976
79723
|
const tasks2 = withBoughtJobs(nextState.manualTasks?.[rk] ?? [], ticks ? catalogJobs({ handoffs: [], obligations: boughtList }) : []);
|
|
78977
79724
|
if (tasks2.length || nextState.manualTasks?.[rk]) nextState = { ...nextState, manualTasks: { ...nextState.manualTasks, [rk]: tasks2 } };
|
|
78978
79725
|
}
|
|
78979
79726
|
writeCockpitState(nextState);
|
|
78980
79727
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
78981
|
-
|
|
79728
|
+
const builtBefore = nextState.outcomes?.[`${rk}:3`]?.ok === true;
|
|
79729
|
+
res.end(JSON.stringify({ ok: true, scope: readCockpitState().scope?.[rk] ?? null, ...withdrawn ? { approvalWithdrawn: true, message: builtBefore && asksIn !== void 0 ? BUILD_WHAT_WAS_WAITING : APPROVAL_WITHDRAWN_FOR_SCOPE } : {} }));
|
|
78982
79730
|
} catch (e) {
|
|
78983
79731
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
78984
79732
|
res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "bad request" }));
|
|
@@ -79111,11 +79859,11 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
79111
79859
|
const run = () => {
|
|
79112
79860
|
const row = routeOf(req.method, url);
|
|
79113
79861
|
if (row?.write && req.method !== "GET" && req.method !== "HEAD") {
|
|
79114
|
-
const
|
|
79862
|
+
const held2 = req[GATE_BODY];
|
|
79115
79863
|
let client = new URL(req.url || "/", "http://x").searchParams.get("locationId") ?? void 0;
|
|
79116
|
-
if (!client &&
|
|
79864
|
+
if (!client && held2) {
|
|
79117
79865
|
try {
|
|
79118
|
-
const b = JSON.parse(
|
|
79866
|
+
const b = JSON.parse(held2);
|
|
79119
79867
|
const c = b.locationId ?? b.client ?? b.clientLocationId;
|
|
79120
79868
|
if (typeof c === "string") client = c;
|
|
79121
79869
|
} catch {
|
|
@@ -79136,9 +79884,9 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
79136
79884
|
});
|
|
79137
79885
|
}, { brainHandoff: () => isConnected("brain") }));
|
|
79138
79886
|
try {
|
|
79139
|
-
await new Promise((
|
|
79887
|
+
await new Promise((resolve15, reject) => {
|
|
79140
79888
|
server2.once("error", reject);
|
|
79141
|
-
server2.listen(port, "127.0.0.1",
|
|
79889
|
+
server2.listen(port, "127.0.0.1", resolve15);
|
|
79142
79890
|
});
|
|
79143
79891
|
} catch (e) {
|
|
79144
79892
|
const msg3 = e?.code === "EADDRINUSE" ? `Port ${port} is busy with another program. Close it, or start Command OS on a different port.` : `Command OS could not start: ${e instanceof Error ? e.message : String(e)}`;
|
|
@@ -79160,16 +79908,16 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
79160
79908
|
if (adopted) process.stderr.write(` Synced ${adopted} client${adopted === 1 ? "" : "s"} from your GHL (teammate updates).
|
|
79161
79909
|
`);
|
|
79162
79910
|
});
|
|
79163
|
-
return await new Promise((
|
|
79911
|
+
return await new Promise((resolve15) => {
|
|
79164
79912
|
const stop3 = () => {
|
|
79165
79913
|
closeAllSecretChannels();
|
|
79166
|
-
server2.close(() =>
|
|
79914
|
+
server2.close(() => resolve15(0));
|
|
79167
79915
|
};
|
|
79168
79916
|
process.on("SIGINT", stop3);
|
|
79169
79917
|
process.on("SIGTERM", stop3);
|
|
79170
79918
|
});
|
|
79171
79919
|
}
|
|
79172
|
-
var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
79920
|
+
var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, BUILD_WHAT_WAS_WAITING, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, runStarting, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
79173
79921
|
var init_dashboard = __esm({
|
|
79174
79922
|
"src/dashboard.ts"() {
|
|
79175
79923
|
"use strict";
|
|
@@ -79309,6 +80057,7 @@ var init_dashboard = __esm({
|
|
|
79309
80057
|
init_daily_brief2();
|
|
79310
80058
|
init_daily_brief_collect();
|
|
79311
80059
|
init_brief_deadline();
|
|
80060
|
+
init_client_asks();
|
|
79312
80061
|
VIEWER_COCKPIT_MSG = "Your team login on this license is view only, and Command OS runs work that changes accounts, so it did not open. Ask the license owner to make you a team member, then open Command OS again.";
|
|
79313
80062
|
STAGES2 = [
|
|
79314
80063
|
"Intake",
|
|
@@ -79343,10 +80092,12 @@ var init_dashboard = __esm({
|
|
|
79343
80092
|
NOTHING_TICKED_IN_PLAN = "None of the ticked features is in the plan for this client's industry, so nothing was saved. Tick features from the list, or choose Everything in the plan.";
|
|
79344
80093
|
SCOPE_CHANGED_SINCE_APPROVAL = "What they bought changed after the build was approved, so Build did not start. Approve this plan again, then build.";
|
|
79345
80094
|
APPROVAL_WITHDRAWN_FOR_SCOPE = "What they bought changed, so the build approval was withdrawn. Approve this plan again before building.";
|
|
80095
|
+
BUILD_WHAT_WAS_WAITING = "Your client's answers changed what gets built, so the build approval was withdrawn. Approve the updated plan, then run Build: it adds only what was waiting and makes nothing twice.";
|
|
79346
80096
|
INACTIVE = "License is not active. Contact support@ghlcommand.com.";
|
|
79347
80097
|
OFF_LICENSE = "This computer is no longer on your license. Free a device on your license, then restart Command OS.";
|
|
79348
80098
|
LICENSE_RECHECK_MS = 4 * 60 * 60 * 1e3;
|
|
79349
80099
|
activeRun2 = null;
|
|
80100
|
+
runStarting = false;
|
|
79350
80101
|
buildOneTime = /* @__PURE__ */ new Map();
|
|
79351
80102
|
oneTimeView = (e) => ({ locationId: e.locationId, ...e.clientKey ? { clientKey: e.clientKey } : {}, ...e.clientName ? { clientName: e.clientName } : {}, lines: Object.values(e.byEmail) });
|
|
79352
80103
|
runForTests = {
|
|
@@ -79374,7 +80125,7 @@ var init_dashboard = __esm({
|
|
|
79374
80125
|
DOC_NOT_SAVED = "That document is not saved for this client any more. Choose it again from the list, or add it again.";
|
|
79375
80126
|
NO_KEY_ON_FILE = "No key on file for this account. Register it in GHL Command first.";
|
|
79376
80127
|
PARKED_NOT_BUILT = "Template and hidden accounts are not built from Command OS. Change the account's type to Client first.";
|
|
79377
|
-
NEEDS_AGENCY_KEY = "
|
|
80128
|
+
NEEDS_AGENCY_KEY = "Your agency is not connected yet. Open Your agency, then Connections, and add your agency's key there.";
|
|
79378
80129
|
INTAKE_UNREADABLE = "Command OS could not read this client's account. Paste its key again under Reconnect this client, at the top of its steps on Build a client account.";
|
|
79379
80130
|
INTAKE_READ_TRANSIENT = "We could not reach this client's account just now. Try again in a moment.";
|
|
79380
80131
|
INTAKE_REFRESH_RETRYING = "Could not refresh just now; retrying.";
|
|
@@ -80082,7 +80833,7 @@ var init_location_switcher = __esm({
|
|
|
80082
80833
|
|
|
80083
80834
|
// src/tools/bulk-operations.ts
|
|
80084
80835
|
function delay2(ms) {
|
|
80085
|
-
return new Promise((
|
|
80836
|
+
return new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
80086
80837
|
}
|
|
80087
80838
|
function formatResults(op, results, total2) {
|
|
80088
80839
|
return `${op}: ${results.success} success, ${results.failed} failed out of ${total2}.${results.errors.length ? "\nErrors:\n" + results.errors.join("\n") : ""}`;
|
|
@@ -80487,7 +81238,7 @@ var init_handoff_doc = __esm({
|
|
|
80487
81238
|
|
|
80488
81239
|
// src/tools/account-export.ts
|
|
80489
81240
|
function delay3(ms) {
|
|
80490
|
-
return new Promise((
|
|
81241
|
+
return new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
80491
81242
|
}
|
|
80492
81243
|
function builderSectionAvailability(builderLocationId, requestedLocationId) {
|
|
80493
81244
|
if (!builderLocationId) {
|
|
@@ -81037,12 +81788,12 @@ function registerWorkflowClonerTools(server2, builderClient) {
|
|
|
81037
81788
|
const triggers = source.triggers || [];
|
|
81038
81789
|
for (const action of actions) {
|
|
81039
81790
|
if (action.id) {
|
|
81040
|
-
idMap.set(action.id,
|
|
81791
|
+
idMap.set(action.id, crypto14.randomUUID());
|
|
81041
81792
|
}
|
|
81042
81793
|
}
|
|
81043
81794
|
for (const trigger of triggers) {
|
|
81044
81795
|
if (trigger.id) {
|
|
81045
|
-
idMap.set(trigger.id,
|
|
81796
|
+
idMap.set(trigger.id, crypto14.randomUUID());
|
|
81046
81797
|
}
|
|
81047
81798
|
}
|
|
81048
81799
|
const remap = (id2) => {
|
|
@@ -81155,12 +81906,12 @@ function registerWorkflowClonerTools(server2, builderClient) {
|
|
|
81155
81906
|
}
|
|
81156
81907
|
);
|
|
81157
81908
|
}
|
|
81158
|
-
var import_zod98,
|
|
81909
|
+
var import_zod98, crypto14;
|
|
81159
81910
|
var init_workflow_cloner = __esm({
|
|
81160
81911
|
"src/tools/workflow-cloner.ts"() {
|
|
81161
81912
|
"use strict";
|
|
81162
81913
|
import_zod98 = require("zod");
|
|
81163
|
-
|
|
81914
|
+
crypto14 = __toESM(require("crypto"));
|
|
81164
81915
|
}
|
|
81165
81916
|
});
|
|
81166
81917
|
|
|
@@ -81206,13 +81957,13 @@ function contactServiceNote(payload) {
|
|
|
81206
81957
|
function registerSmartListTools(server2, builderClient) {
|
|
81207
81958
|
if (!builderClient) return;
|
|
81208
81959
|
const client = builderClient;
|
|
81209
|
-
async function smartListRequest(method,
|
|
81210
|
-
const url = `${SMARTLIST_BASE}${
|
|
81960
|
+
async function smartListRequest(method, path67, body) {
|
|
81961
|
+
const url = `${SMARTLIST_BASE}${path67}`;
|
|
81211
81962
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
81212
81963
|
const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
|
|
81213
81964
|
if (!response.ok) {
|
|
81214
81965
|
const text4 = await response.text();
|
|
81215
|
-
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${
|
|
81966
|
+
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path67}
|
|
81216
81967
|
${text4}`);
|
|
81217
81968
|
}
|
|
81218
81969
|
const text3 = await response.text();
|
|
@@ -81457,11 +82208,11 @@ var init_smart_lists = __esm({
|
|
|
81457
82208
|
function registerReputationTools(server2, builderClient) {
|
|
81458
82209
|
if (!builderClient) return;
|
|
81459
82210
|
const client = builderClient;
|
|
81460
|
-
async function reputationRequest(method,
|
|
81461
|
-
const response = await builderRequest(client, method, `${REPUTATION_BASE}${
|
|
82211
|
+
async function reputationRequest(method, path67) {
|
|
82212
|
+
const response = await builderRequest(client, method, `${REPUTATION_BASE}${path67}`);
|
|
81462
82213
|
if (!response.ok) {
|
|
81463
82214
|
const text4 = await response.text();
|
|
81464
|
-
throw new Error(`Reputation API Error ${response.status}: ${method} ${
|
|
82215
|
+
throw new Error(`Reputation API Error ${response.status}: ${method} ${path67}
|
|
81465
82216
|
${text4}`);
|
|
81466
82217
|
}
|
|
81467
82218
|
const text3 = await response.text();
|
|
@@ -81599,11 +82350,11 @@ var init_email_campaigns = __esm({
|
|
|
81599
82350
|
function registerMembershipTools(server2, builderClient) {
|
|
81600
82351
|
if (!builderClient) return;
|
|
81601
82352
|
const client = builderClient;
|
|
81602
|
-
async function membershipRequest(
|
|
81603
|
-
const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${
|
|
82353
|
+
async function membershipRequest(path67, method = "GET", body) {
|
|
82354
|
+
const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path67}`, body ? { body } : {});
|
|
81604
82355
|
if (!response.ok) {
|
|
81605
82356
|
const text4 = await response.text();
|
|
81606
|
-
throw new Error(`Membership API Error ${response.status}: ${method} ${
|
|
82357
|
+
throw new Error(`Membership API Error ${response.status}: ${method} ${path67}
|
|
81607
82358
|
${text4}`);
|
|
81608
82359
|
}
|
|
81609
82360
|
const text3 = await response.text();
|
|
@@ -81789,7 +82540,7 @@ var init_memberships = __esm({
|
|
|
81789
82540
|
|
|
81790
82541
|
// src/tools/template-deployer.ts
|
|
81791
82542
|
function delay4(ms) {
|
|
81792
|
-
return new Promise((
|
|
82543
|
+
return new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
81793
82544
|
}
|
|
81794
82545
|
function isAlreadyExistsError(message) {
|
|
81795
82546
|
return /already\s+exists?\b/i.test(message) || /duplicate/i.test(message);
|
|
@@ -81922,7 +82673,7 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
81922
82673
|
const locId = client.resolveLocationId(locationId2);
|
|
81923
82674
|
const safePath = validateTemplatePath(templateFile);
|
|
81924
82675
|
const template = TemplateSchema.parse(JSON.parse(fs54.readFileSync(safePath, "utf-8")));
|
|
81925
|
-
const
|
|
82676
|
+
const resolve15 = (text3) => {
|
|
81926
82677
|
if (typeof text3 !== "string") return text3;
|
|
81927
82678
|
let result = text3;
|
|
81928
82679
|
for (const [key2, value] of Object.entries(answers)) {
|
|
@@ -81935,7 +82686,7 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
81935
82686
|
return result;
|
|
81936
82687
|
};
|
|
81937
82688
|
const resolveObj = (obj2) => {
|
|
81938
|
-
if (typeof obj2 === "string") return
|
|
82689
|
+
if (typeof obj2 === "string") return resolve15(obj2);
|
|
81939
82690
|
if (Array.isArray(obj2)) return obj2.map(resolveObj);
|
|
81940
82691
|
if (obj2 && typeof obj2 === "object") {
|
|
81941
82692
|
const result = {};
|
|
@@ -82644,10 +83395,10 @@ async function confirm(run) {
|
|
|
82644
83395
|
async function runProbes(client, loc) {
|
|
82645
83396
|
return Promise.all(
|
|
82646
83397
|
PROBES.map((probe) => confirm(async () => {
|
|
82647
|
-
const
|
|
83398
|
+
const path67 = probe.path.replace("{loc}", loc);
|
|
82648
83399
|
const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
|
|
82649
83400
|
try {
|
|
82650
|
-
const raw = await client.get(
|
|
83401
|
+
const raw = await client.get(path67, { params, noRetry: true });
|
|
82651
83402
|
const count4 = countRows(raw, probe.listKeys);
|
|
82652
83403
|
return {
|
|
82653
83404
|
key: probe.key,
|
|
@@ -85894,7 +86645,7 @@ function claudeGenerate(model = DEFAULT_MODEL, bin = "claude") {
|
|
|
85894
86645
|
return (instruction, { cwd }) => new Promise((resolveP) => {
|
|
85895
86646
|
const t0 = Date.now();
|
|
85896
86647
|
const cmd = claudeCommand(bin, ["-p", "--output-format", "json", "--max-turns", "1", "--tools", "", "--strict-mcp-config", "--model", model, ...NO_MEMBER_HOOKS]);
|
|
85897
|
-
const child = (0,
|
|
86648
|
+
const child = (0, import_node_child_process8.spawn)(cmd.command, cmd.args, {
|
|
85898
86649
|
cwd,
|
|
85899
86650
|
env: { ...process.env, CLAUDECODE: "", CLAUDE_CODE_ENTRYPOINT: "" },
|
|
85900
86651
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -86046,11 +86797,11 @@ function summariseRun(r) {
|
|
|
86046
86797
|
const copy = !r.copy ? "" : "error" in r.copy ? ` \xB7 copy NOT GRADED (${r.copy.error})` : ` \xB7 copy ${r.copy.verdict}${r.copy.firstBreakdown ? ` (first breakdown: ${r.copy.firstBreakdown})` : ""}`;
|
|
86047
86798
|
return `${r.medium} for ${r.brandSlug}: design ${r.ok ? `PASS on pass ${r.greenAt}` : "NOT GREEN"}${copy} \xB7 ${passes} \xB7 $${r.costUsd.toFixed(3)} \xB7 ${Math.round(r.ms / 1e3)}s \xB7 ${r.model ?? "model unrecorded"}${r.file ? ` \xB7 ${r.file}` : ""}${r.report ? ` \xB7 ${r.report}` : ""}`;
|
|
86048
86799
|
}
|
|
86049
|
-
var
|
|
86800
|
+
var import_node_child_process8, import_node_fs18, import_node_path16, DEFAULT_MODEL, FIRST2;
|
|
86050
86801
|
var init_run2 = __esm({
|
|
86051
86802
|
"src/command-os/design/run.ts"() {
|
|
86052
86803
|
"use strict";
|
|
86053
|
-
|
|
86804
|
+
import_node_child_process8 = require("node:child_process");
|
|
86054
86805
|
init_claude_locate();
|
|
86055
86806
|
init_result_text();
|
|
86056
86807
|
import_node_fs18 = require("node:fs");
|
|
@@ -89837,23 +90588,23 @@ function ownsForSignal(p, jobDir, installs, platform2 = process.platform, stat)
|
|
|
89837
90588
|
if (!installs.some((dir) => exeInside(p, dir, platform2, stat))) return false;
|
|
89838
90589
|
return profileInside(argvOf(p, platform2), jobDir, platform2);
|
|
89839
90590
|
}
|
|
89840
|
-
var import_node_fs19, import_node_path17,
|
|
90591
|
+
var import_node_fs19, import_node_path17, import_node_child_process9, LIST_TIMEOUT_MS, execText, execTextSync, WIN_FIELDS, LISTING, listingCommand, oneCommand, exeCommand, listingOf, bounded, argvOf, sepOf, foldText, CODE_SIGN_CLONE;
|
|
89841
90592
|
var init_ownership = __esm({
|
|
89842
90593
|
"src/command-os/prospect/ownership.ts"() {
|
|
89843
90594
|
"use strict";
|
|
89844
90595
|
import_node_fs19 = __toESM(require("node:fs"));
|
|
89845
90596
|
import_node_path17 = __toESM(require("node:path"));
|
|
89846
|
-
|
|
90597
|
+
import_node_child_process9 = require("node:child_process");
|
|
89847
90598
|
LIST_TIMEOUT_MS = 1e4;
|
|
89848
|
-
execText = (file3, args, timeoutMs) => new Promise((
|
|
89849
|
-
(0,
|
|
90599
|
+
execText = (file3, args, timeoutMs) => new Promise((resolve15) => {
|
|
90600
|
+
(0, import_node_child_process9.execFile)(file3, args, { maxBuffer: 64 << 20, windowsHide: true, timeout: Math.max(1, Math.floor(timeoutMs)), killSignal: "SIGKILL" }, (e, stdout) => {
|
|
89850
90601
|
const code = e ? typeof e.code === "number" ? e.code : null : 0;
|
|
89851
|
-
|
|
90602
|
+
resolve15({ code, stdout: String(stdout ?? "") });
|
|
89852
90603
|
});
|
|
89853
90604
|
});
|
|
89854
90605
|
execTextSync = (file3, args, timeoutMs) => {
|
|
89855
90606
|
try {
|
|
89856
|
-
return { code: 0, stdout: String((0,
|
|
90607
|
+
return { code: 0, stdout: String((0, import_node_child_process9.execFileSync)(file3, args, { encoding: "utf8", timeout: Math.max(1, Math.floor(timeoutMs)), killSignal: "SIGKILL", maxBuffer: 64 << 20, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] })) };
|
|
89857
90608
|
} catch {
|
|
89858
90609
|
return { code: null, stdout: "" };
|
|
89859
90610
|
}
|
|
@@ -89988,12 +90739,12 @@ async function killJob(child, marker, o = {}) {
|
|
|
89988
90739
|
return { killed: [...killed], remaining: remaining.map((p) => p.pid), failures, confirmed: clear && rootGone };
|
|
89989
90740
|
}
|
|
89990
90741
|
function shopperSlot() {
|
|
89991
|
-
let
|
|
90742
|
+
let held2 = false;
|
|
89992
90743
|
for (const j of liveJobs) {
|
|
89993
90744
|
if (j.stuck) return "stuck";
|
|
89994
|
-
if (j.held)
|
|
90745
|
+
if (j.held) held2 = true;
|
|
89995
90746
|
}
|
|
89996
|
-
return
|
|
90747
|
+
return held2 ? "held" : "free";
|
|
89997
90748
|
}
|
|
89998
90749
|
function killJobsNow(platform2 = process.platform) {
|
|
89999
90750
|
if (!liveJobs.size) return [];
|
|
@@ -90061,7 +90812,7 @@ async function runIsolated(input, o = {}) {
|
|
|
90061
90812
|
const jobDir = import_node_fs20.default.mkdtempSync(import_node_path18.default.join(import_node_fs20.default.realpathSync(import_node_os2.default.tmpdir()), "ghl-shopper-job-"));
|
|
90062
90813
|
const marker = jobMarker(jobDir, platform2);
|
|
90063
90814
|
o.onJob?.({ dir: jobDir, marker });
|
|
90064
|
-
const child = (0,
|
|
90815
|
+
const child = (0, import_node_child_process10.spawn)(process.execPath, [`--max-old-space-size=${o.maxOldSpaceMb ?? CHILD_MAX_OLD_SPACE_MB}`, o.entry ?? defaultEntry(), ...o.childArg ? [o.childArg] : []], {
|
|
90065
90816
|
stdio: ["pipe", "ignore", "pipe", "pipe", "pipe", "pipe"],
|
|
90066
90817
|
// The job's folder is its temp folder, on every platform (Windows reads TEMP/TMP), so Chrome's profile is in it.
|
|
90067
90818
|
env: { ...o.env ?? childEnv(), TMPDIR: jobDir, TMP: jobDir, TEMP: jobDir },
|
|
@@ -90290,7 +91041,7 @@ async function sweepStaleJobFolders(o = {}) {
|
|
|
90290
91041
|
}
|
|
90291
91042
|
return removed;
|
|
90292
91043
|
}
|
|
90293
|
-
var import_node_fs20, import_node_os2, import_node_path18, import_node_stream,
|
|
91044
|
+
var import_node_fs20, import_node_os2, import_node_path18, import_node_stream, import_node_child_process10, JOB_DEADLINE_MS, CHILD_MAX_OLD_SPACE_MB, TREE_MEMORY_MAX_BYTES, RESULT_MAX_BYTES, STDERR_PASS_BYTES, STDERR_TAIL_BYTES, STDERR_SCAN_BYTES, OUT_OF_MEMORY, NC_TIME_LIMIT, NC_MEMORY_LIMIT, BLIND_LOG, NC_ENDED, CHILD_ENV_KEYS, markerDir, CHANNEL_EXE, isMain, sleep5, ROOT_WAIT_MS, liveJobs, CLEANUP_RETRY_MS, CLEANUP_HOLD_MS, HELD_LOG, STUCK_LOG, RELEASED_LOG, exitHookSet, FOLDER_WAIT_MS, FOLDER_LEFT_LOG, STALE_AFTER_MS;
|
|
90294
91045
|
var init_isolate = __esm({
|
|
90295
91046
|
"src/command-os/prospect/isolate.ts"() {
|
|
90296
91047
|
"use strict";
|
|
@@ -90298,7 +91049,7 @@ var init_isolate = __esm({
|
|
|
90298
91049
|
import_node_os2 = __toESM(require("node:os"));
|
|
90299
91050
|
import_node_path18 = __toESM(require("node:path"));
|
|
90300
91051
|
import_node_stream = require("node:stream");
|
|
90301
|
-
|
|
91052
|
+
import_node_child_process10 = require("node:child_process");
|
|
90302
91053
|
init_run3();
|
|
90303
91054
|
init_ownership();
|
|
90304
91055
|
init_ownership();
|
|
@@ -90950,14 +91701,14 @@ function buildReplyResponse(full, mode, locationId2) {
|
|
|
90950
91701
|
function readBuildDetail(locationId2, detail, budget = BUILD_REPLY_BUDGET) {
|
|
90951
91702
|
let full = lastFull.get(keyOf5(locationId2, detail.of, detail.clientKey));
|
|
90952
91703
|
if (!full && !detail.clientKey) {
|
|
90953
|
-
const
|
|
90954
|
-
if (
|
|
90955
|
-
if (
|
|
91704
|
+
const held2 = [...lastFull.keys()].filter((k) => k.startsWith(`${locationId2}|`) && k.endsWith(`|${detail.of}`));
|
|
91705
|
+
if (held2.length > 1) return { ok: false, error: `Replies for more than one client in this account are held. Pass detail.clientKey (the clientKey on the reply you are reading).` };
|
|
91706
|
+
if (held2.length === 1) full = lastFull.get(held2[0]);
|
|
90956
91707
|
}
|
|
90957
91708
|
if (!full) return { ok: false, error: `No ${detail.of} reply from apply_build_plan is held for this ${detail.clientKey ? "client" : "account"} in this session. Run apply_build_plan with mode:"${detail.of}" first.` };
|
|
90958
|
-
const
|
|
91709
|
+
const path67 = detail.section.split(".");
|
|
90959
91710
|
let value = full;
|
|
90960
|
-
for (const p of
|
|
91711
|
+
for (const p of path67) value = obj(value)[p];
|
|
90961
91712
|
if (value === void 0) return { ok: false, error: `The ${detail.of} reply has no section "${detail.section}".`, sections: Object.keys(full) };
|
|
90962
91713
|
const room = budget - 600;
|
|
90963
91714
|
let pages;
|
|
@@ -91193,6 +91944,123 @@ var init_publish_report = __esm({
|
|
|
91193
91944
|
}
|
|
91194
91945
|
});
|
|
91195
91946
|
|
|
91947
|
+
// src/build-lock.ts
|
|
91948
|
+
function pidAlive2(pid) {
|
|
91949
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
91950
|
+
try {
|
|
91951
|
+
process.kill(pid, 0);
|
|
91952
|
+
return true;
|
|
91953
|
+
} catch (e) {
|
|
91954
|
+
return e.code === "EPERM";
|
|
91955
|
+
}
|
|
91956
|
+
}
|
|
91957
|
+
function buildLockPath(locationId2, base = appDataDir()) {
|
|
91958
|
+
return path63.join(base, "build-locks", `${locationId2.replace(/[^A-Za-z0-9_-]/g, "_")}.lock`);
|
|
91959
|
+
}
|
|
91960
|
+
function claimBuildLock(locationId2, base) {
|
|
91961
|
+
const lock = buildLockPath(locationId2, base);
|
|
91962
|
+
fs63.mkdirSync(path63.dirname(lock), { recursive: true, mode: 448 });
|
|
91963
|
+
const mine = JSON.stringify({ pid: process.pid, token: crypto15.randomUUID(), at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
91964
|
+
for (let attempt2 = 0; attempt2 < 3; attempt2++) {
|
|
91965
|
+
try {
|
|
91966
|
+
const fd = fs63.openSync(lock, "wx", 384);
|
|
91967
|
+
try {
|
|
91968
|
+
fs63.writeSync(fd, mine);
|
|
91969
|
+
} finally {
|
|
91970
|
+
fs63.closeSync(fd);
|
|
91971
|
+
}
|
|
91972
|
+
held.set(lock, mine);
|
|
91973
|
+
return () => {
|
|
91974
|
+
if (held.get(lock) === mine) held.delete(lock);
|
|
91975
|
+
try {
|
|
91976
|
+
if (fs63.readFileSync(lock, "utf8") === mine) fs63.unlinkSync(lock);
|
|
91977
|
+
} catch {
|
|
91978
|
+
}
|
|
91979
|
+
};
|
|
91980
|
+
} catch (e) {
|
|
91981
|
+
if (e.code !== "EEXIST") throw e;
|
|
91982
|
+
let seen = "";
|
|
91983
|
+
let age = 0;
|
|
91984
|
+
try {
|
|
91985
|
+
seen = fs63.readFileSync(lock, "utf8");
|
|
91986
|
+
age = Date.now() - fs63.statSync(lock).mtimeMs;
|
|
91987
|
+
} catch {
|
|
91988
|
+
continue;
|
|
91989
|
+
}
|
|
91990
|
+
let pid = 0;
|
|
91991
|
+
try {
|
|
91992
|
+
pid = Number(JSON.parse(seen).pid);
|
|
91993
|
+
} catch {
|
|
91994
|
+
}
|
|
91995
|
+
const stale = pid > 0 && !pidAlive2(pid) || age > BUILD_LOCK_STALE_MS;
|
|
91996
|
+
if (!stale) return null;
|
|
91997
|
+
if (!takeOver(lock, seen)) return null;
|
|
91998
|
+
}
|
|
91999
|
+
}
|
|
92000
|
+
return null;
|
|
92001
|
+
}
|
|
92002
|
+
function takeOver(lock, seen, retried = false) {
|
|
92003
|
+
const mutex = `${lock}.takeover`;
|
|
92004
|
+
let fd;
|
|
92005
|
+
try {
|
|
92006
|
+
fd = fs63.openSync(mutex, "wx", 384);
|
|
92007
|
+
} catch (e) {
|
|
92008
|
+
if (e.code !== "EEXIST") throw e;
|
|
92009
|
+
let aged = false;
|
|
92010
|
+
try {
|
|
92011
|
+
if (Date.now() - fs63.statSync(mutex).mtimeMs > TAKEOVER_STALE_MS) {
|
|
92012
|
+
fs63.unlinkSync(mutex);
|
|
92013
|
+
aged = true;
|
|
92014
|
+
}
|
|
92015
|
+
} catch {
|
|
92016
|
+
}
|
|
92017
|
+
return aged && !retried ? takeOver(lock, seen, true) : false;
|
|
92018
|
+
}
|
|
92019
|
+
try {
|
|
92020
|
+
try {
|
|
92021
|
+
if (fs63.readFileSync(lock, "utf8") === seen) fs63.unlinkSync(lock);
|
|
92022
|
+
} catch {
|
|
92023
|
+
}
|
|
92024
|
+
} finally {
|
|
92025
|
+
fs63.closeSync(fd);
|
|
92026
|
+
try {
|
|
92027
|
+
fs63.unlinkSync(mutex);
|
|
92028
|
+
} catch {
|
|
92029
|
+
}
|
|
92030
|
+
}
|
|
92031
|
+
return true;
|
|
92032
|
+
}
|
|
92033
|
+
function touchBuildLock(locationId2, base) {
|
|
92034
|
+
const lock = buildLockPath(locationId2, base);
|
|
92035
|
+
const mine = held.get(lock);
|
|
92036
|
+
if (!mine) return;
|
|
92037
|
+
try {
|
|
92038
|
+
if (fs63.readFileSync(lock, "utf8") !== mine) {
|
|
92039
|
+
held.delete(lock);
|
|
92040
|
+
return;
|
|
92041
|
+
}
|
|
92042
|
+
const now = /* @__PURE__ */ new Date();
|
|
92043
|
+
fs63.utimesSync(lock, now, now);
|
|
92044
|
+
} catch {
|
|
92045
|
+
}
|
|
92046
|
+
}
|
|
92047
|
+
function buildRunningRefusal(accountName2) {
|
|
92048
|
+
return `A build is already running for ${accountName2}. Nothing was written by this call. Wait for that build's answer before running Build again; calling it again while it runs would create everything twice.`;
|
|
92049
|
+
}
|
|
92050
|
+
var crypto15, fs63, path63, BUILD_LOCK_STALE_MS, held, TAKEOVER_STALE_MS;
|
|
92051
|
+
var init_build_lock = __esm({
|
|
92052
|
+
"src/build-lock.ts"() {
|
|
92053
|
+
"use strict";
|
|
92054
|
+
crypto15 = __toESM(require("node:crypto"));
|
|
92055
|
+
fs63 = __toESM(require("node:fs"));
|
|
92056
|
+
path63 = __toESM(require("node:path"));
|
|
92057
|
+
init_credentials_store();
|
|
92058
|
+
BUILD_LOCK_STALE_MS = 20 * 6e4;
|
|
92059
|
+
held = /* @__PURE__ */ new Map();
|
|
92060
|
+
TAKEOVER_STALE_MS = 1e4;
|
|
92061
|
+
}
|
|
92062
|
+
});
|
|
92063
|
+
|
|
91196
92064
|
// src/intake-to-build/revert.ts
|
|
91197
92065
|
function plainDate(iso3, timeZone) {
|
|
91198
92066
|
const d = new Date(iso3);
|
|
@@ -91297,7 +92165,7 @@ function planRevert(record, opts) {
|
|
|
91297
92165
|
function errText(e) {
|
|
91298
92166
|
return e instanceof Error ? e.message : String(e);
|
|
91299
92167
|
}
|
|
91300
|
-
async function executeRevert(plan, deps, wait = (ms) => new Promise((
|
|
92168
|
+
async function executeRevert(plan, deps, wait = (ms) => new Promise((resolve15) => setTimeout(resolve15, ms))) {
|
|
91301
92169
|
const removed = [];
|
|
91302
92170
|
const refused = [];
|
|
91303
92171
|
const attempted = [];
|
|
@@ -92971,8 +93839,8 @@ async function executeBackbone(plan, rawDeps, opts = {}) {
|
|
|
92971
93839
|
calendarPending.push({ workflowRef: wf.ref, workflowName: wf.name, calendarRef: g.calendarRef, calendarName: g.calendarName, steps: g.steps, status: "pending" });
|
|
92972
93840
|
}
|
|
92973
93841
|
const waitsOnCalendar = expansion.calendarPending.length > 0;
|
|
92974
|
-
const
|
|
92975
|
-
if (
|
|
93842
|
+
const held2 = opts.heldWorkflowGates?.get(wf.ref);
|
|
93843
|
+
if (held2?.length) manual.push({ ref: wf.ref, type: "workflow-publish", name: wf.name, reason: heldWorkflowReason(wf.name, held2) });
|
|
92976
93844
|
if (opts.publishWorkflows && !opts.gatedWorkflowRefs?.has(wf.ref) && waitsOnCalendar) {
|
|
92977
93845
|
manual.push({ ref: wf.ref, type: "workflow-publish", name: wf.name, reason: `[${wf.name}] left as a draft and NOT switched on: it waits on a calendar that does not exist yet. Switch it on only after the calendar is picked on its trigger.` });
|
|
92978
93846
|
}
|
|
@@ -93543,13 +94411,13 @@ ${text4}`);
|
|
|
93543
94411
|
};
|
|
93544
94412
|
}
|
|
93545
94413
|
function makePipelineApi(builderClient) {
|
|
93546
|
-
return async (method,
|
|
93547
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
94414
|
+
return async (method, path67, body) => {
|
|
94415
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path67}`;
|
|
93548
94416
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
93549
94417
|
const response = await builderRequest(builderClient, method, url, sendsBody ? { body } : {});
|
|
93550
94418
|
if (!response.ok) {
|
|
93551
94419
|
const text4 = await response.text();
|
|
93552
|
-
throw new Error(`Pipeline API ${response.status}: ${method} ${
|
|
94420
|
+
throw new Error(`Pipeline API ${response.status}: ${method} ${path67}
|
|
93553
94421
|
${text4}`);
|
|
93554
94422
|
}
|
|
93555
94423
|
const text3 = await response.text();
|
|
@@ -93562,8 +94430,8 @@ ${text4}`);
|
|
|
93562
94430
|
};
|
|
93563
94431
|
}
|
|
93564
94432
|
function makeFunnelApi(builderClient) {
|
|
93565
|
-
return async (method,
|
|
93566
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
94433
|
+
return async (method, path67, body) => {
|
|
94434
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path67}`;
|
|
93567
94435
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
93568
94436
|
const response = await builderRequest(builderClient, method, url, {
|
|
93569
94437
|
...sendsBody ? { body } : {},
|
|
@@ -93576,7 +94444,7 @@ function makeFunnelApi(builderClient) {
|
|
|
93576
94444
|
});
|
|
93577
94445
|
if (!response.ok) {
|
|
93578
94446
|
const text4 = await response.text();
|
|
93579
|
-
throw new Error(`Funnel API ${response.status}: ${method} ${
|
|
94447
|
+
throw new Error(`Funnel API ${response.status}: ${method} ${path67}
|
|
93580
94448
|
${text4.slice(0, 300)}`);
|
|
93581
94449
|
}
|
|
93582
94450
|
const text3 = await response.text();
|
|
@@ -94081,7 +94949,7 @@ function createdId(response, key2) {
|
|
|
94081
94949
|
}
|
|
94082
94950
|
function runningStageFor(locationId2) {
|
|
94083
94951
|
try {
|
|
94084
|
-
const st = JSON.parse(
|
|
94952
|
+
const st = JSON.parse(fs64.readFileSync(path64.join(appDataDir(), "cockpit-state.json"), "utf8"));
|
|
94085
94953
|
const clients = st && typeof st === "object" ? Reflect.get(st, "clients") : void 0;
|
|
94086
94954
|
const entry = clients && typeof clients === "object" ? Reflect.get(clients, locationId2) : void 0;
|
|
94087
94955
|
const stages = entry && typeof entry === "object" ? Reflect.get(entry, "stages") : void 0;
|
|
@@ -94158,6 +95026,27 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94158
95026
|
clientKey: import_zod120.z.string().optional().describe(`Which client in this sub-account the build belongs to, when the sub-account holds more than one (lowercase letters, digits, hyphens, e.g. "qpt"). The saved plan, review scope, approval, obligations and progress for this client are kept apart from the sub-account's default client, so a second clinic's build never overwrites the first one's records. Omit for the sub-account's default client (every build before this existed). Defaults to the clinicPrefix in lower case when a prefix is given.`)
|
|
94159
95027
|
};
|
|
94160
95028
|
const buildPlanArgs = import_zod120.z.object(buildPlanSchema2);
|
|
95029
|
+
const runLockedBuildPlan = async (args) => {
|
|
95030
|
+
if ((args.mode ?? "dry_run") !== "execute") return runBuildPlan(args);
|
|
95031
|
+
const locationId2 = client.resolveLocationId();
|
|
95032
|
+
const release = claimBuildLock(locationId2);
|
|
95033
|
+
if (!release) {
|
|
95034
|
+
let name2 = locationId2;
|
|
95035
|
+
try {
|
|
95036
|
+
name2 = (await readLocationBasics(client, locationId2)).name || locationId2;
|
|
95037
|
+
} catch {
|
|
95038
|
+
}
|
|
95039
|
+
return jsonResponse({ ok: false, phase: "build", buildRunning: true, error: buildRunningRefusal(name2) });
|
|
95040
|
+
}
|
|
95041
|
+
const beat = setInterval(() => touchBuildLock(locationId2), 6e4);
|
|
95042
|
+
beat.unref?.();
|
|
95043
|
+
try {
|
|
95044
|
+
return await runBuildPlan(args);
|
|
95045
|
+
} finally {
|
|
95046
|
+
clearInterval(beat);
|
|
95047
|
+
release();
|
|
95048
|
+
}
|
|
95049
|
+
};
|
|
94161
95050
|
const runBuildPlan = async ({ plan: planArgIn, fromPreset, brief: briefArg, preset: presetArg, copy: copyArg, useSavedPlan, replaceSavedPlan, mode, locationId: locationId2, metHandoffs, publishWorkflows, textTemplates, onConflict, clinicPrefix: clinicPrefixArg, clientKey: clientKeyArg, acknowledgeSharedWorkflows }, persistPreview = false) => {
|
|
94162
95051
|
try {
|
|
94163
95052
|
const resolvedMode = mode ?? "dry_run";
|
|
@@ -94198,6 +95087,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94198
95087
|
const recordKey = clientRecordKey(activeLocation, clientKey);
|
|
94199
95088
|
let planArg = planArgIn;
|
|
94200
95089
|
let composed;
|
|
95090
|
+
let asksHash;
|
|
94201
95091
|
if (fromPreset) {
|
|
94202
95092
|
if (planArg) {
|
|
94203
95093
|
return jsonResponse({ ok: false, phase: "compose", error: "fromPreset composes the plan itself \u2014 pass brief (and optionally preset / copy), not a plan. To run a hand-authored plan, drop fromPreset." });
|
|
@@ -94255,6 +95145,12 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94255
95145
|
if (!chosen.ok) {
|
|
94256
95146
|
return jsonResponse({ ok: false, phase: "compose", error: chosen.error });
|
|
94257
95147
|
}
|
|
95148
|
+
{
|
|
95149
|
+
const board = readBoardState();
|
|
95150
|
+
const ticks = board.bought?.[recordKey];
|
|
95151
|
+
briefForCompose = briefWithAsks(briefForCompose, board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0, chosen.preset.skeleton?.workflows ?? []);
|
|
95152
|
+
asksHash = asksBindingHash(board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0);
|
|
95153
|
+
}
|
|
94258
95154
|
const composition = planFromPreset({ preset: chosen.preset, brief: briefForCompose, locationId: activeLocation, copy: copyArg });
|
|
94259
95155
|
if (!composition.ok) {
|
|
94260
95156
|
return jsonResponse({
|
|
@@ -94385,7 +95281,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94385
95281
|
const { name: locationName, timeZone: locationTimeZone } = await readLocationBasics(client, activeLocation);
|
|
94386
95282
|
const { assets, warnings: scanWarnings } = await scanExistingAssets(client, activeLocation, registry2);
|
|
94387
95283
|
const result = resolvePlan(typedPlan, assets, { metHandoffs });
|
|
94388
|
-
const clientFields = { ...clientKey ? { clientKey } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
|
|
95284
|
+
const clientFields = { ...clientKey ? { clientKey } : {}, ...asksHash ? { asksHash } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
|
|
94389
95285
|
if (clinicPrefix && clinicScoping) {
|
|
94390
95286
|
let checked = false;
|
|
94391
95287
|
let findings = [];
|
|
@@ -94555,6 +95451,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94555
95451
|
const record = (item) => {
|
|
94556
95452
|
try {
|
|
94557
95453
|
appendBuildObject({ runId, locationId: activeLocation, item, parents: journalParents });
|
|
95454
|
+
touchBuildLock(activeLocation);
|
|
94558
95455
|
} catch (e) {
|
|
94559
95456
|
throw new JournalWriteError(e instanceof Error ? e.message : String(e), { ref: item.ref, type: item.type, realId: item.realId });
|
|
94560
95457
|
}
|
|
@@ -94820,7 +95717,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94820
95717
|
clientKey: import_zod120.z.string().optional().describe("The clientKey on the reply being read, when it carries one (two clients in one sub-account keep separate replies).")
|
|
94821
95718
|
}).optional().describe("READ ONLY, builds nothing: when a reply was shortened to fit (it carries replyTrimmed), read one left-out section of that full reply, a page at a time. Pass detail alone, with no other argument.")
|
|
94822
95719
|
},
|
|
94823
|
-
({ detail, ...args }) => detail ? Promise.resolve({ content: [{ type: "text", text: JSON.stringify(readBuildDetail(client.resolveLocationId(), detail)) }] }) :
|
|
95720
|
+
({ detail, ...args }) => detail ? Promise.resolve({ content: [{ type: "text", text: JSON.stringify(readBuildDetail(client.resolveLocationId(), detail)) }] }) : runLockedBuildPlan(args)
|
|
94824
95721
|
);
|
|
94825
95722
|
const withClaimWarnings = (r) => r.plan ? { ...r, warnings: [...r.warnings, ...claimWarningsForPlan(r.plan)] } : r;
|
|
94826
95723
|
server2.tool(
|
|
@@ -95300,7 +96197,7 @@ function intakeFormMatches(expected, actual) {
|
|
|
95300
96197
|
const keys = ["id", "tag", "type", "hiddenFieldQueryKey", "required", "label", "options", "picklistOptions"];
|
|
95301
96198
|
return expected.every((field2, i) => keys.every((key2) => field2[key2] === void 0 || JSON.stringify(field2[key2]) === JSON.stringify(actual[i]?.[key2])));
|
|
95302
96199
|
}
|
|
95303
|
-
var
|
|
96200
|
+
var fs64, path64, import_zod120, buildNet, customFieldItemSchema, sleep7, NO_SELECTION_SCOPE, NOT_CONFIRMED_SCOPE, WHOLE_PRESET_NOT_KEPT, workflowInventorySchema, notConfirmedAsk, STAGE_LABELS;
|
|
95304
96201
|
var init_intake_to_build = __esm({
|
|
95305
96202
|
"src/tools/intake-to-build.ts"() {
|
|
95306
96203
|
"use strict";
|
|
@@ -95309,8 +96206,8 @@ var init_intake_to_build = __esm({
|
|
|
95309
96206
|
init_build_contract();
|
|
95310
96207
|
init_build_obligations();
|
|
95311
96208
|
init_allowed_writes();
|
|
95312
|
-
|
|
95313
|
-
|
|
96209
|
+
fs64 = __toESM(require("node:fs"));
|
|
96210
|
+
path64 = __toESM(require("node:path"));
|
|
95314
96211
|
init_credentials_store();
|
|
95315
96212
|
import_zod120 = require("zod");
|
|
95316
96213
|
init_workflow_builder_client();
|
|
@@ -95334,6 +96231,7 @@ var init_intake_to_build = __esm({
|
|
|
95334
96231
|
init_brief2();
|
|
95335
96232
|
init_plan_store();
|
|
95336
96233
|
init_jobs();
|
|
96234
|
+
init_build_lock();
|
|
95337
96235
|
init_client_record();
|
|
95338
96236
|
init_clinic_prefix();
|
|
95339
96237
|
init_clinic_prefix_registry();
|
|
@@ -95347,6 +96245,7 @@ var init_intake_to_build = __esm({
|
|
|
95347
96245
|
init_claims_guard();
|
|
95348
96246
|
init_plan_from_preset();
|
|
95349
96247
|
init_board_roster();
|
|
96248
|
+
init_client_asks();
|
|
95350
96249
|
init_features();
|
|
95351
96250
|
init_executor();
|
|
95352
96251
|
init_build_confirm();
|
|
@@ -95427,7 +96326,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env2 = process
|
|
|
95427
96326
|
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.` }));
|
|
95428
96327
|
}
|
|
95429
96328
|
const reader = token2 ? new GHLClient({ apiKey: token2.apiKey, locationId: loc }) : client;
|
|
95430
|
-
return refuseUnseenFindings2((
|
|
96329
|
+
return refuseUnseenFindings2((path67, params) => reader.get(path67, params ? { params } : void 0), loc, findings);
|
|
95431
96330
|
}, () => client.defaultLocationId, clientScope);
|
|
95432
96331
|
registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2, clientScope);
|
|
95433
96332
|
registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
|
|
@@ -95746,7 +96645,7 @@ function parseArgs3(argv) {
|
|
|
95746
96645
|
function openInBrowser(file3) {
|
|
95747
96646
|
const cmd = process.platform === "darwin" ? ["open", file3] : process.platform === "win32" ? ["cmd", "/c", "start", "", file3] : ["xdg-open", file3];
|
|
95748
96647
|
try {
|
|
95749
|
-
(0,
|
|
96648
|
+
(0, import_node_child_process11.spawn)(cmd[0], cmd.slice(1), { stdio: "ignore", detached: true }).unref();
|
|
95750
96649
|
} catch {
|
|
95751
96650
|
}
|
|
95752
96651
|
}
|
|
@@ -95834,13 +96733,13 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
|
|
|
95834
96733
|
}
|
|
95835
96734
|
return r.ok ? 0 : 1;
|
|
95836
96735
|
}
|
|
95837
|
-
var import_zod122,
|
|
96736
|
+
var import_zod122, import_node_child_process11, import_node_path19, import_node_os3, USAGE2;
|
|
95838
96737
|
var init_cli2 = __esm({
|
|
95839
96738
|
"src/command-os/design/cli.ts"() {
|
|
95840
96739
|
"use strict";
|
|
95841
96740
|
import_zod122 = require("zod");
|
|
95842
96741
|
init_brief_validation();
|
|
95843
|
-
|
|
96742
|
+
import_node_child_process11 = require("node:child_process");
|
|
95844
96743
|
import_node_path19 = require("node:path");
|
|
95845
96744
|
import_node_os3 = require("node:os");
|
|
95846
96745
|
init_run2();
|
|
@@ -95868,8 +96767,8 @@ __export(index_exports, {
|
|
|
95868
96767
|
});
|
|
95869
96768
|
module.exports = __toCommonJS(index_exports);
|
|
95870
96769
|
var dotenv2 = __toESM(require("dotenv"));
|
|
95871
|
-
var
|
|
95872
|
-
var
|
|
96770
|
+
var path66 = __toESM(require("path"));
|
|
96771
|
+
var fs66 = __toESM(require("fs"));
|
|
95873
96772
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
95874
96773
|
init_version_compare();
|
|
95875
96774
|
init_ghl_client();
|
|
@@ -95916,8 +96815,8 @@ var import_zod121 = require("zod");
|
|
|
95916
96815
|
|
|
95917
96816
|
// src/skill-installer.ts
|
|
95918
96817
|
var import_node_crypto34 = require("node:crypto");
|
|
95919
|
-
var
|
|
95920
|
-
var
|
|
96818
|
+
var fs65 = __toESM(require("node:fs"));
|
|
96819
|
+
var path65 = __toESM(require("node:path"));
|
|
95921
96820
|
var os11 = __toESM(require("node:os"));
|
|
95922
96821
|
function sha2563(buf) {
|
|
95923
96822
|
return (0, import_node_crypto34.createHash)("sha256").update(buf).digest("hex");
|
|
@@ -95932,9 +96831,9 @@ function isOlderRelease(candidate, installed) {
|
|
|
95932
96831
|
return false;
|
|
95933
96832
|
}
|
|
95934
96833
|
function bundledSkillsDir(baseDir) {
|
|
95935
|
-
const candidate =
|
|
96834
|
+
const candidate = path65.resolve(baseDir, "..", "skills");
|
|
95936
96835
|
try {
|
|
95937
|
-
return
|
|
96836
|
+
return fs65.statSync(candidate).isDirectory() ? candidate : null;
|
|
95938
96837
|
} catch {
|
|
95939
96838
|
return null;
|
|
95940
96839
|
}
|
|
@@ -95942,10 +96841,10 @@ function bundledSkillsDir(baseDir) {
|
|
|
95942
96841
|
function listFiles(root) {
|
|
95943
96842
|
const out = [];
|
|
95944
96843
|
const walk2 = (dir) => {
|
|
95945
|
-
for (const entry of
|
|
95946
|
-
const full =
|
|
96844
|
+
for (const entry of fs65.readdirSync(dir, { withFileTypes: true })) {
|
|
96845
|
+
const full = path65.join(dir, entry.name);
|
|
95947
96846
|
if (entry.isDirectory()) walk2(full);
|
|
95948
|
-
else if (entry.isFile()) out.push(
|
|
96847
|
+
else if (entry.isFile()) out.push(path65.relative(root, full));
|
|
95949
96848
|
}
|
|
95950
96849
|
};
|
|
95951
96850
|
walk2(root);
|
|
@@ -95953,7 +96852,7 @@ function listFiles(root) {
|
|
|
95953
96852
|
}
|
|
95954
96853
|
function readMarker(markerPath) {
|
|
95955
96854
|
try {
|
|
95956
|
-
const parsed2 = JSON.parse(
|
|
96855
|
+
const parsed2 = JSON.parse(fs65.readFileSync(markerPath, "utf8"));
|
|
95957
96856
|
return parsed2 && typeof parsed2 === "object" && parsed2.files ? parsed2 : null;
|
|
95958
96857
|
} catch {
|
|
95959
96858
|
return null;
|
|
@@ -95961,7 +96860,7 @@ function readMarker(markerPath) {
|
|
|
95961
96860
|
}
|
|
95962
96861
|
function defaultSkillsDir(env2 = process.env) {
|
|
95963
96862
|
const dir = env2.CLAUDE_CONFIG_DIR?.trim();
|
|
95964
|
-
return dir ?
|
|
96863
|
+
return dir ? path65.join(dir, "skills") : path65.join(os11.homedir(), ".claude", "skills");
|
|
95965
96864
|
}
|
|
95966
96865
|
function installBundledSkills(opts) {
|
|
95967
96866
|
const result = {
|
|
@@ -95978,28 +96877,28 @@ function installBundledSkills(opts) {
|
|
|
95978
96877
|
result.bundledDir = bundled;
|
|
95979
96878
|
if (!bundled) return result;
|
|
95980
96879
|
try {
|
|
95981
|
-
|
|
96880
|
+
fs65.mkdirSync(result.targetDir, { recursive: true });
|
|
95982
96881
|
} catch (e) {
|
|
95983
96882
|
result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
|
|
95984
96883
|
return result;
|
|
95985
96884
|
}
|
|
95986
|
-
const markerPath =
|
|
96885
|
+
const markerPath = path65.join(result.targetDir, ".ghl-command-skills.json");
|
|
95987
96886
|
const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
|
|
95988
96887
|
const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
|
|
95989
96888
|
for (const rel of listFiles(bundled)) {
|
|
95990
96889
|
try {
|
|
95991
|
-
const src =
|
|
95992
|
-
const dest =
|
|
96890
|
+
const src = fs65.readFileSync(path65.join(bundled, rel));
|
|
96891
|
+
const dest = path65.join(result.targetDir, rel);
|
|
95993
96892
|
const srcHash = sha2563(src);
|
|
95994
96893
|
let destBuf = null;
|
|
95995
96894
|
try {
|
|
95996
|
-
destBuf =
|
|
96895
|
+
destBuf = fs65.readFileSync(dest);
|
|
95997
96896
|
} catch {
|
|
95998
96897
|
destBuf = null;
|
|
95999
96898
|
}
|
|
96000
96899
|
if (destBuf === null) {
|
|
96001
|
-
|
|
96002
|
-
|
|
96900
|
+
fs65.mkdirSync(path65.dirname(dest), { recursive: true });
|
|
96901
|
+
fs65.writeFileSync(dest, src);
|
|
96003
96902
|
newMarker.files[rel] = srcHash;
|
|
96004
96903
|
result.installed.push(rel);
|
|
96005
96904
|
continue;
|
|
@@ -96016,7 +96915,7 @@ function installBundledSkills(opts) {
|
|
|
96016
96915
|
newMarker.packageVersion = marker.packageVersion;
|
|
96017
96916
|
result.skippedOlderPackage.push(rel);
|
|
96018
96917
|
} else if (lastWritten && destHash === lastWritten) {
|
|
96019
|
-
|
|
96918
|
+
fs65.writeFileSync(dest, src);
|
|
96020
96919
|
newMarker.files[rel] = srcHash;
|
|
96021
96920
|
result.updated.push(rel);
|
|
96022
96921
|
} else {
|
|
@@ -96027,7 +96926,7 @@ function installBundledSkills(opts) {
|
|
|
96027
96926
|
}
|
|
96028
96927
|
}
|
|
96029
96928
|
try {
|
|
96030
|
-
|
|
96929
|
+
fs65.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
|
|
96031
96930
|
} catch (e) {
|
|
96032
96931
|
result.errors.push(`marker write failed: ${String(e)}`);
|
|
96033
96932
|
}
|
|
@@ -96193,7 +97092,7 @@ dotenv2.config();
|
|
|
96193
97092
|
setPkgVersion(pkg.version);
|
|
96194
97093
|
{
|
|
96195
97094
|
const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
|
|
96196
|
-
if (configDirOverride && !
|
|
97095
|
+
if (configDirOverride && !path66.isAbsolute(configDirOverride)) {
|
|
96197
97096
|
process.stderr.write(
|
|
96198
97097
|
`[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.
|
|
96199
97098
|
`
|
|
@@ -96206,20 +97105,20 @@ process.on("unhandledRejection", (reason3) => {
|
|
|
96206
97105
|
`);
|
|
96207
97106
|
});
|
|
96208
97107
|
function hardenSecretFilePerms() {
|
|
96209
|
-
const repoDir =
|
|
97108
|
+
const repoDir = path66.resolve(__dirname, "..");
|
|
96210
97109
|
const candidates = [
|
|
96211
|
-
{ file:
|
|
97110
|
+
{ file: path66.join(repoDir, "start-mcp.sh"), mode: 448 },
|
|
96212
97111
|
// Legacy registry location (pre-migration); new location lives in app-data.
|
|
96213
|
-
{ file:
|
|
97112
|
+
{ file: path66.join(repoDir, ".ghl-tokens.json"), mode: 384 },
|
|
96214
97113
|
{ file: tokenRegistryPath(), mode: 384 }
|
|
96215
97114
|
];
|
|
96216
97115
|
for (const { file: file3, mode } of candidates) {
|
|
96217
97116
|
let current;
|
|
96218
97117
|
try {
|
|
96219
|
-
if (!
|
|
96220
|
-
current =
|
|
97118
|
+
if (!fs66.existsSync(file3)) continue;
|
|
97119
|
+
current = fs66.statSync(file3).mode & 511;
|
|
96221
97120
|
if (current !== mode) {
|
|
96222
|
-
|
|
97121
|
+
fs66.chmodSync(file3, mode);
|
|
96223
97122
|
}
|
|
96224
97123
|
} catch (error) {
|
|
96225
97124
|
const message = error instanceof Error ? error.message : String(error);
|