@elitedcs/ghl-mcp 3.84.7 → 3.84.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/index.js +1227 -592
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -127,7 +127,7 @@ var init_account_read_retry = __esm({
|
|
|
127
127
|
inBuildRequestScope = () => buildScope.getStore() !== void 0;
|
|
128
128
|
inAccountReadAttempt = () => readScope.getStore() === true;
|
|
129
129
|
withBuildRequestScope = (run, wait = sleep) => buildScope.run({ wait }, run);
|
|
130
|
-
sleep = (ms) => new Promise((
|
|
130
|
+
sleep = (ms) => new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
|
|
@@ -446,13 +446,13 @@ function setProtectedAccount(locationId2, name, file2 = accountRulesPath()) {
|
|
|
446
446
|
function isWriteMethod(method) {
|
|
447
447
|
return WRITE_METHODS.has(method.toUpperCase());
|
|
448
448
|
}
|
|
449
|
-
function locationIdsInRequest(
|
|
449
|
+
function locationIdsInRequest(path42, opts = {}) {
|
|
450
450
|
const found = /* @__PURE__ */ new Set();
|
|
451
451
|
const add2 = (v) => {
|
|
452
452
|
if (typeof v === "string" && v.trim()) found.add(v.trim());
|
|
453
453
|
else if (Array.isArray(v)) v.forEach(add2);
|
|
454
454
|
};
|
|
455
|
-
const seg = /\/locations\/([^/?#]+)/i.exec(
|
|
455
|
+
const seg = /\/locations\/([^/?#]+)/i.exec(path42);
|
|
456
456
|
if (seg) add2(decodeURIComponent(seg[1]));
|
|
457
457
|
for (const src of [opts.params, opts.body]) {
|
|
458
458
|
if (!src || typeof src !== "object") continue;
|
|
@@ -479,7 +479,7 @@ function protectedRefusalFor(locationId2, rulesFile) {
|
|
|
479
479
|
const name = rules.protected[id];
|
|
480
480
|
return name ? protectedRefusalMessage(name, false) : null;
|
|
481
481
|
}
|
|
482
|
-
function assertWriteAllowed(method,
|
|
482
|
+
function assertWriteAllowed(method, path42, opts = {}) {
|
|
483
483
|
if (!isWriteMethod(method)) return;
|
|
484
484
|
let rules;
|
|
485
485
|
try {
|
|
@@ -488,7 +488,7 @@ function assertWriteAllowed(method, path41, opts = {}) {
|
|
|
488
488
|
throw new Error(UNREADABLE_RULES_MSG);
|
|
489
489
|
}
|
|
490
490
|
if (Object.keys(rules.protected).length === 0) return;
|
|
491
|
-
for (const id of locationIdsInRequest(
|
|
491
|
+
for (const id of locationIdsInRequest(path42, opts)) {
|
|
492
492
|
const name = rules.protected[id];
|
|
493
493
|
if (name) throw new Error(protectedRefusalMessage(name, false));
|
|
494
494
|
}
|
|
@@ -646,8 +646,8 @@ var init_ghl_client = __esm({
|
|
|
646
646
|
Version: version || GHL_API_VERSION
|
|
647
647
|
};
|
|
648
648
|
}
|
|
649
|
-
buildUrl(
|
|
650
|
-
const url = new URL(
|
|
649
|
+
buildUrl(path42, params) {
|
|
650
|
+
const url = new URL(path42, GHL_BASE_URL);
|
|
651
651
|
if (params) {
|
|
652
652
|
for (const [key, value] of Object.entries(params)) {
|
|
653
653
|
if (value !== void 0 && value !== null) {
|
|
@@ -657,19 +657,19 @@ var init_ghl_client = __esm({
|
|
|
657
657
|
}
|
|
658
658
|
return url.toString();
|
|
659
659
|
}
|
|
660
|
-
async request(method,
|
|
660
|
+
async request(method, path42, options = {}, attempt = 0) {
|
|
661
661
|
this.signal?.throwIfAborted();
|
|
662
662
|
if (inBuildRequestScope() && method === "GET" && !inAccountReadAttempt()) {
|
|
663
|
-
return retryAccountRead(() => this.request(method,
|
|
663
|
+
return retryAccountRead(() => this.request(method, path42, options, attempt), `${method} ${path42}`);
|
|
664
664
|
}
|
|
665
665
|
if (attempt === 0) {
|
|
666
|
-
assertWriteAllowed(method,
|
|
666
|
+
assertWriteAllowed(method, path42, {
|
|
667
667
|
params: options.params,
|
|
668
668
|
body: options.body,
|
|
669
669
|
defaultLocationId: this.defaultLocationId
|
|
670
670
|
});
|
|
671
671
|
}
|
|
672
|
-
const url = this.buildUrl(
|
|
672
|
+
const url = this.buildUrl(path42, options.params);
|
|
673
673
|
const headers = this.buildHeaders(options.version);
|
|
674
674
|
const fetchOptions = {
|
|
675
675
|
method,
|
|
@@ -690,14 +690,14 @@ var init_ghl_client = __esm({
|
|
|
690
690
|
clearTimeout(timeout);
|
|
691
691
|
this.signal?.throwIfAborted();
|
|
692
692
|
if (error instanceof Error && error.name === "AbortError") {
|
|
693
|
-
throw requestError(new Error(`Request timeout (30s): ${method} ${
|
|
693
|
+
throw requestError(new Error(`Request timeout (30s): ${method} ${path42}`));
|
|
694
694
|
}
|
|
695
695
|
if (!inBuildRequestScope() && !options.noRetry && attempt < MAX_RETRIES) {
|
|
696
696
|
const delay5 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
|
|
697
|
-
process.stderr.write(`[ghl-mcp] Network error on ${method} ${
|
|
697
|
+
process.stderr.write(`[ghl-mcp] Network error on ${method} ${path42}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
|
|
698
698
|
`);
|
|
699
699
|
await new Promise((r) => setTimeout(r, delay5));
|
|
700
|
-
return this.request(method,
|
|
700
|
+
return this.request(method, path42, options, attempt + 1);
|
|
701
701
|
}
|
|
702
702
|
throw requestError(error);
|
|
703
703
|
} finally {
|
|
@@ -705,10 +705,10 @@ var init_ghl_client = __esm({
|
|
|
705
705
|
}
|
|
706
706
|
if (!inBuildRequestScope() && !options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
|
|
707
707
|
const delay5 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
|
|
708
|
-
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${
|
|
708
|
+
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path42}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
|
|
709
709
|
`);
|
|
710
710
|
await new Promise((r) => setTimeout(r, delay5));
|
|
711
|
-
return this.request(method,
|
|
711
|
+
return this.request(method, path42, options, attempt + 1);
|
|
712
712
|
}
|
|
713
713
|
if (!response.ok) {
|
|
714
714
|
let errorBody = "";
|
|
@@ -717,7 +717,7 @@ var init_ghl_client = __esm({
|
|
|
717
717
|
} catch {
|
|
718
718
|
}
|
|
719
719
|
throw requestError(new Error(
|
|
720
|
-
`GHL API Error ${response.status} ${response.statusText}: ${method} ${
|
|
720
|
+
`GHL API Error ${response.status} ${response.statusText}: ${method} ${path42}
|
|
721
721
|
${errorBody}`
|
|
722
722
|
));
|
|
723
723
|
}
|
|
@@ -738,20 +738,20 @@ ${errorBody}`
|
|
|
738
738
|
combined?.dispose();
|
|
739
739
|
}
|
|
740
740
|
}
|
|
741
|
-
async get(
|
|
742
|
-
return this.request("GET",
|
|
741
|
+
async get(path42, options) {
|
|
742
|
+
return this.request("GET", path42, options);
|
|
743
743
|
}
|
|
744
|
-
async post(
|
|
745
|
-
return this.request("POST",
|
|
744
|
+
async post(path42, options) {
|
|
745
|
+
return this.request("POST", path42, options);
|
|
746
746
|
}
|
|
747
|
-
async put(
|
|
748
|
-
return this.request("PUT",
|
|
747
|
+
async put(path42, options) {
|
|
748
|
+
return this.request("PUT", path42, options);
|
|
749
749
|
}
|
|
750
|
-
async patch(
|
|
751
|
-
return this.request("PATCH",
|
|
750
|
+
async patch(path42, options) {
|
|
751
|
+
return this.request("PATCH", path42, options);
|
|
752
752
|
}
|
|
753
|
-
async delete(
|
|
754
|
-
return this.request("DELETE",
|
|
753
|
+
async delete(path42, options) {
|
|
754
|
+
return this.request("DELETE", path42, options);
|
|
755
755
|
}
|
|
756
756
|
/**
|
|
757
757
|
* Helper: resolves locationId from args or falls back to default
|
|
@@ -5472,12 +5472,12 @@ function registerFormTools(server2, client) {
|
|
|
5472
5472
|
startAt: import_zod16.z.string().optional().describe("Start date filter (ISO string)"),
|
|
5473
5473
|
endAt: import_zod16.z.string().optional().describe("End date filter (ISO string)")
|
|
5474
5474
|
},
|
|
5475
|
-
async ({ locationId: locationId2, page, limit, formId, q:
|
|
5475
|
+
async ({ locationId: locationId2, page, limit, formId, q: q3, startAt, endAt }) => {
|
|
5476
5476
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
5477
5477
|
const params = { locationId: resolvedLocationId, limit: limit ?? 20 };
|
|
5478
5478
|
if (page !== void 0) params.page = page;
|
|
5479
5479
|
if (formId !== void 0) params.formId = formId;
|
|
5480
|
-
if (
|
|
5480
|
+
if (q3 !== void 0) params.q = q3;
|
|
5481
5481
|
if (startAt !== void 0) params.startAt = startAt;
|
|
5482
5482
|
if (endAt !== void 0) params.endAt = endAt;
|
|
5483
5483
|
return client.get("/forms/submissions", { params });
|
|
@@ -5526,13 +5526,13 @@ function registerSurveyTools(server2, client) {
|
|
|
5526
5526
|
startAt: import_zod17.z.string().optional().describe("Start date filter (ISO string)"),
|
|
5527
5527
|
endAt: import_zod17.z.string().optional().describe("End date filter (ISO string)")
|
|
5528
5528
|
},
|
|
5529
|
-
async ({ locationId: locationId2, page, limit, surveyId, q:
|
|
5529
|
+
async ({ locationId: locationId2, page, limit, surveyId, q: q3, startAt, endAt }) => {
|
|
5530
5530
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
5531
5531
|
const params = { locationId: resolvedLocationId };
|
|
5532
5532
|
if (page !== void 0) params.page = page;
|
|
5533
5533
|
if (limit !== void 0) params.limit = limit;
|
|
5534
5534
|
if (surveyId !== void 0) params.surveyId = surveyId;
|
|
5535
|
-
if (
|
|
5535
|
+
if (q3 !== void 0) params.q = q3;
|
|
5536
5536
|
if (startAt !== void 0) params.startAt = startAt;
|
|
5537
5537
|
if (endAt !== void 0) params.endAt = endAt;
|
|
5538
5538
|
return client.get("/surveys/submissions", { params });
|
|
@@ -6192,12 +6192,12 @@ function buildCreateUserBody(args, opts = {}) {
|
|
|
6192
6192
|
function generateTemporaryPassword() {
|
|
6193
6193
|
const upper2 = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
6194
6194
|
const lower = "abcdefghijkmnopqrstuvwxyz";
|
|
6195
|
-
const
|
|
6195
|
+
const digits = "23456789";
|
|
6196
6196
|
const symbols = "!@#$%&*?";
|
|
6197
|
-
const all = upper2 + lower +
|
|
6197
|
+
const all = upper2 + lower + digits + symbols;
|
|
6198
6198
|
const bytes = (0, import_node_crypto.randomBytes)(24);
|
|
6199
6199
|
const pick2 = (set, b) => set[b % set.length];
|
|
6200
|
-
const chars = [pick2(upper2, bytes[0]), pick2(lower, bytes[1]), pick2(
|
|
6200
|
+
const chars = [pick2(upper2, bytes[0]), pick2(lower, bytes[1]), pick2(digits, bytes[2]), pick2(symbols, bytes[3])];
|
|
6201
6201
|
for (let i = 4; i < 20; i++) chars.push(pick2(all, bytes[i]));
|
|
6202
6202
|
for (let i = chars.length - 1; i > 0; i--) {
|
|
6203
6203
|
const j = bytes[(i + 20) % bytes.length] % (i + 1);
|
|
@@ -7105,11 +7105,11 @@ function registerEmailTools(server2, client) {
|
|
|
7105
7105
|
function registerEmailBuilderInternalTools(server2, builderClient, publicClient) {
|
|
7106
7106
|
if (!builderClient) return;
|
|
7107
7107
|
const client = builderClient;
|
|
7108
|
-
async function builderRequest2(method,
|
|
7109
|
-
const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${
|
|
7108
|
+
async function builderRequest2(method, path42, body) {
|
|
7109
|
+
const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path42}`, body ? { body } : {});
|
|
7110
7110
|
if (!response.ok) {
|
|
7111
7111
|
const text3 = await response.text();
|
|
7112
|
-
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${
|
|
7112
|
+
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path42}
|
|
7113
7113
|
${text3}`);
|
|
7114
7114
|
}
|
|
7115
7115
|
const text2 = await response.text();
|
|
@@ -8330,18 +8330,18 @@ async function readAgent(client, agentId) {
|
|
|
8330
8330
|
return a;
|
|
8331
8331
|
}
|
|
8332
8332
|
async function updateAgentFields(client, agentId, change) {
|
|
8333
|
-
const
|
|
8334
|
-
for (const [k, val] of Object.entries(change)) if (val !== void 0)
|
|
8335
|
-
if (Object.keys(
|
|
8333
|
+
const clean2 = {};
|
|
8334
|
+
for (const [k, val] of Object.entries(change)) if (val !== void 0) clean2[k] = val;
|
|
8335
|
+
if (Object.keys(clean2).length === 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
|
|
8336
8336
|
const before = await readAgent(client, agentId);
|
|
8337
|
-
const body = { ...promptTrio(before), ...
|
|
8337
|
+
const body = { ...promptTrio(before), ...clean2 };
|
|
8338
8338
|
const merged = promptTrio(body);
|
|
8339
8339
|
if (!merged.personality || !merged.goal || !merged.instructions) {
|
|
8340
8340
|
throw new Error(`Agent ${agentId} read back without its personality/goal/instructions, and GoHighLevel refuses an update that does not carry all three. Pass all three explicitly (get_conversation_agent shows what is stored).`);
|
|
8341
8341
|
}
|
|
8342
8342
|
await client.put(`/conversation-ai/agents/${agentId}`, { version: AI_API_VERSION, body });
|
|
8343
8343
|
const after = await readAgent(client, agentId);
|
|
8344
|
-
return { before, changed: Object.keys(
|
|
8344
|
+
return { before, changed: Object.keys(clean2), after };
|
|
8345
8345
|
}
|
|
8346
8346
|
function registerConversationAiTools(server2, client) {
|
|
8347
8347
|
const v = { version: AI_API_VERSION };
|
|
@@ -8978,7 +8978,7 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
|
|
|
8978
8978
|
const utc = (s) => /^\d{4}-\d{2}-\d{2}T[\d:.]+$/.test(s) ? `${s}Z` : s;
|
|
8979
8979
|
const when = (m) => Date.parse(utc(String(m.dateAdded ?? m.createdAt ?? m.dateUpdated ?? ""))) || 0;
|
|
8980
8980
|
const start = Date.parse(run.startedAt) || 0;
|
|
8981
|
-
const
|
|
8981
|
+
const stop2 = run.endedAt ? Date.parse(run.endedAt) || Infinity : Infinity;
|
|
8982
8982
|
const SKEW_MS = 15e3;
|
|
8983
8983
|
let beforeStart = 0;
|
|
8984
8984
|
let afterEnd = 0;
|
|
@@ -8993,7 +8993,7 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
|
|
|
8993
8993
|
beforeStart++;
|
|
8994
8994
|
return false;
|
|
8995
8995
|
}
|
|
8996
|
-
if (t >
|
|
8996
|
+
if (t > stop2 + SKEW_MS) {
|
|
8997
8997
|
afterEnd++;
|
|
8998
8998
|
return false;
|
|
8999
8999
|
}
|
|
@@ -10919,11 +10919,11 @@ async function readPageFull(client, pageId, fetcher = fetch) {
|
|
|
10919
10919
|
const headers = await client.buildHeaders();
|
|
10920
10920
|
headers.Origin = "https://app.gohighlevel.com";
|
|
10921
10921
|
headers.Referer = "https://app.gohighlevel.com/";
|
|
10922
|
-
const
|
|
10923
|
-
const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${
|
|
10922
|
+
const path42 = `/page/${pageId}?locationId=${client.locationId}`;
|
|
10923
|
+
const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path42}`, { method: "GET", headers });
|
|
10924
10924
|
if (!response.ok) {
|
|
10925
10925
|
const text3 = await response.text();
|
|
10926
|
-
throw new Error(`Funnel API Error ${response.status}: GET ${
|
|
10926
|
+
throw new Error(`Funnel API Error ${response.status}: GET ${path42}
|
|
10927
10927
|
${text3}`);
|
|
10928
10928
|
}
|
|
10929
10929
|
const text2 = await response.text();
|
|
@@ -10982,20 +10982,20 @@ function registerFunnelBuilderTools(server2, builderClient) {
|
|
|
10982
10982
|
return;
|
|
10983
10983
|
}
|
|
10984
10984
|
const client = builderClient;
|
|
10985
|
-
async function internalGet(
|
|
10986
|
-
return client.request("GET",
|
|
10985
|
+
async function internalGet(path42) {
|
|
10986
|
+
return client.request("GET", path42);
|
|
10987
10987
|
}
|
|
10988
|
-
async function internalPost(
|
|
10989
|
-
return client.request("POST",
|
|
10988
|
+
async function internalPost(path42, body) {
|
|
10989
|
+
return client.request("POST", path42, body);
|
|
10990
10990
|
}
|
|
10991
|
-
async function internalPut(
|
|
10992
|
-
return client.request("PUT",
|
|
10991
|
+
async function internalPut(path42, body) {
|
|
10992
|
+
return client.request("PUT", path42, body);
|
|
10993
10993
|
}
|
|
10994
|
-
async function internalDelete(
|
|
10995
|
-
return client.request("DELETE",
|
|
10994
|
+
async function internalDelete(path42) {
|
|
10995
|
+
return client.request("DELETE", path42);
|
|
10996
10996
|
}
|
|
10997
|
-
async function funnelRequest(method,
|
|
10998
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
10997
|
+
async function funnelRequest(method, path42, body) {
|
|
10998
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path42}`;
|
|
10999
10999
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
11000
11000
|
const response = await builderRequest(client, method, url, {
|
|
11001
11001
|
...sendsBody ? { body } : {},
|
|
@@ -11008,7 +11008,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
|
|
|
11008
11008
|
});
|
|
11009
11009
|
if (!response.ok) {
|
|
11010
11010
|
const text3 = await response.text();
|
|
11011
|
-
throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
11011
|
+
throw new Error(`Funnel API Error ${response.status}: ${method} ${path42}
|
|
11012
11012
|
${text3}`);
|
|
11013
11013
|
}
|
|
11014
11014
|
const text2 = await response.text();
|
|
@@ -11599,30 +11599,30 @@ function requireCompany(api) {
|
|
|
11599
11599
|
}
|
|
11600
11600
|
return company;
|
|
11601
11601
|
}
|
|
11602
|
-
async function call(api,
|
|
11602
|
+
async function call(api, path42, init) {
|
|
11603
11603
|
const headers = { ...await api.headers(), Origin: "https://app.gohighlevel.com", Referer: "https://app.gohighlevel.com/" };
|
|
11604
|
-
if (MUTATING_PATHS.some((mutating) =>
|
|
11605
|
-
assertWriteAllowed("POST", SVC +
|
|
11604
|
+
if (MUTATING_PATHS.some((mutating) => path42.startsWith(mutating))) {
|
|
11605
|
+
assertWriteAllowed("POST", SVC + path42, { defaultLocationId: api.locationId() });
|
|
11606
11606
|
}
|
|
11607
11607
|
const doFetch = api.fetch ?? fetch;
|
|
11608
|
-
const response = await doFetch(SVC +
|
|
11608
|
+
const response = await doFetch(SVC + path42, { ...init, headers });
|
|
11609
11609
|
const text2 = await response.text();
|
|
11610
11610
|
if (!response.ok) {
|
|
11611
11611
|
const hint = response.status === 401 && /not authorized to access this company/i.test(text2) ? " \u2014 the Firebase session is authenticated as a DIFFERENT company than the one that owns this sub-account. Run switch_location for this sub-account and try again." : response.status === 401 ? " \u2014 the template library needs the workflow-builder Firebase session, not the API key alone. Run enable_workflow_builder for this sub-account." : "";
|
|
11612
|
-
throw new Error(`GoHighLevel template library ${response.status} on ${
|
|
11612
|
+
throw new Error(`GoHighLevel template library ${response.status} on ${path42}${hint}
|
|
11613
11613
|
${text2.slice(0, 300)}`);
|
|
11614
11614
|
}
|
|
11615
11615
|
return text2 ? JSON.parse(text2) : {};
|
|
11616
11616
|
}
|
|
11617
|
-
async function searchTemplates(api,
|
|
11618
|
-
if (
|
|
11619
|
-
const wanted = new Set(
|
|
11620
|
-
const limit = Math.min(Math.max(
|
|
11621
|
-
const offset = Math.max(
|
|
11617
|
+
async function searchTemplates(api, q3 = {}) {
|
|
11618
|
+
if (q3.categories?.length) {
|
|
11619
|
+
const wanted = new Set(q3.categories.map((name) => name.trim().toLowerCase()));
|
|
11620
|
+
const limit = Math.min(Math.max(q3.limit ?? 24, 1), 200);
|
|
11621
|
+
const offset = Math.max(q3.offset ?? 0, 0);
|
|
11622
11622
|
const matches = [];
|
|
11623
11623
|
const seen = /* @__PURE__ */ new Set();
|
|
11624
11624
|
for (let upstreamOffset = 0; ; upstreamOffset += 200) {
|
|
11625
|
-
const page = await searchTemplates(api, { ...
|
|
11625
|
+
const page = await searchTemplates(api, { ...q3, categories: [], limit: 200, offset: upstreamOffset });
|
|
11626
11626
|
const fresh = page.filter((row) => !seen.has(row._id));
|
|
11627
11627
|
if (page.length && !fresh.length) throw new Error("GoHighLevel repeated a template page. The category search could not finish.");
|
|
11628
11628
|
for (const row of fresh) {
|
|
@@ -11637,19 +11637,19 @@ async function searchTemplates(api, q2 = {}) {
|
|
|
11637
11637
|
const body = {
|
|
11638
11638
|
companyId: requireCompany(api),
|
|
11639
11639
|
locationId: api.locationId(),
|
|
11640
|
-
product:
|
|
11641
|
-
keyword:
|
|
11640
|
+
product: q3.product ?? "funnels",
|
|
11641
|
+
keyword: q3.keyword ?? "",
|
|
11642
11642
|
type: "all",
|
|
11643
|
-
categories:
|
|
11644
|
-
features:
|
|
11645
|
-
tags:
|
|
11646
|
-
colors:
|
|
11643
|
+
categories: q3.categories ?? [],
|
|
11644
|
+
features: q3.features ?? [],
|
|
11645
|
+
tags: q3.tags ?? [],
|
|
11646
|
+
colors: q3.colors ?? [],
|
|
11647
11647
|
status: [],
|
|
11648
11648
|
createdBy: [],
|
|
11649
11649
|
isAdminView: false,
|
|
11650
11650
|
system: false,
|
|
11651
|
-
limit: Math.min(Math.max(
|
|
11652
|
-
offset:
|
|
11651
|
+
limit: Math.min(Math.max(q3.limit ?? 24, 1), 200),
|
|
11652
|
+
offset: q3.offset ?? 0
|
|
11653
11653
|
};
|
|
11654
11654
|
const out = await call(api, "/templates/list", { method: "POST", body: JSON.stringify(body) });
|
|
11655
11655
|
return Array.isArray(out) ? out : out.templates ?? [];
|
|
@@ -11966,12 +11966,12 @@ function normaliseColour(value) {
|
|
|
11966
11966
|
function renderedPalette(envelope) {
|
|
11967
11967
|
const defs = pageVariables(envelope);
|
|
11968
11968
|
const paints = [];
|
|
11969
|
-
const walk = (node,
|
|
11970
|
-
if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${
|
|
11969
|
+
const walk = (node, path42, prop) => {
|
|
11970
|
+
if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${path42}[${i}]`, prop));
|
|
11971
11971
|
if (isRecord8(node)) {
|
|
11972
11972
|
for (const [k, x] of Object.entries(node)) {
|
|
11973
11973
|
if (DEFINITION_KEYS.has(k) || k.startsWith("--")) continue;
|
|
11974
|
-
walk(x, `${
|
|
11974
|
+
walk(x, `${path42}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
|
|
11975
11975
|
}
|
|
11976
11976
|
return;
|
|
11977
11977
|
}
|
|
@@ -11981,7 +11981,7 @@ function renderedPalette(envelope) {
|
|
|
11981
11981
|
const resolved2 = resolveValue(raw, defs);
|
|
11982
11982
|
const colour = normaliseColour(resolved2);
|
|
11983
11983
|
if (colour === null && resolved2 !== null) return;
|
|
11984
|
-
paints.push({ path:
|
|
11984
|
+
paints.push({ path: path42, prop, kind: kindOf(prop), colour });
|
|
11985
11985
|
};
|
|
11986
11986
|
walk(envelope, "", "");
|
|
11987
11987
|
const face = (name, settingsKey) => {
|
|
@@ -12020,8 +12020,8 @@ function repaintReport(before, after) {
|
|
|
12020
12020
|
const coloured = (c) => c.startsWith("#") && hexToOklch(c).c >= CARRIES_COLOUR;
|
|
12021
12021
|
for (const p of a.paints) {
|
|
12022
12022
|
if (!p.colour || p.colour === "transparent") continue;
|
|
12023
|
-
const
|
|
12024
|
-
const to =
|
|
12023
|
+
const q3 = afterByPath.get(p.path);
|
|
12024
|
+
const to = q3?.colour ?? null;
|
|
12025
12025
|
const differs = to !== null && looksDifferent(p.colour, to);
|
|
12026
12026
|
if (p.kind === "background" || p.kind === "text") {
|
|
12027
12027
|
const t = p.kind === "text" ? tally.text : coloured(p.colour) ? tally.background : tally.neutral;
|
|
@@ -12076,7 +12076,7 @@ var init_rendered_palette = __esm({
|
|
|
12076
12076
|
VISIBLE_CHANGE = 0.05;
|
|
12077
12077
|
BARELY_SHARE = 0.5;
|
|
12078
12078
|
CARRIES_COLOUR = 0.02;
|
|
12079
|
-
paintGroup = (
|
|
12079
|
+
paintGroup = (path42) => /^\.(sections|popups)\[\d+\]/.exec(path42)?.[0]?.slice(1) ?? "page";
|
|
12080
12080
|
looksDifferent = (a, b) => {
|
|
12081
12081
|
if (a === b) return false;
|
|
12082
12082
|
if (a === "transparent" || b === "transparent" || a.startsWith("rgba") || b.startsWith("rgba")) return a !== b;
|
|
@@ -15700,12 +15700,12 @@ function deriveBrandDna(raw, opts = {}) {
|
|
|
15700
15700
|
fixed = true;
|
|
15701
15701
|
}
|
|
15702
15702
|
const vb = svg.match(/viewBox=["']\s*[\d.-]+\s+[\d.-]+\s+([\d.]+)\s+([\d.]+)/i);
|
|
15703
|
-
const
|
|
15704
|
-
if (
|
|
15705
|
-
logoSvg =
|
|
15706
|
-
logo = { inlineSvg:
|
|
15703
|
+
const clean2 = sanitizeSvgOrDrop(svg);
|
|
15704
|
+
if (clean2) {
|
|
15705
|
+
logoSvg = clean2;
|
|
15706
|
+
logo = { inlineSvg: clean2, whiteFillFixed: fixed, source: bl.where, rights: opts.rights ? "declared" : "inherits", ...vb && { width: parseFloat(vb[1]), height: parseFloat(vb[2]) } };
|
|
15707
15707
|
declared++;
|
|
15708
|
-
evidence["logo"] = `${bl.where}${fixed ? "; white fills replaced with currentColor" : ""}${
|
|
15708
|
+
evidence["logo"] = `${bl.where}${fixed ? "; white fills replaced with currentColor" : ""}${clean2 !== svg ? "; scripts, handlers and outside references stripped" : ""}`;
|
|
15709
15709
|
} else {
|
|
15710
15710
|
evidence["logo"] = `${bl.where}: not a plain drawing (or over 60 KB), so it was not kept`;
|
|
15711
15711
|
}
|
|
@@ -16651,10 +16651,10 @@ function removalHoldBack(opts) {
|
|
|
16651
16651
|
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`;
|
|
16652
16652
|
return null;
|
|
16653
16653
|
}
|
|
16654
|
-
function brandedPath(
|
|
16655
|
-
const
|
|
16656
|
-
const base =
|
|
16657
|
-
const used = new Set([...taken].map(
|
|
16654
|
+
function brandedPath(path42, name, taken) {
|
|
16655
|
+
const normalize2 = (s) => s.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
16656
|
+
const base = normalize2(path42 ?? "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
|
|
16657
|
+
const used = new Set([...taken].map(normalize2));
|
|
16658
16658
|
let candidate = `${base}-branded`;
|
|
16659
16659
|
for (let n = 2; used.has(candidate); n++) candidate = `${base}-branded-${n}`;
|
|
16660
16660
|
taken.add(`/${candidate}`);
|
|
@@ -16663,7 +16663,7 @@ function brandedPath(path41, name, taken) {
|
|
|
16663
16663
|
function rewriteTemplateLinks(envelope, destinations) {
|
|
16664
16664
|
const links = [];
|
|
16665
16665
|
const unmapped = [];
|
|
16666
|
-
const
|
|
16666
|
+
const normalize2 = (s) => s.replace(/^\/+|\/+$/g, "");
|
|
16667
16667
|
function rewrite(from, kind) {
|
|
16668
16668
|
if (!from) return from;
|
|
16669
16669
|
let matches = [];
|
|
@@ -16681,9 +16681,9 @@ function rewriteTemplateLinks(envelope, destinations) {
|
|
|
16681
16681
|
representation = "page";
|
|
16682
16682
|
matches = destinations.filter((d2) => d2.pageId === preview[2]);
|
|
16683
16683
|
} else if (!/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(from) && !from.startsWith("#")) {
|
|
16684
|
-
const [
|
|
16685
|
-
suffix = from.slice(
|
|
16686
|
-
matches = destinations.filter((d2) => d2.path &&
|
|
16684
|
+
const [path42] = from.split(/[?#]/);
|
|
16685
|
+
suffix = from.slice(path42.length);
|
|
16686
|
+
matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(path42));
|
|
16687
16687
|
}
|
|
16688
16688
|
}
|
|
16689
16689
|
if (matches.length !== 1) {
|
|
@@ -16695,7 +16695,7 @@ function rewriteTemplateLinks(envelope, destinations) {
|
|
|
16695
16695
|
unmapped.push({ from, reason: "the target step was not rebuilt" });
|
|
16696
16696
|
return from;
|
|
16697
16697
|
}
|
|
16698
|
-
const to = representation === "step" ? d.newStepId : representation === "page" ? `${previewPrefix}${d.newPageId}${suffix}` : `${from.startsWith("/") ? d.newPath :
|
|
16698
|
+
const to = representation === "step" ? d.newStepId : representation === "page" ? `${previewPrefix}${d.newPageId}${suffix}` : `${from.startsWith("/") ? d.newPath : normalize2(d.newPath)}${suffix}`;
|
|
16699
16699
|
links.push({ from, to });
|
|
16700
16700
|
return to;
|
|
16701
16701
|
}
|
|
@@ -16766,8 +16766,8 @@ function registerPageStudioTools(server2, builderClient) {
|
|
|
16766
16766
|
}
|
|
16767
16767
|
return { map: out, truncated };
|
|
16768
16768
|
}
|
|
16769
|
-
async function funnelRequest(method,
|
|
16770
|
-
const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${
|
|
16769
|
+
async function funnelRequest(method, path42, body) {
|
|
16770
|
+
const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path42}`, {
|
|
16771
16771
|
...body ? { body } : {},
|
|
16772
16772
|
headers: {
|
|
16773
16773
|
// Internal-API calls require the app origin (autosave needs the
|
|
@@ -16776,7 +16776,7 @@ function registerPageStudioTools(server2, builderClient) {
|
|
|
16776
16776
|
Referer: "https://app.gohighlevel.com/"
|
|
16777
16777
|
}
|
|
16778
16778
|
});
|
|
16779
|
-
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
16779
|
+
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path42}
|
|
16780
16780
|
${await response.text()}`);
|
|
16781
16781
|
const text2 = await response.text();
|
|
16782
16782
|
return text2 ? JSON.parse(text2) : {};
|
|
@@ -17283,7 +17283,7 @@ ${await response.text()}`);
|
|
|
17283
17283
|
}
|
|
17284
17284
|
const confirmed = /* @__PURE__ */ new Set();
|
|
17285
17285
|
for (let attempt = 0; accepted.length && attempt < 5; attempt++) {
|
|
17286
|
-
if (attempt) await new Promise((
|
|
17286
|
+
if (attempt) await new Promise((resolve13) => setTimeout(resolve13, attempt * 1e3));
|
|
17287
17287
|
try {
|
|
17288
17288
|
const listed = await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=${FUNNEL_PAGE}`);
|
|
17289
17289
|
const current = listed.funnels?.find((item) => item._id === funnel._id);
|
|
@@ -17645,13 +17645,13 @@ function cleanFormDataForSave(formData) {
|
|
|
17645
17645
|
const cleaned = dropRedundantCaseCollidingKeys(formData);
|
|
17646
17646
|
return isJsonObject(cleaned) ? cleaned : formData;
|
|
17647
17647
|
}
|
|
17648
|
-
async function formApiRequest(client, method,
|
|
17649
|
-
const url = `https://backend.leadconnectorhq.com/forms${
|
|
17648
|
+
async function formApiRequest(client, method, path42, body) {
|
|
17649
|
+
const url = `https://backend.leadconnectorhq.com/forms${path42}`;
|
|
17650
17650
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
17651
17651
|
const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
|
|
17652
17652
|
if (!response.ok) {
|
|
17653
17653
|
const text3 = await response.text();
|
|
17654
|
-
throw new Error(`Form API Error ${response.status}: ${method} ${
|
|
17654
|
+
throw new Error(`Form API Error ${response.status}: ${method} ${path42}
|
|
17655
17655
|
${text3}`);
|
|
17656
17656
|
}
|
|
17657
17657
|
const text2 = await response.text();
|
|
@@ -17693,7 +17693,7 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
|
|
|
17693
17693
|
);
|
|
17694
17694
|
return;
|
|
17695
17695
|
}
|
|
17696
|
-
const formRequest = (method,
|
|
17696
|
+
const formRequest = (method, path42, body) => formApiRequest(client, method, path42, body);
|
|
17697
17697
|
server2.tool(
|
|
17698
17698
|
"get_form_full",
|
|
17699
17699
|
"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.",
|
|
@@ -17827,10 +17827,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
|
|
|
17827
17827
|
},
|
|
17828
17828
|
async ({ formId, limit, skip }) => {
|
|
17829
17829
|
try {
|
|
17830
|
-
let
|
|
17831
|
-
if (formId)
|
|
17832
|
-
if (skip)
|
|
17833
|
-
const result = await formRequest("GET",
|
|
17830
|
+
let path42 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
|
|
17831
|
+
if (formId) path42 += `&formId=${formId}`;
|
|
17832
|
+
if (skip) path42 += `&skip=${skip}`;
|
|
17833
|
+
const result = await formRequest("GET", path42);
|
|
17834
17834
|
return {
|
|
17835
17835
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
17836
17836
|
};
|
|
@@ -18074,7 +18074,7 @@ function registerFunnelQaTools(server2, client, builderClient) {
|
|
|
18074
18074
|
last_name: contact.lastName,
|
|
18075
18075
|
name: contact.name
|
|
18076
18076
|
};
|
|
18077
|
-
const
|
|
18077
|
+
const same2 = (key, a, b) => key === "phone" ? (a ?? "").replace(/\D/g, "") === (b ?? "").replace(/\D/g, "") : (a ?? "").trim().toLowerCase() === (b ?? "").trim().toLowerCase();
|
|
18078
18078
|
for (const k of args.expectStandard ?? []) {
|
|
18079
18079
|
const key = k.toLowerCase();
|
|
18080
18080
|
if (!(key in got)) {
|
|
@@ -18087,7 +18087,7 @@ function registerFunnelQaTools(server2, client, builderClient) {
|
|
|
18087
18087
|
continue;
|
|
18088
18088
|
}
|
|
18089
18089
|
if (!got[key]) probs.push(`standard ${k}: not saved`);
|
|
18090
|
-
else if (!
|
|
18090
|
+
else if (!same2(key, got[key], exp)) probs.push(`standard ${k}: saved "${got[key]}" \u2260 sent "${exp}"`);
|
|
18091
18091
|
}
|
|
18092
18092
|
const okCount = (args.expectCustom?.length ?? 0) + (args.expectStandard?.length ?? 0) - probs.length - notes.length;
|
|
18093
18093
|
checks.push(probs.length ? { id: "field_fidelity", label: "Field-value persistence", status: "fail", detail: [probs.join("; "), notes.join("; ")].filter(Boolean).join(" | ") } : { id: "field_fidelity", label: "Field-value persistence", status: "pass", detail: `${okCount} asserted field value(s) persisted with the exact submitted value.${notes.length ? " " + notes.join("; ") : ""}` });
|
|
@@ -18238,13 +18238,13 @@ var init_funnel_qa = __esm({
|
|
|
18238
18238
|
function registerPipelineBuilderTools(server2, builderClient) {
|
|
18239
18239
|
if (!builderClient) return;
|
|
18240
18240
|
const client = builderClient;
|
|
18241
|
-
async function pipelineRequest(method,
|
|
18242
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
18241
|
+
async function pipelineRequest(method, path42, body) {
|
|
18242
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path42}`;
|
|
18243
18243
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT" || method === "PATCH");
|
|
18244
18244
|
const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
|
|
18245
18245
|
if (!response.ok) {
|
|
18246
18246
|
const text3 = await response.text();
|
|
18247
|
-
throw new Error(`Pipeline API Error ${response.status}: ${method} ${
|
|
18247
|
+
throw new Error(`Pipeline API Error ${response.status}: ${method} ${path42}
|
|
18248
18248
|
${text3}`);
|
|
18249
18249
|
}
|
|
18250
18250
|
const text2 = await response.text();
|
|
@@ -18548,6 +18548,22 @@ var init_features = __esm({
|
|
|
18548
18548
|
});
|
|
18549
18549
|
|
|
18550
18550
|
// src/agency-profile.ts
|
|
18551
|
+
var agency_profile_exports = {};
|
|
18552
|
+
__export(agency_profile_exports, {
|
|
18553
|
+
AGENCY_PROFILE_ENV: () => AGENCY_PROFILE_ENV,
|
|
18554
|
+
agencyProfilePath: () => agencyProfilePath,
|
|
18555
|
+
demoRailProfile: () => demoRailProfile,
|
|
18556
|
+
mergeAgencyProfile: () => mergeAgencyProfile,
|
|
18557
|
+
normalizeAccent: () => normalizeAccent,
|
|
18558
|
+
profileIsUsable: () => profileIsUsable,
|
|
18559
|
+
readAgencyProfile: () => readAgencyProfile,
|
|
18560
|
+
safeDir: () => safeDir,
|
|
18561
|
+
safeUrl: () => safeUrl2,
|
|
18562
|
+
sanitizeAgencyOs: () => sanitizeAgencyOs,
|
|
18563
|
+
sanitizeProfile: () => sanitizeProfile,
|
|
18564
|
+
saveAgencyProfileAnswer: () => saveAgencyProfileAnswer,
|
|
18565
|
+
writeAgencyProfile: () => writeAgencyProfile
|
|
18566
|
+
});
|
|
18551
18567
|
function agencyProfilePath() {
|
|
18552
18568
|
const override = process.env[AGENCY_PROFILE_ENV];
|
|
18553
18569
|
return override && override.trim() ? override : path8.join(appDataDir(), "agency-profile.json");
|
|
@@ -18572,6 +18588,11 @@ function safeUrl2(x, max = MAX_URL) {
|
|
|
18572
18588
|
if (u.protocol === "http:" && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(u.hostname)) return raw;
|
|
18573
18589
|
return void 0;
|
|
18574
18590
|
}
|
|
18591
|
+
function safeDir(x) {
|
|
18592
|
+
const raw = typeof x === "string" ? x.trim() : "";
|
|
18593
|
+
if (!raw || raw.length > 1024 || raw.includes("\0") || !path8.isAbsolute(raw)) return void 0;
|
|
18594
|
+
return path8.normalize(raw);
|
|
18595
|
+
}
|
|
18575
18596
|
function normalizeAccent(x) {
|
|
18576
18597
|
const s = typeof x === "string" ? x.trim() : "";
|
|
18577
18598
|
return /^#[0-9a-fA-F]{6}$/.test(s) ? s.toLowerCase() : void 0;
|
|
@@ -18646,6 +18667,7 @@ function sanitizeProfile(raw) {
|
|
|
18646
18667
|
industry: str2(defaults?.industry, 64)
|
|
18647
18668
|
}),
|
|
18648
18669
|
agencyOs: sanitizeAgencyOs(r.agencyOs),
|
|
18670
|
+
clientFilesDir: safeDir(r.clientFilesDir),
|
|
18649
18671
|
updated_at: str2(r.updated_at, 40)
|
|
18650
18672
|
};
|
|
18651
18673
|
}
|
|
@@ -18790,6 +18812,17 @@ function writeAgencyProfile(patch, file2 = agencyProfilePath()) {
|
|
|
18790
18812
|
function profileIsUsable(p) {
|
|
18791
18813
|
return Boolean(p.agencyName);
|
|
18792
18814
|
}
|
|
18815
|
+
function demoRailProfile(p = readAgencyProfile(), ghl, demoDays = 7) {
|
|
18816
|
+
if (!profileIsUsable(p)) return void 0;
|
|
18817
|
+
const rec = { agencyName: p.agencyName };
|
|
18818
|
+
if (p.accent) rec.accent = p.accent;
|
|
18819
|
+
if (p.logoUrl) rec.logoUrl = p.logoUrl;
|
|
18820
|
+
if (p.sender) rec.senderIdentity = p.sender;
|
|
18821
|
+
const g = drop({ locationId: str2(ghl?.locationId), triggerTag: str2(ghl?.triggerTag, 60) });
|
|
18822
|
+
if (g) rec.ghl = g;
|
|
18823
|
+
rec.demoDays = num(demoDays) ?? 7;
|
|
18824
|
+
return rec;
|
|
18825
|
+
}
|
|
18793
18826
|
function sanitizePlanEvidence(raw) {
|
|
18794
18827
|
if (!raw || typeof raw !== "object") return void 0;
|
|
18795
18828
|
const r = raw;
|
|
@@ -18839,7 +18872,8 @@ var init_agency_profile = __esm({
|
|
|
18839
18872
|
"defaults",
|
|
18840
18873
|
"agencyOs",
|
|
18841
18874
|
"canCreateSubAccounts",
|
|
18842
|
-
"ghlPlanEvidence"
|
|
18875
|
+
"ghlPlanEvidence",
|
|
18876
|
+
"clientFilesDir"
|
|
18843
18877
|
];
|
|
18844
18878
|
NESTED = /* @__PURE__ */ new Set(["sender", "pricing", "delivery", "defaults", "agencyOs"]);
|
|
18845
18879
|
isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -20487,8 +20521,8 @@ function summarisePrice(sub) {
|
|
|
20487
20521
|
unpriced = true;
|
|
20488
20522
|
continue;
|
|
20489
20523
|
}
|
|
20490
|
-
const
|
|
20491
|
-
const qty = typeof
|
|
20524
|
+
const q3 = i.quantity;
|
|
20525
|
+
const qty = typeof q3 === "number" && isFinite(q3) && q3 > 0 ? Math.round(q3) : 1;
|
|
20492
20526
|
total += unit * qty;
|
|
20493
20527
|
}
|
|
20494
20528
|
if (unpriced) {
|
|
@@ -20816,7 +20850,7 @@ function calendarSetupDetail() {
|
|
|
20816
20850
|
}
|
|
20817
20851
|
function openBrowser(url) {
|
|
20818
20852
|
const [file2, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
20819
|
-
return new Promise((
|
|
20853
|
+
return new Promise((resolve13) => (0, import_node_child_process.execFile)(file2, args, { timeout: 5e3 }, (error) => resolve13(!error)));
|
|
20820
20854
|
}
|
|
20821
20855
|
function parsed(schema, value) {
|
|
20822
20856
|
const result = schema.safeParse(value);
|
|
@@ -20913,16 +20947,16 @@ function createGoogleCalendar(deps = {}) {
|
|
|
20913
20947
|
requireOnlyCalendar(result.scope);
|
|
20914
20948
|
return result.access_token;
|
|
20915
20949
|
}
|
|
20916
|
-
async function pages(
|
|
20950
|
+
async function pages(path42, access, item, query = {}) {
|
|
20917
20951
|
const result = [];
|
|
20918
20952
|
const seen = /* @__PURE__ */ new Set();
|
|
20919
20953
|
let pageToken;
|
|
20920
20954
|
for (let page = 0; page < 100; page++) {
|
|
20921
|
-
const url = new URL(`${API}${
|
|
20955
|
+
const url = new URL(`${API}${path42}`);
|
|
20922
20956
|
for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
|
|
20923
20957
|
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
|
20924
20958
|
const data = parsed(
|
|
20925
|
-
import_zod52.z.object({ items: import_zod52.z.array(item).optional(), nextPageToken: import_zod52.z.string().optional(), kind: import_zod52.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (
|
|
20959
|
+
import_zod52.z.object({ items: import_zod52.z.array(item).optional(), nextPageToken: import_zod52.z.string().optional(), kind: import_zod52.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path42.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
|
|
20926
20960
|
await request(url.href, { headers: { Authorization: `Bearer ${access}` } })
|
|
20927
20961
|
);
|
|
20928
20962
|
result.push(...data.items ?? []);
|
|
@@ -20974,9 +21008,9 @@ function createGoogleCalendar(deps = {}) {
|
|
|
20974
21008
|
server2.requestTimeout = 5e3;
|
|
20975
21009
|
server2.headersTimeout = 5e3;
|
|
20976
21010
|
try {
|
|
20977
|
-
await new Promise((
|
|
21011
|
+
await new Promise((resolve13, reject) => {
|
|
20978
21012
|
server2.once("error", reject);
|
|
20979
|
-
server2.listen(0, "127.0.0.1",
|
|
21013
|
+
server2.listen(0, "127.0.0.1", resolve13);
|
|
20980
21014
|
});
|
|
20981
21015
|
} catch {
|
|
20982
21016
|
server2.close();
|
|
@@ -21173,8 +21207,8 @@ function redactConnectionKey(value, key) {
|
|
|
21173
21207
|
return value;
|
|
21174
21208
|
}
|
|
21175
21209
|
function stripeFetcher(key, signal) {
|
|
21176
|
-
return async (
|
|
21177
|
-
const url = new URL(`https://api.stripe.com${
|
|
21210
|
+
return async (path42, query) => {
|
|
21211
|
+
const url = new URL(`https://api.stripe.com${path42}`);
|
|
21178
21212
|
for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
|
|
21179
21213
|
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, signal });
|
|
21180
21214
|
if (!res.ok) {
|
|
@@ -21263,11 +21297,11 @@ var init_handlers = __esm({
|
|
|
21263
21297
|
});
|
|
21264
21298
|
|
|
21265
21299
|
// src/command-os/connectors/stripe-read.ts
|
|
21266
|
-
async function listAll(f,
|
|
21300
|
+
async function listAll(f, path42, schema, extra = {}) {
|
|
21267
21301
|
const out = [];
|
|
21268
21302
|
let after;
|
|
21269
21303
|
for (let page = 0; page < 20; page++) {
|
|
21270
|
-
const result = import_zod53.z.object({ data: import_zod53.z.array(schema), has_more: import_zod53.z.boolean().optional() }).parse(await f(
|
|
21304
|
+
const result = import_zod53.z.object({ data: import_zod53.z.array(schema), has_more: import_zod53.z.boolean().optional() }).parse(await f(path42, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
|
|
21271
21305
|
out.push(...result.data);
|
|
21272
21306
|
if (!result.has_more) return out;
|
|
21273
21307
|
const last = import_zod53.z.object({ id: import_zod53.z.string().min(1) }).parse(result.data.at(-1));
|
|
@@ -21397,8 +21431,8 @@ function sanitizeEngagements(raw) {
|
|
|
21397
21431
|
for (const [id, rec] of Object.entries(src).slice(0, MAX_CLIENTS)) {
|
|
21398
21432
|
const key = str2(id, 64);
|
|
21399
21433
|
if (!key || FORBIDDEN_KEY.has(key)) continue;
|
|
21400
|
-
const
|
|
21401
|
-
if (
|
|
21434
|
+
const clean2 = sanitizeEngagement(rec);
|
|
21435
|
+
if (clean2) clients[key] = clean2;
|
|
21402
21436
|
}
|
|
21403
21437
|
}
|
|
21404
21438
|
return { v: 1, clients: { ...clients }, updated_at: str2(r.updated_at, 40) };
|
|
@@ -21879,16 +21913,16 @@ function deriveFieldKey(fieldName) {
|
|
|
21879
21913
|
function intakeFieldName(label) {
|
|
21880
21914
|
return `${INTAKE_FIELD_NAME_PREFIX}${label}`;
|
|
21881
21915
|
}
|
|
21882
|
-
function expectedFieldKey(
|
|
21883
|
-
return deriveFieldKey(intakeFieldName(
|
|
21916
|
+
function expectedFieldKey(q3) {
|
|
21917
|
+
return deriveFieldKey(intakeFieldName(q3.label));
|
|
21884
21918
|
}
|
|
21885
21919
|
function intakeSectionCount(questions = INTAKE_QUESTIONS) {
|
|
21886
21920
|
let count2 = 0;
|
|
21887
21921
|
let last;
|
|
21888
|
-
for (const
|
|
21889
|
-
if (
|
|
21922
|
+
for (const q3 of questions) {
|
|
21923
|
+
if (q3.section !== last) {
|
|
21890
21924
|
count2++;
|
|
21891
|
-
last =
|
|
21925
|
+
last = q3.section;
|
|
21892
21926
|
}
|
|
21893
21927
|
}
|
|
21894
21928
|
return count2;
|
|
@@ -24902,15 +24936,15 @@ function shimTarget(shim) {
|
|
|
24902
24936
|
return null;
|
|
24903
24937
|
}
|
|
24904
24938
|
const refs = [...text2.matchAll(/"%~?dp0%?\\([^"\r\n]+)"/g)].map((m) => m[1]);
|
|
24905
|
-
const
|
|
24939
|
+
const resolve13 = (rel) => path17.join(path17.dirname(shim), ...rel.split(/[\\/]/));
|
|
24906
24940
|
const script = refs.find((r) => /\.(?:js|cjs|mjs)$/i.test(r));
|
|
24907
24941
|
if (script) {
|
|
24908
|
-
const p =
|
|
24942
|
+
const p = resolve13(script);
|
|
24909
24943
|
return fs18.existsSync(p) ? { kind: "script", path: p } : null;
|
|
24910
24944
|
}
|
|
24911
24945
|
const program = refs.find((r) => /\.exe$/i.test(r) && !/(^|[\\/])node\.exe$/i.test(r));
|
|
24912
24946
|
if (program) {
|
|
24913
|
-
const p =
|
|
24947
|
+
const p = resolve13(program);
|
|
24914
24948
|
return fs18.existsSync(p) ? { kind: "program", path: p } : null;
|
|
24915
24949
|
}
|
|
24916
24950
|
return null;
|
|
@@ -25032,12 +25066,12 @@ function noteObservedEngine(observed, file2 = enginePath()) {
|
|
|
25032
25066
|
return true;
|
|
25033
25067
|
}
|
|
25034
25068
|
function runSmoke(bin, timeoutMs = 9e4) {
|
|
25035
|
-
return new Promise((
|
|
25069
|
+
return new Promise((resolve13) => {
|
|
25036
25070
|
let settled = false;
|
|
25037
25071
|
const done = (r) => {
|
|
25038
25072
|
if (!settled) {
|
|
25039
25073
|
settled = true;
|
|
25040
|
-
|
|
25074
|
+
resolve13(r);
|
|
25041
25075
|
}
|
|
25042
25076
|
};
|
|
25043
25077
|
const cmd = claudeCommand(bin, [
|
|
@@ -25262,13 +25296,13 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
25262
25296
|
const locationName = SANDBOX_ALLOWLIST[locationId2] || opts.name || locationId2;
|
|
25263
25297
|
const spec = opts.spec ?? STAGE_SPECS[stage];
|
|
25264
25298
|
if (!spec) return Promise.reject(new Error(`Stage ${stage} is not powered yet.`));
|
|
25265
|
-
return new Promise((
|
|
25299
|
+
return new Promise((resolve13) => {
|
|
25266
25300
|
void (async () => {
|
|
25267
25301
|
const bin = claudeBin();
|
|
25268
25302
|
const engineCheck = await ensureEngineReady(bin, (line3) => onEvent({ kind: "status", line: line3 }));
|
|
25269
25303
|
if (!engineCheck.ok) {
|
|
25270
25304
|
onEvent({ kind: "error", line: engineCheck.reason });
|
|
25271
|
-
|
|
25305
|
+
resolve13({ ok: false, error: engineCheck.reason, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
|
|
25272
25306
|
return;
|
|
25273
25307
|
}
|
|
25274
25308
|
onEvent({ kind: "status", line: `Starting ${spec.name} for ${locationName}.` });
|
|
@@ -25360,7 +25394,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
25360
25394
|
if (code !== 0 && !lastText) {
|
|
25361
25395
|
const outcome2 = { ok: false, error: `claude exited ${code}: ${stderrTail.trim().slice(-200) || "no output"}`, ...engine };
|
|
25362
25396
|
onEvent({ kind: "error", line: outcome2.error });
|
|
25363
|
-
|
|
25397
|
+
resolve13(outcome2);
|
|
25364
25398
|
return;
|
|
25365
25399
|
}
|
|
25366
25400
|
const outcome = { ...parseResultLine(lastText), ...engine };
|
|
@@ -25373,19 +25407,19 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
25373
25407
|
const detail = outcome.error || (outcome.issues?.length ? outcome.issues.join(" \xB7 ") : "") || outcome.summary || "no detail reported";
|
|
25374
25408
|
const success = outcome.formId ? `Done: form "${outcome.formName}" (${outcome.formId})` : `Done: ${outcome.summary ?? "stage complete"}`;
|
|
25375
25409
|
onEvent({ kind: "result", line: outcome.ok ? success : `Not passed: ${detail}` });
|
|
25376
|
-
|
|
25410
|
+
resolve13(outcome);
|
|
25377
25411
|
});
|
|
25378
25412
|
child.on("error", (err) => {
|
|
25379
25413
|
clearTimeout(timeout);
|
|
25380
25414
|
if (progressPoll) clearInterval(progressPoll);
|
|
25381
25415
|
const outcome = { ok: false, error: `could not start claude: ${err.message}` };
|
|
25382
25416
|
onEvent({ kind: "error", line: outcome.error });
|
|
25383
|
-
|
|
25417
|
+
resolve13(outcome);
|
|
25384
25418
|
});
|
|
25385
25419
|
})().catch((err) => {
|
|
25386
25420
|
const line3 = err instanceof Error ? err.message : String(err);
|
|
25387
25421
|
onEvent({ kind: "error", line: line3 });
|
|
25388
|
-
|
|
25422
|
+
resolve13({ ok: false, error: line3 });
|
|
25389
25423
|
});
|
|
25390
25424
|
});
|
|
25391
25425
|
}
|
|
@@ -26245,8 +26279,8 @@ function lintDesignContract(html2, medium) {
|
|
|
26245
26279
|
for (let i = 0; i < sectionTags.length; i++) {
|
|
26246
26280
|
const start = sectionTags[i].index;
|
|
26247
26281
|
const end = i + 1 < sectionTags.length ? sectionTags[i + 1].index : html2.length;
|
|
26248
|
-
const
|
|
26249
|
-
headlinesPerSection.push(
|
|
26282
|
+
const inside3 = opens.filter((t) => t.index > start && t.index < end);
|
|
26283
|
+
headlinesPerSection.push(inside3.filter((t) => t.attrs["data-role"] === "headline").length);
|
|
26250
26284
|
}
|
|
26251
26285
|
for (const t of opens) {
|
|
26252
26286
|
const r = t.attrs["data-role"];
|
|
@@ -26584,13 +26618,13 @@ var init_handoff_pack = __esm({
|
|
|
26584
26618
|
|
|
26585
26619
|
// src/command-os/handoff-run.ts
|
|
26586
26620
|
function askNoTools(prompt, opts = {}) {
|
|
26587
|
-
return new Promise((
|
|
26621
|
+
return new Promise((resolve13) => {
|
|
26588
26622
|
let bin;
|
|
26589
26623
|
try {
|
|
26590
26624
|
bin = claudeBin();
|
|
26591
26625
|
} catch {
|
|
26592
26626
|
opts.onCost?.();
|
|
26593
|
-
|
|
26627
|
+
resolve13("");
|
|
26594
26628
|
return;
|
|
26595
26629
|
}
|
|
26596
26630
|
const cmd = claudeCommand(bin, ["-p", prompt, "--output-format", "stream-json", "--verbose", "--max-turns", String(opts.maxTurns ?? 3), "--allowedTools", "", "--disallowedTools", NO_TOOLS_DENY]);
|
|
@@ -26628,12 +26662,12 @@ function askNoTools(prompt, opts = {}) {
|
|
|
26628
26662
|
child.on("error", () => {
|
|
26629
26663
|
clearTimeout(timeout);
|
|
26630
26664
|
report2();
|
|
26631
|
-
|
|
26665
|
+
resolve13(lastText);
|
|
26632
26666
|
});
|
|
26633
26667
|
child.on("close", () => {
|
|
26634
26668
|
clearTimeout(timeout);
|
|
26635
26669
|
report2();
|
|
26636
|
-
|
|
26670
|
+
resolve13(lastText);
|
|
26637
26671
|
});
|
|
26638
26672
|
});
|
|
26639
26673
|
}
|
|
@@ -27726,8 +27760,8 @@ function dailyBrief(input) {
|
|
|
27726
27760
|
} else if (renewals.length) {
|
|
27727
27761
|
headline3 = `${renewals[0].client}'s agreement renews ${renewals[0].inWords}.`;
|
|
27728
27762
|
} else if (payingButQuiet.length) {
|
|
27729
|
-
const
|
|
27730
|
-
headline3 = payingButQuiet.length === 1 ? `${
|
|
27763
|
+
const q3 = payingButQuiet[0];
|
|
27764
|
+
headline3 = payingButQuiet.length === 1 ? `${q3.client} is paying you and has booked nothing in ${q3.days} days.` : `${payingButQuiet.length} clients are paying you and have booked nothing in ${q3.days} days. The largest is ${q3.client}.`;
|
|
27731
27765
|
} else if (underFloor.length) {
|
|
27732
27766
|
headline3 = `Nothing is waiting, but ${underFloor.length} client${underFloor.length === 1 ? " is" : "s are"} billed below your own floor.`;
|
|
27733
27767
|
} else if (newLeads.length) {
|
|
@@ -28057,7 +28091,7 @@ function stopReasonSentence(reason3, fetched, cap) {
|
|
|
28057
28091
|
}
|
|
28058
28092
|
}
|
|
28059
28093
|
function delay(ms) {
|
|
28060
|
-
return new Promise((
|
|
28094
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
28061
28095
|
}
|
|
28062
28096
|
async function fetchAllContacts(client, locationId2, options = {}) {
|
|
28063
28097
|
const cap = options.maxContacts ?? DEFAULT_MAX_CONTACTS;
|
|
@@ -28763,7 +28797,7 @@ var init_client_capabilities = __esm({
|
|
|
28763
28797
|
{ key: "reputation", label: "Reputation and content", blurb: "Reviews, social posts and the blog." },
|
|
28764
28798
|
{ key: "health", label: "Health and handoff", blurb: "Proving it works, keeping it clean, and handing it over." }
|
|
28765
28799
|
];
|
|
28766
|
-
N = (label,
|
|
28800
|
+
N = (label, path42, params, key, nameField, version) => ({ label, path: path42, params, key, nameField, version });
|
|
28767
28801
|
AI2 = "2021-04-15";
|
|
28768
28802
|
CAPABILITIES = [
|
|
28769
28803
|
// ── Set up ──────────────────────────────────────────────────────────────
|
|
@@ -29024,7 +29058,7 @@ var require_package = __commonJS({
|
|
|
29024
29058
|
"package.json"(exports2, module2) {
|
|
29025
29059
|
module2.exports = {
|
|
29026
29060
|
name: "@elitedcs/ghl-mcp",
|
|
29027
|
-
version: "3.84.
|
|
29061
|
+
version: "3.84.8",
|
|
29028
29062
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
29029
29063
|
description: "GoHighLevel MCP Server for Claude. 250 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
29030
29064
|
main: "dist/index.js",
|
|
@@ -31395,20 +31429,20 @@ function clientHeadline(items) {
|
|
|
31395
31429
|
const live = items.filter((i) => i.state === "in-place").length;
|
|
31396
31430
|
const notYet = items.filter((i) => i.state === "not-yet").length;
|
|
31397
31431
|
const waiting = items.filter((i) => i.state === "waiting").length;
|
|
31398
|
-
const
|
|
31432
|
+
const plural2 = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
31399
31433
|
if (!items.length) return { headline: "Your account has not been checked yet", sub: "As soon as we run the first check, this page fills in on its own." };
|
|
31400
31434
|
if (!notYet && !waiting) {
|
|
31401
31435
|
return {
|
|
31402
31436
|
headline: "Everything we set out to build is live",
|
|
31403
|
-
sub: `All ${
|
|
31437
|
+
sub: `All ${plural2(live, "item was", "items were")} read back from your live account, not assumed. This page re-checks itself and updates here, so it is always current.`
|
|
31404
31438
|
};
|
|
31405
31439
|
}
|
|
31406
31440
|
const parts = [];
|
|
31407
|
-
if (live) parts.push(`${
|
|
31408
|
-
if (notYet) parts.push(`${
|
|
31409
|
-
if (waiting) parts.push(`${
|
|
31441
|
+
if (live) parts.push(`${plural2(live, "thing is", "things are")} live in your account`);
|
|
31442
|
+
if (notYet) parts.push(`${plural2(notYet, "is", "are")} built but not confirmed yet`);
|
|
31443
|
+
if (waiting) parts.push(`${plural2(waiting, "is", "are")} waiting on someone else`);
|
|
31410
31444
|
return {
|
|
31411
|
-
headline: notYet ? `${
|
|
31445
|
+
headline: notYet ? `${plural2(notYet, "item", "items")} still to confirm` : `${plural2(waiting, "item is", "items are")} waiting on someone else`,
|
|
31412
31446
|
sub: `${parts.join(", ").replace(/, ([^,]*)$/, " and $1")}. Everything below was read back from your live account.`
|
|
31413
31447
|
};
|
|
31414
31448
|
}
|
|
@@ -31912,9 +31946,9 @@ function intakeInput(answers, industry, questions) {
|
|
|
31912
31946
|
}
|
|
31913
31947
|
function shortenPlanLine(text2, limit = 360) {
|
|
31914
31948
|
if (text2.length <= limit) return text2;
|
|
31915
|
-
const
|
|
31949
|
+
const tokens2 = text2.match(/(?:\[[^\]]*\]|[^\s[])+/g) ?? [];
|
|
31916
31950
|
let end = "";
|
|
31917
|
-
for (const token2 of
|
|
31951
|
+
for (const token2 of tokens2) {
|
|
31918
31952
|
const next = end ? `${end} ${token2}` : token2;
|
|
31919
31953
|
if (next.length > limit - 1) {
|
|
31920
31954
|
if (!end) return token2.length === text2.length ? text2 : `${token2}\u2026`;
|
|
@@ -31982,8 +32016,8 @@ function executablePlanLines(saved) {
|
|
|
31982
32016
|
const refs = [...names.keys()].sort((a, b) => b.length - a.length);
|
|
31983
32017
|
const refPattern = refs.length ? new RegExp(`(?<![\\w./:@])(?:${refs.map(escapeRE).join("|")})(?![\\w]|\\.[\\w])`, "g") : void 0;
|
|
31984
32018
|
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;
|
|
31985
|
-
const
|
|
31986
|
-
const renderRefs = (s) =>
|
|
32019
|
+
const resolve13 = (s) => s.replace(missingRefPattern, (ref) => names.get(ref) || `Unnamed ${kindOf2(ref.split(".")[0])}`);
|
|
32020
|
+
const renderRefs = (s) => resolve13(refPattern ? s.replace(refPattern, (ref) => names.get(ref)) : s);
|
|
31987
32021
|
const mergeLabel = (field) => {
|
|
31988
32022
|
const [group, ...parts] = field.trim().split(".");
|
|
31989
32023
|
const key = parts.join(".");
|
|
@@ -32954,7 +32988,7 @@ var init_run_cost = __esm({
|
|
|
32954
32988
|
// src/claude-call.ts
|
|
32955
32989
|
function callClaudeRecorded(bin, argv, context, onCost = () => {
|
|
32956
32990
|
}) {
|
|
32957
|
-
return new Promise((
|
|
32991
|
+
return new Promise((resolve13, reject) => {
|
|
32958
32992
|
let text2 = "";
|
|
32959
32993
|
let buffer = "";
|
|
32960
32994
|
let stderr = "";
|
|
@@ -32973,7 +33007,7 @@ function callClaudeRecorded(bin, argv, context, onCost = () => {
|
|
|
32973
33007
|
onCost("The cost record could not be saved.");
|
|
32974
33008
|
}
|
|
32975
33009
|
onCost(`${context.kind}: ${facts.costUsd === void 0 ? "cost not reported" : formatUsd(facts.costUsd)}.`);
|
|
32976
|
-
if (ok)
|
|
33010
|
+
if (ok) resolve13(text2);
|
|
32977
33011
|
else reject(new Error("Check your Claude authentication in Your agency before reading this source."));
|
|
32978
33012
|
};
|
|
32979
33013
|
const consume = (line3) => {
|
|
@@ -33082,9 +33116,9 @@ function unknownKeys(layer, against) {
|
|
|
33082
33116
|
return referenced.filter((k) => !have.has(k));
|
|
33083
33117
|
}
|
|
33084
33118
|
function installQuestionSet(opts = {}) {
|
|
33085
|
-
return composeQuestionSet(opts).map((
|
|
33086
|
-
const base = INTAKE_QUESTIONS.find((b) => b.key ===
|
|
33087
|
-
return base && base.label !==
|
|
33119
|
+
return composeQuestionSet(opts).map((q3) => {
|
|
33120
|
+
const base = INTAKE_QUESTIONS.find((b) => b.key === q3.key);
|
|
33121
|
+
return base && base.label !== q3.label ? { ...q3, label: base.label } : q3;
|
|
33088
33122
|
});
|
|
33089
33123
|
}
|
|
33090
33124
|
function validatePacks() {
|
|
@@ -33391,7 +33425,7 @@ function parsePricePoints(text2) {
|
|
|
33391
33425
|
const out = [];
|
|
33392
33426
|
const segments = text2.split(/\r?\n|;|\.\s+/);
|
|
33393
33427
|
const ENTRY = /^(.*?)\s*[-–—:|]?\s*\$?\s*([\d,]+(?:\.\d+)?)\s*(?:[-–—]\s*\$?\s*([\d,]+(?:\.\d+)?))?\s*(\/\s*[a-z]+|(?:per|an?|each)\s+[a-z]+)?\s*$/i;
|
|
33394
|
-
const
|
|
33428
|
+
const clean2 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
|
|
33395
33429
|
const num3 = (s) => s.replace(/,/g, "");
|
|
33396
33430
|
const recur = (s) => s ? "/" + s.replace(/^\/|^(?:per|an?|each)\b/i, "").trim().toLowerCase() : "";
|
|
33397
33431
|
for (const segment of segments) {
|
|
@@ -33399,7 +33433,7 @@ function parsePricePoints(text2) {
|
|
|
33399
33433
|
if (!t) continue;
|
|
33400
33434
|
const m = t.match(ENTRY);
|
|
33401
33435
|
if (m && m[2]) {
|
|
33402
|
-
const name =
|
|
33436
|
+
const name = clean2(m[1]);
|
|
33403
33437
|
const price = (m[3] ? `${num3(m[2])}-${num3(m[3])}` : num3(m[2])) + recur(m[4]);
|
|
33404
33438
|
out.push({ name: name || t, price });
|
|
33405
33439
|
} else {
|
|
@@ -33529,11 +33563,11 @@ function splitOnMarkers(line3) {
|
|
|
33529
33563
|
}
|
|
33530
33564
|
function splitOnAnd(entry) {
|
|
33531
33565
|
if (countDurations(entry) < 2) return [entry];
|
|
33532
|
-
const
|
|
33533
|
-
if (
|
|
33566
|
+
const tokens2 = entry.split(/,?\s+and\s+/i);
|
|
33567
|
+
if (tokens2.length < 2) return [entry];
|
|
33534
33568
|
const out = [];
|
|
33535
33569
|
let acc = "";
|
|
33536
|
-
for (const tok of
|
|
33570
|
+
for (const tok of tokens2) {
|
|
33537
33571
|
if (acc && countDurations(acc) > 0 && countDurations(tok) > 0) {
|
|
33538
33572
|
out.push(acc);
|
|
33539
33573
|
acc = tok;
|
|
@@ -33748,9 +33782,9 @@ function presetForBusinessType(type) {
|
|
|
33748
33782
|
return "generic";
|
|
33749
33783
|
}
|
|
33750
33784
|
}
|
|
33751
|
-
function setPath(target,
|
|
33785
|
+
function setPath(target, path42, value) {
|
|
33752
33786
|
if (value === void 0) return;
|
|
33753
|
-
const parts =
|
|
33787
|
+
const parts = path42.split(".");
|
|
33754
33788
|
let node = target;
|
|
33755
33789
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
33756
33790
|
const k = parts[i];
|
|
@@ -33785,10 +33819,10 @@ function buildFieldMapFromFormFields(fields, questions = INTAKE_QUESTIONS) {
|
|
|
33785
33819
|
if (fieldKey && id) byFieldKey.set(fieldKey.toLowerCase(), id);
|
|
33786
33820
|
}
|
|
33787
33821
|
const map = {};
|
|
33788
|
-
for (const
|
|
33789
|
-
if (
|
|
33790
|
-
const id = byFieldKey.get(expectedFieldKey(
|
|
33791
|
-
if (id) map[
|
|
33822
|
+
for (const q3 of questions) {
|
|
33823
|
+
if (q3.field.kind !== "custom") continue;
|
|
33824
|
+
const id = byFieldKey.get(expectedFieldKey(q3).toLowerCase());
|
|
33825
|
+
if (id) map[q3.key] = id;
|
|
33792
33826
|
}
|
|
33793
33827
|
return map;
|
|
33794
33828
|
}
|
|
@@ -33850,11 +33884,11 @@ function normalizeSubmissionToBrief(opts) {
|
|
|
33850
33884
|
goal: {}
|
|
33851
33885
|
};
|
|
33852
33886
|
const warnings = [];
|
|
33853
|
-
const rawFor = (
|
|
33854
|
-
const rawKey =
|
|
33887
|
+
const rawFor = (q3) => {
|
|
33888
|
+
const rawKey = q3.field.kind === "standard" ? q3.field.tag : opts.fieldMap[q3.key];
|
|
33855
33889
|
return rawKey ? opts.others[rawKey] : void 0;
|
|
33856
33890
|
};
|
|
33857
|
-
const staffQ = questions.find((
|
|
33891
|
+
const staffQ = questions.find((q3) => q3.briefKind === "staffList" && q3.briefPath);
|
|
33858
33892
|
let staff;
|
|
33859
33893
|
if (staffQ) {
|
|
33860
33894
|
const s = firstString(rawFor(staffQ));
|
|
@@ -33865,22 +33899,22 @@ function normalizeSubmissionToBrief(opts) {
|
|
|
33865
33899
|
}
|
|
33866
33900
|
}
|
|
33867
33901
|
const staffNames = (staff ?? []).map((m) => m.name);
|
|
33868
|
-
for (const
|
|
33869
|
-
if (!
|
|
33902
|
+
for (const q3 of questions) {
|
|
33903
|
+
if (!q3.briefPath || q3.briefKind === "identity") continue;
|
|
33870
33904
|
let value;
|
|
33871
|
-
if (
|
|
33905
|
+
if (q3 === staffQ) {
|
|
33872
33906
|
value = staff;
|
|
33873
|
-
} else if (
|
|
33874
|
-
const s = firstString(rawFor(
|
|
33907
|
+
} else if (q3.briefKind === "calendarList") {
|
|
33908
|
+
const s = firstString(rawFor(q3));
|
|
33875
33909
|
if (s) {
|
|
33876
33910
|
const r = parseCalendarLines(s, staffNames);
|
|
33877
33911
|
warnings.push(...r.warnings);
|
|
33878
33912
|
value = r.calendars.length ? r.calendars : void 0;
|
|
33879
33913
|
}
|
|
33880
33914
|
} else {
|
|
33881
|
-
value = coerceValue(rawFor(
|
|
33915
|
+
value = coerceValue(rawFor(q3), q3.briefKind, warnings);
|
|
33882
33916
|
}
|
|
33883
|
-
setPath(brief,
|
|
33917
|
+
setPath(brief, q3.briefPath, value);
|
|
33884
33918
|
}
|
|
33885
33919
|
distillAssignments(brief, staffNames, warnings);
|
|
33886
33920
|
if (warnings.length) brief.warnings = warnings;
|
|
@@ -34790,7 +34824,7 @@ function compute(ans, skip, asm) {
|
|
|
34790
34824
|
var monthly = F.reduce(function(s, f) {
|
|
34791
34825
|
return s + f.monthly;
|
|
34792
34826
|
}, 0);
|
|
34793
|
-
var
|
|
34827
|
+
var stop2 = monthly * a("recovery");
|
|
34794
34828
|
var oneoff = null;
|
|
34795
34829
|
if (core && raw("dormant_count") !== null && a("reactivation") > 0) {
|
|
34796
34830
|
oneoff = raw("dormant_count") * a("reactivation") * c * v;
|
|
@@ -34823,8 +34857,8 @@ function compute(ans, skip, asm) {
|
|
|
34823
34857
|
core,
|
|
34824
34858
|
monthly,
|
|
34825
34859
|
annual: monthly * 12,
|
|
34826
|
-
stop,
|
|
34827
|
-
annualStop:
|
|
34860
|
+
stop: stop2,
|
|
34861
|
+
annualStop: stop2 * 12,
|
|
34828
34862
|
oneoff,
|
|
34829
34863
|
hrs,
|
|
34830
34864
|
hrsWk,
|
|
@@ -34926,8 +34960,8 @@ var init_engine = __esm({
|
|
|
34926
34960
|
show_rate: "How many booked appointments actually happen"
|
|
34927
34961
|
};
|
|
34928
34962
|
UNIT = {};
|
|
34929
|
-
Q.forEach(function(
|
|
34930
|
-
UNIT[
|
|
34963
|
+
Q.forEach(function(q3) {
|
|
34964
|
+
UNIT[q3[0]] = q3[4];
|
|
34931
34965
|
});
|
|
34932
34966
|
}
|
|
34933
34967
|
});
|
|
@@ -34946,7 +34980,7 @@ function stageFor(opts) {
|
|
|
34946
34980
|
function tagFor(stage) {
|
|
34947
34981
|
return "assessment:" + stage.toLowerCase().replace(/\s+/g, "-");
|
|
34948
34982
|
}
|
|
34949
|
-
function renderHuman(state, computed,
|
|
34983
|
+
function renderHuman(state, computed, q3, today2) {
|
|
34950
34984
|
const lines = [];
|
|
34951
34985
|
lines.push(`${NOTE_TITLE} \u2014 ${state.biz || "unnamed"}${state.trade ? " \u2014 " + state.trade : ""} \u2014 ${today2}`);
|
|
34952
34986
|
if (computed) {
|
|
@@ -34961,8 +34995,8 @@ function renderHuman(state, computed, q2, today2) {
|
|
|
34961
34995
|
const byArea = /* @__PURE__ */ new Map();
|
|
34962
34996
|
const ids = [.../* @__PURE__ */ new Set([...Object.keys(state.ans), ...Object.keys(state.skip)])];
|
|
34963
34997
|
for (const id of ids) {
|
|
34964
|
-
const ask =
|
|
34965
|
-
const area =
|
|
34998
|
+
const ask = q3.ask(id);
|
|
34999
|
+
const area = q3.area(id);
|
|
34966
35000
|
if (!ask || !area) continue;
|
|
34967
35001
|
const skipped = state.skip[id] === true;
|
|
34968
35002
|
const raw = state.ans[id];
|
|
@@ -34974,7 +35008,7 @@ function renderHuman(state, computed, q2, today2) {
|
|
|
34974
35008
|
${value}`);
|
|
34975
35009
|
byArea.set(area, bucket);
|
|
34976
35010
|
}
|
|
34977
|
-
for (const area of
|
|
35011
|
+
for (const area of q3.areaOrder()) {
|
|
34978
35012
|
const bucket = byArea.get(area);
|
|
34979
35013
|
if (!bucket || bucket.length === 0) continue;
|
|
34980
35014
|
lines.push("");
|
|
@@ -34986,7 +35020,7 @@ function renderHuman(state, computed, q2, today2) {
|
|
|
34986
35020
|
lines.push("");
|
|
34987
35021
|
lines.push("ASSUMPTIONS THE OPERATOR SET");
|
|
34988
35022
|
for (const [k, v] of asm) {
|
|
34989
|
-
const label =
|
|
35023
|
+
const label = q3.assumption?.(k) ?? k.replace(/_/g, " ");
|
|
34990
35024
|
lines.push(` ${label}: ${Math.round((Number(v) || 0) * 100)}%${Number(v) > 0 ? "" : " (set to zero \u2014 its finding was dropped, not estimated)"}`);
|
|
34991
35025
|
}
|
|
34992
35026
|
}
|
|
@@ -35007,9 +35041,23 @@ function freshSalt() {
|
|
|
35007
35041
|
function sanitiseHuman(text2) {
|
|
35008
35042
|
return text2.split(MACHINE_MARKER).join("--- (marker text removed) ---");
|
|
35009
35043
|
}
|
|
35010
|
-
function
|
|
35011
|
-
const
|
|
35012
|
-
|
|
35044
|
+
function describeSource(src) {
|
|
35045
|
+
const title2 = src.title?.trim() ? ` "${src.title.trim()}"` : "";
|
|
35046
|
+
const by = src.fetchedBy === "claude" ? ", fetched by Claude" : src.fetchedBy === "upload" ? ", uploaded as a file" : "";
|
|
35047
|
+
return `${src.recorder} recording${title2}, recorded ${src.recordedAt}${by}`;
|
|
35048
|
+
}
|
|
35049
|
+
function buildNote(state, computed, q3, today2, source, confirmedByOwner) {
|
|
35050
|
+
const confirmed = confirmedByOwner?.length ? [...confirmedByOwner] : void 0;
|
|
35051
|
+
const payload = JSON.stringify({ v: 1, savedAt: today2, state, computed, ...source ? { source } : {}, ...confirmed ? { confirmedByOwner: confirmed } : {} });
|
|
35052
|
+
const human = renderHuman(state, computed, q3, today2);
|
|
35053
|
+
const lines = [
|
|
35054
|
+
...source ? [`Source: ${describeSource(source)}`] : [],
|
|
35055
|
+
...confirmed ? [`Confirmed by the owner, not heard word for word: ${confirmed.map((id) => q3.ask(id) ?? id).join("; ")}`] : []
|
|
35056
|
+
];
|
|
35057
|
+
const withSource = lines.length ? human.replace(/\n/, `
|
|
35058
|
+
${lines.join("\n")}
|
|
35059
|
+
`) : human;
|
|
35060
|
+
return `${sanitiseHuman(withSource)}
|
|
35013
35061
|
|
|
35014
35062
|
${MACHINE_MARKER}
|
|
35015
35063
|
${payload}`;
|
|
@@ -35034,7 +35082,9 @@ function parseNote(body) {
|
|
|
35034
35082
|
if (!s.ans || typeof s.ans !== "object") return null;
|
|
35035
35083
|
return {
|
|
35036
35084
|
state: { biz: s.biz, trade: s.trade, ans: s.ans, skip: s.skip ?? {}, asm: s.asm ?? {}, ...s.client && typeof s.client === "object" ? { client: s.client } : {} },
|
|
35037
|
-
computed: p.computed ?? null
|
|
35085
|
+
computed: p.computed ?? null,
|
|
35086
|
+
// Which answers the owner confirmed rather than were heard (BUG-2026-09-24-04), so the next save can carry the mark.
|
|
35087
|
+
...Array.isArray(p.confirmedByOwner) ? { confirmedByOwner: p.confirmedByOwner.filter((x) => typeof x === "string") } : {}
|
|
35038
35088
|
};
|
|
35039
35089
|
}
|
|
35040
35090
|
function buildContact(state, stage, contact) {
|
|
@@ -35111,7 +35161,16 @@ function parseNotes(bodies) {
|
|
|
35111
35161
|
${parts.map((p) => p.text).join("")}`);
|
|
35112
35162
|
}
|
|
35113
35163
|
function pickLatest(notes) {
|
|
35114
|
-
|
|
35164
|
+
for (const parsed2 of latestFirst(notes)) if (parsed2) return parsed2;
|
|
35165
|
+
return null;
|
|
35166
|
+
}
|
|
35167
|
+
function pickLatestStrict(notes) {
|
|
35168
|
+
const all = latestFirst(notes);
|
|
35169
|
+
if (all.length === 0) return { record: null, clean: true };
|
|
35170
|
+
return { record: all[0], clean: all[0] !== null };
|
|
35171
|
+
}
|
|
35172
|
+
function latestFirst(notes) {
|
|
35173
|
+
if (!Array.isArray(notes)) return [];
|
|
35115
35174
|
const groups = /* @__PURE__ */ new Map();
|
|
35116
35175
|
notes.forEach((note, i) => {
|
|
35117
35176
|
const body = typeof note?.body === "string" ? note.body : null;
|
|
@@ -35129,18 +35188,21 @@ function pickLatest(notes) {
|
|
|
35129
35188
|
groups.set(key, g);
|
|
35130
35189
|
});
|
|
35131
35190
|
const ordered = [...groups.values()].sort((a, b) => a.when === b.when ? 0 : a.when > b.when ? -1 : 1);
|
|
35191
|
+
const out = [];
|
|
35132
35192
|
for (const g of ordered) {
|
|
35133
35193
|
const newestPerK = /* @__PURE__ */ new Map();
|
|
35134
35194
|
for (const p of g.parts) {
|
|
35135
35195
|
const seen = newestPerK.get(p.k);
|
|
35136
35196
|
if (!seen || p.when > seen.when) newestPerK.set(p.k, p);
|
|
35137
35197
|
}
|
|
35138
|
-
if (newestPerK.size !== g.n)
|
|
35198
|
+
if (newestPerK.size !== g.n) {
|
|
35199
|
+
out.push(null);
|
|
35200
|
+
continue;
|
|
35201
|
+
}
|
|
35139
35202
|
const bodies = [...newestPerK.values()].sort((a, b) => a.k - b.k).map((p) => p.body);
|
|
35140
|
-
|
|
35141
|
-
if (parsed2) return parsed2;
|
|
35203
|
+
out.push(bodies.length === 1 ? parseNote(bodies[0]) : parseNotes(bodies));
|
|
35142
35204
|
}
|
|
35143
|
-
return
|
|
35205
|
+
return out;
|
|
35144
35206
|
}
|
|
35145
35207
|
var PIPELINE_NAME, NOTE_TITLE, MACHINE_MARKER, STAGES, MARKER_LINE, NOTE_LIMIT;
|
|
35146
35208
|
var init_store3 = __esm({
|
|
@@ -35216,13 +35278,13 @@ function count(value) {
|
|
|
35216
35278
|
const n = Number(value);
|
|
35217
35279
|
return Number.isSafeInteger(n) ? n : void 0;
|
|
35218
35280
|
}
|
|
35219
|
-
function matchesQuestionChoices(
|
|
35220
|
-
const fixed =
|
|
35281
|
+
function matchesQuestionChoices(q3, value) {
|
|
35282
|
+
const fixed = q3.options !== void 0 || q3.field.kind === "custom" && ["SINGLE_OPTIONS", "MULTIPLE_OPTIONS", "CHECKBOX"].includes(q3.field.dataType);
|
|
35221
35283
|
if (!fixed) return true;
|
|
35222
35284
|
if (Array.isArray(value)) {
|
|
35223
|
-
return
|
|
35285
|
+
return q3.field.kind === "custom" && ["MULTIPLE_OPTIONS", "CHECKBOX"].includes(q3.field.dataType) && value.every((choice) => q3.options?.includes(choice) === true);
|
|
35224
35286
|
}
|
|
35225
|
-
return
|
|
35287
|
+
return q3.options?.includes(value) === true;
|
|
35226
35288
|
}
|
|
35227
35289
|
function mapAssessmentToIntake(s, questions, contact) {
|
|
35228
35290
|
const values = {
|
|
@@ -35248,14 +35310,14 @@ function mapAssessmentToIntake(s, questions, contact) {
|
|
|
35248
35310
|
const answers = {};
|
|
35249
35311
|
const filled = [], leftForMember = [];
|
|
35250
35312
|
for (const mapping of ASSESSMENT_INTAKE_MAPPING) {
|
|
35251
|
-
const
|
|
35313
|
+
const q3 = questions.find((q4) => q4.key === mapping.target);
|
|
35252
35314
|
const value = values[mapping.target]?.trim();
|
|
35253
|
-
if (!
|
|
35254
|
-
leftForMember.push({ key: mapping.target, question:
|
|
35315
|
+
if (!q3 || !value || !matchesQuestionChoices(q3, value)) {
|
|
35316
|
+
leftForMember.push({ key: mapping.target, question: q3?.label ?? mapping.target, reason: !q3 ? "This question is not in this intake." : !value ? "The assessment does not give this answer." : "The answer does not match one of this intake\u2019s choices." });
|
|
35255
35317
|
continue;
|
|
35256
35318
|
}
|
|
35257
|
-
answers[
|
|
35258
|
-
filled.push(
|
|
35319
|
+
answers[q3.label] = q3.field.kind === "custom" && q3.field.dataType === "MULTIPLE_OPTIONS" ? [value] : value;
|
|
35320
|
+
filled.push(q3.label);
|
|
35259
35321
|
}
|
|
35260
35322
|
return { answers, filled, leftForMember };
|
|
35261
35323
|
}
|
|
@@ -35273,19 +35335,19 @@ function fillFromSnapshot(state, locationId2, questions) {
|
|
|
35273
35335
|
const result = mapAssessmentToIntake(snapshot.assessment, questions, snapshot.contact);
|
|
35274
35336
|
const answers = { ...state.intakeInputs?.[locationId2]?.answers, ...state.intakeDrafts?.[locationId2]?.answers };
|
|
35275
35337
|
for (const [label, value] of Object.entries(result.answers)) {
|
|
35276
|
-
const
|
|
35277
|
-
const previous = answers[label] ?? answers[
|
|
35338
|
+
const q3 = questions.find((q4) => q4.label === label);
|
|
35339
|
+
const previous = answers[label] ?? answers[q3.key];
|
|
35278
35340
|
let merged = value;
|
|
35279
|
-
if ((
|
|
35341
|
+
if ((q3.key === "anything_else" || q3.key === "price_points") && previous && !String(previous).includes(String(value))) merged = `${previous}
|
|
35280
35342
|
${value}`;
|
|
35281
|
-
else if ((
|
|
35282
|
-
if (!matchesQuestionChoices(
|
|
35343
|
+
else if ((q3.key === "anything_else" || q3.key === "price_points") && previous) merged = previous;
|
|
35344
|
+
if (!matchesQuestionChoices(q3, merged)) {
|
|
35283
35345
|
result.filled = result.filled.filter((question) => question !== label);
|
|
35284
|
-
result.leftForMember.push({ key:
|
|
35346
|
+
result.leftForMember.push({ key: q3.key, question: label, reason: "The combined answer does not match this intake\u2019s choices. Your saved answer was left unchanged." });
|
|
35285
35347
|
continue;
|
|
35286
35348
|
}
|
|
35287
35349
|
answers[label] = merged;
|
|
35288
|
-
if (
|
|
35350
|
+
if (q3.key !== label) delete answers[q3.key];
|
|
35289
35351
|
}
|
|
35290
35352
|
return { state: { ...state, intakeDrafts: { ...state.intakeDrafts, [locationId2]: { answers, savedAt: (/* @__PURE__ */ new Date()).toISOString() } } }, filled: result.filled, leftForMember: result.leftForMember };
|
|
35291
35353
|
}
|
|
@@ -35373,10 +35435,10 @@ var init_attachment = __esm({
|
|
|
35373
35435
|
async function pickIntakeContact(client, locationId2, fieldIdToLabel, knownContactId, formName) {
|
|
35374
35436
|
const ids = /* @__PURE__ */ new Set();
|
|
35375
35437
|
if (knownContactId) ids.add(knownContactId);
|
|
35376
|
-
for (const
|
|
35438
|
+
for (const q3 of [...INTAKE_TAGS, ...formName ? [formName] : []]) {
|
|
35377
35439
|
try {
|
|
35378
|
-
const found = await client.get("/contacts/", { params: { locationId: locationId2, query:
|
|
35379
|
-
for (const c of found.contacts ?? []) if ((c.tags ?? []).some((t) => INTAKE_TAGS.includes(t)) ||
|
|
35440
|
+
const found = await client.get("/contacts/", { params: { locationId: locationId2, query: q3, limit: 20 } });
|
|
35441
|
+
for (const c of found.contacts ?? []) if ((c.tags ?? []).some((t) => INTAKE_TAGS.includes(t)) || q3 === formName) ids.add(c.id);
|
|
35380
35442
|
} catch {
|
|
35381
35443
|
}
|
|
35382
35444
|
}
|
|
@@ -35459,17 +35521,17 @@ function buildHeaderField(title2, index) {
|
|
|
35459
35521
|
weight: 400
|
|
35460
35522
|
};
|
|
35461
35523
|
}
|
|
35462
|
-
function buildStandardFormField(
|
|
35463
|
-
if (
|
|
35464
|
-
const tag =
|
|
35524
|
+
function buildStandardFormField(q3) {
|
|
35525
|
+
if (q3.field.kind !== "standard") throw new Error(`buildStandardFormField: ${q3.key} is not standard`);
|
|
35526
|
+
const tag = q3.field.tag;
|
|
35465
35527
|
const isEmail = tag === "email";
|
|
35466
35528
|
const isPhone = tag === "phone";
|
|
35467
35529
|
const base = {
|
|
35468
35530
|
fieldWidthPercentage: 100,
|
|
35469
35531
|
hiddenFieldQueryKey: tag,
|
|
35470
|
-
label:
|
|
35471
|
-
placeholder:
|
|
35472
|
-
required:
|
|
35532
|
+
label: q3.label,
|
|
35533
|
+
placeholder: q3.placeholder ?? q3.label,
|
|
35534
|
+
required: q3.required,
|
|
35473
35535
|
standard: true,
|
|
35474
35536
|
tag,
|
|
35475
35537
|
type: isEmail ? "email" : "text",
|
|
@@ -35478,7 +35540,7 @@ function buildStandardFormField(q2) {
|
|
|
35478
35540
|
if (isPhone) base.enableCountryPicker = false;
|
|
35479
35541
|
return base;
|
|
35480
35542
|
}
|
|
35481
|
-
function buildCustomFormField(
|
|
35543
|
+
function buildCustomFormField(q3, record, locationId2, position2) {
|
|
35482
35544
|
const type = formFieldType(record.dataType);
|
|
35483
35545
|
const field = {
|
|
35484
35546
|
Id: record.id,
|
|
@@ -35486,7 +35548,7 @@ function buildCustomFormField(q2, record, locationId2, position2) {
|
|
|
35486
35548
|
tag: record.id,
|
|
35487
35549
|
active: false,
|
|
35488
35550
|
allowCustomOption: false,
|
|
35489
|
-
customFieldLabel:
|
|
35551
|
+
customFieldLabel: q3.label,
|
|
35490
35552
|
dataType: record.dataType,
|
|
35491
35553
|
dateAdded: record.dateAdded ?? "",
|
|
35492
35554
|
description: "",
|
|
@@ -35496,19 +35558,19 @@ function buildCustomFormField(q2, record, locationId2, position2) {
|
|
|
35496
35558
|
fieldWidthPercentage: 100,
|
|
35497
35559
|
fieldsCount: 0,
|
|
35498
35560
|
hiddenFieldQueryKey: slug(record.name),
|
|
35499
|
-
label:
|
|
35561
|
+
label: q3.label,
|
|
35500
35562
|
locationId: locationId2,
|
|
35501
35563
|
model: record.model ?? "contact",
|
|
35502
35564
|
name: record.name,
|
|
35503
35565
|
parentId: record.parentId ?? "",
|
|
35504
|
-
placeholder:
|
|
35566
|
+
placeholder: q3.placeholder ?? "",
|
|
35505
35567
|
position: position2,
|
|
35506
|
-
required:
|
|
35568
|
+
required: q3.required,
|
|
35507
35569
|
showInForms: true,
|
|
35508
35570
|
standard: false,
|
|
35509
35571
|
type
|
|
35510
35572
|
};
|
|
35511
|
-
const options = record.picklistOptions ??
|
|
35573
|
+
const options = record.picklistOptions ?? q3.options ?? void 0;
|
|
35512
35574
|
if (type === "single_options" || type === "multiple_options" || type === "checkbox") {
|
|
35513
35575
|
field.picklistOptions = options ? [...options] : [];
|
|
35514
35576
|
}
|
|
@@ -35557,17 +35619,17 @@ function buildIntakeFormData(opts) {
|
|
|
35557
35619
|
let lastSection = "";
|
|
35558
35620
|
let headerIndex = 0;
|
|
35559
35621
|
let position2 = 0;
|
|
35560
|
-
for (const
|
|
35561
|
-
if (
|
|
35562
|
-
fields.push(buildHeaderField(
|
|
35563
|
-
lastSection =
|
|
35622
|
+
for (const q3 of questions) {
|
|
35623
|
+
if (q3.section !== lastSection) {
|
|
35624
|
+
fields.push(buildHeaderField(q3.sectionLabel, headerIndex++));
|
|
35625
|
+
lastSection = q3.section;
|
|
35564
35626
|
}
|
|
35565
|
-
if (
|
|
35566
|
-
fields.push(buildStandardFormField(
|
|
35627
|
+
if (q3.field.kind === "standard") {
|
|
35628
|
+
fields.push(buildStandardFormField(q3));
|
|
35567
35629
|
} else {
|
|
35568
|
-
const record = opts.resolved.get(
|
|
35630
|
+
const record = opts.resolved.get(q3.key);
|
|
35569
35631
|
if (!record) continue;
|
|
35570
|
-
fields.push(buildCustomFormField(
|
|
35632
|
+
fields.push(buildCustomFormField(q3, record, opts.locationId, position2 += 50));
|
|
35571
35633
|
}
|
|
35572
35634
|
}
|
|
35573
35635
|
fields.push(buildSubmitButton("Submit"));
|
|
@@ -35597,33 +35659,67 @@ var init_form_template = __esm({
|
|
|
35597
35659
|
}
|
|
35598
35660
|
});
|
|
35599
35661
|
|
|
35662
|
+
// src/command-os/assessment/words.ts
|
|
35663
|
+
function tokens(s) {
|
|
35664
|
+
return String(s ?? "").toLowerCase().replace(/[‘’ʼ]/g, "'").replace(/[–—]/g, "-").replace(/%/g, " percent ").split(/[^a-z0-9.']+/).map((w) => w.replace(/^\.+|\.+$/g, "")).filter(Boolean);
|
|
35665
|
+
}
|
|
35666
|
+
function numericValue(value) {
|
|
35667
|
+
const v = String(value ?? "").trim().toLowerCase().replace(/[$,\s]/g, "");
|
|
35668
|
+
const m = /^(-?\d+(?:\.\d+)?)(%|k|m)?$/.exec(v);
|
|
35669
|
+
if (!m) return null;
|
|
35670
|
+
const n = Number(m[1]) * (m[2] === "k" ? 1e3 : m[2] === "m" ? 1e6 : 1);
|
|
35671
|
+
return { n, percent: m[2] === "%" };
|
|
35672
|
+
}
|
|
35673
|
+
var init_words = __esm({
|
|
35674
|
+
"src/command-os/assessment/words.ts"() {
|
|
35675
|
+
"use strict";
|
|
35676
|
+
}
|
|
35677
|
+
});
|
|
35678
|
+
|
|
35600
35679
|
// src/command-os/assessment/transcript.ts
|
|
35601
35680
|
function loose(s) {
|
|
35602
35681
|
return String(s ?? "").replace(/[‘’ʼ]/g, "'").replace(/[“”]/g, '"').replace(/[–—]/g, "-").replace(/\s+/g, " ").trim().toLowerCase();
|
|
35603
35682
|
}
|
|
35604
|
-
function
|
|
35605
|
-
|
|
35606
|
-
}
|
|
35607
|
-
|
|
35608
|
-
|
|
35609
|
-
|
|
35610
|
-
if (v.length === 0) return false;
|
|
35611
|
-
if (q2.includes(v)) return true;
|
|
35612
|
-
const dv = digits(value);
|
|
35613
|
-
if (dv.length > 0 && digits(quote).includes(dv)) return true;
|
|
35614
|
-
return false;
|
|
35683
|
+
function valueHeard(value, quote, unit) {
|
|
35684
|
+
const v = String(value ?? "").trim();
|
|
35685
|
+
if (unit ? NUMERIC_UNITS.has(unit) : numericValue(v) !== null) return { ok: false, because: NUMBER_HELD };
|
|
35686
|
+
const tv = tokens(v).join(" ");
|
|
35687
|
+
if (tv && ` ${tokens(quote).join(" ")} `.includes(` ${tv} `)) return { ok: true };
|
|
35688
|
+
return { ok: false, because: `"${v}" is not in what they said, so it was worked out rather than heard. Confirm it before it counts.` };
|
|
35615
35689
|
}
|
|
35616
|
-
function citationCheck(material, value, quote) {
|
|
35690
|
+
function citationCheck(material, value, quote, unit) {
|
|
35617
35691
|
const v = String(value ?? "").trim();
|
|
35618
|
-
const
|
|
35692
|
+
const q3 = String(quote ?? "").trim();
|
|
35619
35693
|
if (v === "") return { verdict: "refused", because: "no value was given." };
|
|
35620
|
-
|
|
35621
|
-
if (
|
|
35694
|
+
const said = quoteCheck(material, q3);
|
|
35695
|
+
if (said.verdict === "refused") return said;
|
|
35696
|
+
const heard = valueHeard(v, q3, unit);
|
|
35697
|
+
if (!heard.ok) return { verdict: "held", because: heard.because };
|
|
35698
|
+
return { verdict: "ok" };
|
|
35699
|
+
}
|
|
35700
|
+
function quoteCheck(material, quote, opts = {}) {
|
|
35701
|
+
const q3 = String(quote ?? "").trim();
|
|
35702
|
+
if (q3 === "") return { verdict: "refused", because: "no quote was given, so there is nothing tying this answer to what they said." };
|
|
35703
|
+
if (q3.length > MAX_QUOTE) return { verdict: "refused", because: `the quote is ${q3.length} characters. A quote that long is a summary, not a citation.` };
|
|
35704
|
+
const needle = loose(q3);
|
|
35705
|
+
const words = needle.split(" ").filter((w) => WORDCHAR.test(w)).length;
|
|
35706
|
+
if (opts.minWords && words < opts.minWords) {
|
|
35707
|
+
return { verdict: "refused", because: `the quote is ${words === 1 ? "one word" : `${words} words`}. It needs at least ${opts.minWords} words to show they actually said it.` };
|
|
35708
|
+
}
|
|
35622
35709
|
const hay = loose(material);
|
|
35623
|
-
if (hay.length === 0 || !hay
|
|
35624
|
-
if (!valueIsInQuote(v, q2)) return { verdict: "held", because: `"${v}" is not in what they said, so it was worked out rather than heard. Confirm it before it counts.` };
|
|
35710
|
+
if (hay.length === 0 || !containsAsWords(hay, needle)) return { verdict: "refused", because: "that quote does not appear in the transcript." };
|
|
35625
35711
|
return { verdict: "ok" };
|
|
35626
35712
|
}
|
|
35713
|
+
function containsAsWords(hay, needle) {
|
|
35714
|
+
const checkStart = WORDCHAR.test(needle[0] ?? "");
|
|
35715
|
+
const checkEnd = WORDCHAR.test(needle[needle.length - 1] ?? "");
|
|
35716
|
+
for (let i = hay.indexOf(needle); i !== -1; i = hay.indexOf(needle, i + 1)) {
|
|
35717
|
+
const before = i > 0 ? hay[i - 1] : "";
|
|
35718
|
+
const after = hay[i + needle.length] ?? "";
|
|
35719
|
+
if ((!checkStart || !WORDCHAR.test(before)) && (!checkEnd || !WORDCHAR.test(after))) return true;
|
|
35720
|
+
}
|
|
35721
|
+
return false;
|
|
35722
|
+
}
|
|
35627
35723
|
function fitsItsUnit(value, unit) {
|
|
35628
35724
|
if (unit === void 0 || unit === "text") return true;
|
|
35629
35725
|
const n = parseTyped(value, unit);
|
|
@@ -35659,7 +35755,7 @@ function gateExtraction(transcript, proposed, questions) {
|
|
|
35659
35755
|
rejected.push({ ...item, because: "the same question was answered twice; only the first was taken." });
|
|
35660
35756
|
continue;
|
|
35661
35757
|
}
|
|
35662
|
-
const cite = citationCheck(transcript, item.value, item.quote);
|
|
35758
|
+
const cite = citationCheck(transcript, item.value, item.quote, questions.get(item.questionId));
|
|
35663
35759
|
if (cite.verdict === "refused") {
|
|
35664
35760
|
rejected.push({ ...item, because: cite.because });
|
|
35665
35761
|
continue;
|
|
@@ -35685,12 +35781,16 @@ function gateExtraction(transcript, proposed, questions) {
|
|
|
35685
35781
|
function mergeAccepted(ans, result) {
|
|
35686
35782
|
return { ...ans, ...result.patch };
|
|
35687
35783
|
}
|
|
35688
|
-
var MAX_QUOTE;
|
|
35784
|
+
var NUMERIC_UNITS, NUMBER_HELD, MAX_QUOTE, WORDCHAR;
|
|
35689
35785
|
var init_transcript = __esm({
|
|
35690
35786
|
"src/command-os/assessment/transcript.ts"() {
|
|
35691
35787
|
"use strict";
|
|
35692
35788
|
init_engine();
|
|
35789
|
+
init_words();
|
|
35790
|
+
NUMERIC_UNITS = /* @__PURE__ */ new Set(["count", "currency", "rate", "minutes", "hours"]);
|
|
35791
|
+
NUMBER_HELD = "a number: confirm it. Numbers always wait for the owner's OK, because no word check can tell a figure that answers the question from one said in passing.";
|
|
35693
35792
|
MAX_QUOTE = 400;
|
|
35793
|
+
WORDCHAR = /[\p{L}\p{N}]/u;
|
|
35694
35794
|
}
|
|
35695
35795
|
});
|
|
35696
35796
|
|
|
@@ -35767,8 +35867,8 @@ async function readIntakeSnapshot(client, locationId2, sentMarker) {
|
|
|
35767
35867
|
if (!submissions) {
|
|
35768
35868
|
try {
|
|
35769
35869
|
const seen = /* @__PURE__ */ new Map();
|
|
35770
|
-
for (const
|
|
35771
|
-
const found = await client.get("/contacts/", { params: { locationId: locationId2, query:
|
|
35870
|
+
for (const q3 of [INTAKE_CONTACT_TAG, INTAKE_FORM_NAME2]) {
|
|
35871
|
+
const found = await client.get("/contacts/", { params: { locationId: locationId2, query: q3, limit: 20 } });
|
|
35772
35872
|
for (const c of found.contacts ?? []) seen.set(c.id, c);
|
|
35773
35873
|
}
|
|
35774
35874
|
const isFormSubmission = (c) => c.source === INTAKE_FORM_NAME2 || (c.attributions ?? []).some((a) => a.mediumId === form.id || a.medium === "form");
|
|
@@ -35902,15 +36002,15 @@ async function runPrefill(call2, labels, material) {
|
|
|
35902
36002
|
}
|
|
35903
36003
|
function validatePrefillAnswers(questions, proposed) {
|
|
35904
36004
|
const result = {};
|
|
35905
|
-
for (const
|
|
35906
|
-
const raw = proposed[
|
|
36005
|
+
for (const q3 of questions) {
|
|
36006
|
+
const raw = proposed[q3.label];
|
|
35907
36007
|
if (raw === void 0) continue;
|
|
35908
36008
|
const values = (Array.isArray(raw) ? raw : [raw]).map((x) => x.trim()).filter(Boolean);
|
|
35909
36009
|
if (!values.length) continue;
|
|
35910
|
-
if (
|
|
35911
|
-
if (values.some((v) => !
|
|
35912
|
-
result[
|
|
35913
|
-
} else if (values.length === 1) result[
|
|
36010
|
+
if (q3.options?.length) {
|
|
36011
|
+
if (values.some((v) => !q3.options.includes(v)) || !q3.multi && values.length !== 1) continue;
|
|
36012
|
+
result[q3.label] = q3.multi ? [...new Set(values)] : values[0];
|
|
36013
|
+
} else if (values.length === 1) result[q3.label] = values[0];
|
|
35914
36014
|
}
|
|
35915
36015
|
return result;
|
|
35916
36016
|
}
|
|
@@ -39232,9 +39332,9 @@ function renderReport2(opts) {
|
|
|
39232
39332
|
if (opts.answered && opts.answered.length) {
|
|
39233
39333
|
out.push('<h2>What we started from, in your words</h2><table class="qa">');
|
|
39234
39334
|
for (var a = 0; a < opts.answered.length; a++) {
|
|
39235
|
-
var
|
|
39236
|
-
var numeric =
|
|
39237
|
-
out.push("<tr><td>" + esc7(
|
|
39335
|
+
var q3 = opts.answered[a];
|
|
39336
|
+
var numeric = q3.unit !== void 0 ? q3.unit !== "text" : isFigure(q3.value);
|
|
39337
|
+
out.push("<tr><td>" + esc7(q3.ask) + '</td><td class="' + (numeric ? "num" : "said") + '">' + (q3.unknown ? '<span class="unk">you did not know</span>' : esc7(q3.value)) + "</td></tr>");
|
|
39238
39338
|
}
|
|
39239
39339
|
out.push("</table>");
|
|
39240
39340
|
}
|
|
@@ -39379,6 +39479,491 @@ var init_report = __esm({
|
|
|
39379
39479
|
}
|
|
39380
39480
|
});
|
|
39381
39481
|
|
|
39482
|
+
// src/command-os/client-calls.ts
|
|
39483
|
+
var client_calls_exports = {};
|
|
39484
|
+
__export(client_calls_exports, {
|
|
39485
|
+
ANALYSIS_MIN_WORDS: () => ANALYSIS_MIN_WORDS,
|
|
39486
|
+
CLIENT_FILES_ROOT: () => CLIENT_FILES_ROOT,
|
|
39487
|
+
CONFIRM_INSTRUCTION: () => CONFIRM_INSTRUCTION,
|
|
39488
|
+
EXISTING_WITH_CONTACT: () => EXISTING_WITH_CONTACT,
|
|
39489
|
+
RECORDERS: () => RECORDERS,
|
|
39490
|
+
SOLUTION_KINDS: () => SOLUTION_KINDS,
|
|
39491
|
+
UNCLEAN_RECORD: () => UNCLEAN_RECORD,
|
|
39492
|
+
buildClientFile: () => buildClientFile,
|
|
39493
|
+
callDate: () => callDate,
|
|
39494
|
+
confirmList: () => confirmList,
|
|
39495
|
+
gateAnalysis: () => gateAnalysis,
|
|
39496
|
+
processClientCall: () => processClientCall,
|
|
39497
|
+
safeName: () => safeName,
|
|
39498
|
+
writeClientFileLocally: () => writeClientFileLocally
|
|
39499
|
+
});
|
|
39500
|
+
function gateAnalysis(transcript, analysis) {
|
|
39501
|
+
const rejected = [];
|
|
39502
|
+
const kept = {
|
|
39503
|
+
saidTheyWant: (analysis.saidTheyWant ?? []).map(clean).filter(Boolean),
|
|
39504
|
+
actuallyNeed: [],
|
|
39505
|
+
unnamedProblems: [],
|
|
39506
|
+
recommendedSolutions: [],
|
|
39507
|
+
nextCallQuestions: (analysis.nextCallQuestions ?? []).map(clean).filter(Boolean)
|
|
39508
|
+
};
|
|
39509
|
+
const check = (section2, item, quote) => {
|
|
39510
|
+
if (!item) {
|
|
39511
|
+
rejected.push({ section: section2, item, quote, why: "the point itself was empty." });
|
|
39512
|
+
return false;
|
|
39513
|
+
}
|
|
39514
|
+
const said = quoteCheck(transcript, quote, { minWords: ANALYSIS_MIN_WORDS });
|
|
39515
|
+
if (said.verdict === "refused") {
|
|
39516
|
+
rejected.push({ section: section2, item, quote, why: said.because });
|
|
39517
|
+
return false;
|
|
39518
|
+
}
|
|
39519
|
+
return true;
|
|
39520
|
+
};
|
|
39521
|
+
for (const section2 of ["actuallyNeed", "unnamedProblems"]) {
|
|
39522
|
+
for (const p of analysis[section2] ?? []) {
|
|
39523
|
+
const point = clean(p?.point), quote = String(p?.quote ?? "").trim();
|
|
39524
|
+
if (check(section2, point, quote)) kept[section2].push({ point, quote });
|
|
39525
|
+
}
|
|
39526
|
+
}
|
|
39527
|
+
for (const s of analysis.recommendedSolutions ?? []) {
|
|
39528
|
+
const solution = clean(s?.solution), quote = String(s?.quote ?? "").trim();
|
|
39529
|
+
if (!SOLUTION_KINDS.includes(s?.kind)) {
|
|
39530
|
+
rejected.push({ section: "recommendedSolutions", item: solution, quote, why: `"${String(s?.kind)}" is not a kind; use ${SOLUTION_KINDS.join(", ")}.` });
|
|
39531
|
+
continue;
|
|
39532
|
+
}
|
|
39533
|
+
if (check("recommendedSolutions", solution, quote)) kept.recommendedSolutions.push({ solution, kind: s.kind, why: clean(s?.why), quote });
|
|
39534
|
+
}
|
|
39535
|
+
return { kept, rejected };
|
|
39536
|
+
}
|
|
39537
|
+
function safeName(raw, max = 80) {
|
|
39538
|
+
let s = String(raw ?? "").normalize("NFC").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/[/\\:*?"<>|]/g, " ").replace(/(^|\s)\.+(?=\s|$)/g, " ").replace(/\s+/g, " ").trim().replace(/^[.\s]+|[.\s]+$/g, "");
|
|
39539
|
+
const points = Array.from(s);
|
|
39540
|
+
if (points.length > max) s = points.slice(0, max).join("").replace(/[.\s]+$/g, "");
|
|
39541
|
+
if (WINDOWS_RESERVED.test(s)) return void 0;
|
|
39542
|
+
return s || void 0;
|
|
39543
|
+
}
|
|
39544
|
+
function callDate(source, now) {
|
|
39545
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(source?.recordedAt ?? "").trim());
|
|
39546
|
+
if (m) {
|
|
39547
|
+
const d = /* @__PURE__ */ new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00Z`);
|
|
39548
|
+
if (!isNaN(d.getTime()) && d.toISOString().slice(0, 10) === `${m[1]}-${m[2]}-${m[3]}`) return { date: d.toISOString().slice(0, 10), fromRecording: true };
|
|
39549
|
+
}
|
|
39550
|
+
return { date: now.toISOString().slice(0, 10), fromRecording: false };
|
|
39551
|
+
}
|
|
39552
|
+
function confirmList(held) {
|
|
39553
|
+
return held.map((h) => {
|
|
39554
|
+
const saved = /already has "([^"]*)" saved/.exec(h.because)?.[1];
|
|
39555
|
+
return `${ASK.get(h.questionId) ?? h.questionId} \u2192 ${h.value} (their words: ${q2(h.quote)})${saved !== void 0 ? ` [replaces the saved "${saved}"]` : ""}`;
|
|
39556
|
+
});
|
|
39557
|
+
}
|
|
39558
|
+
function analysisMarkdown(a, level) {
|
|
39559
|
+
return [
|
|
39560
|
+
`${level} What they said they want`,
|
|
39561
|
+
bullet(a.saidTheyWant),
|
|
39562
|
+
"",
|
|
39563
|
+
`${level} What they actually need`,
|
|
39564
|
+
bullet(a.actuallyNeed.map((p) => `${stop(p.point)} They said: ${q2(p.quote)}`)),
|
|
39565
|
+
"",
|
|
39566
|
+
`${level} Problems they did not name`,
|
|
39567
|
+
bullet(a.unnamedProblems.map((p) => `${stop(p.point)} They said: ${q2(p.quote)}`)),
|
|
39568
|
+
"",
|
|
39569
|
+
`${level} Recommended solutions`,
|
|
39570
|
+
bullet(a.recommendedSolutions.map((s) => `**${s.solution}** (${s.kind}).${s.why ? ` ${stop(s.why)}` : ""} They said: ${q2(s.quote)}`)),
|
|
39571
|
+
"",
|
|
39572
|
+
`${level} Questions for the next call`,
|
|
39573
|
+
bullet(a.nextCallQuestions)
|
|
39574
|
+
].join("\n");
|
|
39575
|
+
}
|
|
39576
|
+
function buildClientFile(args) {
|
|
39577
|
+
const client = safeName(args.clientName) ?? "Unnamed client";
|
|
39578
|
+
const folderParts = [...CLIENT_FILES_ROOT, client];
|
|
39579
|
+
const recorder = args.source?.recorder ?? "Call";
|
|
39580
|
+
const title2 = args.source?.title?.trim() || "call";
|
|
39581
|
+
const stem = safeName(`${args.date} ${recorder} ${title2}`, 120) ?? `${args.date} call`;
|
|
39582
|
+
const name = `${stem}.md`;
|
|
39583
|
+
const transcriptName = `${stem} transcript.md`;
|
|
39584
|
+
const ask = (id) => ASK.get(id) ?? id;
|
|
39585
|
+
const src = args.source ? describeSource({ ...args.source, recordedAt: args.date }) : "Not recorded (no source was given)";
|
|
39586
|
+
const call2 = [
|
|
39587
|
+
`# Call with ${client}, ${args.date}`,
|
|
39588
|
+
"",
|
|
39589
|
+
`Source: ${src}.`,
|
|
39590
|
+
`Assessment: ${args.saveLine}`,
|
|
39591
|
+
"",
|
|
39592
|
+
"## Answers saved or ready to save",
|
|
39593
|
+
bullet(args.gated.accepted.map((a) => `${ask(a.questionId)} **${a.value}**${args.confirmedByOwner?.includes(a.questionId) ? " (confirmed by you)" : ""}. They said: ${q2(a.quote)}`)),
|
|
39594
|
+
"",
|
|
39595
|
+
"## Waiting for your confirmation",
|
|
39596
|
+
...args.gated.held.length ? [confirmList(args.gated.held).map((l) => `- ${l}`).join("\n"), "", CONFIRM_INSTRUCTION] : ["- (none)"],
|
|
39597
|
+
"",
|
|
39598
|
+
"## Thrown out",
|
|
39599
|
+
bullet(args.gated.rejected.map((r) => `${ask(r.questionId)} ${r.value}: ${r.because}`)),
|
|
39600
|
+
"",
|
|
39601
|
+
...args.analysis ? [
|
|
39602
|
+
"## Needs analysis (for you, not for the client)",
|
|
39603
|
+
"",
|
|
39604
|
+
analysisMarkdown(args.analysis.kept, "###"),
|
|
39605
|
+
"",
|
|
39606
|
+
...args.analysis.rejected.length ? [
|
|
39607
|
+
"### Left out: the quote was not in the transcript",
|
|
39608
|
+
bullet(args.analysis.rejected.map((r) => `${r.item || "(empty)"}: ${r.why}`)),
|
|
39609
|
+
""
|
|
39610
|
+
] : []
|
|
39611
|
+
] : [],
|
|
39612
|
+
"## Transcript",
|
|
39613
|
+
"",
|
|
39614
|
+
`Kept in its own file: Calls/${transcriptName}. If that file was not written, the recording in ${args.source?.recorder ?? "the recorder"} is the copy.`,
|
|
39615
|
+
""
|
|
39616
|
+
].join("\n");
|
|
39617
|
+
const transcript = [
|
|
39618
|
+
`# Transcript: call with ${client}, ${args.date}`,
|
|
39619
|
+
"",
|
|
39620
|
+
`Source: ${src}.`,
|
|
39621
|
+
"",
|
|
39622
|
+
args.transcript.trim(),
|
|
39623
|
+
""
|
|
39624
|
+
].join("\n");
|
|
39625
|
+
const understanding = [
|
|
39626
|
+
"",
|
|
39627
|
+
`## ${args.date}: ${recorder} call${args.source?.title?.trim() ? `, ${q2(args.source.title)}` : ""}`,
|
|
39628
|
+
"",
|
|
39629
|
+
...args.analysis ? [analysisMarkdown(args.analysis.kept, "###"), ""] : ["No needs analysis was written for this call.", ""],
|
|
39630
|
+
`Assessment: ${args.saveLine}`,
|
|
39631
|
+
`Full call: Calls/${name}`,
|
|
39632
|
+
""
|
|
39633
|
+
].join("\n");
|
|
39634
|
+
return {
|
|
39635
|
+
clientName: client,
|
|
39636
|
+
folder: folderParts.join("/"),
|
|
39637
|
+
folderParts,
|
|
39638
|
+
call: { folderParts: [...folderParts, "Calls"], name, mode: "create-new", content: call2 },
|
|
39639
|
+
transcript: { folderParts: [...folderParts, "Calls"], name: transcriptName, mode: "create-new", optional: true, characters: args.transcript.trim().length, content: transcript },
|
|
39640
|
+
understanding: {
|
|
39641
|
+
folderParts,
|
|
39642
|
+
name: "Client understanding.md",
|
|
39643
|
+
mode: "append",
|
|
39644
|
+
headerIfNew: `# Client understanding: ${client}
|
|
39645
|
+
|
|
39646
|
+
What we know about this client, newest call last. Advice for the agency, never sent to the client.
|
|
39647
|
+
`,
|
|
39648
|
+
append: understanding
|
|
39649
|
+
},
|
|
39650
|
+
howToWrite: `Search for each folder in folderParts before creating it, one level at a time, and use the one that exists; create it only when the search finds none, and never make a second copy of the tree. Write call.content as a NEW file named call.name in the Calls folder; if that name is taken, add " (2)" rather than replacing it. Write transcript.content as its own new file, transcript.name, beside it; it is optional: when it is too large to write in one go (see transcript.characters), skip it, and a link to the recording or the recorder's own copy is enough. For understanding: read "Client understanding.md", add understanding.append at the end, write it back, then read it again and check every earlier dated section is still there. If it does not exist yet, create it with headerIfNew followed by the append. Never drop what was there: it is the running record of every call.`
|
|
39651
|
+
};
|
|
39652
|
+
}
|
|
39653
|
+
function inside(base, p) {
|
|
39654
|
+
return p.startsWith(base.endsWith(path32.sep) ? base : base + path32.sep);
|
|
39655
|
+
}
|
|
39656
|
+
function writeClientFileLocally(baseDir, file2) {
|
|
39657
|
+
try {
|
|
39658
|
+
if (!path32.isAbsolute(baseDir)) return { written: false, error: `the client files folder must be a full path, not "${baseDir}".` };
|
|
39659
|
+
const base = path32.resolve(baseDir);
|
|
39660
|
+
let st;
|
|
39661
|
+
try {
|
|
39662
|
+
st = fs34.statSync(base);
|
|
39663
|
+
} catch {
|
|
39664
|
+
return { written: false, error: `the client files folder ${base} does not exist. If it is your Google Drive folder, Google Drive for desktop needs to be installed and signed in.` };
|
|
39665
|
+
}
|
|
39666
|
+
if (!st.isDirectory()) return { written: false, error: `${base} is not a folder.` };
|
|
39667
|
+
const realBase = fs34.realpathSync(base);
|
|
39668
|
+
const callsDir = path32.resolve(base, ...file2.call.folderParts);
|
|
39669
|
+
const clientDir = path32.resolve(base, ...file2.understanding.folderParts);
|
|
39670
|
+
if (!inside(base, callsDir) || !inside(base, clientDir)) return { written: false, error: "the client folder name would land outside the client files folder." };
|
|
39671
|
+
let cur = base;
|
|
39672
|
+
for (const part of file2.call.folderParts) {
|
|
39673
|
+
cur = path32.join(cur, part);
|
|
39674
|
+
let here;
|
|
39675
|
+
try {
|
|
39676
|
+
here = fs34.lstatSync(cur);
|
|
39677
|
+
} catch {
|
|
39678
|
+
}
|
|
39679
|
+
if (here && (here.isSymbolicLink() || !here.isDirectory())) {
|
|
39680
|
+
return { written: false, error: `${cur} is a link or a file, not a folder, so nothing was written.` };
|
|
39681
|
+
}
|
|
39682
|
+
if (!here) fs34.mkdirSync(cur, { mode: 448 });
|
|
39683
|
+
}
|
|
39684
|
+
if (!inside(realBase, fs34.realpathSync(callsDir))) return { written: false, error: "the client folder resolves outside the client files folder, so nothing was written." };
|
|
39685
|
+
const stem = file2.call.name.replace(/\.md$/, "");
|
|
39686
|
+
const tStem = file2.transcript.name.replace(/\.md$/, "");
|
|
39687
|
+
let callFile = "", transcriptFile = "";
|
|
39688
|
+
for (let n = 1; n <= 50 && !callFile; n++) {
|
|
39689
|
+
const suffix = n === 1 ? "" : ` (${n})`;
|
|
39690
|
+
const callPath = path32.join(callsDir, `${stem}${suffix}.md`);
|
|
39691
|
+
const tName = `${tStem.slice(0, tStem.length - " transcript".length)}${suffix} transcript.md`;
|
|
39692
|
+
const tPath = path32.join(callsDir, tName);
|
|
39693
|
+
if (fs34.existsSync(callPath) || fs34.existsSync(tPath)) continue;
|
|
39694
|
+
try {
|
|
39695
|
+
fs34.writeFileSync(tPath, file2.transcript.content, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
39696
|
+
fs34.writeFileSync(callPath, file2.call.content.split(`Calls/${file2.transcript.name}`).join(`Calls/${tName}`), { encoding: "utf8", flag: "wx", mode: 384 });
|
|
39697
|
+
callFile = callPath;
|
|
39698
|
+
transcriptFile = tPath;
|
|
39699
|
+
} catch (e) {
|
|
39700
|
+
if (e.code !== "EEXIST") throw e;
|
|
39701
|
+
}
|
|
39702
|
+
}
|
|
39703
|
+
if (!callFile) return { written: false, error: `fifty calls named "${stem}" already exist; nothing was overwritten.` };
|
|
39704
|
+
fs34.chmodSync(callFile, 384);
|
|
39705
|
+
fs34.chmodSync(transcriptFile, 384);
|
|
39706
|
+
const understandingFile = path32.join(clientDir, file2.understanding.name);
|
|
39707
|
+
let created = false;
|
|
39708
|
+
try {
|
|
39709
|
+
fs34.writeFileSync(understandingFile, file2.understanding.headerIfNew + file2.understanding.append, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
39710
|
+
created = true;
|
|
39711
|
+
} catch (e) {
|
|
39712
|
+
if (e.code !== "EEXIST") throw e;
|
|
39713
|
+
}
|
|
39714
|
+
if (!created) {
|
|
39715
|
+
const st2 = fs34.lstatSync(understandingFile);
|
|
39716
|
+
if (st2.isSymbolicLink() || !st2.isFile() || st2.nlink > 1) return { written: false, error: `${understandingFile} is a link, not a plain file, so it was not appended to. The call file was written: ${callFile}` };
|
|
39717
|
+
fs34.appendFileSync(understandingFile, file2.understanding.append, { encoding: "utf8", mode: 384 });
|
|
39718
|
+
}
|
|
39719
|
+
fs34.chmodSync(understandingFile, 384);
|
|
39720
|
+
return { written: true, callFile, transcriptFile, understandingFile };
|
|
39721
|
+
} catch (e) {
|
|
39722
|
+
return { written: false, error: `could not write the client file: ${e?.message ?? String(e)}` };
|
|
39723
|
+
}
|
|
39724
|
+
}
|
|
39725
|
+
function contactNames(raw) {
|
|
39726
|
+
const c = raw?.contact ?? {};
|
|
39727
|
+
const s = (k) => typeof c[k] === "string" && c[k].trim() ? c[k].trim() : "";
|
|
39728
|
+
const name = s("name") || [s("firstName"), s("lastName")].filter(Boolean).join(" ") || s("contactName");
|
|
39729
|
+
return { name: name || null, business: s("companyName") || null };
|
|
39730
|
+
}
|
|
39731
|
+
async function withContactLock(key, fn) {
|
|
39732
|
+
const before = contactLocks.get(key) ?? Promise.resolve();
|
|
39733
|
+
let release;
|
|
39734
|
+
const mine = before.then(() => new Promise((r) => {
|
|
39735
|
+
release = r;
|
|
39736
|
+
}));
|
|
39737
|
+
contactLocks.set(key, mine);
|
|
39738
|
+
await before;
|
|
39739
|
+
try {
|
|
39740
|
+
return await fn();
|
|
39741
|
+
} finally {
|
|
39742
|
+
release();
|
|
39743
|
+
if (contactLocks.get(key) === mine) contactLocks.delete(key);
|
|
39744
|
+
}
|
|
39745
|
+
}
|
|
39746
|
+
async function readStrict(client, contactId) {
|
|
39747
|
+
const raw = await client.get(`/contacts/${encodeURIComponent(contactId)}/notes`);
|
|
39748
|
+
return pickLatestStrict(raw?.notes ?? []);
|
|
39749
|
+
}
|
|
39750
|
+
async function saveToContact(input, deps, gated, source) {
|
|
39751
|
+
const contactId = input.contactId;
|
|
39752
|
+
let effective = gated;
|
|
39753
|
+
const out = {
|
|
39754
|
+
status: "NOT SAVED",
|
|
39755
|
+
contactId,
|
|
39756
|
+
contactName: null,
|
|
39757
|
+
businessName: null,
|
|
39758
|
+
savedAnswers: 0,
|
|
39759
|
+
confirmedByOwner: [],
|
|
39760
|
+
confirmationsNotUsed: [],
|
|
39761
|
+
confirmedOnContact: [],
|
|
39762
|
+
reason: null,
|
|
39763
|
+
answersOnContact: null,
|
|
39764
|
+
notesWritten: 0,
|
|
39765
|
+
opportunityNote: null
|
|
39766
|
+
};
|
|
39767
|
+
const done = (o) => ({ outcome: { ...out, ...o }, effective });
|
|
39768
|
+
const fail = (reason3) => done({ status: "NOT SAVED", savedAnswers: 0, reason: reason3 });
|
|
39769
|
+
const msg3 = (e) => e?.message ?? String(e);
|
|
39770
|
+
if (input.existing && Object.keys(input.existing).length) return fail(EXISTING_WITH_CONTACT);
|
|
39771
|
+
try {
|
|
39772
|
+
const who = contactNames(await deps.client.get(`/contacts/${encodeURIComponent(contactId)}`));
|
|
39773
|
+
out.contactName = who.name;
|
|
39774
|
+
out.businessName = who.business;
|
|
39775
|
+
} catch (e) {
|
|
39776
|
+
return fail(`could not open contact ${contactId} in this account (${msg3(e)}).`);
|
|
39777
|
+
}
|
|
39778
|
+
let record;
|
|
39779
|
+
try {
|
|
39780
|
+
const got = await readStrict(deps.client, contactId);
|
|
39781
|
+
if (!got.clean) return fail(UNCLEAN_RECORD);
|
|
39782
|
+
record = got.record;
|
|
39783
|
+
} catch (e) {
|
|
39784
|
+
return fail(`could not read the answers already saved on this contact, and saving without them would hide them (${msg3(e)}).`);
|
|
39785
|
+
}
|
|
39786
|
+
if (record) out.businessName = out.businessName ?? record.state.biz;
|
|
39787
|
+
const sameValue = (a, b) => {
|
|
39788
|
+
const na = numericValue(a), nb = numericValue(b);
|
|
39789
|
+
return a.trim() === b.trim() || Boolean(na && nb && na.n === nb.n && na.percent === nb.percent);
|
|
39790
|
+
};
|
|
39791
|
+
const confirmedIds = /* @__PURE__ */ new Set();
|
|
39792
|
+
const heldAfterConfirm = gated.held.filter((h) => {
|
|
39793
|
+
const c = (input.confirmed ?? []).find((x) => String(x?.questionId) === h.questionId);
|
|
39794
|
+
if (c && sameValue(String(c.value), h.value)) {
|
|
39795
|
+
confirmedIds.add(h.questionId);
|
|
39796
|
+
return false;
|
|
39797
|
+
}
|
|
39798
|
+
return true;
|
|
39799
|
+
});
|
|
39800
|
+
for (const c of input.confirmed ?? []) {
|
|
39801
|
+
const id = String(c?.questionId), value = String(c?.value ?? "");
|
|
39802
|
+
if (confirmedIds.has(id)) continue;
|
|
39803
|
+
const held = gated.held.find((h) => h.questionId === id);
|
|
39804
|
+
out.confirmationsNotUsed.push({ questionId: id, value, why: held ? `the held answer is "${held.value}", not "${value}". Propose "${value}" with its quote, then confirm it.` : gated.rejected.some((r) => r.questionId === id) ? "that answer was thrown out: its quote is not in the transcript." : "no answer to this question is waiting for confirmation." });
|
|
39805
|
+
}
|
|
39806
|
+
const candidates = [...gated.accepted, ...gated.held.filter((h) => confirmedIds.has(h.questionId)).map(({ because: _b, ...a }) => a)];
|
|
39807
|
+
const prior = record?.state.ans ?? {};
|
|
39808
|
+
const replace = new Set(input.proposed.filter((p) => p?.replace === true).map((p) => String(p.questionId)));
|
|
39809
|
+
const conflicts = [];
|
|
39810
|
+
const accepted = candidates.filter((a) => {
|
|
39811
|
+
const had = prior[a.questionId];
|
|
39812
|
+
if (had === void 0 || String(had).trim() === "" || String(had) === a.value || replace.has(a.questionId)) return true;
|
|
39813
|
+
conflicts.push({ ...a, because: `the contact already has "${had}" saved for this question and this call says "${a.value}". Confirm before it replaces the saved answer (send it again with replace: true).` });
|
|
39814
|
+
return false;
|
|
39815
|
+
});
|
|
39816
|
+
out.confirmedByOwner = accepted.filter((a) => confirmedIds.has(a.questionId)).map((a) => a.questionId);
|
|
39817
|
+
effective = { ...gated, accepted, held: [...heldAfterConfirm, ...conflicts], patch: Object.fromEntries(accepted.map((a) => [a.questionId, a.value])) };
|
|
39818
|
+
if (accepted.length === 0) {
|
|
39819
|
+
return done({ status: "NOTHING TO SAVE", reason: effective.held.length ? "every answer is waiting for your OK (numbers always do), so nothing new was written yet." : "no new answer passed the quote check, so nothing new was written.", answersOnContact: record?.state.ans ?? null });
|
|
39820
|
+
}
|
|
39821
|
+
const biz = record?.state.biz || clean(input.businessName) || out.businessName || out.contactName;
|
|
39822
|
+
if (!biz) return fail("neither the contact nor a saved assessment has a business name. Give businessName and try again.");
|
|
39823
|
+
out.businessName = out.businessName ?? biz;
|
|
39824
|
+
const skip = { ...record?.state.skip ?? {} };
|
|
39825
|
+
for (const a of accepted) delete skip[a.questionId];
|
|
39826
|
+
const expected = { ...prior, ...effective.patch };
|
|
39827
|
+
const confirmedAll = [.../* @__PURE__ */ new Set([
|
|
39828
|
+
...(record?.confirmedByOwner ?? []).filter((id) => !(id in effective.patch) || out.confirmedByOwner.includes(id)),
|
|
39829
|
+
...out.confirmedByOwner
|
|
39830
|
+
])];
|
|
39831
|
+
const state = { ...record?.state ?? { asm: {} }, biz, ans: expected, skip, asm: record?.state.asm ?? {} };
|
|
39832
|
+
let failure2 = null;
|
|
39833
|
+
try {
|
|
39834
|
+
const saved = await saveAssessment(deps.client, deps.locationId, {
|
|
39835
|
+
assessment: state,
|
|
39836
|
+
contactId,
|
|
39837
|
+
source,
|
|
39838
|
+
createPipeline: input.createPipeline,
|
|
39839
|
+
confirmedByOwner: confirmedAll
|
|
39840
|
+
});
|
|
39841
|
+
if (!saved.saved) failure2 = saved.error ?? "GoHighLevel did not accept the save.";
|
|
39842
|
+
out.notesWritten = saved.notesWritten ?? 0;
|
|
39843
|
+
out.opportunityNote = saved.opportunityNote ?? null;
|
|
39844
|
+
} catch (e) {
|
|
39845
|
+
failure2 = `GoHighLevel reported an error (${msg3(e)}).`;
|
|
39846
|
+
}
|
|
39847
|
+
let back;
|
|
39848
|
+
try {
|
|
39849
|
+
const got = await readStrict(deps.client, contactId);
|
|
39850
|
+
back = got.clean ? got.record : null;
|
|
39851
|
+
} catch (e) {
|
|
39852
|
+
return done({ status: "NOT CONFIRMED", reason: `${failure2 ? failure2 + " " : ""}The contact could not be read back (${msg3(e)}). Treat the answers as not saved until you open the contact.` });
|
|
39853
|
+
}
|
|
39854
|
+
const off = back ? same(back.state.ans, expected) : null;
|
|
39855
|
+
const marks = back?.confirmedByOwner ?? [];
|
|
39856
|
+
if (off && off.length === 0 && marks.length === confirmedAll.length && confirmedAll.every((id) => marks.includes(id))) {
|
|
39857
|
+
return done({ status: "SAVED", savedAnswers: accepted.length, answersOnContact: back.state.ans, confirmedOnContact: marks, reason: failure2 ? `${failure2} The answers did land and read back correctly.` : null });
|
|
39858
|
+
}
|
|
39859
|
+
const unchanged = back ? same(back.state.ans, prior).length === 0 : !record;
|
|
39860
|
+
if (failure2 && unchanged) return done({ status: "NOT SAVED", savedAnswers: 0, reason: failure2, answersOnContact: back?.state.ans ?? null });
|
|
39861
|
+
return done({
|
|
39862
|
+
status: "NOT CONFIRMED",
|
|
39863
|
+
answersOnContact: back?.state.ans ?? null,
|
|
39864
|
+
reason: `${failure2 ? failure2 + " " : ""}Reading the contact back ${back ? `did not match what was saved (${off.length ? off.join(", ") : "the confirmed-by-you marks"}); the contact may have changed at the same time` : "found no complete assessment"}. Treat the answers as not saved until you open the contact.`
|
|
39865
|
+
});
|
|
39866
|
+
}
|
|
39867
|
+
async function processClientCall(input, deps) {
|
|
39868
|
+
let gated = gateExtraction(input.transcript, input.proposed, QUESTION_UNITS);
|
|
39869
|
+
const legacyOf = (gated2) => ({
|
|
39870
|
+
applied: gated2.accepted.length,
|
|
39871
|
+
answers: mergeAccepted(input.existing ?? {}, gated2),
|
|
39872
|
+
confirmBeforeTheseCount: gated2.held.map((h) => ({
|
|
39873
|
+
question: ASK.get(h.questionId) ?? h.questionId,
|
|
39874
|
+
proposedValue: h.value,
|
|
39875
|
+
theyActuallySaid: h.quote,
|
|
39876
|
+
howItWasWorkedOut: h.reasoning ?? null,
|
|
39877
|
+
why: h.because
|
|
39878
|
+
})),
|
|
39879
|
+
thrownOut: gated2.rejected.map((r) => ({ questionId: r.questionId, value: r.value, why: r.because })),
|
|
39880
|
+
note: gated2.rejected.some((r) => /does not appear in the transcript/.test(r.because)) ? "One or more answers cited words that are not in the transcript. Those were discarded, not stored. That check is the only thing standing between a recording and an invented figure in a client's report." : null
|
|
39881
|
+
});
|
|
39882
|
+
const legacy = legacyOf(gated);
|
|
39883
|
+
if (!input.contactId && !input.analysis && !input.source) return legacy;
|
|
39884
|
+
const now = deps.now();
|
|
39885
|
+
const { date, fromRecording } = callDate(input.source, now);
|
|
39886
|
+
const noteSource = input.source ? { recorder: input.source.recorder, recordedAt: date, title: input.source.title?.trim() || void 0, fetchedBy: input.source.fetchedBy } : void 0;
|
|
39887
|
+
const saving = input.contactId ? await withContactLock(`${deps.locationId}:${input.contactId}`, () => saveToContact(input, deps, gated, noteSource)) : null;
|
|
39888
|
+
const save = saving?.outcome ?? null;
|
|
39889
|
+
if (saving) gated = saving.effective;
|
|
39890
|
+
const analysis = input.analysis ? gateAnalysis(input.transcript, input.analysis) : null;
|
|
39891
|
+
const counts = `${plural(gated.held.length, "answer needs", "answers need")} your confirmation, ${plural(gated.rejected.length, "was", "were")} thrown out`;
|
|
39892
|
+
const who = save ? `${save.businessName ?? save.contactName ?? "the contact"}${save.contactName && save.businessName ? ` (${save.contactName})` : ""}, contact ${save.contactId}` : "";
|
|
39893
|
+
let reply;
|
|
39894
|
+
let saveLine;
|
|
39895
|
+
if (!save) {
|
|
39896
|
+
reply = `NOT SAVED: no contact was named, so these answers are only proposed. ${gated.accepted.length} passed the check, ${counts}. Call again with the client's contactId to save them.`;
|
|
39897
|
+
saveLine = "not saved to GoHighLevel (no contact was named).";
|
|
39898
|
+
} else if (save.status === "SAVED") {
|
|
39899
|
+
const byYou = save.confirmedByOwner.length ? ` (${save.confirmedByOwner.length} confirmed by you)` : "";
|
|
39900
|
+
reply = `SAVED ${plural(save.savedAnswers, "answer", "answers")}${byYou} to ${who}. ${counts[0].toUpperCase()}${counts.slice(1)}.${save.reason ? ` Note: ${save.reason}` : ""}`;
|
|
39901
|
+
saveLine = `saved ${plural(save.savedAnswers, "answer", "answers")}${byYou} on the GoHighLevel contact ${save.contactId}.`;
|
|
39902
|
+
} else if (save.status === "NOTHING TO SAVE") {
|
|
39903
|
+
const why = gated.held.length ? "every answer is waiting for your OK (numbers always do, and any that differ from a saved answer)" : "no answer passed the quote check";
|
|
39904
|
+
reply = `NOTHING TO SAVE for ${who}: ${why}. ${counts[0].toUpperCase()}${counts.slice(1)}.`;
|
|
39905
|
+
saveLine = `nothing new saved (${why}).`;
|
|
39906
|
+
} else {
|
|
39907
|
+
reply = `${save.status} for ${who}: ${save.reason} ${save.status === "NOT SAVED" ? "Nothing from this call is on the contact." : ""} Of the proposed answers, ${counts}.`.replace(/\s+/g, " ");
|
|
39908
|
+
saveLine = `${save.status}. ${save.reason}`;
|
|
39909
|
+
}
|
|
39910
|
+
if (save?.opportunityNote) reply += ` ${save.opportunityNote}`;
|
|
39911
|
+
if (save?.confirmationsNotUsed.length) reply += ` ${plural(save.confirmationsNotUsed.length, "confirmation was", "confirmations were")} not used: ${save.confirmationsNotUsed.map((c) => `${c.questionId} (${c.why})`).join("; ")}`;
|
|
39912
|
+
if (analysis?.rejected.length) reply += ` ${plural(analysis.rejected.length, "analysis point was", "analysis points were")} left out because the quote is not in the transcript.`;
|
|
39913
|
+
if (gated.held.length) reply += `
|
|
39914
|
+
|
|
39915
|
+
To confirm:
|
|
39916
|
+
${confirmList(gated.held).map((l) => `- ${l}`).join("\n")}
|
|
39917
|
+
${CONFIRM_INSTRUCTION}`;
|
|
39918
|
+
const clientName = save?.businessName ?? save?.contactName ?? clean(input.businessName);
|
|
39919
|
+
const clientFile = clientName ? buildClientFile({ clientName, date, source: input.source, gated, analysis, transcript: input.transcript, saveLine, confirmedByOwner: save?.confirmedByOwner }) : null;
|
|
39920
|
+
const localCopy = clientFile && deps.clientFilesDir ? writeClientFileLocally(deps.clientFilesDir, clientFile) : null;
|
|
39921
|
+
const { applied, ...legacyRest } = legacyOf(gated);
|
|
39922
|
+
return {
|
|
39923
|
+
// Never hand back unchecked values beside a failed save (review D-3).
|
|
39924
|
+
...save ? (({ answers: _unchecked, ...rest }) => rest)(legacyRest) : { applied, ...legacyRest },
|
|
39925
|
+
...save?.answersOnContact ? { answers: save.answersOnContact } : {},
|
|
39926
|
+
reply,
|
|
39927
|
+
toConfirm: gated.held.map((h) => ({ questionId: h.questionId, question: ASK.get(h.questionId) ?? h.questionId, value: h.value, theirWords: h.quote, replacesSaved: /already has "/.test(h.because) })),
|
|
39928
|
+
saved: save,
|
|
39929
|
+
source: noteSource ? { ...noteSource, dateFromRecording: fromRecording } : null,
|
|
39930
|
+
analysis: analysis ? { ...analysis.kept, leftOutForQuote: analysis.rejected, forTheOwnerOnly: true } : null,
|
|
39931
|
+
clientFile,
|
|
39932
|
+
clientFileNote: clientFile ? null : "No client name was known (no contact, no businessName), so no client file entry was made.",
|
|
39933
|
+
localCopy
|
|
39934
|
+
};
|
|
39935
|
+
}
|
|
39936
|
+
var fs34, path32, RECORDERS, SOLUTION_KINDS, clean, ANALYSIS_MIN_WORDS, CLIENT_FILES_ROOT, WINDOWS_RESERVED, bullet, q2, CONFIRM_INSTRUCTION, stop, EXISTING_WITH_CONTACT, UNCLEAN_RECORD, contactLocks, same, plural;
|
|
39937
|
+
var init_client_calls = __esm({
|
|
39938
|
+
"src/command-os/client-calls.ts"() {
|
|
39939
|
+
"use strict";
|
|
39940
|
+
fs34 = __toESM(require("node:fs"));
|
|
39941
|
+
path32 = __toESM(require("node:path"));
|
|
39942
|
+
init_transcript();
|
|
39943
|
+
init_words();
|
|
39944
|
+
init_store3();
|
|
39945
|
+
init_assessment();
|
|
39946
|
+
RECORDERS = ["Plaud", "Zoom", "Pocket", "Fathom", "Other"];
|
|
39947
|
+
SOLUTION_KINDS = ["GoHighLevel build", "AI tool", "custom build", "process"];
|
|
39948
|
+
clean = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
|
|
39949
|
+
ANALYSIS_MIN_WORDS = 3;
|
|
39950
|
+
CLIENT_FILES_ROOT = ["Command OS", "Clients"];
|
|
39951
|
+
WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
39952
|
+
bullet = (xs) => xs.length ? xs.map((x) => `- ${x}`).join("\n") : "- (none)";
|
|
39953
|
+
q2 = (s) => `"${s.replace(/\s+/g, " ").trim()}"`;
|
|
39954
|
+
CONFIRM_INSTRUCTION = "Reply 'confirm all', or 'confirm all except \u2026' naming the ones to leave out.";
|
|
39955
|
+
stop = (s) => s && !/[.!?]["')\]]?$/.test(s) ? `${s}.` : s;
|
|
39956
|
+
EXISTING_WITH_CONTACT = "existing cannot be used with a contact: the contact's saved answers are the starting point, and only answers quoted from this call are added. Leave existing out and call again.";
|
|
39957
|
+
UNCLEAN_RECORD = "the newest saved assessment on this contact is incomplete or could not be read, so nothing was changed. It may have been edited by hand, or a save may have stopped partway; saving the assessment again with save_assessment writes a complete one, and then this call can be added.";
|
|
39958
|
+
contactLocks = /* @__PURE__ */ new Map();
|
|
39959
|
+
same = (a, b) => {
|
|
39960
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
39961
|
+
return [...keys].filter((k) => String(a[k] ?? "") !== String(b[k] ?? ""));
|
|
39962
|
+
};
|
|
39963
|
+
plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
39964
|
+
}
|
|
39965
|
+
});
|
|
39966
|
+
|
|
39382
39967
|
// src/tools/assessment.ts
|
|
39383
39968
|
function assessmentValidationMessage(error) {
|
|
39384
39969
|
if (!(error instanceof import_zod61.z.ZodError)) return void 0;
|
|
@@ -39461,19 +40046,34 @@ async function saveAssessment(client, locationId2, args, onOpportunityNoteFailur
|
|
|
39461
40046
|
const state = normalise(args.assessment);
|
|
39462
40047
|
const computed = computedFor(state);
|
|
39463
40048
|
const stage = stageFor({ verdict: computed.verdict, proposalSent: args.proposalSent, outcome: args.outcome });
|
|
39464
|
-
const { payload: contactBody, dedupable } = buildContact(state, stage, {
|
|
40049
|
+
const { payload: contactBody, dedupable: dedupableByDetails } = buildContact(state, stage, {
|
|
39465
40050
|
name: args.contactName,
|
|
39466
40051
|
email: args.email,
|
|
39467
40052
|
phone: args.phone
|
|
39468
40053
|
});
|
|
39469
|
-
|
|
39470
|
-
|
|
39471
|
-
|
|
39472
|
-
|
|
40054
|
+
let contactId;
|
|
40055
|
+
if (args.contactId) {
|
|
40056
|
+
const found = await client.get(`/contacts/${encodeURIComponent(args.contactId)}`);
|
|
40057
|
+
const existingContact = found?.contact;
|
|
40058
|
+
contactId = existingContact?.id;
|
|
40059
|
+
if (!contactId) {
|
|
40060
|
+
return { saved: false, error: `GoHighLevel has no contact ${args.contactId} in this account, so nothing was written.` };
|
|
40061
|
+
}
|
|
40062
|
+
if (!existingContact?.companyName?.trim() && contactBody.companyName) {
|
|
40063
|
+
await client.put(`/contacts/${encodeURIComponent(contactId)}`, { body: { companyName: contactBody.companyName } });
|
|
40064
|
+
}
|
|
40065
|
+
await client.post(`/contacts/${encodeURIComponent(contactId)}/tags`, { body: { tags: contactBody.tags } });
|
|
40066
|
+
} else {
|
|
40067
|
+
const upserted = await client.post("/contacts/upsert", {
|
|
40068
|
+
body: { ...contactBody, locationId: locationId2 }
|
|
40069
|
+
});
|
|
40070
|
+
contactId = upserted?.contact?.id ?? upserted?.id;
|
|
40071
|
+
}
|
|
39473
40072
|
if (!contactId) {
|
|
39474
40073
|
return { saved: false, error: "GoHighLevel did not return a contact id, so nothing was written. Nothing partial was left behind." };
|
|
39475
40074
|
}
|
|
39476
|
-
const
|
|
40075
|
+
const dedupable = dedupableByDetails || Boolean(args.contactId);
|
|
40076
|
+
const bodies = buildNoteBodies(buildNote(state, computed, LOOKUP, today(), args.source, args.confirmedByOwner));
|
|
39477
40077
|
const noteIds = [];
|
|
39478
40078
|
for (const body of bodies) {
|
|
39479
40079
|
const note = await client.post(`/contacts/${contactId}/notes`, { body: { body } });
|
|
@@ -39645,33 +40245,56 @@ function registerAssessmentTools(server2, client) {
|
|
|
39645
40245
|
safeTool(
|
|
39646
40246
|
server2,
|
|
39647
40247
|
"read_assessment_transcript",
|
|
39648
|
-
|
|
40248
|
+
`Bring a recorded client call (Plaud, Zoom, Pocket, Fathom, or any transcript file the owner attaches) into the client's assessment, their needs analysis and their client file. The flow: 1) get the transcript yourself, with your own Plaud or Zoom connection or from the attached file; 2) propose assessment answers, each with the client's words copied VERBATIM from the transcript as the quote; for a text question the value is the client's own words taken from that quote, and for a number it is the number they said, written in digits (Plaud writes "a hundred percent"; send "100%"); 3) write the needs analysis, every need, unnamed problem and recommended solution tied to a verbatim quote of at least three words; 4) call this with the client's contactId and the source; 5) write the returned clientFile to the owner's Google Drive with your Drive connection, following clientFile.howToWrite; 6) show the owner the reply as given, including the "To confirm" list. EVERY NUMBER waits for the owner's OK and never saves on its own, because no word check can tell a figure that answers the question from one said in passing. Text answers in the client's own words save straight away. When the owner answers the list ("confirm all", or "confirm all except ..."), call again with the same transcript, proposed and contactId, and confirmed holding exactly the approved items (questionId and value from toConfirm); for an item marked replacesSaved, also set replace: true on that proposed answer. Never put an item in confirmed that the owner has not approved in this conversation. Drive allows two folders with the same name, so SEARCH for each folder in the path (and for the client's folder above all) before creating it, and never create a second tree. To append to "Client understanding": read it, add the new dated section at the end, write it back, then read it again and confirm every earlier section is still there. Answers and analysis points whose quote is not in the transcript are thrown out. With a contactId the saved answers are read back: the reply starts SAVED, NOT SAVED, NOT CONFIRMED or NOTHING TO SAVE, and only SAVED means they are on the contact. The needs analysis is advice for the owner and is never sent to the client. Without a contactId nothing is saved.`,
|
|
39649
40249
|
{
|
|
39650
40250
|
transcript: import_zod61.z.string().min(20).describe("The recording, as text. Paste the whole thing."),
|
|
39651
40251
|
proposed: import_zod61.z.array(import_zod61.z.object({
|
|
39652
40252
|
questionId: import_zod61.z.string().describe("The assessment question this answers."),
|
|
39653
40253
|
value: import_zod61.z.string().describe("What to store, in the unit the question wants."),
|
|
39654
40254
|
quote: import_zod61.z.string().describe("The owner's own words, VERBATIM from the transcript. Do not tidy it up \u2014 if it is not in the transcript the answer is thrown out."),
|
|
39655
|
-
reasoning: import_zod61.z.string().optional().describe("How you got from the quote to the value, when they are not the same.")
|
|
40255
|
+
reasoning: import_zod61.z.string().optional().describe("How you got from the quote to the value, when they are not the same."),
|
|
40256
|
+
replace: import_zod61.z.boolean().optional().describe("Only after the owner confirmed it: let this answer replace a different one already saved on the contact.")
|
|
39656
40257
|
})).describe("One entry per question you could answer. Leave out anything the conversation did not cover."),
|
|
39657
|
-
|
|
40258
|
+
confirmed: import_zod61.z.array(import_zod61.z.object({
|
|
40259
|
+
questionId: import_zod61.z.string(),
|
|
40260
|
+
value: import_zod61.z.union([import_zod61.z.string(), import_zod61.z.number()]).describe("The exact value the owner said yes to.")
|
|
40261
|
+
})).optional().describe("Held answers the owner has said yes to, that specific value, in THIS conversation. Never add one the owner has not confirmed. Its quote must still be in the transcript, and it does not replace a different saved answer without replace: true."),
|
|
40262
|
+
existing: import_zod61.z.record(import_zod61.z.string(), import_zod61.z.union([import_zod61.z.string(), import_zod61.z.number()])).optional().describe("Answers already captured, so this adds to them rather than replacing them. Not with contactId: there the contact's saved answers are the starting point."),
|
|
40263
|
+
contactId: import_zod61.z.string().optional().describe("The client's GoHighLevel contact. Given, the checked answers are added to the assessment already saved on it and saved in this same call. Find it with search_contacts first."),
|
|
40264
|
+
locationId: import_zod61.z.string().optional().describe("The sub-account the contact is in. Defaults to the active one."),
|
|
40265
|
+
businessName: import_zod61.z.string().optional().describe("The client's business name, used when the contact and its saved assessment carry none."),
|
|
40266
|
+
createPipeline: import_zod61.z.boolean().optional().describe('Create the "Assessments" pipeline if it is missing. Only when the owner has said yes.'),
|
|
40267
|
+
source: import_zod61.z.object({
|
|
40268
|
+
recorder: import_zod61.z.enum(["Plaud", "Zoom", "Pocket", "Fathom", "Other"]).describe("What recorded the call."),
|
|
40269
|
+
recordedAt: import_zod61.z.string().describe("When the call happened, as an ISO date (2026-09-24) or timestamp, from the recording itself."),
|
|
40270
|
+
title: import_zod61.z.string().optional().describe("The recording's title, as the recorder shows it."),
|
|
40271
|
+
fetchedBy: import_zod61.z.enum(["claude", "upload"]).optional().describe("claude when you fetched it with your own connector, upload when the owner attached a file.")
|
|
40272
|
+
}).optional().describe("Where the transcript came from. Written onto the saved note and into the client file."),
|
|
40273
|
+
analysis: import_zod61.z.object({
|
|
40274
|
+
saidTheyWant: import_zod61.z.array(import_zod61.z.string()).optional().describe("What the client asked for, in plain words."),
|
|
40275
|
+
actuallyNeed: import_zod61.z.array(import_zod61.z.object({ point: import_zod61.z.string(), quote: import_zod61.z.string().describe("VERBATIM from the transcript.") })).optional().describe("What they really need, each with the words that show it."),
|
|
40276
|
+
unnamedProblems: import_zod61.z.array(import_zod61.z.object({ point: import_zod61.z.string(), quote: import_zod61.z.string().describe("VERBATIM from the transcript.") })).optional().describe("Problems they described without naming, each with the words that show it."),
|
|
40277
|
+
recommendedSolutions: import_zod61.z.array(import_zod61.z.object({
|
|
40278
|
+
solution: import_zod61.z.string(),
|
|
40279
|
+
kind: import_zod61.z.enum(["GoHighLevel build", "AI tool", "custom build", "process"]),
|
|
40280
|
+
why: import_zod61.z.string().describe("Why it fits this client."),
|
|
40281
|
+
quote: import_zod61.z.string().describe("VERBATIM: what they said that this answers.")
|
|
40282
|
+
})).optional().describe("What you would build or change, including things they did not ask for."),
|
|
40283
|
+
nextCallQuestions: import_zod61.z.array(import_zod61.z.string()).optional().describe("What to ask on the next call.")
|
|
40284
|
+
}).optional().describe("Your needs analysis of the call. For the owner only; it is never sent to the client. Every quote is checked against the transcript like the answers are.")
|
|
39658
40285
|
},
|
|
39659
|
-
async (
|
|
39660
|
-
const
|
|
39661
|
-
|
|
39662
|
-
|
|
39663
|
-
|
|
39664
|
-
|
|
39665
|
-
|
|
39666
|
-
|
|
39667
|
-
|
|
39668
|
-
|
|
39669
|
-
|
|
39670
|
-
|
|
39671
|
-
})),
|
|
39672
|
-
thrownOut: gated.rejected.map((r) => ({ questionId: r.questionId, value: r.value, why: r.because })),
|
|
39673
|
-
note: gated.rejected.some((r) => /does not appear in the transcript/.test(r.because)) ? "One or more answers cited words that are not in the transcript. Those were discarded, not stored. That check is the only thing standing between a recording and an invented figure in a client's report." : null
|
|
39674
|
-
};
|
|
40286
|
+
async (args) => {
|
|
40287
|
+
const [{ processClientCall: processClientCall2 }, { readAgencyProfile: readAgencyProfile2 }] = await Promise.all([
|
|
40288
|
+
Promise.resolve().then(() => (init_client_calls(), client_calls_exports)),
|
|
40289
|
+
Promise.resolve().then(() => (init_agency_profile(), agency_profile_exports))
|
|
40290
|
+
]);
|
|
40291
|
+
const wantsFiles = Boolean(args.contactId || args.analysis || args.source);
|
|
40292
|
+
return processClientCall2(args, {
|
|
40293
|
+
client,
|
|
40294
|
+
locationId: args.contactId ? client.resolveLocationId(args.locationId) : "",
|
|
40295
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
40296
|
+
clientFilesDir: wantsFiles ? readAgencyProfile2().clientFilesDir : void 0
|
|
40297
|
+
});
|
|
39675
40298
|
}
|
|
39676
40299
|
);
|
|
39677
40300
|
safeTool(
|
|
@@ -39695,10 +40318,10 @@ function registerAssessmentTools(server2, client) {
|
|
|
39695
40318
|
const r = compute(state.ans, state.skip, state.asm);
|
|
39696
40319
|
const labels = {};
|
|
39697
40320
|
for (const a of ASSUMPTIONS) labels[a[0]] = a[3];
|
|
39698
|
-
const answered = Q.filter((
|
|
39699
|
-
const id =
|
|
40321
|
+
const answered = Q.filter((q3) => {
|
|
40322
|
+
const id = q3[0];
|
|
39700
40323
|
return state.skip[id] === true || state.ans[id] !== void 0 && String(state.ans[id]).trim() !== "";
|
|
39701
|
-
}).map((
|
|
40324
|
+
}).map((q3) => state.skip[q3[0]] ? { ask: q3[2], unknown: true, unit: q3[4] } : { ask: q3[2], value: String(state.ans[q3[0]]), unit: q3[4] });
|
|
39702
40325
|
const html2 = renderReport2({
|
|
39703
40326
|
state,
|
|
39704
40327
|
computed: r,
|
|
@@ -39759,16 +40382,15 @@ var init_assessment = __esm({
|
|
|
39759
40382
|
init_tool_helpers();
|
|
39760
40383
|
init_store3();
|
|
39761
40384
|
init_engine();
|
|
39762
|
-
init_transcript();
|
|
39763
40385
|
init_report();
|
|
39764
40386
|
import_node_crypto11 = require("node:crypto");
|
|
39765
40387
|
import_node_fs6 = require("node:fs");
|
|
39766
40388
|
import_node_os = require("node:os");
|
|
39767
40389
|
import_node_path6 = require("node:path");
|
|
39768
|
-
QUESTION_UNITS = new Map(Q.map((
|
|
40390
|
+
QUESTION_UNITS = new Map(Q.map((q3) => [q3[0], q3[4]]));
|
|
39769
40391
|
AREA_TITLE = new Map(AREAS.map((a) => [a[0], a[1]]));
|
|
39770
|
-
ASK = new Map(Q.map((
|
|
39771
|
-
AREA_OF = new Map(Q.map((
|
|
40392
|
+
ASK = new Map(Q.map((q3) => [q3[0], q3[2]]));
|
|
40393
|
+
AREA_OF = new Map(Q.map((q3) => [q3[0], q3[1]]));
|
|
39772
40394
|
ASSUMPTION_LABEL = new Map(ASSUMPTIONS.map((a) => [a[0], a[1]]));
|
|
39773
40395
|
LOOKUP = {
|
|
39774
40396
|
ask: (id) => ASK.get(id),
|
|
@@ -39876,6 +40498,7 @@ __export(dashboard_exports, {
|
|
|
39876
40498
|
INTAKE_OWN_MESSAGES: () => INTAKE_OWN_MESSAGES,
|
|
39877
40499
|
INTAKE_UNREADABLE: () => INTAKE_UNREADABLE,
|
|
39878
40500
|
NOT_SIGNED_IN_MSG: () => NOT_SIGNED_IN_MSG,
|
|
40501
|
+
PARKED_NOT_BUILT: () => PARKED_NOT_BUILT,
|
|
39879
40502
|
SEAT_GUARDED_READS: () => SEAT_GUARDED_READS,
|
|
39880
40503
|
SEAT_TOKEN_HEADER: () => SEAT_TOKEN_HEADER,
|
|
39881
40504
|
STAGES: () => STAGES2,
|
|
@@ -39947,11 +40570,11 @@ function recoverStuckStages(state) {
|
|
|
39947
40570
|
return { state: { ...state, clients }, recovered };
|
|
39948
40571
|
}
|
|
39949
40572
|
function cockpitStatePath() {
|
|
39950
|
-
return
|
|
40573
|
+
return path33.join(appDataDir(), "cockpit-state.json");
|
|
39951
40574
|
}
|
|
39952
40575
|
function readCockpitState() {
|
|
39953
40576
|
try {
|
|
39954
|
-
const raw = JSON.parse(
|
|
40577
|
+
const raw = JSON.parse(fs35.readFileSync(cockpitStatePath(), "utf8"));
|
|
39955
40578
|
if (raw && raw.v === 1 && raw.clients && typeof raw.clients === "object") return raw;
|
|
39956
40579
|
} catch {
|
|
39957
40580
|
}
|
|
@@ -39959,7 +40582,7 @@ function readCockpitState() {
|
|
|
39959
40582
|
}
|
|
39960
40583
|
function writeCockpitState(state) {
|
|
39961
40584
|
ensureAppDataDir();
|
|
39962
|
-
|
|
40585
|
+
fs35.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
|
|
39963
40586
|
}
|
|
39964
40587
|
function setStage(state, locationId2, stageIndex, status) {
|
|
39965
40588
|
if (!Number.isInteger(stageIndex) || stageIndex < 0 || stageIndex >= STAGES2.length) throw new Error("bad stage index");
|
|
@@ -40103,11 +40726,11 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
40103
40726
|
industry,
|
|
40104
40727
|
manualObligations: stage === 4 ? readBuildObligations(locationId2) : void 0,
|
|
40105
40728
|
intakeSummary: describeComposition({ industry, agency }),
|
|
40106
|
-
customLabels: composed.filter((
|
|
40107
|
-
const base = INTAKE_QUESTIONS.find((b) => b.key ===
|
|
40108
|
-
return base && base.label !==
|
|
40109
|
-
}).map((
|
|
40110
|
-
removedKeys: INTAKE_QUESTIONS.filter((b) => !composed.some((
|
|
40729
|
+
customLabels: composed.filter((q3) => {
|
|
40730
|
+
const base = INTAKE_QUESTIONS.find((b) => b.key === q3.key);
|
|
40731
|
+
return base && base.label !== q3.label;
|
|
40732
|
+
}).map((q3) => ({ key: q3.key, label: q3.label, from: INTAKE_QUESTIONS.find((b) => b.key === q3.key)?.label })),
|
|
40733
|
+
removedKeys: INTAKE_QUESTIONS.filter((b) => !composed.some((q3) => q3.key === b.key)).map((b) => b.key),
|
|
40111
40734
|
// A build re-run reuses the approved plan already saved for this account (finding 10).
|
|
40112
40735
|
// ...and only while it still builds the scope the member has confirmed now: a
|
|
40113
40736
|
// changed selection composes the plan fresh from the preset (delta round 3, T-M4).
|
|
@@ -40290,7 +40913,7 @@ function notify(message) {
|
|
|
40290
40913
|
`);
|
|
40291
40914
|
try {
|
|
40292
40915
|
ensureAppDataDir();
|
|
40293
|
-
|
|
40916
|
+
fs35.appendFileSync(path33.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
|
|
40294
40917
|
`);
|
|
40295
40918
|
} catch {
|
|
40296
40919
|
}
|
|
@@ -40360,20 +40983,20 @@ function withCockpitShell(page, screen, extraHead = "") {
|
|
|
40360
40983
|
function clientDocsDir(locationId2, base = appDataDir()) {
|
|
40361
40984
|
const safeLoc = String(locationId2).replace(/[^A-Za-z0-9_-]/g, "");
|
|
40362
40985
|
if (!safeLoc) throw new Error("no client");
|
|
40363
|
-
return
|
|
40986
|
+
return path33.join(base, "clients", safeLoc, "documents");
|
|
40364
40987
|
}
|
|
40365
40988
|
function safeDocName(name) {
|
|
40366
|
-
const cleaned =
|
|
40367
|
-
const ext =
|
|
40989
|
+
const cleaned = path33.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, " ").trim();
|
|
40990
|
+
const ext = path33.extname(cleaned).toLowerCase();
|
|
40368
40991
|
if (!cleaned || cleaned.startsWith(".") || !DOC_EXTS.has(ext)) throw new Error("Only plain text (.txt, .md, .csv), .vtt and .srt files can be saved here.");
|
|
40369
40992
|
const stem = cleaned.slice(0, cleaned.length - ext.length).slice(0, 120 - ext.length).trim();
|
|
40370
40993
|
return `${stem || "document"}${ext}`;
|
|
40371
40994
|
}
|
|
40372
40995
|
function listClientDocs(locationId2, base = appDataDir()) {
|
|
40373
40996
|
const dir = clientDocsDir(locationId2, base);
|
|
40374
|
-
if (!
|
|
40375
|
-
return
|
|
40376
|
-
const st =
|
|
40997
|
+
if (!fs35.existsSync(dir)) return [];
|
|
40998
|
+
return fs35.readdirSync(dir).filter((f) => DOC_EXTS.has(path33.extname(f).toLowerCase())).map((f) => {
|
|
40999
|
+
const st = fs35.statSync(path33.join(dir, f));
|
|
40377
41000
|
return { name: f, bytes: st.size, savedAt: st.mtime.toISOString() };
|
|
40378
41001
|
}).sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
|
40379
41002
|
}
|
|
@@ -40391,7 +41014,7 @@ function readFailureText(e) {
|
|
|
40391
41014
|
function logQuietly(message) {
|
|
40392
41015
|
try {
|
|
40393
41016
|
ensureAppDataDir();
|
|
40394
|
-
|
|
41017
|
+
fs35.appendFileSync(path33.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
|
|
40395
41018
|
`);
|
|
40396
41019
|
} catch {
|
|
40397
41020
|
}
|
|
@@ -40464,40 +41087,40 @@ function newClientFailure(error, surface = "command-os", account2) {
|
|
|
40464
41087
|
}
|
|
40465
41088
|
function insideDir(dir, p) {
|
|
40466
41089
|
try {
|
|
40467
|
-
const real =
|
|
40468
|
-
const base =
|
|
40469
|
-
return real === base || real.startsWith(base +
|
|
41090
|
+
const real = fs35.realpathSync(p);
|
|
41091
|
+
const base = fs35.realpathSync(dir);
|
|
41092
|
+
return real === base || real.startsWith(base + path33.sep);
|
|
40470
41093
|
} catch {
|
|
40471
41094
|
return false;
|
|
40472
41095
|
}
|
|
40473
41096
|
}
|
|
40474
41097
|
function uniqueDocName(dir, name) {
|
|
40475
|
-
const ext =
|
|
41098
|
+
const ext = path33.extname(name), stem = name.slice(0, name.length - ext.length);
|
|
40476
41099
|
let candidate = name;
|
|
40477
|
-
for (let n = 2;
|
|
41100
|
+
for (let n = 2; fs35.existsSync(path33.join(dir, candidate)); n++) candidate = `${stem} (${n})${ext}`;
|
|
40478
41101
|
return candidate;
|
|
40479
41102
|
}
|
|
40480
41103
|
function writeDocAtomically(dir, name, bytes) {
|
|
40481
|
-
|
|
41104
|
+
fs35.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
40482
41105
|
const finalName = uniqueDocName(dir, name);
|
|
40483
|
-
const tmp =
|
|
40484
|
-
const fd =
|
|
41106
|
+
const tmp = path33.join(dir, `.${finalName}.${process.pid}.tmp`);
|
|
41107
|
+
const fd = fs35.openSync(tmp, fs35.constants.O_WRONLY | fs35.constants.O_CREAT | fs35.constants.O_EXCL | (fs35.constants.O_NOFOLLOW ?? 0), 384);
|
|
40485
41108
|
try {
|
|
40486
|
-
|
|
40487
|
-
|
|
41109
|
+
fs35.writeSync(fd, bytes);
|
|
41110
|
+
fs35.fsyncSync(fd);
|
|
40488
41111
|
} finally {
|
|
40489
|
-
|
|
41112
|
+
fs35.closeSync(fd);
|
|
40490
41113
|
}
|
|
40491
|
-
const dest =
|
|
41114
|
+
const dest = path33.join(dir, finalName);
|
|
40492
41115
|
try {
|
|
40493
|
-
if (
|
|
40494
|
-
|
|
41116
|
+
if (fs35.lstatSync(dest).isSymbolicLink()) {
|
|
41117
|
+
fs35.unlinkSync(tmp);
|
|
40495
41118
|
throw new Error("that name is taken by a link");
|
|
40496
41119
|
}
|
|
40497
41120
|
} catch (e) {
|
|
40498
41121
|
if (e.code !== "ENOENT") throw e;
|
|
40499
41122
|
}
|
|
40500
|
-
|
|
41123
|
+
fs35.renameSync(tmp, dest);
|
|
40501
41124
|
return finalName;
|
|
40502
41125
|
}
|
|
40503
41126
|
function readableAccount(locationId2, roster = currentRosterRef(), state = readCockpitState()) {
|
|
@@ -41252,6 +41875,9 @@ function preflightCard(c){const box=document.createElement("section");box.classN
|
|
|
41252
41875
|
const a=document.createElement("a");a.href="/agency#connections";a.textContent="Open Your agency";box.appendChild(a);return box}
|
|
41253
41876
|
function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES.map(()=>"pending")}).stages;
|
|
41254
41877
|
const sandbox=typeOf(state,c.locationId)==="sandbox";
|
|
41878
|
+
// A template or hidden account opens only so its owner can lock or unlock it and change its
|
|
41879
|
+
// type: nothing on its card builds into it (review L-1; a template can be the live snapshot source).
|
|
41880
|
+
const parkedType=["template","hidden"].includes(typeOf(state,c.locationId));
|
|
41255
41881
|
const div=document.createElement("div");div.className="client";
|
|
41256
41882
|
const head=document.createElement("div");head.className="client-head";
|
|
41257
41883
|
const av=document.createElement("span");av.className="avatar lg";av.textContent=initials(c.name);head.appendChild(av);
|
|
@@ -41269,7 +41895,8 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41269
41895
|
isel.onchange=async()=>{await postJSON("/api/industry",{locationId:c.locationId,industry:isel.value});load()};
|
|
41270
41896
|
sels.appendChild(isel);head.appendChild(sels);div.appendChild(head);
|
|
41271
41897
|
const costs=(state.costRecords||[]).filter(r=>r.locationId===c.locationId);if(costs.length){const details=document.createElement("details");const title=document.createElement("summary");title.textContent="Claude usage for this client";details.appendChild(title);for(const cost of costs){const p=document.createElement("p");p.textContent=costLabel(cost.kind)+": "+(typeof cost.costUsd==="number"?"$"+cost.costUsd.toFixed(4):"cost not reported")+". "+new Date(cost.at).toLocaleString();details.appendChild(p)}div.appendChild(details)}
|
|
41272
|
-
if(
|
|
41898
|
+
if(parkedType){const pn=document.createElement("p");pn.className="muted";pn.textContent="This is a "+typeOf(state,c.locationId)+" account, so nothing is built into it from here. You can lock or unlock it below, or change its type to Client to build it.";div.appendChild(pn)}
|
|
41899
|
+
else if(!st.some(s=>s==="done"))div.appendChild(preflightCard(c));
|
|
41273
41900
|
const approveBuilding=async()=>{const typed=prompt('Approve the Blueprint Review for "'+c.name+'" and let Command OS build in this account.'+String.fromCharCode(10,10)+'Type the client name exactly to confirm:');if(typed===null)return;
|
|
41274
41901
|
try{const response=await fetch("/api/build-approval",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,confirmName:typed})});const r=await response.json();
|
|
41275
41902
|
if(!response.ok||r.ok!==true){r.ok=false;r.error=r.error||"Command OS could not confirm the result. Refresh this client to read its saved state before retrying."}
|
|
@@ -41315,8 +41942,8 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41315
41942
|
el.appendChild(note)}
|
|
41316
41943
|
if(POWERED.includes(i)&&(st[i]||"pending")!=="done"){el.style.cursor="default";
|
|
41317
41944
|
el.title="Command OS runs and checks this step. Its result comes from a run, never from a click."}
|
|
41318
|
-
if(gate&&!anyBusy){const action=document.createElement("button");action.className="intakebtn";action.textContent=({2:"Approve this plan",5:"Confirm these tasks are finished",6:"Sign this client live"})[i];action.onclick=()=>{if(i===2)return approveBuilding();return markStage(c.locationId,i,"done")};el.appendChild(action)}
|
|
41319
|
-
const canBuild
|
|
41945
|
+
if(gate&&!anyBusy&&!parkedType){const action=document.createElement("button");action.className="intakebtn";action.textContent=({2:"Approve this plan",5:"Confirm these tasks are finished",6:"Sign this client live"})[i];action.onclick=()=>{if(i===2)return approveBuilding();return markStage(c.locationId,i,"done")};el.appendChild(action)}
|
|
41946
|
+
const canBuild=!parkedType&&(i<=1||sandbox||Boolean((state.buildApproved||{})[c.locationId]));
|
|
41320
41947
|
if(canBuild&&POWERED.includes(i)){const b=document.createElement("button");b.className="runbtn";
|
|
41321
41948
|
const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":"Run this step";
|
|
41322
41949
|
b.disabled=Boolean(busy)||(!st.some(s=>s==="done")&&!preflights[c.locationId]?.ok);
|
|
@@ -41337,7 +41964,7 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41337
41964
|
for(const stale of view.stale){const p=document.createElement("p");p.textContent=LABELS[stale.stage]+": stale. "+stale.reason;div.appendChild(p)}
|
|
41338
41965
|
if(view.approvedAt){const p=document.createElement("p");p.textContent="Building approved on "+new Date(view.approvedAt).toLocaleString()+".";div.appendChild(p)}
|
|
41339
41966
|
for(const repair of view.repairs){const p=document.createElement("p");p.textContent="Live workflow repair: "+repair.name+". "+repair.changes.join(" ");div.appendChild(p)}
|
|
41340
|
-
if(view.planHash){const approve=document.createElement("button");approve.textContent="Approve building\u2026";approve.onclick=async()=>{const typed=sandbox?c.name:prompt("Approve building and the listed live workflow repairs for "+c.name+". Type the client name to confirm.");if(typed===null)return;if(await postJSON("/api/build-approval",{locationId:c.locationId,confirmName:typed,repairHashes:view.repairs.map(r=>r.hash)}))load()};div.appendChild(approve)}
|
|
41967
|
+
if(view.planHash&&!parkedType){const approve=document.createElement("button");approve.textContent="Approve building\u2026";approve.onclick=async()=>{const typed=sandbox?c.name:prompt("Approve building and the listed live workflow repairs for "+c.name+". Type the client name to confirm.");if(typed===null)return;if(await postJSON("/api/build-approval",{locationId:c.locationId,confirmName:typed,repairHashes:view.repairs.map(r=>r.hash)}))load()};div.appendChild(approve)}
|
|
41341
41968
|
}else{const p=document.createElement("p");p.textContent="The executable plan is prepared when Build runs.";div.appendChild(p)}
|
|
41342
41969
|
if(view?.handoff)div.appendChild(handoffPanel(c,view));
|
|
41343
41970
|
const _prot=PROTECTED[c.locationId];
|
|
@@ -41354,6 +41981,7 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41354
41981
|
const rv=document.createElement("button");rv.className="qdel";rv.style.marginLeft=".5rem";rv.textContent="revoke";
|
|
41355
41982
|
rv.onclick=async()=>{await postJSON("/api/build-approval",{locationId:c.locationId,revoke:true});load()};
|
|
41356
41983
|
ab.appendChild(s1);ab.appendChild(rv);ab.appendChild(protectBtn(c))}
|
|
41984
|
+
else if(parkedType){ab.appendChild(protectBtn(c))}
|
|
41357
41985
|
else{const w=document.createElement("span");w.className="muted";w.textContent="Automated building is off for this client. ";
|
|
41358
41986
|
const btn=document.createElement("button");btn.className="intakebtn";btn.style.padding=".25rem .55rem";btn.textContent="Approve building\u2026";
|
|
41359
41987
|
btn.onclick=approveBuilding;
|
|
@@ -41734,10 +42362,12 @@ function render(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...sta
|
|
|
41734
42362
|
const parked=sorted.filter(c=>match(c)&&["template","hidden"].includes(typeOf(state,c.locationId)));
|
|
41735
42363
|
if(!board.length&&!parked.length){const d=document.createElement("div");d.className="client";d.innerHTML="<h2>No matches</h2>";m.appendChild(d);return}
|
|
41736
42364
|
/* One card open at a time. Default: the account being built, else the busiest one. */
|
|
41737
|
-
|
|
42365
|
+
/* A template or hidden account opens too: its card carries the lock, and an owner must never be locked out of their own account (BUG-2026-09-24-07). */
|
|
42366
|
+
const openable=[...board,...parked];
|
|
42367
|
+
let act=(activeId&&openable.some(c=>c.locationId===activeId))?activeId:null;
|
|
41738
42368
|
if(!act&&run&&run.running&&board.some(c=>c.locationId===run.locationId))act=run.locationId;
|
|
41739
42369
|
if(!act&&board.length)act=board[0].locationId;
|
|
41740
|
-
const actC=
|
|
42370
|
+
const actC=openable.find(c=>c.locationId===act);
|
|
41741
42371
|
if(run&&run.running&&actC&&run.locationId!==actC.locationId){
|
|
41742
42372
|
const other=ROSTER.find(c=>c.locationId===run.locationId);
|
|
41743
42373
|
if(other){const ban=document.createElement("div");ban.className="card";ban.style.cssText="display:flex;align-items:center;gap:.6rem;padding:.55rem .9rem;margin-bottom:1rem;border-color:var(--gold)";
|
|
@@ -41755,11 +42385,12 @@ function render(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...sta
|
|
|
41755
42385
|
const hint=document.createElement("p");hint.className="switchnote";
|
|
41756
42386
|
hint.textContent="One account at a time. Opening another puts this one back in the list. Nothing is lost.";
|
|
41757
42387
|
m.appendChild(hint)}
|
|
41758
|
-
|
|
41759
|
-
|
|
42388
|
+
const parkedRest=parked.filter(c=>c.locationId!==act);
|
|
42389
|
+
if(parkedRest.length){const gh=document.createElement("div");gh.className="grouphead";
|
|
42390
|
+
gh.textContent=(showParked?"\u25BE":"\u25B8")+" Templates & hidden ("+parkedRest.length+")";
|
|
41760
42391
|
gh.onclick=()=>{showParked=!showParked;render(state,run)};m.appendChild(gh);
|
|
41761
42392
|
if(showParked){const list2=document.createElement("div");list2.className="card acct-list";
|
|
41762
|
-
for(const c of
|
|
42393
|
+
for(const c of parkedRest)list2.appendChild(acctRow(c,state,run));m.appendChild(list2)}}}
|
|
41763
42394
|
${COCKPIT_TYPE_JS}
|
|
41764
42395
|
load();
|
|
41765
42396
|
</script></body></html>`;
|
|
@@ -42301,8 +42932,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42301
42932
|
if (!bytes.length) throw new Error("that file is empty");
|
|
42302
42933
|
if (bytes.length > DOC_MAX_BYTES) throw new Error("that file is over 20 MB");
|
|
42303
42934
|
const dir = clientDocsDir(args.locationId);
|
|
42304
|
-
|
|
42305
|
-
if (!insideDir(
|
|
42935
|
+
fs35.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
42936
|
+
if (!insideDir(path33.join(appDataDir(), "clients"), dir)) throw new Error("This client's documents folder is not where it should be, so nothing was saved.");
|
|
42306
42937
|
const saved = writeDocAtomically(dir, name, bytes);
|
|
42307
42938
|
res.end(JSON.stringify({ ok: true, name: saved, files: listClientDocs(args.locationId) }));
|
|
42308
42939
|
} catch (e) {
|
|
@@ -42477,6 +43108,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42477
43108
|
try {
|
|
42478
43109
|
const { locationId: locationId2, stage, intakeConfirmed } = JSON.parse(body);
|
|
42479
43110
|
if (activeRun2?.running) throw new Error("a run is already in progress");
|
|
43111
|
+
const runType = readCockpitState().accounts?.[locationId2]?.type;
|
|
43112
|
+
if (runType === "template" || runType === "hidden") throw new Error(PARKED_NOT_BUILT);
|
|
42480
43113
|
let st0 = readCockpitState();
|
|
42481
43114
|
let inputHash;
|
|
42482
43115
|
let normalizedAnswers;
|
|
@@ -42505,8 +43138,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42505
43138
|
const snap = await readIntakeSnapshot(c, locationId2);
|
|
42506
43139
|
if (snap.status === "unavailable") throw new Error(INTAKE_UNREADABLE);
|
|
42507
43140
|
if (snap.status !== "received") throw new Error("Open the real form, submit it, then refresh.");
|
|
42508
|
-
const required = composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }).filter((
|
|
42509
|
-
if (required.length) throw new Error("Complete these required intake answers: " + required.map((
|
|
43141
|
+
const required = composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }).filter((q3) => q3.required && !String(normalizedAnswers?.[q3.label] ?? normalizedAnswers?.[q3.key] ?? "").trim());
|
|
43142
|
+
if (required.length) throw new Error("Complete these required intake answers: " + required.map((q3) => q3.label).join(", ") + ".");
|
|
42510
43143
|
}
|
|
42511
43144
|
if (stage === 0 && intakeConfirmed !== true) throw new Error("Confirm the custom fields, form and published notification workflow on this client\u2019s Intake button.");
|
|
42512
43145
|
if (!st0.accounts?.[locationId2]?.industry) throw new Error("Choose the client industry before running Intake.");
|
|
@@ -42552,7 +43185,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42552
43185
|
industries: INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })),
|
|
42553
43186
|
overlay: agency,
|
|
42554
43187
|
warnings: unknownKeys(agency, INTAKE_QUESTIONS).map((k) => `Your overlay references "${k}", which isn't in the base intake.`),
|
|
42555
|
-
questions: composed.map((
|
|
43188
|
+
questions: composed.map((q3) => ({ key: q3.key, label: q3.label, sectionLabel: q3.sectionLabel, required: q3.required, base: INTAKE_QUESTIONS.some((b) => b.key === q3.key) }))
|
|
42556
43189
|
}));
|
|
42557
43190
|
return;
|
|
42558
43191
|
}
|
|
@@ -42588,14 +43221,14 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42588
43221
|
const snap = c ? await readIntakeSnapshot(c, locationId2, Boolean(st.intakeSent?.[locationId2])) : { status: "not_installed", fields: {}, queryKeys: {}, submissions: 0 };
|
|
42589
43222
|
if (snap.status === "unavailable") throw new Error(INTAKE_UNREADABLE);
|
|
42590
43223
|
const industry = st.accounts?.[locationId2]?.industry;
|
|
42591
|
-
const questions = composeQuestionSet({ industry, agency: readOverlay() }).map((
|
|
43224
|
+
const questions = composeQuestionSet({ industry, agency: readOverlay() }).map((q3) => ({ key: q3.key, contactTag: q3.field.kind === "standard" ? q3.field.tag : void 0, label: q3.label, sectionLabel: q3.sectionLabel, required: q3.required, options: q3.options ?? null, placeholder: q3.placeholder ?? "", multi: q3.field.kind === "custom" && q3.field.dataType === "MULTIPLE_OPTIONS" }));
|
|
42592
43225
|
const answers = normalizedIntake(snap, st.intakeDrafts?.[locationId2]);
|
|
42593
43226
|
const submittedAnswers = normalizedIntake(snap);
|
|
42594
43227
|
const hash = intakeInput(submittedAnswers, industry, composeQuestionSet({ industry, agency: readOverlay() }));
|
|
42595
43228
|
const latestState = readCockpitState();
|
|
42596
43229
|
writeCockpitState({ ...latestState, intakeInputs: { ...latestState.intakeInputs, [locationId2]: { hash, answers: submittedAnswers, status: snap.status } } });
|
|
42597
43230
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
42598
|
-
res.end(JSON.stringify({ ok: true, ...snap, questions, industry, inputHash: hash, unansweredRequired: questions.filter((
|
|
43231
|
+
res.end(JSON.stringify({ ok: true, ...snap, questions, industry, inputHash: hash, unansweredRequired: questions.filter((q3) => q3.required && !String(answers[q3.label] ?? answers[q3.key] ?? "").trim()).map((q3) => q3.label), costs: readRunCosts().filter((r) => r.locationId === locationId2 && (r.kind === "intake extraction" || r.kind === "engine-check")).slice(-10), submittedAnswers: snap.answers ?? {}, answers, savedAt: st.intakeDrafts?.[locationId2]?.savedAt, connectionPending: !c }));
|
|
42599
43232
|
} catch (e) {
|
|
42600
43233
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
42601
43234
|
logQuietly(`client-intake ${new URL(req.url || "/", "http://x").searchParams.get("locationId") || ""}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -42614,7 +43247,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42614
43247
|
const snap = await readIntakeSnapshot(c, args.locationId, false);
|
|
42615
43248
|
if (!snap.formId || !snap.formUrl) throw new Error(INTAKE_FORM_REQUIRED);
|
|
42616
43249
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
42617
|
-
const identityLabels = new Set(composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).filter((
|
|
43250
|
+
const identityLabels = new Set(composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).filter((q3) => q3.field.kind === "standard").map((q3) => q3.label));
|
|
42618
43251
|
const customAnswers = Object.fromEntries(Object.entries(args.answers ?? {}).filter(([label]) => !identityLabels.has(label)));
|
|
42619
43252
|
const pf = prefillFormUrl(snap.formUrl, snap.queryKeys, customAnswers, args.contact ?? {}, snap.altQueryKeys ?? {});
|
|
42620
43253
|
res.end(JSON.stringify({ ok: true, url: pf.url, leftovers: pf.leftovers }));
|
|
@@ -42656,28 +43289,28 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42656
43289
|
if (args.file) {
|
|
42657
43290
|
const name = safeDocName(args.file);
|
|
42658
43291
|
const dir = clientDocsDir(args.locationId);
|
|
42659
|
-
const p =
|
|
42660
|
-
if (!
|
|
42661
|
-
if (
|
|
42662
|
-
const text2 =
|
|
43292
|
+
const p = path33.join(dir, name);
|
|
43293
|
+
if (!fs35.existsSync(p)) throw new Error("that document is not saved for this client");
|
|
43294
|
+
if (fs35.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error("that document is not saved for this client");
|
|
43295
|
+
const text2 = fs35.readFileSync(p, "utf8");
|
|
42663
43296
|
if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
|
|
42664
43297
|
fileSource = { kind: "text", text: text2 };
|
|
42665
43298
|
}
|
|
42666
43299
|
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 ?? "") };
|
|
42667
43300
|
let recorderPatterns = [];
|
|
42668
43301
|
if (source.kind === "recorder") {
|
|
42669
|
-
recorderPatterns = await new Promise((
|
|
43302
|
+
recorderPatterns = await new Promise((resolve13) => {
|
|
42670
43303
|
const mcpList = claudeCommand(claudeBin(), ["mcp", "list"]);
|
|
42671
43304
|
const ls = (0, import_child_process5.spawn)(mcpList.command, mcpList.args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
42672
43305
|
let buf = "";
|
|
42673
43306
|
ls.stdout.on("data", (d) => {
|
|
42674
43307
|
buf += d.toString();
|
|
42675
43308
|
});
|
|
42676
|
-
ls.on("close", () =>
|
|
42677
|
-
ls.on("error", () =>
|
|
43309
|
+
ls.on("close", () => resolve13(recorderAllowPatterns(buf)));
|
|
43310
|
+
ls.on("error", () => resolve13([]));
|
|
42678
43311
|
setTimeout(() => {
|
|
42679
43312
|
ls.kill("SIGKILL");
|
|
42680
|
-
|
|
43313
|
+
resolve13(recorderAllowPatterns(buf));
|
|
42681
43314
|
}, 9e4).unref?.();
|
|
42682
43315
|
});
|
|
42683
43316
|
if (!recorderPatterns.length) {
|
|
@@ -42688,10 +43321,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42688
43321
|
}
|
|
42689
43322
|
const out = await prefillFromSource(
|
|
42690
43323
|
(prompt, tools) => callClaudeRecorded(bin, ["-p", prompt, "--max-turns", "25", "--disallowedTools", tools.deny, "--allowedTools", tools.allow || ""], { locationId: args.locationId, stage: 0, kind: "intake extraction" }),
|
|
42691
|
-
composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((
|
|
43324
|
+
composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((q3) => q3.label),
|
|
42692
43325
|
source,
|
|
42693
43326
|
recorderPatterns,
|
|
42694
|
-
composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((
|
|
43327
|
+
composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((q3) => ({ label: q3.label, options: q3.options, multi: q3.field.kind === "custom" && q3.field.dataType === "MULTIPLE_OPTIONS", dataType: q3.field.kind === "custom" ? q3.field.dataType : q3.field.tag }))
|
|
42695
43328
|
);
|
|
42696
43329
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
42697
43330
|
res.end(JSON.stringify({ ...out, costs: readRunCosts().filter((r) => r.locationId === args.locationId).slice(-2) }));
|
|
@@ -42733,14 +43366,14 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42733
43366
|
}
|
|
42734
43367
|
if (req.method === "GET" && /^\/brand\/[a-z0-9-]+\/screenshot\.png$/.test(url)) {
|
|
42735
43368
|
const slug3 = url.split("/")[2];
|
|
42736
|
-
const file2 =
|
|
42737
|
-
if (!
|
|
43369
|
+
const file2 = path33.join(brandDir(slug3), "screenshot.png");
|
|
43370
|
+
if (!fs35.existsSync(file2)) {
|
|
42738
43371
|
res.writeHead(404);
|
|
42739
43372
|
res.end();
|
|
42740
43373
|
return;
|
|
42741
43374
|
}
|
|
42742
43375
|
res.writeHead(200, { "Content-Type": "image/png", "Cache-Control": "no-store" });
|
|
42743
|
-
res.end(
|
|
43376
|
+
res.end(fs35.readFileSync(file2));
|
|
42744
43377
|
return;
|
|
42745
43378
|
}
|
|
42746
43379
|
if (req.method === "GET" && /^\/brand\/[a-z0-9-]+$/.test(url)) {
|
|
@@ -42749,7 +43382,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42749
43382
|
const dna = houseLooks().find((d) => d.slug === slug3) ?? readBrand(slug3);
|
|
42750
43383
|
const dir = brandExists(slug3) ? brandDir(slug3) : void 0;
|
|
42751
43384
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
42752
|
-
res.end(brandCardHtml(dna, { screenshot: dir ?
|
|
43385
|
+
res.end(brandCardHtml(dna, { screenshot: dir ? fs35.existsSync(path33.join(dir, "screenshot.png")) : false, dir, seatToken: mayServeSeat(req, port) ? seatToken : "" }));
|
|
42753
43386
|
} catch (e) {
|
|
42754
43387
|
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
42755
43388
|
res.end(e.message);
|
|
@@ -42911,7 +43544,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42911
43544
|
}
|
|
42912
43545
|
try {
|
|
42913
43546
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
42914
|
-
res.end(
|
|
43547
|
+
res.end(fs35.readFileSync(localCopyPath(locationId2), "utf8"));
|
|
42915
43548
|
} catch {
|
|
42916
43549
|
res.end("The handoff is not prepared yet. Run Verify first.");
|
|
42917
43550
|
}
|
|
@@ -43011,8 +43644,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
43011
43644
|
return;
|
|
43012
43645
|
}
|
|
43013
43646
|
if (req.method === "GET" && url === "/blueprint") {
|
|
43014
|
-
const
|
|
43015
|
-
const locationId2 =
|
|
43647
|
+
const q3 = new URL(req.url || "/", "http://x").searchParams;
|
|
43648
|
+
const locationId2 = q3.get("locationId") || "";
|
|
43016
43649
|
const st = readCockpitState();
|
|
43017
43650
|
const review = st.reviews?.[locationId2];
|
|
43018
43651
|
if (!review?.ok) {
|
|
@@ -43022,7 +43655,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
43022
43655
|
}
|
|
43023
43656
|
const name = currentRoster().find((r) => r.locationId === locationId2)?.name || locationId2;
|
|
43024
43657
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
43025
|
-
res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name, agencyName:
|
|
43658
|
+
res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
|
|
43026
43659
|
return;
|
|
43027
43660
|
}
|
|
43028
43661
|
if (req.method === "GET" && url === "/api/review") {
|
|
@@ -43483,9 +44116,9 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
43483
44116
|
res.end();
|
|
43484
44117
|
}));
|
|
43485
44118
|
try {
|
|
43486
|
-
await new Promise((
|
|
44119
|
+
await new Promise((resolve13, reject) => {
|
|
43487
44120
|
server2.once("error", reject);
|
|
43488
|
-
server2.listen(port, "127.0.0.1",
|
|
44121
|
+
server2.listen(port, "127.0.0.1", resolve13);
|
|
43489
44122
|
});
|
|
43490
44123
|
} catch (e) {
|
|
43491
44124
|
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)}`;
|
|
@@ -43511,13 +44144,13 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
43511
44144
|
if (adopted) process.stderr.write(` Synced ${adopted} client${adopted === 1 ? "" : "s"} from your GHL (teammate updates).
|
|
43512
44145
|
`);
|
|
43513
44146
|
});
|
|
43514
|
-
return await new Promise((
|
|
43515
|
-
const
|
|
43516
|
-
process.on("SIGINT",
|
|
43517
|
-
process.on("SIGTERM",
|
|
44147
|
+
return await new Promise((resolve13) => {
|
|
44148
|
+
const stop2 = () => server2.close(() => resolve13(0));
|
|
44149
|
+
process.on("SIGINT", stop2);
|
|
44150
|
+
process.on("SIGTERM", stop2);
|
|
43518
44151
|
});
|
|
43519
44152
|
}
|
|
43520
|
-
var import_zod62, import_node_util4,
|
|
44153
|
+
var import_zod62, import_node_util4, fs35, path33, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, BOARD_ROUTES, BOARD_TITLES, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest;
|
|
43521
44154
|
var init_dashboard = __esm({
|
|
43522
44155
|
"src/dashboard.ts"() {
|
|
43523
44156
|
"use strict";
|
|
@@ -43545,8 +44178,8 @@ var init_dashboard = __esm({
|
|
|
43545
44178
|
init_handoff_snapshot();
|
|
43546
44179
|
init_member_progress();
|
|
43547
44180
|
init_build_obligations();
|
|
43548
|
-
|
|
43549
|
-
|
|
44181
|
+
fs35 = __toESM(require("fs"));
|
|
44182
|
+
path33 = __toESM(require("path"));
|
|
43550
44183
|
http = __toESM(require("http"));
|
|
43551
44184
|
import_child_process4 = require("child_process");
|
|
43552
44185
|
init_credentials_store();
|
|
@@ -43624,6 +44257,7 @@ var init_dashboard = __esm({
|
|
|
43624
44257
|
UPGRADE_MSG = "\n The cockpit is not included in this licence, so it did not start.\n Nothing was changed.\n\n";
|
|
43625
44258
|
DOC_EXTS = /* @__PURE__ */ new Set([".md", ".txt", ".vtt", ".srt", ".csv"]);
|
|
43626
44259
|
DOC_MAX_BYTES = 20 * 1024 * 1024;
|
|
44260
|
+
PARKED_NOT_BUILT = "Template and hidden accounts are not built from Command OS. Change the account's type to Client first.";
|
|
43627
44261
|
NEEDS_AGENCY_KEY = "No agency key on file. Run register_agency_key in GHL Command first.";
|
|
43628
44262
|
INTAKE_UNREADABLE = "Command OS could not read this client's account. Paste its key again in Build a Client Account.";
|
|
43629
44263
|
INTAKE_FORM_REQUIRED = "Run the Intake stage in Client Builds to create the form, then come back and submit.";
|
|
@@ -44115,12 +44749,12 @@ ${companies.map((c) => ` ${c.name || c.companyId} (${c.companyId})`).join("\n")
|
|
|
44115
44749
|
]
|
|
44116
44750
|
};
|
|
44117
44751
|
}
|
|
44118
|
-
const
|
|
44752
|
+
const tokens2 = registry2.listLocations().map((loc) => ({
|
|
44119
44753
|
...loc,
|
|
44120
44754
|
companyId: registry2.getToken(loc.locationId)?.companyId
|
|
44121
44755
|
}));
|
|
44122
44756
|
const currentId = client.defaultLocationId;
|
|
44123
|
-
const lines =
|
|
44757
|
+
const lines = tokens2.map((loc) => {
|
|
44124
44758
|
const marker = loc.locationId === currentId ? "\u2192 " : " ";
|
|
44125
44759
|
const company = loc.companyId ? ` [company ${loc.companyId}${companiesWithFirebase.has(loc.companyId) ? ", Firebase \u2713" : ", no Firebase"}]` : "";
|
|
44126
44760
|
return `${marker}${loc.name} (${loc.locationId})${company}`;
|
|
@@ -44133,7 +44767,7 @@ ${companies.map((c) => ` ${c.name || c.companyId} (${c.companyId})`).join("\n")
|
|
|
44133
44767
|
content: [
|
|
44134
44768
|
{
|
|
44135
44769
|
type: "text",
|
|
44136
|
-
text: `Token Registry (${
|
|
44770
|
+
text: `Token Registry (${tokens2.length} locations):
|
|
44137
44771
|
${lines.join("\n")}
|
|
44138
44772
|
|
|
44139
44773
|
\u2192 = currently active${fbSection}`
|
|
@@ -44288,7 +44922,7 @@ var init_location_switcher = __esm({
|
|
|
44288
44922
|
|
|
44289
44923
|
// src/tools/bulk-operations.ts
|
|
44290
44924
|
function delay2(ms) {
|
|
44291
|
-
return new Promise((
|
|
44925
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
44292
44926
|
}
|
|
44293
44927
|
function formatResults(op, results, total) {
|
|
44294
44928
|
return `${op}: ${results.success} success, ${results.failed} failed out of ${total}.${results.errors.length ? "\nErrors:\n" + results.errors.join("\n") : ""}`;
|
|
@@ -44533,7 +45167,7 @@ var init_asset_ownership = __esm({
|
|
|
44533
45167
|
function sectionItems(section2) {
|
|
44534
45168
|
return Array.isArray(section2) ? section2 : [];
|
|
44535
45169
|
}
|
|
44536
|
-
function
|
|
45170
|
+
function bullet2(lines) {
|
|
44537
45171
|
return lines.length ? lines.map((l) => `- ${l}`).join("\n") : "- None found.";
|
|
44538
45172
|
}
|
|
44539
45173
|
function plainReason(reason3) {
|
|
@@ -44555,7 +45189,7 @@ function sectionList(section2, render) {
|
|
|
44555
45189
|
if (!Array.isArray(section2)) {
|
|
44556
45190
|
return `**Not read.** ${plainReason(section2.unavailable)}. This section is unknown, which is NOT the same as empty. Check it by hand before you rely on this document.`;
|
|
44557
45191
|
}
|
|
44558
|
-
return
|
|
45192
|
+
return bullet2(section2.map(render));
|
|
44559
45193
|
}
|
|
44560
45194
|
function buildHandoffDoc(input) {
|
|
44561
45195
|
const {
|
|
@@ -44577,13 +45211,13 @@ function buildHandoffDoc(input) {
|
|
|
44577
45211
|
if (!items.length) return "";
|
|
44578
45212
|
return `**${kind}** ${OWNER_MEANING[kind]}
|
|
44579
45213
|
|
|
44580
|
-
${
|
|
45214
|
+
${bullet2(
|
|
44581
45215
|
items.map((i) => i.note ? `${i.asset} (${i.note})` : i.asset)
|
|
44582
45216
|
)}`;
|
|
44583
45217
|
}).filter(Boolean).join("\n\n") : "Nothing has been recorded yet. Every asset below is unanswered.";
|
|
44584
45218
|
const unknownSection = ownership.unknown.length ? `These exist in the account but nobody has said who owns them. Answer them once and they stay answered:
|
|
44585
45219
|
|
|
44586
|
-
${
|
|
45220
|
+
${bullet2(ownership.unknown)}` : "Every asset found in this account has a recorded owner.";
|
|
44587
45221
|
const webhookSection = sectionList(webhooks, (w) => `${w.name} sends to ${w.url}${w.events.length ? ` when: ${w.events.join(", ")}` : ""}`) + (webhookScanPartial ? `
|
|
44588
45222
|
|
|
44589
45223
|
**This list may be incomplete.** ${webhookScanPartial}` : "");
|
|
@@ -44660,7 +45294,7 @@ ${ownership.unreadable.length ? `
|
|
|
44660
45294
|
|
|
44661
45295
|
Somebody edited these by hand and the answer no longer parses. They need a human:
|
|
44662
45296
|
|
|
44663
|
-
${
|
|
45297
|
+
${bullet2(ownership.unreadable)}
|
|
44664
45298
|
` : ""}
|
|
44665
45299
|
## Rotate these on the way out
|
|
44666
45300
|
|
|
@@ -44693,7 +45327,7 @@ var init_handoff_doc = __esm({
|
|
|
44693
45327
|
|
|
44694
45328
|
// src/tools/account-export.ts
|
|
44695
45329
|
function delay3(ms) {
|
|
44696
|
-
return new Promise((
|
|
45330
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
44697
45331
|
}
|
|
44698
45332
|
function builderSectionAvailability(builderLocationId, requestedLocationId) {
|
|
44699
45333
|
if (!builderLocationId) {
|
|
@@ -45408,13 +46042,13 @@ function contactServiceNote(payload) {
|
|
|
45408
46042
|
function registerSmartListTools(server2, builderClient) {
|
|
45409
46043
|
if (!builderClient) return;
|
|
45410
46044
|
const client = builderClient;
|
|
45411
|
-
async function smartListRequest(method,
|
|
45412
|
-
const url = `${SMARTLIST_BASE}${
|
|
46045
|
+
async function smartListRequest(method, path42, body) {
|
|
46046
|
+
const url = `${SMARTLIST_BASE}${path42}`;
|
|
45413
46047
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
45414
46048
|
const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
|
|
45415
46049
|
if (!response.ok) {
|
|
45416
46050
|
const text3 = await response.text();
|
|
45417
|
-
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${
|
|
46051
|
+
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path42}
|
|
45418
46052
|
${text3}`);
|
|
45419
46053
|
}
|
|
45420
46054
|
const text2 = await response.text();
|
|
@@ -45659,11 +46293,11 @@ var init_smart_lists = __esm({
|
|
|
45659
46293
|
function registerReputationTools(server2, builderClient) {
|
|
45660
46294
|
if (!builderClient) return;
|
|
45661
46295
|
const client = builderClient;
|
|
45662
|
-
async function reputationRequest(method,
|
|
45663
|
-
const response = await builderRequest(client, method, `${REPUTATION_BASE}${
|
|
46296
|
+
async function reputationRequest(method, path42) {
|
|
46297
|
+
const response = await builderRequest(client, method, `${REPUTATION_BASE}${path42}`);
|
|
45664
46298
|
if (!response.ok) {
|
|
45665
46299
|
const text3 = await response.text();
|
|
45666
|
-
throw new Error(`Reputation API Error ${response.status}: ${method} ${
|
|
46300
|
+
throw new Error(`Reputation API Error ${response.status}: ${method} ${path42}
|
|
45667
46301
|
${text3}`);
|
|
45668
46302
|
}
|
|
45669
46303
|
const text2 = await response.text();
|
|
@@ -45705,7 +46339,7 @@ ${text3}`);
|
|
|
45705
46339
|
);
|
|
45706
46340
|
}
|
|
45707
46341
|
function buildReviewsQuery(locationId2, opts = {}) {
|
|
45708
|
-
const
|
|
46342
|
+
const q3 = [
|
|
45709
46343
|
`filterParams[locationId][0][value]=${locationId2}`,
|
|
45710
46344
|
`filterParams[locationId][0][condition]=eq`,
|
|
45711
46345
|
`filterParams[deleted][0][value]=${opts.includeDeleted ? "true" : "false"}`,
|
|
@@ -45719,13 +46353,13 @@ function buildReviewsQuery(locationId2, opts = {}) {
|
|
|
45719
46353
|
if (opts.maxRating !== void 0) bounds.push([opts.maxRating, "lte"]);
|
|
45720
46354
|
}
|
|
45721
46355
|
bounds.forEach(([value, condition], i) => {
|
|
45722
|
-
|
|
45723
|
-
|
|
46356
|
+
q3.push(`filterParams[starRating][${i}][value]=${value}`);
|
|
46357
|
+
q3.push(`filterParams[starRating][${i}][condition]=${condition}`);
|
|
45724
46358
|
});
|
|
45725
|
-
|
|
45726
|
-
|
|
45727
|
-
|
|
45728
|
-
const query =
|
|
46359
|
+
q3.push(`sortParams[dateAdded]=-1`);
|
|
46360
|
+
q3.push(`pageNumber=${opts.pageNumber ?? 1}`);
|
|
46361
|
+
q3.push(`pageSize=${opts.pageSize ?? 10}`);
|
|
46362
|
+
const query = q3.map((p) => {
|
|
45729
46363
|
const i = p.indexOf("=");
|
|
45730
46364
|
return `${encodeURIComponent(p.slice(0, i))}=${encodeURIComponent(p.slice(i + 1))}`;
|
|
45731
46365
|
}).join("&");
|
|
@@ -45801,11 +46435,11 @@ var init_email_campaigns = __esm({
|
|
|
45801
46435
|
function registerMembershipTools(server2, builderClient) {
|
|
45802
46436
|
if (!builderClient) return;
|
|
45803
46437
|
const client = builderClient;
|
|
45804
|
-
async function membershipRequest(
|
|
45805
|
-
const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${
|
|
46438
|
+
async function membershipRequest(path42, method = "GET", body) {
|
|
46439
|
+
const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path42}`, body ? { body } : {});
|
|
45806
46440
|
if (!response.ok) {
|
|
45807
46441
|
const text3 = await response.text();
|
|
45808
|
-
throw new Error(`Membership API Error ${response.status}: ${method} ${
|
|
46442
|
+
throw new Error(`Membership API Error ${response.status}: ${method} ${path42}
|
|
45809
46443
|
${text3}`);
|
|
45810
46444
|
}
|
|
45811
46445
|
const text2 = await response.text();
|
|
@@ -45991,28 +46625,28 @@ var init_memberships = __esm({
|
|
|
45991
46625
|
|
|
45992
46626
|
// src/tools/template-deployer.ts
|
|
45993
46627
|
function delay4(ms) {
|
|
45994
|
-
return new Promise((
|
|
46628
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
45995
46629
|
}
|
|
45996
46630
|
function isAlreadyExistsError(message) {
|
|
45997
46631
|
return /already\s+exists?\b/i.test(message) || /duplicate/i.test(message);
|
|
45998
46632
|
}
|
|
45999
46633
|
function registerTemplateDeployerTools(server2, client) {
|
|
46000
46634
|
const builderClient = WorkflowBuilderClient.fromEnv();
|
|
46001
|
-
const templatesDir =
|
|
46635
|
+
const templatesDir = path34.resolve(__dirname, "..", "templates");
|
|
46002
46636
|
function validateTemplatePath(templateFile) {
|
|
46003
|
-
const resolved2 =
|
|
46004
|
-
const realPath =
|
|
46637
|
+
const resolved2 = path34.resolve(templateFile);
|
|
46638
|
+
const realPath = fs36.existsSync(resolved2) ? fs36.realpathSync(resolved2) : resolved2;
|
|
46005
46639
|
const allowedDirs = [
|
|
46006
46640
|
templatesDir,
|
|
46007
|
-
|
|
46008
|
-
|
|
46009
|
-
].map((d) =>
|
|
46010
|
-
const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir +
|
|
46641
|
+
path34.resolve(process.cwd(), "templates"),
|
|
46642
|
+
path34.resolve(__dirname, "..", "..", "templates")
|
|
46643
|
+
].map((d) => fs36.existsSync(d) ? fs36.realpathSync(d) : d);
|
|
46644
|
+
const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir + path34.sep));
|
|
46011
46645
|
if (!isAllowed) {
|
|
46012
46646
|
throw new Error(`Template file must be inside a templates/ directory. Got: ${realPath}`);
|
|
46013
46647
|
}
|
|
46014
|
-
if (
|
|
46015
|
-
const otherMatch = allowedDirs.slice(1).some((d) => !
|
|
46648
|
+
if (path34.relative(allowedDirs[0], realPath).startsWith("..")) {
|
|
46649
|
+
const otherMatch = allowedDirs.slice(1).some((d) => !path34.relative(d, realPath).startsWith(".."));
|
|
46016
46650
|
if (!otherMatch) {
|
|
46017
46651
|
throw new Error(`Template path escapes allowed directory.`);
|
|
46018
46652
|
}
|
|
@@ -46027,26 +46661,26 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
46027
46661
|
try {
|
|
46028
46662
|
const dirs = [
|
|
46029
46663
|
templatesDir,
|
|
46030
|
-
|
|
46031
|
-
|
|
46664
|
+
path34.resolve(process.cwd(), "templates"),
|
|
46665
|
+
path34.resolve(__dirname, "..", "..", "templates")
|
|
46032
46666
|
];
|
|
46033
46667
|
const templates = [];
|
|
46034
46668
|
const seen = /* @__PURE__ */ new Set();
|
|
46035
46669
|
for (const dir of dirs) {
|
|
46036
|
-
if (!
|
|
46037
|
-
const files =
|
|
46670
|
+
if (!fs36.existsSync(dir)) continue;
|
|
46671
|
+
const files = fs36.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
46038
46672
|
for (const file2 of files) {
|
|
46039
|
-
const full =
|
|
46673
|
+
const full = path34.join(dir, file2);
|
|
46040
46674
|
let real;
|
|
46041
46675
|
try {
|
|
46042
|
-
real =
|
|
46676
|
+
real = fs36.realpathSync(full);
|
|
46043
46677
|
} catch {
|
|
46044
46678
|
continue;
|
|
46045
46679
|
}
|
|
46046
46680
|
if (seen.has(real)) continue;
|
|
46047
46681
|
seen.add(real);
|
|
46048
46682
|
try {
|
|
46049
|
-
const content = JSON.parse(
|
|
46683
|
+
const content = JSON.parse(fs36.readFileSync(full, "utf-8"));
|
|
46050
46684
|
if (typeof content.templateName !== "string" || !content.templateName) continue;
|
|
46051
46685
|
const parsed2 = TemplateSchema.safeParse(content);
|
|
46052
46686
|
templates.push({
|
|
@@ -46083,7 +46717,7 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
46083
46717
|
async ({ templateFile }) => {
|
|
46084
46718
|
try {
|
|
46085
46719
|
const safePath = validateTemplatePath(templateFile);
|
|
46086
|
-
const content = JSON.parse(
|
|
46720
|
+
const content = JSON.parse(fs36.readFileSync(safePath, "utf-8"));
|
|
46087
46721
|
return {
|
|
46088
46722
|
content: [
|
|
46089
46723
|
{
|
|
@@ -46123,8 +46757,8 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
46123
46757
|
try {
|
|
46124
46758
|
const locId = client.resolveLocationId(locationId2);
|
|
46125
46759
|
const safePath = validateTemplatePath(templateFile);
|
|
46126
|
-
const template = TemplateSchema.parse(JSON.parse(
|
|
46127
|
-
const
|
|
46760
|
+
const template = TemplateSchema.parse(JSON.parse(fs36.readFileSync(safePath, "utf-8")));
|
|
46761
|
+
const resolve13 = (text2) => {
|
|
46128
46762
|
if (typeof text2 !== "string") return text2;
|
|
46129
46763
|
let result = text2;
|
|
46130
46764
|
for (const [key, value] of Object.entries(answers)) {
|
|
@@ -46137,7 +46771,7 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
46137
46771
|
return result;
|
|
46138
46772
|
};
|
|
46139
46773
|
const resolveObj = (obj) => {
|
|
46140
|
-
if (typeof obj === "string") return
|
|
46774
|
+
if (typeof obj === "string") return resolve13(obj);
|
|
46141
46775
|
if (Array.isArray(obj)) return obj.map(resolveObj);
|
|
46142
46776
|
if (obj && typeof obj === "object") {
|
|
46143
46777
|
const result = {};
|
|
@@ -46495,15 +47129,15 @@ ${errors.join("\n")}` : dryRun ? "\nNo errors!" : "\nNo errors \u2014 every item
|
|
|
46495
47129
|
}
|
|
46496
47130
|
);
|
|
46497
47131
|
}
|
|
46498
|
-
var import_zod71,
|
|
47132
|
+
var import_zod71, fs36, path34, TemplateSchema;
|
|
46499
47133
|
var init_template_deployer = __esm({
|
|
46500
47134
|
"src/tools/template-deployer.ts"() {
|
|
46501
47135
|
"use strict";
|
|
46502
47136
|
import_zod71 = require("zod");
|
|
46503
47137
|
init_workflow_builder_client();
|
|
46504
47138
|
init_tool_helpers();
|
|
46505
|
-
|
|
46506
|
-
|
|
47139
|
+
fs36 = __toESM(require("fs"));
|
|
47140
|
+
path34 = __toESM(require("path"));
|
|
46507
47141
|
init_workflow_builder_client();
|
|
46508
47142
|
TemplateSchema = import_zod71.z.object({
|
|
46509
47143
|
templateName: import_zod71.z.string(),
|
|
@@ -46824,10 +47458,10 @@ async function confirm(run) {
|
|
|
46824
47458
|
async function runProbes(client, loc) {
|
|
46825
47459
|
return Promise.all(
|
|
46826
47460
|
PROBES.map((probe) => confirm(async () => {
|
|
46827
|
-
const
|
|
47461
|
+
const path42 = probe.path.replace("{loc}", loc);
|
|
46828
47462
|
const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
|
|
46829
47463
|
try {
|
|
46830
|
-
const raw = await client.get(
|
|
47464
|
+
const raw = await client.get(path42, { params, noRetry: true });
|
|
46831
47465
|
const count2 = countRows(raw, probe.listKeys);
|
|
46832
47466
|
return {
|
|
46833
47467
|
key: probe.key,
|
|
@@ -47103,14 +47737,14 @@ var init_checkup = __esm({
|
|
|
47103
47737
|
|
|
47104
47738
|
// src/tools/user-guide.ts
|
|
47105
47739
|
function resolveGuidePath() {
|
|
47106
|
-
const bundled =
|
|
47107
|
-
if (
|
|
47108
|
-
const devRoot =
|
|
47740
|
+
const bundled = path35.resolve(__dirname, "..", "guide", "guide.html");
|
|
47741
|
+
if (fs37.existsSync(bundled)) return bundled;
|
|
47742
|
+
const devRoot = path35.resolve(__dirname, "..", "..");
|
|
47109
47743
|
try {
|
|
47110
|
-
const pkg2 = JSON.parse(
|
|
47744
|
+
const pkg2 = JSON.parse(fs37.readFileSync(path35.join(devRoot, "package.json"), "utf8"));
|
|
47111
47745
|
if (pkg2.name === "@elitedcs/ghl-mcp") {
|
|
47112
|
-
const dev =
|
|
47113
|
-
if (
|
|
47746
|
+
const dev = path35.join(devRoot, "guide", "guide.html");
|
|
47747
|
+
if (fs37.existsSync(dev)) return dev;
|
|
47114
47748
|
}
|
|
47115
47749
|
} catch {
|
|
47116
47750
|
}
|
|
@@ -47119,7 +47753,7 @@ function resolveGuidePath() {
|
|
|
47119
47753
|
function guideContents(file2) {
|
|
47120
47754
|
let html2 = "";
|
|
47121
47755
|
try {
|
|
47122
|
-
html2 =
|
|
47756
|
+
html2 = fs37.readFileSync(file2, "utf8");
|
|
47123
47757
|
} catch {
|
|
47124
47758
|
return "The guide is installed but could not be read.";
|
|
47125
47759
|
}
|
|
@@ -47143,7 +47777,7 @@ function registerUserGuideTools(server2) {
|
|
|
47143
47777
|
{ open: import_zod73.z.boolean().optional().describe("Open the guide in the browser. Takes over the person's screen \u2014 only when they asked for it.") },
|
|
47144
47778
|
async ({ open }) => {
|
|
47145
47779
|
const guidePath = resolveGuidePath();
|
|
47146
|
-
if (!
|
|
47780
|
+
if (!fs37.existsSync(guidePath)) {
|
|
47147
47781
|
return {
|
|
47148
47782
|
content: [{
|
|
47149
47783
|
type: "text",
|
|
@@ -47176,14 +47810,14 @@ ${guidePath}`;
|
|
|
47176
47810
|
}
|
|
47177
47811
|
);
|
|
47178
47812
|
}
|
|
47179
|
-
var import_child_process6, import_zod73,
|
|
47813
|
+
var import_child_process6, import_zod73, fs37, path35;
|
|
47180
47814
|
var init_user_guide = __esm({
|
|
47181
47815
|
"src/tools/user-guide.ts"() {
|
|
47182
47816
|
"use strict";
|
|
47183
47817
|
import_child_process6 = require("child_process");
|
|
47184
47818
|
import_zod73 = require("zod");
|
|
47185
|
-
|
|
47186
|
-
|
|
47819
|
+
fs37 = __toESM(require("fs"));
|
|
47820
|
+
path35 = __toESM(require("path"));
|
|
47187
47821
|
}
|
|
47188
47822
|
});
|
|
47189
47823
|
|
|
@@ -47244,7 +47878,8 @@ function registerAgencyProfileTools(server2) {
|
|
|
47244
47878
|
defaults: import_zod74.z.object({
|
|
47245
47879
|
timezone: import_zod74.z.string().optional(),
|
|
47246
47880
|
industry: import_zod74.z.string().optional()
|
|
47247
|
-
}).optional()
|
|
47881
|
+
}).optional(),
|
|
47882
|
+
clientFilesDir: import_zod74.z.string().optional().describe("A full folder path on this machine where client call files are also written, usually your Google Drive for desktop folder so Drive syncs them. A relative path is refused.")
|
|
47248
47883
|
},
|
|
47249
47884
|
async (args) => {
|
|
47250
47885
|
const patch = Object.fromEntries(
|
|
@@ -47822,7 +48457,7 @@ function blockContrast(t, fallbackBg) {
|
|
|
47822
48457
|
if (!fg || !bg) return null;
|
|
47823
48458
|
return Math.round(contrastHex(fg, bg) * 100) / 100;
|
|
47824
48459
|
}
|
|
47825
|
-
function
|
|
48460
|
+
function inside2(inner, outer, inset = 0) {
|
|
47826
48461
|
return inner.x >= outer.x + inset - 0.5 && inner.y >= outer.y + inset - 0.5 && inner.x + inner.w <= outer.x + outer.w - inset + 0.5 && inner.y + inner.h <= outer.y + outer.h - inset + 0.5;
|
|
47827
48462
|
}
|
|
47828
48463
|
function clusterBoxes(boxes, gap = 16) {
|
|
@@ -47917,7 +48552,7 @@ function checkSlides(probe, pixels, ctx, rendered) {
|
|
|
47917
48552
|
const bad = [];
|
|
47918
48553
|
for (const s of slides) {
|
|
47919
48554
|
const inset = Math.round(s.box.w * 0.05);
|
|
47920
|
-
const outside = [...s.textBlocks.filter((t) => t.words > 0).map((t) => ({ box: t.box, what: `"${t.text.slice(0, 20)}"` })), ...s.images.map((i) => ({ box: i.box, what: i.tag }))].filter((e) => !
|
|
48555
|
+
const outside = [...s.textBlocks.filter((t) => t.words > 0).map((t) => ({ box: t.box, what: `"${t.text.slice(0, 20)}"` })), ...s.images.map((i) => ({ box: i.box, what: i.tag }))].filter((e) => !inside2(e.box, s.box, inset));
|
|
47921
48556
|
if (outside.length) bad.push(`${slideName(s)}: ${outside.slice(0, 2).map((o) => o.what).join(", ")} inside the ${inset} px band`);
|
|
47922
48557
|
}
|
|
47923
48558
|
out.push(graded(rule("S5"), bad.length === 0, bad.length ? fails3(bad) : `all content \u2265${Math.round(slides[0].box.w * 0.05)} px from every edge`));
|
|
@@ -48139,7 +48774,7 @@ function checkSlides(probe, pixels, ctx, rendered) {
|
|
|
48139
48774
|
const l = logos[0];
|
|
48140
48775
|
if (l) {
|
|
48141
48776
|
if (l.box.h > 28) bad.push(`${slideName(s)} logo ${Math.round(l.box.h)} px tall`);
|
|
48142
|
-
if (!
|
|
48777
|
+
if (!inside2(l.box, s.box, inset)) bad.push(`${slideName(s)} logo inside the safe band`);
|
|
48143
48778
|
} else if (byHook && ctx.logoSig) bad.push(`${slideName(s)} has a data-role="logo" element but not the stored logo's bytes (conformance)`);
|
|
48144
48779
|
}
|
|
48145
48780
|
out.push(graded(rule("S21"), bad.length === 0, bad.length ? fails3(bad) : "stored logo on every slide, \u226424 px, inside the safe zone", void 0, ctx.logoSig ? void 0 : 'No stored logo to match; only the data-role="logo" hook was available.'));
|
|
@@ -48699,20 +49334,20 @@ function checkConversion(p, ctx, rendered) {
|
|
|
48699
49334
|
invented.push(`quote attributed to ${t.name} that they did not give`);
|
|
48700
49335
|
}
|
|
48701
49336
|
}
|
|
48702
|
-
const
|
|
49337
|
+
const digits = (x) => x.replace(/[,\s]/g, "");
|
|
48703
49338
|
const STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "from", "our", "over", "of", "in", "on", "a", "an", "we", "us", "have", "been"]);
|
|
48704
49339
|
const unitAfter = (text2, at) => norm4(text2.slice(at)).split(" ").slice(1).filter((w) => w && !STOP.has(w) && !/^[\d,.]+$/.test(w)).slice(0, 2);
|
|
48705
49340
|
const briefFigures = b.proof.aggregates.flatMap((agg) => {
|
|
48706
49341
|
const line3 = `${agg.value} ${agg.label}`;
|
|
48707
|
-
return [...line3.matchAll(/\d[\d,.]*/g)].map((m) => ({ n:
|
|
49342
|
+
return [...line3.matchAll(/\d[\d,.]*/g)].map((m) => ({ n: digits(m[0]), units: /* @__PURE__ */ new Set([...unitAfter(line3, m.index ?? 0), ...norm4(agg.label).split(" ").filter((w) => w && !STOP.has(w))]) }));
|
|
48708
49343
|
});
|
|
48709
49344
|
const seenClaims = /* @__PURE__ */ new Set();
|
|
48710
49345
|
const claimed = (text2) => {
|
|
48711
49346
|
for (const m of text2.matchAll(/\d[\d,.]*/g)) {
|
|
48712
|
-
const key = `${
|
|
49347
|
+
const key = `${digits(m[0])}@${norm4(text2).slice(0, 40)}`;
|
|
48713
49348
|
if (seenClaims.has(key)) continue;
|
|
48714
49349
|
seenClaims.add(key);
|
|
48715
|
-
const hit = briefFigures.find((f) => f.n ===
|
|
49350
|
+
const hit = briefFigures.find((f) => f.n === digits(m[0]));
|
|
48716
49351
|
if (!hit) {
|
|
48717
49352
|
invented.push(`figure "${m[0]}" in "${text2.slice(0, 30)}"`);
|
|
48718
49353
|
continue;
|
|
@@ -49393,9 +50028,9 @@ async function settle(page) {
|
|
|
49393
50028
|
}
|
|
49394
50029
|
async function readTarget(target) {
|
|
49395
50030
|
if (isUrl(target)) return { html: null, url: target };
|
|
49396
|
-
const abs =
|
|
49397
|
-
if (!
|
|
49398
|
-
return { html:
|
|
50031
|
+
const abs = path36.resolve(target);
|
|
50032
|
+
if (!fs38.existsSync(abs)) throw new Error(`No file at ${abs}.`);
|
|
50033
|
+
return { html: fs38.readFileSync(abs, "utf8"), url: (0, import_node_url.pathToFileURL)(abs).toString() };
|
|
49399
50034
|
}
|
|
49400
50035
|
function slidePixels(index, png, file2, sec, brand) {
|
|
49401
50036
|
let img;
|
|
@@ -49443,7 +50078,7 @@ async function verifyDesign(opts) {
|
|
|
49443
50078
|
const { html: html2, url } = await readTarget(opts.target);
|
|
49444
50079
|
const placement = opts.placement ?? "html";
|
|
49445
50080
|
const medium = opts.medium;
|
|
49446
|
-
|
|
50081
|
+
fs38.mkdirSync(opts.outDir, { recursive: true });
|
|
49447
50082
|
const files = [];
|
|
49448
50083
|
const screenshots = [];
|
|
49449
50084
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -49488,8 +50123,8 @@ async function verifyDesign(opts) {
|
|
|
49488
50123
|
try {
|
|
49489
50124
|
await loc.scrollIntoViewIfNeeded({ timeout: 5e3 });
|
|
49490
50125
|
const png = await loc.screenshot({ type: "png", timeout: 15e3 });
|
|
49491
|
-
|
|
49492
|
-
files.push(
|
|
50126
|
+
fs38.writeFileSync(path36.join(opts.outDir, file2), png);
|
|
50127
|
+
files.push(path36.join(opts.outDir, file2));
|
|
49493
50128
|
screenshots.push(file2);
|
|
49494
50129
|
pixels.push(slidePixels(sec.index, png, file2, sec, opts.brand));
|
|
49495
50130
|
} catch {
|
|
@@ -49521,8 +50156,8 @@ async function verifyDesign(opts) {
|
|
|
49521
50156
|
const shot = "page-1440.png";
|
|
49522
50157
|
try {
|
|
49523
50158
|
const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
|
|
49524
|
-
|
|
49525
|
-
files.push(
|
|
50159
|
+
fs38.writeFileSync(path36.join(opts.outDir, shot), png);
|
|
50160
|
+
files.push(path36.join(opts.outDir, shot));
|
|
49526
50161
|
screenshots.push(shot);
|
|
49527
50162
|
} catch {
|
|
49528
50163
|
}
|
|
@@ -49530,8 +50165,8 @@ async function verifyDesign(opts) {
|
|
|
49530
50165
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
49531
50166
|
await page.evaluate(SETTLE_SCROLL_JS).catch(() => void 0);
|
|
49532
50167
|
const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
|
|
49533
|
-
|
|
49534
|
-
files.push(
|
|
50168
|
+
fs38.writeFileSync(path36.join(opts.outDir, "page-390.png"), png);
|
|
50169
|
+
files.push(path36.join(opts.outDir, "page-390.png"));
|
|
49535
50170
|
screenshots.push("page-390.png");
|
|
49536
50171
|
} catch {
|
|
49537
50172
|
}
|
|
@@ -49604,21 +50239,21 @@ async function verifyDesign(opts) {
|
|
|
49604
50239
|
screenshots
|
|
49605
50240
|
};
|
|
49606
50241
|
const checked = VerificationSchema.parse(report2);
|
|
49607
|
-
const jsonPath =
|
|
49608
|
-
|
|
50242
|
+
const jsonPath = path36.join(opts.outDir, "verification.json");
|
|
50243
|
+
fs38.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
|
|
49609
50244
|
files.push(jsonPath);
|
|
49610
50245
|
const reportHtml = renderVerificationHtml(checked, opts.branding);
|
|
49611
|
-
const htmlPath =
|
|
49612
|
-
|
|
50246
|
+
const htmlPath = path36.join(opts.outDir, "verification.html");
|
|
50247
|
+
fs38.writeFileSync(htmlPath, reportHtml);
|
|
49613
50248
|
files.push(htmlPath);
|
|
49614
50249
|
return { report: checked, files, reportHtml };
|
|
49615
50250
|
}
|
|
49616
|
-
var
|
|
50251
|
+
var fs38, path36, import_node_url, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS;
|
|
49617
50252
|
var init_verify = __esm({
|
|
49618
50253
|
"src/command-os/design/verify.ts"() {
|
|
49619
50254
|
"use strict";
|
|
49620
|
-
|
|
49621
|
-
|
|
50255
|
+
fs38 = __toESM(require("node:fs"));
|
|
50256
|
+
path36 = __toESM(require("node:path"));
|
|
49622
50257
|
import_node_url = require("node:url");
|
|
49623
50258
|
init_lint();
|
|
49624
50259
|
init_probe();
|
|
@@ -50037,7 +50672,7 @@ function registerDesignStudioTools(server2) {
|
|
|
50037
50672
|
const { dna, logoSvg, logoPng } = deriveBrandDna(raw, { name, slug: slug3, rights: { declaration: a.rights, at: (/* @__PURE__ */ new Date()).toISOString() }, browser, firecrawl: fc });
|
|
50038
50673
|
if (a.rights === "none" && dna.logo) notes.push("Rights declared as none: the logo is stored for reference only; do not place it on a client-facing page.");
|
|
50039
50674
|
const written = writeBrand(dna, { logoSvg, logoPng, screenshotPng: browser?.screenshotPng });
|
|
50040
|
-
return { ok: true, brand: summarise2(dna), passes: { static: true, browser: Boolean(browser), firecrawl: Boolean(fc) }, spaShell: raw.spaShell, dir: written.dir, files: written.files.map((f) =>
|
|
50675
|
+
return { ok: true, brand: summarise2(dna), passes: { static: true, browser: Boolean(browser), firecrawl: Boolean(fc) }, spaShell: raw.spaShell, dir: written.dir, files: written.files.map((f) => path37.basename(f)), notes };
|
|
50041
50676
|
}
|
|
50042
50677
|
);
|
|
50043
50678
|
safeTool(
|
|
@@ -50141,14 +50776,14 @@ function registerDesignStudioTools(server2) {
|
|
|
50141
50776
|
d.provenance.evidence["shape.radius"] = `set by ${stamp}`;
|
|
50142
50777
|
}
|
|
50143
50778
|
if (a.logoSvg) {
|
|
50144
|
-
let
|
|
50779
|
+
let clean2;
|
|
50145
50780
|
try {
|
|
50146
|
-
|
|
50781
|
+
clean2 = sanitizeSvg(a.logoSvg);
|
|
50147
50782
|
} catch (e) {
|
|
50148
50783
|
throw new Error(`${e.message} Nothing was changed.`);
|
|
50149
50784
|
}
|
|
50150
|
-
a.logoSvg =
|
|
50151
|
-
d.logo = { inlineSvg:
|
|
50785
|
+
a.logoSvg = clean2;
|
|
50786
|
+
d.logo = { inlineSvg: clean2, whiteFillFixed: false, source: `set by ${stamp}`, rights: "declared" };
|
|
50152
50787
|
d.provenance.evidence["logo"] = `set by ${stamp}`;
|
|
50153
50788
|
}
|
|
50154
50789
|
if (a.tone) d.voice.tone = a.tone;
|
|
@@ -50204,9 +50839,9 @@ function registerDesignStudioTools(server2) {
|
|
|
50204
50839
|
"Check a written HTML file honours the output contract before grading: section and role markers present and consistent, no outside scripts, one h1 on a page, every image described. Deterministic; no browser. A file that fails this is not graded by verify_design.",
|
|
50205
50840
|
{ file: import_zod76.z.string().describe("Path to the .html file."), medium: import_zod76.z.enum(MEDIA) },
|
|
50206
50841
|
async ({ file: file2, medium }) => {
|
|
50207
|
-
const abs =
|
|
50208
|
-
if (!
|
|
50209
|
-
const r = lintDesignContract(
|
|
50842
|
+
const abs = path37.resolve(file2);
|
|
50843
|
+
if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
|
|
50844
|
+
const r = lintDesignContract(fs39.readFileSync(abs, "utf8"), medium);
|
|
50210
50845
|
return { ok: r.ok, problems: r.problems, warnings: r.warnings, facts: r.facts };
|
|
50211
50846
|
}
|
|
50212
50847
|
);
|
|
@@ -50227,7 +50862,7 @@ function registerDesignStudioTools(server2) {
|
|
|
50227
50862
|
async (a) => {
|
|
50228
50863
|
const brand = a.brandSlug ? houseLooks().find((d) => d.slug === a.brandSlug) ?? readBrand(slugify(a.brandSlug)) : void 0;
|
|
50229
50864
|
const isUrl2 = /^https?:\/\//i.test(a.target);
|
|
50230
|
-
const outDir = a.outDir ?? (isUrl2 ?
|
|
50865
|
+
const outDir = a.outDir ?? (isUrl2 ? path37.resolve(`verify-${new URL(a.target).hostname.replace(/[^a-z0-9.-]/gi, "_")}`) : path37.join(path37.dirname(path37.resolve(a.target)), `verify-${path37.basename(a.target, ".html")}`));
|
|
50231
50866
|
const r = await verifyDesign({ target: a.target, medium: a.medium, brand, outDir, placement: a.placement, mode: a.mode, ratio: a.ratio, logo: a.logo, branding: readAgencyBranding() });
|
|
50232
50867
|
const v = r.report;
|
|
50233
50868
|
const failed = v.rules.filter((x) => x.state === "MEASURED-FAIL").map((x) => `${x.ruleId} ${x.name}: ${x.measured ?? ""}`);
|
|
@@ -50242,7 +50877,7 @@ function registerDesignStudioTools(server2) {
|
|
|
50242
50877
|
judgments: v.rules.filter((x) => x.state === "ASSERTED").map((x) => `${x.ruleId} ${x.name}: ${x.detail ?? ""}`),
|
|
50243
50878
|
notApplicable: v.rules.filter((x) => x.state === "N-A").map((x) => `${x.ruleId}: ${x.detail ?? ""}`),
|
|
50244
50879
|
brandMatch: v.brandMatch,
|
|
50245
|
-
report:
|
|
50880
|
+
report: path37.join(outDir, "verification.html"),
|
|
50246
50881
|
files: r.files,
|
|
50247
50882
|
...a.placement === "ghl" && { builderOwned: builderOwnedRules().map((b) => `${b.rule.id} ${b.rule.name}: ${b.why}`) }
|
|
50248
50883
|
};
|
|
@@ -50270,9 +50905,9 @@ function registerDesignStudioTools(server2) {
|
|
|
50270
50905
|
if (!a.pageDataDownloadUrl && !a.file) throw new Error("Pass pageDataDownloadUrl (from get_page_full) or file.");
|
|
50271
50906
|
let page;
|
|
50272
50907
|
if (a.file) {
|
|
50273
|
-
const abs =
|
|
50274
|
-
if (!
|
|
50275
|
-
page = JSON.parse(
|
|
50908
|
+
const abs = path37.resolve(a.file);
|
|
50909
|
+
if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
|
|
50910
|
+
page = JSON.parse(fs39.readFileSync(abs, "utf8"));
|
|
50276
50911
|
} else {
|
|
50277
50912
|
const response = await fetch(a.pageDataDownloadUrl, { headers: { Accept: "application/json,*/*" } });
|
|
50278
50913
|
if (!response.ok) throw new Error(`Could not download the page data: HTTP ${response.status}. Re-read get_page_full \u2014 the download URL expires.`);
|
|
@@ -50313,9 +50948,9 @@ function registerDesignStudioTools(server2) {
|
|
|
50313
50948
|
if (!a.file && !a.text) throw new Error("Pass file or text.");
|
|
50314
50949
|
let html2 = a.text ?? "";
|
|
50315
50950
|
if (a.file) {
|
|
50316
|
-
const abs =
|
|
50317
|
-
if (!
|
|
50318
|
-
html2 =
|
|
50951
|
+
const abs = path37.resolve(a.file);
|
|
50952
|
+
if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
|
|
50953
|
+
html2 = fs39.readFileSync(abs, "utf8");
|
|
50319
50954
|
}
|
|
50320
50955
|
const r = await gradeCopy(html2, claudeGenerate(a.model), { icp: a.icp });
|
|
50321
50956
|
if (!r.grade) {
|
|
@@ -50350,13 +50985,13 @@ function registerDesignStudioTools(server2) {
|
|
|
50350
50985
|
}
|
|
50351
50986
|
);
|
|
50352
50987
|
}
|
|
50353
|
-
var import_zod76,
|
|
50988
|
+
var import_zod76, fs39, path37, summarise2;
|
|
50354
50989
|
var init_design_studio = __esm({
|
|
50355
50990
|
"src/tools/design-studio.ts"() {
|
|
50356
50991
|
"use strict";
|
|
50357
50992
|
import_zod76 = require("zod");
|
|
50358
|
-
|
|
50359
|
-
|
|
50993
|
+
fs39 = __toESM(require("node:fs"));
|
|
50994
|
+
path37 = __toESM(require("node:path"));
|
|
50360
50995
|
init_tool_helpers();
|
|
50361
50996
|
init_extract();
|
|
50362
50997
|
init_extract_browser();
|
|
@@ -51080,11 +51715,11 @@ var init_audit = __esm({
|
|
|
51080
51715
|
// src/command-os/delivery-audit/store.ts
|
|
51081
51716
|
function deliveryAuditPath() {
|
|
51082
51717
|
const override = process.env[DELIVERY_AUDIT_ENV];
|
|
51083
|
-
return override && override.trim() ? override :
|
|
51718
|
+
return override && override.trim() ? override : path38.join(appDataDir(), "delivery-audit.json");
|
|
51084
51719
|
}
|
|
51085
51720
|
function readDeliveryAudit() {
|
|
51086
51721
|
try {
|
|
51087
|
-
const parsed2 = JSON.parse(
|
|
51722
|
+
const parsed2 = JSON.parse(fs40.readFileSync(deliveryAuditPath(), "utf-8"));
|
|
51088
51723
|
if (!parsed2 || typeof parsed2 !== "object" || !parsed2.answers || typeof parsed2.answers !== "object") return { ...EMPTY3 };
|
|
51089
51724
|
return { v: 1, answers: parsed2.answers, updated_at: parsed2.updated_at };
|
|
51090
51725
|
} catch {
|
|
@@ -51095,7 +51730,7 @@ function readForWrite2() {
|
|
|
51095
51730
|
const target = deliveryAuditPath();
|
|
51096
51731
|
let raw;
|
|
51097
51732
|
try {
|
|
51098
|
-
raw =
|
|
51733
|
+
raw = fs40.readFileSync(target, "utf-8");
|
|
51099
51734
|
} catch (e) {
|
|
51100
51735
|
if (e?.code === "ENOENT") return { ...EMPTY3 };
|
|
51101
51736
|
throw new Error("Your saved audit could not be read, so nothing was changed. Nothing was lost.");
|
|
@@ -51107,7 +51742,7 @@ function readForWrite2() {
|
|
|
51107
51742
|
} catch {
|
|
51108
51743
|
const kept = `${target}.unreadable-${Date.now()}`;
|
|
51109
51744
|
try {
|
|
51110
|
-
|
|
51745
|
+
fs40.copyFileSync(target, kept);
|
|
51111
51746
|
} catch {
|
|
51112
51747
|
}
|
|
51113
51748
|
throw new Error(`Your saved audit is damaged, so nothing was written over it. A copy is at ${kept}. Start again with reset:true when you are ready.`);
|
|
@@ -51119,13 +51754,13 @@ function writeFile2(f) {
|
|
|
51119
51754
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
51120
51755
|
let renamed = false;
|
|
51121
51756
|
try {
|
|
51122
|
-
|
|
51123
|
-
|
|
51757
|
+
fs40.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
51758
|
+
fs40.renameSync(tmp, target);
|
|
51124
51759
|
renamed = true;
|
|
51125
51760
|
} finally {
|
|
51126
51761
|
if (!renamed) {
|
|
51127
51762
|
try {
|
|
51128
|
-
|
|
51763
|
+
fs40.unlinkSync(tmp);
|
|
51129
51764
|
} catch {
|
|
51130
51765
|
}
|
|
51131
51766
|
}
|
|
@@ -51146,12 +51781,12 @@ function resetDeliveryAudit(now = () => /* @__PURE__ */ new Date()) {
|
|
|
51146
51781
|
writeFile2(next);
|
|
51147
51782
|
return next;
|
|
51148
51783
|
}
|
|
51149
|
-
var
|
|
51784
|
+
var fs40, path38, EMPTY3, DELIVERY_AUDIT_ENV;
|
|
51150
51785
|
var init_store4 = __esm({
|
|
51151
51786
|
"src/command-os/delivery-audit/store.ts"() {
|
|
51152
51787
|
"use strict";
|
|
51153
|
-
|
|
51154
|
-
|
|
51788
|
+
fs40 = __toESM(require("node:fs"));
|
|
51789
|
+
path38 = __toESM(require("node:path"));
|
|
51155
51790
|
init_credentials_store();
|
|
51156
51791
|
EMPTY3 = { v: 1, answers: {} };
|
|
51157
51792
|
DELIVERY_AUDIT_ENV = "GHL_MCP_DELIVERY_AUDIT";
|
|
@@ -51472,7 +52107,7 @@ function position(a) {
|
|
|
51472
52107
|
const cyclic = a.steps.length > 0 && check.order.length === 0;
|
|
51473
52108
|
const order = check.order.length ? check.order : a.steps.map((s) => s.id);
|
|
51474
52109
|
const lastAttempt = /* @__PURE__ */ new Map();
|
|
51475
|
-
for (const x of [...a.applied].sort((p,
|
|
52110
|
+
for (const x of [...a.applied].sort((p, q3) => p.at < q3.at ? -1 : p.at > q3.at ? 1 : 0)) {
|
|
51476
52111
|
lastAttempt.set(x.stepId, x);
|
|
51477
52112
|
}
|
|
51478
52113
|
const doneIds = new Set([...lastAttempt.values()].filter((x) => x.state === "applied").map((x) => x.stepId));
|
|
@@ -51515,7 +52150,7 @@ function position(a) {
|
|
|
51515
52150
|
line3 = `STALLED before it started: this plan's steps wait on each other (${names}), so there is no order that works. It cannot be run as it stands and needs planning again.`;
|
|
51516
52151
|
} else if (failed.length) {
|
|
51517
52152
|
phase = "stalled";
|
|
51518
|
-
const f = failed.reduce((p,
|
|
52153
|
+
const f = failed.reduce((p, q3) => q3.at > p.at ? q3 : p);
|
|
51519
52154
|
stalledOn = { step: f.stepId, because: f.because ?? "no reason was recorded" };
|
|
51520
52155
|
const rest = total - done - 1;
|
|
51521
52156
|
line3 = `STALLED on ${f.stepId} (${byId2.get(f.stepId)?.why ?? "step"}): ${f.because ?? "no reason was recorded"}. ${done} of ${total} steps are done and the remaining ${rest === 1 ? "step is" : `${rest} steps are`} on hold \u2014 nothing else runs until this one is sorted, even the steps that do not depend on it, because the plan's order is part of what was approved. Fix what went wrong and record ${f.stepId} again with retry, or abandon the build.`;
|
|
@@ -51607,7 +52242,7 @@ var init_automation = __esm({
|
|
|
51607
52242
|
// src/command-os/build/store.ts
|
|
51608
52243
|
function automationsPath() {
|
|
51609
52244
|
const override = process.env[AUTOMATIONS_ENV];
|
|
51610
|
-
return override && override.trim() ? override :
|
|
52245
|
+
return override && override.trim() ? override : path39.join(appDataDir(), "automations.json");
|
|
51611
52246
|
}
|
|
51612
52247
|
function parse4(raw) {
|
|
51613
52248
|
const p = JSON.parse(raw);
|
|
@@ -51616,7 +52251,7 @@ function parse4(raw) {
|
|
|
51616
52251
|
}
|
|
51617
52252
|
function readAutomations() {
|
|
51618
52253
|
try {
|
|
51619
|
-
return parse4(
|
|
52254
|
+
return parse4(fs41.readFileSync(automationsPath(), "utf-8")).builds;
|
|
51620
52255
|
} catch {
|
|
51621
52256
|
return { ...EMPTY4.builds };
|
|
51622
52257
|
}
|
|
@@ -51625,7 +52260,7 @@ function readForWrite3() {
|
|
|
51625
52260
|
const target = automationsPath();
|
|
51626
52261
|
let raw;
|
|
51627
52262
|
try {
|
|
51628
|
-
raw =
|
|
52263
|
+
raw = fs41.readFileSync(target, "utf-8");
|
|
51629
52264
|
} catch (e) {
|
|
51630
52265
|
if (e?.code === "ENOENT") return { v: 1, builds: {} };
|
|
51631
52266
|
throw new Error("Your builds file could not be read, so nothing was changed. Nothing was lost.");
|
|
@@ -51635,7 +52270,7 @@ function readForWrite3() {
|
|
|
51635
52270
|
} catch {
|
|
51636
52271
|
const kept = `${target}.unreadable-${Date.now()}`;
|
|
51637
52272
|
try {
|
|
51638
|
-
|
|
52273
|
+
fs41.copyFileSync(target, kept);
|
|
51639
52274
|
} catch {
|
|
51640
52275
|
}
|
|
51641
52276
|
throw new Error(
|
|
@@ -51649,13 +52284,13 @@ function writeFile3(f) {
|
|
|
51649
52284
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
51650
52285
|
let renamed = false;
|
|
51651
52286
|
try {
|
|
51652
|
-
|
|
51653
|
-
|
|
52287
|
+
fs41.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
52288
|
+
fs41.renameSync(tmp, target);
|
|
51654
52289
|
renamed = true;
|
|
51655
52290
|
} finally {
|
|
51656
52291
|
if (!renamed) {
|
|
51657
52292
|
try {
|
|
51658
|
-
|
|
52293
|
+
fs41.unlinkSync(tmp);
|
|
51659
52294
|
} catch {
|
|
51660
52295
|
}
|
|
51661
52296
|
}
|
|
@@ -51681,12 +52316,12 @@ function newAutomationId(now = () => /* @__PURE__ */ new Date()) {
|
|
|
51681
52316
|
}
|
|
51682
52317
|
return id;
|
|
51683
52318
|
}
|
|
51684
|
-
var
|
|
52319
|
+
var fs41, path39, AUTOMATIONS_ENV, EMPTY4;
|
|
51685
52320
|
var init_store5 = __esm({
|
|
51686
52321
|
"src/command-os/build/store.ts"() {
|
|
51687
52322
|
"use strict";
|
|
51688
|
-
|
|
51689
|
-
|
|
52323
|
+
fs41 = __toESM(require("node:fs"));
|
|
52324
|
+
path39 = __toESM(require("node:path"));
|
|
51690
52325
|
init_credentials_store();
|
|
51691
52326
|
AUTOMATIONS_ENV = "GHL_MCP_AUTOMATIONS";
|
|
51692
52327
|
EMPTY4 = { v: 1, builds: {} };
|
|
@@ -52220,15 +52855,15 @@ var init_agency_build = __esm({
|
|
|
52220
52855
|
|
|
52221
52856
|
// src/tools/client-engagements.ts
|
|
52222
52857
|
function resolveClient(who, roster) {
|
|
52223
|
-
const
|
|
52224
|
-
if (!
|
|
52225
|
-
const byId2 = roster.find((r) => r.locationId ===
|
|
52858
|
+
const q3 = who.trim();
|
|
52859
|
+
if (!q3) return { error: "Name who you mean \u2014 either the way you refer to them, or their account id." };
|
|
52860
|
+
const byId2 = roster.find((r) => r.locationId === q3);
|
|
52226
52861
|
if (byId2) return { locationId: byId2.locationId, name: byId2.name, onTheBoard: true };
|
|
52227
|
-
const hits = roster.filter((r) => r.name?.trim().toLowerCase() ===
|
|
52228
|
-
const loose2 = hits.length ? hits : roster.filter((r) => r.name?.toLowerCase().includes(
|
|
52862
|
+
const hits = roster.filter((r) => r.name?.trim().toLowerCase() === q3.toLowerCase());
|
|
52863
|
+
const loose2 = hits.length ? hits : roster.filter((r) => r.name?.toLowerCase().includes(q3.toLowerCase()));
|
|
52229
52864
|
if (loose2.length === 1) return { locationId: loose2[0].locationId, name: loose2[0].name, onTheBoard: true };
|
|
52230
|
-
if (loose2.length > 1) return { error: `More than one match for "${
|
|
52231
|
-
return { error: `Nothing called "${
|
|
52865
|
+
if (loose2.length > 1) return { error: `More than one match for "${q3}": ${loose2.map((r) => r.name).join(", ")}. Use the full name.` };
|
|
52866
|
+
return { error: `Nothing called "${q3}" is on your board or in your records.` };
|
|
52232
52867
|
}
|
|
52233
52868
|
function line2(locationId2, e, roster, profile) {
|
|
52234
52869
|
const boardName = roster.find((r) => r.locationId === locationId2)?.name;
|
|
@@ -52483,9 +53118,9 @@ function looksLikeRawId(word) {
|
|
|
52483
53118
|
if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(word)) return true;
|
|
52484
53119
|
if (/^[0-9a-f]{24}$/.test(word)) return true;
|
|
52485
53120
|
if (!/^[A-Za-z0-9]{17,}$/.test(word)) return false;
|
|
52486
|
-
const
|
|
53121
|
+
const digits = (word.match(/\d/g) ?? []).length;
|
|
52487
53122
|
const letters = (word.match(/[A-Za-z]/g) ?? []).length;
|
|
52488
|
-
if (
|
|
53123
|
+
if (digits < 2 || letters < 2) return false;
|
|
52489
53124
|
const longestLetterRun = Math.max(0, ...(word.match(/[a-z]+|[A-Z]+/g) ?? []).map((r) => r.length));
|
|
52490
53125
|
return longestLetterRun <= 3;
|
|
52491
53126
|
}
|
|
@@ -53532,7 +54167,7 @@ function planRevert(record, opts) {
|
|
|
53532
54167
|
function errText(e) {
|
|
53533
54168
|
return e instanceof Error ? e.message : String(e);
|
|
53534
54169
|
}
|
|
53535
|
-
async function executeRevert(plan, deps, wait = (ms) => new Promise((
|
|
54170
|
+
async function executeRevert(plan, deps, wait = (ms) => new Promise((resolve13) => setTimeout(resolve13, ms))) {
|
|
53536
54171
|
const removed = [];
|
|
53537
54172
|
const refused = [];
|
|
53538
54173
|
const attempted = [];
|
|
@@ -53651,8 +54286,8 @@ function renderDryRun(plan, opts = {}) {
|
|
|
53651
54286
|
const children2 = withParent.filter((w) => w.object.parentRef && g.items.some((i) => i.object.ref === w.object.parentRef));
|
|
53652
54287
|
if (children2.length) {
|
|
53653
54288
|
const kind = children2[0].object.type;
|
|
53654
|
-
const
|
|
53655
|
-
L.push(` (plus the ${children2.length} ${PLURAL[kind].toLowerCase()} ${
|
|
54289
|
+
const inside3 = g.items.length === 1 ? "inside it" : "inside them";
|
|
54290
|
+
L.push(` (plus the ${children2.length} ${PLURAL[kind].toLowerCase()} ${inside3}, which GoHighLevel removes at the same time)`);
|
|
53656
54291
|
}
|
|
53657
54292
|
}
|
|
53658
54293
|
const kept = [...bound, ...cannot, ...gone];
|
|
@@ -54845,11 +55480,11 @@ function extractFormFields(formFull) {
|
|
|
54845
55480
|
const fields = Array.isArray(inner.fields) ? inner.fields : [];
|
|
54846
55481
|
return fields.filter((f) => !!f && typeof f === "object");
|
|
54847
55482
|
}
|
|
54848
|
-
function findRecordForQuestion(
|
|
54849
|
-
const wantKey = expectedFieldKey(
|
|
55483
|
+
function findRecordForQuestion(q3, records) {
|
|
55484
|
+
const wantKey = expectedFieldKey(q3).toLowerCase();
|
|
54850
55485
|
const byKey = records.find((r) => r.fieldKey.toLowerCase() === wantKey);
|
|
54851
55486
|
if (byKey) return byKey;
|
|
54852
|
-
const wantName = intakeFieldName(
|
|
55487
|
+
const wantName = intakeFieldName(q3.label).toLowerCase();
|
|
54853
55488
|
return records.find((r) => r.name.toLowerCase() === wantName);
|
|
54854
55489
|
}
|
|
54855
55490
|
function isFormNotYetPropagated(error) {
|
|
@@ -55074,13 +55709,13 @@ ${text3}`);
|
|
|
55074
55709
|
};
|
|
55075
55710
|
}
|
|
55076
55711
|
function makePipelineApi(builderClient) {
|
|
55077
|
-
return async (method,
|
|
55078
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
55712
|
+
return async (method, path42, body) => {
|
|
55713
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path42}`;
|
|
55079
55714
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
55080
55715
|
const response = await builderRequest(builderClient, method, url, sendsBody ? { body } : {});
|
|
55081
55716
|
if (!response.ok) {
|
|
55082
55717
|
const text3 = await response.text();
|
|
55083
|
-
throw new Error(`Pipeline API ${response.status}: ${method} ${
|
|
55718
|
+
throw new Error(`Pipeline API ${response.status}: ${method} ${path42}
|
|
55084
55719
|
${text3}`);
|
|
55085
55720
|
}
|
|
55086
55721
|
const text2 = await response.text();
|
|
@@ -55093,8 +55728,8 @@ ${text3}`);
|
|
|
55093
55728
|
};
|
|
55094
55729
|
}
|
|
55095
55730
|
function makeFunnelApi(builderClient) {
|
|
55096
|
-
return async (method,
|
|
55097
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
55731
|
+
return async (method, path42, body) => {
|
|
55732
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path42}`;
|
|
55098
55733
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
55099
55734
|
const response = await builderRequest(builderClient, method, url, {
|
|
55100
55735
|
...sendsBody ? { body } : {},
|
|
@@ -55107,7 +55742,7 @@ function makeFunnelApi(builderClient) {
|
|
|
55107
55742
|
});
|
|
55108
55743
|
if (!response.ok) {
|
|
55109
55744
|
const text3 = await response.text();
|
|
55110
|
-
throw new Error(`Funnel API ${response.status}: ${method} ${
|
|
55745
|
+
throw new Error(`Funnel API ${response.status}: ${method} ${path42}
|
|
55111
55746
|
${text3.slice(0, 300)}`);
|
|
55112
55747
|
}
|
|
55113
55748
|
const text2 = await response.text();
|
|
@@ -55474,17 +56109,17 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
55474
56109
|
industry: industryPack(industry)?.slug,
|
|
55475
56110
|
intake: describeComposition({ industry, agency }),
|
|
55476
56111
|
count: questions.length,
|
|
55477
|
-
questions: questions.map((
|
|
55478
|
-
key:
|
|
55479
|
-
label:
|
|
55480
|
-
section:
|
|
55481
|
-
required:
|
|
55482
|
-
fieldKind:
|
|
55483
|
-
dataType:
|
|
55484
|
-
standardTag:
|
|
55485
|
-
expectedFieldKey:
|
|
55486
|
-
options:
|
|
55487
|
-
briefPath:
|
|
56112
|
+
questions: questions.map((q3) => ({
|
|
56113
|
+
key: q3.key,
|
|
56114
|
+
label: q3.label,
|
|
56115
|
+
section: q3.sectionLabel,
|
|
56116
|
+
required: q3.required,
|
|
56117
|
+
fieldKind: q3.field.kind,
|
|
56118
|
+
dataType: q3.field.kind === "custom" ? q3.field.dataType : void 0,
|
|
56119
|
+
standardTag: q3.field.kind === "standard" ? q3.field.tag : void 0,
|
|
56120
|
+
expectedFieldKey: q3.field.kind === "custom" ? expectedFieldKey(q3) : void 0,
|
|
56121
|
+
options: q3.options,
|
|
56122
|
+
briefPath: q3.briefPath
|
|
55488
56123
|
}))
|
|
55489
56124
|
};
|
|
55490
56125
|
}
|
|
@@ -56088,26 +56723,26 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
56088
56723
|
if (!builderClient) return;
|
|
56089
56724
|
const bc = builderClient;
|
|
56090
56725
|
async function resolveCustomFields(locationId2, dryRun, questions) {
|
|
56091
|
-
const cqs = questions.filter((
|
|
56726
|
+
const cqs = questions.filter((q3) => q3.field.kind === "custom");
|
|
56092
56727
|
const existing = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
|
|
56093
56728
|
const created = [];
|
|
56094
56729
|
const reused = [];
|
|
56095
|
-
for (const
|
|
56096
|
-
const found = findRecordForQuestion(
|
|
56730
|
+
for (const q3 of cqs) {
|
|
56731
|
+
const found = findRecordForQuestion(q3, existing);
|
|
56097
56732
|
if (found) {
|
|
56098
|
-
reused.push(
|
|
56733
|
+
reused.push(q3.label);
|
|
56099
56734
|
} else {
|
|
56100
|
-
created.push(
|
|
56735
|
+
created.push(q3.label);
|
|
56101
56736
|
if (!dryRun) {
|
|
56102
56737
|
const body = {
|
|
56103
|
-
name: intakeFieldName(
|
|
56104
|
-
dataType:
|
|
56738
|
+
name: intakeFieldName(q3.label),
|
|
56739
|
+
dataType: q3.field.kind === "custom" ? q3.field.dataType : "TEXT",
|
|
56105
56740
|
model: "contact"
|
|
56106
56741
|
};
|
|
56107
|
-
if (
|
|
56108
|
-
const dt =
|
|
56742
|
+
if (q3.options && q3.field.kind === "custom") {
|
|
56743
|
+
const dt = q3.field.dataType;
|
|
56109
56744
|
if (dt === "SINGLE_OPTIONS" || dt === "MULTIPLE_OPTIONS" || dt === "CHECKBOX") {
|
|
56110
|
-
body.options = [...
|
|
56745
|
+
body.options = [...q3.options];
|
|
56111
56746
|
}
|
|
56112
56747
|
}
|
|
56113
56748
|
await client.post(`/locations/${locationId2}/customFields`, { body });
|
|
@@ -56121,13 +56756,13 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
56121
56756
|
const after = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
|
|
56122
56757
|
resolved2.clear();
|
|
56123
56758
|
missing = void 0;
|
|
56124
|
-
for (const
|
|
56125
|
-
const rec = findRecordForQuestion(
|
|
56759
|
+
for (const q3 of cqs) {
|
|
56760
|
+
const rec = findRecordForQuestion(q3, after);
|
|
56126
56761
|
if (!rec) {
|
|
56127
|
-
missing =
|
|
56762
|
+
missing = q3;
|
|
56128
56763
|
break;
|
|
56129
56764
|
}
|
|
56130
|
-
resolved2.set(
|
|
56765
|
+
resolved2.set(q3.key, rec);
|
|
56131
56766
|
}
|
|
56132
56767
|
if (!missing) break;
|
|
56133
56768
|
if (attempt < 6) await sleep6(700 * attempt);
|
|
@@ -56177,7 +56812,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
56177
56812
|
industry: pack?.slug,
|
|
56178
56813
|
intake,
|
|
56179
56814
|
questionCount: questions.length,
|
|
56180
|
-
standardFields: questions.filter((
|
|
56815
|
+
standardFields: questions.filter((q3) => q3.field.kind === "standard").map((q3) => q3.label),
|
|
56181
56816
|
customFieldsToCreate: created,
|
|
56182
56817
|
customFieldsToReuse: reused,
|
|
56183
56818
|
note: "No changes written. Re-run without dryRun to install."
|
|
@@ -56422,7 +57057,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env2 = process
|
|
|
56422
57057
|
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.` }));
|
|
56423
57058
|
}
|
|
56424
57059
|
const reader = token2 ? new GHLClient({ apiKey: token2.apiKey, locationId: loc }) : client;
|
|
56425
|
-
return refuseUnseenFindings2((
|
|
57060
|
+
return refuseUnseenFindings2((path42, params) => reader.get(path42, params ? { params } : void 0), loc, findings);
|
|
56426
57061
|
}, () => client.defaultLocationId);
|
|
56427
57062
|
registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
|
|
56428
57063
|
registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
|
|
@@ -56842,8 +57477,8 @@ __export(index_exports, {
|
|
|
56842
57477
|
});
|
|
56843
57478
|
module.exports = __toCommonJS(index_exports);
|
|
56844
57479
|
var dotenv2 = __toESM(require("dotenv"));
|
|
56845
|
-
var
|
|
56846
|
-
var
|
|
57480
|
+
var path41 = __toESM(require("path"));
|
|
57481
|
+
var fs43 = __toESM(require("fs"));
|
|
56847
57482
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
56848
57483
|
|
|
56849
57484
|
// src/version-compare.ts
|
|
@@ -56915,8 +57550,8 @@ var import_zod87 = require("zod");
|
|
|
56915
57550
|
|
|
56916
57551
|
// src/skill-installer.ts
|
|
56917
57552
|
var import_node_crypto13 = require("node:crypto");
|
|
56918
|
-
var
|
|
56919
|
-
var
|
|
57553
|
+
var fs42 = __toESM(require("node:fs"));
|
|
57554
|
+
var path40 = __toESM(require("node:path"));
|
|
56920
57555
|
var os7 = __toESM(require("node:os"));
|
|
56921
57556
|
function sha2562(buf) {
|
|
56922
57557
|
return (0, import_node_crypto13.createHash)("sha256").update(buf).digest("hex");
|
|
@@ -56931,9 +57566,9 @@ function isOlderRelease(candidate, installed) {
|
|
|
56931
57566
|
return false;
|
|
56932
57567
|
}
|
|
56933
57568
|
function bundledSkillsDir(baseDir) {
|
|
56934
|
-
const candidate =
|
|
57569
|
+
const candidate = path40.resolve(baseDir, "..", "skills");
|
|
56935
57570
|
try {
|
|
56936
|
-
return
|
|
57571
|
+
return fs42.statSync(candidate).isDirectory() ? candidate : null;
|
|
56937
57572
|
} catch {
|
|
56938
57573
|
return null;
|
|
56939
57574
|
}
|
|
@@ -56941,10 +57576,10 @@ function bundledSkillsDir(baseDir) {
|
|
|
56941
57576
|
function listFiles(root) {
|
|
56942
57577
|
const out = [];
|
|
56943
57578
|
const walk = (dir) => {
|
|
56944
|
-
for (const entry of
|
|
56945
|
-
const full =
|
|
57579
|
+
for (const entry of fs42.readdirSync(dir, { withFileTypes: true })) {
|
|
57580
|
+
const full = path40.join(dir, entry.name);
|
|
56946
57581
|
if (entry.isDirectory()) walk(full);
|
|
56947
|
-
else if (entry.isFile()) out.push(
|
|
57582
|
+
else if (entry.isFile()) out.push(path40.relative(root, full));
|
|
56948
57583
|
}
|
|
56949
57584
|
};
|
|
56950
57585
|
walk(root);
|
|
@@ -56952,7 +57587,7 @@ function listFiles(root) {
|
|
|
56952
57587
|
}
|
|
56953
57588
|
function readMarker(markerPath) {
|
|
56954
57589
|
try {
|
|
56955
|
-
const parsed2 = JSON.parse(
|
|
57590
|
+
const parsed2 = JSON.parse(fs42.readFileSync(markerPath, "utf8"));
|
|
56956
57591
|
return parsed2 && typeof parsed2 === "object" && parsed2.files ? parsed2 : null;
|
|
56957
57592
|
} catch {
|
|
56958
57593
|
return null;
|
|
@@ -56966,35 +57601,35 @@ function installBundledSkills(opts) {
|
|
|
56966
57601
|
skippedOlderPackage: [],
|
|
56967
57602
|
unchanged: [],
|
|
56968
57603
|
errors: [],
|
|
56969
|
-
targetDir: opts.targetDir ??
|
|
57604
|
+
targetDir: opts.targetDir ?? path40.join(os7.homedir(), ".claude", "skills"),
|
|
56970
57605
|
bundledDir: null
|
|
56971
57606
|
};
|
|
56972
57607
|
const bundled = bundledSkillsDir(opts.baseDir);
|
|
56973
57608
|
result.bundledDir = bundled;
|
|
56974
57609
|
if (!bundled) return result;
|
|
56975
57610
|
try {
|
|
56976
|
-
|
|
57611
|
+
fs42.mkdirSync(result.targetDir, { recursive: true });
|
|
56977
57612
|
} catch (e) {
|
|
56978
57613
|
result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
|
|
56979
57614
|
return result;
|
|
56980
57615
|
}
|
|
56981
|
-
const markerPath =
|
|
57616
|
+
const markerPath = path40.join(result.targetDir, ".ghl-command-skills.json");
|
|
56982
57617
|
const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
|
|
56983
57618
|
const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
|
|
56984
57619
|
for (const rel of listFiles(bundled)) {
|
|
56985
57620
|
try {
|
|
56986
|
-
const src =
|
|
56987
|
-
const dest =
|
|
57621
|
+
const src = fs42.readFileSync(path40.join(bundled, rel));
|
|
57622
|
+
const dest = path40.join(result.targetDir, rel);
|
|
56988
57623
|
const srcHash = sha2562(src);
|
|
56989
57624
|
let destBuf = null;
|
|
56990
57625
|
try {
|
|
56991
|
-
destBuf =
|
|
57626
|
+
destBuf = fs42.readFileSync(dest);
|
|
56992
57627
|
} catch {
|
|
56993
57628
|
destBuf = null;
|
|
56994
57629
|
}
|
|
56995
57630
|
if (destBuf === null) {
|
|
56996
|
-
|
|
56997
|
-
|
|
57631
|
+
fs42.mkdirSync(path40.dirname(dest), { recursive: true });
|
|
57632
|
+
fs42.writeFileSync(dest, src);
|
|
56998
57633
|
newMarker.files[rel] = srcHash;
|
|
56999
57634
|
result.installed.push(rel);
|
|
57000
57635
|
continue;
|
|
@@ -57011,7 +57646,7 @@ function installBundledSkills(opts) {
|
|
|
57011
57646
|
newMarker.packageVersion = marker.packageVersion;
|
|
57012
57647
|
result.skippedOlderPackage.push(rel);
|
|
57013
57648
|
} else if (lastWritten && destHash === lastWritten) {
|
|
57014
|
-
|
|
57649
|
+
fs42.writeFileSync(dest, src);
|
|
57015
57650
|
newMarker.files[rel] = srcHash;
|
|
57016
57651
|
result.updated.push(rel);
|
|
57017
57652
|
} else {
|
|
@@ -57022,7 +57657,7 @@ function installBundledSkills(opts) {
|
|
|
57022
57657
|
}
|
|
57023
57658
|
}
|
|
57024
57659
|
try {
|
|
57025
|
-
|
|
57660
|
+
fs42.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
|
|
57026
57661
|
} catch (e) {
|
|
57027
57662
|
result.errors.push(`marker write failed: ${String(e)}`);
|
|
57028
57663
|
}
|
|
@@ -57178,7 +57813,7 @@ dotenv2.config();
|
|
|
57178
57813
|
setPkgVersion(pkg.version);
|
|
57179
57814
|
{
|
|
57180
57815
|
const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
|
|
57181
|
-
if (configDirOverride && !
|
|
57816
|
+
if (configDirOverride && !path41.isAbsolute(configDirOverride)) {
|
|
57182
57817
|
process.stderr.write(
|
|
57183
57818
|
`[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.
|
|
57184
57819
|
`
|
|
@@ -57191,20 +57826,20 @@ process.on("unhandledRejection", (reason3) => {
|
|
|
57191
57826
|
`);
|
|
57192
57827
|
});
|
|
57193
57828
|
function hardenSecretFilePerms() {
|
|
57194
|
-
const repoDir =
|
|
57829
|
+
const repoDir = path41.resolve(__dirname, "..");
|
|
57195
57830
|
const candidates = [
|
|
57196
|
-
{ file:
|
|
57831
|
+
{ file: path41.join(repoDir, "start-mcp.sh"), mode: 448 },
|
|
57197
57832
|
// Legacy registry location (pre-migration); new location lives in app-data.
|
|
57198
|
-
{ file:
|
|
57833
|
+
{ file: path41.join(repoDir, ".ghl-tokens.json"), mode: 384 },
|
|
57199
57834
|
{ file: tokenRegistryPath(), mode: 384 }
|
|
57200
57835
|
];
|
|
57201
57836
|
for (const { file: file2, mode } of candidates) {
|
|
57202
57837
|
let current;
|
|
57203
57838
|
try {
|
|
57204
|
-
if (!
|
|
57205
|
-
current =
|
|
57839
|
+
if (!fs43.existsSync(file2)) continue;
|
|
57840
|
+
current = fs43.statSync(file2).mode & 511;
|
|
57206
57841
|
if (current !== mode) {
|
|
57207
|
-
|
|
57842
|
+
fs43.chmodSync(file2, mode);
|
|
57208
57843
|
}
|
|
57209
57844
|
} catch (error) {
|
|
57210
57845
|
const message = error instanceof Error ? error.message : String(error);
|