@elitedcs/ghl-mcp 3.84.0 → 3.84.1
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 +35 -0
- package/README.md +1 -1
- package/dist/index.js +929 -228
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9483,12 +9483,24 @@ function boundNumbers(agent) {
|
|
|
9483
9483
|
}
|
|
9484
9484
|
return [...out];
|
|
9485
9485
|
}
|
|
9486
|
+
function hasSingleNumberField(agent) {
|
|
9487
|
+
const a = isRecord7(agent) ? agent : {};
|
|
9488
|
+
return a.inboundNumber !== null && a.inboundNumber !== void 0 && a.inboundNumber !== "";
|
|
9489
|
+
}
|
|
9490
|
+
function routedNumbers(agent) {
|
|
9491
|
+
const a = isRecord7(agent) ? agent : {};
|
|
9492
|
+
if (hasSingleNumberField(a) || !Array.isArray(a.inboundNumbers)) return [];
|
|
9493
|
+
return [...new Set(a.inboundNumbers.filter((n) => typeof n === "string" && n.length > 0))];
|
|
9494
|
+
}
|
|
9495
|
+
function unprovenNumbers(agent) {
|
|
9496
|
+
return hasSingleNumberField(agent) ? boundNumbers(agent) : [];
|
|
9497
|
+
}
|
|
9486
9498
|
function poolBinding(agent) {
|
|
9487
9499
|
const a = isRecord7(agent) ? agent : {};
|
|
9488
9500
|
return typeof a.numberPoolId === "string" && a.numberPoolId.trim() ? a.numberPoolId : null;
|
|
9489
9501
|
}
|
|
9490
9502
|
function isLiveBound(agent) {
|
|
9491
|
-
return
|
|
9503
|
+
return routedNumbers(agent).length > 0 || poolBinding(agent) !== null;
|
|
9492
9504
|
}
|
|
9493
9505
|
function inBackupRota(agent) {
|
|
9494
9506
|
return isRecord7(agent) && agent.isAgentAsBackupDisabled === false;
|
|
@@ -9498,7 +9510,7 @@ function canTakeCall(agent) {
|
|
|
9498
9510
|
}
|
|
9499
9511
|
function liveDescription(agent) {
|
|
9500
9512
|
const parts = [];
|
|
9501
|
-
const nums =
|
|
9513
|
+
const nums = routedNumbers(agent);
|
|
9502
9514
|
if (nums.length) parts.push(`answers on ${nums.join(", ")}`);
|
|
9503
9515
|
const pool = poolBinding(agent);
|
|
9504
9516
|
if (pool) parts.push(`answers through pool ${pool}`);
|
|
@@ -9506,7 +9518,7 @@ function liveDescription(agent) {
|
|
|
9506
9518
|
return parts.length ? parts.join(", and ") : null;
|
|
9507
9519
|
}
|
|
9508
9520
|
function agentWriteDifferences(sent, stored) {
|
|
9509
|
-
return Object.keys(sent).filter((k) => k !== "locationId" && k !== "inboundNumber" && k !== "numberPoolId" && !sentSubsetOfStored(sent[k], stored[k])).sort();
|
|
9521
|
+
return Object.keys(sent).filter((k) => k !== "locationId" && k !== "inboundNumber" && k !== "inboundNumbers" && k !== "numberPoolId" && !sentSubsetOfStored(sent[k], stored[k])).sort();
|
|
9510
9522
|
}
|
|
9511
9523
|
function poolNumbers(pool) {
|
|
9512
9524
|
if (!isRecord7(pool)) return [];
|
|
@@ -9778,7 +9790,12 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9778
9790
|
const { numbers, pools, poolObjects } = await readNumbers(client, loc);
|
|
9779
9791
|
const agents = await listAllVoiceAgents(client, loc);
|
|
9780
9792
|
const holders = /* @__PURE__ */ new Map();
|
|
9781
|
-
for (const a of agents) for (const n of
|
|
9793
|
+
for (const a of agents) for (const n of routedNumbers(a)) holders.set(n, { agentId: String(a.id), agentName: String(a.agentName), via: "inboundNumber" });
|
|
9794
|
+
const unproven = agents.filter((a) => unprovenNumbers(a).length > 0).map((a) => {
|
|
9795
|
+
const nums = unprovenNumbers(a);
|
|
9796
|
+
return { agentId: String(a.id), agentName: String(a.agentName), numbers: nums, stored: { inboundNumber: a.inboundNumber, inboundNumbers: a.inboundNumbers }, fix: UNPROVEN_BINDING_FIX(String(a.agentName), nums) };
|
|
9797
|
+
});
|
|
9798
|
+
const heldUnproven = new Set(unproven.flatMap((u) => u.numbers));
|
|
9782
9799
|
const poolBound = agents.filter((a) => poolBinding(a) !== null).map((a) => {
|
|
9783
9800
|
const pool = poolBinding(a);
|
|
9784
9801
|
const obj = poolObjects.find((p) => poolId(p) === pool);
|
|
@@ -9788,10 +9805,11 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9788
9805
|
});
|
|
9789
9806
|
const unresolvedPool = poolBound.some((p) => typeof p.poolNumbers === "string");
|
|
9790
9807
|
const numberRows = numbers.map((n) => ({ number: n, answeredBy: holders.get(n) ?? null }));
|
|
9791
|
-
const free = unresolvedPool ? [] : numberRows.filter((r) => !r.answeredBy).map((r) => r.number);
|
|
9808
|
+
const free = unresolvedPool ? [] : numberRows.filter((r) => !r.answeredBy && !heldUnproven.has(r.number)).map((r) => r.number);
|
|
9792
9809
|
const rota = agents.filter(inBackupRota).map((a) => ({ agentId: String(a.id), agentName: String(a.agentName) }));
|
|
9793
9810
|
const ready = numberRows.some((r) => r.answeredBy) || poolBound.length > 0;
|
|
9794
|
-
const
|
|
9811
|
+
const unprovenLine = unproven.length ? ` ${unproven.length} agent(s) carry a number binding NOT proven to route (${unproven.map((u) => `"${u.agentName}" on ${u.numbers.join(", ")}`).join("; ")}): see unprovenBindings for the fix.` : "";
|
|
9812
|
+
const verdict = numbers.length === 0 ? "NOT READY: no phone number on this sub-account." : ready ? `READY: ${numberRows.filter((r) => r.answeredBy).length} number(s) bound to a voice agent${poolBound.length ? `, and ${poolBound.length} agent(s) answering through a number pool` : ""}${rota.length ? `; ${rota.length} agent(s) also sit in the backup rota` : ""}.${unprovenLine}` : unproven.length ? `NOT PROVEN TO ROUTE: no number is bound in the shape GoHighLevel routes to an agent.${unprovenLine}${rota.length ? ` ${rota.length} agent(s) sit in the backup rota (${rota.map((r) => r.agentName).join(", ")}) and may be handed unanswered calls.` : ""}` : `NUMBER PRESENT, NO AGENT BOUND BY API: ${numbers.length} number(s), none carried in inboundNumbers or as numberPoolId by any voice agent.${rota.length ? ` ${rota.length} agent(s) sit in the backup rota (${rota.map((r) => r.agentName).join(", ")}) and may be handed unanswered calls.` : ""} Calls may still reach an agent through the number's own routing, which this API cannot read.`;
|
|
9795
9813
|
return {
|
|
9796
9814
|
locationId: loc,
|
|
9797
9815
|
verdict,
|
|
@@ -9800,10 +9818,11 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9800
9818
|
freeNumbersNote: unresolvedPool ? "No number is called free: at least one agent answers through a pool whose numbers this API does not list. Check the pool in GoHighLevel before binding anything." : void 0,
|
|
9801
9819
|
pools,
|
|
9802
9820
|
poolBoundAgents: poolBound,
|
|
9821
|
+
...unproven.length ? { unprovenBindings: unproven } : {},
|
|
9803
9822
|
backupRotaAgents: rota,
|
|
9804
|
-
agents: agents.map((a) => ({ id: a.id, agentName: a.agentName, businessName: a.businessName, answersOn:
|
|
9823
|
+
agents: agents.map((a) => ({ id: a.id, agentName: a.agentName, businessName: a.businessName, answersOn: routedNumbers(a), ...unprovenNumbers(a).length ? { heldNotProvenToRoute: unprovenNumbers(a) } : {}, pool: poolBinding(a), inBackupRota: inBackupRota(a), canTakeCall: canTakeCall(a), language: a.language, maxCallDuration: a.maxCallDuration, actions: Array.isArray(a.actions) ? a.actions.length : 0 })),
|
|
9805
9824
|
manualTask: numbers.length === 0 ? NO_NUMBER_TASK : void 0,
|
|
9806
|
-
note: "This reads what the API exposes: each agent's
|
|
9825
|
+
note: "This reads what the API exposes: each agent's inboundNumbers and numberPoolId, and whether it sits in the backup rota (isAgentAsBackupDisabled false). A number counts as answered only when it is in an agent's inboundNumbers while that agent's inboundNumber is null, the shape GoHighLevel's Deploy tab stores and the one that routed a real call (2026-09-15); a number carried in inboundNumber did not. Two things it cannot see: routing set on the number itself in GoHighLevel's Phone Numbers screen, and A2P/SHAKEN-STIR status in Trust Center \u2014 check both there before promising real caller traffic. One real account seen while this was built answers calls with agents that carry no inboundNumber by API (read 2026-09-04)."
|
|
9807
9826
|
};
|
|
9808
9827
|
}
|
|
9809
9828
|
);
|
|
@@ -9839,7 +9858,9 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9839
9858
|
const loc = client.resolveLocationId(locationId2);
|
|
9840
9859
|
const agent = await readVoiceAgent(client, loc, agentId);
|
|
9841
9860
|
const live = liveDescription(agent);
|
|
9842
|
-
|
|
9861
|
+
const unproven = unprovenNumbers(agent);
|
|
9862
|
+
const unprovenNote = unproven.length ? `Its number binding is not proven to route: ${UNPROVEN_BINDING_FIX(String(agent.agentName), unproven)}` : "";
|
|
9863
|
+
return { agent, answersOn: routedNumbers(agent), ...unproven.length ? { heldNotProvenToRoute: unproven } : {}, pool: poolBinding(agent), inBackupRota: inBackupRota(agent), note: live ? `This agent can take calls: it ${live}.${unprovenNote ? ` ${unprovenNote}` : ""}` : unprovenNote || UNBOUND_NOTE };
|
|
9843
9864
|
}
|
|
9844
9865
|
);
|
|
9845
9866
|
safeTool(
|
|
@@ -9852,11 +9873,12 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9852
9873
|
businessName: import_zod37.z.string().min(1).describe("The business it answers for, as it should say it."),
|
|
9853
9874
|
...(({ businessName: _b, isAgentAsBackupDisabled: _k, ...rest }) => rest)(AGENT_FIELDS),
|
|
9854
9875
|
inboundNumber: import_zod37.z.string().nullable().optional().describe("REFUSED here. Creation never binds a number; create, read the agent, then bind with update_voice_agent."),
|
|
9876
|
+
inboundNumbers: import_zod37.z.array(import_zod37.z.string()).optional().describe("REFUSED here, like inboundNumber. Creation never binds a number; bind with update_voice_agent afterwards."),
|
|
9855
9877
|
numberPoolId: import_zod37.z.string().nullable().optional().describe("REFUSED here. Creation never binds a pool; use update_voice_agent afterwards."),
|
|
9856
9878
|
isAgentAsBackupDisabled: import_zod37.z.boolean().optional().describe("Always true at creation. false is REFUSED here; let the agent into the backup rota with update_voice_agent once you have read it.")
|
|
9857
9879
|
},
|
|
9858
|
-
async ({ locationId: locationId2, inboundNumber, numberPoolId, isAgentAsBackupDisabled, ...fields }) => {
|
|
9859
|
-
if (inboundNumber !== void 0 || numberPoolId !== void 0) throw new Error("create_voice_agent never binds a number or a number pool \u2014 an agent is born non-live so nothing half-built can take a call. Create it, read it with get_voice_agent, then bind with update_voice_agent (inboundNumber or numberPoolId), which checks the number is this sub-account's and free.");
|
|
9880
|
+
async ({ locationId: locationId2, inboundNumber, inboundNumbers, numberPoolId, isAgentAsBackupDisabled, ...fields }) => {
|
|
9881
|
+
if (inboundNumber !== void 0 || inboundNumbers !== void 0 || numberPoolId !== void 0) throw new Error("create_voice_agent never binds a number or a number pool \u2014 an agent is born non-live so nothing half-built can take a call. Create it, read it with get_voice_agent, then bind with update_voice_agent (inboundNumber or numberPoolId), which checks the number is this sub-account's and free.");
|
|
9860
9882
|
if (isAgentAsBackupDisabled === false) throw new Error("create_voice_agent always keeps the new agent OUT of the backup rota (isAgentAsBackupDisabled true). Let it in with update_voice_agent once you have read the agent.");
|
|
9861
9883
|
const client = shared.pinned();
|
|
9862
9884
|
const loc = client.resolveLocationId(locationId2);
|
|
@@ -9864,6 +9886,7 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9864
9886
|
resolveResponsiveness(body);
|
|
9865
9887
|
if (body.maxCallDuration === void 0) body.maxCallDuration = DEFAULT_MAX_CALL_SECONDS;
|
|
9866
9888
|
delete body.inboundNumber;
|
|
9889
|
+
delete body.inboundNumbers;
|
|
9867
9890
|
delete body.numberPoolId;
|
|
9868
9891
|
body.isAgentAsBackupDisabled = true;
|
|
9869
9892
|
const problem = agentBodyProblem(body);
|
|
@@ -9899,13 +9922,13 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9899
9922
|
safeTool(
|
|
9900
9923
|
server2,
|
|
9901
9924
|
"update_voice_agent",
|
|
9902
|
-
"Change any part of a Voice AI agent, including the two things creation never does: bind a number (inboundNumber, or numberPoolId) and let it into the backup rota (isAgentAsBackupDisabled: false). Send only what changes: GoHighLevel applies it as a partial update and the rest survives (read back and diffed here). A number must be this sub-account's and free; inboundNumber: null unbinds. Post-call workflows must be published. Returns what changed, the fields that differ before \u2192 after, and the agent as it now reads.",
|
|
9925
|
+
"Change any part of a Voice AI agent, including the two things creation never does: bind a number (inboundNumber, or numberPoolId) and let it into the backup rota (isAgentAsBackupDisabled: false). Send only what changes: GoHighLevel applies it as a partial update and the rest survives (read back and diffed here). A number must be this sub-account's and free. The number is written to the agent's inboundNumbers list with inboundNumber cleared, the shape GoHighLevel's Deploy tab stores and the one that routed a real call; it is VERIFIED only when it reads back that way. inboundNumber: null unbinds every number. Re-binding an agent's own number repairs a binding check_voice_ai_readiness calls not proven to route. Post-call workflows must be published. Returns what changed, the fields that differ before \u2192 after, and the agent as it now reads.",
|
|
9903
9926
|
{
|
|
9904
9927
|
agentId: import_zod37.z.string().describe("Agent id."),
|
|
9905
9928
|
locationId: LOCATION,
|
|
9906
9929
|
agentName: import_zod37.z.string().min(1).max(40).optional(),
|
|
9907
9930
|
...AGENT_FIELDS,
|
|
9908
|
-
inboundNumber: import_zod37.z.string().nullable().optional().describe("Bind a number this sub-account owns that no other agent answers on
|
|
9931
|
+
inboundNumber: import_zod37.z.string().nullable().optional().describe("Bind a number this sub-account owns that no other agent answers on. Stored in the agent's inboundNumbers list beside any it already answers on, with GoHighLevel's single inboundNumber field cleared. null unbinds every number."),
|
|
9909
9932
|
numberPoolId: import_zod37.z.string().nullable().optional().describe("Bind a number pool; null unbinds.")
|
|
9910
9933
|
},
|
|
9911
9934
|
async ({ agentId, locationId: locationId2, inboundNumber, numberPoolId, ...fields }) => {
|
|
@@ -9913,10 +9936,17 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9913
9936
|
const loc = client.resolveLocationId(locationId2);
|
|
9914
9937
|
const change = dropUndefined({ ...fields });
|
|
9915
9938
|
resolveResponsiveness(change);
|
|
9916
|
-
if (inboundNumber !== void 0) change.inboundNumber = inboundNumber;
|
|
9917
9939
|
if (numberPoolId !== void 0) change.numberPoolId = numberPoolId;
|
|
9918
|
-
if (Object.keys(change).length === 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
|
|
9940
|
+
if (Object.keys(change).length === 0 && inboundNumber === void 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
|
|
9919
9941
|
const before = await readVoiceAgent(client, loc, agentId);
|
|
9942
|
+
if (typeof inboundNumber === "string") {
|
|
9943
|
+
const current = Array.isArray(before.inboundNumbers) ? before.inboundNumbers.filter((n) => typeof n === "string" && n.length > 0) : [];
|
|
9944
|
+
change.inboundNumbers = [.../* @__PURE__ */ new Set([...current, inboundNumber])];
|
|
9945
|
+
if (hasSingleNumberField(before)) change.inboundNumber = null;
|
|
9946
|
+
} else if (inboundNumber === null) {
|
|
9947
|
+
change.inboundNumber = null;
|
|
9948
|
+
change.inboundNumbers = [];
|
|
9949
|
+
}
|
|
9920
9950
|
const merged = { ...change };
|
|
9921
9951
|
if (change.language !== void 0 || change.translation !== void 0) {
|
|
9922
9952
|
if (merged.language === void 0 && typeof before.language === "string") merged.language = before.language;
|
|
@@ -9968,11 +9998,15 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9968
9998
|
if (typeof inboundNumber === "string" && !boundNumbers(after).includes(inboundNumber)) {
|
|
9969
9999
|
asserted2 = true;
|
|
9970
10000
|
notes.push(`ASSERTED ONLY: ${inboundNumber} was sent but the read-back does not show it bound.`);
|
|
10001
|
+
} else if (typeof inboundNumber === "string" && !routedNumbers(after).includes(inboundNumber)) {
|
|
10002
|
+
asserted2 = true;
|
|
10003
|
+
notes.push(`ASSERTED ONLY: ${inboundNumber} reads back, but not in the shape proven to route a call (inboundNumber ${JSON.stringify(after.inboundNumber)}, inboundNumbers ${JSON.stringify(after.inboundNumbers)}). Check the agent's Deploy tab in GoHighLevel before telling anyone it answers.`);
|
|
9971
10004
|
}
|
|
9972
10005
|
if (inboundNumber === null && boundNumbers(after).length) {
|
|
9973
10006
|
asserted2 = true;
|
|
9974
10007
|
notes.push(`ASSERTED ONLY: unbind was sent but the read-back still shows ${boundNumbers(after).join(", ")}.`);
|
|
9975
10008
|
}
|
|
10009
|
+
if (inboundNumber === void 0 && unprovenNumbers(after).length) notes.push(`Its number binding is not proven to route: ${UNPROVEN_BINDING_FIX(String(after.agentName), unprovenNumbers(after))}`);
|
|
9976
10010
|
if (typeof numberPoolId === "string" && poolBinding(after) !== numberPoolId) {
|
|
9977
10011
|
asserted2 = true;
|
|
9978
10012
|
notes.push(`ASSERTED ONLY: number pool ${numberPoolId} was sent but the read-back shows ${JSON.stringify(after.numberPoolId)}.`);
|
|
@@ -9981,7 +10015,7 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9981
10015
|
asserted2 = true;
|
|
9982
10016
|
notes.push(`ASSERTED ONLY: pool unbind was sent but the read-back still shows pool ${poolBinding(after)}.`);
|
|
9983
10017
|
}
|
|
9984
|
-
const bound =
|
|
10018
|
+
const bound = routedNumbers(after);
|
|
9985
10019
|
const live = liveDescription(after);
|
|
9986
10020
|
if (change.isAgentAsBackupDisabled === false && inBackupRota(after)) notes.push("This agent is now in the backup rota: GoHighLevel may hand it unanswered calls on this sub-account's numbers even with nothing bound.");
|
|
9987
10021
|
if (!canTakeCall(after)) notes.push(UNBOUND_NOTE);
|
|
@@ -10191,7 +10225,7 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
10191
10225
|
}
|
|
10192
10226
|
);
|
|
10193
10227
|
}
|
|
10194
|
-
var import_zod37, VOICE_LANGUAGES, PATIENCE_LEVELS, VOICE_ACTION_TYPES, PATIENCE_TO_RESPONSIVENESS, ACTION_NAME, TRANSFER_NUMBER, IANA_TZ, MAX_CALL_SECONDS, DEFAULT_MAX_CALL_SECONDS, VOICE_AGENT_COMPARABLE, READBACK_FAILED, AGENT_PAGE_SIZE, AGENT_PAGE_CAP, NO_NUMBER_TASK, WorkingHours, PostCallNotify, Translation, AGENT_FIELDS;
|
|
10228
|
+
var import_zod37, VOICE_LANGUAGES, PATIENCE_LEVELS, VOICE_ACTION_TYPES, PATIENCE_TO_RESPONSIVENESS, ACTION_NAME, TRANSFER_NUMBER, IANA_TZ, MAX_CALL_SECONDS, DEFAULT_MAX_CALL_SECONDS, VOICE_AGENT_COMPARABLE, UNPROVEN_BINDING_FIX, READBACK_FAILED, AGENT_PAGE_SIZE, AGENT_PAGE_CAP, NO_NUMBER_TASK, WorkingHours, PostCallNotify, Translation, AGENT_FIELDS;
|
|
10195
10229
|
var init_voice_ai = __esm({
|
|
10196
10230
|
"src/tools/voice-ai.ts"() {
|
|
10197
10231
|
"use strict";
|
|
@@ -10228,6 +10262,7 @@ var init_voice_ai = __esm({
|
|
|
10228
10262
|
"isAgentAsBackupDisabled",
|
|
10229
10263
|
"translation"
|
|
10230
10264
|
];
|
|
10265
|
+
UNPROVEN_BINDING_FIX = (agentName, numbers) => `"${agentName}" carries ${numbers.join(", ")} in a shape GoHighLevel did not route to the agent when tested (a call went to voicemail, 2026-09-15). Rebind it with update_voice_agent (inboundNumber "${numbers[0]}"), or connect the number in the agent's Deploy tab in GoHighLevel, then run check_voice_ai_readiness again.`;
|
|
10231
10266
|
READBACK_FAILED = (what, id, error) => `GoHighLevel accepted the ${what} (${id}) but the read after it failed: ${error.slice(0, 200)}. The write is ASSERTED, not proven. Do NOT repeat the write \u2014 read get_voice_agent first.`;
|
|
10232
10267
|
AGENT_PAGE_SIZE = 50;
|
|
10233
10268
|
AGENT_PAGE_CAP = 100;
|
|
@@ -11773,6 +11808,9 @@ function hexToOklch(hex) {
|
|
|
11773
11808
|
if (!c) throw new Error(`not a colour: ${hex}`);
|
|
11774
11809
|
return rgbToOklch(c);
|
|
11775
11810
|
}
|
|
11811
|
+
function oklchToHex(o) {
|
|
11812
|
+
return toHex(oklchToRgb(o));
|
|
11813
|
+
}
|
|
11776
11814
|
function oklchCss(o) {
|
|
11777
11815
|
return `oklch(${(o.l * 100).toFixed(1)}% ${o.c.toFixed(3)} ${o.h.toFixed(1)})`;
|
|
11778
11816
|
}
|
|
@@ -12182,6 +12220,176 @@ var init_ghl_page = __esm({
|
|
|
12182
12220
|
}
|
|
12183
12221
|
});
|
|
12184
12222
|
|
|
12223
|
+
// src/command-os/design/rendered-palette.ts
|
|
12224
|
+
function kindOf(prop) {
|
|
12225
|
+
if (/background|bgcolor/i.test(prop)) return "background";
|
|
12226
|
+
if (/border|outline|shadow/i.test(prop)) return "border";
|
|
12227
|
+
if (prop === "color") return "text";
|
|
12228
|
+
return "other";
|
|
12229
|
+
}
|
|
12230
|
+
function pageVariables(envelope) {
|
|
12231
|
+
const defs = /* @__PURE__ */ new Map();
|
|
12232
|
+
const fromCss = (css2) => {
|
|
12233
|
+
for (const block of css2.matchAll(/:root\s*\{([^}]*)\}/g)) {
|
|
12234
|
+
for (const m of block[1].matchAll(/--([\w-]+)\s*:\s*([^;]+)/g)) if (!defs.has(m[1])) defs.set(m[1], m[2].trim());
|
|
12235
|
+
}
|
|
12236
|
+
};
|
|
12237
|
+
const root = isRecord8(envelope) ? envelope : {};
|
|
12238
|
+
if (typeof root.pageStyles === "string") fromCss(root.pageStyles);
|
|
12239
|
+
const walk = (node) => {
|
|
12240
|
+
if (Array.isArray(node)) return void node.forEach(walk);
|
|
12241
|
+
if (!isRecord8(node)) return;
|
|
12242
|
+
for (const [k, v] of Object.entries(node)) {
|
|
12243
|
+
if (k.startsWith("--") && typeof v === "string" && !defs.has(k.slice(2))) defs.set(k.slice(2), v.trim());
|
|
12244
|
+
else if (typeof v === "string" && v.includes(":root")) fromCss(v);
|
|
12245
|
+
else walk(v);
|
|
12246
|
+
}
|
|
12247
|
+
};
|
|
12248
|
+
walk(root);
|
|
12249
|
+
return defs;
|
|
12250
|
+
}
|
|
12251
|
+
function resolveValue(raw, defs) {
|
|
12252
|
+
let v = raw.trim();
|
|
12253
|
+
for (let i = 0; i < 10; i++) {
|
|
12254
|
+
const m = /^var\(\s*--([\w-]+)\s*(?:,\s*([^)]*))?\)$/.exec(v);
|
|
12255
|
+
if (!m) return v;
|
|
12256
|
+
const next = defs.get(m[1]) ?? m[2];
|
|
12257
|
+
if (next === void 0) return null;
|
|
12258
|
+
v = next.trim();
|
|
12259
|
+
}
|
|
12260
|
+
return null;
|
|
12261
|
+
}
|
|
12262
|
+
function normaliseColour(value) {
|
|
12263
|
+
if (value === null) return null;
|
|
12264
|
+
if (/^transparent$/i.test(value)) return "transparent";
|
|
12265
|
+
const c = parseColor(value);
|
|
12266
|
+
if (!c) return null;
|
|
12267
|
+
if (c.a === 0) return "transparent";
|
|
12268
|
+
if (c.a < 1) return `rgba(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)}, ${Number(c.a.toFixed(3))})`;
|
|
12269
|
+
return toHex(c);
|
|
12270
|
+
}
|
|
12271
|
+
function renderedPalette(envelope) {
|
|
12272
|
+
const defs = pageVariables(envelope);
|
|
12273
|
+
const paints = [];
|
|
12274
|
+
const walk = (node, path40, prop) => {
|
|
12275
|
+
if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${path40}[${i}]`, prop));
|
|
12276
|
+
if (isRecord8(node)) {
|
|
12277
|
+
for (const [k, x] of Object.entries(node)) {
|
|
12278
|
+
if (DEFINITION_KEYS.has(k) || k.startsWith("--")) continue;
|
|
12279
|
+
walk(x, `${path40}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
|
|
12280
|
+
}
|
|
12281
|
+
return;
|
|
12282
|
+
}
|
|
12283
|
+
if (typeof node !== "string") return;
|
|
12284
|
+
const raw = node.trim();
|
|
12285
|
+
if (!/^var\(/.test(raw) && !COLOUR_LITERAL.test(raw)) return;
|
|
12286
|
+
const resolved2 = resolveValue(raw, defs);
|
|
12287
|
+
const colour = normaliseColour(resolved2);
|
|
12288
|
+
if (colour === null && resolved2 !== null) return;
|
|
12289
|
+
paints.push({ path: path40, prop, kind: kindOf(prop), colour });
|
|
12290
|
+
};
|
|
12291
|
+
walk(envelope, "", "");
|
|
12292
|
+
const face = (name, settingsKey) => {
|
|
12293
|
+
const fromVar = resolveValue(`var(--${name})`, defs);
|
|
12294
|
+
if (fromVar) return fromVar.replace(/['"]/g, "").trim() || null;
|
|
12295
|
+
const settings = envelope?.settings?.settings?.typography?.fonts?.[settingsKey];
|
|
12296
|
+
return typeof settings?.value?.text === "string" && settings.value.text.trim() ? settings.value.text.trim() : null;
|
|
12297
|
+
};
|
|
12298
|
+
return { paints, fonts: { headline: face("headlinefont", "headlineFont"), body: face("contentfont", "contentFont") } };
|
|
12299
|
+
}
|
|
12300
|
+
function contrastLosses(before, after) {
|
|
12301
|
+
const afterByPath = new Map(after.paints.map((p) => [p.path, p.colour]));
|
|
12302
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12303
|
+
for (const p of before.paints) {
|
|
12304
|
+
const to = afterByPath.get(p.path);
|
|
12305
|
+
if (p.kind !== "text" && p.kind !== "background" || !p.colour?.startsWith("#") || !to?.startsWith("#")) continue;
|
|
12306
|
+
const g = groups.get(paintGroup(p.path)) ?? { texts: [], backgrounds: [] };
|
|
12307
|
+
(p.kind === "text" ? g.texts : g.backgrounds).push([p.colour, to]);
|
|
12308
|
+
groups.set(paintGroup(p.path), g);
|
|
12309
|
+
}
|
|
12310
|
+
const out = /* @__PURE__ */ new Map();
|
|
12311
|
+
for (const [where, g] of groups) {
|
|
12312
|
+
for (const [t0, t1] of g.texts) for (const [b0, b1] of g.backgrounds) {
|
|
12313
|
+
const was = contrastHex(t0, b0), now = contrastHex(t1, b1);
|
|
12314
|
+
if (was >= 4.5 && now < 4.5 && !out.has(`${t1} ${b1}`)) out.set(`${t1} ${b1}`, { where, text: t1, background: b1, before: Number(was.toFixed(2)), after: Number(now.toFixed(2)) });
|
|
12315
|
+
}
|
|
12316
|
+
}
|
|
12317
|
+
return [...out.values()];
|
|
12318
|
+
}
|
|
12319
|
+
function repaintReport(before, after) {
|
|
12320
|
+
const a = renderedPalette(before), b = renderedPalette(after);
|
|
12321
|
+
const afterByPath = new Map(b.paints.map((p) => [p.path, p]));
|
|
12322
|
+
const changes = /* @__PURE__ */ new Map();
|
|
12323
|
+
const unchanged = /* @__PURE__ */ new Map();
|
|
12324
|
+
const tally = { background: { uses: 0, changed: 0 }, neutral: { uses: 0, changed: 0 }, text: { uses: 0, changed: 0 } };
|
|
12325
|
+
const coloured = (c) => c.startsWith("#") && hexToOklch(c).c >= CARRIES_COLOUR;
|
|
12326
|
+
for (const p of a.paints) {
|
|
12327
|
+
if (!p.colour || p.colour === "transparent") continue;
|
|
12328
|
+
const q2 = afterByPath.get(p.path);
|
|
12329
|
+
const to = q2?.colour ?? null;
|
|
12330
|
+
const differs = to !== null && looksDifferent(p.colour, to);
|
|
12331
|
+
if (p.kind === "background" || p.kind === "text") {
|
|
12332
|
+
const t = p.kind === "text" ? tally.text : coloured(p.colour) ? tally.background : tally.neutral;
|
|
12333
|
+
t.uses++;
|
|
12334
|
+
if (differs) t.changed++;
|
|
12335
|
+
else {
|
|
12336
|
+
const key = `${p.kind} ${p.colour}`;
|
|
12337
|
+
const u = unchanged.get(key) ?? { kind: p.kind, colour: p.colour, uses: 0 };
|
|
12338
|
+
u.uses++;
|
|
12339
|
+
unchanged.set(key, u);
|
|
12340
|
+
}
|
|
12341
|
+
}
|
|
12342
|
+
if (differs) {
|
|
12343
|
+
const key = `${p.kind} ${p.colour} ${to}`;
|
|
12344
|
+
const c = changes.get(key) ?? { kind: p.kind, from: p.colour, to, uses: 0 };
|
|
12345
|
+
c.uses++;
|
|
12346
|
+
changes.set(key, c);
|
|
12347
|
+
}
|
|
12348
|
+
}
|
|
12349
|
+
const byUses = (xs) => [...xs].sort((x, y) => y.uses - x.uses);
|
|
12350
|
+
const changeList = byUses(changes.values());
|
|
12351
|
+
const unchangedList = byUses(unchanged.values());
|
|
12352
|
+
const fonts = { headline: { from: a.fonts.headline, to: b.fonts.headline }, body: { from: a.fonts.body, to: b.fonts.body } };
|
|
12353
|
+
const fontsChanged = fonts.headline.from !== fonts.headline.to || fonts.body.from !== fonts.body.to;
|
|
12354
|
+
const bg = tally.background;
|
|
12355
|
+
const share = bg.uses ? bg.changed / bg.uses : 0;
|
|
12356
|
+
const verdict = changeList.length === 0 && !fontsChanged ? "unchanged" : share < BARELY_SHARE ? "barely" : "repainted";
|
|
12357
|
+
const list2 = (xs, n = 6) => xs.slice(0, n).map((x) => x.colour ? `${x.colour} on ${x.uses}` : `${x.from} to ${x.to} on ${x.uses}`).join(", ");
|
|
12358
|
+
const bgChanges = changeList.filter((c) => c.kind === "background" && coloured(c.from));
|
|
12359
|
+
const bgKept = unchangedList.filter((u) => u.kind === "background" && coloured(u.colour));
|
|
12360
|
+
const faceLine = (role, f) => f.from === f.to ? `${role} still ${f.from ?? "no set face"}` : `${role} ${f.from ?? "no set face"} to ${f.to ?? "no set face"}`;
|
|
12361
|
+
const colourLine = verdict === "unchanged" ? "Nothing the page paints changed: every background, text colour and face is the template's own." : bg.uses === 0 ? "The visible palette barely changed: the template paints no background in a colour, only white, black and greys, so there was little colour to repaint." : verdict === "barely" ? `The visible palette barely changed: ${bg.changed} of ${bg.uses} coloured backgrounds now look different${bgChanges.length ? ` (${list2(bgChanges)})` : ""}.${bgKept.length ? ` Still in the template's own colours: ${list2(bgKept)}.` : ""}` : `Repainted: ${bg.changed} of ${bg.uses} coloured backgrounds now look different (${list2(bgChanges)}).${bgKept.length ? ` Left looking as they were: ${list2(bgKept)}.` : ""}`;
|
|
12362
|
+
const contrast = contrastLosses(a, b);
|
|
12363
|
+
const summary = [
|
|
12364
|
+
...contrast.length ? [`Harder to read: ${contrast.length} text and background pair(s) that met WCAG AA no longer do (${contrast.slice(0, 4).map((c) => `${c.text} type on ${c.background} is ${c.after}:1, was ${c.before}:1`).join("; ")}).`] : [],
|
|
12365
|
+
colourLine,
|
|
12366
|
+
`White, black and grey backgrounds: ${tally.neutral.changed} of ${tally.neutral.uses} look different.`,
|
|
12367
|
+
`Text colour changed on ${tally.text.changed} of ${tally.text.uses} uses.`,
|
|
12368
|
+
`Faces: ${faceLine("headlines", fonts.headline)}, ${faceLine("body text", fonts.body)}.`
|
|
12369
|
+
].join(" ");
|
|
12370
|
+
return { verdict, summary, backgrounds: bg, neutralBackgrounds: tally.neutral, text: tally.text, changes: changeList, unchanged: unchangedList, fonts, contrast };
|
|
12371
|
+
}
|
|
12372
|
+
var DEFINITION_KEYS, WRAPPER_KEYS, isRecord8, COLOUR_LITERAL, VISIBLE_CHANGE, BARELY_SHARE, CARRIES_COLOUR, paintGroup, looksDifferent;
|
|
12373
|
+
var init_rendered_palette = __esm({
|
|
12374
|
+
"src/command-os/design/rendered-palette.ts"() {
|
|
12375
|
+
"use strict";
|
|
12376
|
+
init_color();
|
|
12377
|
+
DEFINITION_KEYS = /* @__PURE__ */ new Set(["pageStyles", "general", "rootVars", "sectionStyles", "typography", "fontsToLoad", "fontsToLoadForPreview"]);
|
|
12378
|
+
WRAPPER_KEYS = /* @__PURE__ */ new Set(["value", "styles", "extra", "metaData"]);
|
|
12379
|
+
isRecord8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
12380
|
+
COLOUR_LITERAL = /^(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\)|transparent)$/i;
|
|
12381
|
+
VISIBLE_CHANGE = 0.05;
|
|
12382
|
+
BARELY_SHARE = 0.5;
|
|
12383
|
+
CARRIES_COLOUR = 0.02;
|
|
12384
|
+
paintGroup = (path40) => /^\.(sections|popups)\[\d+\]/.exec(path40)?.[0]?.slice(1) ?? "page";
|
|
12385
|
+
looksDifferent = (a, b) => {
|
|
12386
|
+
if (a === b) return false;
|
|
12387
|
+
if (a === "transparent" || b === "transparent" || a.startsWith("rgba") || b.startsWith("rgba")) return a !== b;
|
|
12388
|
+
return deltaEHex(a, b) >= VISIBLE_CHANGE;
|
|
12389
|
+
};
|
|
12390
|
+
}
|
|
12391
|
+
});
|
|
12392
|
+
|
|
12185
12393
|
// src/command-os/design/retheme.ts
|
|
12186
12394
|
function hexHistogram(envelope) {
|
|
12187
12395
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -12194,49 +12402,147 @@ function hexHistogram(envelope) {
|
|
|
12194
12402
|
function isIconFont(family) {
|
|
12195
12403
|
return ICON_FONTS.test(family);
|
|
12196
12404
|
}
|
|
12197
|
-
function
|
|
12198
|
-
const
|
|
12405
|
+
function usageOf(paints) {
|
|
12406
|
+
const u = { background: /* @__PURE__ */ new Map(), text: /* @__PURE__ */ new Map(), all: /* @__PURE__ */ new Map() };
|
|
12407
|
+
const bump = (m, hex) => m.set(hex, (m.get(hex) ?? 0) + 1);
|
|
12408
|
+
for (const p of paints) {
|
|
12409
|
+
if (!p.colour || !p.colour.startsWith("#")) continue;
|
|
12410
|
+
const hex = p.colour.toLowerCase();
|
|
12411
|
+
bump(u.all, hex);
|
|
12412
|
+
if (p.kind === "background") bump(u.background, hex);
|
|
12413
|
+
if (p.kind === "text") bump(u.text, hex);
|
|
12414
|
+
}
|
|
12415
|
+
return u;
|
|
12416
|
+
}
|
|
12417
|
+
function usageOrHexes(envelope) {
|
|
12418
|
+
const u = usageOf(renderedPalette(envelope).paints);
|
|
12419
|
+
if (u.all.size === 0) for (const [hex, n] of hexHistogram(envelope)) u.all.set(hex, n);
|
|
12420
|
+
return u;
|
|
12421
|
+
}
|
|
12422
|
+
function derivePaletteFromUsage(u, fonts) {
|
|
12423
|
+
const sorted = (m) => [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
12424
|
+
const painted = u.background.size + u.text.size > 0;
|
|
12425
|
+
const merged = new Map(u.background);
|
|
12426
|
+
for (const [hex, n] of u.text) merged.set(hex, (merged.get(hex) ?? 0) + n);
|
|
12427
|
+
const primary = painted ? merged : u.all;
|
|
12428
|
+
const hist = sorted(primary);
|
|
12199
12429
|
const undecided = [];
|
|
12200
|
-
const
|
|
12201
|
-
const
|
|
12202
|
-
const
|
|
12430
|
+
const isColoured = (hex) => !isNeutralHex(hex) && hexToOklch(hex).c >= BRAND_CHROMA;
|
|
12431
|
+
const isGreyish = (hex) => isNeutralHex(hex) || hexToOklch(hex).c < BRAND_CHROMA;
|
|
12432
|
+
const firstOf = (pred, ...maps) => {
|
|
12433
|
+
for (const m of maps) {
|
|
12434
|
+
const hit = sorted(m).find(([hex]) => pred(hex));
|
|
12435
|
+
if (hit) return hit[0];
|
|
12436
|
+
}
|
|
12437
|
+
return null;
|
|
12438
|
+
};
|
|
12439
|
+
const accent = painted ? firstOf(isColoured, u.background, u.text) : firstOf(isColoured, primary);
|
|
12203
12440
|
if (!accent) undecided.push("no strongly-coloured hex appears in this page, so it has no accent to swap \u2014 it is already a greyscale design");
|
|
12204
12441
|
const SECOND_COLOUR_DISTANCE = 0.12;
|
|
12205
|
-
const secondary = accent ?
|
|
12206
|
-
const
|
|
12207
|
-
const
|
|
12208
|
-
const surface =
|
|
12209
|
-
const text2 =
|
|
12442
|
+
const secondary = accent ? hist.find(([hex]) => isColoured(hex) && deltaEHex(hex, accent) > SECOND_COLOUR_DISTANCE)?.[0] ?? null : null;
|
|
12443
|
+
const light = (hex) => isGreyish(hex) && hexToOklch(hex).l > 0.85;
|
|
12444
|
+
const dark = (hex) => isGreyish(hex) && hexToOklch(hex).l < 0.4;
|
|
12445
|
+
const surface = painted ? firstOf(light, u.background, primary) : firstOf(light, primary);
|
|
12446
|
+
const text2 = painted ? firstOf(dark, u.text, primary) : firstOf(dark, primary);
|
|
12210
12447
|
if (!surface) undecided.push("no light neutral is used often enough to be the page background");
|
|
12211
12448
|
if (!text2) undecided.push("no dark neutral is used often enough to be the body text colour");
|
|
12212
|
-
const
|
|
12213
|
-
const serialized = JSON.stringify(envelope);
|
|
12214
|
-
const fonts = declaredFonts(envelope).filter((f) => !isIconFont(f)).map((f) => ({ family: f, uses: (serialized.match(new RegExp(`--${varSlug(f)}\\b`, "g")) ?? []).length })).filter((f) => f.uses > 0).sort((a, b) => b.uses - a.uses || a.family.localeCompare(b.family)).map((f) => f.family).slice(0, 2);
|
|
12215
|
-
const tint = hist.find(([hex]) => {
|
|
12449
|
+
const isTint = (hex) => {
|
|
12216
12450
|
if (hex === surface || hex === accent) return false;
|
|
12217
12451
|
const { l } = hexToOklch(hex);
|
|
12218
12452
|
return l > 0.7 && l < 0.97;
|
|
12219
|
-
}
|
|
12220
|
-
|
|
12453
|
+
};
|
|
12454
|
+
const tint = painted ? firstOf(isTint, u.background, primary) : firstOf(isTint, primary);
|
|
12455
|
+
const taken = new Set([accent, surface, text2, secondary, tint].filter((x) => !!x));
|
|
12456
|
+
const tones = painted ? hist.map(([hex]) => hex).filter((hex) => !taken.has(hex) && isGreyish(hex) && hexToOklch(hex).c >= TONE_CHROMA) : [];
|
|
12457
|
+
return { accent, surface, text: text2, secondary, tint, tones, fonts, undecided };
|
|
12458
|
+
}
|
|
12459
|
+
function pageFaces(envelope) {
|
|
12460
|
+
const named = renderedPalette(envelope).fonts;
|
|
12461
|
+
const real = (f) => !!f && !isIconFont(f) && !/^var\(/.test(f);
|
|
12462
|
+
const varSlug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
12463
|
+
const serialized = JSON.stringify(envelope);
|
|
12464
|
+
const byUse = declaredFonts(envelope).filter((f) => !isIconFont(f)).map((f) => ({ family: f, uses: (serialized.match(new RegExp(`--${varSlug(f)}\\b`, "g")) ?? []).length })).filter((f) => f.uses > 0).sort((a, b) => b.uses - a.uses || a.family.localeCompare(b.family)).map((f) => f.family);
|
|
12465
|
+
const ordered = [.../* @__PURE__ */ new Set([...real(named.headline) ? [named.headline] : [], ...real(named.body) ? [named.body] : [], ...byUse])];
|
|
12466
|
+
return ordered.slice(0, 2);
|
|
12467
|
+
}
|
|
12468
|
+
function derivePalette(envelope) {
|
|
12469
|
+
return derivePaletteFromUsage(usageOrHexes(envelope), pageFaces(envelope));
|
|
12470
|
+
}
|
|
12471
|
+
function sectionPairs(envelope) {
|
|
12472
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12473
|
+
for (const p of renderedPalette(envelope).paints) {
|
|
12474
|
+
if (!p.colour?.startsWith("#") || p.kind !== "background" && p.kind !== "text") continue;
|
|
12475
|
+
const key = /^\.(sections|popups)\[\d+\]/.exec(p.path)?.[0] ?? "page";
|
|
12476
|
+
const g = groups.get(key) ?? { where: key.replace(/^\./, ""), backgrounds: /* @__PURE__ */ new Set(), texts: /* @__PURE__ */ new Set() };
|
|
12477
|
+
(p.kind === "background" ? g.backgrounds : g.texts).add(p.colour.toLowerCase());
|
|
12478
|
+
groups.set(key, g);
|
|
12479
|
+
}
|
|
12480
|
+
return [...groups.values()];
|
|
12221
12481
|
}
|
|
12222
12482
|
function rethemeEnvelope(envelope, brand, derived = derivePalette(envelope)) {
|
|
12223
12483
|
const swaps = [];
|
|
12224
12484
|
const skipped = [];
|
|
12225
12485
|
const warnings = [...derived.undecided];
|
|
12226
|
-
|
|
12227
|
-
const
|
|
12486
|
+
const plan = [];
|
|
12487
|
+
const want = (role, from, to, guarded2 = false) => {
|
|
12228
12488
|
if (!from) return void skipped.push(`${role}: the template has none to replace`);
|
|
12229
12489
|
if (from.toLowerCase() === to.toLowerCase()) return void skipped.push(`${role}: the template already uses ${to}`);
|
|
12230
|
-
|
|
12490
|
+
plan.push({ role, from: from.toLowerCase(), to, guarded: guarded2 });
|
|
12491
|
+
};
|
|
12492
|
+
want("accent", derived.accent, brand.colors.accent.hex);
|
|
12493
|
+
want("surface", derived.surface, brand.colors.surface.hex);
|
|
12494
|
+
want("body text", derived.text, brand.colors.fg.hex);
|
|
12495
|
+
if (derived.secondary) want("second colour", derived.secondary, brand.colors.muted.hex);
|
|
12496
|
+
if (derived.tint) want("section tint", derived.tint, brand.pageStudioTheme.primarySoft, true);
|
|
12497
|
+
const brandHue = hexToOklch(brand.colors.accent.hex);
|
|
12498
|
+
for (const tone of derived.tones ?? []) {
|
|
12499
|
+
if (brandHue.c < NEUTRAL_CHROMA) {
|
|
12500
|
+
skipped.push(`tone ${tone}: the brand's accent is a grey, so it has no hue to carry`);
|
|
12501
|
+
continue;
|
|
12502
|
+
}
|
|
12503
|
+
const o = hexToOklch(tone);
|
|
12504
|
+
want("design tone", tone, oklchToHex({ l: o.l, c: o.c, h: brandHue.h }), true);
|
|
12505
|
+
}
|
|
12506
|
+
const target = (hex) => plan.find((x) => x.from === hex)?.to ?? hex;
|
|
12507
|
+
const pairs = sectionPairs(envelope);
|
|
12508
|
+
const losses = () => {
|
|
12509
|
+
const out = [];
|
|
12510
|
+
for (const g of pairs) for (const bg of g.backgrounds) for (const t of g.texts) {
|
|
12511
|
+
const before = contrastHex(t, bg);
|
|
12512
|
+
if (before < 4.5) continue;
|
|
12513
|
+
const after = contrastHex(target(t), target(bg));
|
|
12514
|
+
if (after >= 4.5) continue;
|
|
12515
|
+
out.push({ where: g.where, t, bg, before, after, involved: plan.filter((x) => x.from === t || x.from === bg) });
|
|
12516
|
+
}
|
|
12517
|
+
return out;
|
|
12518
|
+
};
|
|
12519
|
+
const sayLoss = (l) => `${l.where}: ${target(l.t).toUpperCase()} type on ${target(l.bg).toUpperCase()} would be ${l.after.toFixed(2)}:1 (it was ${l.before.toFixed(2)}:1); WCAG AA wants 4.5.`;
|
|
12520
|
+
for (let pass = 0; pass <= plan.length; pass++) {
|
|
12521
|
+
const hit = losses().find((l) => l.involved.some((x) => x.guarded));
|
|
12522
|
+
if (!hit) break;
|
|
12523
|
+
for (const step of hit.involved.filter((x) => x.guarded)) {
|
|
12524
|
+
skipped.push(`${step.role} ${step.from}: left as it was, because ${sayLoss(hit)}`);
|
|
12525
|
+
plan.splice(plan.indexOf(step), 1);
|
|
12526
|
+
}
|
|
12527
|
+
}
|
|
12528
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12529
|
+
for (const l of losses()) {
|
|
12530
|
+
const line3 = `${sayLoss(l)} Kept, because ${l.involved.map((x) => `the ${x.role}`).join(" and ")} ${l.involved.length > 1 ? "are" : "is"} the brand's own colour: check this section before a client sees it.`;
|
|
12531
|
+
if (!seen.has(`${target(l.t)} ${target(l.bg)}`)) warnings.push(line3);
|
|
12532
|
+
seen.add(`${target(l.t)} ${target(l.bg)}`);
|
|
12533
|
+
}
|
|
12534
|
+
let text2 = JSON.stringify(envelope);
|
|
12535
|
+
const colourTokens = [];
|
|
12536
|
+
plan.forEach((step, i) => {
|
|
12537
|
+
const re = new RegExp(escapeRe(step.from), "gi");
|
|
12231
12538
|
const occurrences = (text2.match(re) ?? []).length;
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
if (derived.tint) swap("section tint", derived.tint, brand.pageStudioTheme.primarySoft);
|
|
12539
|
+
if (occurrences === 0) return;
|
|
12540
|
+
const token2 = `\0COLOUR${i}\0`;
|
|
12541
|
+
text2 = text2.replace(re, token2);
|
|
12542
|
+
colourTokens.push({ token: token2, to: step.to });
|
|
12543
|
+
swaps.push({ role: step.role, from: step.from, to: step.to, occurrences });
|
|
12544
|
+
});
|
|
12545
|
+
for (const { token: token2, to } of colourTokens) text2 = text2.split(token2).join(to);
|
|
12240
12546
|
const display = brand.pageStudioTheme.headingFont;
|
|
12241
12547
|
const body = brand.pageStudioTheme.bodyFont;
|
|
12242
12548
|
const slug3 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
@@ -12262,27 +12568,30 @@ function rethemeEnvelope(envelope, brand, derived = derivePalette(envelope)) {
|
|
|
12262
12568
|
`White text on this brand's accent ${brand.colors.accent.hex} is ${onAccent.toFixed(2)}:1 \u2014 WCAG AA wants 4.5. If the template puts white on its buttons they will fail; ${brand.colors.fg.hex} on that accent is ${contrastHex(brand.colors.fg.hex, brand.colors.accent.hex).toFixed(2)}:1.`
|
|
12263
12569
|
);
|
|
12264
12570
|
}
|
|
12265
|
-
|
|
12571
|
+
const painted = JSON.parse(text2);
|
|
12572
|
+
return { envelope: painted, swaps, skipped, warnings, report: repaintReport(envelope, painted) };
|
|
12266
12573
|
}
|
|
12267
12574
|
function derivePaletteAcross(envelopes) {
|
|
12268
|
-
const pooled2 = /* @__PURE__ */ new Map();
|
|
12575
|
+
const pooled2 = { background: /* @__PURE__ */ new Map(), text: /* @__PURE__ */ new Map(), all: /* @__PURE__ */ new Map() };
|
|
12269
12576
|
for (const env2 of envelopes) {
|
|
12270
|
-
|
|
12577
|
+
const u = usageOrHexes(env2);
|
|
12578
|
+
for (const k of ["background", "text", "all"]) for (const [hex, n] of u[k]) pooled2[k].set(hex, (pooled2[k].get(hex) ?? 0) + n);
|
|
12271
12579
|
}
|
|
12272
|
-
const synthetic = [...pooled2.entries()].flatMap(([hex, n]) => Array.from({ length: n }, () => hex));
|
|
12273
|
-
const base = derivePalette({ pooled: synthetic });
|
|
12274
12580
|
const fonts = [];
|
|
12275
|
-
for (const env2 of envelopes) for (const f of
|
|
12276
|
-
return
|
|
12581
|
+
for (const env2 of envelopes) for (const f of pageFaces(env2)) if (!fonts.includes(f)) fonts.push(f);
|
|
12582
|
+
return derivePaletteFromUsage(pooled2, fonts.slice(0, 2));
|
|
12277
12583
|
}
|
|
12278
|
-
var ICON_FONTS, BRAND_CHROMA, escapeRe;
|
|
12584
|
+
var ICON_FONTS, BRAND_CHROMA, TONE_CHROMA, escapeRe;
|
|
12279
12585
|
var init_retheme = __esm({
|
|
12280
12586
|
"src/command-os/design/retheme.ts"() {
|
|
12281
12587
|
"use strict";
|
|
12282
12588
|
init_color();
|
|
12283
12589
|
init_ghl_page();
|
|
12590
|
+
init_rendered_palette();
|
|
12591
|
+
init_rendered_palette();
|
|
12284
12592
|
ICON_FONTS = /font\s*awesome|material\s*(icons|symbols)|glyphicons|ionicons|feather|bootstrap-icons|fontello|typicons|dashicons/i;
|
|
12285
12593
|
BRAND_CHROMA = 0.06;
|
|
12594
|
+
TONE_CHROMA = 0.02;
|
|
12286
12595
|
escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12287
12596
|
}
|
|
12288
12597
|
});
|
|
@@ -16782,7 +17091,8 @@ ${await response.text()}`);
|
|
|
16782
17091
|
problems.push(`${step.name}: the branded page saved but GoHighLevel is not serving it`);
|
|
16783
17092
|
continue;
|
|
16784
17093
|
}
|
|
16785
|
-
const
|
|
17094
|
+
const savedPage = await (await fetch(savedUrl)).json();
|
|
17095
|
+
const savedText = JSON.stringify(savedPage).toLowerCase();
|
|
16786
17096
|
const stillThere = palette.accent && savedText.includes(palette.accent.toLowerCase());
|
|
16787
17097
|
if (stillThere) {
|
|
16788
17098
|
problems.push(`${step.name}: the template's own ${palette.accent} is still in the saved page \u2014 a partial swap, so the original was kept`);
|
|
@@ -16793,14 +17103,25 @@ ${await response.text()}`);
|
|
|
16793
17103
|
problems.push(`${step.name}: the brand's ${brand.colors.accent.hex} is not in the saved page \u2014 the paint did not land, so the original was kept`);
|
|
16794
17104
|
continue;
|
|
16795
17105
|
}
|
|
17106
|
+
const repaint = repaintReport(envelope, savedPage);
|
|
17107
|
+
const pageWarnings = painted.warnings.filter((w) => !palette.undecided.includes(w));
|
|
16796
17108
|
done.push({
|
|
16797
17109
|
page: step.name,
|
|
16798
17110
|
newPageId,
|
|
16799
17111
|
oldStepId: step.stepId,
|
|
16800
|
-
|
|
17112
|
+
repaint: {
|
|
17113
|
+
verdict: repaint.verdict,
|
|
17114
|
+
summary: repaint.summary,
|
|
17115
|
+
backgrounds: repaint.backgrounds,
|
|
17116
|
+
neutralBackgrounds: repaint.neutralBackgrounds,
|
|
17117
|
+
text: repaint.text,
|
|
17118
|
+
changes: repaint.changes.filter((c) => c.kind === "background" || c.kind === "text"),
|
|
17119
|
+
fonts: repaint.fonts,
|
|
17120
|
+
contrast: repaint.contrast
|
|
17121
|
+
},
|
|
16801
17122
|
skipped: painted.skipped,
|
|
16802
|
-
|
|
16803
|
-
|
|
17123
|
+
...pageWarnings.length ? { warnings: pageWarnings } : {},
|
|
17124
|
+
preview: ghlPreviewUrl(newPageId)
|
|
16804
17125
|
});
|
|
16805
17126
|
} catch (e) {
|
|
16806
17127
|
problems.push(`${step.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -16827,6 +17148,19 @@ ${await response.text()}`);
|
|
|
16827
17148
|
brand: { slug: brand.slug, accent: brand.colors.accent.hex, display: brand.pageStudioTheme.headingFont, body: brand.pageStudioTheme.bodyFont },
|
|
16828
17149
|
templatePalette: { accent: palette.accent, surface: palette.surface, text: palette.text, secondary: palette.secondary, faces: palette.fonts, undecided: palette.undecided },
|
|
16829
17150
|
pages: done,
|
|
17151
|
+
...(() => {
|
|
17152
|
+
const faint = done.filter((d) => d.repaint.verdict !== "repainted").map((d) => String(d.page));
|
|
17153
|
+
const hard = done.filter((d) => d.repaint.contrast.length > 0);
|
|
17154
|
+
const lines = [
|
|
17155
|
+
...faint.length ? [`On ${faint.join(", ")} the visible palette barely changed. Each page's repaint says what still paints in the template's own colours.`] : [],
|
|
17156
|
+
...hard.length ? [`On ${hard.map((d) => String(d.page)).join(", ")} some type is now harder to read than WCAG AA allows; each page's repaint.contrast names the colours.`] : []
|
|
17157
|
+
];
|
|
17158
|
+
const contrast = hard.flatMap((d) => d.repaint.contrast.map((c) => `${String(d.page)}: ${c.text} type on ${c.background} is ${c.after}:1 (was ${c.before}:1).`));
|
|
17159
|
+
return {
|
|
17160
|
+
...lines.length ? { repaintNote: `${lines.join(" ")} Look at the preview before calling it branded.` } : {},
|
|
17161
|
+
...contrast.length ? { contrast } : {}
|
|
17162
|
+
};
|
|
17163
|
+
})(),
|
|
16830
17164
|
originalsRemoved: complete && removeOriginals ? removed : 0,
|
|
16831
17165
|
...problems.length > 0 && { problems, kept: "Every original step was LEFT IN PLACE because something failed. Nothing was deleted." },
|
|
16832
17166
|
...namedButNotNew && { identification: "GoHighLevel named a funnel that this account ALREADY had before the install. That name was discarded and the account was compared instead, so the funnel above was identified the cautious way and originals are only removed if you asked for it explicitly. Worth reporting: it means the install response no longer means what it did." },
|
|
@@ -19516,7 +19850,7 @@ function traceOpportunityContext(node, byId2) {
|
|
|
19516
19850
|
}
|
|
19517
19851
|
return "dangling";
|
|
19518
19852
|
}
|
|
19519
|
-
function
|
|
19853
|
+
function isRecord9(v) {
|
|
19520
19854
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
19521
19855
|
}
|
|
19522
19856
|
function isUuidV4(id) {
|
|
@@ -19612,8 +19946,8 @@ function checkSaveBlockers(workflow) {
|
|
|
19612
19946
|
});
|
|
19613
19947
|
}
|
|
19614
19948
|
{
|
|
19615
|
-
const attrs =
|
|
19616
|
-
const startAfter = attrs &&
|
|
19949
|
+
const attrs = isRecord9(a.attributes) ? a.attributes : void 0;
|
|
19950
|
+
const startAfter = attrs && isRecord9(attrs.startAfter) ? attrs.startAfter : void 0;
|
|
19617
19951
|
if (startAfter && (startAfter.type === "day" || startAfter.type === "minute")) {
|
|
19618
19952
|
findings.push({
|
|
19619
19953
|
severity: "warning",
|
|
@@ -19625,8 +19959,8 @@ function checkSaveBlockers(workflow) {
|
|
|
19625
19959
|
}
|
|
19626
19960
|
}
|
|
19627
19961
|
if (type === "internal_notification") {
|
|
19628
|
-
const attrs =
|
|
19629
|
-
const notif = attrs &&
|
|
19962
|
+
const attrs = isRecord9(a.attributes) ? a.attributes : void 0;
|
|
19963
|
+
const notif = attrs && isRecord9(attrs.notification) ? attrs.notification : void 0;
|
|
19630
19964
|
const selected = notif?.selectedUser;
|
|
19631
19965
|
const blank = selected === void 0 || selected === null || typeof selected === "string" && selected.trim() === "";
|
|
19632
19966
|
if (notif?.userType === "user" && blank) {
|
|
@@ -22362,7 +22696,7 @@ function executablePlanLines(saved) {
|
|
|
22362
22696
|
funnel: "funnel"
|
|
22363
22697
|
};
|
|
22364
22698
|
const words = (s) => s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_.-]+/g, " ").trim();
|
|
22365
|
-
const
|
|
22699
|
+
const kindOf2 = (s) => kinds[s] || words(s).replace(/s$/, "") || "plan object";
|
|
22366
22700
|
const names = /* @__PURE__ */ new Map();
|
|
22367
22701
|
const objects = /* @__PURE__ */ new Map();
|
|
22368
22702
|
const label = (x, kind) => x.name?.trim() || x.title?.trim() || [x.firstName, x.lastName].filter(Boolean).join(" ") || `Unnamed ${kind}`;
|
|
@@ -22384,7 +22718,7 @@ function executablePlanLines(saved) {
|
|
|
22384
22718
|
const refs = [...names.keys()].sort((a, b) => b.length - a.length);
|
|
22385
22719
|
const refPattern = refs.length ? new RegExp(`(?<![\\w./:@])(?:${refs.map(escapeRE).join("|")})(?![\\w]|\\.[\\w])`, "g") : void 0;
|
|
22386
22720
|
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;
|
|
22387
|
-
const resolve12 = (s) => s.replace(missingRefPattern, (ref) => names.get(ref) || `Unnamed ${
|
|
22721
|
+
const resolve12 = (s) => s.replace(missingRefPattern, (ref) => names.get(ref) || `Unnamed ${kindOf2(ref.split(".")[0])}`);
|
|
22388
22722
|
const renderRefs = (s) => resolve12(refPattern ? s.replace(refPattern, (ref) => names.get(ref)) : s);
|
|
22389
22723
|
const mergeLabel = (field) => {
|
|
22390
22724
|
const [group, ...parts] = field.trim().split(".");
|
|
@@ -22403,7 +22737,7 @@ function executablePlanLines(saved) {
|
|
|
22403
22737
|
const code = entity[2].toLowerCase() === "x" ? parseInt(entity.slice(3), 16) : parseInt(entity.slice(2), 10);
|
|
22404
22738
|
return code <= 1114111 ? String.fromCodePoint(code) : entity;
|
|
22405
22739
|
}).replace(/{{\s*([^{}]+?)\s*}}/g, (_all, field) => `[${mergeLabel(field)}]`)).replace(/\bMCP\b/g, "GHL Command").replace(/Agency OS/g, "Command OS").replace(/\s*—\s*/g, " ").replace(/\s+/g, " ").replace(/\s+([.,;!?])/g, "$1").trim();
|
|
22406
|
-
const show = (v, key = "") => typeof v === "string" ? names.get(v) || (/Refs?$/.test(key) ? `Unnamed ${
|
|
22740
|
+
const show = (v, key = "") => typeof v === "string" ? names.get(v) || (/Refs?$/.test(key) ? `Unnamed ${kindOf2(key.replace(/Refs?$/, "").replace(/^teamMember$/, "user").replace(/^template$/, "message template"))}` : plain(key === "mergeTags" ? `{{${v}}}` : key === "type" ? words(v) : v)) : Array.isArray(v) ? v.map((value) => show(value, key)).join(", ") : v && typeof v === "object" ? Object.entries(v).filter(([k]) => k !== "ref").map(([k, value]) => `${words(k.replace(/Refs?$/, ""))}: ${show(value, k)}`).join("; ") : String(v ?? "");
|
|
22407
22741
|
const sentence = (s) => {
|
|
22408
22742
|
const text2 = plain(s);
|
|
22409
22743
|
return text2 && !/[.!?…]$/.test(text2) ? `${text2}.` : text2;
|
|
@@ -22411,7 +22745,7 @@ function executablePlanLines(saved) {
|
|
|
22411
22745
|
const messageCopy = (t) => [t.subject ? `Subject: ${sentence(t.subject)}` : "", t.body || t.html ? `Message: ${sentence(t.body || t.html)}` : t.bodyOutline ? `Message outline: ${sentence(t.bodyOutline)}` : ""].filter(Boolean).join(" ");
|
|
22412
22746
|
for (const section2 of ["users", "pipelines", "customFields", "fields", "tags", "customValues", "calendars", "forms", "funnels"]) for (const x of p?.[section2] ?? []) {
|
|
22413
22747
|
const existing = preview?.items?.find((i) => i.ref === x.ref)?.status === "existing";
|
|
22414
|
-
lines.push(`${existing ? "Reuse" : "Create"} ${
|
|
22748
|
+
lines.push(`${existing ? "Reuse" : "Create"} ${kindOf2(section2)}: ${label(x, kindOf2(section2))}. ${show(Object.fromEntries(Object.entries(x).filter(([k]) => !["ref", "name"].includes(k))))}`);
|
|
22415
22749
|
}
|
|
22416
22750
|
for (const w of p?.workflows ?? []) {
|
|
22417
22751
|
lines.push(`Workflow: ${label(w, "workflow")}. ${show(w.trigger || w.triggers || "")}`);
|
|
@@ -26851,6 +27185,7 @@ ${saved.summary}
|
|
|
26851
27185
|
5. ${STAFF_NOTIFICATION_RULE} A notification step whose recipient is empty is listed in issues as waiting for a staff member, never left unmentioned.
|
|
26852
27186
|
6. ${MISSING_STEP_RULE}
|
|
26853
27187
|
If the tool reports that no saved plan exists, STOP and finish with RESULT: {"ok":false,"error":"no saved plan on this machine \u2014 run the build stage fresh"}.
|
|
27188
|
+
If apply_build_plan answers phase:"scope" (nothing is selected for this client, or the selection changed after the plan was saved), STOP: build nothing else and finish with RESULT: {"ok":false,"error":"<its error, word for word>"}.
|
|
26854
27189
|
${NO_INVENTED_CAUSE_RULE}
|
|
26855
27190
|
Finish with EXACTLY one final line:
|
|
26856
27191
|
RESULT: {"ok":true,"summary":"<what was created vs already in place: counts of pipelines/fields/tags/workflows>","issues":["<anything skipped or needing a human>"]}`;
|
|
@@ -30060,14 +30395,7 @@ function assessmentFeatureLines(snapshot, purchased = [], availableOnly = true)
|
|
|
30060
30395
|
});
|
|
30061
30396
|
}
|
|
30062
30397
|
function assessmentPurchasesFor(state, id) {
|
|
30063
|
-
|
|
30064
|
-
if (!scope) return saved;
|
|
30065
|
-
return [...new Set([
|
|
30066
|
-
...saved ?? [],
|
|
30067
|
-
...scope.features.map((f) => BUILD_ASSESSMENT_FEATURE[f]),
|
|
30068
|
-
"pipeline"
|
|
30069
|
-
// Every scoped build includes the pipeline foundation.
|
|
30070
|
-
].filter(Boolean))];
|
|
30398
|
+
return state.assessmentPurchases?.[id];
|
|
30071
30399
|
}
|
|
30072
30400
|
function upsellFigure(snapshot, feature) {
|
|
30073
30401
|
const id = BUILD_ASSESSMENT_FEATURE[feature];
|
|
@@ -30707,6 +31035,16 @@ function savePlanRecord(rec, base) {
|
|
|
30707
31035
|
fs22.writeFileSync(file2, JSON.stringify({ ...rec, contentHash: contentHash(rec.plan), inputHash: rec.inputHash ?? process.env.GHL_COMMAND_INTAKE_HASH, dependencies: rec.dependencies ?? planDependencies(rec.plan) }, null, 2), { mode: 384 });
|
|
30708
31036
|
return file2;
|
|
30709
31037
|
}
|
|
31038
|
+
function savedPlanMatchesScope(saved, current) {
|
|
31039
|
+
if (!saved) return false;
|
|
31040
|
+
if (!current || current.wholePreset) return !saved.scope;
|
|
31041
|
+
if (!saved.scope) return false;
|
|
31042
|
+
const omitted = (saved.scope.omitted ?? []).map((f) => f.id);
|
|
31043
|
+
const offered = /* @__PURE__ */ new Set([...saved.scope.built, ...saved.scope.notBuilt.map((f) => f.id), ...omitted]);
|
|
31044
|
+
const selected = new Set(current.features.filter((f) => offered.has(f)));
|
|
31045
|
+
const planned = /* @__PURE__ */ new Set([...saved.scope.built, ...omitted]);
|
|
31046
|
+
return selected.size === planned.size && [...planned].every((f) => selected.has(f));
|
|
31047
|
+
}
|
|
30710
31048
|
function readSavedPlan(locationId2, base) {
|
|
30711
31049
|
try {
|
|
30712
31050
|
return JSON.parse(fs22.readFileSync(path20.join(plansDir(base), `${locationId2}.json`), "utf8"));
|
|
@@ -31230,7 +31568,7 @@ var require_package = __commonJS({
|
|
|
31230
31568
|
"package.json"(exports2, module2) {
|
|
31231
31569
|
module2.exports = {
|
|
31232
31570
|
name: "@elitedcs/ghl-mcp",
|
|
31233
|
-
version: "3.84.
|
|
31571
|
+
version: "3.84.1",
|
|
31234
31572
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
31235
31573
|
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.",
|
|
31236
31574
|
main: "dist/index.js",
|
|
@@ -32240,16 +32578,8 @@ async function assessmentAttachmentAction(args, deps) {
|
|
|
32240
32578
|
const chosen = args.purchasedFeatureIds;
|
|
32241
32579
|
if (!Array.isArray(chosen) || chosen.some((id2) => !ASSESSMENT_FEATURES.some((f) => f.id === id2))) throw new Error("Choose features from the list.");
|
|
32242
32580
|
const state2 = deps.read();
|
|
32243
|
-
|
|
32244
|
-
|
|
32245
|
-
const next = {
|
|
32246
|
-
...state2,
|
|
32247
|
-
scope: { ...state2.scope, [id]: { ...scope ?? {}, features, setAt: (/* @__PURE__ */ new Date()).toISOString() } },
|
|
32248
|
-
assessmentPurchases: { ...state2.assessmentPurchases, [id]: [...new Set(chosen)] }
|
|
32249
|
-
};
|
|
32250
|
-
next.assessmentPurchases[id] = assessmentPurchasesFor(next, id);
|
|
32251
|
-
deps.write(next);
|
|
32252
|
-
detail.message = "What they bought is saved for this client document.";
|
|
32581
|
+
deps.write({ ...state2, assessmentPurchases: { ...state2.assessmentPurchases, [id]: [...new Set(chosen)] } });
|
|
32582
|
+
detail.message = state2.scope?.[id] ? "What they bought is saved for this client document. The build still follows the selection confirmed on the Review screen." : "What they bought is saved. Nothing is built until you confirm what to build on the Review screen, which is ticked from this.";
|
|
32253
32583
|
break;
|
|
32254
32584
|
}
|
|
32255
32585
|
default:
|
|
@@ -32271,7 +32601,6 @@ var init_attachment_actions = __esm({
|
|
|
32271
32601
|
"src/command-os/assessment/attachment-actions.ts"() {
|
|
32272
32602
|
"use strict";
|
|
32273
32603
|
init_attachment();
|
|
32274
|
-
init_features();
|
|
32275
32604
|
}
|
|
32276
32605
|
});
|
|
32277
32606
|
|
|
@@ -34745,14 +35074,15 @@ function reviewDocumentHtml(review, opts) {
|
|
|
34745
35074
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
|
|
34746
35075
|
const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc8(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc8(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc8(g.fix)}</div>` : ""}</div>`;
|
|
34747
35076
|
const o = review.offer;
|
|
34748
|
-
const
|
|
35077
|
+
const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
|
|
35078
|
+
const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
|
|
34749
35079
|
...opts.purchasedFeatureIds ?? [],
|
|
34750
|
-
...
|
|
35080
|
+
...confirmed ? [...confirmed.built.map((f) => BUILD_ASSESSMENT_FEATURE[f.id]), "pipeline"] : []
|
|
34751
35081
|
])] : void 0;
|
|
34752
|
-
const
|
|
34753
|
-
const represented = [...opts.scope?.built ?? [], ...
|
|
35082
|
+
const unbuilt = opts.scope?.notBuilt ?? [];
|
|
35083
|
+
const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
|
|
34754
35084
|
const assessmentNext = opts.assessment && purchased ? assessmentFeatureLines(opts.assessment, [...purchased, ...represented]) : [];
|
|
34755
|
-
const available =
|
|
35085
|
+
const available = unbuilt.map((f) => `<div class="build"><b>${esc8(f.name)}</b><span>${esc8(f.what)}${f.figure ? ` <b>${esc8(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line3) => `<p>${esc8(line3)}</p>`).join("");
|
|
34756
35086
|
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc8(opts.clientName)} \u2014 Growth Blueprint</title><style>
|
|
34757
35087
|
@page{margin:0.6in}
|
|
34758
35088
|
*{box-sizing:border-box}
|
|
@@ -34809,7 +35139,9 @@ function reviewDocumentHtml(review, opts) {
|
|
|
34809
35139
|
${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc8(o.recommendedOffer)}</div>` : ""}` : ""}
|
|
34810
35140
|
${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
|
|
34811
35141
|
${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc8(b.item)}</b><span>${esc8(b.because)}</span></div>`).join("")}` : ""}
|
|
34812
|
-
${opts.scope ? `<div class="head">What this build includes</div>
|
|
35142
|
+
${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
|
|
35143
|
+
<p>Not confirmed yet. What you bought is recorded, and the build waits until the features to build are confirmed.</p>
|
|
35144
|
+
` : opts.scope ? `<div class="head">What this build includes</div>
|
|
34813
35145
|
<p>${opts.scope.package ? `Built to your <b>${esc8(opts.scope.package)}</b> package: ` : ""}${opts.scope.built.length ? esc8(opts.scope.built.map((b) => b.name).join(", ")) : "the foundation only"}. Always included: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing.</p>
|
|
34814
35146
|
` : ""}
|
|
34815
35147
|
${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc8(u)}</li>`).join("")}</ul></div>` : ""}
|
|
@@ -35985,6 +36317,7 @@ __export(dashboard_exports, {
|
|
|
35985
36317
|
resolvePlan: () => resolvePlan2,
|
|
35986
36318
|
runDashboard: () => runDashboard,
|
|
35987
36319
|
safeDocName: () => safeDocName,
|
|
36320
|
+
scopePrefillFor: () => scopePrefillFor,
|
|
35988
36321
|
setAccountIndustry: () => setAccountIndustry,
|
|
35989
36322
|
setAccountType: () => setAccountType,
|
|
35990
36323
|
setBuildApproval: () => setBuildApproval,
|
|
@@ -36046,6 +36379,8 @@ function setClientScope(state, locationId2, scope, now = () => /* @__PURE__ */ n
|
|
|
36046
36379
|
const all = { ...state.scope ?? {} };
|
|
36047
36380
|
if (!scope) {
|
|
36048
36381
|
delete all[locationId2];
|
|
36382
|
+
} else if ("wholePreset" in scope) {
|
|
36383
|
+
all[locationId2] = { wholePreset: true, features: [...FEATURE_IDS], setAt: now().toISOString() };
|
|
36049
36384
|
} else {
|
|
36050
36385
|
const features = [...new Set(scope.features.filter(isFeatureId))];
|
|
36051
36386
|
const pkg2 = typeof scope.package === "string" ? scope.package.trim().slice(0, 80) : "";
|
|
@@ -36053,19 +36388,18 @@ function setClientScope(state, locationId2, scope, now = () => /* @__PURE__ */ n
|
|
|
36053
36388
|
}
|
|
36054
36389
|
const next = { ...state, scope: all };
|
|
36055
36390
|
if (!Object.keys(all).length) delete next.scope;
|
|
36056
|
-
|
|
36057
|
-
if (scope) purchases[locationId2] = assessmentPurchasesFor(next, locationId2);
|
|
36058
|
-
return { ...next, assessmentPurchases: purchases };
|
|
36391
|
+
return next;
|
|
36059
36392
|
}
|
|
36060
36393
|
function documentScopeFor(state, locationId2) {
|
|
36061
36394
|
const sc = state.scope?.[locationId2];
|
|
36062
|
-
if (!sc) return
|
|
36395
|
+
if (!sc && Array.isArray(state.assessmentPurchases?.[locationId2])) return { awaitingConfirmation: true, built: [], notBuilt: [] };
|
|
36063
36396
|
let offered = FEATURE_IDS;
|
|
36064
36397
|
try {
|
|
36065
36398
|
const chosen = loadPresetChoice(void 0, presetForIndustry(state.accounts?.[locationId2]?.industry));
|
|
36066
36399
|
if (chosen.ok) offered = featuresInPreset(chosen.preset.skeleton?.workflows ?? []);
|
|
36067
36400
|
} catch {
|
|
36068
36401
|
}
|
|
36402
|
+
if (!sc || sc.wholePreset) return { wholePreset: true, built: offered.map((id) => ({ id, name: FEATURES[id].name })), notBuilt: [] };
|
|
36069
36403
|
return {
|
|
36070
36404
|
...sc.package ? { package: sc.package } : {},
|
|
36071
36405
|
// Built = the confirmed scope, the one record the build follows. The purchase
|
|
@@ -36078,6 +36412,11 @@ function documentScopeFor(state, locationId2) {
|
|
|
36078
36412
|
})
|
|
36079
36413
|
};
|
|
36080
36414
|
}
|
|
36415
|
+
function scopePrefillFor(state, locationId2) {
|
|
36416
|
+
const bought = state.assessmentPurchases?.[locationId2];
|
|
36417
|
+
if (state.scope?.[locationId2] || !Array.isArray(bought)) return void 0;
|
|
36418
|
+
return FEATURE_IDS.filter((f) => bought.includes(BUILD_ASSESSMENT_FEATURE[f]));
|
|
36419
|
+
}
|
|
36081
36420
|
function assessmentFiguresFor(state, locationId2) {
|
|
36082
36421
|
const snap = state.assessmentAttachments?.[locationId2];
|
|
36083
36422
|
if (!snap) return {};
|
|
@@ -36164,9 +36503,11 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
36164
36503
|
}).map((q2) => ({ key: q2.key, label: q2.label, from: INTAKE_QUESTIONS.find((b) => b.key === q2.key)?.label })),
|
|
36165
36504
|
removedKeys: INTAKE_QUESTIONS.filter((b) => !composed.some((q2) => q2.key === b.key)).map((b) => b.key),
|
|
36166
36505
|
// A build re-run reuses the approved plan already saved for this account (finding 10).
|
|
36506
|
+
// ...and only while it still builds the scope the member has confirmed now: a
|
|
36507
|
+
// changed selection composes the plan fresh from the preset (delta round 3, T-M4).
|
|
36167
36508
|
savedPlan: (() => {
|
|
36168
36509
|
const sp = stage === 3 ? readSavedPlan(locationId2) : null;
|
|
36169
|
-
return sp && sp.inputHash === meta.inputHash ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
|
|
36510
|
+
return sp && sp.inputHash === meta.inputHash && savedPlanMatchesScope(sp, state.scope?.[locationId2]) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
|
|
36170
36511
|
})()
|
|
36171
36512
|
};
|
|
36172
36513
|
if (context.savedPlan) run.lines.push(`Using the build plan saved on ${context.savedPlan.savedAt}.`);
|
|
@@ -37090,7 +37431,7 @@ function scopeBlock(id,state){
|
|
|
37090
37431
|
const sel=document.createElement("select");sel.style.cssText="padding:.35rem .5rem;border:1px solid var(--line);border-radius:6px;font-size:var(--fs-sm);background:var(--surface);max-width:100%";
|
|
37091
37432
|
const o0=document.createElement("option");o0.value="";o0.textContent=pk.length?"Everything the preset has (no package)":"Everything the preset has (no packages saved yet: add them on Your agency)";sel.appendChild(o0);
|
|
37092
37433
|
for(const p of pk){const o=document.createElement("option");o.value=p.id;o.textContent=p.name+(p.price?" \xB7 "+p.price:"");sel.appendChild(o)}
|
|
37093
|
-
let ticked=new Set(saved?saved.features:Object.keys(FEATS));if(saved&&saved.package)sel.value=saved.package;
|
|
37434
|
+
const prefill=(state.scopePrefill||{})[id];let ticked=new Set(saved&&!saved.wholePreset?saved.features:(prefill||Object.keys(FEATS)));if(saved&&saved.package)sel.value=saved.package;
|
|
37094
37435
|
const boxes=document.createElement("div");boxes.style.cssText="margin:.5rem 0";
|
|
37095
37436
|
function drawBoxes(){boxes.innerHTML="";for(const k of Object.keys(FEATS)){const l=document.createElement("label");l.style.cssText="display:flex;gap:.5rem;align-items:flex-start;margin:.3rem 0;font-size:var(--fs-sm);cursor:pointer";
|
|
37096
37437
|
const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(k);c.style.marginTop=".25rem";c.onchange=()=>{if(c.checked)ticked.add(k);else ticked.delete(k)};l.appendChild(c);
|
|
@@ -37102,7 +37443,7 @@ function scopeBlock(id,state){
|
|
|
37102
37443
|
const note=document.createElement("p");note.className="muted";note.textContent="Always built, whatever is ticked: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing. Unticked features go in the client document as available next.";
|
|
37103
37444
|
const sv=document.createElement("button");sv.className="runbtn";sv.textContent="Save what they bought";
|
|
37104
37445
|
sv.onclick=async()=>{const all=Object.keys(FEATS).every(k=>ticked.has(k));const body={locationId:id,features:(all&&!sel.value)?null:[...ticked]};if(sel.value)body.package=sel.value;if(await postJSON("/api/scope",body)){delete assessmentCards[id];load()}};
|
|
37105
|
-
const status=document.createElement("p");status.className="muted";status.textContent=saved?("Saved: "+saved.features.length+" of "+Object.keys(FEATS).length+" features"+(saved.package?" from the "+saved.package+" package":"")+". The build follows this."):"Nothing saved: the whole preset is built.";
|
|
37446
|
+
const status=document.createElement("p");status.className="muted";status.textContent=saved?(saved.wholePreset?"Saved: the whole preset. The build follows this.":"Saved: "+saved.features.length+" of "+Object.keys(FEATS).length+" features"+(saved.package?" from the "+saved.package+" package":"")+". The build follows this."):prefill?"Not confirmed yet: ticked from what they bought. Nothing is built until you save.":"Nothing saved: the whole preset is built.";
|
|
37106
37447
|
sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(status);
|
|
37107
37448
|
return sc}
|
|
37108
37449
|
async function rLoad(id){const r=await(await fetch("/api/review?locationId="+id)).json();rData[id]=r;render(lastState,lastRun)}
|
|
@@ -38351,6 +38692,11 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
38351
38692
|
}
|
|
38352
38693
|
const assessmentFigures = {};
|
|
38353
38694
|
for (const loc of Object.keys(st.assessmentAttachments ?? {})) assessmentFigures[loc] = assessmentFiguresFor(st, loc);
|
|
38695
|
+
const scopePrefill = {};
|
|
38696
|
+
for (const loc of Object.keys(st.assessmentPurchases ?? {})) {
|
|
38697
|
+
const p = scopePrefillFor(st, loc);
|
|
38698
|
+
if (p) scopePrefill[loc] = p;
|
|
38699
|
+
}
|
|
38354
38700
|
const buildViews = Object.fromEntries(currentRoster().map((c) => {
|
|
38355
38701
|
const saved = readSavedPlan(c.locationId);
|
|
38356
38702
|
const approval = st.planApprovals?.[c.locationId];
|
|
@@ -38368,7 +38714,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
38368
38714
|
}));
|
|
38369
38715
|
for (const [id, view] of Object.entries(buildViews)) if (!view.planHash || !view.approvedAt || view.stale.length || st.outcomes?.[`${id}:4`]?.ok !== true || st.manualTasks?.[id]?.length || st.outcomes?.[`${id}:4`]?.issues?.length) liveClaims[id] = { live: false };
|
|
38370
38716
|
const outcomes = Object.fromEntries(Object.entries(st.outcomes ?? {}).map(([key, value]) => [key, { ...value, ...key.endsWith(":4") && value.verification ? { plain: verifyPanelOutcome(value) } : { ...plainFallbackOutcome(value), plain: plainFallbackOutcome(value.plain ?? value) }, error: value.error ? "The step did not finish. Check the account before retrying." : void 0 }]));
|
|
38371
|
-
res.end(JSON.stringify({ ...st, assessmentFigures, outcomes, manualTasks: Object.fromEntries(Object.entries(st.manualTasks ?? {}).map(([id, tasks2]) => {
|
|
38717
|
+
res.end(JSON.stringify({ ...st, assessmentFigures, scopePrefill, outcomes, manualTasks: Object.fromEntries(Object.entries(st.manualTasks ?? {}).map(([id, tasks2]) => {
|
|
38372
38718
|
const records = st.outcomes?.[`${id}:4`]?.verification;
|
|
38373
38719
|
const allowed = records ? verifyReadback(records).allowPublishing : false;
|
|
38374
38720
|
return [id, tasks2.map((t) => ({ ...t, text: verifyPublishingText(plainFallback(t.text, { nextStep: true }), allowed) })).filter((t) => t.text)];
|
|
@@ -39325,8 +39671,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
39325
39671
|
try {
|
|
39326
39672
|
const { locationId: locationId2, package: pkg2, features } = JSON.parse(body);
|
|
39327
39673
|
if (!locationId2 || !currentRoster().some((r) => r.locationId === locationId2)) throw new Error("unknown client");
|
|
39328
|
-
if (features !== null && !Array.isArray(features)) throw new Error("features must be a list of feature ids, or null to
|
|
39329
|
-
const next = features === null ?
|
|
39674
|
+
if (features !== null && !Array.isArray(features)) throw new Error("features must be a list of feature ids, or null to confirm the whole preset");
|
|
39675
|
+
const next = features === null ? { wholePreset: true } : { ...typeof pkg2 === "string" ? { package: pkg2 } : {}, features: features.map(String) };
|
|
39330
39676
|
writeCockpitState(setClientScope(readCockpitState(), locationId2, next));
|
|
39331
39677
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
39332
39678
|
res.end(JSON.stringify({ ok: true, scope: readCockpitState().scope?.[locationId2] ?? null }));
|
|
@@ -48495,6 +48841,137 @@ var init_delivery_audit = __esm({
|
|
|
48495
48841
|
}
|
|
48496
48842
|
});
|
|
48497
48843
|
|
|
48844
|
+
// src/command-os/build/verify-findings.ts
|
|
48845
|
+
var verify_findings_exports = {};
|
|
48846
|
+
__export(verify_findings_exports, {
|
|
48847
|
+
CHECKABLE_KINDS: () => CHECKABLE_KINDS,
|
|
48848
|
+
CHECKABLE_KIND_NAMES: () => CHECKABLE_KIND_NAMES,
|
|
48849
|
+
isCheckableKind: () => isCheckableKind,
|
|
48850
|
+
neverLookedAt: () => neverLookedAt,
|
|
48851
|
+
normaliseKind: () => normaliseKind,
|
|
48852
|
+
refuseUnseenFindings: () => refuseUnseenFindings
|
|
48853
|
+
});
|
|
48854
|
+
async function readPaged(read2, spec, locationId2, wanted) {
|
|
48855
|
+
const base = spec.params?.(locationId2) ?? {};
|
|
48856
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48857
|
+
let skip = 0;
|
|
48858
|
+
for (let reads = 0; reads < MAX_READS; reads++) {
|
|
48859
|
+
const raw = await read2(spec.path(locationId2), { ...base, limit: String(PAGE), skip: String(skip) });
|
|
48860
|
+
const ids = spec.ids(raw);
|
|
48861
|
+
const total = totalOf(raw);
|
|
48862
|
+
let added = 0;
|
|
48863
|
+
for (const id of ids) if (!seen.has(id)) {
|
|
48864
|
+
seen.add(id);
|
|
48865
|
+
added++;
|
|
48866
|
+
}
|
|
48867
|
+
if ([...wanted].every((id) => seen.has(id))) return { seen };
|
|
48868
|
+
if (total !== void 0 && seen.size >= total) return { seen };
|
|
48869
|
+
if (ids.length === 0) {
|
|
48870
|
+
return total !== void 0 && seen.size < total ? { seen, incomplete: `the account says it has ${total} but returned ${seen.size}` } : { seen };
|
|
48871
|
+
}
|
|
48872
|
+
if (added === 0) return { seen, incomplete: "the account returned the same entries again when asked for the next page" };
|
|
48873
|
+
skip += ids.length;
|
|
48874
|
+
}
|
|
48875
|
+
return { seen, incomplete: `it was still returning new entries after ${MAX_READS} pages` };
|
|
48876
|
+
}
|
|
48877
|
+
function isCheckableKind(kind) {
|
|
48878
|
+
return Object.hasOwn(CHECKABLE_KINDS, normaliseKind(kind));
|
|
48879
|
+
}
|
|
48880
|
+
function normaliseKind(kind) {
|
|
48881
|
+
const snake = kind.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toLowerCase();
|
|
48882
|
+
const singular = snake.endsWith("s") && !snake.endsWith("ss") ? snake.slice(0, -1) : snake;
|
|
48883
|
+
return singular === "stage" ? "pipeline_stage" : singular;
|
|
48884
|
+
}
|
|
48885
|
+
async function refuseUnseenFindings(read2, locationId2, findings) {
|
|
48886
|
+
const refusals = [];
|
|
48887
|
+
const rawByRead = /* @__PURE__ */ new Map();
|
|
48888
|
+
const readByKind = /* @__PURE__ */ new Map();
|
|
48889
|
+
const wantedByKind = /* @__PURE__ */ new Map();
|
|
48890
|
+
for (const f of findings) {
|
|
48891
|
+
const kind = normaliseKind(f.kind);
|
|
48892
|
+
wantedByKind.set(kind, (wantedByKind.get(kind) ?? /* @__PURE__ */ new Set()).add(f.id));
|
|
48893
|
+
}
|
|
48894
|
+
for (const f of findings) {
|
|
48895
|
+
const kind = normaliseKind(f.kind);
|
|
48896
|
+
const spec = Object.hasOwn(CHECKABLE_KINDS, kind) ? CHECKABLE_KINDS[kind] : void 0;
|
|
48897
|
+
if (!spec) {
|
|
48898
|
+
refusals.push({ finding: f, because: `"${f.id}" is a "${f.kind}", which the planner cannot check against the account. Findings it can check: ${CHECKABLE_KIND_NAMES}.` });
|
|
48899
|
+
continue;
|
|
48900
|
+
}
|
|
48901
|
+
let got = readByKind.get(kind);
|
|
48902
|
+
if (got === void 0) {
|
|
48903
|
+
if (spec.paged) {
|
|
48904
|
+
try {
|
|
48905
|
+
got = await readPaged(read2, spec, locationId2, wantedByKind.get(kind) ?? /* @__PURE__ */ new Set());
|
|
48906
|
+
} catch (e) {
|
|
48907
|
+
got = e instanceof Error ? e : new Error(String(e));
|
|
48908
|
+
}
|
|
48909
|
+
} else {
|
|
48910
|
+
const readKey = `${spec.path(locationId2)}?${JSON.stringify(spec.params?.(locationId2) ?? {})}`;
|
|
48911
|
+
let raw = rawByRead.get(readKey);
|
|
48912
|
+
if (raw === void 0) {
|
|
48913
|
+
try {
|
|
48914
|
+
raw = await read2(spec.path(locationId2), spec.params?.(locationId2));
|
|
48915
|
+
} catch (e) {
|
|
48916
|
+
raw = e instanceof Error ? e : new Error(String(e));
|
|
48917
|
+
}
|
|
48918
|
+
rawByRead.set(readKey, raw);
|
|
48919
|
+
}
|
|
48920
|
+
got = raw instanceof Error ? raw : { seen: new Set(spec.ids(raw)) };
|
|
48921
|
+
}
|
|
48922
|
+
readByKind.set(kind, got);
|
|
48923
|
+
}
|
|
48924
|
+
if (got instanceof Error) {
|
|
48925
|
+
const why = got.message.split("\n")[0].slice(0, 160);
|
|
48926
|
+
refusals.push({ finding: f, because: `The account's ${kind.replace(/_/g, " ")} list could not be read (${why}), so "${f.id}" cannot be verified. Nothing is planned against an id that could not be checked.` });
|
|
48927
|
+
continue;
|
|
48928
|
+
}
|
|
48929
|
+
if (got.seen.has(f.id)) continue;
|
|
48930
|
+
refusals.push({
|
|
48931
|
+
finding: f,
|
|
48932
|
+
because: got.incomplete ? `The account's ${kind.replace(/_/g, " ")} list could not be read in full (${got.incomplete}). "${f.id}" was not among the entries it did return, but the list was not read to the end, so it is neither verified nor called missing. Nothing is planned against an id that could not be checked.` : neverLookedAt(f.id)
|
|
48933
|
+
});
|
|
48934
|
+
}
|
|
48935
|
+
return refusals;
|
|
48936
|
+
}
|
|
48937
|
+
var PAGE, MAX_READS, totalOf, rows, idsOf, byLocation, CHECKABLE_KINDS, CHECKABLE_KIND_NAMES, neverLookedAt;
|
|
48938
|
+
var init_verify_findings = __esm({
|
|
48939
|
+
"src/command-os/build/verify-findings.ts"() {
|
|
48940
|
+
"use strict";
|
|
48941
|
+
PAGE = 100;
|
|
48942
|
+
MAX_READS = 50;
|
|
48943
|
+
totalOf = (raw) => {
|
|
48944
|
+
const body = raw;
|
|
48945
|
+
const t = body?.total ?? body?.meta?.total;
|
|
48946
|
+
return typeof t === "number" && Number.isFinite(t) && t >= 0 ? t : void 0;
|
|
48947
|
+
};
|
|
48948
|
+
rows = (raw, key) => {
|
|
48949
|
+
const body = raw;
|
|
48950
|
+
const list2 = Array.isArray(body?.[key]) ? body?.[key] : Array.isArray(raw) ? raw : [];
|
|
48951
|
+
return list2.filter((x) => !!x && typeof x === "object");
|
|
48952
|
+
};
|
|
48953
|
+
idsOf = (raw, key) => rows(raw, key).map((r) => r.id).filter((id) => typeof id === "string" && id.length > 0);
|
|
48954
|
+
byLocation = (locationId2) => ({ locationId: locationId2 });
|
|
48955
|
+
CHECKABLE_KINDS = {
|
|
48956
|
+
pipeline: { path: () => "/opportunities/pipelines", params: byLocation, ids: (raw) => idsOf(raw, "pipelines") },
|
|
48957
|
+
pipeline_stage: {
|
|
48958
|
+
path: () => "/opportunities/pipelines",
|
|
48959
|
+
params: byLocation,
|
|
48960
|
+
ids: (raw) => rows(raw, "pipelines").flatMap((p) => idsOf({ stages: p.stages }, "stages"))
|
|
48961
|
+
},
|
|
48962
|
+
workflow: { path: () => "/workflows/", params: byLocation, ids: (raw) => idsOf(raw, "workflows") },
|
|
48963
|
+
custom_field: { path: (loc) => `/locations/${loc}/customFields`, ids: (raw) => idsOf(raw, "customFields") },
|
|
48964
|
+
custom_value: { path: (loc) => `/locations/${loc}/customValues`, ids: (raw) => idsOf(raw, "customValues") },
|
|
48965
|
+
tag: { path: (loc) => `/locations/${loc}/tags`, ids: (raw) => idsOf(raw, "tags"), paged: true },
|
|
48966
|
+
calendar: { path: () => "/calendars/", params: byLocation, ids: (raw) => idsOf(raw, "calendars") },
|
|
48967
|
+
form: { path: () => "/forms/", params: byLocation, ids: (raw) => idsOf(raw, "forms"), paged: true },
|
|
48968
|
+
user: { path: () => "/users/", params: byLocation, ids: (raw) => idsOf(raw, "users") }
|
|
48969
|
+
};
|
|
48970
|
+
CHECKABLE_KIND_NAMES = Object.keys(CHECKABLE_KINDS).join(", ");
|
|
48971
|
+
neverLookedAt = (id) => `"${id}" was never looked at. A step aimed at an unverified id silently kills every action after it, so this is refused rather than attempted.`;
|
|
48972
|
+
}
|
|
48973
|
+
});
|
|
48974
|
+
|
|
48498
48975
|
// src/command-os/build/automation.ts
|
|
48499
48976
|
function canonical(v) {
|
|
48500
48977
|
if (Array.isArray(v)) return v.map(canonical);
|
|
@@ -48535,7 +49012,7 @@ function checkPlan(goal, findings, steps, now = /* @__PURE__ */ new Date(), know
|
|
|
48535
49012
|
const at = new Date(f.observedAt).getTime();
|
|
48536
49013
|
if (!Number.isFinite(at)) continue;
|
|
48537
49014
|
if (at < cutoff) continue;
|
|
48538
|
-
explored.set(f.id, f);
|
|
49015
|
+
explored.set(f.id, (explored.get(f.id) ?? /* @__PURE__ */ new Set()).add(normaliseKind(f.kind)));
|
|
48539
49016
|
}
|
|
48540
49017
|
const staleIds = new Set(
|
|
48541
49018
|
findings.filter((f) => {
|
|
@@ -48555,7 +49032,13 @@ function checkPlan(goal, findings, steps, now = /* @__PURE__ */ new Date(), know
|
|
|
48555
49032
|
const deps = [];
|
|
48556
49033
|
for (const t of s.targets) {
|
|
48557
49034
|
if (isExisting(t)) {
|
|
48558
|
-
|
|
49035
|
+
const kinds = explored.get(t.existing);
|
|
49036
|
+
if (kinds) {
|
|
49037
|
+
if (!kinds.has(normaliseKind(t.kind))) {
|
|
49038
|
+
refusals.push({ step: s.id, because: `"${t.existing}" was looked at as a ${[...kinds].join(" or ").replace(/_/g, " ")}, and this step aims at it as a ${t.kind}. The wrong kind of id silently kills every action after it, so this is refused rather than attempted.` });
|
|
49039
|
+
}
|
|
49040
|
+
continue;
|
|
49041
|
+
}
|
|
48559
49042
|
if (staleIds.has(t.existing)) {
|
|
48560
49043
|
refusals.push({ step: s.id, because: `"${t.existing}" was last looked at more than an hour ago. Ids move; look again before planning against it.` });
|
|
48561
49044
|
} else {
|
|
@@ -48727,6 +49210,7 @@ var init_automation = __esm({
|
|
|
48727
49210
|
"src/command-os/build/automation.ts"() {
|
|
48728
49211
|
"use strict";
|
|
48729
49212
|
import_node_crypto11 = require("node:crypto");
|
|
49213
|
+
init_verify_findings();
|
|
48730
49214
|
isExisting = (t) => "existing" in t;
|
|
48731
49215
|
FINDINGS_STALE_AFTER_MINUTES = 60;
|
|
48732
49216
|
}
|
|
@@ -48821,113 +49305,263 @@ var init_store5 = __esm({
|
|
|
48821
49305
|
}
|
|
48822
49306
|
});
|
|
48823
49307
|
|
|
48824
|
-
// src/command-os/build/
|
|
48825
|
-
|
|
48826
|
-
|
|
48827
|
-
CHECKABLE_KINDS: () => CHECKABLE_KINDS,
|
|
48828
|
-
CHECKABLE_KIND_NAMES: () => CHECKABLE_KIND_NAMES,
|
|
48829
|
-
isCheckableKind: () => isCheckableKind,
|
|
48830
|
-
neverLookedAt: () => neverLookedAt,
|
|
48831
|
-
normaliseKind: () => normaliseKind,
|
|
48832
|
-
refuseUnseenFindings: () => refuseUnseenFindings
|
|
48833
|
-
});
|
|
48834
|
-
async function readAll(read2, spec, locationId2) {
|
|
48835
|
-
const base = spec.params?.(locationId2);
|
|
48836
|
-
if (!spec.paged) return new Set(spec.ids(await read2(spec.path(locationId2), base)));
|
|
48837
|
-
const seen = /* @__PURE__ */ new Set();
|
|
48838
|
-
for (let page = 0; page < MAX_PAGES2; page++) {
|
|
48839
|
-
const ids = spec.ids(await read2(spec.path(locationId2), { ...base ?? {}, limit: String(PAGE), skip: String(page * PAGE) }));
|
|
48840
|
-
for (const id of ids) seen.add(id);
|
|
48841
|
-
if (ids.length < PAGE) return seen;
|
|
48842
|
-
}
|
|
48843
|
-
throw new Error(`more than ${PAGE * MAX_PAGES2} entries; the list was not read to the end`);
|
|
49308
|
+
// src/command-os/build/arg-ids.ts
|
|
49309
|
+
function isIdShapedKey(key) {
|
|
49310
|
+
return /(?:Id|ID)s?$|_ids?$/.test(key);
|
|
48844
49311
|
}
|
|
48845
|
-
function
|
|
48846
|
-
|
|
48847
|
-
|
|
48848
|
-
|
|
48849
|
-
|
|
48850
|
-
|
|
48851
|
-
|
|
48852
|
-
}
|
|
48853
|
-
async function refuseUnseenFindings(read2, locationId2, findings) {
|
|
48854
|
-
const refusals = [];
|
|
48855
|
-
const rawByRead = /* @__PURE__ */ new Map();
|
|
48856
|
-
const seenByKind = /* @__PURE__ */ new Map();
|
|
48857
|
-
for (const f of findings) {
|
|
48858
|
-
const kind = normaliseKind(f.kind);
|
|
48859
|
-
const spec = Object.hasOwn(CHECKABLE_KINDS, kind) ? CHECKABLE_KINDS[kind] : void 0;
|
|
48860
|
-
if (!spec) {
|
|
48861
|
-
refusals.push({ finding: f, because: `"${f.id}" is a "${f.kind}", which the planner cannot check against the account. Findings it can check: ${CHECKABLE_KIND_NAMES}.` });
|
|
48862
|
-
continue;
|
|
49312
|
+
function scanArgs(args, maxNodes = MAX_ARG_NODES) {
|
|
49313
|
+
const out = { ids: [], locations: [], unknown: [], uncheckable: [] };
|
|
49314
|
+
const add = (kind, v, key) => {
|
|
49315
|
+
const list2 = Array.isArray(v) ? v : [v];
|
|
49316
|
+
for (const x of list2) {
|
|
49317
|
+
const id = typeof x === "number" ? String(x) : x;
|
|
49318
|
+
if (typeof id === "string" && id.trim()) out.ids.push({ kind, id, key });
|
|
48863
49319
|
}
|
|
48864
|
-
|
|
48865
|
-
|
|
48866
|
-
|
|
48867
|
-
|
|
48868
|
-
|
|
48869
|
-
|
|
48870
|
-
|
|
48871
|
-
|
|
48872
|
-
|
|
48873
|
-
const readKey = `${spec.path(locationId2)}?${JSON.stringify(spec.params?.(locationId2) ?? {})}`;
|
|
48874
|
-
let raw = rawByRead.get(readKey);
|
|
48875
|
-
if (raw === void 0) {
|
|
48876
|
-
try {
|
|
48877
|
-
raw = await read2(spec.path(locationId2), spec.params?.(locationId2));
|
|
48878
|
-
} catch (e) {
|
|
48879
|
-
raw = e instanceof Error ? e : new Error(String(e));
|
|
48880
|
-
}
|
|
48881
|
-
rawByRead.set(readKey, raw);
|
|
48882
|
-
}
|
|
48883
|
-
seen = raw instanceof Error ? raw : new Set(spec.ids(raw));
|
|
48884
|
-
}
|
|
48885
|
-
seenByKind.set(kind, seen);
|
|
49320
|
+
};
|
|
49321
|
+
const hasValue = (v) => typeof v === "string" && v.trim().length > 0 || typeof v === "number" || Array.isArray(v) && v.some(hasValue);
|
|
49322
|
+
const stack = [{ v: args, inFields: false }];
|
|
49323
|
+
let visited = 0;
|
|
49324
|
+
while (stack.length) {
|
|
49325
|
+
const { v, inFields, nodeType: parentType } = stack.pop();
|
|
49326
|
+
if (++visited > maxNodes) {
|
|
49327
|
+
out.incomplete = `The step's arguments are too large to check in full (more than ${maxNodes} values), so the ids in them could not all be verified.`;
|
|
49328
|
+
break;
|
|
48886
49329
|
}
|
|
48887
|
-
if (
|
|
48888
|
-
const
|
|
48889
|
-
refusals.push({ finding: f, because: `The account's ${kind.replace(/_/g, " ")} list could not be read (${why}), so "${f.id}" cannot be verified. Nothing is planned against an id that could not be checked.` });
|
|
49330
|
+
if (Array.isArray(v)) {
|
|
49331
|
+
for (const x of v) stack.push({ v: x, inFields, nodeType: parentType });
|
|
48890
49332
|
continue;
|
|
48891
49333
|
}
|
|
48892
|
-
if (!
|
|
49334
|
+
if (!isRecord10(v)) continue;
|
|
49335
|
+
const nodeType = typeof v.type === "string" && v.type ? v.type : parentType;
|
|
49336
|
+
const refs = [];
|
|
49337
|
+
if (typeof v.type === "string" && isRecord10(v.attributes)) extractFromAction(v, refs);
|
|
49338
|
+
if (Array.isArray(v.conditions)) extractFromTrigger(v, refs);
|
|
49339
|
+
for (const r of refs) {
|
|
49340
|
+
if (r.byKey) continue;
|
|
49341
|
+
const kind = AUDIT_KIND[r.kind];
|
|
49342
|
+
if (kind) out.ids.push({ kind, id: r.id, key: r.where });
|
|
49343
|
+
else out.uncheckable.push({ kind: r.kind, id: r.id, key: r.where });
|
|
49344
|
+
}
|
|
49345
|
+
for (const [k, x] of Object.entries(v)) {
|
|
49346
|
+
if (k === "locationId") {
|
|
49347
|
+
if (typeof x === "string" && x.length > 0) out.locations.push(x);
|
|
49348
|
+
} else if (k === "traffic_weightage" && isRecord10(x)) {
|
|
49349
|
+
add("user", Object.keys(x), k);
|
|
49350
|
+
continue;
|
|
49351
|
+
} else if (k === "traffic_index" && Array.isArray(x)) {
|
|
49352
|
+
add("user", x.map((e) => isRecord10(e) ? e.id : void 0), k);
|
|
49353
|
+
} else if (k === "selectedUser") {
|
|
49354
|
+
if (v.userType !== "all") add("user", x, k);
|
|
49355
|
+
} else if (k === "field" && inFields) {
|
|
49356
|
+
if (typeof x === "string") {
|
|
49357
|
+
const standardKey = STANDARD_CONTACT_FIELDS.has(x.toLowerCase());
|
|
49358
|
+
if (!standardKey || isIdShaped(x)) add("custom_field", x, "fields[].field");
|
|
49359
|
+
}
|
|
49360
|
+
} else if (k === "customFields" && Array.isArray(x)) {
|
|
49361
|
+
add("custom_field", x.map((e) => isRecord10(e) ? e.id : void 0), "customFields[].id");
|
|
49362
|
+
} else if (k === "customField" && isRecord10(x)) {
|
|
49363
|
+
add("custom_field", x.id, "customField.id");
|
|
49364
|
+
} else if (Object.hasOwn(ID_ARG_KINDS, k)) {
|
|
49365
|
+
add(ID_ARG_KINDS[k], x, k);
|
|
49366
|
+
} else if (isIdShapedKey(k) && !Object.hasOwn(NOT_CHECKABLE_ID_ARGS, k) && hasValue(x)) {
|
|
49367
|
+
out.unknown.push({ key: k, value: JSON.stringify(x).slice(0, 80), ...nodeType ? { nodeType } : {} });
|
|
49368
|
+
}
|
|
49369
|
+
if ((k === "filterField" || k === "field" && !inFields) && typeof x === "string" && Object.hasOwn(CONDITION_FIELD_KINDS, x)) {
|
|
49370
|
+
add(CONDITION_FIELD_KINDS[x], Object.hasOwn(v, "secondValue") ? v.secondValue : v.value, `${k} ${x}`);
|
|
49371
|
+
}
|
|
49372
|
+
if (x && typeof x === "object") stack.push({ v: x, inFields: k === "fields" && Array.isArray(x), nodeType });
|
|
49373
|
+
}
|
|
48893
49374
|
}
|
|
48894
|
-
return
|
|
49375
|
+
return out;
|
|
48895
49376
|
}
|
|
48896
|
-
var
|
|
48897
|
-
var
|
|
48898
|
-
"src/command-os/build/
|
|
49377
|
+
var ID_ARG_KINDS, CONDITION_FIELD_KINDS, AUDIT_KIND, NOT_CHECKABLE_ID_ARGS, MAX_ARG_NODES, isRecord10;
|
|
49378
|
+
var init_arg_ids = __esm({
|
|
49379
|
+
"src/command-os/build/arg-ids.ts"() {
|
|
48899
49380
|
"use strict";
|
|
48900
|
-
|
|
48901
|
-
|
|
48902
|
-
|
|
48903
|
-
|
|
48904
|
-
|
|
48905
|
-
|
|
48906
|
-
|
|
48907
|
-
|
|
48908
|
-
|
|
48909
|
-
|
|
48910
|
-
|
|
48911
|
-
|
|
48912
|
-
|
|
48913
|
-
|
|
48914
|
-
|
|
48915
|
-
|
|
48916
|
-
|
|
48917
|
-
|
|
48918
|
-
|
|
48919
|
-
|
|
48920
|
-
|
|
48921
|
-
|
|
48922
|
-
|
|
48923
|
-
|
|
48924
|
-
|
|
48925
|
-
|
|
49381
|
+
init_validators();
|
|
49382
|
+
init_id_shape();
|
|
49383
|
+
ID_ARG_KINDS = Object.freeze({
|
|
49384
|
+
pipelineId: "pipeline",
|
|
49385
|
+
pipelineIds: "pipeline",
|
|
49386
|
+
pipeline_id: "pipeline",
|
|
49387
|
+
defaultInPipelines: "pipeline",
|
|
49388
|
+
pipelineStageId: "pipeline_stage",
|
|
49389
|
+
pipeline_stage_id: "pipeline_stage",
|
|
49390
|
+
// the workflow create_opportunity node (validators.ts)
|
|
49391
|
+
stageId: "pipeline_stage",
|
|
49392
|
+
workflowId: "workflow",
|
|
49393
|
+
workflowIds: "workflow",
|
|
49394
|
+
workflow_id: "workflow",
|
|
49395
|
+
sourceWorkflowId: "workflow",
|
|
49396
|
+
callEndWorkflowIds: "workflow",
|
|
49397
|
+
fieldId: "custom_field",
|
|
49398
|
+
customFieldId: "custom_field",
|
|
49399
|
+
consentFieldId: "custom_field",
|
|
49400
|
+
customDateFieldId: "custom_field",
|
|
49401
|
+
valueId: "custom_value",
|
|
49402
|
+
customValueId: "custom_value",
|
|
49403
|
+
// The live DBR workflows: update_custom_value and math_operation
|
|
49404
|
+
// (round-4 delta review, 2026-09-15).
|
|
49405
|
+
custom_value_id: "custom_value",
|
|
49406
|
+
sourceCustomValueId: "custom_value",
|
|
49407
|
+
targetCustomValueId: "custom_value",
|
|
49408
|
+
tagId: "tag",
|
|
49409
|
+
calendarId: "calendar",
|
|
49410
|
+
formId: "form",
|
|
49411
|
+
formIds: "form",
|
|
49412
|
+
// a form_submission goal's extras (build_goal_event)
|
|
49413
|
+
userId: "user",
|
|
49414
|
+
userIds: "user",
|
|
49415
|
+
assignedUserId: "user",
|
|
49416
|
+
sentBy: "user",
|
|
49417
|
+
// send_document's sender (release review F2)
|
|
49418
|
+
// Workflow action payloads (CLAUDE.md action schemas). These do not end in
|
|
49419
|
+
// "Id", which is exactly why the guess never saw them.
|
|
49420
|
+
assignedTo: "user",
|
|
49421
|
+
user_list: "user"
|
|
49422
|
+
});
|
|
49423
|
+
CONDITION_FIELD_KINDS = Object.freeze({
|
|
49424
|
+
...ID_ARG_KINDS,
|
|
49425
|
+
"form.id": "form",
|
|
49426
|
+
"calendar.id": "calendar",
|
|
49427
|
+
"opportunity.pipelineId": "pipeline",
|
|
49428
|
+
"opportunity.pipelineStageId": "pipeline_stage",
|
|
49429
|
+
"opportunity.assignedTo": "user",
|
|
49430
|
+
"contact.assignedTo": "user",
|
|
49431
|
+
"workflow.id": "workflow"
|
|
49432
|
+
});
|
|
49433
|
+
AUDIT_KIND = Object.freeze({
|
|
49434
|
+
pipeline: "pipeline",
|
|
49435
|
+
stage: "pipeline_stage",
|
|
49436
|
+
custom_field: "custom_field",
|
|
49437
|
+
user: "user",
|
|
49438
|
+
workflow: "workflow",
|
|
49439
|
+
form: "form",
|
|
49440
|
+
calendar: "calendar",
|
|
49441
|
+
survey: null
|
|
49442
|
+
});
|
|
49443
|
+
NOT_CHECKABLE_ID_ARGS = Object.freeze({
|
|
49444
|
+
// The account itself, not an object inside it. One account per plan is
|
|
49445
|
+
// enforced separately, and the build is bound to it (agency-build.ts).
|
|
49446
|
+
locationId: "the sub-account itself",
|
|
49447
|
+
location_id: "the sub-account itself",
|
|
49448
|
+
altId: "the sub-account itself",
|
|
49449
|
+
companyId: "the agency itself",
|
|
49450
|
+
company_id: "the agency itself",
|
|
49451
|
+
ghl_location_id: "the sub-account itself",
|
|
49452
|
+
ghl_company_id: "the agency itself",
|
|
49453
|
+
test_location_id: "the sub-account itself",
|
|
49454
|
+
demoLocationId: "a sub-account, not an object inside one",
|
|
49455
|
+
clientLocationId: "a sub-account, not an object inside one",
|
|
49456
|
+
sourceLocationId: "a sub-account, not an object inside one",
|
|
49457
|
+
// Objects that live in the account but have no list the planner reads back.
|
|
49458
|
+
contactId: "contacts are not listed in full by the planner",
|
|
49459
|
+
contactIds: "contacts are not listed in full by the planner",
|
|
49460
|
+
opportunityId: "opportunities are not listed in full by the planner",
|
|
49461
|
+
templateId: "templates have no read-back list here",
|
|
49462
|
+
agentId: "AI agents have no read-back list here",
|
|
49463
|
+
assignedEmployeeId: "a Conversation AI employee; no read-back list here",
|
|
49464
|
+
knowledgeBaseId: "knowledge bases have no read-back list here",
|
|
49465
|
+
knowledgeBaseIds: "knowledge bases have no read-back list here",
|
|
49466
|
+
lastKnowledgeBaseId: "knowledge bases have no read-back list here",
|
|
49467
|
+
faqId: "knowledge base entries have no read-back list here",
|
|
49468
|
+
lastFaqId: "knowledge base entries have no read-back list here",
|
|
49469
|
+
urlIds: "knowledge base entries have no read-back list here",
|
|
49470
|
+
funnelId: "funnels have no read-back list here",
|
|
49471
|
+
pageId: "funnel pages have no read-back list here",
|
|
49472
|
+
stepId: "funnel steps (and this plan's own steps) have no read-back list here",
|
|
49473
|
+
recordId: "custom object records have no read-back list here",
|
|
49474
|
+
targetRecordId: "custom object records have no read-back list here",
|
|
49475
|
+
relationId: "associations have no read-back list here",
|
|
49476
|
+
associationId: "associations have no read-back list here",
|
|
49477
|
+
productId: "products have no read-back list here",
|
|
49478
|
+
productIds: "products have no read-back list here",
|
|
49479
|
+
invoiceId: "invoices have no read-back list here",
|
|
49480
|
+
estimateId: "estimates have no read-back list here",
|
|
49481
|
+
orderId: "orders have no read-back list here",
|
|
49482
|
+
subscriptionId: "subscriptions have no read-back list here",
|
|
49483
|
+
couponId: "coupons have no read-back list here",
|
|
49484
|
+
entityId: "payment records have no read-back list here",
|
|
49485
|
+
actionId: "AI agent actions have no read-back list here",
|
|
49486
|
+
actionIds: "AI agent actions have no read-back list here",
|
|
49487
|
+
listId: "smart lists have no read-back list here",
|
|
49488
|
+
webhookId: "webhooks have no read-back list here",
|
|
49489
|
+
taskId: "contact tasks have no read-back list here",
|
|
49490
|
+
eventId: "appointments have no read-back list here",
|
|
49491
|
+
postId: "social posts have no read-back list here",
|
|
49492
|
+
accountIds: "social accounts have no read-back list here",
|
|
49493
|
+
messageId: "messages have no read-back list here",
|
|
49494
|
+
conversationId: "conversations have no read-back list here",
|
|
49495
|
+
conversationProviderId: "conversation providers have no read-back list here",
|
|
49496
|
+
documentId: "documents have no read-back list here",
|
|
49497
|
+
businessId: "businesses have no read-back list here",
|
|
49498
|
+
numberPoolId: "number pools have no read-back list here",
|
|
49499
|
+
linkId: "trigger links have no read-back list here",
|
|
49500
|
+
blogId: "blogs have no read-back list here",
|
|
49501
|
+
categoryId: "categories have no read-back list here",
|
|
49502
|
+
surveyId: "surveys have no read-back list here",
|
|
49503
|
+
submissionId: "form submissions have no read-back list here",
|
|
49504
|
+
mediaId: "media files have no read-back list here",
|
|
49505
|
+
photoAssetId: "media files have no read-back list here",
|
|
49506
|
+
beforeAssetId: "media files have no read-back list here",
|
|
49507
|
+
afterAssetId: "media files have no read-back list here",
|
|
49508
|
+
domainId: "domains have no read-back list here",
|
|
49509
|
+
chatWidgetId: "chat widgets have no read-back list here",
|
|
49510
|
+
callId: "calls have no read-back list here",
|
|
49511
|
+
voiceId: "voices have no read-back list here",
|
|
49512
|
+
placeId: "a Google Business place, outside the account",
|
|
49513
|
+
reviewLinkId: "review links have no read-back list here",
|
|
49514
|
+
linkIds: "a link_click goal's trigger links, which have no read-back list here",
|
|
49515
|
+
globalProductIds: "a payment_received goal's products, which have no read-back list here",
|
|
49516
|
+
global_product_ids: "products have no read-back list here",
|
|
49517
|
+
snapshot_id: "snapshots belong to the agency, not the sub-account",
|
|
49518
|
+
parentId: "a folder or parent record with no read-back list here",
|
|
49519
|
+
// Outside GoHighLevel altogether.
|
|
49520
|
+
stripeCustomerId: "a Stripe record, not a GoHighLevel one",
|
|
49521
|
+
google_oauth_client_id: "a Google OAuth client, not a GoHighLevel record",
|
|
49522
|
+
ghl_user_id: "the Firebase login's uid, not an object in the sub-account",
|
|
49523
|
+
device_id: "this machine's id",
|
|
49524
|
+
// This product's own records.
|
|
49525
|
+
buildId: "a Layer 5 build on this machine",
|
|
49526
|
+
runId: "a build run on this machine",
|
|
49527
|
+
planId: "a saved plan on this machine",
|
|
49528
|
+
briefId: "a brief on this machine",
|
|
49529
|
+
operationId: "an operation on this machine",
|
|
49530
|
+
questionId: "an intake question",
|
|
49531
|
+
ruleId: "a design rule",
|
|
49532
|
+
startAfterId: "a paging cursor",
|
|
49533
|
+
purchasedFeatureIds: "feature names, not account objects",
|
|
49534
|
+
// Ids inside a workflow's or a page's own document: the node, branch or
|
|
49535
|
+
// element they belong to, never an object in the account.
|
|
49536
|
+
_id: "an element's own id inside a document",
|
|
49537
|
+
nodeId: "an element's own id inside a page",
|
|
49538
|
+
appId: "an element's own id inside a page",
|
|
49539
|
+
goToId: "an element's own id inside a page",
|
|
49540
|
+
__conditionId: "a condition's own id inside a workflow",
|
|
49541
|
+
__id: "an operator's own id inside a math step",
|
|
49542
|
+
__segmentId: "a segment's own id inside a workflow",
|
|
49543
|
+
ifElseNodeId: "a node's own id inside a workflow",
|
|
49544
|
+
target_node_id: "a node's own id inside a workflow",
|
|
49545
|
+
targetNodeId: "the step a goto jumps to, inside the same workflow",
|
|
49546
|
+
stepIds: "the steps an email_event goal waits on, inside the same workflow",
|
|
49547
|
+
invoiceStepId: "the step an invoice_paid goal waits on, inside the same workflow",
|
|
49548
|
+
sourceId: `GoHighLevel's attribution key on an email edited in its UI, "<workflowId>:<stepId>"; not checked on save (workflow-builder-client.ts)`,
|
|
49549
|
+
origin_id: "a trigger's own id inside a workflow"
|
|
49550
|
+
});
|
|
49551
|
+
MAX_ARG_NODES = 2e5;
|
|
49552
|
+
isRecord10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
48926
49553
|
}
|
|
48927
49554
|
});
|
|
48928
49555
|
|
|
48929
49556
|
// src/tools/agency-build.ts
|
|
48930
|
-
function
|
|
49557
|
+
function accountMismatch(a, active) {
|
|
49558
|
+
if (!a.locationId) {
|
|
49559
|
+
return "This build was saved before builds recorded the sub-account their ids were checked against, so nothing can say which account they belong to: plan it again (plan_automation) and approve the new plan.";
|
|
49560
|
+
}
|
|
49561
|
+
if (active === a.locationId) return null;
|
|
49562
|
+
return active ? `This build's ids were checked against sub-account ${a.locationId}, and the server is now pointed at ${active}. An id checked on one account means nothing on another, so nothing is approved or recorded here. switch_location back to ${a.locationId}, confirm it with get_current_location, then try again.` : `This build's ids were checked against sub-account ${a.locationId}, and the server is not pointed at any sub-account. switch_location to ${a.locationId}, confirm it with get_current_location, then try again.`;
|
|
49563
|
+
}
|
|
49564
|
+
function registerAgencyBuildTools(server2, knownTools, verify, activeLocation) {
|
|
48931
49565
|
safeTool(
|
|
48932
49566
|
server2,
|
|
48933
49567
|
"plan_automation",
|
|
@@ -48955,43 +49589,67 @@ function registerAgencyBuildTools(server2, knownTools, verify) {
|
|
|
48955
49589
|
const found = findings.map((f) => ({ ...f, observedAt: f.observedAt ?? now.toISOString() }));
|
|
48956
49590
|
const plan = steps;
|
|
48957
49591
|
const check = checkPlan(goal, found, plan, now, knownTools?.());
|
|
49592
|
+
let boundAccount;
|
|
48958
49593
|
if (check.ok) {
|
|
48959
|
-
const
|
|
48960
|
-
const
|
|
48961
|
-
if (!v || typeof v !== "object" || depth > 4) return;
|
|
48962
|
-
for (const [k, x] of Object.entries(v)) {
|
|
48963
|
-
if (k === "locationId" && typeof x === "string" && x.length > 0) named.add(x);
|
|
48964
|
-
collectLocations(x, depth + 1);
|
|
48965
|
-
}
|
|
48966
|
-
};
|
|
48967
|
-
for (const s of plan) collectLocations(s.args, 0);
|
|
49594
|
+
const scans = plan.map((s) => ({ step: s, scan: scanArgs(s.args) }));
|
|
49595
|
+
const named = new Set(scans.flatMap(({ scan }) => scan.locations));
|
|
48968
49596
|
if (locationId2) named.add(locationId2);
|
|
48969
49597
|
if (named.size > 1) {
|
|
48970
49598
|
return { planned: false, refusals: [{ because: `The steps name ${named.size} different sub-accounts (${[...named].join(", ")}). A plan runs against one account; split it.` }], note: "Nothing was saved." };
|
|
48971
49599
|
}
|
|
48972
|
-
const account2 = [...named][0];
|
|
48973
|
-
|
|
48974
|
-
|
|
49600
|
+
const account2 = [...named][0] ?? activeLocation?.();
|
|
49601
|
+
if (!account2) {
|
|
49602
|
+
return { planned: false, refusals: [{ because: "The plan names no sub-account and the server is pointed at none, so there is no sub-account to check its ids against or to run it on. switch_location to the client's account first." }], note: "Nothing was saved." };
|
|
49603
|
+
}
|
|
49604
|
+
const refKinds = new Map(plan.flatMap((s) => (s.creates ?? []).map((c) => [c.ref, normaliseKind(c.kind)])));
|
|
49605
|
+
const seenKeys = new Set(found.map((f) => `${normaliseKind(f.kind)}:${f.id}`));
|
|
48975
49606
|
const hidden = [];
|
|
48976
|
-
const
|
|
48977
|
-
const
|
|
48978
|
-
if (
|
|
48979
|
-
for (const
|
|
48980
|
-
|
|
48981
|
-
|
|
48982
|
-
|
|
49607
|
+
const argRefusals = [];
|
|
49608
|
+
for (const { step: s, scan } of scans) {
|
|
49609
|
+
if (scan.incomplete) argRefusals.push({ because: `Step "${s.id}": ${scan.incomplete}`, finding: { kind: "arguments", id: s.id } });
|
|
49610
|
+
for (const u of scan.unknown) {
|
|
49611
|
+
const inNode = u.nodeType ? ` inside a "${u.nodeType}" node` : "";
|
|
49612
|
+
argRefusals.push({
|
|
49613
|
+
because: `Step "${s.id}" passes "${u.key}"${inNode}: ${u.value}. The planner has no way to check that id against the account, so the plan is refused rather than trusted: a step aimed at an unverified id silently kills every action after it. Two ways on: leave that step out of the plan and do it by hand, or report the node type and key (${u.nodeType ? `"${u.nodeType}" / ` : ""}"${u.key}") so it can be added to the ids this checks.`,
|
|
49614
|
+
finding: { kind: u.key, id: u.value }
|
|
49615
|
+
});
|
|
49616
|
+
}
|
|
49617
|
+
for (const { kind, id } of scan.uncheckable) {
|
|
49618
|
+
const key = `${kind}:${id}`;
|
|
49619
|
+
if (seenKeys.has(key)) continue;
|
|
49620
|
+
seenKeys.add(key);
|
|
49621
|
+
hidden.push({ kind, id, observedAt: now.toISOString() });
|
|
49622
|
+
}
|
|
49623
|
+
for (const { kind, id } of scan.ids) {
|
|
49624
|
+
const refKind = refKinds.get(id);
|
|
49625
|
+
if (refKind !== void 0) {
|
|
49626
|
+
if (refKind !== kind) {
|
|
49627
|
+
argRefusals.push({
|
|
49628
|
+
because: `Step "${s.id}" passes "${id}" where a ${kind.replace(/_/g, " ")} goes, but "${id}" is what an earlier step creates as a ${refKind.replace(/_/g, " ")}. The wrong kind of id silently kills every action after it, so this is refused.`,
|
|
49629
|
+
finding: { kind, id }
|
|
49630
|
+
});
|
|
49631
|
+
}
|
|
49632
|
+
continue;
|
|
48983
49633
|
}
|
|
48984
|
-
|
|
49634
|
+
const key = `${kind}:${id}`;
|
|
49635
|
+
if (seenKeys.has(key)) continue;
|
|
49636
|
+
seenKeys.add(key);
|
|
49637
|
+
hidden.push({ kind, id, observedAt: now.toISOString() });
|
|
48985
49638
|
}
|
|
48986
|
-
}
|
|
48987
|
-
|
|
49639
|
+
}
|
|
49640
|
+
if (argRefusals.length) {
|
|
49641
|
+
return { planned: false, refusals: argRefusals, note: "Nothing was saved. These arguments carry ids that cannot be checked as they stand." };
|
|
49642
|
+
}
|
|
48988
49643
|
const toVerify = [...found, ...hidden];
|
|
49644
|
+
boundAccount = account2;
|
|
48989
49645
|
const refusals = toVerify.length === 0 ? [] : verify ? await verify(account2, toVerify) : toVerify.map((f) => ({ finding: f, because: `"${f.id}" could not be checked against any account, so it is refused rather than trusted.` }));
|
|
48990
49646
|
if (refusals.length) {
|
|
48991
49647
|
return {
|
|
48992
49648
|
planned: false,
|
|
48993
49649
|
refusals: refusals.map((r) => ({ because: r.because, finding: { kind: r.finding.kind, id: r.finding.id } })),
|
|
48994
|
-
|
|
49650
|
+
// Not "these were not there": a list that could not be read in full
|
|
49651
|
+
// proves nothing is missing, and each refusal says which it was (G-M2).
|
|
49652
|
+
note: "Nothing was saved. Every finding is read back from the account before a plan is kept, and these could not be verified there. Each reason above says whether the id is missing or the list could not be read."
|
|
48995
49653
|
};
|
|
48996
49654
|
}
|
|
48997
49655
|
}
|
|
@@ -49005,6 +49663,8 @@ function registerAgencyBuildTools(server2, knownTools, verify) {
|
|
|
49005
49663
|
const a = {
|
|
49006
49664
|
id: newAutomationId(),
|
|
49007
49665
|
goal,
|
|
49666
|
+
// check.ok is true here, so the block above set it.
|
|
49667
|
+
locationId: boundAccount,
|
|
49008
49668
|
createdAt: now.toISOString(),
|
|
49009
49669
|
planId: planFingerprint(goal, plan),
|
|
49010
49670
|
findings: found,
|
|
@@ -49021,7 +49681,8 @@ function registerAgencyBuildTools(server2, knownTools, verify) {
|
|
|
49021
49681
|
const s = plan.find((x) => x.id === id);
|
|
49022
49682
|
return { step: s.id, does: s.why, tool: s.tool };
|
|
49023
49683
|
}),
|
|
49024
|
-
|
|
49684
|
+
account: a.locationId,
|
|
49685
|
+
nextStep: `Read the steps above. Nothing runs until you approve this exact plan: approve_automation with buildId ${a.id} and planId ${a.planId}. Its ids were checked against sub-account ${a.locationId}, so it is approved and run only while the server is pointed at that account.`,
|
|
49025
49686
|
note: "Change any step after this and the plan gets a new fingerprint, which voids the approval. That is on purpose: the plan you read and the plan that runs have to be the same document."
|
|
49026
49687
|
};
|
|
49027
49688
|
}
|
|
@@ -49038,6 +49699,8 @@ function registerAgencyBuildTools(server2, knownTools, verify) {
|
|
|
49038
49699
|
async ({ buildId, planId, approvedBy }) => {
|
|
49039
49700
|
const a = readAutomation(buildId);
|
|
49040
49701
|
if (!a) return { approved: false, error: `There is no build "${buildId}" on this machine.` };
|
|
49702
|
+
const wrong = accountMismatch(a, activeLocation?.());
|
|
49703
|
+
if (wrong) return { approved: false, error: wrong };
|
|
49041
49704
|
const now = /* @__PURE__ */ new Date();
|
|
49042
49705
|
const r = approve(a, approvedBy, planId, now.toISOString(), now);
|
|
49043
49706
|
if (!r.ok) return { approved: false, error: r.because };
|
|
@@ -49063,6 +49726,8 @@ function registerAgencyBuildTools(server2, knownTools, verify) {
|
|
|
49063
49726
|
async ({ buildId, stepId, state, undo, because, result, resolved: resolved2, retry }) => {
|
|
49064
49727
|
const a = readAutomation(buildId);
|
|
49065
49728
|
if (!a) return { recorded: false, error: `There is no build "${buildId}" on this machine.` };
|
|
49729
|
+
const wrong = state === "applied" ? accountMismatch(a, activeLocation?.()) : null;
|
|
49730
|
+
if (wrong) return { recorded: false, error: wrong };
|
|
49066
49731
|
const r = recordStep(a, { stepId, state, at: (/* @__PURE__ */ new Date()).toISOString(), undo, because, result, resolved: resolved2, retryOf: retry });
|
|
49067
49732
|
if (!r.ok) return { recorded: false, error: r.because };
|
|
49068
49733
|
saveAutomation(r.automation);
|
|
@@ -49111,13 +49776,16 @@ function registerAgencyBuildTools(server2, knownTools, verify) {
|
|
|
49111
49776
|
const rows2 = ids.map((id) => {
|
|
49112
49777
|
const a = all[id];
|
|
49113
49778
|
const p = position(a);
|
|
49779
|
+
const wrongAccount = p.nextStep ? accountMismatch(a, activeLocation?.()) : null;
|
|
49114
49780
|
return {
|
|
49115
49781
|
buildId: id,
|
|
49116
49782
|
goal: a.goal,
|
|
49783
|
+
account: a.locationId ?? null,
|
|
49117
49784
|
where: p.line,
|
|
49118
49785
|
phase: p.phase,
|
|
49119
49786
|
stepsDone: `${p.done} of ${p.total}`,
|
|
49120
|
-
nextStep: p.nextStep ? { step: p.nextStep.id, does: p.nextStep.why, tool: p.nextStep.tool } : null,
|
|
49787
|
+
nextStep: p.nextStep && !wrongAccount ? { step: p.nextStep.id, does: p.nextStep.why, tool: p.nextStep.tool } : null,
|
|
49788
|
+
...wrongAccount ? { wrongAccount } : {},
|
|
49121
49789
|
stalledOn: p.stalledOn,
|
|
49122
49790
|
blocked: p.blocked,
|
|
49123
49791
|
approvedBy: a.approval?.by ?? null,
|
|
@@ -49142,6 +49810,7 @@ var init_agency_build = __esm({
|
|
|
49142
49810
|
init_automation();
|
|
49143
49811
|
init_store5();
|
|
49144
49812
|
init_verify_findings();
|
|
49813
|
+
init_arg_ids();
|
|
49145
49814
|
targetSchema = import_zod74.z.union([
|
|
49146
49815
|
import_zod74.z.object({ existing: import_zod74.z.string().describe("A real id, which must appear in `findings`."), kind: import_zod74.z.string() }),
|
|
49147
49816
|
import_zod74.z.object({ ref: import_zod74.z.string().describe("Something an earlier step in this plan creates."), kind: import_zod74.z.string() })
|
|
@@ -53085,8 +53754,13 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
53085
53754
|
});
|
|
53086
53755
|
}
|
|
53087
53756
|
if (!briefArg.scope) {
|
|
53088
|
-
const
|
|
53089
|
-
|
|
53757
|
+
const board = readBoardState();
|
|
53758
|
+
const saved2 = board.scope?.[activeLocation];
|
|
53759
|
+
if (saved2?.wholePreset) {
|
|
53760
|
+
scopeSource = "cockpit";
|
|
53761
|
+
} else if (!saved2 && Array.isArray(board.assessmentPurchases?.[activeLocation])) {
|
|
53762
|
+
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error: NOT_CONFIRMED_SCOPE });
|
|
53763
|
+
} else if (saved2 && Array.isArray(saved2.features)) {
|
|
53090
53764
|
const features = saved2.features.filter(isFeatureId);
|
|
53091
53765
|
if (!features.length) {
|
|
53092
53766
|
return jsonResponse({
|
|
@@ -53143,6 +53817,31 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
53143
53817
|
if (!gate2.ok) return jsonResponse({ ok: false, phase: "plan", error: gate2.error, savedPlan: saved ? { planId: saved.planId, savedAt: saved.savedAt } : void 0 });
|
|
53144
53818
|
const plan = gate2.plan;
|
|
53145
53819
|
const planSource = gate2.source;
|
|
53820
|
+
if (planSource === "saved") {
|
|
53821
|
+
const board = readBoardState();
|
|
53822
|
+
const current = board.scope?.[activeLocation];
|
|
53823
|
+
if (!current && Array.isArray(board.assessmentPurchases?.[activeLocation])) {
|
|
53824
|
+
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error: NOT_CONFIRMED_SCOPE });
|
|
53825
|
+
}
|
|
53826
|
+
if (current && !current.wholePreset && Array.isArray(current.features) && !current.features.some(isFeatureId)) {
|
|
53827
|
+
return jsonResponse({
|
|
53828
|
+
ok: false,
|
|
53829
|
+
phase: "scope",
|
|
53830
|
+
scopeSource: "cockpit",
|
|
53831
|
+
error: "No features are selected for this client, so nothing was built, not even from the saved plan. Open the client's Review screen, tick the features to build, confirm, and run the build again."
|
|
53832
|
+
});
|
|
53833
|
+
}
|
|
53834
|
+
if (!savedPlanMatchesScope(saved, current)) {
|
|
53835
|
+
const planned = saved?.scope ? [...saved.scope.built, ...(saved.scope.omitted ?? []).map((f) => f.id)].join(", ") || "no features" : "everything in the preset";
|
|
53836
|
+
const now = current?.wholePreset ? "everything in the preset (confirmed)" : current ? current.features.filter(isFeatureId).join(", ") : "everything in the preset (nothing is saved)";
|
|
53837
|
+
return jsonResponse({
|
|
53838
|
+
ok: false,
|
|
53839
|
+
phase: "scope",
|
|
53840
|
+
scopeSource: "cockpit",
|
|
53841
|
+
error: `The features selected for this client changed after this plan was saved (the saved plan builds: ${planned}; the Review screen now says: ${now}), so the saved plan is not what was confirmed and nothing was built. Run the build again without useSavedPlan, composing the plan fresh from the preset (fromPreset), so it follows the current selection.`
|
|
53842
|
+
});
|
|
53843
|
+
}
|
|
53844
|
+
}
|
|
53146
53845
|
const scopeForValidation = composed?.scope ?? (planSource === "saved" ? saved?.scope : void 0);
|
|
53147
53846
|
const validation = validateBuildPlan(plan, { nurtureNotBought: Boolean(scopeForValidation && !scopeForValidation.built.includes("nurture")) });
|
|
53148
53847
|
if (!validation.valid || !validation.plan) {
|
|
@@ -53211,7 +53910,8 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
53211
53910
|
contentHash: contentHash(typedPlan),
|
|
53212
53911
|
inputHash: process.env.GHL_COMMAND_INTAKE_HASH || saved?.inputHash,
|
|
53213
53912
|
dependencies: planDependencies(typedPlan),
|
|
53214
|
-
preview: result
|
|
53913
|
+
preview: result,
|
|
53914
|
+
scope: scopeForValidation
|
|
53215
53915
|
});
|
|
53216
53916
|
const deps = makeExecuteDeps(client, builderClient, activeLocation, registry2);
|
|
53217
53917
|
const gatedWorkflowRefs = new Set(result.workflows.filter((w) => w.gatedBy.length > 0).map((w) => w.ref));
|
|
@@ -53823,7 +54523,7 @@ function intakeFormMatches(expected, actual) {
|
|
|
53823
54523
|
const keys = ["id", "tag", "type", "hiddenFieldQueryKey", "required", "label", "options", "picklistOptions"];
|
|
53824
54524
|
return expected.every((field, i) => keys.every((key) => field[key] === void 0 || JSON.stringify(field[key]) === JSON.stringify(actual[i]?.[key])));
|
|
53825
54525
|
}
|
|
53826
|
-
var import_zod81, customFieldItemSchema, sleep6;
|
|
54526
|
+
var import_zod81, customFieldItemSchema, sleep6, NOT_CONFIRMED_SCOPE;
|
|
53827
54527
|
var init_intake_to_build = __esm({
|
|
53828
54528
|
"src/tools/intake-to-build.ts"() {
|
|
53829
54529
|
"use strict";
|
|
@@ -53869,6 +54569,7 @@ var init_intake_to_build = __esm({
|
|
|
53869
54569
|
picklistOptions: import_zod81.z.array(import_zod81.z.string()).optional()
|
|
53870
54570
|
}).passthrough();
|
|
53871
54571
|
sleep6 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
54572
|
+
NOT_CONFIRMED_SCOPE = "What this client bought is recorded, but nobody has confirmed what to build, so nothing was built. Open the client's Review screen: the features are ticked from what they bought. Change them if you need to, save to confirm, and run the build again.";
|
|
53872
54573
|
}
|
|
53873
54574
|
});
|
|
53874
54575
|
|
|
@@ -53915,7 +54616,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env2 = process
|
|
|
53915
54616
|
}
|
|
53916
54617
|
const reader = token2 ? new GHLClient({ apiKey: token2.apiKey, locationId: loc }) : client;
|
|
53917
54618
|
return refuseUnseenFindings2((path40, params) => reader.get(path40, params ? { params } : void 0), loc, findings);
|
|
53918
|
-
});
|
|
54619
|
+
}, () => client.defaultLocationId);
|
|
53919
54620
|
registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
|
|
53920
54621
|
registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
|
|
53921
54622
|
registerDailyBriefTools(wrap(DAILY_BRIEF_MODULE), registry2);
|