@elitedcs/ghl-mcp 3.84.7 → 3.84.9
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 +12 -0
- package/README.md +1 -1
- package/dist/index.js +1344 -621
- 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.9",
|
|
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", "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 or Zoom through your own connection, or a transcript file the owner exports from any other recorder and 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", "Other"]).describe("What recorded the call. Use Other for any recorder whose transcript was exported and attached."),
|
|
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()) {
|
|
@@ -40579,17 +41202,46 @@ function html(roster, seatToken, screen = "board", cap) {
|
|
|
40579
41202
|
.sandbox-badge{background:var(--ok-soft);color:var(--ok);border-radius:999px;padding:.14rem .55rem;font-size:var(--fs-label);font-weight:700;letter-spacing:.04em;vertical-align:middle;white-space:nowrap}
|
|
40580
41203
|
|
|
40581
41204
|
/* Stage tiles . same shapes as the hub's 7-stage track, clickable */
|
|
40582
|
-
|
|
40583
|
-
.
|
|
40584
|
-
.
|
|
40585
|
-
.
|
|
40586
|
-
.
|
|
40587
|
-
.
|
|
40588
|
-
.
|
|
40589
|
-
.
|
|
40590
|
-
.
|
|
40591
|
-
.
|
|
40592
|
-
.
|
|
41205
|
+
/* The steps (owner-approved layout, 2026-09-25): progress bar, next-step panel, one row per step. */
|
|
41206
|
+
.sprog{margin:1rem 0 .9rem}
|
|
41207
|
+
.strack{display:grid;grid-template-columns:repeat(7,1fr);gap:.35rem}
|
|
41208
|
+
.sseg{height:.45rem;border-radius:999px;background:var(--wait-soft)}
|
|
41209
|
+
.sseg.done{background:var(--ok)}.sseg.now{background:var(--gold)}.sseg.stale{background:var(--warn)}
|
|
41210
|
+
.splabel{display:flex;justify-content:space-between;flex-wrap:wrap;gap:.3rem 1rem;margin-top:.55rem;font-size:var(--fs-sm)}
|
|
41211
|
+
.splabel b{color:var(--heading);font-weight:600}.splabel span{color:var(--ink-3)}
|
|
41212
|
+
.snext{border:1px solid var(--gold);background:var(--gold-soft);border-radius:var(--radius);padding:1rem 1.1rem;display:grid;grid-template-columns:1fr auto;gap:.6rem 1.2rem;align-items:center}
|
|
41213
|
+
.snext .sk{font-size:var(--fs-label);letter-spacing:.14em;text-transform:uppercase;color:var(--gold-ink);font-weight:700;margin:0 0 .2rem}
|
|
41214
|
+
.snext h3{margin:0 0 .35rem;font-size:var(--fs-lg);color:var(--heading)}
|
|
41215
|
+
.snext p{margin:0;color:var(--ink-2);font-size:var(--fs-sm);max-width:60ch}
|
|
41216
|
+
.snext .swarn{margin-top:.5rem;color:var(--warn);font-weight:500}
|
|
41217
|
+
.snext .snoteline{margin-top:.4rem}
|
|
41218
|
+
.snext .slive{margin-top:.5rem}
|
|
41219
|
+
.sbtns{display:flex;flex-direction:column;align-items:flex-end;gap:.5rem}
|
|
41220
|
+
.sbtns .sbig{padding:.6rem 1.1rem;font-size:var(--fs-body)}
|
|
41221
|
+
.stage button,.snext button{box-sizing:border-box;max-width:100%;white-space:normal;overflow-wrap:anywhere;height:auto}
|
|
41222
|
+
.sbtns .runbtn.sbig{color:var(--on-accent)}
|
|
41223
|
+
.sbtns .swhy{margin:0;font-size:var(--fs-sm);max-width:22rem;text-align:right}
|
|
41224
|
+
.stages{margin-top:1rem;border-top:1px solid var(--line)}
|
|
41225
|
+
.stage{display:grid;grid-template-columns:2rem 9.5rem 7.5rem 1fr auto;gap:.4rem 1rem;align-items:center;padding:.7rem .3rem;border-bottom:1px solid var(--line)}
|
|
41226
|
+
.stage.here{background:var(--gold-soft)}
|
|
41227
|
+
.snum{width:1.7rem;height:1.7rem;border-radius:50%;display:grid;place-items:center;font-size:var(--fs-label);font-weight:700;background:var(--wait-soft);color:var(--wait)}
|
|
41228
|
+
.snum.done{background:var(--ok-soft);color:var(--ok)}.snum.stale{background:var(--warn-soft);color:var(--warn)}.snum.now{background:var(--gold);color:var(--on-accent)}
|
|
41229
|
+
.sname{font-weight:600;color:var(--heading)}
|
|
41230
|
+
.schip{justify-self:start;font-size:var(--fs-label);letter-spacing:.06em;text-transform:uppercase;font-weight:700;padding:.15rem .55rem;border-radius:999px;white-space:nowrap}
|
|
41231
|
+
.schip.done{background:var(--ok-soft);color:var(--ok)}.schip.stale{background:var(--warn-soft);color:var(--warn)}.schip.wait{background:var(--wait-soft);color:var(--wait)}.schip.you,.schip.now{background:var(--gold-soft);color:var(--gold-ink)}
|
|
41232
|
+
.swhat{color:var(--ink-2);font-size:var(--fs-sm);min-width:0;overflow-wrap:anywhere}
|
|
41233
|
+
.swhat .sreason{color:var(--warn)}
|
|
41234
|
+
.snote{display:block;font-size:var(--fs-sm);line-height:1.35}
|
|
41235
|
+
.slive{font-size:var(--fs-sm);line-height:1.35;margin-top:.25rem}
|
|
41236
|
+
.sact{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:.2rem .8rem}
|
|
41237
|
+
.sactbtn{font-size:var(--fs-sm);color:var(--gold-ink);font-weight:600;background:none;border:0;padding:.2rem 0;cursor:pointer;text-align:right}
|
|
41238
|
+
.sactbtn:hover{text-decoration:underline}
|
|
41239
|
+
.sactbtn:disabled{color:var(--ink-3);cursor:default;text-decoration:none}
|
|
41240
|
+
@media (max-width: 52rem){
|
|
41241
|
+
.stage{grid-template-columns:2rem 1fr auto;grid-template-areas:"n name act" ". chip chip" ". what what"}
|
|
41242
|
+
.stage .snum{grid-area:n}.stage .sname{grid-area:name}.stage .schip{grid-area:chip}.stage .swhat{grid-area:what}.stage .sact{grid-area:act}
|
|
41243
|
+
.snext{grid-template-columns:1fr}.sbtns{align-items:flex-start}.sbtns .swhy{text-align:left}
|
|
41244
|
+
}
|
|
40593
41245
|
|
|
40594
41246
|
/* \u2500\u2500 Other accounts: one-line rows. One account at a time is the rule. \u2500\u2500 */
|
|
40595
41247
|
.acct-list{padding:.2rem .9rem}
|
|
@@ -40699,8 +41351,6 @@ function html(roster, seatToken, screen = "board", cap) {
|
|
|
40699
41351
|
|
|
40700
41352
|
@media (max-width: 46rem){
|
|
40701
41353
|
.brand-sub,.localpill{display:none}
|
|
40702
|
-
.stages{gap:.3rem}
|
|
40703
|
-
.stage{min-width:4.2rem}
|
|
40704
41354
|
}
|
|
40705
41355
|
@media (prefers-reduced-motion: reduce){*{transition:none !important}}
|
|
40706
41356
|
` })}<body data-screen="${screen}"${cap ? ` data-capability="${cap.key}"` : ""}>
|
|
@@ -40735,6 +41385,14 @@ const GATES=${scriptJson(HUMAN_GATE_STAGES)};let PROTECTED=${scriptJson(PROTECTE
|
|
|
40735
41385
|
const TYPES=["client","sandbox","template","hidden"];
|
|
40736
41386
|
const TYPE_LABELS={client:"Client",sandbox:"Practice sandbox",template:"Template",hidden:"Hidden"};
|
|
40737
41387
|
const LABELS=["Intake","Brief","Approve","Build","Verify","Manual tasks","Go-live"];
|
|
41388
|
+
// What each step does: [the one line on its row, the fuller sentence in the next-step panel].
|
|
41389
|
+
const STEP_ABOUT=[["Creates the intake form, its fields and one notification workflow.","Creates or repairs this client's intake form, its custom fields and one published notification workflow."],
|
|
41390
|
+
["Reads the intake into a brief.","Reads this client's intake, from the submitted form or the one completed here, and turns it into the brief the plan is made from."],
|
|
41391
|
+
["You approve the proposed build.","Review the intake and the brief, then approve the proposed build for this client."],
|
|
41392
|
+
["Builds the approved plan into this account.","Builds the approved plan into this client's account. Anything it cannot build is listed as an issue."],
|
|
41393
|
+
["Checks the build for broken links.","Checks the account for broken links between workflows, pipelines and fields, and tests the form. It may submit and delete one test lead. Anything it cannot check is reported as unchecked."],
|
|
41394
|
+
["Jobs only a person can do. You confirm them.","The jobs only a person can do, such as A2P, Google sign-in, staff and phone. Confirm them when they are finished."],
|
|
41395
|
+
["You sign this client live.","Sign this client live once Verify is clean and the manual tasks are finished."]];
|
|
40738
41396
|
const STATUS_TEXT={pending:"not started",in_progress:"in progress",done:"done"};
|
|
40739
41397
|
const NEXT={pending:"in_progress",in_progress:"done",done:"pending"};
|
|
40740
41398
|
let polling=false,query="",showParked=false,showNew=false,snapshots=null,snapshotError="";
|
|
@@ -41252,6 +41910,9 @@ function preflightCard(c){const box=document.createElement("section");box.classN
|
|
|
41252
41910
|
const a=document.createElement("a");a.href="/agency#connections";a.textContent="Open Your agency";box.appendChild(a);return box}
|
|
41253
41911
|
function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES.map(()=>"pending")}).stages;
|
|
41254
41912
|
const sandbox=typeOf(state,c.locationId)==="sandbox";
|
|
41913
|
+
// A template or hidden account opens only so its owner can lock or unlock it and change its
|
|
41914
|
+
// type: nothing on its card builds into it (review L-1; a template can be the live snapshot source).
|
|
41915
|
+
const parkedType=["template","hidden"].includes(typeOf(state,c.locationId));
|
|
41255
41916
|
const div=document.createElement("div");div.className="client";
|
|
41256
41917
|
const head=document.createElement("div");head.className="client-head";
|
|
41257
41918
|
const av=document.createElement("span");av.className="avatar lg";av.textContent=initials(c.name);head.appendChild(av);
|
|
@@ -41269,14 +41930,29 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41269
41930
|
isel.onchange=async()=>{await postJSON("/api/industry",{locationId:c.locationId,industry:isel.value});load()};
|
|
41270
41931
|
sels.appendChild(isel);head.appendChild(sels);div.appendChild(head);
|
|
41271
41932
|
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(
|
|
41933
|
+
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)}
|
|
41934
|
+
else if(!st.some(s=>s==="done"))div.appendChild(preflightCard(c));
|
|
41273
41935
|
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
41936
|
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
41937
|
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."}
|
|
41276
41938
|
if(!r.ok)alert(r.error);else load()
|
|
41277
41939
|
}catch(e){alert("Command OS could not confirm the result. Refresh this client to read its saved state before retrying.")}};
|
|
41278
|
-
|
|
41279
|
-
|
|
41940
|
+
// The steps, laid out as the owner approved on 2026-09-25: a progress bar, the next step in
|
|
41941
|
+
// full, then one row per step. Only the layout changed. Every button below keeps the condition
|
|
41942
|
+
// and the handler it had in the seven-column layout; the next-step panel draws the SAME
|
|
41943
|
+
// buttons again for the current step, so nothing appears there that its row would not offer.
|
|
41944
|
+
const staleOf=i=>state.buildViews?.[c.locationId]?.stale?.find(x=>x.stage===i);
|
|
41945
|
+
const isDone=i=>(st[i]||"pending")==="done"&&!staleOf(i);
|
|
41946
|
+
let curI=-1;for(let i=0;i<STAGES.length;i++){if(!isDone(i)){curI=i;break}}
|
|
41947
|
+
const nDone=STAGES.filter((_,i)=>isDone(i)).length;
|
|
41948
|
+
const prog=document.createElement("div");prog.className="sprog";prog.setAttribute("aria-label","Progress: "+nDone+" of "+STAGES.length+" steps done");
|
|
41949
|
+
const track=document.createElement("div");track.className="strack";prog.appendChild(track);
|
|
41950
|
+
const plabel=document.createElement("div");plabel.className="splabel";prog.appendChild(plabel);
|
|
41951
|
+
div.appendChild(prog);
|
|
41952
|
+
const nextBox=document.createElement("div");nextBox.className="snext";div.appendChild(nextBox);
|
|
41953
|
+
const row=document.createElement("div");row.className="stages";row.setAttribute("role","list");
|
|
41954
|
+
let curLive=false;
|
|
41955
|
+
STAGES.forEach((name,i)=>{const stale=staleOf(i);const el=document.createElement("div");el.className="stage "+(stale?"pending":(st[i]||"pending"))+(i===curI?" here":"");el.setAttribute("role","listitem");
|
|
41280
41956
|
const gate=GATES[String(i)];
|
|
41281
41957
|
const oc0=(state.outcomes||{})[c.locationId+":"+i];const notPassed=(st[i]||"pending")==="pending"&&oc0&&(oc0.issues||[]).length>0;
|
|
41282
41958
|
if(notPassed)el.className+=" notpassed";
|
|
@@ -41286,8 +41962,16 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41286
41962
|
// from before the run (a "stale" warning and an old issue count are both wrong then).
|
|
41287
41963
|
const live=Boolean(run&&run.running&&run.locationId===c.locationId&&run.stage===i);
|
|
41288
41964
|
const anyBusy=Boolean(run&&run.running&&run.locationId===c.locationId);
|
|
41289
|
-
|
|
41290
|
-
|
|
41965
|
+
if(i===curI&&live)curLive=true;
|
|
41966
|
+
const tone=i===curI?"now":isDone(i)?"done":(stale||notPassed)?"stale":"";
|
|
41967
|
+
const seg=document.createElement("span");seg.className="sseg "+tone;track.appendChild(seg);
|
|
41968
|
+
const num=document.createElement("span");num.className="snum "+tone;num.textContent=String(i+1);el.appendChild(num);
|
|
41969
|
+
const nm=document.createElement("b");nm.className="sname";nm.textContent=LABELS[i];el.appendChild(nm);
|
|
41970
|
+
const chipOf=live?["Running\u2026","now"]:stale?["Run again","stale"]:notPassed?["Not passed","stale"]:isDone(i)?["Done","done"]:gate?["Your call","you"]:(st[i]||"pending")==="in_progress"?["In progress","now"]:i===curI?["Up next","now"]:["Waiting","wait"];
|
|
41971
|
+
const chip=document.createElement("span");chip.className="schip "+chipOf[1];chip.textContent=chipOf[0];el.appendChild(chip);
|
|
41972
|
+
const what=document.createElement("div");what.className="swhat";el.appendChild(what);
|
|
41973
|
+
const act=document.createElement("div");act.className="sact";el.appendChild(act);
|
|
41974
|
+
const mkLive=()=>{const box=document.createElement("div");box.className="slive";
|
|
41291
41975
|
const clock=document.createElement("div");clock.style.cssText="font-variant-numeric:tabular-nums;color:var(--ok)";
|
|
41292
41976
|
const began=run.startedAt?new Date(run.startedAt).getTime():0;
|
|
41293
41977
|
const tick=()=>{if(!began)return;const sec=Math.max(0,Math.round((Date.now()-began)/1000));
|
|
@@ -41300,29 +41984,60 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41300
41984
|
line.textContent=(doneAll?"\u2713 ":g.state==="in_progress"?"\u203A ":" ")+g.label+" "+g.done+"/"+g.total;box.appendChild(line)}}
|
|
41301
41985
|
if(pr&&pr.current){const now=document.createElement("div");now.style.cssText="margin-top:.25rem;color:var(--muted)";
|
|
41302
41986
|
now.textContent=String(pr.current).replace(/^"|"$/g,"").slice(0,110);box.appendChild(now)}
|
|
41303
|
-
|
|
41987
|
+
return box};
|
|
41304
41988
|
// One short line here; the button's confirmation carries the full wording. At 135
|
|
41305
41989
|
// characters this sentence made every stage box 677 px tall (auditor, 2026-09-07).
|
|
41306
|
-
|
|
41990
|
+
let about=document.createElement("span");about.textContent=STEP_ABOUT[i][0];
|
|
41991
|
+
if(i===4){const disclosure=document.createElement("span");disclosure.textContent="Checks links and the form. May submit and delete one test lead.";about=disclosure}
|
|
41307
41992
|
if(gate)el.title=gate;
|
|
41308
|
-
if(stale&&!live){const reason=document.createElement("small");reason.textContent=stale.reason;el.appendChild(reason)}
|
|
41309
41993
|
const oc=(state.outcomes||{})[c.locationId+":"+i];
|
|
41310
|
-
|
|
41311
|
-
|
|
41312
|
-
|
|
41994
|
+
const nIss=oc?(oc.issues||[]).length:0;
|
|
41995
|
+
const toggleIssues=(ev)=>{ev.stopPropagation();oOpen[c.locationId+":"+i]=!oOpen[c.locationId+":"+i];render(lastState,lastRun)};
|
|
41996
|
+
const mkNote=()=>{let note=null;
|
|
41997
|
+
if(!live&&oc&&(oc.summary||(oc.issues||[]).length)){note=document.createElement("small");note.className="snote";
|
|
41998
|
+
note.style.cssText="color:"+(nIss?"var(--warn)":"var(--ok)")+";text-decoration:"+(nIss?"underline":"none")+";cursor:"+(nIss?"pointer":"default");
|
|
41313
41999
|
note.textContent=i===4&&oc.ok===true?(${verifyPassedLine.toString()})((state.manualTasks?.[c.locationId]||[]).length):nIss?("\u26A0 "+nIss+" issue"+(nIss>1?"s":"")+". See details"):((oc.plain&&oc.plain.summary)||oc.summary||"").slice(0,90);
|
|
41314
|
-
if(nIss)note.onclick=
|
|
41315
|
-
|
|
42000
|
+
if(nIss)note.onclick=toggleIssues}
|
|
42001
|
+
return note};
|
|
42002
|
+
// What the row says, in one place: the live run, else why it went stale, else what the last
|
|
42003
|
+
// run found, else what the step does.
|
|
42004
|
+
if(live){const rn=document.createElement("span");rn.textContent="Running now.";what.appendChild(rn);what.appendChild(mkLive())}
|
|
42005
|
+
if(stale&&!live){const reason=document.createElement("span");reason.className="sreason";reason.textContent=stale.reason;what.appendChild(reason)}
|
|
42006
|
+
else if(!live){const note=mkNote();what.appendChild(note||about)}
|
|
41316
42007
|
if(POWERED.includes(i)&&(st[i]||"pending")!=="done"){el.style.cursor="default";
|
|
41317
42008
|
el.title="Command OS runs and checks this step. Its result comes from a run, never from a click."}
|
|
41318
|
-
|
|
41319
|
-
const
|
|
41320
|
-
|
|
41321
|
-
|
|
42009
|
+
const gateLabel=({2:"Approve this plan",5:"Confirm these tasks are finished",6:"Sign this client live"})[i];
|
|
42010
|
+
const mkGate=cls=>{const action=document.createElement("button");action.className=cls;action.textContent=gateLabel;action.onclick=()=>{if(i===2)return approveBuilding();return markStage(c.locationId,i,"done")};return action};
|
|
42011
|
+
const canBuild=!parkedType&&(i<=1||sandbox||Boolean((state.buildApproved||{})[c.locationId]));
|
|
42012
|
+
const runIt=async(ev)=>{ev.stopPropagation();if(i===0&&!confirm("In "+c.name+", Intake writes custom fields, one form, and one published notification workflow."+String.fromCharCode(10,10)+INTAKE_NOTIFICATION_REVIEW.join(String.fromCharCode(10))+String.fromCharCode(10,10)+"Create or repair these intake items?"))return;if(i===4&&!confirm("Verify submits one test lead to "+c.name+"\u2019s funnel, watches it arrive, and deletes the test contact afterwards. It may fire this client\u2019s own automations once. Run the check?"))return;const res=await fetch("/api/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,stage:i,intakeConfirmed:i===0})});const j=await res.json();if(!j.ok)alert(j.error);load()};
|
|
42013
|
+
const mkRun=(cls,label)=>{const b=document.createElement("button");b.className=cls;
|
|
42014
|
+
const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":label;
|
|
41322
42015
|
b.disabled=Boolean(busy)||(!st.some(s=>s==="done")&&!preflights[c.locationId]?.ok);
|
|
41323
|
-
b.onclick=
|
|
41324
|
-
|
|
42016
|
+
b.onclick=runIt;return b};
|
|
42017
|
+
if(!live&&nIss){const si=document.createElement("button");si.className="sactbtn";si.textContent=nIss>1?"See issues":"See issue";si.onclick=toggleIssues;act.appendChild(si)}
|
|
42018
|
+
if(gate&&!anyBusy&&!parkedType)act.appendChild(mkGate("sactbtn"));
|
|
42019
|
+
if(canBuild&&POWERED.includes(i))act.appendChild(mkRun("sactbtn srun",i===curI?"Run "+LABELS[i]:(st[i]||"pending")==="done"?"Run again":"Run this step"));
|
|
42020
|
+
if(i===curI){const main=document.createElement("div");
|
|
42021
|
+
const k=document.createElement("p");k.className="sk";k.textContent=parkedType?"Building is off":"Next step";main.appendChild(k);
|
|
42022
|
+
const h3=document.createElement("h3");h3.textContent=parkedType?"Nothing is built into this account":(i+1)+". "+LABELS[i];main.appendChild(h3);
|
|
42023
|
+
const pa=document.createElement("p");pa.textContent=parkedType?"It opens here so you can lock or unlock it, or change its type to Client to build it.":STEP_ABOUT[i][1];main.appendChild(pa);
|
|
42024
|
+
if(live)main.appendChild(mkLive());
|
|
42025
|
+
if(stale&&!live){const w=document.createElement("p");w.className="swarn";w.textContent=stale.reason;main.appendChild(w)}
|
|
42026
|
+
const pn=mkNote();if(pn){const w=document.createElement("p");w.className="snoteline";w.appendChild(pn);main.appendChild(w)}
|
|
42027
|
+
const btns=document.createElement("div");btns.className="sbtns";
|
|
42028
|
+
if(gate&&!anyBusy&&!parkedType)btns.appendChild(mkGate("intakebtn sbig"));
|
|
42029
|
+
if(canBuild&&POWERED.includes(i))btns.appendChild(mkRun("runbtn sbig","Run "+LABELS[i]));
|
|
42030
|
+
if(!parkedType&&canBuild&&POWERED.includes(i)&&run&&run.running&&run.locationId!==c.locationId){const why=document.createElement("p");why.className="muted swhy";why.textContent="A step is running for another client. This one can start when it finishes.";btns.appendChild(why)}
|
|
42031
|
+
if(!btns.children.length&&!live&&!parkedType){const why=document.createElement("p");why.className="muted swhy";
|
|
42032
|
+
why.textContent=anyBusy?"Another step is running for this client. This one opens when it finishes.":POWERED.includes(i)?"Approve building for this client below, and this step can run.":"";
|
|
42033
|
+
if(why.textContent)btns.appendChild(why)}
|
|
42034
|
+
nextBox.title=el.title||"";nextBox.appendChild(main);nextBox.appendChild(btns)}
|
|
41325
42035
|
row.appendChild(el)});
|
|
42036
|
+
if(curI<0){const main=document.createElement("div");const k=document.createElement("p");k.className="sk";k.textContent="All done";main.appendChild(k);
|
|
42037
|
+
const h3=document.createElement("h3");h3.textContent="All "+STAGES.length+" steps are done.";main.appendChild(h3);nextBox.appendChild(main)}
|
|
42038
|
+
{const b=document.createElement("b");b.textContent=curI<0?"All "+STAGES.length+" steps done":"Step "+(curI+1)+" of "+STAGES.length+": "+LABELS[curI];plabel.appendChild(b);
|
|
42039
|
+
const after=STAGES.length-nDone-(curI<0?0:1);const s=document.createElement("span");
|
|
42040
|
+
s.textContent=nDone+" done"+(curI<0?"":" \xB7 "+(curLive?"1 running now":"1 needs you")+" \xB7 "+after+" after that");plabel.appendChild(s)}
|
|
41326
42041
|
div.appendChild(row);
|
|
41327
42042
|
for(let i=0;i<STAGES.length;i++){if(oOpen[c.locationId+":"+i]){const op=outcomePanel(c.locationId,i,state);if(op)div.appendChild(op)}}
|
|
41328
42043
|
const view=(state.buildViews||{})[c.locationId];
|
|
@@ -41337,7 +42052,7 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41337
42052
|
for(const stale of view.stale){const p=document.createElement("p");p.textContent=LABELS[stale.stage]+": stale. "+stale.reason;div.appendChild(p)}
|
|
41338
42053
|
if(view.approvedAt){const p=document.createElement("p");p.textContent="Building approved on "+new Date(view.approvedAt).toLocaleString()+".";div.appendChild(p)}
|
|
41339
42054
|
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)}
|
|
42055
|
+
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
42056
|
}else{const p=document.createElement("p");p.textContent="The executable plan is prepared when Build runs.";div.appendChild(p)}
|
|
41342
42057
|
if(view?.handoff)div.appendChild(handoffPanel(c,view));
|
|
41343
42058
|
const _prot=PROTECTED[c.locationId];
|
|
@@ -41354,6 +42069,7 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41354
42069
|
const rv=document.createElement("button");rv.className="qdel";rv.style.marginLeft=".5rem";rv.textContent="revoke";
|
|
41355
42070
|
rv.onclick=async()=>{await postJSON("/api/build-approval",{locationId:c.locationId,revoke:true});load()};
|
|
41356
42071
|
ab.appendChild(s1);ab.appendChild(rv);ab.appendChild(protectBtn(c))}
|
|
42072
|
+
else if(parkedType){ab.appendChild(protectBtn(c))}
|
|
41357
42073
|
else{const w=document.createElement("span");w.className="muted";w.textContent="Automated building is off for this client. ";
|
|
41358
42074
|
const btn=document.createElement("button");btn.className="intakebtn";btn.style.padding=".25rem .55rem";btn.textContent="Approve building\u2026";
|
|
41359
42075
|
btn.onclick=approveBuilding;
|
|
@@ -41734,10 +42450,12 @@ function render(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...sta
|
|
|
41734
42450
|
const parked=sorted.filter(c=>match(c)&&["template","hidden"].includes(typeOf(state,c.locationId)));
|
|
41735
42451
|
if(!board.length&&!parked.length){const d=document.createElement("div");d.className="client";d.innerHTML="<h2>No matches</h2>";m.appendChild(d);return}
|
|
41736
42452
|
/* One card open at a time. Default: the account being built, else the busiest one. */
|
|
41737
|
-
|
|
42453
|
+
/* 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). */
|
|
42454
|
+
const openable=[...board,...parked];
|
|
42455
|
+
let act=(activeId&&openable.some(c=>c.locationId===activeId))?activeId:null;
|
|
41738
42456
|
if(!act&&run&&run.running&&board.some(c=>c.locationId===run.locationId))act=run.locationId;
|
|
41739
42457
|
if(!act&&board.length)act=board[0].locationId;
|
|
41740
|
-
const actC=
|
|
42458
|
+
const actC=openable.find(c=>c.locationId===act);
|
|
41741
42459
|
if(run&&run.running&&actC&&run.locationId!==actC.locationId){
|
|
41742
42460
|
const other=ROSTER.find(c=>c.locationId===run.locationId);
|
|
41743
42461
|
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 +42473,12 @@ function render(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...sta
|
|
|
41755
42473
|
const hint=document.createElement("p");hint.className="switchnote";
|
|
41756
42474
|
hint.textContent="One account at a time. Opening another puts this one back in the list. Nothing is lost.";
|
|
41757
42475
|
m.appendChild(hint)}
|
|
41758
|
-
|
|
41759
|
-
|
|
42476
|
+
const parkedRest=parked.filter(c=>c.locationId!==act);
|
|
42477
|
+
if(parkedRest.length){const gh=document.createElement("div");gh.className="grouphead";
|
|
42478
|
+
gh.textContent=(showParked?"\u25BE":"\u25B8")+" Templates & hidden ("+parkedRest.length+")";
|
|
41760
42479
|
gh.onclick=()=>{showParked=!showParked;render(state,run)};m.appendChild(gh);
|
|
41761
42480
|
if(showParked){const list2=document.createElement("div");list2.className="card acct-list";
|
|
41762
|
-
for(const c of
|
|
42481
|
+
for(const c of parkedRest)list2.appendChild(acctRow(c,state,run));m.appendChild(list2)}}}
|
|
41763
42482
|
${COCKPIT_TYPE_JS}
|
|
41764
42483
|
load();
|
|
41765
42484
|
</script></body></html>`;
|
|
@@ -42301,8 +43020,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42301
43020
|
if (!bytes.length) throw new Error("that file is empty");
|
|
42302
43021
|
if (bytes.length > DOC_MAX_BYTES) throw new Error("that file is over 20 MB");
|
|
42303
43022
|
const dir = clientDocsDir(args.locationId);
|
|
42304
|
-
|
|
42305
|
-
if (!insideDir(
|
|
43023
|
+
fs35.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
43024
|
+
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
43025
|
const saved = writeDocAtomically(dir, name, bytes);
|
|
42307
43026
|
res.end(JSON.stringify({ ok: true, name: saved, files: listClientDocs(args.locationId) }));
|
|
42308
43027
|
} catch (e) {
|
|
@@ -42477,6 +43196,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42477
43196
|
try {
|
|
42478
43197
|
const { locationId: locationId2, stage, intakeConfirmed } = JSON.parse(body);
|
|
42479
43198
|
if (activeRun2?.running) throw new Error("a run is already in progress");
|
|
43199
|
+
const runType = readCockpitState().accounts?.[locationId2]?.type;
|
|
43200
|
+
if (runType === "template" || runType === "hidden") throw new Error(PARKED_NOT_BUILT);
|
|
42480
43201
|
let st0 = readCockpitState();
|
|
42481
43202
|
let inputHash;
|
|
42482
43203
|
let normalizedAnswers;
|
|
@@ -42505,8 +43226,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42505
43226
|
const snap = await readIntakeSnapshot(c, locationId2);
|
|
42506
43227
|
if (snap.status === "unavailable") throw new Error(INTAKE_UNREADABLE);
|
|
42507
43228
|
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((
|
|
43229
|
+
const required = composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }).filter((q3) => q3.required && !String(normalizedAnswers?.[q3.label] ?? normalizedAnswers?.[q3.key] ?? "").trim());
|
|
43230
|
+
if (required.length) throw new Error("Complete these required intake answers: " + required.map((q3) => q3.label).join(", ") + ".");
|
|
42510
43231
|
}
|
|
42511
43232
|
if (stage === 0 && intakeConfirmed !== true) throw new Error("Confirm the custom fields, form and published notification workflow on this client\u2019s Intake button.");
|
|
42512
43233
|
if (!st0.accounts?.[locationId2]?.industry) throw new Error("Choose the client industry before running Intake.");
|
|
@@ -42552,7 +43273,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42552
43273
|
industries: INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })),
|
|
42553
43274
|
overlay: agency,
|
|
42554
43275
|
warnings: unknownKeys(agency, INTAKE_QUESTIONS).map((k) => `Your overlay references "${k}", which isn't in the base intake.`),
|
|
42555
|
-
questions: composed.map((
|
|
43276
|
+
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
43277
|
}));
|
|
42557
43278
|
return;
|
|
42558
43279
|
}
|
|
@@ -42588,14 +43309,14 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42588
43309
|
const snap = c ? await readIntakeSnapshot(c, locationId2, Boolean(st.intakeSent?.[locationId2])) : { status: "not_installed", fields: {}, queryKeys: {}, submissions: 0 };
|
|
42589
43310
|
if (snap.status === "unavailable") throw new Error(INTAKE_UNREADABLE);
|
|
42590
43311
|
const industry = st.accounts?.[locationId2]?.industry;
|
|
42591
|
-
const questions = composeQuestionSet({ industry, agency: readOverlay() }).map((
|
|
43312
|
+
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
43313
|
const answers = normalizedIntake(snap, st.intakeDrafts?.[locationId2]);
|
|
42593
43314
|
const submittedAnswers = normalizedIntake(snap);
|
|
42594
43315
|
const hash = intakeInput(submittedAnswers, industry, composeQuestionSet({ industry, agency: readOverlay() }));
|
|
42595
43316
|
const latestState = readCockpitState();
|
|
42596
43317
|
writeCockpitState({ ...latestState, intakeInputs: { ...latestState.intakeInputs, [locationId2]: { hash, answers: submittedAnswers, status: snap.status } } });
|
|
42597
43318
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
42598
|
-
res.end(JSON.stringify({ ok: true, ...snap, questions, industry, inputHash: hash, unansweredRequired: questions.filter((
|
|
43319
|
+
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
43320
|
} catch (e) {
|
|
42600
43321
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
42601
43322
|
logQuietly(`client-intake ${new URL(req.url || "/", "http://x").searchParams.get("locationId") || ""}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -42614,7 +43335,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42614
43335
|
const snap = await readIntakeSnapshot(c, args.locationId, false);
|
|
42615
43336
|
if (!snap.formId || !snap.formUrl) throw new Error(INTAKE_FORM_REQUIRED);
|
|
42616
43337
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
42617
|
-
const identityLabels = new Set(composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).filter((
|
|
43338
|
+
const identityLabels = new Set(composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).filter((q3) => q3.field.kind === "standard").map((q3) => q3.label));
|
|
42618
43339
|
const customAnswers = Object.fromEntries(Object.entries(args.answers ?? {}).filter(([label]) => !identityLabels.has(label)));
|
|
42619
43340
|
const pf = prefillFormUrl(snap.formUrl, snap.queryKeys, customAnswers, args.contact ?? {}, snap.altQueryKeys ?? {});
|
|
42620
43341
|
res.end(JSON.stringify({ ok: true, url: pf.url, leftovers: pf.leftovers }));
|
|
@@ -42656,28 +43377,28 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42656
43377
|
if (args.file) {
|
|
42657
43378
|
const name = safeDocName(args.file);
|
|
42658
43379
|
const dir = clientDocsDir(args.locationId);
|
|
42659
|
-
const p =
|
|
42660
|
-
if (!
|
|
42661
|
-
if (
|
|
42662
|
-
const text2 =
|
|
43380
|
+
const p = path33.join(dir, name);
|
|
43381
|
+
if (!fs35.existsSync(p)) throw new Error("that document is not saved for this client");
|
|
43382
|
+
if (fs35.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error("that document is not saved for this client");
|
|
43383
|
+
const text2 = fs35.readFileSync(p, "utf8");
|
|
42663
43384
|
if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
|
|
42664
43385
|
fileSource = { kind: "text", text: text2 };
|
|
42665
43386
|
}
|
|
42666
43387
|
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
43388
|
let recorderPatterns = [];
|
|
42668
43389
|
if (source.kind === "recorder") {
|
|
42669
|
-
recorderPatterns = await new Promise((
|
|
43390
|
+
recorderPatterns = await new Promise((resolve13) => {
|
|
42670
43391
|
const mcpList = claudeCommand(claudeBin(), ["mcp", "list"]);
|
|
42671
43392
|
const ls = (0, import_child_process5.spawn)(mcpList.command, mcpList.args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
42672
43393
|
let buf = "";
|
|
42673
43394
|
ls.stdout.on("data", (d) => {
|
|
42674
43395
|
buf += d.toString();
|
|
42675
43396
|
});
|
|
42676
|
-
ls.on("close", () =>
|
|
42677
|
-
ls.on("error", () =>
|
|
43397
|
+
ls.on("close", () => resolve13(recorderAllowPatterns(buf)));
|
|
43398
|
+
ls.on("error", () => resolve13([]));
|
|
42678
43399
|
setTimeout(() => {
|
|
42679
43400
|
ls.kill("SIGKILL");
|
|
42680
|
-
|
|
43401
|
+
resolve13(recorderAllowPatterns(buf));
|
|
42681
43402
|
}, 9e4).unref?.();
|
|
42682
43403
|
});
|
|
42683
43404
|
if (!recorderPatterns.length) {
|
|
@@ -42688,10 +43409,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42688
43409
|
}
|
|
42689
43410
|
const out = await prefillFromSource(
|
|
42690
43411
|
(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((
|
|
43412
|
+
composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((q3) => q3.label),
|
|
42692
43413
|
source,
|
|
42693
43414
|
recorderPatterns,
|
|
42694
|
-
composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((
|
|
43415
|
+
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
43416
|
);
|
|
42696
43417
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
42697
43418
|
res.end(JSON.stringify({ ...out, costs: readRunCosts().filter((r) => r.locationId === args.locationId).slice(-2) }));
|
|
@@ -42733,14 +43454,14 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42733
43454
|
}
|
|
42734
43455
|
if (req.method === "GET" && /^\/brand\/[a-z0-9-]+\/screenshot\.png$/.test(url)) {
|
|
42735
43456
|
const slug3 = url.split("/")[2];
|
|
42736
|
-
const file2 =
|
|
42737
|
-
if (!
|
|
43457
|
+
const file2 = path33.join(brandDir(slug3), "screenshot.png");
|
|
43458
|
+
if (!fs35.existsSync(file2)) {
|
|
42738
43459
|
res.writeHead(404);
|
|
42739
43460
|
res.end();
|
|
42740
43461
|
return;
|
|
42741
43462
|
}
|
|
42742
43463
|
res.writeHead(200, { "Content-Type": "image/png", "Cache-Control": "no-store" });
|
|
42743
|
-
res.end(
|
|
43464
|
+
res.end(fs35.readFileSync(file2));
|
|
42744
43465
|
return;
|
|
42745
43466
|
}
|
|
42746
43467
|
if (req.method === "GET" && /^\/brand\/[a-z0-9-]+$/.test(url)) {
|
|
@@ -42749,7 +43470,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42749
43470
|
const dna = houseLooks().find((d) => d.slug === slug3) ?? readBrand(slug3);
|
|
42750
43471
|
const dir = brandExists(slug3) ? brandDir(slug3) : void 0;
|
|
42751
43472
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
42752
|
-
res.end(brandCardHtml(dna, { screenshot: dir ?
|
|
43473
|
+
res.end(brandCardHtml(dna, { screenshot: dir ? fs35.existsSync(path33.join(dir, "screenshot.png")) : false, dir, seatToken: mayServeSeat(req, port) ? seatToken : "" }));
|
|
42753
43474
|
} catch (e) {
|
|
42754
43475
|
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
42755
43476
|
res.end(e.message);
|
|
@@ -42911,7 +43632,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
42911
43632
|
}
|
|
42912
43633
|
try {
|
|
42913
43634
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
42914
|
-
res.end(
|
|
43635
|
+
res.end(fs35.readFileSync(localCopyPath(locationId2), "utf8"));
|
|
42915
43636
|
} catch {
|
|
42916
43637
|
res.end("The handoff is not prepared yet. Run Verify first.");
|
|
42917
43638
|
}
|
|
@@ -43011,8 +43732,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
43011
43732
|
return;
|
|
43012
43733
|
}
|
|
43013
43734
|
if (req.method === "GET" && url === "/blueprint") {
|
|
43014
|
-
const
|
|
43015
|
-
const locationId2 =
|
|
43735
|
+
const q3 = new URL(req.url || "/", "http://x").searchParams;
|
|
43736
|
+
const locationId2 = q3.get("locationId") || "";
|
|
43016
43737
|
const st = readCockpitState();
|
|
43017
43738
|
const review = st.reviews?.[locationId2];
|
|
43018
43739
|
if (!review?.ok) {
|
|
@@ -43022,7 +43743,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
43022
43743
|
}
|
|
43023
43744
|
const name = currentRoster().find((r) => r.locationId === locationId2)?.name || locationId2;
|
|
43024
43745
|
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:
|
|
43746
|
+
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
43747
|
return;
|
|
43027
43748
|
}
|
|
43028
43749
|
if (req.method === "GET" && url === "/api/review") {
|
|
@@ -43483,9 +44204,9 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
43483
44204
|
res.end();
|
|
43484
44205
|
}));
|
|
43485
44206
|
try {
|
|
43486
|
-
await new Promise((
|
|
44207
|
+
await new Promise((resolve13, reject) => {
|
|
43487
44208
|
server2.once("error", reject);
|
|
43488
|
-
server2.listen(port, "127.0.0.1",
|
|
44209
|
+
server2.listen(port, "127.0.0.1", resolve13);
|
|
43489
44210
|
});
|
|
43490
44211
|
} catch (e) {
|
|
43491
44212
|
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 +44232,13 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
43511
44232
|
if (adopted) process.stderr.write(` Synced ${adopted} client${adopted === 1 ? "" : "s"} from your GHL (teammate updates).
|
|
43512
44233
|
`);
|
|
43513
44234
|
});
|
|
43514
|
-
return await new Promise((
|
|
43515
|
-
const
|
|
43516
|
-
process.on("SIGINT",
|
|
43517
|
-
process.on("SIGTERM",
|
|
44235
|
+
return await new Promise((resolve13) => {
|
|
44236
|
+
const stop2 = () => server2.close(() => resolve13(0));
|
|
44237
|
+
process.on("SIGINT", stop2);
|
|
44238
|
+
process.on("SIGTERM", stop2);
|
|
43518
44239
|
});
|
|
43519
44240
|
}
|
|
43520
|
-
var import_zod62, import_node_util4,
|
|
44241
|
+
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
44242
|
var init_dashboard = __esm({
|
|
43522
44243
|
"src/dashboard.ts"() {
|
|
43523
44244
|
"use strict";
|
|
@@ -43545,8 +44266,8 @@ var init_dashboard = __esm({
|
|
|
43545
44266
|
init_handoff_snapshot();
|
|
43546
44267
|
init_member_progress();
|
|
43547
44268
|
init_build_obligations();
|
|
43548
|
-
|
|
43549
|
-
|
|
44269
|
+
fs35 = __toESM(require("fs"));
|
|
44270
|
+
path33 = __toESM(require("path"));
|
|
43550
44271
|
http = __toESM(require("http"));
|
|
43551
44272
|
import_child_process4 = require("child_process");
|
|
43552
44273
|
init_credentials_store();
|
|
@@ -43624,6 +44345,7 @@ var init_dashboard = __esm({
|
|
|
43624
44345
|
UPGRADE_MSG = "\n The cockpit is not included in this licence, so it did not start.\n Nothing was changed.\n\n";
|
|
43625
44346
|
DOC_EXTS = /* @__PURE__ */ new Set([".md", ".txt", ".vtt", ".srt", ".csv"]);
|
|
43626
44347
|
DOC_MAX_BYTES = 20 * 1024 * 1024;
|
|
44348
|
+
PARKED_NOT_BUILT = "Template and hidden accounts are not built from Command OS. Change the account's type to Client first.";
|
|
43627
44349
|
NEEDS_AGENCY_KEY = "No agency key on file. Run register_agency_key in GHL Command first.";
|
|
43628
44350
|
INTAKE_UNREADABLE = "Command OS could not read this client's account. Paste its key again in Build a Client Account.";
|
|
43629
44351
|
INTAKE_FORM_REQUIRED = "Run the Intake stage in Client Builds to create the form, then come back and submit.";
|
|
@@ -44115,12 +44837,12 @@ ${companies.map((c) => ` ${c.name || c.companyId} (${c.companyId})`).join("\n")
|
|
|
44115
44837
|
]
|
|
44116
44838
|
};
|
|
44117
44839
|
}
|
|
44118
|
-
const
|
|
44840
|
+
const tokens2 = registry2.listLocations().map((loc) => ({
|
|
44119
44841
|
...loc,
|
|
44120
44842
|
companyId: registry2.getToken(loc.locationId)?.companyId
|
|
44121
44843
|
}));
|
|
44122
44844
|
const currentId = client.defaultLocationId;
|
|
44123
|
-
const lines =
|
|
44845
|
+
const lines = tokens2.map((loc) => {
|
|
44124
44846
|
const marker = loc.locationId === currentId ? "\u2192 " : " ";
|
|
44125
44847
|
const company = loc.companyId ? ` [company ${loc.companyId}${companiesWithFirebase.has(loc.companyId) ? ", Firebase \u2713" : ", no Firebase"}]` : "";
|
|
44126
44848
|
return `${marker}${loc.name} (${loc.locationId})${company}`;
|
|
@@ -44133,7 +44855,7 @@ ${companies.map((c) => ` ${c.name || c.companyId} (${c.companyId})`).join("\n")
|
|
|
44133
44855
|
content: [
|
|
44134
44856
|
{
|
|
44135
44857
|
type: "text",
|
|
44136
|
-
text: `Token Registry (${
|
|
44858
|
+
text: `Token Registry (${tokens2.length} locations):
|
|
44137
44859
|
${lines.join("\n")}
|
|
44138
44860
|
|
|
44139
44861
|
\u2192 = currently active${fbSection}`
|
|
@@ -44288,7 +45010,7 @@ var init_location_switcher = __esm({
|
|
|
44288
45010
|
|
|
44289
45011
|
// src/tools/bulk-operations.ts
|
|
44290
45012
|
function delay2(ms) {
|
|
44291
|
-
return new Promise((
|
|
45013
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
44292
45014
|
}
|
|
44293
45015
|
function formatResults(op, results, total) {
|
|
44294
45016
|
return `${op}: ${results.success} success, ${results.failed} failed out of ${total}.${results.errors.length ? "\nErrors:\n" + results.errors.join("\n") : ""}`;
|
|
@@ -44533,7 +45255,7 @@ var init_asset_ownership = __esm({
|
|
|
44533
45255
|
function sectionItems(section2) {
|
|
44534
45256
|
return Array.isArray(section2) ? section2 : [];
|
|
44535
45257
|
}
|
|
44536
|
-
function
|
|
45258
|
+
function bullet2(lines) {
|
|
44537
45259
|
return lines.length ? lines.map((l) => `- ${l}`).join("\n") : "- None found.";
|
|
44538
45260
|
}
|
|
44539
45261
|
function plainReason(reason3) {
|
|
@@ -44555,7 +45277,7 @@ function sectionList(section2, render) {
|
|
|
44555
45277
|
if (!Array.isArray(section2)) {
|
|
44556
45278
|
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
45279
|
}
|
|
44558
|
-
return
|
|
45280
|
+
return bullet2(section2.map(render));
|
|
44559
45281
|
}
|
|
44560
45282
|
function buildHandoffDoc(input) {
|
|
44561
45283
|
const {
|
|
@@ -44577,13 +45299,13 @@ function buildHandoffDoc(input) {
|
|
|
44577
45299
|
if (!items.length) return "";
|
|
44578
45300
|
return `**${kind}** ${OWNER_MEANING[kind]}
|
|
44579
45301
|
|
|
44580
|
-
${
|
|
45302
|
+
${bullet2(
|
|
44581
45303
|
items.map((i) => i.note ? `${i.asset} (${i.note})` : i.asset)
|
|
44582
45304
|
)}`;
|
|
44583
45305
|
}).filter(Boolean).join("\n\n") : "Nothing has been recorded yet. Every asset below is unanswered.";
|
|
44584
45306
|
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
45307
|
|
|
44586
|
-
${
|
|
45308
|
+
${bullet2(ownership.unknown)}` : "Every asset found in this account has a recorded owner.";
|
|
44587
45309
|
const webhookSection = sectionList(webhooks, (w) => `${w.name} sends to ${w.url}${w.events.length ? ` when: ${w.events.join(", ")}` : ""}`) + (webhookScanPartial ? `
|
|
44588
45310
|
|
|
44589
45311
|
**This list may be incomplete.** ${webhookScanPartial}` : "");
|
|
@@ -44660,7 +45382,7 @@ ${ownership.unreadable.length ? `
|
|
|
44660
45382
|
|
|
44661
45383
|
Somebody edited these by hand and the answer no longer parses. They need a human:
|
|
44662
45384
|
|
|
44663
|
-
${
|
|
45385
|
+
${bullet2(ownership.unreadable)}
|
|
44664
45386
|
` : ""}
|
|
44665
45387
|
## Rotate these on the way out
|
|
44666
45388
|
|
|
@@ -44693,7 +45415,7 @@ var init_handoff_doc = __esm({
|
|
|
44693
45415
|
|
|
44694
45416
|
// src/tools/account-export.ts
|
|
44695
45417
|
function delay3(ms) {
|
|
44696
|
-
return new Promise((
|
|
45418
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
44697
45419
|
}
|
|
44698
45420
|
function builderSectionAvailability(builderLocationId, requestedLocationId) {
|
|
44699
45421
|
if (!builderLocationId) {
|
|
@@ -45408,13 +46130,13 @@ function contactServiceNote(payload) {
|
|
|
45408
46130
|
function registerSmartListTools(server2, builderClient) {
|
|
45409
46131
|
if (!builderClient) return;
|
|
45410
46132
|
const client = builderClient;
|
|
45411
|
-
async function smartListRequest(method,
|
|
45412
|
-
const url = `${SMARTLIST_BASE}${
|
|
46133
|
+
async function smartListRequest(method, path42, body) {
|
|
46134
|
+
const url = `${SMARTLIST_BASE}${path42}`;
|
|
45413
46135
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
45414
46136
|
const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
|
|
45415
46137
|
if (!response.ok) {
|
|
45416
46138
|
const text3 = await response.text();
|
|
45417
|
-
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${
|
|
46139
|
+
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path42}
|
|
45418
46140
|
${text3}`);
|
|
45419
46141
|
}
|
|
45420
46142
|
const text2 = await response.text();
|
|
@@ -45659,11 +46381,11 @@ var init_smart_lists = __esm({
|
|
|
45659
46381
|
function registerReputationTools(server2, builderClient) {
|
|
45660
46382
|
if (!builderClient) return;
|
|
45661
46383
|
const client = builderClient;
|
|
45662
|
-
async function reputationRequest(method,
|
|
45663
|
-
const response = await builderRequest(client, method, `${REPUTATION_BASE}${
|
|
46384
|
+
async function reputationRequest(method, path42) {
|
|
46385
|
+
const response = await builderRequest(client, method, `${REPUTATION_BASE}${path42}`);
|
|
45664
46386
|
if (!response.ok) {
|
|
45665
46387
|
const text3 = await response.text();
|
|
45666
|
-
throw new Error(`Reputation API Error ${response.status}: ${method} ${
|
|
46388
|
+
throw new Error(`Reputation API Error ${response.status}: ${method} ${path42}
|
|
45667
46389
|
${text3}`);
|
|
45668
46390
|
}
|
|
45669
46391
|
const text2 = await response.text();
|
|
@@ -45705,7 +46427,7 @@ ${text3}`);
|
|
|
45705
46427
|
);
|
|
45706
46428
|
}
|
|
45707
46429
|
function buildReviewsQuery(locationId2, opts = {}) {
|
|
45708
|
-
const
|
|
46430
|
+
const q3 = [
|
|
45709
46431
|
`filterParams[locationId][0][value]=${locationId2}`,
|
|
45710
46432
|
`filterParams[locationId][0][condition]=eq`,
|
|
45711
46433
|
`filterParams[deleted][0][value]=${opts.includeDeleted ? "true" : "false"}`,
|
|
@@ -45719,13 +46441,13 @@ function buildReviewsQuery(locationId2, opts = {}) {
|
|
|
45719
46441
|
if (opts.maxRating !== void 0) bounds.push([opts.maxRating, "lte"]);
|
|
45720
46442
|
}
|
|
45721
46443
|
bounds.forEach(([value, condition], i) => {
|
|
45722
|
-
|
|
45723
|
-
|
|
46444
|
+
q3.push(`filterParams[starRating][${i}][value]=${value}`);
|
|
46445
|
+
q3.push(`filterParams[starRating][${i}][condition]=${condition}`);
|
|
45724
46446
|
});
|
|
45725
|
-
|
|
45726
|
-
|
|
45727
|
-
|
|
45728
|
-
const query =
|
|
46447
|
+
q3.push(`sortParams[dateAdded]=-1`);
|
|
46448
|
+
q3.push(`pageNumber=${opts.pageNumber ?? 1}`);
|
|
46449
|
+
q3.push(`pageSize=${opts.pageSize ?? 10}`);
|
|
46450
|
+
const query = q3.map((p) => {
|
|
45729
46451
|
const i = p.indexOf("=");
|
|
45730
46452
|
return `${encodeURIComponent(p.slice(0, i))}=${encodeURIComponent(p.slice(i + 1))}`;
|
|
45731
46453
|
}).join("&");
|
|
@@ -45801,11 +46523,11 @@ var init_email_campaigns = __esm({
|
|
|
45801
46523
|
function registerMembershipTools(server2, builderClient) {
|
|
45802
46524
|
if (!builderClient) return;
|
|
45803
46525
|
const client = builderClient;
|
|
45804
|
-
async function membershipRequest(
|
|
45805
|
-
const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${
|
|
46526
|
+
async function membershipRequest(path42, method = "GET", body) {
|
|
46527
|
+
const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path42}`, body ? { body } : {});
|
|
45806
46528
|
if (!response.ok) {
|
|
45807
46529
|
const text3 = await response.text();
|
|
45808
|
-
throw new Error(`Membership API Error ${response.status}: ${method} ${
|
|
46530
|
+
throw new Error(`Membership API Error ${response.status}: ${method} ${path42}
|
|
45809
46531
|
${text3}`);
|
|
45810
46532
|
}
|
|
45811
46533
|
const text2 = await response.text();
|
|
@@ -45991,28 +46713,28 @@ var init_memberships = __esm({
|
|
|
45991
46713
|
|
|
45992
46714
|
// src/tools/template-deployer.ts
|
|
45993
46715
|
function delay4(ms) {
|
|
45994
|
-
return new Promise((
|
|
46716
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
45995
46717
|
}
|
|
45996
46718
|
function isAlreadyExistsError(message) {
|
|
45997
46719
|
return /already\s+exists?\b/i.test(message) || /duplicate/i.test(message);
|
|
45998
46720
|
}
|
|
45999
46721
|
function registerTemplateDeployerTools(server2, client) {
|
|
46000
46722
|
const builderClient = WorkflowBuilderClient.fromEnv();
|
|
46001
|
-
const templatesDir =
|
|
46723
|
+
const templatesDir = path34.resolve(__dirname, "..", "templates");
|
|
46002
46724
|
function validateTemplatePath(templateFile) {
|
|
46003
|
-
const resolved2 =
|
|
46004
|
-
const realPath =
|
|
46725
|
+
const resolved2 = path34.resolve(templateFile);
|
|
46726
|
+
const realPath = fs36.existsSync(resolved2) ? fs36.realpathSync(resolved2) : resolved2;
|
|
46005
46727
|
const allowedDirs = [
|
|
46006
46728
|
templatesDir,
|
|
46007
|
-
|
|
46008
|
-
|
|
46009
|
-
].map((d) =>
|
|
46010
|
-
const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir +
|
|
46729
|
+
path34.resolve(process.cwd(), "templates"),
|
|
46730
|
+
path34.resolve(__dirname, "..", "..", "templates")
|
|
46731
|
+
].map((d) => fs36.existsSync(d) ? fs36.realpathSync(d) : d);
|
|
46732
|
+
const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir + path34.sep));
|
|
46011
46733
|
if (!isAllowed) {
|
|
46012
46734
|
throw new Error(`Template file must be inside a templates/ directory. Got: ${realPath}`);
|
|
46013
46735
|
}
|
|
46014
|
-
if (
|
|
46015
|
-
const otherMatch = allowedDirs.slice(1).some((d) => !
|
|
46736
|
+
if (path34.relative(allowedDirs[0], realPath).startsWith("..")) {
|
|
46737
|
+
const otherMatch = allowedDirs.slice(1).some((d) => !path34.relative(d, realPath).startsWith(".."));
|
|
46016
46738
|
if (!otherMatch) {
|
|
46017
46739
|
throw new Error(`Template path escapes allowed directory.`);
|
|
46018
46740
|
}
|
|
@@ -46027,26 +46749,26 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
46027
46749
|
try {
|
|
46028
46750
|
const dirs = [
|
|
46029
46751
|
templatesDir,
|
|
46030
|
-
|
|
46031
|
-
|
|
46752
|
+
path34.resolve(process.cwd(), "templates"),
|
|
46753
|
+
path34.resolve(__dirname, "..", "..", "templates")
|
|
46032
46754
|
];
|
|
46033
46755
|
const templates = [];
|
|
46034
46756
|
const seen = /* @__PURE__ */ new Set();
|
|
46035
46757
|
for (const dir of dirs) {
|
|
46036
|
-
if (!
|
|
46037
|
-
const files =
|
|
46758
|
+
if (!fs36.existsSync(dir)) continue;
|
|
46759
|
+
const files = fs36.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
46038
46760
|
for (const file2 of files) {
|
|
46039
|
-
const full =
|
|
46761
|
+
const full = path34.join(dir, file2);
|
|
46040
46762
|
let real;
|
|
46041
46763
|
try {
|
|
46042
|
-
real =
|
|
46764
|
+
real = fs36.realpathSync(full);
|
|
46043
46765
|
} catch {
|
|
46044
46766
|
continue;
|
|
46045
46767
|
}
|
|
46046
46768
|
if (seen.has(real)) continue;
|
|
46047
46769
|
seen.add(real);
|
|
46048
46770
|
try {
|
|
46049
|
-
const content = JSON.parse(
|
|
46771
|
+
const content = JSON.parse(fs36.readFileSync(full, "utf-8"));
|
|
46050
46772
|
if (typeof content.templateName !== "string" || !content.templateName) continue;
|
|
46051
46773
|
const parsed2 = TemplateSchema.safeParse(content);
|
|
46052
46774
|
templates.push({
|
|
@@ -46083,7 +46805,7 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
46083
46805
|
async ({ templateFile }) => {
|
|
46084
46806
|
try {
|
|
46085
46807
|
const safePath = validateTemplatePath(templateFile);
|
|
46086
|
-
const content = JSON.parse(
|
|
46808
|
+
const content = JSON.parse(fs36.readFileSync(safePath, "utf-8"));
|
|
46087
46809
|
return {
|
|
46088
46810
|
content: [
|
|
46089
46811
|
{
|
|
@@ -46123,8 +46845,8 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
46123
46845
|
try {
|
|
46124
46846
|
const locId = client.resolveLocationId(locationId2);
|
|
46125
46847
|
const safePath = validateTemplatePath(templateFile);
|
|
46126
|
-
const template = TemplateSchema.parse(JSON.parse(
|
|
46127
|
-
const
|
|
46848
|
+
const template = TemplateSchema.parse(JSON.parse(fs36.readFileSync(safePath, "utf-8")));
|
|
46849
|
+
const resolve13 = (text2) => {
|
|
46128
46850
|
if (typeof text2 !== "string") return text2;
|
|
46129
46851
|
let result = text2;
|
|
46130
46852
|
for (const [key, value] of Object.entries(answers)) {
|
|
@@ -46137,7 +46859,7 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
46137
46859
|
return result;
|
|
46138
46860
|
};
|
|
46139
46861
|
const resolveObj = (obj) => {
|
|
46140
|
-
if (typeof obj === "string") return
|
|
46862
|
+
if (typeof obj === "string") return resolve13(obj);
|
|
46141
46863
|
if (Array.isArray(obj)) return obj.map(resolveObj);
|
|
46142
46864
|
if (obj && typeof obj === "object") {
|
|
46143
46865
|
const result = {};
|
|
@@ -46495,15 +47217,15 @@ ${errors.join("\n")}` : dryRun ? "\nNo errors!" : "\nNo errors \u2014 every item
|
|
|
46495
47217
|
}
|
|
46496
47218
|
);
|
|
46497
47219
|
}
|
|
46498
|
-
var import_zod71,
|
|
47220
|
+
var import_zod71, fs36, path34, TemplateSchema;
|
|
46499
47221
|
var init_template_deployer = __esm({
|
|
46500
47222
|
"src/tools/template-deployer.ts"() {
|
|
46501
47223
|
"use strict";
|
|
46502
47224
|
import_zod71 = require("zod");
|
|
46503
47225
|
init_workflow_builder_client();
|
|
46504
47226
|
init_tool_helpers();
|
|
46505
|
-
|
|
46506
|
-
|
|
47227
|
+
fs36 = __toESM(require("fs"));
|
|
47228
|
+
path34 = __toESM(require("path"));
|
|
46507
47229
|
init_workflow_builder_client();
|
|
46508
47230
|
TemplateSchema = import_zod71.z.object({
|
|
46509
47231
|
templateName: import_zod71.z.string(),
|
|
@@ -46824,10 +47546,10 @@ async function confirm(run) {
|
|
|
46824
47546
|
async function runProbes(client, loc) {
|
|
46825
47547
|
return Promise.all(
|
|
46826
47548
|
PROBES.map((probe) => confirm(async () => {
|
|
46827
|
-
const
|
|
47549
|
+
const path42 = probe.path.replace("{loc}", loc);
|
|
46828
47550
|
const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
|
|
46829
47551
|
try {
|
|
46830
|
-
const raw = await client.get(
|
|
47552
|
+
const raw = await client.get(path42, { params, noRetry: true });
|
|
46831
47553
|
const count2 = countRows(raw, probe.listKeys);
|
|
46832
47554
|
return {
|
|
46833
47555
|
key: probe.key,
|
|
@@ -47103,14 +47825,14 @@ var init_checkup = __esm({
|
|
|
47103
47825
|
|
|
47104
47826
|
// src/tools/user-guide.ts
|
|
47105
47827
|
function resolveGuidePath() {
|
|
47106
|
-
const bundled =
|
|
47107
|
-
if (
|
|
47108
|
-
const devRoot =
|
|
47828
|
+
const bundled = path35.resolve(__dirname, "..", "guide", "guide.html");
|
|
47829
|
+
if (fs37.existsSync(bundled)) return bundled;
|
|
47830
|
+
const devRoot = path35.resolve(__dirname, "..", "..");
|
|
47109
47831
|
try {
|
|
47110
|
-
const pkg2 = JSON.parse(
|
|
47832
|
+
const pkg2 = JSON.parse(fs37.readFileSync(path35.join(devRoot, "package.json"), "utf8"));
|
|
47111
47833
|
if (pkg2.name === "@elitedcs/ghl-mcp") {
|
|
47112
|
-
const dev =
|
|
47113
|
-
if (
|
|
47834
|
+
const dev = path35.join(devRoot, "guide", "guide.html");
|
|
47835
|
+
if (fs37.existsSync(dev)) return dev;
|
|
47114
47836
|
}
|
|
47115
47837
|
} catch {
|
|
47116
47838
|
}
|
|
@@ -47119,7 +47841,7 @@ function resolveGuidePath() {
|
|
|
47119
47841
|
function guideContents(file2) {
|
|
47120
47842
|
let html2 = "";
|
|
47121
47843
|
try {
|
|
47122
|
-
html2 =
|
|
47844
|
+
html2 = fs37.readFileSync(file2, "utf8");
|
|
47123
47845
|
} catch {
|
|
47124
47846
|
return "The guide is installed but could not be read.";
|
|
47125
47847
|
}
|
|
@@ -47143,7 +47865,7 @@ function registerUserGuideTools(server2) {
|
|
|
47143
47865
|
{ 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
47866
|
async ({ open }) => {
|
|
47145
47867
|
const guidePath = resolveGuidePath();
|
|
47146
|
-
if (!
|
|
47868
|
+
if (!fs37.existsSync(guidePath)) {
|
|
47147
47869
|
return {
|
|
47148
47870
|
content: [{
|
|
47149
47871
|
type: "text",
|
|
@@ -47176,14 +47898,14 @@ ${guidePath}`;
|
|
|
47176
47898
|
}
|
|
47177
47899
|
);
|
|
47178
47900
|
}
|
|
47179
|
-
var import_child_process6, import_zod73,
|
|
47901
|
+
var import_child_process6, import_zod73, fs37, path35;
|
|
47180
47902
|
var init_user_guide = __esm({
|
|
47181
47903
|
"src/tools/user-guide.ts"() {
|
|
47182
47904
|
"use strict";
|
|
47183
47905
|
import_child_process6 = require("child_process");
|
|
47184
47906
|
import_zod73 = require("zod");
|
|
47185
|
-
|
|
47186
|
-
|
|
47907
|
+
fs37 = __toESM(require("fs"));
|
|
47908
|
+
path35 = __toESM(require("path"));
|
|
47187
47909
|
}
|
|
47188
47910
|
});
|
|
47189
47911
|
|
|
@@ -47244,7 +47966,8 @@ function registerAgencyProfileTools(server2) {
|
|
|
47244
47966
|
defaults: import_zod74.z.object({
|
|
47245
47967
|
timezone: import_zod74.z.string().optional(),
|
|
47246
47968
|
industry: import_zod74.z.string().optional()
|
|
47247
|
-
}).optional()
|
|
47969
|
+
}).optional(),
|
|
47970
|
+
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
47971
|
},
|
|
47249
47972
|
async (args) => {
|
|
47250
47973
|
const patch = Object.fromEntries(
|
|
@@ -47822,7 +48545,7 @@ function blockContrast(t, fallbackBg) {
|
|
|
47822
48545
|
if (!fg || !bg) return null;
|
|
47823
48546
|
return Math.round(contrastHex(fg, bg) * 100) / 100;
|
|
47824
48547
|
}
|
|
47825
|
-
function
|
|
48548
|
+
function inside2(inner, outer, inset = 0) {
|
|
47826
48549
|
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
48550
|
}
|
|
47828
48551
|
function clusterBoxes(boxes, gap = 16) {
|
|
@@ -47917,7 +48640,7 @@ function checkSlides(probe, pixels, ctx, rendered) {
|
|
|
47917
48640
|
const bad = [];
|
|
47918
48641
|
for (const s of slides) {
|
|
47919
48642
|
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) => !
|
|
48643
|
+
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
48644
|
if (outside.length) bad.push(`${slideName(s)}: ${outside.slice(0, 2).map((o) => o.what).join(", ")} inside the ${inset} px band`);
|
|
47922
48645
|
}
|
|
47923
48646
|
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 +48862,7 @@ function checkSlides(probe, pixels, ctx, rendered) {
|
|
|
48139
48862
|
const l = logos[0];
|
|
48140
48863
|
if (l) {
|
|
48141
48864
|
if (l.box.h > 28) bad.push(`${slideName(s)} logo ${Math.round(l.box.h)} px tall`);
|
|
48142
|
-
if (!
|
|
48865
|
+
if (!inside2(l.box, s.box, inset)) bad.push(`${slideName(s)} logo inside the safe band`);
|
|
48143
48866
|
} else if (byHook && ctx.logoSig) bad.push(`${slideName(s)} has a data-role="logo" element but not the stored logo's bytes (conformance)`);
|
|
48144
48867
|
}
|
|
48145
48868
|
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 +49422,20 @@ function checkConversion(p, ctx, rendered) {
|
|
|
48699
49422
|
invented.push(`quote attributed to ${t.name} that they did not give`);
|
|
48700
49423
|
}
|
|
48701
49424
|
}
|
|
48702
|
-
const
|
|
49425
|
+
const digits = (x) => x.replace(/[,\s]/g, "");
|
|
48703
49426
|
const STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "from", "our", "over", "of", "in", "on", "a", "an", "we", "us", "have", "been"]);
|
|
48704
49427
|
const unitAfter = (text2, at) => norm4(text2.slice(at)).split(" ").slice(1).filter((w) => w && !STOP.has(w) && !/^[\d,.]+$/.test(w)).slice(0, 2);
|
|
48705
49428
|
const briefFigures = b.proof.aggregates.flatMap((agg) => {
|
|
48706
49429
|
const line3 = `${agg.value} ${agg.label}`;
|
|
48707
|
-
return [...line3.matchAll(/\d[\d,.]*/g)].map((m) => ({ n:
|
|
49430
|
+
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
49431
|
});
|
|
48709
49432
|
const seenClaims = /* @__PURE__ */ new Set();
|
|
48710
49433
|
const claimed = (text2) => {
|
|
48711
49434
|
for (const m of text2.matchAll(/\d[\d,.]*/g)) {
|
|
48712
|
-
const key = `${
|
|
49435
|
+
const key = `${digits(m[0])}@${norm4(text2).slice(0, 40)}`;
|
|
48713
49436
|
if (seenClaims.has(key)) continue;
|
|
48714
49437
|
seenClaims.add(key);
|
|
48715
|
-
const hit = briefFigures.find((f) => f.n ===
|
|
49438
|
+
const hit = briefFigures.find((f) => f.n === digits(m[0]));
|
|
48716
49439
|
if (!hit) {
|
|
48717
49440
|
invented.push(`figure "${m[0]}" in "${text2.slice(0, 30)}"`);
|
|
48718
49441
|
continue;
|
|
@@ -49393,9 +50116,9 @@ async function settle(page) {
|
|
|
49393
50116
|
}
|
|
49394
50117
|
async function readTarget(target) {
|
|
49395
50118
|
if (isUrl(target)) return { html: null, url: target };
|
|
49396
|
-
const abs =
|
|
49397
|
-
if (!
|
|
49398
|
-
return { html:
|
|
50119
|
+
const abs = path36.resolve(target);
|
|
50120
|
+
if (!fs38.existsSync(abs)) throw new Error(`No file at ${abs}.`);
|
|
50121
|
+
return { html: fs38.readFileSync(abs, "utf8"), url: (0, import_node_url.pathToFileURL)(abs).toString() };
|
|
49399
50122
|
}
|
|
49400
50123
|
function slidePixels(index, png, file2, sec, brand) {
|
|
49401
50124
|
let img;
|
|
@@ -49443,7 +50166,7 @@ async function verifyDesign(opts) {
|
|
|
49443
50166
|
const { html: html2, url } = await readTarget(opts.target);
|
|
49444
50167
|
const placement = opts.placement ?? "html";
|
|
49445
50168
|
const medium = opts.medium;
|
|
49446
|
-
|
|
50169
|
+
fs38.mkdirSync(opts.outDir, { recursive: true });
|
|
49447
50170
|
const files = [];
|
|
49448
50171
|
const screenshots = [];
|
|
49449
50172
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -49488,8 +50211,8 @@ async function verifyDesign(opts) {
|
|
|
49488
50211
|
try {
|
|
49489
50212
|
await loc.scrollIntoViewIfNeeded({ timeout: 5e3 });
|
|
49490
50213
|
const png = await loc.screenshot({ type: "png", timeout: 15e3 });
|
|
49491
|
-
|
|
49492
|
-
files.push(
|
|
50214
|
+
fs38.writeFileSync(path36.join(opts.outDir, file2), png);
|
|
50215
|
+
files.push(path36.join(opts.outDir, file2));
|
|
49493
50216
|
screenshots.push(file2);
|
|
49494
50217
|
pixels.push(slidePixels(sec.index, png, file2, sec, opts.brand));
|
|
49495
50218
|
} catch {
|
|
@@ -49521,8 +50244,8 @@ async function verifyDesign(opts) {
|
|
|
49521
50244
|
const shot = "page-1440.png";
|
|
49522
50245
|
try {
|
|
49523
50246
|
const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
|
|
49524
|
-
|
|
49525
|
-
files.push(
|
|
50247
|
+
fs38.writeFileSync(path36.join(opts.outDir, shot), png);
|
|
50248
|
+
files.push(path36.join(opts.outDir, shot));
|
|
49526
50249
|
screenshots.push(shot);
|
|
49527
50250
|
} catch {
|
|
49528
50251
|
}
|
|
@@ -49530,8 +50253,8 @@ async function verifyDesign(opts) {
|
|
|
49530
50253
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
49531
50254
|
await page.evaluate(SETTLE_SCROLL_JS).catch(() => void 0);
|
|
49532
50255
|
const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
|
|
49533
|
-
|
|
49534
|
-
files.push(
|
|
50256
|
+
fs38.writeFileSync(path36.join(opts.outDir, "page-390.png"), png);
|
|
50257
|
+
files.push(path36.join(opts.outDir, "page-390.png"));
|
|
49535
50258
|
screenshots.push("page-390.png");
|
|
49536
50259
|
} catch {
|
|
49537
50260
|
}
|
|
@@ -49604,21 +50327,21 @@ async function verifyDesign(opts) {
|
|
|
49604
50327
|
screenshots
|
|
49605
50328
|
};
|
|
49606
50329
|
const checked = VerificationSchema.parse(report2);
|
|
49607
|
-
const jsonPath =
|
|
49608
|
-
|
|
50330
|
+
const jsonPath = path36.join(opts.outDir, "verification.json");
|
|
50331
|
+
fs38.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
|
|
49609
50332
|
files.push(jsonPath);
|
|
49610
50333
|
const reportHtml = renderVerificationHtml(checked, opts.branding);
|
|
49611
|
-
const htmlPath =
|
|
49612
|
-
|
|
50334
|
+
const htmlPath = path36.join(opts.outDir, "verification.html");
|
|
50335
|
+
fs38.writeFileSync(htmlPath, reportHtml);
|
|
49613
50336
|
files.push(htmlPath);
|
|
49614
50337
|
return { report: checked, files, reportHtml };
|
|
49615
50338
|
}
|
|
49616
|
-
var
|
|
50339
|
+
var fs38, path36, import_node_url, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS;
|
|
49617
50340
|
var init_verify = __esm({
|
|
49618
50341
|
"src/command-os/design/verify.ts"() {
|
|
49619
50342
|
"use strict";
|
|
49620
|
-
|
|
49621
|
-
|
|
50343
|
+
fs38 = __toESM(require("node:fs"));
|
|
50344
|
+
path36 = __toESM(require("node:path"));
|
|
49622
50345
|
import_node_url = require("node:url");
|
|
49623
50346
|
init_lint();
|
|
49624
50347
|
init_probe();
|
|
@@ -50037,7 +50760,7 @@ function registerDesignStudioTools(server2) {
|
|
|
50037
50760
|
const { dna, logoSvg, logoPng } = deriveBrandDna(raw, { name, slug: slug3, rights: { declaration: a.rights, at: (/* @__PURE__ */ new Date()).toISOString() }, browser, firecrawl: fc });
|
|
50038
50761
|
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
50762
|
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) =>
|
|
50763
|
+
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
50764
|
}
|
|
50042
50765
|
);
|
|
50043
50766
|
safeTool(
|
|
@@ -50141,14 +50864,14 @@ function registerDesignStudioTools(server2) {
|
|
|
50141
50864
|
d.provenance.evidence["shape.radius"] = `set by ${stamp}`;
|
|
50142
50865
|
}
|
|
50143
50866
|
if (a.logoSvg) {
|
|
50144
|
-
let
|
|
50867
|
+
let clean2;
|
|
50145
50868
|
try {
|
|
50146
|
-
|
|
50869
|
+
clean2 = sanitizeSvg(a.logoSvg);
|
|
50147
50870
|
} catch (e) {
|
|
50148
50871
|
throw new Error(`${e.message} Nothing was changed.`);
|
|
50149
50872
|
}
|
|
50150
|
-
a.logoSvg =
|
|
50151
|
-
d.logo = { inlineSvg:
|
|
50873
|
+
a.logoSvg = clean2;
|
|
50874
|
+
d.logo = { inlineSvg: clean2, whiteFillFixed: false, source: `set by ${stamp}`, rights: "declared" };
|
|
50152
50875
|
d.provenance.evidence["logo"] = `set by ${stamp}`;
|
|
50153
50876
|
}
|
|
50154
50877
|
if (a.tone) d.voice.tone = a.tone;
|
|
@@ -50204,9 +50927,9 @@ function registerDesignStudioTools(server2) {
|
|
|
50204
50927
|
"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
50928
|
{ file: import_zod76.z.string().describe("Path to the .html file."), medium: import_zod76.z.enum(MEDIA) },
|
|
50206
50929
|
async ({ file: file2, medium }) => {
|
|
50207
|
-
const abs =
|
|
50208
|
-
if (!
|
|
50209
|
-
const r = lintDesignContract(
|
|
50930
|
+
const abs = path37.resolve(file2);
|
|
50931
|
+
if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
|
|
50932
|
+
const r = lintDesignContract(fs39.readFileSync(abs, "utf8"), medium);
|
|
50210
50933
|
return { ok: r.ok, problems: r.problems, warnings: r.warnings, facts: r.facts };
|
|
50211
50934
|
}
|
|
50212
50935
|
);
|
|
@@ -50227,7 +50950,7 @@ function registerDesignStudioTools(server2) {
|
|
|
50227
50950
|
async (a) => {
|
|
50228
50951
|
const brand = a.brandSlug ? houseLooks().find((d) => d.slug === a.brandSlug) ?? readBrand(slugify(a.brandSlug)) : void 0;
|
|
50229
50952
|
const isUrl2 = /^https?:\/\//i.test(a.target);
|
|
50230
|
-
const outDir = a.outDir ?? (isUrl2 ?
|
|
50953
|
+
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
50954
|
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
50955
|
const v = r.report;
|
|
50233
50956
|
const failed = v.rules.filter((x) => x.state === "MEASURED-FAIL").map((x) => `${x.ruleId} ${x.name}: ${x.measured ?? ""}`);
|
|
@@ -50242,7 +50965,7 @@ function registerDesignStudioTools(server2) {
|
|
|
50242
50965
|
judgments: v.rules.filter((x) => x.state === "ASSERTED").map((x) => `${x.ruleId} ${x.name}: ${x.detail ?? ""}`),
|
|
50243
50966
|
notApplicable: v.rules.filter((x) => x.state === "N-A").map((x) => `${x.ruleId}: ${x.detail ?? ""}`),
|
|
50244
50967
|
brandMatch: v.brandMatch,
|
|
50245
|
-
report:
|
|
50968
|
+
report: path37.join(outDir, "verification.html"),
|
|
50246
50969
|
files: r.files,
|
|
50247
50970
|
...a.placement === "ghl" && { builderOwned: builderOwnedRules().map((b) => `${b.rule.id} ${b.rule.name}: ${b.why}`) }
|
|
50248
50971
|
};
|
|
@@ -50270,9 +50993,9 @@ function registerDesignStudioTools(server2) {
|
|
|
50270
50993
|
if (!a.pageDataDownloadUrl && !a.file) throw new Error("Pass pageDataDownloadUrl (from get_page_full) or file.");
|
|
50271
50994
|
let page;
|
|
50272
50995
|
if (a.file) {
|
|
50273
|
-
const abs =
|
|
50274
|
-
if (!
|
|
50275
|
-
page = JSON.parse(
|
|
50996
|
+
const abs = path37.resolve(a.file);
|
|
50997
|
+
if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
|
|
50998
|
+
page = JSON.parse(fs39.readFileSync(abs, "utf8"));
|
|
50276
50999
|
} else {
|
|
50277
51000
|
const response = await fetch(a.pageDataDownloadUrl, { headers: { Accept: "application/json,*/*" } });
|
|
50278
51001
|
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 +51036,9 @@ function registerDesignStudioTools(server2) {
|
|
|
50313
51036
|
if (!a.file && !a.text) throw new Error("Pass file or text.");
|
|
50314
51037
|
let html2 = a.text ?? "";
|
|
50315
51038
|
if (a.file) {
|
|
50316
|
-
const abs =
|
|
50317
|
-
if (!
|
|
50318
|
-
html2 =
|
|
51039
|
+
const abs = path37.resolve(a.file);
|
|
51040
|
+
if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
|
|
51041
|
+
html2 = fs39.readFileSync(abs, "utf8");
|
|
50319
51042
|
}
|
|
50320
51043
|
const r = await gradeCopy(html2, claudeGenerate(a.model), { icp: a.icp });
|
|
50321
51044
|
if (!r.grade) {
|
|
@@ -50350,13 +51073,13 @@ function registerDesignStudioTools(server2) {
|
|
|
50350
51073
|
}
|
|
50351
51074
|
);
|
|
50352
51075
|
}
|
|
50353
|
-
var import_zod76,
|
|
51076
|
+
var import_zod76, fs39, path37, summarise2;
|
|
50354
51077
|
var init_design_studio = __esm({
|
|
50355
51078
|
"src/tools/design-studio.ts"() {
|
|
50356
51079
|
"use strict";
|
|
50357
51080
|
import_zod76 = require("zod");
|
|
50358
|
-
|
|
50359
|
-
|
|
51081
|
+
fs39 = __toESM(require("node:fs"));
|
|
51082
|
+
path37 = __toESM(require("node:path"));
|
|
50360
51083
|
init_tool_helpers();
|
|
50361
51084
|
init_extract();
|
|
50362
51085
|
init_extract_browser();
|
|
@@ -51080,11 +51803,11 @@ var init_audit = __esm({
|
|
|
51080
51803
|
// src/command-os/delivery-audit/store.ts
|
|
51081
51804
|
function deliveryAuditPath() {
|
|
51082
51805
|
const override = process.env[DELIVERY_AUDIT_ENV];
|
|
51083
|
-
return override && override.trim() ? override :
|
|
51806
|
+
return override && override.trim() ? override : path38.join(appDataDir(), "delivery-audit.json");
|
|
51084
51807
|
}
|
|
51085
51808
|
function readDeliveryAudit() {
|
|
51086
51809
|
try {
|
|
51087
|
-
const parsed2 = JSON.parse(
|
|
51810
|
+
const parsed2 = JSON.parse(fs40.readFileSync(deliveryAuditPath(), "utf-8"));
|
|
51088
51811
|
if (!parsed2 || typeof parsed2 !== "object" || !parsed2.answers || typeof parsed2.answers !== "object") return { ...EMPTY3 };
|
|
51089
51812
|
return { v: 1, answers: parsed2.answers, updated_at: parsed2.updated_at };
|
|
51090
51813
|
} catch {
|
|
@@ -51095,7 +51818,7 @@ function readForWrite2() {
|
|
|
51095
51818
|
const target = deliveryAuditPath();
|
|
51096
51819
|
let raw;
|
|
51097
51820
|
try {
|
|
51098
|
-
raw =
|
|
51821
|
+
raw = fs40.readFileSync(target, "utf-8");
|
|
51099
51822
|
} catch (e) {
|
|
51100
51823
|
if (e?.code === "ENOENT") return { ...EMPTY3 };
|
|
51101
51824
|
throw new Error("Your saved audit could not be read, so nothing was changed. Nothing was lost.");
|
|
@@ -51107,7 +51830,7 @@ function readForWrite2() {
|
|
|
51107
51830
|
} catch {
|
|
51108
51831
|
const kept = `${target}.unreadable-${Date.now()}`;
|
|
51109
51832
|
try {
|
|
51110
|
-
|
|
51833
|
+
fs40.copyFileSync(target, kept);
|
|
51111
51834
|
} catch {
|
|
51112
51835
|
}
|
|
51113
51836
|
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 +51842,13 @@ function writeFile2(f) {
|
|
|
51119
51842
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
51120
51843
|
let renamed = false;
|
|
51121
51844
|
try {
|
|
51122
|
-
|
|
51123
|
-
|
|
51845
|
+
fs40.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
51846
|
+
fs40.renameSync(tmp, target);
|
|
51124
51847
|
renamed = true;
|
|
51125
51848
|
} finally {
|
|
51126
51849
|
if (!renamed) {
|
|
51127
51850
|
try {
|
|
51128
|
-
|
|
51851
|
+
fs40.unlinkSync(tmp);
|
|
51129
51852
|
} catch {
|
|
51130
51853
|
}
|
|
51131
51854
|
}
|
|
@@ -51146,12 +51869,12 @@ function resetDeliveryAudit(now = () => /* @__PURE__ */ new Date()) {
|
|
|
51146
51869
|
writeFile2(next);
|
|
51147
51870
|
return next;
|
|
51148
51871
|
}
|
|
51149
|
-
var
|
|
51872
|
+
var fs40, path38, EMPTY3, DELIVERY_AUDIT_ENV;
|
|
51150
51873
|
var init_store4 = __esm({
|
|
51151
51874
|
"src/command-os/delivery-audit/store.ts"() {
|
|
51152
51875
|
"use strict";
|
|
51153
|
-
|
|
51154
|
-
|
|
51876
|
+
fs40 = __toESM(require("node:fs"));
|
|
51877
|
+
path38 = __toESM(require("node:path"));
|
|
51155
51878
|
init_credentials_store();
|
|
51156
51879
|
EMPTY3 = { v: 1, answers: {} };
|
|
51157
51880
|
DELIVERY_AUDIT_ENV = "GHL_MCP_DELIVERY_AUDIT";
|
|
@@ -51472,7 +52195,7 @@ function position(a) {
|
|
|
51472
52195
|
const cyclic = a.steps.length > 0 && check.order.length === 0;
|
|
51473
52196
|
const order = check.order.length ? check.order : a.steps.map((s) => s.id);
|
|
51474
52197
|
const lastAttempt = /* @__PURE__ */ new Map();
|
|
51475
|
-
for (const x of [...a.applied].sort((p,
|
|
52198
|
+
for (const x of [...a.applied].sort((p, q3) => p.at < q3.at ? -1 : p.at > q3.at ? 1 : 0)) {
|
|
51476
52199
|
lastAttempt.set(x.stepId, x);
|
|
51477
52200
|
}
|
|
51478
52201
|
const doneIds = new Set([...lastAttempt.values()].filter((x) => x.state === "applied").map((x) => x.stepId));
|
|
@@ -51515,7 +52238,7 @@ function position(a) {
|
|
|
51515
52238
|
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
52239
|
} else if (failed.length) {
|
|
51517
52240
|
phase = "stalled";
|
|
51518
|
-
const f = failed.reduce((p,
|
|
52241
|
+
const f = failed.reduce((p, q3) => q3.at > p.at ? q3 : p);
|
|
51519
52242
|
stalledOn = { step: f.stepId, because: f.because ?? "no reason was recorded" };
|
|
51520
52243
|
const rest = total - done - 1;
|
|
51521
52244
|
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 +52330,7 @@ var init_automation = __esm({
|
|
|
51607
52330
|
// src/command-os/build/store.ts
|
|
51608
52331
|
function automationsPath() {
|
|
51609
52332
|
const override = process.env[AUTOMATIONS_ENV];
|
|
51610
|
-
return override && override.trim() ? override :
|
|
52333
|
+
return override && override.trim() ? override : path39.join(appDataDir(), "automations.json");
|
|
51611
52334
|
}
|
|
51612
52335
|
function parse4(raw) {
|
|
51613
52336
|
const p = JSON.parse(raw);
|
|
@@ -51616,7 +52339,7 @@ function parse4(raw) {
|
|
|
51616
52339
|
}
|
|
51617
52340
|
function readAutomations() {
|
|
51618
52341
|
try {
|
|
51619
|
-
return parse4(
|
|
52342
|
+
return parse4(fs41.readFileSync(automationsPath(), "utf-8")).builds;
|
|
51620
52343
|
} catch {
|
|
51621
52344
|
return { ...EMPTY4.builds };
|
|
51622
52345
|
}
|
|
@@ -51625,7 +52348,7 @@ function readForWrite3() {
|
|
|
51625
52348
|
const target = automationsPath();
|
|
51626
52349
|
let raw;
|
|
51627
52350
|
try {
|
|
51628
|
-
raw =
|
|
52351
|
+
raw = fs41.readFileSync(target, "utf-8");
|
|
51629
52352
|
} catch (e) {
|
|
51630
52353
|
if (e?.code === "ENOENT") return { v: 1, builds: {} };
|
|
51631
52354
|
throw new Error("Your builds file could not be read, so nothing was changed. Nothing was lost.");
|
|
@@ -51635,7 +52358,7 @@ function readForWrite3() {
|
|
|
51635
52358
|
} catch {
|
|
51636
52359
|
const kept = `${target}.unreadable-${Date.now()}`;
|
|
51637
52360
|
try {
|
|
51638
|
-
|
|
52361
|
+
fs41.copyFileSync(target, kept);
|
|
51639
52362
|
} catch {
|
|
51640
52363
|
}
|
|
51641
52364
|
throw new Error(
|
|
@@ -51649,13 +52372,13 @@ function writeFile3(f) {
|
|
|
51649
52372
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
51650
52373
|
let renamed = false;
|
|
51651
52374
|
try {
|
|
51652
|
-
|
|
51653
|
-
|
|
52375
|
+
fs41.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
52376
|
+
fs41.renameSync(tmp, target);
|
|
51654
52377
|
renamed = true;
|
|
51655
52378
|
} finally {
|
|
51656
52379
|
if (!renamed) {
|
|
51657
52380
|
try {
|
|
51658
|
-
|
|
52381
|
+
fs41.unlinkSync(tmp);
|
|
51659
52382
|
} catch {
|
|
51660
52383
|
}
|
|
51661
52384
|
}
|
|
@@ -51681,12 +52404,12 @@ function newAutomationId(now = () => /* @__PURE__ */ new Date()) {
|
|
|
51681
52404
|
}
|
|
51682
52405
|
return id;
|
|
51683
52406
|
}
|
|
51684
|
-
var
|
|
52407
|
+
var fs41, path39, AUTOMATIONS_ENV, EMPTY4;
|
|
51685
52408
|
var init_store5 = __esm({
|
|
51686
52409
|
"src/command-os/build/store.ts"() {
|
|
51687
52410
|
"use strict";
|
|
51688
|
-
|
|
51689
|
-
|
|
52411
|
+
fs41 = __toESM(require("node:fs"));
|
|
52412
|
+
path39 = __toESM(require("node:path"));
|
|
51690
52413
|
init_credentials_store();
|
|
51691
52414
|
AUTOMATIONS_ENV = "GHL_MCP_AUTOMATIONS";
|
|
51692
52415
|
EMPTY4 = { v: 1, builds: {} };
|
|
@@ -52220,15 +52943,15 @@ var init_agency_build = __esm({
|
|
|
52220
52943
|
|
|
52221
52944
|
// src/tools/client-engagements.ts
|
|
52222
52945
|
function resolveClient(who, roster) {
|
|
52223
|
-
const
|
|
52224
|
-
if (!
|
|
52225
|
-
const byId2 = roster.find((r) => r.locationId ===
|
|
52946
|
+
const q3 = who.trim();
|
|
52947
|
+
if (!q3) return { error: "Name who you mean \u2014 either the way you refer to them, or their account id." };
|
|
52948
|
+
const byId2 = roster.find((r) => r.locationId === q3);
|
|
52226
52949
|
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(
|
|
52950
|
+
const hits = roster.filter((r) => r.name?.trim().toLowerCase() === q3.toLowerCase());
|
|
52951
|
+
const loose2 = hits.length ? hits : roster.filter((r) => r.name?.toLowerCase().includes(q3.toLowerCase()));
|
|
52229
52952
|
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 "${
|
|
52953
|
+
if (loose2.length > 1) return { error: `More than one match for "${q3}": ${loose2.map((r) => r.name).join(", ")}. Use the full name.` };
|
|
52954
|
+
return { error: `Nothing called "${q3}" is on your board or in your records.` };
|
|
52232
52955
|
}
|
|
52233
52956
|
function line2(locationId2, e, roster, profile) {
|
|
52234
52957
|
const boardName = roster.find((r) => r.locationId === locationId2)?.name;
|
|
@@ -52483,9 +53206,9 @@ function looksLikeRawId(word) {
|
|
|
52483
53206
|
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
53207
|
if (/^[0-9a-f]{24}$/.test(word)) return true;
|
|
52485
53208
|
if (!/^[A-Za-z0-9]{17,}$/.test(word)) return false;
|
|
52486
|
-
const
|
|
53209
|
+
const digits = (word.match(/\d/g) ?? []).length;
|
|
52487
53210
|
const letters = (word.match(/[A-Za-z]/g) ?? []).length;
|
|
52488
|
-
if (
|
|
53211
|
+
if (digits < 2 || letters < 2) return false;
|
|
52489
53212
|
const longestLetterRun = Math.max(0, ...(word.match(/[a-z]+|[A-Z]+/g) ?? []).map((r) => r.length));
|
|
52490
53213
|
return longestLetterRun <= 3;
|
|
52491
53214
|
}
|
|
@@ -53532,7 +54255,7 @@ function planRevert(record, opts) {
|
|
|
53532
54255
|
function errText(e) {
|
|
53533
54256
|
return e instanceof Error ? e.message : String(e);
|
|
53534
54257
|
}
|
|
53535
|
-
async function executeRevert(plan, deps, wait = (ms) => new Promise((
|
|
54258
|
+
async function executeRevert(plan, deps, wait = (ms) => new Promise((resolve13) => setTimeout(resolve13, ms))) {
|
|
53536
54259
|
const removed = [];
|
|
53537
54260
|
const refused = [];
|
|
53538
54261
|
const attempted = [];
|
|
@@ -53651,8 +54374,8 @@ function renderDryRun(plan, opts = {}) {
|
|
|
53651
54374
|
const children2 = withParent.filter((w) => w.object.parentRef && g.items.some((i) => i.object.ref === w.object.parentRef));
|
|
53652
54375
|
if (children2.length) {
|
|
53653
54376
|
const kind = children2[0].object.type;
|
|
53654
|
-
const
|
|
53655
|
-
L.push(` (plus the ${children2.length} ${PLURAL[kind].toLowerCase()} ${
|
|
54377
|
+
const inside3 = g.items.length === 1 ? "inside it" : "inside them";
|
|
54378
|
+
L.push(` (plus the ${children2.length} ${PLURAL[kind].toLowerCase()} ${inside3}, which GoHighLevel removes at the same time)`);
|
|
53656
54379
|
}
|
|
53657
54380
|
}
|
|
53658
54381
|
const kept = [...bound, ...cannot, ...gone];
|
|
@@ -54845,11 +55568,11 @@ function extractFormFields(formFull) {
|
|
|
54845
55568
|
const fields = Array.isArray(inner.fields) ? inner.fields : [];
|
|
54846
55569
|
return fields.filter((f) => !!f && typeof f === "object");
|
|
54847
55570
|
}
|
|
54848
|
-
function findRecordForQuestion(
|
|
54849
|
-
const wantKey = expectedFieldKey(
|
|
55571
|
+
function findRecordForQuestion(q3, records) {
|
|
55572
|
+
const wantKey = expectedFieldKey(q3).toLowerCase();
|
|
54850
55573
|
const byKey = records.find((r) => r.fieldKey.toLowerCase() === wantKey);
|
|
54851
55574
|
if (byKey) return byKey;
|
|
54852
|
-
const wantName = intakeFieldName(
|
|
55575
|
+
const wantName = intakeFieldName(q3.label).toLowerCase();
|
|
54853
55576
|
return records.find((r) => r.name.toLowerCase() === wantName);
|
|
54854
55577
|
}
|
|
54855
55578
|
function isFormNotYetPropagated(error) {
|
|
@@ -55074,13 +55797,13 @@ ${text3}`);
|
|
|
55074
55797
|
};
|
|
55075
55798
|
}
|
|
55076
55799
|
function makePipelineApi(builderClient) {
|
|
55077
|
-
return async (method,
|
|
55078
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
55800
|
+
return async (method, path42, body) => {
|
|
55801
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path42}`;
|
|
55079
55802
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
55080
55803
|
const response = await builderRequest(builderClient, method, url, sendsBody ? { body } : {});
|
|
55081
55804
|
if (!response.ok) {
|
|
55082
55805
|
const text3 = await response.text();
|
|
55083
|
-
throw new Error(`Pipeline API ${response.status}: ${method} ${
|
|
55806
|
+
throw new Error(`Pipeline API ${response.status}: ${method} ${path42}
|
|
55084
55807
|
${text3}`);
|
|
55085
55808
|
}
|
|
55086
55809
|
const text2 = await response.text();
|
|
@@ -55093,8 +55816,8 @@ ${text3}`);
|
|
|
55093
55816
|
};
|
|
55094
55817
|
}
|
|
55095
55818
|
function makeFunnelApi(builderClient) {
|
|
55096
|
-
return async (method,
|
|
55097
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
55819
|
+
return async (method, path42, body) => {
|
|
55820
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path42}`;
|
|
55098
55821
|
const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
|
|
55099
55822
|
const response = await builderRequest(builderClient, method, url, {
|
|
55100
55823
|
...sendsBody ? { body } : {},
|
|
@@ -55107,7 +55830,7 @@ function makeFunnelApi(builderClient) {
|
|
|
55107
55830
|
});
|
|
55108
55831
|
if (!response.ok) {
|
|
55109
55832
|
const text3 = await response.text();
|
|
55110
|
-
throw new Error(`Funnel API ${response.status}: ${method} ${
|
|
55833
|
+
throw new Error(`Funnel API ${response.status}: ${method} ${path42}
|
|
55111
55834
|
${text3.slice(0, 300)}`);
|
|
55112
55835
|
}
|
|
55113
55836
|
const text2 = await response.text();
|
|
@@ -55474,17 +56197,17 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
55474
56197
|
industry: industryPack(industry)?.slug,
|
|
55475
56198
|
intake: describeComposition({ industry, agency }),
|
|
55476
56199
|
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:
|
|
56200
|
+
questions: questions.map((q3) => ({
|
|
56201
|
+
key: q3.key,
|
|
56202
|
+
label: q3.label,
|
|
56203
|
+
section: q3.sectionLabel,
|
|
56204
|
+
required: q3.required,
|
|
56205
|
+
fieldKind: q3.field.kind,
|
|
56206
|
+
dataType: q3.field.kind === "custom" ? q3.field.dataType : void 0,
|
|
56207
|
+
standardTag: q3.field.kind === "standard" ? q3.field.tag : void 0,
|
|
56208
|
+
expectedFieldKey: q3.field.kind === "custom" ? expectedFieldKey(q3) : void 0,
|
|
56209
|
+
options: q3.options,
|
|
56210
|
+
briefPath: q3.briefPath
|
|
55488
56211
|
}))
|
|
55489
56212
|
};
|
|
55490
56213
|
}
|
|
@@ -56088,26 +56811,26 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
56088
56811
|
if (!builderClient) return;
|
|
56089
56812
|
const bc = builderClient;
|
|
56090
56813
|
async function resolveCustomFields(locationId2, dryRun, questions) {
|
|
56091
|
-
const cqs = questions.filter((
|
|
56814
|
+
const cqs = questions.filter((q3) => q3.field.kind === "custom");
|
|
56092
56815
|
const existing = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
|
|
56093
56816
|
const created = [];
|
|
56094
56817
|
const reused = [];
|
|
56095
|
-
for (const
|
|
56096
|
-
const found = findRecordForQuestion(
|
|
56818
|
+
for (const q3 of cqs) {
|
|
56819
|
+
const found = findRecordForQuestion(q3, existing);
|
|
56097
56820
|
if (found) {
|
|
56098
|
-
reused.push(
|
|
56821
|
+
reused.push(q3.label);
|
|
56099
56822
|
} else {
|
|
56100
|
-
created.push(
|
|
56823
|
+
created.push(q3.label);
|
|
56101
56824
|
if (!dryRun) {
|
|
56102
56825
|
const body = {
|
|
56103
|
-
name: intakeFieldName(
|
|
56104
|
-
dataType:
|
|
56826
|
+
name: intakeFieldName(q3.label),
|
|
56827
|
+
dataType: q3.field.kind === "custom" ? q3.field.dataType : "TEXT",
|
|
56105
56828
|
model: "contact"
|
|
56106
56829
|
};
|
|
56107
|
-
if (
|
|
56108
|
-
const dt =
|
|
56830
|
+
if (q3.options && q3.field.kind === "custom") {
|
|
56831
|
+
const dt = q3.field.dataType;
|
|
56109
56832
|
if (dt === "SINGLE_OPTIONS" || dt === "MULTIPLE_OPTIONS" || dt === "CHECKBOX") {
|
|
56110
|
-
body.options = [...
|
|
56833
|
+
body.options = [...q3.options];
|
|
56111
56834
|
}
|
|
56112
56835
|
}
|
|
56113
56836
|
await client.post(`/locations/${locationId2}/customFields`, { body });
|
|
@@ -56121,13 +56844,13 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
56121
56844
|
const after = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
|
|
56122
56845
|
resolved2.clear();
|
|
56123
56846
|
missing = void 0;
|
|
56124
|
-
for (const
|
|
56125
|
-
const rec = findRecordForQuestion(
|
|
56847
|
+
for (const q3 of cqs) {
|
|
56848
|
+
const rec = findRecordForQuestion(q3, after);
|
|
56126
56849
|
if (!rec) {
|
|
56127
|
-
missing =
|
|
56850
|
+
missing = q3;
|
|
56128
56851
|
break;
|
|
56129
56852
|
}
|
|
56130
|
-
resolved2.set(
|
|
56853
|
+
resolved2.set(q3.key, rec);
|
|
56131
56854
|
}
|
|
56132
56855
|
if (!missing) break;
|
|
56133
56856
|
if (attempt < 6) await sleep6(700 * attempt);
|
|
@@ -56177,7 +56900,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
56177
56900
|
industry: pack?.slug,
|
|
56178
56901
|
intake,
|
|
56179
56902
|
questionCount: questions.length,
|
|
56180
|
-
standardFields: questions.filter((
|
|
56903
|
+
standardFields: questions.filter((q3) => q3.field.kind === "standard").map((q3) => q3.label),
|
|
56181
56904
|
customFieldsToCreate: created,
|
|
56182
56905
|
customFieldsToReuse: reused,
|
|
56183
56906
|
note: "No changes written. Re-run without dryRun to install."
|
|
@@ -56422,7 +57145,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env2 = process
|
|
|
56422
57145
|
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
57146
|
}
|
|
56424
57147
|
const reader = token2 ? new GHLClient({ apiKey: token2.apiKey, locationId: loc }) : client;
|
|
56425
|
-
return refuseUnseenFindings2((
|
|
57148
|
+
return refuseUnseenFindings2((path42, params) => reader.get(path42, params ? { params } : void 0), loc, findings);
|
|
56426
57149
|
}, () => client.defaultLocationId);
|
|
56427
57150
|
registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
|
|
56428
57151
|
registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
|
|
@@ -56842,8 +57565,8 @@ __export(index_exports, {
|
|
|
56842
57565
|
});
|
|
56843
57566
|
module.exports = __toCommonJS(index_exports);
|
|
56844
57567
|
var dotenv2 = __toESM(require("dotenv"));
|
|
56845
|
-
var
|
|
56846
|
-
var
|
|
57568
|
+
var path41 = __toESM(require("path"));
|
|
57569
|
+
var fs43 = __toESM(require("fs"));
|
|
56847
57570
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
56848
57571
|
|
|
56849
57572
|
// src/version-compare.ts
|
|
@@ -56915,8 +57638,8 @@ var import_zod87 = require("zod");
|
|
|
56915
57638
|
|
|
56916
57639
|
// src/skill-installer.ts
|
|
56917
57640
|
var import_node_crypto13 = require("node:crypto");
|
|
56918
|
-
var
|
|
56919
|
-
var
|
|
57641
|
+
var fs42 = __toESM(require("node:fs"));
|
|
57642
|
+
var path40 = __toESM(require("node:path"));
|
|
56920
57643
|
var os7 = __toESM(require("node:os"));
|
|
56921
57644
|
function sha2562(buf) {
|
|
56922
57645
|
return (0, import_node_crypto13.createHash)("sha256").update(buf).digest("hex");
|
|
@@ -56931,9 +57654,9 @@ function isOlderRelease(candidate, installed) {
|
|
|
56931
57654
|
return false;
|
|
56932
57655
|
}
|
|
56933
57656
|
function bundledSkillsDir(baseDir) {
|
|
56934
|
-
const candidate =
|
|
57657
|
+
const candidate = path40.resolve(baseDir, "..", "skills");
|
|
56935
57658
|
try {
|
|
56936
|
-
return
|
|
57659
|
+
return fs42.statSync(candidate).isDirectory() ? candidate : null;
|
|
56937
57660
|
} catch {
|
|
56938
57661
|
return null;
|
|
56939
57662
|
}
|
|
@@ -56941,10 +57664,10 @@ function bundledSkillsDir(baseDir) {
|
|
|
56941
57664
|
function listFiles(root) {
|
|
56942
57665
|
const out = [];
|
|
56943
57666
|
const walk = (dir) => {
|
|
56944
|
-
for (const entry of
|
|
56945
|
-
const full =
|
|
57667
|
+
for (const entry of fs42.readdirSync(dir, { withFileTypes: true })) {
|
|
57668
|
+
const full = path40.join(dir, entry.name);
|
|
56946
57669
|
if (entry.isDirectory()) walk(full);
|
|
56947
|
-
else if (entry.isFile()) out.push(
|
|
57670
|
+
else if (entry.isFile()) out.push(path40.relative(root, full));
|
|
56948
57671
|
}
|
|
56949
57672
|
};
|
|
56950
57673
|
walk(root);
|
|
@@ -56952,7 +57675,7 @@ function listFiles(root) {
|
|
|
56952
57675
|
}
|
|
56953
57676
|
function readMarker(markerPath) {
|
|
56954
57677
|
try {
|
|
56955
|
-
const parsed2 = JSON.parse(
|
|
57678
|
+
const parsed2 = JSON.parse(fs42.readFileSync(markerPath, "utf8"));
|
|
56956
57679
|
return parsed2 && typeof parsed2 === "object" && parsed2.files ? parsed2 : null;
|
|
56957
57680
|
} catch {
|
|
56958
57681
|
return null;
|
|
@@ -56966,35 +57689,35 @@ function installBundledSkills(opts) {
|
|
|
56966
57689
|
skippedOlderPackage: [],
|
|
56967
57690
|
unchanged: [],
|
|
56968
57691
|
errors: [],
|
|
56969
|
-
targetDir: opts.targetDir ??
|
|
57692
|
+
targetDir: opts.targetDir ?? path40.join(os7.homedir(), ".claude", "skills"),
|
|
56970
57693
|
bundledDir: null
|
|
56971
57694
|
};
|
|
56972
57695
|
const bundled = bundledSkillsDir(opts.baseDir);
|
|
56973
57696
|
result.bundledDir = bundled;
|
|
56974
57697
|
if (!bundled) return result;
|
|
56975
57698
|
try {
|
|
56976
|
-
|
|
57699
|
+
fs42.mkdirSync(result.targetDir, { recursive: true });
|
|
56977
57700
|
} catch (e) {
|
|
56978
57701
|
result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
|
|
56979
57702
|
return result;
|
|
56980
57703
|
}
|
|
56981
|
-
const markerPath =
|
|
57704
|
+
const markerPath = path40.join(result.targetDir, ".ghl-command-skills.json");
|
|
56982
57705
|
const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
|
|
56983
57706
|
const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
|
|
56984
57707
|
for (const rel of listFiles(bundled)) {
|
|
56985
57708
|
try {
|
|
56986
|
-
const src =
|
|
56987
|
-
const dest =
|
|
57709
|
+
const src = fs42.readFileSync(path40.join(bundled, rel));
|
|
57710
|
+
const dest = path40.join(result.targetDir, rel);
|
|
56988
57711
|
const srcHash = sha2562(src);
|
|
56989
57712
|
let destBuf = null;
|
|
56990
57713
|
try {
|
|
56991
|
-
destBuf =
|
|
57714
|
+
destBuf = fs42.readFileSync(dest);
|
|
56992
57715
|
} catch {
|
|
56993
57716
|
destBuf = null;
|
|
56994
57717
|
}
|
|
56995
57718
|
if (destBuf === null) {
|
|
56996
|
-
|
|
56997
|
-
|
|
57719
|
+
fs42.mkdirSync(path40.dirname(dest), { recursive: true });
|
|
57720
|
+
fs42.writeFileSync(dest, src);
|
|
56998
57721
|
newMarker.files[rel] = srcHash;
|
|
56999
57722
|
result.installed.push(rel);
|
|
57000
57723
|
continue;
|
|
@@ -57011,7 +57734,7 @@ function installBundledSkills(opts) {
|
|
|
57011
57734
|
newMarker.packageVersion = marker.packageVersion;
|
|
57012
57735
|
result.skippedOlderPackage.push(rel);
|
|
57013
57736
|
} else if (lastWritten && destHash === lastWritten) {
|
|
57014
|
-
|
|
57737
|
+
fs42.writeFileSync(dest, src);
|
|
57015
57738
|
newMarker.files[rel] = srcHash;
|
|
57016
57739
|
result.updated.push(rel);
|
|
57017
57740
|
} else {
|
|
@@ -57022,7 +57745,7 @@ function installBundledSkills(opts) {
|
|
|
57022
57745
|
}
|
|
57023
57746
|
}
|
|
57024
57747
|
try {
|
|
57025
|
-
|
|
57748
|
+
fs42.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
|
|
57026
57749
|
} catch (e) {
|
|
57027
57750
|
result.errors.push(`marker write failed: ${String(e)}`);
|
|
57028
57751
|
}
|
|
@@ -57178,7 +57901,7 @@ dotenv2.config();
|
|
|
57178
57901
|
setPkgVersion(pkg.version);
|
|
57179
57902
|
{
|
|
57180
57903
|
const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
|
|
57181
|
-
if (configDirOverride && !
|
|
57904
|
+
if (configDirOverride && !path41.isAbsolute(configDirOverride)) {
|
|
57182
57905
|
process.stderr.write(
|
|
57183
57906
|
`[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
57907
|
`
|
|
@@ -57191,20 +57914,20 @@ process.on("unhandledRejection", (reason3) => {
|
|
|
57191
57914
|
`);
|
|
57192
57915
|
});
|
|
57193
57916
|
function hardenSecretFilePerms() {
|
|
57194
|
-
const repoDir =
|
|
57917
|
+
const repoDir = path41.resolve(__dirname, "..");
|
|
57195
57918
|
const candidates = [
|
|
57196
|
-
{ file:
|
|
57919
|
+
{ file: path41.join(repoDir, "start-mcp.sh"), mode: 448 },
|
|
57197
57920
|
// Legacy registry location (pre-migration); new location lives in app-data.
|
|
57198
|
-
{ file:
|
|
57921
|
+
{ file: path41.join(repoDir, ".ghl-tokens.json"), mode: 384 },
|
|
57199
57922
|
{ file: tokenRegistryPath(), mode: 384 }
|
|
57200
57923
|
];
|
|
57201
57924
|
for (const { file: file2, mode } of candidates) {
|
|
57202
57925
|
let current;
|
|
57203
57926
|
try {
|
|
57204
|
-
if (!
|
|
57205
|
-
current =
|
|
57927
|
+
if (!fs43.existsSync(file2)) continue;
|
|
57928
|
+
current = fs43.statSync(file2).mode & 511;
|
|
57206
57929
|
if (current !== mode) {
|
|
57207
|
-
|
|
57930
|
+
fs43.chmodSync(file2, mode);
|
|
57208
57931
|
}
|
|
57209
57932
|
} catch (error) {
|
|
57210
57933
|
const message = error instanceof Error ? error.message : String(error);
|