@elitedcs/ghl-mcp 3.93.1 → 3.94.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 +38 -0
- package/README.md +15 -10
- package/dist/index.js +1019 -232
- package/guide/guide.html +3 -8
- package/package.json +1 -1
- package/skills/blueprint/examples/validate-plan.cjs +9 -0
- package/skills/blueprint/presets/clinic.preset.json +49 -5
- package/skills/blueprint/presets/coach.preset.json +48 -4
- package/skills/blueprint/presets/ecommerce.preset.json +46 -2
- package/skills/blueprint/presets/generic-client.preset.json +47 -3
- package/skills/blueprint/presets/local-service.preset.json +48 -4
- package/skills/blueprint/presets/med-spa.preset.json +48 -4
- package/skills/blueprint/presets/wellness-clinic.preset.json +122 -12
package/dist/index.js
CHANGED
|
@@ -1717,12 +1717,12 @@ function looksLikeCredential(value) {
|
|
|
1717
1717
|
function maskUrl(url, rec2 = newRecorder(), path67 = []) {
|
|
1718
1718
|
let out = url.replace(USERINFO_RE, (_m, pre, pass, at) => `${pre}${maskCredential(pass, rec2, path67, "@")}${at}`);
|
|
1719
1719
|
const seen = /* @__PURE__ */ new Map();
|
|
1720
|
-
out = out.replace(QUERY_RE, (m,
|
|
1720
|
+
out = out.replace(QUERY_RE, (m, sep7, name2, val) => {
|
|
1721
1721
|
const decoded2 = decodeName(name2);
|
|
1722
1722
|
const n = seen.get(decoded2) ?? 0;
|
|
1723
1723
|
seen.set(decoded2, n + 1);
|
|
1724
1724
|
if (!val || !isSensitiveName(decoded2)) return m;
|
|
1725
|
-
return `${
|
|
1725
|
+
return `${sep7}${name2}=${maskCredential(val, rec2, path67, `?${decoded2}#${n}`)}`;
|
|
1726
1726
|
});
|
|
1727
1727
|
return out;
|
|
1728
1728
|
}
|
|
@@ -1911,7 +1911,7 @@ function redactText(text3) {
|
|
|
1911
1911
|
out = out.replace(/\b(Bearer|Basic)(\s+)([A-Za-z0-9._~+/=-]{8,})/g, (_m, s, sp, v) => `${s}${sp}${maskToken(v)}`);
|
|
1912
1912
|
out = out.replace(
|
|
1913
1913
|
/\b([A-Za-z0-9_-]*(?:key|token|secret|auth|signature|passw)[A-Za-z0-9_-]*)(\s*[:=]\s*)([^\s,;"'&<>]{8,})/gi,
|
|
1914
|
-
(m, name2,
|
|
1914
|
+
(m, name2, sep7, val) => MASK_PRESENT_RE.test(val) || /^(bearer|basic)$/i.test(val) || !isSensitiveName(name2) ? m : `${name2}${sep7}${maskToken(val)}`
|
|
1915
1915
|
);
|
|
1916
1916
|
return out;
|
|
1917
1917
|
}
|
|
@@ -2020,11 +2020,11 @@ function restoreMaskedSecrets(incoming, current) {
|
|
|
2020
2020
|
};
|
|
2021
2021
|
let out = str16.replace(USERINFO_RE, (m, pre, pass, at) => MASK_PRESENT_RE.test(pass) ? `${pre}${whole(pass, "@")}${at}` : m);
|
|
2022
2022
|
const seen = /* @__PURE__ */ new Map();
|
|
2023
|
-
out = out.replace(QUERY_RE, (m,
|
|
2023
|
+
out = out.replace(QUERY_RE, (m, sep7, name2, val) => {
|
|
2024
2024
|
const decoded2 = decodeName(name2);
|
|
2025
2025
|
const n = seen.get(decoded2) ?? 0;
|
|
2026
2026
|
seen.set(decoded2, n + 1);
|
|
2027
|
-
return MASK_PRESENT_RE.test(val) ? `${
|
|
2027
|
+
return MASK_PRESENT_RE.test(val) ? `${sep7}${name2}=${whole(val, `?${decoded2}#${n}`)}` : m;
|
|
2028
2028
|
});
|
|
2029
2029
|
if (MASK_PRESENT_RE.test(out)) out = whole(out, "=");
|
|
2030
2030
|
return out;
|
|
@@ -10998,18 +10998,18 @@ async function readAgent(client, agentId) {
|
|
|
10998
10998
|
return a;
|
|
10999
10999
|
}
|
|
11000
11000
|
async function updateAgentFields(client, agentId, change) {
|
|
11001
|
-
const
|
|
11002
|
-
for (const [k, val] of Object.entries(change)) if (val !== void 0)
|
|
11003
|
-
if (Object.keys(
|
|
11001
|
+
const clean6 = {};
|
|
11002
|
+
for (const [k, val] of Object.entries(change)) if (val !== void 0) clean6[k] = val;
|
|
11003
|
+
if (Object.keys(clean6).length === 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
|
|
11004
11004
|
const before = await readAgent(client, agentId);
|
|
11005
|
-
const body = { ...promptTrio(before), ...
|
|
11005
|
+
const body = { ...promptTrio(before), ...clean6 };
|
|
11006
11006
|
const merged = promptTrio(body);
|
|
11007
11007
|
if (!merged.personality || !merged.goal || !merged.instructions) {
|
|
11008
11008
|
throw new Error(`Agent ${agentId} read back without its personality/goal/instructions, and GoHighLevel refuses an update that does not carry all three. Pass all three explicitly (get_conversation_agent shows what is stored).`);
|
|
11009
11009
|
}
|
|
11010
11010
|
await client.put(`/conversation-ai/agents/${agentId}`, { version: AI_API_VERSION, body });
|
|
11011
11011
|
const after = await readAgent(client, agentId);
|
|
11012
|
-
return { before, changed: Object.keys(
|
|
11012
|
+
return { before, changed: Object.keys(clean6), after };
|
|
11013
11013
|
}
|
|
11014
11014
|
function registerConversationAiTools(server2, client) {
|
|
11015
11015
|
const v = { version: AI_API_VERSION };
|
|
@@ -21145,12 +21145,12 @@ function deriveBrandDna(raw, opts = {}) {
|
|
|
21145
21145
|
fixed = true;
|
|
21146
21146
|
}
|
|
21147
21147
|
const vb = svg.match(/viewBox=["']\s*[\d.-]+\s+[\d.-]+\s+([\d.]+)\s+([\d.]+)/i);
|
|
21148
|
-
const
|
|
21149
|
-
if (
|
|
21150
|
-
logoSvg =
|
|
21151
|
-
logo = { inlineSvg:
|
|
21148
|
+
const clean6 = sanitizeSvgOrDrop(svg);
|
|
21149
|
+
if (clean6) {
|
|
21150
|
+
logoSvg = clean6;
|
|
21151
|
+
logo = { inlineSvg: clean6, whiteFillFixed: fixed, source: bl.where, rights: opts.rights ? "declared" : "inherits", ...vb && { width: parseFloat(vb[1]), height: parseFloat(vb[2]) } };
|
|
21152
21152
|
declared++;
|
|
21153
|
-
evidence["logo"] = `${bl.where}${fixed ? "; white fills replaced with currentColor" : ""}${
|
|
21153
|
+
evidence["logo"] = `${bl.where}${fixed ? "; white fills replaced with currentColor" : ""}${clean6 !== svg ? "; scripts, handlers and outside references stripped" : ""}`;
|
|
21154
21154
|
} else {
|
|
21155
21155
|
evidence["logo"] = `${bl.where}: not a plain drawing (or over 60 KB), so it was not kept`;
|
|
21156
21156
|
}
|
|
@@ -24340,6 +24340,8 @@ var init_features = __esm({
|
|
|
24340
24340
|
BY_WORKFLOW_REF = {
|
|
24341
24341
|
"workflow.speed_to_lead": "speed_to_lead",
|
|
24342
24342
|
"workflow.missed_call": "missed_call",
|
|
24343
|
+
// F6 (2026-10-29): a missed caller with no card gets one. Goes with the text-back it hangs off.
|
|
24344
|
+
"workflow.missed_call_card": "missed_call",
|
|
24343
24345
|
"workflow.lead_nurture": "nurture",
|
|
24344
24346
|
"workflow.win_back": "winback",
|
|
24345
24347
|
"workflow.appointment_booked": "booking",
|
|
@@ -26084,10 +26086,14 @@ var init_registry = __esm({
|
|
|
26084
26086
|
whatItIs: "Google sign-in gives this machine permission to read your calendar without pasting a password here.",
|
|
26085
26087
|
checkedOn: null,
|
|
26086
26088
|
gives: "Adds your own day beside your clients' appointments as a separate part of the brief.",
|
|
26087
|
-
|
|
26088
|
-
|
|
26089
|
+
// ba's Hub audit (2026-10-29): the guide read as if this connects today, while CHANGELOG 3.92.2 and the card say
|
|
26090
|
+
// CALENDAR_SETUP_MESSAGE. It does not connect until GHL Command ships its own Google sign-in, so the guide says so
|
|
26091
|
+
// first and the steps below are what it will be.
|
|
26092
|
+
needs: ["A Google account with a calendar you can read.", "Google sign-in for GHL Command, which is not available yet."],
|
|
26093
|
+
before: `${CALENDAR_SETUP_MESSAGE} Choosing Connect says so and saves nothing. When Google sign-in arrives in an update, these are the steps:`,
|
|
26089
26094
|
steps: ["On the Google Calendar card on the Connections screen, choose Connect to open Google sign-in.", "Select your Google account and allow read-only calendar access.", "Return to the Google Calendar card and choose Finish. Your sign-in is saved only after your calendars can be read."],
|
|
26090
|
-
say: [
|
|
26095
|
+
say: [],
|
|
26096
|
+
// none until Google sign-in ships: each one would fail (ba audit 2026-10-29)
|
|
26091
26097
|
revoke: "Choose Disconnect on this system's Connections card to remove the saved sign-in and request revocation with Google. You can also remove the app in your Google Account's third-party connections at myaccount.google.com/connections.",
|
|
26092
26098
|
tools: { connect: "connect_google_calendar", disconnect: "connect_google_calendar", read: "read_my_calendar" }
|
|
26093
26099
|
},
|
|
@@ -32391,7 +32397,7 @@ async function brainWireGhl(deps, args) {
|
|
|
32391
32397
|
}
|
|
32392
32398
|
const specs = wiringSpecs({ testTag: args.testTag, leadTag });
|
|
32393
32399
|
const held2 = [];
|
|
32394
|
-
const
|
|
32400
|
+
const clean6 = (e) => scrub(e instanceof Error ? e.message : String(e), held2);
|
|
32395
32401
|
const survey = async () => {
|
|
32396
32402
|
const refuse4 = (error) => ({ refusal: { ok: false, wrote: "nothing", error } });
|
|
32397
32403
|
const meta = deps.meta();
|
|
@@ -32400,7 +32406,7 @@ async function brainWireGhl(deps, args) {
|
|
|
32400
32406
|
try {
|
|
32401
32407
|
list4 = await writer.listWorkflows();
|
|
32402
32408
|
} catch (e) {
|
|
32403
|
-
return refuse4(`The full list of this account's workflows could not be read from GoHighLevel, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${
|
|
32409
|
+
return refuse4(`The full list of this account's workflows could not be read from GoHighLevel, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${clean6(e)} Run brain_wire_ghl again.`);
|
|
32404
32410
|
}
|
|
32405
32411
|
const found = [];
|
|
32406
32412
|
for (const { id: id2 } of list4) {
|
|
@@ -32408,7 +32414,7 @@ async function brainWireGhl(deps, args) {
|
|
|
32408
32414
|
try {
|
|
32409
32415
|
read2 = await writer.getWorkflow(id2);
|
|
32410
32416
|
} catch (e) {
|
|
32411
|
-
return refuse4(`Workflow ${id2} could not be read from GoHighLevel, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${
|
|
32417
|
+
return refuse4(`Workflow ${id2} could not be read from GoHighLevel, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${clean6(e)} Run brain_wire_ghl again.`);
|
|
32412
32418
|
}
|
|
32413
32419
|
const shape = readShape(read2, id2);
|
|
32414
32420
|
if (shape.kind === "uncertain") return refuse4(`Workflow ${id2} could not be judged: ${shape.why}, so the brain cannot show it has every one of its workflows in view. Nothing was changed. Run brain_wire_ghl again.`);
|
|
@@ -32432,7 +32438,7 @@ async function brainWireGhl(deps, args) {
|
|
|
32432
32438
|
read2 = await writer.getWorkflow(id2);
|
|
32433
32439
|
} catch (e) {
|
|
32434
32440
|
if (isNotFound(e)) continue;
|
|
32435
|
-
return refuse4(`Workflow ${id2} is saved on this computer but GoHighLevel's list did not show it, and it could not be read, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${
|
|
32441
|
+
return refuse4(`Workflow ${id2} is saved on this computer but GoHighLevel's list did not show it, and it could not be read, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${clean6(e)} Run brain_wire_ghl again.`);
|
|
32436
32442
|
}
|
|
32437
32443
|
const shape = readShape(read2, id2);
|
|
32438
32444
|
if (shape.kind === "uncertain") return refuse4(`Workflow ${id2} is saved on this computer but could not be judged: ${shape.why}. Nothing was changed. Run brain_wire_ghl again.`);
|
|
@@ -32570,7 +32576,7 @@ async function brainWireGhl(deps, args) {
|
|
|
32570
32576
|
if ((await writer.getWorkflow(id2)).status !== "published") await fenced.publishWorkflow(id2);
|
|
32571
32577
|
} catch (e) {
|
|
32572
32578
|
if (e instanceof LeaseLost) throw e;
|
|
32573
|
-
error =
|
|
32579
|
+
error = clean6(e);
|
|
32574
32580
|
}
|
|
32575
32581
|
if (!id2) {
|
|
32576
32582
|
results.push({ workflow: spec.workflowName, built: false, error });
|
|
@@ -32610,7 +32616,7 @@ async function brainWireGhl(deps, args) {
|
|
|
32610
32616
|
});
|
|
32611
32617
|
} catch (e) {
|
|
32612
32618
|
if (e instanceof LeaseLost) throw e;
|
|
32613
|
-
results.push({ workflow: spec.workflowName, id: id2, refreshed: false, readBack: `could not be read back: ${
|
|
32619
|
+
results.push({ workflow: spec.workflowName, id: id2, refreshed: false, readBack: `could not be read back: ${clean6(e)}. Run brain_wire_ghl with rewire: true again to finish`, ...error ? { error } : {} });
|
|
32614
32620
|
}
|
|
32615
32621
|
}
|
|
32616
32622
|
if (staleTest) {
|
|
@@ -32620,7 +32626,7 @@ async function brainWireGhl(deps, args) {
|
|
|
32620
32626
|
results.push({ workflow: "earlier test-tag workflow", id: staleTest, removed: true });
|
|
32621
32627
|
} catch (e) {
|
|
32622
32628
|
if (e instanceof LeaseLost) throw e;
|
|
32623
|
-
results.push({ workflow: "earlier test-tag workflow", id: staleTest, removed: false, error:
|
|
32629
|
+
results.push({ workflow: "earlier test-tag workflow", id: staleTest, removed: false, error: clean6(e), note: "It carries the brain's current key, so it keeps working. Run brain_wire_ghl with rewire: true again to remove it." });
|
|
32624
32630
|
}
|
|
32625
32631
|
}
|
|
32626
32632
|
for (const r of results) {
|
|
@@ -34592,8 +34598,8 @@ function datePatterns(value) {
|
|
|
34592
34598
|
if (!y || !(mo >= 1 && mo <= 12) || !(d >= 1 && d <= 31)) return [];
|
|
34593
34599
|
const year = `(?:${y}|${y.slice(2)})`;
|
|
34594
34600
|
const n = (x) => `0?${x}`;
|
|
34595
|
-
const
|
|
34596
|
-
const out = [`${y}${
|
|
34601
|
+
const sep7 = "[-/.\\s]";
|
|
34602
|
+
const out = [`${y}${sep7}${n(mo)}${sep7}${n(d)}`, `${n(mo)}${sep7}${n(d)}${sep7}${year}`, `${n(d)}${sep7}${n(mo)}${sep7}${year}`];
|
|
34597
34603
|
const month = `(?:${MONTHS2[mo - 1]}|${MONTHS2[mo - 1].slice(0, 3)})\\.?`;
|
|
34598
34604
|
const day = `0?${d}(?:st|nd|rd|th)?`;
|
|
34599
34605
|
out.push(`${month}\\s+${day},?\\s+${y}`, `${day}\\s+(?:of\\s+)?${month},?\\s+${y}`);
|
|
@@ -36637,8 +36643,8 @@ function sanitizeEngagements(raw) {
|
|
|
36637
36643
|
for (const [id2, rec2] of Object.entries(src).slice(0, MAX_CLIENTS)) {
|
|
36638
36644
|
const key2 = str4(id2, 64);
|
|
36639
36645
|
if (!key2 || FORBIDDEN_KEY.has(key2)) continue;
|
|
36640
|
-
const
|
|
36641
|
-
if (
|
|
36646
|
+
const clean6 = sanitizeEngagement(rec2);
|
|
36647
|
+
if (clean6) clients[key2] = clean6;
|
|
36642
36648
|
}
|
|
36643
36649
|
}
|
|
36644
36650
|
return { v: 1, clients: { ...clients }, updated_at: str4(r.updated_at, 40) };
|
|
@@ -40663,7 +40669,33 @@ function buildRunRecord(input) {
|
|
|
40663
40669
|
};
|
|
40664
40670
|
}
|
|
40665
40671
|
function appendBuildStart(input, base) {
|
|
40666
|
-
return appendJournal({ kind: "build-start", runId: input.runId, locationId: input.locationId, locationName: input.locationName, at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(), planId: input.planId }, base);
|
|
40672
|
+
return appendJournal({ kind: "build-start", runId: input.runId, locationId: input.locationId, locationName: input.locationName, at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(), planId: input.planId, ...input.planHash ? { planHash: input.planHash } : {} }, base);
|
|
40673
|
+
}
|
|
40674
|
+
function createdByOtherPlans(locationId2, plan, base) {
|
|
40675
|
+
const records = readJournal(locationId2, base);
|
|
40676
|
+
const starts = /* @__PURE__ */ new Map();
|
|
40677
|
+
for (const r of records) if (r.kind === "build-start") starts.set(r.runId, r);
|
|
40678
|
+
const removedBy = /* @__PURE__ */ new Map();
|
|
40679
|
+
const removed = (runId) => {
|
|
40680
|
+
let m = removedBy.get(runId);
|
|
40681
|
+
if (!m) {
|
|
40682
|
+
m = alreadyRemovedIds(locationId2, runId, base);
|
|
40683
|
+
removedBy.set(runId, m);
|
|
40684
|
+
}
|
|
40685
|
+
return m;
|
|
40686
|
+
};
|
|
40687
|
+
const other = (runId) => {
|
|
40688
|
+
const st = starts.get(runId);
|
|
40689
|
+
if (!st) return false;
|
|
40690
|
+
return st.planHash ? st.planHash !== plan.planHash : st.planId !== plan.planId;
|
|
40691
|
+
};
|
|
40692
|
+
const out = /* @__PURE__ */ new Map();
|
|
40693
|
+
for (const r of records) {
|
|
40694
|
+
if (r.kind !== "build-object" || !other(r.runId)) continue;
|
|
40695
|
+
const o = r.object;
|
|
40696
|
+
if (o?.action === "created" && o.ghlId && !out.has(o.ghlId) && !removed(r.runId).has(o.ghlId)) out.set(o.ghlId, { type: o.type, name: o.name, runId: r.runId });
|
|
40697
|
+
}
|
|
40698
|
+
return out;
|
|
40667
40699
|
}
|
|
40668
40700
|
function appendBuildIntent(input, base) {
|
|
40669
40701
|
return appendJournal({ kind: "build-intent", runId: input.runId, locationId: input.locationId, at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(), intent: { type: input.type, name: input.name } }, base);
|
|
@@ -41518,7 +41550,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
41518
41550
|
engineAcct = { engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked };
|
|
41519
41551
|
if (!engineCheck.ok) {
|
|
41520
41552
|
onEvent({ kind: "error", line: engineCheck.reason });
|
|
41521
|
-
resolve15({ ok: false, error: engineCheck.reason, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
|
|
41553
|
+
resolve15({ ok: false, error: engineCheck.reason, engineCheckOk: false, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
|
|
41522
41554
|
return;
|
|
41523
41555
|
}
|
|
41524
41556
|
onEvent({ kind: "status", line: `Starting ${spec.name} for ${locationName}.` });
|
|
@@ -43003,9 +43035,41 @@ var init_handoff_run = __esm({
|
|
|
43003
43035
|
}
|
|
43004
43036
|
});
|
|
43005
43037
|
|
|
43038
|
+
// src/stage-failure.ts
|
|
43039
|
+
function claudeSignedOut(error, lines = []) {
|
|
43040
|
+
return [typeof error === "string" ? error : "", ...lines].some((l) => CLAUDE_SIGNED_OUT.test(String(l)));
|
|
43041
|
+
}
|
|
43042
|
+
function failureNoteFor(stage, outcome, lines, already) {
|
|
43043
|
+
if (stage === 4 || outcome.ok || already || outcome.summary || outcome.issues?.length) return null;
|
|
43044
|
+
const s = stageFailureSummary(stage, outcome.error, lines);
|
|
43045
|
+
return { ...outcome, summary: s, issues: [], plain: { summary: s, issues: [] } };
|
|
43046
|
+
}
|
|
43047
|
+
function stageFailureSummary(stage, error, lines = []) {
|
|
43048
|
+
const step = STEP_NAME[stage] ?? "This step";
|
|
43049
|
+
const again = AGAIN[stage];
|
|
43050
|
+
const safe = READ_ONLY_STAGES.has(stage) ? " Nothing in the account was changed." : "";
|
|
43051
|
+
if (claudeSignedOut(error, lines)) {
|
|
43052
|
+
return `${step} did not run: Claude Code is signed out on this computer. Sign in to Claude Code again (type /login in Claude Code), then click ${again ? `"${again}"` : "the button"} again.${safe}`;
|
|
43053
|
+
}
|
|
43054
|
+
const why = typeof error === "string" && error.trim() ? ` ${clean3(error).replace(/[.]?$/, ".")}` : "";
|
|
43055
|
+
return `${step} did not finish.${why}${safe}${READ_ONLY_STAGES.has(stage) && again ? ` You can click "${again}" again.` : ""}`;
|
|
43056
|
+
}
|
|
43057
|
+
var CLAUDE_SIGNED_OUT, READ_ONLY_STAGES, STEP_NAME, AGAIN, clean3;
|
|
43058
|
+
var init_stage_failure = __esm({
|
|
43059
|
+
"src/stage-failure.ts"() {
|
|
43060
|
+
"use strict";
|
|
43061
|
+
CLAUDE_SIGNED_OUT = /\bnot logged in\b|please run \/login/i;
|
|
43062
|
+
READ_ONLY_STAGES = /* @__PURE__ */ new Set([1, 4]);
|
|
43063
|
+
STEP_NAME = { 1: "The Brief", 3: "Build", 4: "Verify" };
|
|
43064
|
+
AGAIN = { 1: "Write the Brief", 3: "Build", 4: "Verify" };
|
|
43065
|
+
clean3 = (s) => s.replace(/\s+/g, " ").replace(/\s*—\s*/g, ". ").trim().slice(0, 300);
|
|
43066
|
+
}
|
|
43067
|
+
});
|
|
43068
|
+
|
|
43006
43069
|
// src/member-progress.ts
|
|
43007
|
-
function memberProgress(lines, running, ok) {
|
|
43008
|
-
const
|
|
43070
|
+
function memberProgress(lines, running, ok, stage) {
|
|
43071
|
+
const failed = stage !== void 0 && READ_ONLY_STAGES.has(stage) ? "This step did not finish. It changes nothing in the account, so it is safe to run again." : "This step did not finish. Review the account before retrying.";
|
|
43072
|
+
const result = [running ? "Command OS is working on this step." : ok === true ? "This step finished. Review the result below." : failed];
|
|
43009
43073
|
if (lines.some((l) => /writing.*review/i.test(l))) result.push("Command OS is preparing the client review.");
|
|
43010
43074
|
if (lines.some((l) => /plain-English version/i.test(l))) result.push("Command OS is preparing the result for review.");
|
|
43011
43075
|
for (const line4 of lines) if (/^(This step used |This step: cost not reported[.]|Checking Claude Code still works with Command OS used |Plain rewrite: (cost not reported|\$|under ))/.test(line4)) result.push(line4.replace(/—/g, "."));
|
|
@@ -43339,6 +43403,7 @@ var VERIFY_PASSED_COUNTING, readLabels, readLabel, BUILDER_SIGNIN_BODY, BUILDER_
|
|
|
43339
43403
|
var init_member_progress = __esm({
|
|
43340
43404
|
"src/member-progress.ts"() {
|
|
43341
43405
|
"use strict";
|
|
43406
|
+
init_stage_failure();
|
|
43342
43407
|
init_workflow_read_connection();
|
|
43343
43408
|
VERIFY_PASSED_COUNTING = "The check finished. The list of jobs left for you is on the Manual tasks step.";
|
|
43344
43409
|
readLabels = { user: "staff list", staff: "staff list", pipeline: "pipeline list", stage: "pipeline stage list", custom_field: "custom field list", tag: "tag list", custom_value: "custom value list", calendar: "calendar list", form: "form list", funnel: "funnel list", page: "page content", workflow: "workflow list", survey: "survey list", email_template: "email template list", sms_template: "SMS template list" };
|
|
@@ -43423,7 +43488,10 @@ function plainFallback(text3, opts = {}) {
|
|
|
43423
43488
|
if (!/[.!?]$/.test(s)) s += ".";
|
|
43424
43489
|
if (opts.nextStep && !/Settings|then |→/.test(s)) {
|
|
43425
43490
|
const hint = nextStepHint(text3);
|
|
43426
|
-
if (hint
|
|
43491
|
+
if (hint && !opts.given?.has(hint)) {
|
|
43492
|
+
s += " " + hint;
|
|
43493
|
+
opts.given?.add(hint);
|
|
43494
|
+
}
|
|
43427
43495
|
}
|
|
43428
43496
|
return s.replace(/—/g, ".").replace(/Brand DNA/gi, "your brand").replace(/Agency OS/gi, "Command OS").replace(/\bMCP\b/g, "GHL Command");
|
|
43429
43497
|
}
|
|
@@ -43431,7 +43499,10 @@ function plainFallbackOutcome(raw) {
|
|
|
43431
43499
|
if (raw.verification) return renderVerifyReport(raw.verification, plainFallbackOutcome({ summary: raw.summary, issues: raw.issues }));
|
|
43432
43500
|
return {
|
|
43433
43501
|
...raw.summary && { summary: plainFallback(raw.summary) },
|
|
43434
|
-
...raw.issues?.length && { issues:
|
|
43502
|
+
...raw.issues?.length && { issues: (() => {
|
|
43503
|
+
const given = /* @__PURE__ */ new Set();
|
|
43504
|
+
return raw.issues.map((x) => plainFallback(x, { nextStep: true, given }));
|
|
43505
|
+
})() }
|
|
43435
43506
|
};
|
|
43436
43507
|
}
|
|
43437
43508
|
function parseOutcomeRewrite(text3, raw) {
|
|
@@ -43533,6 +43604,7 @@ function goLiveTopic(text3) {
|
|
|
43533
43604
|
function jobTiming(text3) {
|
|
43534
43605
|
const t = String(text3).trim();
|
|
43535
43606
|
if (t === HANDOFF_NO_HOME_JOB) return "later";
|
|
43607
|
+
if (HELD_JOB.test(t)) return "later";
|
|
43536
43608
|
if (LEAD_MAGNET_PAGES_JOB.test(t)) return "later";
|
|
43537
43609
|
if (SWITCH_ON_JOB.test(t) || STILL_OFF_NOTE.test(t)) return "at-go-live";
|
|
43538
43610
|
if (LATER_JOB.test(t)) return "later";
|
|
@@ -43649,11 +43721,12 @@ function manualSignOffRefusal(tasks2, verifyHasRun) {
|
|
|
43649
43721
|
}
|
|
43650
43722
|
return "Everything is ticked, but the check has not confirmed it yet. Run the check again and this will clear on its own.";
|
|
43651
43723
|
}
|
|
43652
|
-
var SELF_REPORTED, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
|
|
43724
|
+
var SELF_REPORTED, HELD_JOB, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
|
|
43653
43725
|
var init_manual_tasks = __esm({
|
|
43654
43726
|
"src/manual-tasks.ts"() {
|
|
43655
43727
|
"use strict";
|
|
43656
43728
|
SELF_REPORTED = /^[^:]+: Bought, but not built automatically yet\./;
|
|
43729
|
+
HELD_JOB = /^(?:Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
|
|
43657
43730
|
SWITCH_ON_JOB = /^[^\n]+: This workflow is built and waiting for you to switch it on in GoHighLevel under Automation > Workflows\.$/;
|
|
43658
43731
|
LATER_JOB = /^Manual action: (?:[^:\n]*\b(?:after (?:a|the|each) (?:trial )?visit|before any bulk)\b[^:\n]*|How [^:\n]{1,80} starts): /;
|
|
43659
43732
|
LEAD_MAGNET_PAGES_JOB = /^[^:\n]+: Bought, but not built automatically yet\. The form is built\. The pages are separate work\.$/;
|
|
@@ -43668,6 +43741,7 @@ var init_manual_tasks = __esm({
|
|
|
43668
43741
|
{ key: "price", title: "Check the trial price, deposit and balance", re: /trial price|deposit amount|balance amount|trial_price/i }
|
|
43669
43742
|
];
|
|
43670
43743
|
NAMED_LATER = [
|
|
43744
|
+
HELD_JOB,
|
|
43671
43745
|
/^[^:\n]+: Bought, but not built automatically yet\. (?:Set up rebooking and recall|Payment setup is done|Set up referral requests|The form is built|It is not in the plan)/,
|
|
43672
43746
|
/^Manual action: Set up the trial balance link\b/,
|
|
43673
43747
|
/^Manual action: Limit the Trial Balance Paid workflow\b/,
|
|
@@ -43951,7 +44025,7 @@ function deriveQueue(input) {
|
|
|
43951
44025
|
title: title(next, name2),
|
|
43952
44026
|
needs: "you",
|
|
43953
44027
|
action: opensStep ? "open" : "mark",
|
|
43954
|
-
why: handRefusal ? `${handRefusal} Open step
|
|
44028
|
+
why: handRefusal ? `${handRefusal} Open step 12 to see them.` : next === 6 ? `${step.why} Open step 13 to read the list and sign off.` : step.why,
|
|
43955
44029
|
rank: RANK.waitingOnYou
|
|
43956
44030
|
});
|
|
43957
44031
|
continue;
|
|
@@ -44185,7 +44259,7 @@ function beyondGoHighLevel(live, engagements, roster, beyond = { ownCalendarConn
|
|
|
44185
44259
|
}
|
|
44186
44260
|
clients.sort((a, b) => a.client.localeCompare(b.client));
|
|
44187
44261
|
const addressesNotRead = [...live?.clientContactsUnread ?? []].sort();
|
|
44188
|
-
const notReadClause = addressesNotRead.length ? ` Then say "Not checked for mail: ${addressesNotRead.join(", ")}" because those clients' addresses could not be read this
|
|
44262
|
+
const notReadClause = addressesNotRead.length ? ` Then say "Not checked for mail: ${addressesNotRead.join(", ")}" because those clients' addresses could not be read for this brief.` : "";
|
|
44189
44263
|
const gmailDo = (clients.length ? 'If you have a Gmail connector: search the last 7 days for mail from the client addresses and domains listed under `clients`, and ONLY those. List every thread whose LAST message is not from the operator under "Client email waiting on you", newest first, one line each: client, subject, how long it has waited. If none: say "No client email is waiting on you."' : 'No client has an address on file, so there is nothing to search for. Say "Client email: no client addresses on file yet" and do not search by name. A client gets addresses once it has a client record (set_income_source), from its sub-account business record and the record\'s own `emails`.') + notReadClause;
|
|
44190
44264
|
const neverMerge = " Never count these as client appointments and never merge the two lists: a GoHighLevel booking synced to Google is the same meeting seen twice.";
|
|
44191
44265
|
const ask = [
|
|
@@ -44275,13 +44349,13 @@ function dailyBrief(input) {
|
|
|
44275
44349
|
} else if (accountsRead === 0 && (live.loading?.length ?? 0) > 0 && !live.unreachable?.length) {
|
|
44276
44350
|
notChecked.push("Every account is still loading from GoHighLevel, so every section below is your own records and the board only for now. Nothing failed; the brief fills in as each account is read.");
|
|
44277
44351
|
} else if (accountsRead === 0) {
|
|
44278
|
-
notChecked.push("Not one account could be read from GoHighLevel this
|
|
44352
|
+
notChecked.push("Not one account could be read from GoHighLevel for this brief, so every section below is your own records and the board only. That is a connection problem to fix, not a quiet day.");
|
|
44279
44353
|
}
|
|
44280
44354
|
for (const note of live?.notes ?? []) notChecked.push(note);
|
|
44281
44355
|
const yourDayNote = input.ownDay?.yourDayNote ?? live?.yourDayNote;
|
|
44282
44356
|
if (yourDayNote) notChecked.push(yourDayNote);
|
|
44283
44357
|
for (const u of live?.unreachable ?? []) {
|
|
44284
|
-
notChecked.push(`${u.client} could not be read this
|
|
44358
|
+
notChecked.push(`${u.client} could not be read for this brief (${u.why}), so nothing from that account is in this brief.`);
|
|
44285
44359
|
}
|
|
44286
44360
|
const stillLoading = live?.loading ?? [];
|
|
44287
44361
|
if (stillLoading.length) {
|
|
@@ -44346,20 +44420,20 @@ function dailyBrief(input) {
|
|
|
44346
44420
|
headline3 = `Nothing is waiting on you. ${neverStarted.length} account${neverStarted.length === 1 ? " has" : "s have"} never been started.`;
|
|
44347
44421
|
} else if (halfRead.length && !unread.length) {
|
|
44348
44422
|
const what = halfRead.length === 1 ? `${halfRead[0].client}'s ${halfRead[0].what}` : `${halfRead.length} accounts were only read in part`;
|
|
44349
|
-
headline3 = `Nothing needs you in what I could read
|
|
44423
|
+
headline3 = `Nothing needs you in what I could read. But ${what}.`;
|
|
44350
44424
|
} else if (unread.length === 1) {
|
|
44351
|
-
headline3 = `Nothing needs you in what I could read
|
|
44425
|
+
headline3 = `Nothing needs you in what I could read. But ${unread[0].client} could not be read for this brief.`;
|
|
44352
44426
|
} else if (unread.length > 1) {
|
|
44353
|
-
headline3 = `Nothing needs you in what I could read
|
|
44427
|
+
headline3 = `Nothing needs you in what I could read. But ${unread.length} accounts could not be read for this brief.`;
|
|
44354
44428
|
} else if (yourDayNote) {
|
|
44355
|
-
headline3 = "Nothing needs you in what I could read
|
|
44429
|
+
headline3 = "Nothing needs you in what I could read. But your own calendar could not be read for this brief.";
|
|
44356
44430
|
} else if (floorUncheckable.length) {
|
|
44357
44431
|
const c = floorUncheckable[0].client;
|
|
44358
44432
|
headline3 = floorUncheckable.length === 1 ? `Nothing needs you \u2014 but ${c}'s price cannot be checked against any floor, so nothing here can tell you whether they are under it.` : `Nothing needs you \u2014 but ${floorUncheckable.length} clients' prices cannot be checked against any floor, ${c} among them.`;
|
|
44359
44433
|
} else if (renewalUnknown.length) {
|
|
44360
44434
|
headline3 = `Nothing needs you \u2014 but ${renewalUnknown.length} active client${renewalUnknown.length === 1 ? " has" : "s have"} no renewal date on file, so a quiet renewals list does not mean a quiet month.`;
|
|
44361
44435
|
} else {
|
|
44362
|
-
headline3 = "Nothing needs you
|
|
44436
|
+
headline3 = "Nothing needs you right now.";
|
|
44363
44437
|
}
|
|
44364
44438
|
const readFrom = accountsRead > 0 ? { accounts: accountsRead } : void 0;
|
|
44365
44439
|
return { date: localDate3(now), headline: headline3, today: today2, yourDay, waitingOnYou, runningNow, neverStarted, renewals, underFloor, payingButQuiet, newLeads, ...live?.leadsBySource?.length ? { leadsBySource: live.leadsBySource } : {}, notChecked, quiet, ...readFrom ? { readFrom } : {}, beyondGoHighLevel: beyondGoHighLevel(live, engagements, roster, input.beyond) };
|
|
@@ -44936,7 +45010,7 @@ async function readYourDay(now, tz, signal, secrets) {
|
|
|
44936
45010
|
}
|
|
44937
45011
|
return { yourDay: ownDayFromCalendar(result) };
|
|
44938
45012
|
} catch {
|
|
44939
|
-
return { yourDayNote: "Your Google Calendar is connected but could not be read this
|
|
45013
|
+
return { yourDayNote: "Your Google Calendar is connected but could not be read for this brief, so your own day is missing from this brief." };
|
|
44940
45014
|
}
|
|
44941
45015
|
}
|
|
44942
45016
|
function operatorTimezone(profileTz) {
|
|
@@ -45666,7 +45740,13 @@ var init_client_capabilities = __esm({
|
|
|
45666
45740
|
what: "One sentence about a prospect's business becomes a bot they can text, on your own demo account, and one command puts it back. It messages a real prospect, so it runs from your own Claude with you watching, never from a button.",
|
|
45667
45741
|
says: "Prepare an AI employee demo for {client} from their website and arm it so they can text it.",
|
|
45668
45742
|
hub: "modules/11-your-ai-employee/lesson-04-the-five-minute-demo.html",
|
|
45669
|
-
|
|
45743
|
+
// command-center 2026-10-29: read live, the same two lists the demo tools use (the demo bot, and one knowledge
|
|
45744
|
+
// base per prepared prospect, named "Demo: ..."). Whether a demo is armed is the bot's mode, which only
|
|
45745
|
+
// show_ai_employee_demo reads; this page does not claim it.
|
|
45746
|
+
reads: [
|
|
45747
|
+
N("bots", "/conversation-ai/agents/search", { limit: "50" }, "agents", "name", AI2),
|
|
45748
|
+
W(N("knowledge bases", "/knowledge-bases/", { locationId: "{loc}", limit: "50" }, "knowledgeBases", "name", AI2), { one: "knowledge base" })
|
|
45749
|
+
]
|
|
45670
45750
|
},
|
|
45671
45751
|
// ── Money and paperwork ─────────────────────────────────────────────────
|
|
45672
45752
|
{
|
|
@@ -45809,7 +45889,7 @@ var require_package = __commonJS({
|
|
|
45809
45889
|
"package.json"(exports2, module2) {
|
|
45810
45890
|
module2.exports = {
|
|
45811
45891
|
name: "@elitedcs/ghl-mcp",
|
|
45812
|
-
version: "3.
|
|
45892
|
+
version: "3.94.1",
|
|
45813
45893
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
45814
45894
|
description: "GoHighLevel MCP Server for Claude. 250 tools for CRM, automation and marketing, an account-wide workflow audit, live funnel-capture verification and a workflow builder, across client accounts.",
|
|
45815
45895
|
main: "dist/index.js",
|
|
@@ -47272,7 +47352,7 @@ var init_cockpit_cache = __esm({
|
|
|
47272
47352
|
};
|
|
47273
47353
|
AGE_LINE_JS = `
|
|
47274
47354
|
function ageWords(ms){ms=Math.max(0,Number(ms)||0);const m=Math.floor(ms/60000);if(m<1)return "just now";if(m<60)return m+(m===1?" minute":" minutes")+" ago";const h=Math.floor(m/60);if(h<48)return h+(h===1?" hour":" hours")+" ago";const d=Math.floor(h/24);return d+" days ago"}
|
|
47275
|
-
function ageLine(v,now){if(!v)return "";if(v.cold||!v.loadedAt){
|
|
47355
|
+
function ageLine(v,now){if(!v)return "";if(v.cold||!v.loadedAt){if(!v.refreshing&&v.lastRefresh&&v.lastRefresh.text)return "Could not load from GoHighLevel: "+v.lastRefresh.text;return "${COLD_TEXT}\u2026"}
|
|
47276
47356
|
const t=Date.parse(v.loadedAt);const base="Loaded "+ageWords((now==null?Date.now():now)-t);
|
|
47277
47357
|
if(v.refreshing)return base+" \xB7 refreshing\u2026";
|
|
47278
47358
|
if(v.lastRefresh&&v.lastRefresh.text&&Date.parse(v.lastRefresh.at)>=t)return base+" \xB7 Refresh failed: "+v.lastRefresh.text;
|
|
@@ -47490,8 +47570,8 @@ var init_analyze = __esm({
|
|
|
47490
47570
|
function extractUrls(text3, html2) {
|
|
47491
47571
|
const out = /* @__PURE__ */ new Set();
|
|
47492
47572
|
const add2 = (u) => {
|
|
47493
|
-
const
|
|
47494
|
-
if (/^https?:\/\//i.test(
|
|
47573
|
+
const clean6 = u.replace(/&/g, "&").replace(/[.,;:!?]+$/, "");
|
|
47574
|
+
if (/^https?:\/\//i.test(clean6)) out.add(clean6);
|
|
47495
47575
|
};
|
|
47496
47576
|
if (html2) for (const m of html2.matchAll(HREF_RE)) add2(m[1]);
|
|
47497
47577
|
for (const m of text3.matchAll(URL_RE)) add2(m[0]);
|
|
@@ -47687,7 +47767,8 @@ function judgeSteps(rawRows, contactId, workflowNames, lead) {
|
|
|
47687
47767
|
else if (outcome === "unclear") detail = `GoHighLevel logged "${status || "no status"}" for this step, which this version does not know how to read. Check it in GoHighLevel's Execution logs.`;
|
|
47688
47768
|
else if (outcome === "ran") detail = type === "wait_finished" ? "The wait ended." : reason3 && !/^\{/.test(reason3) ? reason3 : "Done.";
|
|
47689
47769
|
else detail = contactDataSkip(type, outcome, reason3, lead) ?? staffGapSkip({ workflowId: wfId, stepId: str9(r.stepId), outcome, reason: reason3 }, lead)?.detail ?? translate(type, outcome, reason3, retryAt).detail;
|
|
47690
|
-
|
|
47770
|
+
const notTested = outcome === "did_nothing" && contactDataSkip(type, outcome, reason3, lead) !== null;
|
|
47771
|
+
rows2.push({ ...base, detail, ...notTested ? { notTested: true } : {}, retryAt: outcome === "failed" && !gaveUp ? retryAt : void 0, ...outcome === "waiting" && retryAt ? { waitUntil: retryAt } : {}, reason: reason3 || void 0, ...gaveUp ? { gaveUp: true } : {} });
|
|
47691
47772
|
}
|
|
47692
47773
|
const sameStep = (a, b) => a.workflowId === b.workflowId && a.pass === b.pass && (a.stepId ?? a.stepName) === (b.stepId ?? b.stepName);
|
|
47693
47774
|
const problems = [];
|
|
@@ -48197,6 +48278,59 @@ async function postJson(url, body) {
|
|
|
48197
48278
|
const r = await guardedPost(url, JSON.stringify(body), { "Content-Type": "application/json", Accept: "application/json" });
|
|
48198
48279
|
return r.status;
|
|
48199
48280
|
}
|
|
48281
|
+
async function removeLeadCards(client, run, save) {
|
|
48282
|
+
if (!run.contactId) return;
|
|
48283
|
+
let rows2;
|
|
48284
|
+
try {
|
|
48285
|
+
rows2 = asArr4(asObj8(await client.get("/opportunities/search", { params: { location_id: run.locationId, contact_id: run.contactId, limit: 100 } })).opportunities).map(asObj8);
|
|
48286
|
+
} catch (e) {
|
|
48287
|
+
run.leadCards = [{ id: "", name: "", removed: false, error: `The test contact's pipeline cards could not be read (${e instanceof Error ? e.message : String(e)}), so any card a workflow made for it is still there.` }];
|
|
48288
|
+
return;
|
|
48289
|
+
}
|
|
48290
|
+
let bornHere = false, contactUnread = false;
|
|
48291
|
+
try {
|
|
48292
|
+
const c = asObj8(asObj8(await client.get(`/contacts/${run.contactId}`)).contact);
|
|
48293
|
+
const added = Date.parse(str10(c.dateAdded) ?? str10(c.createdAt) ?? "");
|
|
48294
|
+
bornHere = (str10(c.email) ?? "").trim().toLowerCase() === run.leadEmail.trim().toLowerCase() && Number.isFinite(added) && added >= run.startedAt - 6e4;
|
|
48295
|
+
} catch {
|
|
48296
|
+
bornHere = false;
|
|
48297
|
+
contactUnread = true;
|
|
48298
|
+
}
|
|
48299
|
+
const ours = rows2.filter((o) => {
|
|
48300
|
+
const contact = str10(o.contactId) ?? str10(asObj8(o.contact).id);
|
|
48301
|
+
const made = Date.parse(str10(o.createdAt) ?? str10(o.dateAdded) ?? "");
|
|
48302
|
+
return contact === run.contactId && Number.isFinite(made) && made >= run.startedAt - 6e4 && Boolean(str10(o.id));
|
|
48303
|
+
});
|
|
48304
|
+
if (!ours.length) return;
|
|
48305
|
+
run.leadCards = ours.map((o) => ({ id: str10(o.id), name: str10(o.name) ?? "Test card", ...str10(o.pipelineId) ? { pipelineId: str10(o.pipelineId) } : {}, removed: false, ...bornHere ? {} : { keptBecauseExisting: true, ...contactUnread ? { contactUnread: true } : {} } }));
|
|
48306
|
+
save(run);
|
|
48307
|
+
if (!bornHere) return;
|
|
48308
|
+
for (const c of run.leadCards) {
|
|
48309
|
+
try {
|
|
48310
|
+
await client.delete(`/opportunities/${c.id}`);
|
|
48311
|
+
c.removed = true;
|
|
48312
|
+
} catch (e) {
|
|
48313
|
+
if (notFound(e)) {
|
|
48314
|
+
c.removed = true;
|
|
48315
|
+
continue;
|
|
48316
|
+
}
|
|
48317
|
+
c.error = e instanceof Error ? e.message : String(e);
|
|
48318
|
+
}
|
|
48319
|
+
}
|
|
48320
|
+
}
|
|
48321
|
+
function leadCardsText(cards) {
|
|
48322
|
+
if (!cards?.length) return "";
|
|
48323
|
+
if (cards.length === 1 && !cards[0].id) return cards[0].error ?? "";
|
|
48324
|
+
const kept = cards.filter((c) => c.keptBecauseExisting && !c.contactUnread), unconfirmed = cards.filter((c) => c.keptBecauseExisting && c.contactUnread);
|
|
48325
|
+
const gone = cards.filter((c) => c.removed), left = cards.filter((c) => !c.removed && !c.keptBecauseExisting);
|
|
48326
|
+
const names = (xs) => xs.map((c) => `"${c.name}"`).join(", ");
|
|
48327
|
+
return [
|
|
48328
|
+
gone.length ? `A workflow made ${gone.length === 1 ? "a pipeline card" : `${gone.length} pipeline cards`} for the test contact (${names(gone)}); the check took ${gone.length === 1 ? "it" : "them"} out of the pipeline again.` : "",
|
|
48329
|
+
kept.length ? `This test used an existing contact, so the check left ${kept.length === 1 ? "this pipeline card" : "these pipeline cards"} for you to check: ${names(kept)}.` : "",
|
|
48330
|
+
unconfirmed.length ? `The check could not confirm this test contact was its own, so it left ${unconfirmed.length === 1 ? "this pipeline card" : "these pipeline cards"} for you to check: ${names(unconfirmed)}.` : "",
|
|
48331
|
+
left.length ? `The check could not take ${left.length === 1 ? "this pipeline card" : "these pipeline cards"} out of the pipeline: ${names(left)}. Clear ${left.length === 1 ? "it" : "them"} by hand in GoHighLevel's pipeline.` : ""
|
|
48332
|
+
].filter(Boolean).join(" ");
|
|
48333
|
+
}
|
|
48200
48334
|
async function listLeadCarriers(client, run) {
|
|
48201
48335
|
const carriers = /* @__PURE__ */ new Map();
|
|
48202
48336
|
let partial = false;
|
|
@@ -48334,6 +48468,9 @@ async function finishShopper(locationId2, runId, deps) {
|
|
|
48334
48468
|
run.status = "done";
|
|
48335
48469
|
run.finishedAt = t;
|
|
48336
48470
|
run.cleanup = await listLeadCarriers(deps.client, run);
|
|
48471
|
+
await removeLeadCards(deps.client, run, (r) => saveRun(r, deps.base));
|
|
48472
|
+
const cardLine = leadCardsText(run.leadCards);
|
|
48473
|
+
if (cardLine) run.cleanup = { ...run.cleanup, detail: `${run.cleanup.detail} ${cardLine}`.trim() };
|
|
48337
48474
|
if (run.fastForward && deps.ffAdmin) {
|
|
48338
48475
|
const { removeCopies: removeCopies2 } = await Promise.resolve().then(() => (init_fastforward(), fastforward_exports));
|
|
48339
48476
|
const problems = await removeCopies2(deps.ffAdmin, run.locationId, run.fastForward.copies.map((c) => ({ copyId: c.copyId, name: c.copyName })), deps.base, now);
|
|
@@ -48816,7 +48953,7 @@ function ringHtml(score2, caption, complete = true) {
|
|
|
48816
48953
|
return `<div class="cds-ring" role="img" aria-label="Grade ${g}, ${s} out of 100${complete ? "" : ""}. ${esc2(caption)}"><svg width="160" height="160" viewBox="0 0 120 120" aria-hidden="true"><circle class="trk" cx="60" cy="60" r="${r}" stroke-width="10"></circle><circle class="arc ${k}" cx="60" cy="60" r="${r}" stroke-width="10" stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${(c * (1 - s / 100)).toFixed(2)}" transform="rotate(-90 60 60)"></circle><text class="letter ${k}" x="60" y="66" text-anchor="middle" font-size="44">${g}</text><text class="out" x="60" y="86" text-anchor="middle">${s}/100</text></svg><div class="cap">${esc2(caption)}</div></div>`;
|
|
48817
48954
|
}
|
|
48818
48955
|
function stepRow(r) {
|
|
48819
|
-
const [tone, word] = r.gaveUp ? ["bad", "Failed, gave up"] : OUTCOME[r.outcome] ?? ["", ""];
|
|
48956
|
+
const [tone, word] = r.gaveUp ? ["bad", "Failed, gave up"] : r.notTested ? ["", "Not tested"] : OUTCOME[r.outcome] ?? ["", ""];
|
|
48820
48957
|
return `<li><span class="t"><time data-ms="${r.at}">${esc2(new Date(r.at).toISOString().slice(11, 16))}</time></span>${dot(tone)}<span class="what">${esc2(r.stepName)}<small>${esc2(r.detail)}</small></span>${word ? chip(tone, word) : ""}</li>`;
|
|
48821
48958
|
}
|
|
48822
48959
|
function stepsHtml(a) {
|
|
@@ -49057,7 +49194,11 @@ ${COCKPIT_TYPE_JS}
|
|
|
49057
49194
|
function showStartPanel(ids){
|
|
49058
49195
|
var names=ids.map(function(id){var w=(shownWorkflows||[]).filter(function(x){return x.id===id})[0];return w?w.name:id});
|
|
49059
49196
|
var ff=document.getElementById("ff").checked, email=document.getElementById("owner-email").value, phone=document.getElementById("test-phone").value;
|
|
49060
|
-
|
|
49197
|
+
/* OS audit F18: the run writes a new address made from this one (plusAddress in run.ts), never the typed one; say that. */
|
|
49198
|
+
var em=String(email||"").trim().toLowerCase(),at2=em.lastIndexOf("@"),runEmail=at2>0?em.slice(0,at2).split("+")[0]+"+shopper-(a new code)"+em.slice(at2):em;
|
|
49199
|
+
var li=['A test contact named "Secret Shopper" with an address made from your email, '+esc(runEmail)+' (a new code for each check, still your inbox)'+(phone?' and the phone '+esc(phone):'')+', and the tag command-os-secret-shopper. It stays in the account afterwards; delete it when you no longer need it.'];
|
|
49200
|
+
/* OS audit F19: a copy's "Create opportunity" step makes a real card in the account's real pipeline, and it stays. */
|
|
49201
|
+
li.push('If a workflow makes a pipeline card for the test contact, that card is real and sits in your pipeline while the check runs; the check takes it out again when it ends, and says so if it cannot.');
|
|
49061
49202
|
if(ff)li.push('Temporary copies of '+names.length+' workflow'+(names.length===1?'':'s')+', switched on while the check runs: '+names.map(function(n){return esc("${FF_PREFIX} "+n)}).join(", ")+' (each name also carries a short code). The test lead goes into the copies, not the originals, and the copies are deleted when the check ends.');
|
|
49062
49203
|
else li.push('The test lead goes into '+names.map(esc).join(", ")+'. Those workflows really text and email you, and may alert your staff once.');
|
|
49063
49204
|
// Walk W38: in the copies, staff alerts, tasks and assignments go to one person, marked TEST; say who, by name.
|
|
@@ -49540,7 +49681,7 @@ function deductions(items, capped2) {
|
|
|
49540
49681
|
}
|
|
49541
49682
|
function workflowFlow(steps) {
|
|
49542
49683
|
return `<div class="cds-flow">${steps.map((r) => {
|
|
49543
|
-
const [tone, word] = r.gaveUp ? ["bad", "Failed, gave up"] : OUTCOME2[r.outcome] ?? ["", "Unknown"];
|
|
49684
|
+
const [tone, word] = r.gaveUp ? ["bad", "Failed, gave up"] : r.notTested ? ["", "Not tested"] : OUTCOME2[r.outcome] ?? ["", "Unknown"];
|
|
49544
49685
|
return `<div class="cds-node ${tone === "bad" || tone === "warn" ? "hit" : ""}"><span class="pin ${tone}"></span><span class="nm">${esc4(r.stepName)}${r.detail ? `<small>${esc4(r.detail)}</small>` : ""}</span>${chip2(tone === "wait" ? "" : tone, word)}</div>`;
|
|
49545
49686
|
}).join("")}</div>`;
|
|
49546
49687
|
}
|
|
@@ -51176,7 +51317,8 @@ function draw(){
|
|
|
51176
51317
|
const sum=money?.summary;
|
|
51177
51318
|
if(sum){tile(cashAll(sum.mrr),"monthly recurring revenue");tile(cashAll(sum.received30.totals),"received, 30 days")}
|
|
51178
51319
|
else if(money){tile(amount(money.owed),"owed on invoices");tile(money.renewals.count,"renewals, 30 days")}
|
|
51179
|
-
|
|
51320
|
+
/* OS audit F3: "Not read" was said when Stripe was simply not connected or held no record. Say which. */
|
|
51321
|
+
else{const w=stripe?panelWord(stripe):"Not connected";tile(w,"monthly recurring revenue");tile(w,"received, 30 days")}
|
|
51180
51322
|
tiles.appendChild(el("p",readLine(),"cds-src stats-meta"));
|
|
51181
51323
|
if(data.accounts.loading>0){const nte=el("p","","cds-callout info notice");nte.appendChild(el("b",plural(data.accounts.loading,"account is","accounts are")+" still loading from GoHighLevel."));nte.appendChild(document.createTextNode(" Nothing failed; the brief fills in as each one is read."));tiles.appendChild(nte)}
|
|
51182
51324
|
if(data.accounts.unread>0){const nte=el("p","","cds-callout warn notice");nte.appendChild(el("b",plural(data.accounts.unread,"account","accounts")+" could not be read."));nte.appendChild(document.createTextNode(" This brief is not the full picture; the unread accounts are named in the notes below."));tiles.appendChild(nte)}
|
|
@@ -53038,7 +53180,7 @@ function compute(ans, skip, asm) {
|
|
|
53038
53180
|
function() {
|
|
53039
53181
|
return raw("calls_missed") * share("missed_never_return") * c * v;
|
|
53040
53182
|
},
|
|
53041
|
-
"Call answering
|
|
53183
|
+
"Call answering"
|
|
53042
53184
|
);
|
|
53043
53185
|
add2(
|
|
53044
53186
|
"waited",
|
|
@@ -53721,7 +53863,12 @@ function programBuiltLabel(name2) {
|
|
|
53721
53863
|
}
|
|
53722
53864
|
return `Built: Program follow-up${win ? ` (texts over the ${win} after the visit)` : ""}`;
|
|
53723
53865
|
}
|
|
53724
|
-
function
|
|
53866
|
+
function statusLabel(label2, feature, status) {
|
|
53867
|
+
const what = label2.replace(/^Built: /, "");
|
|
53868
|
+
if (!status?.made) return `Will build: ${what}`;
|
|
53869
|
+
return status.made.has(feature) ? `Built: ${what}` : `Not built yet: ${what}`;
|
|
53870
|
+
}
|
|
53871
|
+
function boughtItemsFor(offered, wording = {}, status) {
|
|
53725
53872
|
return ASSESSMENT_FEATURES.map((f) => {
|
|
53726
53873
|
const id2 = f.id;
|
|
53727
53874
|
const base = { id: id2, name: f.name, means: BOUGHT_MEANS[id2] };
|
|
@@ -53731,11 +53878,12 @@ function boughtItemsFor(offered, wording = {}) {
|
|
|
53731
53878
|
if (!offered.includes(feature)) {
|
|
53732
53879
|
return { ...base, kind: "not-in-plan", feature, ...next, delivery: `Not built for this client: ${FEATURES[feature].name.toLowerCase()} is not in the plan for this industry.` };
|
|
53733
53880
|
}
|
|
53734
|
-
const
|
|
53881
|
+
const label2 = id2 === "quotes" && wording.programFollowUp ? programBuiltLabel(wording.programFollowUp) : BUILT_LABEL[id2];
|
|
53882
|
+
const delivery = status?.held?.[id2] ?? statusLabel(label2, feature, status);
|
|
53735
53883
|
return { ...base, kind: "built", feature, ...next, delivery };
|
|
53736
53884
|
}
|
|
53737
53885
|
const other = OTHER_DELIVERY[id2];
|
|
53738
|
-
return { ...base, kind: other.kind, delivery: other.text };
|
|
53886
|
+
return { ...base, kind: other.kind, delivery: status?.held?.[id2] ?? other.text };
|
|
53739
53887
|
});
|
|
53740
53888
|
}
|
|
53741
53889
|
function featuresFromBought(ticks, offered) {
|
|
@@ -53781,13 +53929,19 @@ function assessmentGateRefusal(state2, rk, clientKey, name2) {
|
|
|
53781
53929
|
function approvalBindingHash(scopeHash2, state2, rk) {
|
|
53782
53930
|
const saved = state2.bought?.[rk];
|
|
53783
53931
|
const answer2 = assessmentAnswerOf(state2, rk);
|
|
53784
|
-
|
|
53932
|
+
const asks = asksBound(state2, rk);
|
|
53933
|
+
if (!Array.isArray(saved) && !answer2 && !asks) return scopeHash2;
|
|
53785
53934
|
const snapshot = state2.assessmentAttachments?.[rk];
|
|
53786
|
-
return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null });
|
|
53935
|
+
return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null, ...asks ? { asks } : {} });
|
|
53787
53936
|
}
|
|
53788
53937
|
function boughtBindingHash(scopeHash2, state2, rk) {
|
|
53789
53938
|
const saved = state2.bought?.[rk];
|
|
53790
|
-
|
|
53939
|
+
const asks = asksBound(state2, rk);
|
|
53940
|
+
return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, ...asks ? { asks } : {} });
|
|
53941
|
+
}
|
|
53942
|
+
function asksBound(state2, rk) {
|
|
53943
|
+
const a = state2.asks?.[rk];
|
|
53944
|
+
return a && Object.keys(a).length ? a : void 0;
|
|
53791
53945
|
}
|
|
53792
53946
|
function intakeChannelAnswers(answers) {
|
|
53793
53947
|
const get2 = (key2, label2) => {
|
|
@@ -53807,6 +53961,12 @@ function boughtJobs(ticks, offered, channels = {}) {
|
|
|
53807
53961
|
reason: item.kind === "not-in-plan" ? "Bought, but not built automatically yet. It is not in the plan for this client's industry." : `Bought, but not built automatically yet. ${OTHER_DELIVERY[item.id].job}`
|
|
53808
53962
|
}));
|
|
53809
53963
|
}
|
|
53964
|
+
function heldJobs(held2) {
|
|
53965
|
+
return held2.map((h) => {
|
|
53966
|
+
const at = h.line.indexOf(": ");
|
|
53967
|
+
return { ref: `bought:held-${h.id}`, type: BOUGHT_JOB_TYPE, name: h.line.slice(0, at), reason: h.line.slice(at + 2) };
|
|
53968
|
+
});
|
|
53969
|
+
}
|
|
53810
53970
|
function boughtButExcludedJobs(ticks, offered, scopeFeatures) {
|
|
53811
53971
|
if (scopeFeatures === "all") return [];
|
|
53812
53972
|
return boughtItemsFor(offered).filter((item) => ticks.includes(item.id) && item.kind === "built" && item.feature && !scopeFeatures.includes(item.feature)).map((item) => ({
|
|
@@ -53829,10 +53989,10 @@ function assessmentChoiceLine(answer2, attached = answer2 === "attached") {
|
|
|
53829
53989
|
if (answer2 === "run-first") return "The Assessment will be run first.";
|
|
53830
53990
|
return "Not answered yet: has the Assessment been run for this client?";
|
|
53831
53991
|
}
|
|
53832
|
-
function boughtViewFor(state2, rk, offered, wording = {}) {
|
|
53992
|
+
function boughtViewFor(state2, rk, offered, wording = {}, status) {
|
|
53833
53993
|
const { ticks, source } = ticksFor(state2, rk);
|
|
53834
53994
|
return {
|
|
53835
|
-
items: boughtItemsFor(offered, wording),
|
|
53995
|
+
items: boughtItemsFor(offered, wording, status),
|
|
53836
53996
|
ticks,
|
|
53837
53997
|
source,
|
|
53838
53998
|
answer: assessmentAnswerOf(state2, rk),
|
|
@@ -53841,17 +54001,20 @@ function boughtViewFor(state2, rk, offered, wording = {}) {
|
|
|
53841
54001
|
suggestions: source === "saved" ? claudeSuggestions(ticks) : []
|
|
53842
54002
|
};
|
|
53843
54003
|
}
|
|
53844
|
-
function boughtDocumentFor(state2, rk, offered, wording = {}) {
|
|
54004
|
+
function boughtDocumentFor(state2, rk, offered, wording = {}, status) {
|
|
53845
54005
|
const { ticks, source } = ticksFor(state2, rk);
|
|
53846
|
-
const items = boughtItemsFor(offered, wording);
|
|
54006
|
+
const items = boughtItemsFor(offered, wording, status);
|
|
53847
54007
|
const confirmed = source === "saved" || source === "scope";
|
|
53848
54008
|
const ticked = items.filter((i) => ticks.includes(i.id));
|
|
53849
54009
|
return {
|
|
53850
54010
|
confirmed,
|
|
53851
54011
|
assessment: assessmentChoiceLine(assessmentAnswerOf(state2, rk), Boolean(state2.assessmentAttachments?.[rk])),
|
|
53852
|
-
|
|
54012
|
+
// "No call goes unanswered" is a promise only an AI answering live keeps, and this version builds none: the client
|
|
54013
|
+
// reads what the build did for calls (promise-vs-build F4 wording, 2026-10-29).
|
|
54014
|
+
bought: ticked.map((i) => ({ name: i.name, means: i.id === "missed" ? status?.made?.has("missed_call") ? "Missed calls get a text back" : "How calls are answered" : i.means, delivery: clientDelivery(i) })),
|
|
53853
54015
|
// The client reads this: never the agency's job text, and never "with Claude" (review of ea3526f).
|
|
53854
54016
|
stillToDo: ticked.flatMap((i) => {
|
|
54017
|
+
if (HELD_LINE.test(i.delivery)) return [`${i.name}: ${clientDelivery(i).toLowerCase()}.`];
|
|
53855
54018
|
if (i.kind === "go-live") return [`${i.name}: being set up in your account.`];
|
|
53856
54019
|
if (i.kind === "separate") return [`${i.name}: the form is ready. The pages are a separate piece of work.`];
|
|
53857
54020
|
if (i.kind === "not-in-plan") return [`${i.name}: being set up separately from this build.`];
|
|
@@ -53861,13 +54024,15 @@ function boughtDocumentFor(state2, rk, offered, wording = {}) {
|
|
|
53861
54024
|
};
|
|
53862
54025
|
}
|
|
53863
54026
|
function clientDelivery(i) {
|
|
54027
|
+
const held2 = HELD_LINE.exec(i.delivery);
|
|
54028
|
+
if (held2) return held2[1] === "Waiting for your client's answer" ? "Waiting on your answer" : held2[1] === "Not built, your client said not yet" ? "Not set up, as you said not yet" : "Chosen, and coming in a later update";
|
|
53864
54029
|
if (i.kind === "go-live") return "Being set up in your account";
|
|
53865
54030
|
if (i.kind === "separate") return "The form is built; the pages are separate work";
|
|
53866
54031
|
if (i.kind === "not-in-plan") return "Not part of this build";
|
|
53867
54032
|
if (i.kind === "claude") return "A weekly report on what is working";
|
|
53868
54033
|
return i.delivery;
|
|
53869
54034
|
}
|
|
53870
|
-
var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE;
|
|
54035
|
+
var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE, HELD_LINE;
|
|
53871
54036
|
var init_bought_list = __esm({
|
|
53872
54037
|
"src/bought-list.ts"() {
|
|
53873
54038
|
"use strict";
|
|
@@ -53876,11 +54041,13 @@ var init_bought_list = __esm({
|
|
|
53876
54041
|
init_build_contract();
|
|
53877
54042
|
BOUGHT_IDS = ASSESSMENT_FEATURES.map((f) => f.id);
|
|
53878
54043
|
BOUGHT_MEANS = {
|
|
53879
|
-
|
|
54044
|
+
// rc.1 MED-1: true whatever the client chooses; nothing answers live until a voice agent is built.
|
|
54045
|
+
missed: "Callers get the answer your client chooses",
|
|
53880
54046
|
waited: "Every new lead hears back in seconds",
|
|
53881
54047
|
quotes: "No quote goes cold",
|
|
53882
54048
|
noshow: "Bookings confirmed, reminded and rescued",
|
|
53883
|
-
|
|
54049
|
+
// rc.1 MED-2: what Win-back reaches (leads who go cold from now on); an old list only once a campaign for it is built.
|
|
54050
|
+
reactivation: "Leads who go quiet are brought back",
|
|
53884
54051
|
nurture: "Leads kept warm until they are ready",
|
|
53885
54052
|
recall: "Clients come back on schedule",
|
|
53886
54053
|
deposits: "Fewer no-shows, cash up front",
|
|
@@ -53918,6 +54085,7 @@ var init_bought_list = __esm({
|
|
|
53918
54085
|
ATTACH_THE_ASSESSMENT = "Attach the Assessment in step 4, or choose Build without an Assessment there.";
|
|
53919
54086
|
RUN_THE_ASSESSMENT_FIRST = "You chose to run the Assessment first. Attach it in step 4 when it is done, or choose Build without an Assessment there.";
|
|
53920
54087
|
BOUGHT_JOB_TYPE = "bought-not-built";
|
|
54088
|
+
HELD_LINE = /^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
|
|
53921
54089
|
}
|
|
53922
54090
|
});
|
|
53923
54091
|
|
|
@@ -53975,6 +54143,23 @@ var init_offer_normalize = __esm({
|
|
|
53975
54143
|
}
|
|
53976
54144
|
});
|
|
53977
54145
|
|
|
54146
|
+
// src/intake-to-build/texting.ts
|
|
54147
|
+
function textingAnswer(brief) {
|
|
54148
|
+
const channels = brief && typeof brief === "object" ? brief.channels : void 0;
|
|
54149
|
+
const sms = channels && typeof channels === "object" ? channels.sms : void 0;
|
|
54150
|
+
if (sms === true) return "yes";
|
|
54151
|
+
if (sms === false) return "not_yet";
|
|
54152
|
+
return "unanswered";
|
|
54153
|
+
}
|
|
54154
|
+
function smsOn(brief) {
|
|
54155
|
+
return textingAnswer(brief) === "yes";
|
|
54156
|
+
}
|
|
54157
|
+
var init_texting = __esm({
|
|
54158
|
+
"src/intake-to-build/texting.ts"() {
|
|
54159
|
+
"use strict";
|
|
54160
|
+
}
|
|
54161
|
+
});
|
|
54162
|
+
|
|
53978
54163
|
// src/intake-to-build/brief.ts
|
|
53979
54164
|
function norm4(s) {
|
|
53980
54165
|
return s.trim().toLowerCase();
|
|
@@ -54232,7 +54417,7 @@ function parsePricePoints(text3) {
|
|
|
54232
54417
|
const out = [];
|
|
54233
54418
|
const segments2 = text3.split(/\r?\n|;|\.\s+/);
|
|
54234
54419
|
const ENTRY = /^(.*?)\s*[-–—:|]?\s*\$?\s*([\d,]+(?:\.\d+)?)\s*(?:[-–—]\s*\$?\s*([\d,]+(?:\.\d+)?))?\s*(\/\s*[a-z]+|(?:per|an?|each)\s+[a-z]+)?\s*$/i;
|
|
54235
|
-
const
|
|
54420
|
+
const clean6 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
|
|
54236
54421
|
const num4 = (s) => s.replace(/,/g, "");
|
|
54237
54422
|
const recur = (s) => s ? "/" + s.replace(/^\/|^(?:per|an?|each)\b/i, "").trim().toLowerCase() : "";
|
|
54238
54423
|
for (const segment of segments2) {
|
|
@@ -54241,11 +54426,11 @@ function parsePricePoints(text3) {
|
|
|
54241
54426
|
const m = t.match(ENTRY);
|
|
54242
54427
|
const withTerms = m ? null : t.match(/^(.*?)\s*[-–—:|]?\s*\$\s*([\d,]+(?:\.\d+)?)\s*(.+)$/);
|
|
54243
54428
|
if (m && m[2]) {
|
|
54244
|
-
const name2 =
|
|
54429
|
+
const name2 = clean6(m[1]);
|
|
54245
54430
|
const price = (m[3] ? `${num4(m[2])}-${num4(m[3])}` : num4(m[2])) + recur(m[4]);
|
|
54246
54431
|
out.push({ name: name2 || t, price });
|
|
54247
|
-
} else if (withTerms &&
|
|
54248
|
-
out.push({ name:
|
|
54432
|
+
} else if (withTerms && clean6(withTerms[1])) {
|
|
54433
|
+
out.push({ name: clean6(withTerms[1]), price: num4(withTerms[2]) + (withTerms[3].trim().startsWith("/") ? "" : " ") + withTerms[3].trim() });
|
|
54249
54434
|
} else {
|
|
54250
54435
|
out.push({ name: t, price: "" });
|
|
54251
54436
|
}
|
|
@@ -54749,7 +54934,7 @@ function deriveFlags(brief) {
|
|
|
54749
54934
|
const channels = brief.channels ?? {};
|
|
54750
54935
|
const goal = brief.goal ?? {};
|
|
54751
54936
|
const a2p = typeof channels.a2pStatus === "string" ? channels.a2pStatus.toLowerCase() : "";
|
|
54752
|
-
if (
|
|
54937
|
+
if (smsOn(brief) && a2p !== "approved" && a2p !== "not needed") {
|
|
54753
54938
|
flags.push("needs_a2p");
|
|
54754
54939
|
}
|
|
54755
54940
|
if (channels.payment === "Stripe not connected" || channels.payment === void 0) flags.push("stripe_not_connected");
|
|
@@ -54757,7 +54942,7 @@ function deriveFlags(brief) {
|
|
|
54757
54942
|
flags.push("calendar_oauth_needed");
|
|
54758
54943
|
}
|
|
54759
54944
|
if (channels.email === false) flags.push("email_not_ready");
|
|
54760
|
-
if (
|
|
54945
|
+
if (smsOn(brief) && channels.hasPhoneNumber !== "yes") {
|
|
54761
54946
|
flags.push("phone_number_needed");
|
|
54762
54947
|
}
|
|
54763
54948
|
return flags;
|
|
@@ -54928,6 +55113,7 @@ var init_normalizer = __esm({
|
|
|
54928
55113
|
"src/intake-to-build/normalizer.ts"() {
|
|
54929
55114
|
"use strict";
|
|
54930
55115
|
init_offer_normalize();
|
|
55116
|
+
init_texting();
|
|
54931
55117
|
init_question_set();
|
|
54932
55118
|
init_brief2();
|
|
54933
55119
|
EMAIL_RE3 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -55169,17 +55355,17 @@ function checkStaff(listed, users, opts = { canAddForYou: false }) {
|
|
|
55169
55355
|
function staffGateReason(check, stageName) {
|
|
55170
55356
|
if (check.status === "ready") return null;
|
|
55171
55357
|
if (check.status === "unreadable") {
|
|
55172
|
-
return `${stageName} is waiting: we could not read this client's staff in GoHighLevel. Click Check again in step
|
|
55358
|
+
return `${stageName} is waiting: we could not read this client's staff in GoHighLevel. Click Check again in step 8, Staff.`;
|
|
55173
55359
|
}
|
|
55174
55360
|
if (check.status === "needs_list") {
|
|
55175
55361
|
return `${stageName} is waiting: nobody is on the team yet. Add them under "Your team" in step 5, Intake.`;
|
|
55176
55362
|
}
|
|
55177
|
-
return `${stageName} is waiting: ${listSentence(check.missing.map(personLabel))} ${check.missing.length === 1 ? "is" : "are"} not in this client's GoHighLevel account yet. Add ${check.missing.length === 1 ? "them" : "everyone"} in step
|
|
55363
|
+
return `${stageName} is waiting: ${listSentence(check.missing.map(personLabel))} ${check.missing.length === 1 ? "is" : "are"} not in this client's GoHighLevel account yet. Add ${check.missing.length === 1 ? "them" : "everyone"} in step 8, Staff. Nothing is built with a blank name.`;
|
|
55178
55364
|
}
|
|
55179
55365
|
function staffPreflightLine(check) {
|
|
55180
|
-
if (!check) return { key: "staff", ok: false, text: "We could not check this client's staff. Use Check again in step
|
|
55366
|
+
if (!check) return { key: "staff", ok: false, text: "We could not check this client's staff. Use Check again in step 8, Staff." };
|
|
55181
55367
|
if (check.status === "ready") return { key: "staff", ok: true, text: `Staff in this account: ${listSentence(check.matched.map((m) => m.name))}.` };
|
|
55182
|
-
return { key: "staff", ok: false, text: `${check.headline}. Finish step
|
|
55368
|
+
return { key: "staff", ok: false, text: `${check.headline}. Finish step 8, Staff, before Build.` };
|
|
55183
55369
|
}
|
|
55184
55370
|
async function readStaffCheck(deps, listed, now) {
|
|
55185
55371
|
let users;
|
|
@@ -55327,8 +55513,8 @@ var init_staff_first = __esm({
|
|
|
55327
55513
|
STAFF_STEP_ABOUT = "Calendars and lead alerts need real people. List everyone who takes appointments and everyone who should hear about new leads. We check this client's account for each email address before anything is built, so no calendar or alert is built with a blank name to fix later.";
|
|
55328
55514
|
STAFF_LIST_HELP = "One person per line: name, email, role. For example: Jane Smith, jane@yourclinic.com, Front desk. If it is just you, add yourself.";
|
|
55329
55515
|
STAFF_HOW_TO_ADD = `In this client's GoHighLevel account, go to ${GHL_ADD_STAFF_CLICK_PATH}. For each person above, ${GHL_ADD_STAFF_FORM_NOTE} shown here. When you are done, click Check again and we will read the account and confirm each person.`;
|
|
55330
|
-
STAFF_REVIEW_WARNING = "Your staff are not all in this client's account yet. Build will not start until they are: without them the calendars cannot be built, and the workflows that start from a booking would be left as drafts with no calendar chosen. Finish step
|
|
55331
|
-
STAFF_REVIEW_UNCHECKED = "We have not checked this client's staff yet. Build will not start until everyone who takes appointments is in the account. Step
|
|
55516
|
+
STAFF_REVIEW_WARNING = "Your staff are not all in this client's account yet. Build will not start until they are: without them the calendars cannot be built, and the workflows that start from a booking would be left as drafts with no calendar chosen. Finish step 8, Staff, first.";
|
|
55517
|
+
STAFF_REVIEW_UNCHECKED = "We have not checked this client's staff yet. Build will not start until everyone who takes appointments is in the account. Step 8, Staff, checks them.";
|
|
55332
55518
|
STAFF_GATED_STAGES = /* @__PURE__ */ new Set([3]);
|
|
55333
55519
|
fullName = (c) => [c.firstName, c.lastName === "-" ? "" : c.lastName].filter(Boolean).join(" ");
|
|
55334
55520
|
STAFF_ADMIN_MARK = "Admin login";
|
|
@@ -55441,7 +55627,7 @@ function intakeAnswer2(intake, key2) {
|
|
|
55441
55627
|
const label2 = questionByKey(key2)?.label;
|
|
55442
55628
|
const direct = intake[key2] ?? (label2 ? intake[label2] : void 0);
|
|
55443
55629
|
const hit = direct ?? Object.entries(intake).find(([k]) => label2 && k.trim().toLowerCase() === label2.trim().toLowerCase())?.[1];
|
|
55444
|
-
return
|
|
55630
|
+
return clean4(Array.isArray(hit) ? hit.join(", ") : hit);
|
|
55445
55631
|
}
|
|
55446
55632
|
function readBackBroken(line4) {
|
|
55447
55633
|
if (line4.state === "CHECKED") return false;
|
|
@@ -55452,7 +55638,7 @@ function readBackBroken(line4) {
|
|
|
55452
55638
|
function withoutBrokenWorkflows(plan, lines) {
|
|
55453
55639
|
if (!plan) return null;
|
|
55454
55640
|
if (!lines?.length || !plan.workflows?.length) return plan;
|
|
55455
|
-
return { ...plan, workflows: plan.workflows.filter((w) => !lines.some((l) =>
|
|
55641
|
+
return { ...plan, workflows: plan.workflows.filter((w) => !lines.some((l) => clean4(l.name) === clean4(w.name) && readBackBroken(l))) };
|
|
55456
55642
|
}
|
|
55457
55643
|
function builtFeatures(plan) {
|
|
55458
55644
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -55469,9 +55655,9 @@ function hasWorkflow(plan, ref) {
|
|
|
55469
55655
|
return (plan?.workflows ?? []).some((w) => w.ref === ref);
|
|
55470
55656
|
}
|
|
55471
55657
|
function workflowsConfirmed(plan, lines) {
|
|
55472
|
-
const names = (plan?.workflows ?? []).map((w) =>
|
|
55658
|
+
const names = (plan?.workflows ?? []).map((w) => clean4(w.name)).filter(Boolean);
|
|
55473
55659
|
if (!names.length || !lines?.length) return false;
|
|
55474
|
-
return names.every((n) => lines.some((l) =>
|
|
55660
|
+
return names.every((n) => lines.some((l) => clean4(l.name) === n && l.state === "CHECKED"));
|
|
55475
55661
|
}
|
|
55476
55662
|
function durationWords(c) {
|
|
55477
55663
|
if (!c.slotDuration) return "";
|
|
@@ -55495,8 +55681,8 @@ function isProgramFollowUp(plan) {
|
|
|
55495
55681
|
function context(src) {
|
|
55496
55682
|
const plan = src.plan ?? null;
|
|
55497
55683
|
const features = builtFeatures(plan);
|
|
55498
|
-
const calendars = (plan?.calendars ?? []).filter((c) =>
|
|
55499
|
-
name: calendarLabel(
|
|
55684
|
+
const calendars = (plan?.calendars ?? []).filter((c) => clean4(c.name)).map((c) => ({
|
|
55685
|
+
name: calendarLabel(clean4(c.name)),
|
|
55500
55686
|
duration: durationWords(c),
|
|
55501
55687
|
team: (c.teamMemberRefs ?? []).map(String)
|
|
55502
55688
|
}));
|
|
@@ -55505,10 +55691,11 @@ function context(src) {
|
|
|
55505
55691
|
const items = boughtItemsFor(offered.length ? offered : features);
|
|
55506
55692
|
const { ticks, source } = src.boughtState && src.recordKey ? ticksFor(src.boughtState, src.recordKey) : { ticks: [], source: "all" };
|
|
55507
55693
|
const confirmedTicks = source !== "all";
|
|
55508
|
-
const notify2 =
|
|
55509
|
-
const calls =
|
|
55694
|
+
const notify2 = clean4(src.roster?.notifyName) || intakeAnswer2(src.intake, "notify_name") || void 0;
|
|
55695
|
+
const calls = clean4(src.roster?.callsName) || intakeAnswer2(src.intake, "calls_name") || void 0;
|
|
55510
55696
|
const notifyShown = notify2 ? teamNames(src).find((n) => sameName(n, notify2)) : void 0;
|
|
55511
|
-
|
|
55697
|
+
const callAnswering = src.recordKey && src.boughtState?.asks?.[src.recordKey]?.callAnswering || [];
|
|
55698
|
+
return { features, live: workflowsConfirmed(plan, src.lines), calendars, notify: notify2, ...notifyShown && { notifyShown }, calls, items, ticked: confirmedTicks ? ticks : [], callAnswering, confirmedTicks, programFollowUp: isProgramFollowUp(plan), review: reviewTimingOf(plan), noShowRescue: hasWorkflow(plan, "workflow.no_show"), reply: replyChannels(plan) };
|
|
55512
55699
|
}
|
|
55513
55700
|
function printableCalendars(ctx) {
|
|
55514
55701
|
return [...new Set(ctx.calendars.map((c) => c.name))].filter((n) => n !== "booking calendar");
|
|
@@ -55518,12 +55705,12 @@ function calendarPhrase(ctx) {
|
|
|
55518
55705
|
return names.length ? `the ${listWords(names, "or")} calendar` : "your booking calendar";
|
|
55519
55706
|
}
|
|
55520
55707
|
function teamNames(src) {
|
|
55521
|
-
const fromPlan = (src.plan?.users ?? []).map((u) =>
|
|
55708
|
+
const fromPlan = (src.plan?.users ?? []).map((u) => clean4(`${u.firstName ?? ""} ${u.lastName ?? ""}`)).filter(Boolean);
|
|
55522
55709
|
if (fromPlan.length) return fromPlan;
|
|
55523
|
-
return parseStaffText(src.roster?.lines || intakeAnswer2(src.intake, "staff_members")).people.map((p) =>
|
|
55710
|
+
return parseStaffText(src.roster?.lines || intakeAnswer2(src.intake, "staff_members")).people.map((p) => clean4(p.name)).filter(Boolean);
|
|
55524
55711
|
}
|
|
55525
55712
|
function calendarLabel(name2) {
|
|
55526
|
-
const n =
|
|
55713
|
+
const n = clean4(name2);
|
|
55527
55714
|
const prose = !n || n.length > 40 || n.split(" ").length > 6 || /[@$%;:()\[\],]|\d{3}[-. )]*\d{3,}|https?:|www\./i.test(n);
|
|
55528
55715
|
return prose ? "booking calendar" : n;
|
|
55529
55716
|
}
|
|
@@ -55570,7 +55757,9 @@ function summaryOf2(ctx, plan) {
|
|
|
55570
55757
|
}
|
|
55571
55758
|
const extras = [
|
|
55572
55759
|
f("missed_call") ? "missed calls get a text back" : "",
|
|
55573
|
-
|
|
55760
|
+
// Win-back starts from a tag Lead Nurture or No-Show Rescue adds, so it reaches leads who go cold from now on,
|
|
55761
|
+
// not an old list (promise-vs-build F5, 2026-10-29).
|
|
55762
|
+
f("winback") ? "leads who go cold get a win-back message" : "",
|
|
55574
55763
|
f("quote_followup") ? ctx.programFollowUp ? PROGRAM_WORDS.quotes.summary : "every quote is followed up" : "",
|
|
55575
55764
|
f("review_requests") ? "happy clients are asked for a review" : ""
|
|
55576
55765
|
].filter(Boolean);
|
|
@@ -55645,10 +55834,10 @@ function stepsOf(ctx, plan) {
|
|
|
55645
55834
|
}
|
|
55646
55835
|
function teamOf(ctx, src) {
|
|
55647
55836
|
const plan = src.plan;
|
|
55648
|
-
const people = (plan?.users ?? []).map((u) => ({ name:
|
|
55837
|
+
const people = (plan?.users ?? []).map((u) => ({ name: clean4(`${u.firstName ?? ""} ${u.lastName ?? ""}`), ref: u.ref })).filter((p) => p.name);
|
|
55649
55838
|
if (!people.length) {
|
|
55650
55839
|
const listed = parseStaffText(src.roster?.lines || intakeAnswer2(src.intake, "staff_members")).people;
|
|
55651
|
-
for (const p of listed) people.push({ name:
|
|
55840
|
+
for (const p of listed) people.push({ name: clean4(p.name) });
|
|
55652
55841
|
}
|
|
55653
55842
|
const rows2 = [];
|
|
55654
55843
|
for (const p of people) {
|
|
@@ -55675,7 +55864,7 @@ function teamOf(ctx, src) {
|
|
|
55675
55864
|
}
|
|
55676
55865
|
function stagesOf2(plan) {
|
|
55677
55866
|
const first2 = (plan?.pipelines ?? [])[0];
|
|
55678
|
-
const all = [...first2?.stages ?? []].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((s) =>
|
|
55867
|
+
const all = [...first2?.stages ?? []].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((s) => clean4(s.name)).filter(Boolean);
|
|
55679
55868
|
return all.length > MAX_STAGE_CHIPS ? [...all.slice(0, MAX_STAGE_CHIPS), `+${all.length - MAX_STAGE_CHIPS} more`] : all;
|
|
55680
55869
|
}
|
|
55681
55870
|
function boughtOf(ctx) {
|
|
@@ -55687,7 +55876,9 @@ function boughtOf(ctx) {
|
|
|
55687
55876
|
if (built) working.push(boughtLabel(item, true, ctx));
|
|
55688
55877
|
else if (item.kind !== "built") next.push(boughtLabel(item, false, ctx));
|
|
55689
55878
|
else next.push(item.name);
|
|
55690
|
-
if (item.id === "missed" &&
|
|
55879
|
+
if (item.id === "missed" && ctx.ticked.includes("missed")) {
|
|
55880
|
+
for (const c of ctx.callAnswering) if (c !== "missed_text") next.push(c === "live_voice" ? "An AI that answers the phone live (chosen, coming in a later update)" : "An AI that replies by text or chat and books (chosen, coming in a later update)");
|
|
55881
|
+
}
|
|
55691
55882
|
}
|
|
55692
55883
|
return { workingLabel: ctx.live ? "Working now" : "Built, ready to switch on", working, next };
|
|
55693
55884
|
}
|
|
@@ -55726,12 +55917,12 @@ function buildOwnerHandoff(input) {
|
|
|
55726
55917
|
const ctx = context(src);
|
|
55727
55918
|
const { rows: rows2, source } = problemsOf(ctx, src);
|
|
55728
55919
|
const { steps, after } = stepsOf(ctx, src.plan);
|
|
55729
|
-
const agencyName =
|
|
55920
|
+
const agencyName = clean4(src.agency?.name) || void 0;
|
|
55730
55921
|
const timeZone = clientTimeZone(src);
|
|
55731
55922
|
return {
|
|
55732
55923
|
// The business's own name from the intake, else the GoHighLevel account's (audit 2026-10-28 #25: an account named
|
|
55733
55924
|
// for the agency's test titled the client's page).
|
|
55734
|
-
accountName: intakeAnswer2(src.intake, "business_name") ||
|
|
55925
|
+
accountName: intakeAnswer2(src.intake, "business_name") || clean4(src.accountName),
|
|
55735
55926
|
checkedAt: src.checkedAt,
|
|
55736
55927
|
...timeZone && { timeZone },
|
|
55737
55928
|
...src.revision !== void 0 && { revision: src.revision },
|
|
@@ -55740,7 +55931,7 @@ function buildOwnerHandoff(input) {
|
|
|
55740
55931
|
...agencyName && { name: agencyName },
|
|
55741
55932
|
accent: isHex(src.agency?.accent) ? src.agency.accent : DEFAULT_ACCENT,
|
|
55742
55933
|
...isRasterDataUri(src.agency?.logoDataUri) && { logoDataUri: src.agency.logoDataUri },
|
|
55743
|
-
...
|
|
55934
|
+
...clean4(src.agency?.contact) && { contact: clean4(src.agency?.contact) }
|
|
55744
55935
|
},
|
|
55745
55936
|
summary: summaryOf2(ctx, src.plan),
|
|
55746
55937
|
problems: rows2,
|
|
@@ -55888,7 +56079,7 @@ ${sections.join("\n")}
|
|
|
55888
56079
|
</script>
|
|
55889
56080
|
</body></html>`;
|
|
55890
56081
|
}
|
|
55891
|
-
var OWNER_UPDATES_LINE, MAX_OWNER_REQUESTS, MAX_PROBLEM_ROWS, MAX_TEAM_ROWS, MAX_STAGE_CHIPS, MAX_CALENDARS_NAMED, NOTHING_NEEDED, PROBLEM, BUILT_FOR, SEE, PROGRAM_WORDS, programWords, UNIT_MINUTES, NUMBER_WORDS, countWords,
|
|
56082
|
+
var OWNER_UPDATES_LINE, MAX_OWNER_REQUESTS, MAX_PROBLEM_ROWS, MAX_TEAM_ROWS, MAX_STAGE_CHIPS, MAX_CALENDARS_NAMED, NOTHING_NEEDED, PROBLEM, BUILT_FOR, SEE, PROGRAM_WORDS, programWords, UNIT_MINUTES, NUMBER_WORDS, countWords, clean4, cap, sameName, FINDING_TO_BOUGHT, GAP_RULES, gapId, OWNER_REQUESTS, NON_US_ZONE_WORDS, US_PLACE_WORDS, US_TIME_ZONE_WORDS, esc10;
|
|
55892
56083
|
var init_owner_handoff = __esm({
|
|
55893
56084
|
"src/command-os/owner-handoff.ts"() {
|
|
55894
56085
|
"use strict";
|
|
@@ -55965,9 +56156,9 @@ var init_owner_handoff = __esm({
|
|
|
55965
56156
|
UNIT_MINUTES = { minute: 1, minutes: 1, hour: 60, hours: 60, day: 1440, days: 1440, week: 10080, weeks: 10080 };
|
|
55966
56157
|
NUMBER_WORDS = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"];
|
|
55967
56158
|
countWords = (n, unit) => `${NUMBER_WORDS[n] ?? String(n)} ${unit}${n === 1 ? "" : "s"}`;
|
|
55968
|
-
|
|
56159
|
+
clean4 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
|
|
55969
56160
|
cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
55970
|
-
sameName = (a, b) => Boolean(a && b &&
|
|
56161
|
+
sameName = (a, b) => Boolean(a && b && clean4(a).toLowerCase() === clean4(b).toLowerCase());
|
|
55971
56162
|
FINDING_TO_BOUGHT = Object.fromEntries(
|
|
55972
56163
|
ASSESSMENT_FEATURES.filter((f) => "finding" in f).map((f) => [f.finding, f.id])
|
|
55973
56164
|
);
|
|
@@ -56831,6 +57022,8 @@ var init_cockpit_routes = __esm({
|
|
|
56831
57022
|
get("/api/snapshots", "agency", BOTH),
|
|
56832
57023
|
get("/api/connections", "agency", BOTH),
|
|
56833
57024
|
get("/api/design/run", "agency", BOTH),
|
|
57025
|
+
// OS audit F10: the finished design opens from the brand page (served sandboxed: it is the model's page).
|
|
57026
|
+
page("/design/result", "agency", BOTH),
|
|
56834
57027
|
get("/api/shopper/report-seat", "agency", BOTH),
|
|
56835
57028
|
// A report's branding is kept per client (report-brand-choices.json[client]): a one-client row, the client in the body.
|
|
56836
57029
|
via(post("/api/shopper/report-brand", "client", BOTH), { body: "client" }),
|
|
@@ -61269,19 +61462,36 @@ function startDesignRun(request2, opts = {}) {
|
|
|
61269
61462
|
});
|
|
61270
61463
|
return job;
|
|
61271
61464
|
}
|
|
61465
|
+
function memberRunLines(lines) {
|
|
61466
|
+
return lines.filter((l) => PROBLEM2.test(l) || !CHATTER.some((re) => re.test(l))).map((l) => l.replace(ABS_PATH, (p) => p.split(/[\\/]/).filter(Boolean).pop() ?? p));
|
|
61467
|
+
}
|
|
61272
61468
|
function designRunStatus(id2) {
|
|
61273
61469
|
const job = jobs.get(id2);
|
|
61274
61470
|
if (!job) return null;
|
|
61471
|
+
const shown = { ...job, tail: memberRunLines(job.tail) };
|
|
61275
61472
|
const runJson = (0, import_node_path13.join)(job.outDir, "run.json");
|
|
61276
|
-
if (!(0, import_node_fs15.existsSync)(runJson)) return
|
|
61473
|
+
if (!(0, import_node_fs15.existsSync)(runJson)) return shown;
|
|
61474
|
+
try {
|
|
61475
|
+
return { ...shown, run: JSON.parse((0, import_node_fs15.readFileSync)(runJson, "utf8")) };
|
|
61476
|
+
} catch {
|
|
61477
|
+
return shown;
|
|
61478
|
+
}
|
|
61479
|
+
}
|
|
61480
|
+
function designResultFile(id2) {
|
|
61481
|
+
const job = jobs.get(id2);
|
|
61482
|
+
if (!job || job.status !== "done") return null;
|
|
61277
61483
|
try {
|
|
61278
|
-
|
|
61484
|
+
const run = JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path13.join)(job.outDir, "run.json"), "utf8"));
|
|
61485
|
+
if (typeof run.file !== "string" || !/\.html?$/i.test(run.file)) return null;
|
|
61486
|
+
const root = (0, import_node_fs15.realpathSync)(job.outDir);
|
|
61487
|
+
const file3 = (0, import_node_fs15.realpathSync)((0, import_node_path13.isAbsolute)(run.file) ? run.file : (0, import_node_path13.join)(job.outDir, run.file));
|
|
61488
|
+
return file3.startsWith(root + import_node_path13.sep) ? file3 : null;
|
|
61279
61489
|
} catch {
|
|
61280
|
-
return
|
|
61490
|
+
return null;
|
|
61281
61491
|
}
|
|
61282
61492
|
}
|
|
61283
61493
|
function listDesignRuns() {
|
|
61284
|
-
return [...jobs.values()].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
61494
|
+
return [...jobs.values()].map((j) => ({ ...j, tail: memberRunLines(j.tail) })).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
61285
61495
|
}
|
|
61286
61496
|
function cancelDesignRun(id2) {
|
|
61287
61497
|
const child = children.get(id2);
|
|
@@ -61295,7 +61505,7 @@ function cancelDesignRun(id2) {
|
|
|
61295
61505
|
}
|
|
61296
61506
|
return true;
|
|
61297
61507
|
}
|
|
61298
|
-
var import_node_child_process6, import_node_fs15, import_node_path13, import_node_crypto29, SLUG2, MODEL, JOINED_FLAG, jobs, children;
|
|
61508
|
+
var import_node_child_process6, import_node_fs15, import_node_path13, import_node_crypto29, SLUG2, MODEL, JOINED_FLAG, jobs, children, CHATTER, PROBLEM2, ABS_PATH;
|
|
61299
61509
|
var init_run_jobs = __esm({
|
|
61300
61510
|
"src/command-os/design/run-jobs.ts"() {
|
|
61301
61511
|
"use strict";
|
|
@@ -61311,6 +61521,9 @@ var init_run_jobs = __esm({
|
|
|
61311
61521
|
JOINED_FLAG = /^--[a-z][a-z-]*=/;
|
|
61312
61522
|
jobs = /* @__PURE__ */ new Map();
|
|
61313
61523
|
children = /* @__PURE__ */ new Map();
|
|
61524
|
+
CHATTER = [/^\s*\[ghl-mcp\] Token registry: loaded key/i, /injected env \(\d+\) from .*\btip:/i, /^\s*[\u25c7\u25c8]\s.*\btip:/i];
|
|
61525
|
+
PROBLEM2 = /\b(warning|failed|fail|could not|cannot|invalid|error|bootstrap|denied|refused|unauthori[sz]ed)\b/i;
|
|
61526
|
+
ABS_PATH = /(?:\/(?:private|Users|var|tmp|home|opt)\/[^\s"')]+|[A-Za-z]:\\[^\s"')]+)/g;
|
|
61314
61527
|
}
|
|
61315
61528
|
});
|
|
61316
61529
|
|
|
@@ -61320,9 +61533,9 @@ var init_run_expectations = __esm({
|
|
|
61320
61533
|
"src/command-os/design/run-expectations.ts"() {
|
|
61321
61534
|
"use strict";
|
|
61322
61535
|
RUN_MAX_PASSES = 3;
|
|
61323
|
-
RUN_PASS_MINUTES = "
|
|
61324
|
-
RUN_PASS_COST = "$
|
|
61325
|
-
RUN_TOTAL_MINUTES = "
|
|
61536
|
+
RUN_PASS_MINUTES = "5 to 13 minutes";
|
|
61537
|
+
RUN_PASS_COST = "$0.85 to $1.75";
|
|
61538
|
+
RUN_TOTAL_MINUTES = "15 to 40 minutes";
|
|
61326
61539
|
RUN_TOTAL_COST = "about $5";
|
|
61327
61540
|
RUN_EXPECTATION = `Each pass takes ${RUN_PASS_MINUTES} and costs ${RUN_PASS_COST} of your own Claude usage, and a run may take up to ${RUN_MAX_PASSES} passes. Allow ${RUN_TOTAL_MINUTES} and up to ${RUN_TOTAL_COST}.`;
|
|
61328
61541
|
}
|
|
@@ -62762,6 +62975,7 @@ function evalCondition(cond, ctx) {
|
|
|
62762
62975
|
const [, pathExpr, op, rawRhs] = cmp;
|
|
62763
62976
|
let lhs = getPath(ctx.brief, pathExpr);
|
|
62764
62977
|
if (pathExpr === "goal.bookingNeeded") lhs = ctx.bookingWanted;
|
|
62978
|
+
if (pathExpr === "channels.sms") lhs = smsOn(ctx.brief);
|
|
62765
62979
|
const rhs = rawRhs.trim().replace(/^'(.*)'$/, "$1").replace(/^"(.*)"$/, "$1");
|
|
62766
62980
|
let equal;
|
|
62767
62981
|
if (rhs === "true" || rhs === "false") equal = lhs === (rhs === "true");
|
|
@@ -63232,20 +63446,20 @@ function presetOwnWords(s) {
|
|
|
63232
63446
|
return s.replace(INLINE_TOKEN_RE, (_whole, _path, def) => def !== void 0 ? ` ${def.trim()} ` : " ").replace(/\|\|/g, " ");
|
|
63233
63447
|
}
|
|
63234
63448
|
function claimSafeBrief(brief, blocked) {
|
|
63235
|
-
const
|
|
63449
|
+
const clean6 = (v) => {
|
|
63236
63450
|
if (typeof v === "string") return blocked(v).length > 0 ? void 0 : v;
|
|
63237
|
-
if (Array.isArray(v)) return v.map(
|
|
63451
|
+
if (Array.isArray(v)) return v.map(clean6).filter((x) => x !== void 0);
|
|
63238
63452
|
if (v && typeof v === "object") {
|
|
63239
63453
|
const out = {};
|
|
63240
63454
|
for (const [k, x] of Object.entries(v)) {
|
|
63241
|
-
const c =
|
|
63455
|
+
const c = clean6(x);
|
|
63242
63456
|
if (c !== void 0) out[k] = c;
|
|
63243
63457
|
}
|
|
63244
63458
|
return out;
|
|
63245
63459
|
}
|
|
63246
63460
|
return v;
|
|
63247
63461
|
};
|
|
63248
|
-
return
|
|
63462
|
+
return clean6(brief);
|
|
63249
63463
|
}
|
|
63250
63464
|
function isHollowMessage(s) {
|
|
63251
63465
|
return visibleText2(stripOptOutLines(s)).replace(/[\s.,!?-]/g, "") === "";
|
|
@@ -63631,7 +63845,7 @@ function planFromPreset(input) {
|
|
|
63631
63845
|
const skTemplates = sk.templates ?? {};
|
|
63632
63846
|
const emailTemplates = keptByCondition(skTemplates.emails, ctx).map((t) => fillNode(t, ctx));
|
|
63633
63847
|
const gsm7 = typeof p.conventions?.gsm7 === "string";
|
|
63634
|
-
const smsTemplates = keptByCondition(skTemplates.sms, ctx).map((t) => {
|
|
63848
|
+
const smsTemplates = (smsOn(input.brief) ? keptByCondition(skTemplates.sms, ctx) : []).map((t) => {
|
|
63635
63849
|
const filled2 = fillNode(t, ctx);
|
|
63636
63850
|
if (gsm7 && typeof filled2.body === "string") filled2.body = toGsm7(filled2.body);
|
|
63637
63851
|
return filled2;
|
|
@@ -64043,6 +64257,7 @@ var init_plan_from_preset = __esm({
|
|
|
64043
64257
|
init_sms_rules();
|
|
64044
64258
|
init_business_hours();
|
|
64045
64259
|
init_plan_guide();
|
|
64260
|
+
init_texting();
|
|
64046
64261
|
init_claims_guard();
|
|
64047
64262
|
PRESET_TOKEN_ROOTS = /* @__PURE__ */ new Set(["business", "offer", "audience", "voice", "goal", "proof"]);
|
|
64048
64263
|
GSM7_REPLACEMENTS = [
|
|
@@ -65212,6 +65427,8 @@ function stepProgress(i) {
|
|
|
65212
65427
|
i.assessment === "attached" || i.assessment === "without" || i.assessment === "" && [2, 3, 4, 5, 6].some((n) => i.stages[n] === "done"),
|
|
65213
65428
|
// Old clients' stage 0 was a form install; it counts only once answers are really in (review of 64da59d).
|
|
65214
65429
|
stage(0) && i.intakeIn !== false,
|
|
65430
|
+
// Clients that approved under the old order (bought inside Approve) count as answered.
|
|
65431
|
+
Boolean(i.scopeSaved) || stage(2) && i.approved,
|
|
65215
65432
|
stage(1),
|
|
65216
65433
|
i.staffReady || stage(3),
|
|
65217
65434
|
stage(2) && i.approved,
|
|
@@ -65239,6 +65456,7 @@ var init_build_steps = __esm({
|
|
|
65239
65456
|
"Industry",
|
|
65240
65457
|
"Assessment",
|
|
65241
65458
|
"Intake",
|
|
65459
|
+
"What they bought",
|
|
65242
65460
|
"Brief",
|
|
65243
65461
|
"Staff",
|
|
65244
65462
|
"Approve",
|
|
@@ -65253,9 +65471,10 @@ var init_build_steps = __esm({
|
|
|
65253
65471
|
"What kind of business is this? It picks the intake questions and the plan.",
|
|
65254
65472
|
"Talk with the owner and attach the Assessment, or build without one.",
|
|
65255
65473
|
"The business details and your team. We fill in what your notes and the call already say.",
|
|
65474
|
+
"Tick what your client paid for, and confirm the questions it asks with them. The plan and the review are written for exactly this.",
|
|
65256
65475
|
"Claude reads the intake and writes the plan for this client.",
|
|
65257
65476
|
"Your team from step 5 must be in the client's GoHighLevel account before anything is built for them.",
|
|
65258
|
-
"Read the plan
|
|
65477
|
+
"Read the plan and say yes. Nothing is built until you do.",
|
|
65259
65478
|
"We build the plan in GoHighLevel. You can watch it happen.",
|
|
65260
65479
|
"We check that every part works.",
|
|
65261
65480
|
"The jobs you do by hand in GoHighLevel. Each says how, and gives you a question to ask Claude for step by step help.",
|
|
@@ -65297,7 +65516,7 @@ function bsInputs(c,state,run){const id=c.locationId;const st=(((state.clients||
|
|
|
65297
65516
|
const sc=(state.staffChecks||{})[id];const ii=((state.intakeInputs||{})[id])||{};
|
|
65298
65517
|
const intakeIn=Boolean((state.intakeCompleted||{})[id])||ii.status==="received";
|
|
65299
65518
|
return {intakeIn:intakeIn,typeConfirmed:Boolean(acc.typeConfirmed),parked:["template","hidden"].includes(typeOf(state,id)),industry:acc.industry||"",assessment:assess,stages:st,stale:stale,
|
|
65300
|
-
running:(run&&run.running&&run.locationId===id&&typeof run.stage==="number")?run.stage:-1,staffReady:Boolean(sc&&sc.status==="ready"),approved:Boolean((state.buildApproved||{})[id])}}
|
|
65519
|
+
running:(run&&run.running&&run.locationId===id&&typeof run.stage==="number")?run.stage:-1,staffReady:Boolean(sc&&sc.status==="ready"),approved:Boolean((state.buildApproved||{})[id]),scopeSaved:scopeChosen(state,id)}}
|
|
65301
65520
|
/* Where a client stands, for the My clients list: "Waiting on step 5 of 12: Intake". */
|
|
65302
65521
|
function bsWhere(c,state,run){const t=typeOf(state,c.locationId);if(t==="template"||t==="hidden")return {step:-1,text:"Not built: a "+TYPE_LABELS[t].toLowerCase()+" account.",cta:"Open"};
|
|
65303
65522
|
const p=stepProgress(bsInputs(c,state,run));const n=BS_NAMES.length;
|
|
@@ -65374,14 +65593,15 @@ function bsSummary(n,c,state){const id=c.locationId;const acc=((state.accounts||
|
|
|
65374
65593
|
if(n===2){const f=INDUSTRIES2.find(x=>x.slug===acc.industry);return f?f.label:(acc.industry||"")}
|
|
65375
65594
|
if(n===3){const a=bsInputs(c,state,null).assessment;return a==="attached"?"Assessment attached.":a==="without"?"Building without an Assessment.":"Answered before this page existed."}
|
|
65376
65595
|
if(n===4)return "Intake finished.";
|
|
65377
|
-
if(n===5)
|
|
65378
|
-
if(n===6)
|
|
65379
|
-
if(n===7){const
|
|
65596
|
+
if(n===5){const sp=(state.scope||{})[id]||{};const k=(sp.features||[]).length;return sp.wholePreset?"Everything in the plan.":"Saved: "+k+" "+(k===1?"feature":"features")+" bought."}
|
|
65597
|
+
if(n===6)return "Brief written. The Blueprint Review is ready for step 9.";
|
|
65598
|
+
if(n===7){const sc=(state.staffChecks||{})[id];return sc&&sc.matched&&sc.matched.length?"In the account: "+sc.matched.map(m=>m.name).join(", ")+".":"Staff confirmed."}
|
|
65599
|
+
if(n===8){const a=(state.buildApproved||{})[id];return a?"Approved "+niceDate(a,true)+".":"Approved."}
|
|
65380
65600
|
/* Run 9: the folded step said only "Built.". It says what was made, from the build's own record. */
|
|
65381
|
-
if(n===
|
|
65601
|
+
if(n===9){const bv0=(((state.buildViews||{})[id])||{});const made=(bv0.made&&bv0.made.length)?bv0.made:(bv0.built||[]).filter(x=>x.status==="created");if(!made.length)return "Built.";
|
|
65382
65602
|
const by={};for(const x of made)by[x.type]=(by[x.type]||0)+1;const KIND={pipeline:"pipeline",stage:"pipeline stage",field:"custom field",tag:"tag",cv:"custom value",calendar:"calendar",form:"form",email_template:"email template",sms_template:"text template",workflow:"draft automation",user:"staff login"};
|
|
65383
65603
|
return "Built in GoHighLevel: "+Object.entries(by).map(([k,v])=>v+" "+plural(v,KIND[k]||k)).join(", ")+". Nothing is switched on."}
|
|
65384
|
-
if(n===
|
|
65604
|
+
if(n===10){const oc=(state.outcomes||{})[id+":4"];const must=bsMustDo(c,state).length;
|
|
65385
65605
|
/* Never "clean" while a job that blocks go-live is open (blind run 2: "clean" beside "no texts can be sent"). */
|
|
65386
65606
|
/* Command-center 2026-10-03: never "clean" on ticks alone. */
|
|
65387
65607
|
const tk=(((state.jobCounts||{})[id])||{}).ticked||0;
|
|
@@ -65390,14 +65610,14 @@ function bsSummary(n,c,state){const id=c.locationId;const acc=((state.accounts||
|
|
|
65390
65610
|
const conf=((oc&&oc.verification&&oc.verification.memberJobs)||{}).confirmed;
|
|
65391
65611
|
const lead=typeof conf==="number"?"GoHighLevel confirms "+conf+" "+plural(conf,"item")+".":"The check finished.";
|
|
65392
65612
|
const mine=(tk||typeof conf==="number")?" You confirmed "+tk+" yourself.":"";
|
|
65393
|
-
if(oc&&oc.ok===true)return must?lead+mine+" "+must+" "+(must===1?"job is":"jobs are")+" required before sign-off: see step
|
|
65613
|
+
if(oc&&oc.ok===true)return must?lead+mine+" "+must+" "+(must===1?"job is":"jobs are")+" required before sign-off: see step 12.":lead+mine+" Nothing else is required before sign-off.";
|
|
65394
65614
|
return oc?"Checked. Some parts need another look: see the notes.":"Checked."}
|
|
65395
|
-
if(n===
|
|
65615
|
+
if(n===11)return "Hand jobs confirmed.";
|
|
65396
65616
|
return "Signed off. Your next jobs in GoHighLevel are listed on this step."}
|
|
65397
65617
|
/* Steps 2, 3, 4, 5 and 7 can be changed later; the server marks what comes after them out of date. */
|
|
65398
|
-
const BS_CHANGEABLE=[1,2,3,4];
|
|
65618
|
+
const BS_CHANGEABLE=[1,2,3,4,5];
|
|
65399
65619
|
/* The saved stage each step runs or marks (null: a step the cockpit keeps itself). */
|
|
65400
|
-
const BS_STAGE_OF=[null,null,null,null,0,1,null,2,3,4,5,6];
|
|
65620
|
+
const BS_STAGE_OF=[null,null,null,null,0,null,1,null,2,3,4,5,6];
|
|
65401
65621
|
function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
|
|
65402
65622
|
/* An older build recorded in the account is named, never applied (run 7: a fresh start showed old steps as done). */
|
|
65403
65623
|
if(n===0){const old=(state.olderShared||{})[id];if(old){const done=(old.stages||[]).filter(s=>s==="done").length;box.appendChild(mk("p","cds-callout","An older build of this client is recorded in its GoHighLevel account ("+done+" "+plural(done,"step")+" done, last updated "+(niceDate(old.updated_at,true)||old.updated_at)+"). It does not match this client's intake here, so none of it is shown as done. These steps start fresh. If a teammate is building this same client, you can use their progress instead."));
|
|
@@ -65429,20 +65649,23 @@ function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
|
|
|
65429
65649
|
const j=await bsPost("/api/intake-complete",body);bsBusy[key]=false;
|
|
65430
65650
|
if(!j.ok)bsErr[key]=j.error;else{delete bsChange[id+":4"];delete bsStaff[id];delete bsDraft[id+":team"];if(j.approvalWithdrawn)bsNote[id]=j.message;iMode[id]=null;if(iDirty[id])iDirty[id].clear();delete iData[id]}load(true)}));
|
|
65431
65651
|
bsErrLine(key,box);return}
|
|
65432
|
-
|
|
65433
|
-
|
|
65434
|
-
if(n===
|
|
65652
|
+
/* OS audit F11 (command-center ruling (c), 2026-10-30): what the client bought is asked before the Brief, so the
|
|
65653
|
+
Blueprint Review is written for exactly what was sold and the normal order never leaves it out of date. */
|
|
65654
|
+
if(n===5){bsHideAssess=true;try{box.appendChild(scopeBlock(id,state))}finally{bsHideAssess=false}return}
|
|
65655
|
+
if(n===6){bsOutcome(c,state,1,box);bsRun(c,state,run,1,(((state.clients[id]||{}).stages||[])[1]==="done")?"Write the Brief again":"Write the Brief",box);return}
|
|
65656
|
+
if(n===7){bsStaffStep(c,state,box);return}
|
|
65657
|
+
if(n===8){const a=bsInputs(c,state,null).assessment;
|
|
65435
65658
|
if(a!=="attached"&&a!=="without"){box.appendChild(mk("p","cds-callout warn","Approve waits for step 4: attach the Assessment there, or choose Build without an Assessment."));box.appendChild(bsBtn("Open step 4",()=>{bsChange[id+":3"]=bsSummary(3,c,state);render(lastState,lastRun)},true))}
|
|
65436
65659
|
if(!rData[id])rLoad(id);bsHideAssess=true;try{box.appendChild(unnest(reviewPanel(id,c.name,state,run)))}finally{bsHideAssess=false}return}
|
|
65437
|
-
if(n===
|
|
65660
|
+
if(n===9){const bp=((state.buildPreviews||{})[id]);const live=run&&run.running&&run.locationId===id&&run.stage===3;
|
|
65438
65661
|
if(bp&&!bp.confirmed&&!live){bsPause(c,state,bp.preview,box);return}
|
|
65439
65662
|
bsOutcome(c,state,3,box);
|
|
65440
65663
|
/* Run 8 finding 4: it ended on just "Built." The finish says what was made, counted from the build's own record. */
|
|
65441
65664
|
if(!live&&(((state.clients||{})[id]||{}).stages||[])[3]==="done"){const bv0=(((state.buildViews||{})[id])||{});const made=(bv0.made&&bv0.made.length)?bv0.made:(bv0.built||[]).filter(x=>x.status==="created");
|
|
65442
65665
|
if(made.length){const by={};for(const x of made)by[x.type]=(by[x.type]||0)+1;const KIND={pipeline:"pipeline",stage:"pipeline stage",field:"custom field",tag:"tag",cv:"custom value",calendar:"calendar",form:"form",email_template:"email template",sms_template:"text template",workflow:"draft automation",user:"staff login"};
|
|
65443
65666
|
box.appendChild(mk("p","","Made in GoHighLevel this build: "+Object.entries(by).map(([k,n])=>n+" "+plural(n,KIND[k]||k)).join(", ")+". Every automation is a draft until you switch it on."))}}if(!live)bsSnippets(c,state,box);bsRun(c,state,run,3,(((state.clients[id]||{}).stages||[])[3]==="done")?"Build again":"Start the build",box);return}
|
|
65444
|
-
if(n===
|
|
65445
|
-
if(n===
|
|
65667
|
+
if(n===10){bsOutcome(c,state,4,box);bsRun(c,state,run,4,(state.outcomes||{})[id+":4"]?"Run the check again":"Run the check",box);if((state.outcomes||{})[id+":4"])bsRebuild(c,state,run,box);return}
|
|
65668
|
+
if(n===11){const jobs=bsJobs(c,state);if(jobs)box.appendChild(jobs);
|
|
65446
65669
|
/* The handoff panel is here as well as in step 12: with a chosen home, its publish job blocks step 11 and only
|
|
65447
65670
|
this panel clears it (round-3 review). With no home chosen it is a later job (option A, 2026-10-02). */
|
|
65448
65671
|
const view=((state.buildViews||{})[id])||{};if(view.handoff)box.appendChild(unnest(handoffPanel(c,view)));
|
|
@@ -65451,7 +65674,7 @@ function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
|
|
|
65451
65674
|
if(left5){const off=mk("button","cds-btn sbig","Confirm these tasks are finished");off.type="button";off.disabled=true;box.appendChild(off);box.appendChild(mk("p","muted",left5+" "+plural(left5,"job")+" before sign-off still open. The button turns on when none are left."))}
|
|
65452
65675
|
else bsGate(c,state,5,"Confirm these tasks are finished",box);
|
|
65453
65676
|
bsRecheck(c,state,run,box);return}
|
|
65454
|
-
if(n===
|
|
65677
|
+
if(n===12){const view=((state.buildViews||{})[id])||{};
|
|
65455
65678
|
/* Jerry 2026-10-03: Sign-Off, not Go-Live. Say what Command OS did, then what the person does by hand. */
|
|
65456
65679
|
box.appendChild(bsSignOffList(c,state));
|
|
65457
65680
|
if(view.handoff)box.appendChild(unnest(handoffPanel(c,view)));bsGate(c,state,6,"Sign off this build",box);bsRecheck(c,state,run,box);return}}
|
|
@@ -65466,7 +65689,18 @@ function bsPause(c,state,pv,box){const id=c.locationId;const key="confirm:"+id;
|
|
|
65466
65689
|
/* Ticks belong to one list: a new list starts unticked, and only keys on the list shown are sent (review of a38ebe1). */
|
|
65467
65690
|
if(!bsAllow[id]||bsAllow[id].fp!==pv.fingerprint)bsAllow[id]={fp:pv.fingerprint,keys:{}};const allow=bsAllow[id].keys;
|
|
65468
65691
|
box.appendChild(mk("h3","","Before Build writes anything"));
|
|
65692
|
+
/* OS audit F9 (command-center ruling, 2026-10-30): a build of a different plan left items this build would build on
|
|
65693
|
+
top of. No building on top: undo that build first (Claude lists what it removes and waits for DELETE), or stop. */
|
|
65694
|
+
if((pv.earlierBuild||[]).length){const eb=pv.earlierBuild,cn=c.name||"This account",runs=[...new Set(eb.map(e=>e.runId))];
|
|
65695
|
+
const w=mk("div","cds-callout warn bs-earlier");w.setAttribute("role","alert");
|
|
65696
|
+
w.appendChild(mk("p","",cn+" already has "+(eb.length===1?"an item":eb.length+" items")+" an earlier build of a different plan made: "+eb.map(e=>"\u201c"+e.name+"\u201d").join(", ")+". Building now would build on top of "+(eb.length===1?"it":"them")+", which is how duplicates happen, so Build will not start. Undo the earlier build first, or stop here."));
|
|
65697
|
+
w.appendChild(mk("b","","Undo the earlier build: ask Claude"));
|
|
65698
|
+
for(const r of runs){const q=mk("p","say","Undo build run "+r+" for "+cn);w.appendChild(q)}
|
|
65699
|
+
w.appendChild(mk("p","muted","Claude lists everything it would remove first, and removes nothing until you type DELETE. Then come back and Start the build."));
|
|
65700
|
+
w.appendChild(mk("b","","Or stop here"));w.appendChild(mk("p","muted","This build has not written anything to "+cn+". Leave this page; nothing changes."));
|
|
65701
|
+
box.appendChild(w);return}
|
|
65469
65702
|
box.appendChild(mk("p","","Nothing has been written to "+(c.name||"this account")+" yet. Here is what Build will do. Check it, then confirm."));
|
|
65703
|
+
if((pv.unrecordedDrafts||[]).length)box.appendChild(mk("p","cds-callout info","Already in the account as drafts, made by no build on record: "+pv.unrecordedDrafts.map(n=>"\u201c"+n+"\u201d").join(", ")+". Build uses them as they are; check they are the ones you mean."));
|
|
65470
65704
|
/* Walk 2026-10-28 W13/W8: "17 emails, 8 email templates" said nothing about the difference, and the list named nothing.
|
|
65471
65705
|
Each kind says what it is, and opens to the name of every object of that kind. */
|
|
65472
65706
|
const kindWord=(k,n)=>{const w=BS_KIND[k]||k;const sp=w.indexOf(" inside ");return n===1?w:(sp>0?w.slice(0,sp)+"s"+w.slice(sp):w+"s")};
|
|
@@ -65681,7 +65915,7 @@ function bsJobs(c,state){const mt=((state.manualTasks||{})[c.locationId])||[];co
|
|
|
65681
65915
|
else if(readJob&&g.timing!=="at-go-live"){
|
|
65682
65916
|
/* Walk W21: what the check last found, and what to do about it. */
|
|
65683
65917
|
const found=why?" "+String(why).replace(/^GoHighLevel shows /,"GoHighLevel still shows "):" The check reads this in GoHighLevel and has not seen it done yet.";
|
|
65684
|
-
note("Waiting for the check:"+found+(/ empty\.$/.test(String(why||""))?" Set it":" Do it in GoHighLevel")+", then press Run the check again in step
|
|
65918
|
+
note("Waiting for the check:"+found+(/ empty\.$/.test(String(why||""))?" Set it":" Do it in GoHighLevel")+", then press Run the check again in step 11, Verify.")}
|
|
65685
65919
|
else if(cb.checked&&g.items.some(t=>t.blocks))note("Ticked. The check confirms it next time it runs.");return row};
|
|
65686
65920
|
const must=groups.filter(g=>g.timing==="before"),on=groups.filter(g=>g.timing==="at-go-live"),later=groups.filter(g=>g.timing==="later");
|
|
65687
65921
|
const left=must.filter(g=>g.items.some(t=>t.blocks!==undefined?t.blocks:!t.claimed)).length;
|
|
@@ -65739,8 +65973,8 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
|
|
|
65739
65973
|
/* A Change closes itself once that step is done with a different answer (review of 64da59d, LOW 15). */
|
|
65740
65974
|
if(bsChange[ck]&&pr.done[i]&&typeof bsChange[ck]==="string"&&bsSummary(i,c,state)!==bsChange[ck])delete bsChange[ck];
|
|
65741
65975
|
/* Step 7 stays open while what the last Add them for me said, or its refusal, is there to read (3.90.1). */
|
|
65742
|
-
if(i===
|
|
65743
|
-
const staffNews=i===
|
|
65976
|
+
if(i===7)bsRevealPasswords(id,(state.staffChecks||{})[id]||bsStaff[id]);
|
|
65977
|
+
const staffNews=i===7&&(((bsStaffSaid[id]||[]).length>0)||Boolean(bsErr["staff:"+id]));
|
|
65744
65978
|
const li=mk("li","bs-step");li.id="step-"+(i+1);const open=(i===pr.current)||Boolean(bsChange[ck])||staffNews;
|
|
65745
65979
|
const num=mk("span","bs-num",String(i+1));num.setAttribute("aria-hidden","true");li.appendChild(num);const main=mk("div","bs-main");li.appendChild(main);
|
|
65746
65980
|
if(open){li.className+=" bs-now";main.appendChild(mk("div","bs-kicker","Step "+(i+1)+" of "+n+(i===pr.current?" · do this now":" · changing")));main.appendChild(mk("h3","bs-title",BS_NAMES[i]));main.appendChild(mk("p","muted",BS_ABOUT[i]));
|
|
@@ -65759,8 +65993,8 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
|
|
|
65759
65993
|
ch.onclick=()=>{bsChange[ck]=bsSummary(i,c,state);render(lastState,lastRun)};main.appendChild(ch);
|
|
65760
65994
|
if(i>=2)main.appendChild(mk("p","muted bs-warn","Saving a change here means the steps after it run again."))}
|
|
65761
65995
|
/* Step 8 can be taken back until Build runs (review of 64da59d: a team change after Approve had no way back). */
|
|
65762
|
-
if(i===
|
|
65763
|
-
un.onclick=()=>icShow(un,uk,{title:"Undo the approval for "+c.name,text:["Step
|
|
65996
|
+
if(i===8&&idle&&!pr.done[9]){const un=mk("button","cds-link","Undo the approval");un.type="button";const uk="unapprove:"+id;
|
|
65997
|
+
un.onclick=()=>icShow(un,uk,{title:"Undo the approval for "+c.name,text:["Step 9 opens again. Nothing is built until you approve again."],ok:"Undo the approval",busyLabel:"Undoing…",
|
|
65764
65998
|
run:async()=>{const j=await bsPost("/api/build-approval",{locationId:id,revoke:true});return j.ok?"":j.error},after:async()=>{load(true)}});
|
|
65765
65999
|
main.appendChild(icAfter(un,uk))}}
|
|
65766
66000
|
else{li.className+=" bs-locked";const t=mk("div","bs-row");t.appendChild(mk("b","bs-title",BS_NAMES[i]));t.appendChild(chip("","After step "+i));main.appendChild(t);main.appendChild(mk("p","muted",BS_ABOUT[i]))}
|
|
@@ -65773,7 +66007,7 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
|
|
|
65773
66007
|
if(view.approvedAt)d.appendChild(mk("p","","Building approved on "+niceDate(view.approvedAt,true)+"."));page.appendChild(d)}}
|
|
65774
66008
|
/* The step that just opened comes into view, once per change (and on first open), after the redraw keeps its scroll. */
|
|
65775
66009
|
if(bsSeen[id]!==pr.current){bsSeen[id]=pr.current;if(pr.current>0)setTimeout(()=>{const el=document.getElementById("step-"+(pr.current+1));if(el)try{el.scrollIntoView({block:"start",behavior:"smooth"})}catch(e){el.scrollIntoView()}},0)}
|
|
65776
|
-
if(pr.current<0){page.appendChild(mk("p","cds-callout info","All "+n+" steps are done. The build is signed off. Switching the client on is yours to do in GoHighLevel: see step
|
|
66010
|
+
if(pr.current<0){page.appendChild(mk("p","cds-callout info","All "+n+" steps are done. The build is signed off. Switching the client on is yours to do in GoHighLevel: see step 13's list."));
|
|
65777
66011
|
/* Walk W26 (2026-10-29): after sign-off nothing could start the build again, and the page still said Built after its
|
|
65778
66012
|
objects were deleted in GoHighLevel. The check can always run again; when it finds built objects gone, it says how
|
|
65779
66013
|
many and Build again is offered beside it. */
|
|
@@ -65902,7 +66136,7 @@ ${cockpitHeaderHtml("agency")}
|
|
|
65902
66136
|
</div>
|
|
65903
66137
|
<div class="ap-save"><span id="msgTop" role="status"></span><button type="submit" form="f" class="cds-btn" data-msg="msgTop">Save your agency details</button></div>
|
|
65904
66138
|
</div>
|
|
65905
|
-
${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an agency name to use this on client documents.</span>Everything else can wait. The name is what every report and proposal carries.</div>
|
|
66139
|
+
${p.agencyName ? "" : ` <div class="cds-callout warn agency-name-nudge"><span class="ttl">Add an agency name to use this on client documents.</span>Everything else can wait. The name is what every report and proposal carries.</div>
|
|
65906
66140
|
`}
|
|
65907
66141
|
<form id="f">
|
|
65908
66142
|
<div class="cds-grid two">
|
|
@@ -66208,7 +66442,7 @@ document.getElementById("f").addEventListener("submit",async e=>{
|
|
|
66208
66442
|
if(!r.ok||(j&&j.ok===false)){m.textContent=(j&&j.error)||"That did not save, and nothing was changed.";return}
|
|
66209
66443
|
const pkgNote=settlePackages(j&&j.profile,pkgRead);
|
|
66210
66444
|
const chatOk=await saveChatCreation();
|
|
66211
|
-
m.textContent=(j&&j.usable?"Saved.":"Saved. Add an agency name to use this on client documents.")+pkgNote+(chatOk?"":" The chat creation setting did not save. Try again.");
|
|
66445
|
+
/* OS audit F15: the nudge stayed beside "Saved." until a reload; it goes once the name is saved. */if(j&&j.usable)document.querySelectorAll(".agency-name-nudge").forEach(function(el){el.remove()});m.textContent=(j&&j.usable?"Saved.":"Saved. Add an agency name to use this on client documents.")+pkgNote+(chatOk?"":" The chat creation setting did not save. Try again.");
|
|
66212
66446
|
});
|
|
66213
66447
|
|
|
66214
66448
|
/* Each client saves on its own. One record at a time is the whole safety story
|
|
@@ -66869,7 +67103,7 @@ function compute(ans, skip, asm) {
|
|
|
66869
67103
|
add("missed", "Calls you never get back to",
|
|
66870
67104
|
[{ id: "calls_missed" }, { id: "missed_never_return", share: true }], null,
|
|
66871
67105
|
function () { return raw("calls_missed") * share("missed_never_return") * c * v; },
|
|
66872
|
-
"Call answering
|
|
67106
|
+
"Call answering");
|
|
66873
67107
|
|
|
66874
67108
|
add("waited", "Enquiries that waited",
|
|
66875
67109
|
[{ id: "waited_month" }], "response_penalty",
|
|
@@ -68770,7 +69004,7 @@ ${yours}
|
|
|
68770
69004
|
<p>Give us the client's website and we read its colors, fonts and logo into a brand you can check and change.</p>
|
|
68771
69005
|
<form id="extract" class="cds-stack bp-form">
|
|
68772
69006
|
<div class="cds-field"><label for="url">Website</label><input class="cds-input" id="url" name="url" type="url" placeholder="https://clinic.example" required autocomplete="url"></div>
|
|
68773
|
-
<div class="cds-field"><label for="rights">Whose site is it?</label><select class="cds-select" id="rights" name="rights"><option value="client-authorized">My client's, and they asked for this</option><option value="own">Mine</option><option value="written-permission">I have written permission</option><option value="none">None of these</option></select></div>
|
|
69007
|
+
<div class="cds-field"><label for="rights">Whose site is it?</label><select class="cds-select" id="rights" name="rights" required><option value="" selected disabled>Choose one</option><option value="client-authorized">My client's, and they asked for this</option><option value="own">Mine</option><option value="written-permission">I have written permission</option><option value="none">None of these</option></select></div>
|
|
68774
69008
|
<div class="cds-field"><label for="name">Name (optional)</label><div class="hint" id="name-hint">What to call this brand here. Left empty, we take it from the site's title.</div><input class="cds-input" id="name" name="name" type="text" autocomplete="off" aria-describedby="name-hint"></div>
|
|
68775
69009
|
<label class="cds-check"><input type="checkbox" id="useBrowser"><span>Also open the site in a browser on this computer<small>Tick this when the first try finds almost nothing. Some sites only draw themselves in a browser. It takes longer.</small></span></label>
|
|
68776
69010
|
<div class="cds-row"><button type="submit" class="cds-btn ghost" id="extract-btn">Pull the brand</button></div>
|
|
@@ -68802,7 +69036,7 @@ fromHash();
|
|
|
68802
69036
|
const rf=document.getElementById("ref-filter");
|
|
68803
69037
|
if(rf)rf.addEventListener("input",()=>{const q=rf.value.trim().toLowerCase();let n=0;for(const b of document.querySelectorAll("[data-look]")){const hit=!q||b.textContent.toLowerCase().includes(q);b.hidden=!hit;if(hit)n++}const c=document.getElementById("ref-count");if(c)c.textContent=q?(n?n+" found":"No look by that name"):""});
|
|
68804
69038
|
})();
|
|
68805
|
-
document.getElementById("extract").addEventListener("submit",async(e)=>{e.preventDefault();const b=document.getElementById("extract-btn");b.disabled=true;say("extract-status","Reading the site\u2026 this takes a few seconds, longer with the browser pass.");try{const j=await post("/api/brand/extract",{url:document.getElementById("url").value,rights:document.getElementById("rights").value,name:document.getElementById("name").value||undefined,useBrowser:document.getElementById("useBrowser").checked});say("extract-status","Stored as "+j.brand.slug+". Opening the card\u2026","ok");location.href="/brand/"+encodeURIComponent(j.brand.slug)}catch(err){say("extract-status",err.message,"err")}finally{b.disabled=false}});
|
|
69039
|
+
document.getElementById("extract").addEventListener("submit",async(e)=>{e.preventDefault();/* OS audit F7: a permission is never answered for the member. */if(!document.getElementById("rights").value){say("extract-status","Choose whose site it is first.","err");return}const b=document.getElementById("extract-btn");b.disabled=true;say("extract-status","Reading the site\u2026 this takes a few seconds, longer with the browser pass.");try{const j=await post("/api/brand/extract",{url:document.getElementById("url").value,rights:document.getElementById("rights").value,name:document.getElementById("name").value||undefined,useBrowser:document.getElementById("useBrowser").checked});say("extract-status","Stored as "+j.brand.slug+". Opening the card\u2026","ok");location.href="/brand/"+encodeURIComponent(j.brand.slug)}catch(err){say("extract-status",err.message,"err")}finally{b.disabled=false}});
|
|
68806
69040
|
for(const btn of document.querySelectorAll("[data-look]")){btn.addEventListener("click",async()=>{btn.disabled=true;say("look-status","Fetching "+btn.textContent+"\u2026");try{const j=await post("/api/brand/import-look",{look:btn.dataset.look});say("look-status","Imported as "+j.brand.slug+". Opening the card\u2026","ok");location.href="/brand/"+encodeURIComponent(j.brand.slug)}catch(err){say("look-status",err.message,"err");btn.disabled=false}})}
|
|
68807
69041
|
for(const btn of document.querySelectorAll("[data-house]")){btn.addEventListener("click",async()=>{const as=prompt("Store this look under which name? (letters, digits, dashes)",btn.dataset.house+"-copy");if(!as)return;btn.disabled=true;try{const j=await post("/api/brand/set",{slug:btn.dataset.house,as});location.href="/brand/"+encodeURIComponent(j.brand.slug)}catch(err){alert(err.message);btn.disabled=false}})}
|
|
68808
69042
|
`)}`;
|
|
@@ -68941,7 +69175,10 @@ document.getElementById("make").addEventListener("submit",async function(e){
|
|
|
68941
69175
|
var run=s.run||{};
|
|
68942
69176
|
var design=run.ok?"design PASS on pass "+run.greenAt:"design NOT GREEN";
|
|
68943
69177
|
var copy=run.copy?(run.copy.error?" \xB7 copy not graded":" \xB7 copy "+run.copy.verdict+(run.copy.firstBreakdown?" (first breakdown: "+run.copy.firstBreakdown+")":"")):"";
|
|
68944
|
-
|
|
69178
|
+
var saved=String(run.file||s.outDir||"").split(/[\\\\/]/).filter(Boolean).pop()||"";
|
|
69179
|
+
say("mk-status",s.status==="done"?design+copy+(saved?" \xB7 saved as "+saved:""):"The run stopped: "+design+copy,s.status==="done"&&run.ok?"ok":"err");
|
|
69180
|
+
/* OS audit F10: a way to open what was made. */
|
|
69181
|
+
if(s.status==="done"&&saved&&/.html?$/i.test(saved)){var st=document.getElementById("mk-status");var a=document.createElement("a");a.className="cds-btn";a.href="/design/result?id="+encodeURIComponent(runId);a.target="_blank";a.rel="noopener";a.textContent="Open the result";st.appendChild(document.createTextNode(" "));st.appendChild(a)}
|
|
68945
69182
|
}
|
|
68946
69183
|
},4000);
|
|
68947
69184
|
}catch(err){stopPolling();started=0;say("mk-status",err.message,"err");go.disabled=false}
|
|
@@ -69652,6 +69889,14 @@ function reviewPrompt(input) {
|
|
|
69652
69889
|
"For each gap, state the cost using their own numbers when they gave any, and what we build to close it:",
|
|
69653
69890
|
...GAP_CHECKLIST.map((g) => ` \u2022 ${g}`),
|
|
69654
69891
|
"",
|
|
69892
|
+
...input.promises ? [
|
|
69893
|
+
"WHAT THIS BUILD MAKES. You do not list what we build or how each gap is fixed: the document takes that from this list itself. You write the diagnosis only. Your recommended offer and biggest lever may name a system or automation ONLY if it is on this list:",
|
|
69894
|
+
...input.promises.willBuild.length ? input.promises.willBuild.map((w) => ` \u2022 ${w}`) : [" \u2022 (nothing yet)"],
|
|
69895
|
+
...input.promises.held.length ? ["WAITING ON THE CLIENT'S ANSWER (never say we build these):", ...input.promises.held.map((h) => ` \u2022 ${h}`)] : [],
|
|
69896
|
+
'Never promise that calls are answered live, "day and night", "24/7" or "every call": nothing in this build answers the phone.',
|
|
69897
|
+
...input.promises.textingOn ? [] : ["This client is not texting: never promise a text, an SMS or a text-back anywhere."],
|
|
69898
|
+
""
|
|
69899
|
+
] : [],
|
|
69655
69900
|
"THE CLIENT'S INTAKE ANSWERS:",
|
|
69656
69901
|
answers || "(no answers provided)",
|
|
69657
69902
|
"",
|
|
@@ -69663,7 +69908,7 @@ function reviewPrompt(input) {
|
|
|
69663
69908
|
] : [],
|
|
69664
69909
|
'A gap about deposits or prepayment is never priority "now": deposits are set up later and never hold up sign-off.',
|
|
69665
69910
|
"Reply with ONE line of JSON and nothing else:",
|
|
69666
|
-
'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","
|
|
69911
|
+
'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","priority":"now"}],"unknowns":[""]}'
|
|
69667
69912
|
].join("\n");
|
|
69668
69913
|
}
|
|
69669
69914
|
function accountPrefixes(clientName) {
|
|
@@ -69719,6 +69964,97 @@ function strictRetryPrompt(input) {
|
|
|
69719
69964
|
"No preamble, no explanation, no markdown fences, no closing remarks."
|
|
69720
69965
|
].join("\n");
|
|
69721
69966
|
}
|
|
69967
|
+
function renderedLines(r) {
|
|
69968
|
+
const out = [];
|
|
69969
|
+
const add2 = (where, text3) => {
|
|
69970
|
+
if (typeof text3 === "string" && text3.trim()) out.push({ where, text: text3 });
|
|
69971
|
+
};
|
|
69972
|
+
add2("headline", r.headline);
|
|
69973
|
+
if (r.ica) {
|
|
69974
|
+
add2("ica.who", r.ica.who);
|
|
69975
|
+
add2("ica.buyingTrigger", r.ica.buyingTrigger);
|
|
69976
|
+
add2("ica.whereTheyAre", r.ica.whereTheyAre);
|
|
69977
|
+
(r.ica.painPoints ?? []).forEach((t, i) => add2(`ica.painPoints.${i}`, t));
|
|
69978
|
+
(r.ica.objections ?? []).forEach((t, i) => add2(`ica.objections.${i}`, t));
|
|
69979
|
+
}
|
|
69980
|
+
if (r.offer) for (const k of ["dreamOutcome", "believability", "speed", "effort", "biggestLever", "recommendedOffer"]) add2(`offer.${k}`, r.offer[k]);
|
|
69981
|
+
(r.gaps ?? []).forEach((g, i) => {
|
|
69982
|
+
add2(`gaps.${i}.title`, g.title);
|
|
69983
|
+
add2(`gaps.${i}.cost`, g.cost);
|
|
69984
|
+
});
|
|
69985
|
+
(r.unknowns ?? []).forEach((t, i) => add2(`unknowns.${i}`, t));
|
|
69986
|
+
return out;
|
|
69987
|
+
}
|
|
69988
|
+
function overPromises(r, p) {
|
|
69989
|
+
if (!p || !r.ok) return [];
|
|
69990
|
+
const made = p.willBuild.join(" | ");
|
|
69991
|
+
return renderedLines(r).filter((l) => LIVE_CALLS.test(l.text) || (!p.textingOn || p.held.some((h) => /texting/i.test(h))) && TEXTS.test(l.text) || TOPICS.some((t) => t.re.test(l.text) && !t.built.test(made))).map((l) => l.where);
|
|
69992
|
+
}
|
|
69993
|
+
function graderPrompt(r, p) {
|
|
69994
|
+
return [
|
|
69995
|
+
"You check a client document for promises. A line promises something if it says or implies that we will build, set up, run or deliver it for the client.",
|
|
69996
|
+
"WHAT THIS BUILD MAKES (the only things a line may promise):",
|
|
69997
|
+
...p.willBuild.length ? p.willBuild.map((w) => `- ${w}`) : ["- (nothing)"],
|
|
69998
|
+
...p.held.length ? ["NOT BUILT, WAITING ON THE CLIENT'S ANSWER (a line promising these is a violation):", ...p.held.map((h) => `- ${h}`)] : [],
|
|
69999
|
+
`Texting is ${p.textingOn ? "on" : "OFF: any promise of a text, SMS or text-back is a violation"}. Nothing in this build answers the phone live: any promise of answered calls is a violation.`,
|
|
70000
|
+
"Diagnosis, observations about the client and questions are not promises. Advice clearly marked as for later is not a promise.",
|
|
70001
|
+
"THE LINES (id: text):",
|
|
70002
|
+
...renderedLines(r).map((l) => `${l.where}: ${l.text}`),
|
|
70003
|
+
'Reply with ONE line of JSON and nothing else: RESULT: {"violations":["<id>", ...]}'
|
|
70004
|
+
].join("\n");
|
|
70005
|
+
}
|
|
70006
|
+
function parseGrader(text3, r) {
|
|
70007
|
+
const json3 = extractResultJson(text3);
|
|
70008
|
+
if (json3 === void 0) return null;
|
|
70009
|
+
try {
|
|
70010
|
+
const v = JSON.parse(json3).violations;
|
|
70011
|
+
if (!Array.isArray(v)) return null;
|
|
70012
|
+
const known = new Set(renderedLines(r).map((l) => l.where));
|
|
70013
|
+
return v.map(String).filter((x) => known.has(x));
|
|
70014
|
+
} catch {
|
|
70015
|
+
return null;
|
|
70016
|
+
}
|
|
70017
|
+
}
|
|
70018
|
+
function trimLines(r, bad) {
|
|
70019
|
+
if (!bad.length) return r;
|
|
70020
|
+
const set = new Set(bad);
|
|
70021
|
+
const trimmed = renderedLines(r).filter((l) => set.has(l.where)).map((l) => l.text);
|
|
70022
|
+
const keep = (where) => !set.has(where);
|
|
70023
|
+
const ica = r.ica && {
|
|
70024
|
+
...r.ica,
|
|
70025
|
+
who: keep("ica.who") ? r.ica.who : "",
|
|
70026
|
+
buyingTrigger: keep("ica.buyingTrigger") ? r.ica.buyingTrigger : "",
|
|
70027
|
+
whereTheyAre: keep("ica.whereTheyAre") ? r.ica.whereTheyAre : "",
|
|
70028
|
+
painPoints: (r.ica.painPoints ?? []).filter((_, i) => keep(`ica.painPoints.${i}`)),
|
|
70029
|
+
objections: (r.ica.objections ?? []).filter((_, i) => keep(`ica.objections.${i}`))
|
|
70030
|
+
};
|
|
70031
|
+
const offer = r.offer && Object.fromEntries(Object.entries(r.offer).map(([k, v]) => [k, typeof v === "string" && !keep(`offer.${k}`) ? "" : v]));
|
|
70032
|
+
return {
|
|
70033
|
+
...r,
|
|
70034
|
+
headline: keep("headline") ? r.headline : void 0,
|
|
70035
|
+
...ica ? { ica } : {},
|
|
70036
|
+
...offer ? { offer } : {},
|
|
70037
|
+
gaps: (r.gaps ?? []).filter((_, i) => keep(`gaps.${i}.title`) && keep(`gaps.${i}.cost`)),
|
|
70038
|
+
unknowns: (r.unknowns ?? []).filter((_, i) => keep(`unknowns.${i}`)),
|
|
70039
|
+
trimmed: [...r.trimmed ?? [], ...trimmed]
|
|
70040
|
+
};
|
|
70041
|
+
}
|
|
70042
|
+
async function violations(spawnClaude, r, p) {
|
|
70043
|
+
if (!p || !r.ok) return { ids: [], graded: true };
|
|
70044
|
+
const fast = overPromises(r, p);
|
|
70045
|
+
let found = null;
|
|
70046
|
+
for (let attempt2 = 0; attempt2 < 2 && found === null; attempt2++) {
|
|
70047
|
+
try {
|
|
70048
|
+
found = parseGrader(await spawnClaude(graderPrompt(r, p)), r);
|
|
70049
|
+
} catch {
|
|
70050
|
+
found = null;
|
|
70051
|
+
}
|
|
70052
|
+
}
|
|
70053
|
+
return { ids: [.../* @__PURE__ */ new Set([...fast, ...found ?? []])], graded: found !== null };
|
|
70054
|
+
}
|
|
70055
|
+
function structural(r, p) {
|
|
70056
|
+
return { ...r, build: [], gaps: (r.gaps ?? []).map((g) => ({ ...g, fix: "" })), ...p ? { promises: p } : {} };
|
|
70057
|
+
}
|
|
69722
70058
|
async function generateReview(spawnClaude, input) {
|
|
69723
70059
|
const answered = Object.values(input.answers).filter((v) => v && String(v).trim()).length;
|
|
69724
70060
|
if (answered < 5) {
|
|
@@ -69731,7 +70067,21 @@ async function generateReview(spawnClaude, input) {
|
|
|
69731
70067
|
lastRaw = raw;
|
|
69732
70068
|
const names = [input.clientName, ...Object.entries(input.answers).filter(([k]) => /business[ _]?name/i.test(k)).map(([, v]) => v)].join(" ");
|
|
69733
70069
|
const parsed2 = parseReview(raw, names);
|
|
69734
|
-
if (parsed2.ok)
|
|
70070
|
+
if (parsed2.ok) {
|
|
70071
|
+
const first2 = structural(parsed2, input.promises);
|
|
70072
|
+
const over2 = await violations(spawnClaude, first2, input.promises);
|
|
70073
|
+
if (!over2.ids.length) return { ...first2, graded: over2.graded };
|
|
70074
|
+
try {
|
|
70075
|
+
const again = parseReview(await spawnClaude([reviewPrompt(input), "", `IMPORTANT: your previous answer promised what this build does not make (${over2.ids.join(", ")}). Promise only what is listed under WHAT THIS BUILD MAKES.`].join("\n")), names);
|
|
70076
|
+
if (again.ok) {
|
|
70077
|
+
const second = structural(again, input.promises);
|
|
70078
|
+
const v = await violations(spawnClaude, second, input.promises);
|
|
70079
|
+
return { ...trimLines(second, v.ids), graded: v.graded };
|
|
70080
|
+
}
|
|
70081
|
+
} catch {
|
|
70082
|
+
}
|
|
70083
|
+
return { ...trimLines(first2, over2.ids), graded: over2.graded };
|
|
70084
|
+
}
|
|
69735
70085
|
} catch (e) {
|
|
69736
70086
|
lastRaw = e instanceof Error ? e.message : String(e);
|
|
69737
70087
|
}
|
|
@@ -69742,12 +70092,15 @@ async function generateReview(spawnClaude, input) {
|
|
|
69742
70092
|
raw: lastRaw.slice(-600)
|
|
69743
70093
|
};
|
|
69744
70094
|
}
|
|
70095
|
+
function clientHeldLine(line4) {
|
|
70096
|
+
return line4.replace(/^Waiting for your client's answer: /, "Waiting on your answer: ").replace(/^Not built, your client said not yet: /, "Not set up, as you said not yet: ").replace(/^Not built yet, chosen and coming in a later update: /, "Chosen, and coming in a later update: ");
|
|
70097
|
+
}
|
|
69745
70098
|
function reviewDocumentHtml(review, opts) {
|
|
69746
70099
|
const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
|
|
69747
70100
|
const agency = opts.agencyName?.trim() || "";
|
|
69748
70101
|
const esc18 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
69749
70102
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
|
|
69750
|
-
const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}
|
|
70103
|
+
const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}</div>`;
|
|
69751
70104
|
const o = review.offer;
|
|
69752
70105
|
const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
|
|
69753
70106
|
const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
|
|
@@ -69758,7 +70111,7 @@ function reviewDocumentHtml(review, opts) {
|
|
|
69758
70111
|
const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
|
|
69759
70112
|
const assessmentNext = opts.assessment && purchased ? assessmentFeatureLines(opts.assessment, [...purchased, ...represented]) : [];
|
|
69760
70113
|
const available = unbuilt.map((f) => `<div class="build"><b>${esc18(f.name)}</b><span>${esc18(f.what)}${f.figure ? ` <b>${esc18(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line4) => `<p>${esc18(line4)}</p>`).join("");
|
|
69761
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)}
|
|
70114
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)}: Growth Blueprint</title><style>
|
|
69762
70115
|
@page{margin:0.6in}
|
|
69763
70116
|
*{box-sizing:border-box}
|
|
69764
70117
|
body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
|
|
@@ -69813,7 +70166,7 @@ function reviewDocumentHtml(review, opts) {
|
|
|
69813
70166
|
${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc18(o.biggestLever)}</p>` : ""}
|
|
69814
70167
|
${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc18(o.recommendedOffer)}</div>` : ""}` : ""}
|
|
69815
70168
|
${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
|
|
69816
|
-
${review.
|
|
70169
|
+
${review.promises && (review.promises.willBuild.length || review.promises.held.length) ? `<div class="head">What we're building</div>${review.promises.willBuild.map((w) => `<div class="build"><b>${esc18(w)}</b></div>`).join("")}${review.promises.held.map((h) => `<div class="build"><b>${esc18(clientHeldLine(h))}</b></div>`).join("")}` : ""}
|
|
69817
70170
|
${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
|
|
69818
70171
|
<p>Not confirmed yet. The build waits until the features to build are confirmed.</p>
|
|
69819
70172
|
` : opts.scope ? `<div class="head">What this build includes</div>
|
|
@@ -69829,7 +70182,7 @@ function reviewDocumentHtml(review, opts) {
|
|
|
69829
70182
|
<footer><span>${agency ? esc18(agency) : "Growth Blueprint"}</span><span>${esc18(opts.clientName)} \xB7 ${date}</span></footer>
|
|
69830
70183
|
</div></body></html>`;
|
|
69831
70184
|
}
|
|
69832
|
-
var GAP_CHECKLIST;
|
|
70185
|
+
var GAP_CHECKLIST, LIVE_CALLS, TEXTS, TOPICS;
|
|
69833
70186
|
var init_blueprint_review = __esm({
|
|
69834
70187
|
"src/blueprint-review.ts"() {
|
|
69835
70188
|
"use strict";
|
|
@@ -69849,6 +70202,12 @@ var init_blueprint_review = __esm({
|
|
|
69849
70202
|
"SMS capability (A2P registered) if they want to text",
|
|
69850
70203
|
"Reporting they will actually look at"
|
|
69851
70204
|
];
|
|
70205
|
+
LIVE_CALLS = /(?:\bday\s*(?:and|&)\s*night\b|\b24\s*[\/\-\u2013]\s*7\b|\b24 hours\b|\b(?:a)?round[- ]the[- ]clock\b|\bafter[- ]hours?\b|\bevery (?:single )?(?:call|caller|enquiry|inquiry|ring)s?\b|\banswer(?:s|ed|ing)?\b[^.]{0,40}\b(?:phones?|calls?|rings?)\b|\b(?:phones?|calls?)\b[^.]{0,20}\banswer(?:s|ed|ing)?\b|\bcalls? (?:are |get )?answered\b|\bpick(?:s|ing)? up\b|\bsomeone rings\b|\bphones? (?:are |is )?covered\b|\b(?:call|phone) coverage\b|\bcoverage on (?:your |the )?phones?\b|\bno missed calls\b|\bnever miss(?:es)?\b[^.]{0,40}\b(?:calls?|leads?|phone)\b|\bno (?:call|caller) (?:goes|is) (?:unanswered|missed)\b|\blive (?:call |phone )?answering\b|\banswering 24\b|\b(?:ai|virtual) (?:receptionist|voice|phone)\b|\breceptionist\b)/i;
|
|
70206
|
+
TEXTS = /\b(?:texts?|texted|texting|sms|text-?backs?|whatsapp|by message on (?:their|your) (?:mobile|phone)|on their mobile|mobile messages?)\b/i;
|
|
70207
|
+
TOPICS = [
|
|
70208
|
+
{ re: /\b(?:missed[- ]calls?|text[- ]?backs?|callers?)\b/i, built: /missed-call text-back/i },
|
|
70209
|
+
{ re: /\b(?:reactivat\w*|win[- ]?backs?|past (?:clients|customers|patients)|old (?:leads|lists?|clients|customers|patients)|dormant|lapsed|bring (?:them|back)\b|brought back)\b/i, built: /win-back/i }
|
|
70210
|
+
];
|
|
69852
70211
|
}
|
|
69853
70212
|
});
|
|
69854
70213
|
|
|
@@ -70202,11 +70561,11 @@ __export(client_calls_exports, {
|
|
|
70202
70561
|
function gateAnalysis(transcript, analysis) {
|
|
70203
70562
|
const rejected = [];
|
|
70204
70563
|
const kept = {
|
|
70205
|
-
saidTheyWant: (analysis.saidTheyWant ?? []).map(
|
|
70564
|
+
saidTheyWant: (analysis.saidTheyWant ?? []).map(clean5).filter(Boolean),
|
|
70206
70565
|
actuallyNeed: [],
|
|
70207
70566
|
unnamedProblems: [],
|
|
70208
70567
|
recommendedSolutions: [],
|
|
70209
|
-
nextCallQuestions: (analysis.nextCallQuestions ?? []).map(
|
|
70568
|
+
nextCallQuestions: (analysis.nextCallQuestions ?? []).map(clean5).filter(Boolean)
|
|
70210
70569
|
};
|
|
70211
70570
|
const check = (section2, item, quote2) => {
|
|
70212
70571
|
if (!item) {
|
|
@@ -70222,17 +70581,17 @@ function gateAnalysis(transcript, analysis) {
|
|
|
70222
70581
|
};
|
|
70223
70582
|
for (const section2 of ["actuallyNeed", "unnamedProblems"]) {
|
|
70224
70583
|
for (const p of analysis[section2] ?? []) {
|
|
70225
|
-
const point =
|
|
70584
|
+
const point = clean5(p?.point), quote2 = String(p?.quote ?? "").trim();
|
|
70226
70585
|
if (check(section2, point, quote2)) kept[section2].push({ point, quote: quote2 });
|
|
70227
70586
|
}
|
|
70228
70587
|
}
|
|
70229
70588
|
for (const s of analysis.recommendedSolutions ?? []) {
|
|
70230
|
-
const solution =
|
|
70589
|
+
const solution = clean5(s?.solution), quote2 = String(s?.quote ?? "").trim();
|
|
70231
70590
|
if (!SOLUTION_KINDS.includes(s?.kind)) {
|
|
70232
70591
|
rejected.push({ section: "recommendedSolutions", item: solution, quote: quote2, why: `"${String(s?.kind)}" is not a kind; use ${SOLUTION_KINDS.join(", ")}.` });
|
|
70233
70592
|
continue;
|
|
70234
70593
|
}
|
|
70235
|
-
if (check("recommendedSolutions", solution, quote2)) kept.recommendedSolutions.push({ solution, kind: s.kind, why:
|
|
70594
|
+
if (check("recommendedSolutions", solution, quote2)) kept.recommendedSolutions.push({ solution, kind: s.kind, why: clean5(s?.why), quote: quote2 });
|
|
70236
70595
|
}
|
|
70237
70596
|
return { kept, rejected };
|
|
70238
70597
|
}
|
|
@@ -70551,7 +70910,7 @@ async function saveToContact(input, deps, gated, source) {
|
|
|
70551
70910
|
if (accepted.length === 0) {
|
|
70552
70911
|
return done({ status: "NOTHING TO SAVE", reason: effective.held.length ? "every answer is waiting for your OK (numbers always do), so nothing new was written yet." : "no new answer passed the quote check, so nothing new was written.", answersOnContact: record?.state.ans ?? null });
|
|
70553
70912
|
}
|
|
70554
|
-
const biz = record?.state.biz ||
|
|
70913
|
+
const biz = record?.state.biz || clean5(input.businessName) || out.businessName || out.contactName;
|
|
70555
70914
|
if (!biz) return fail("neither the contact nor a saved assessment has a business name. Give businessName and try again.");
|
|
70556
70915
|
out.businessName = out.businessName ?? biz;
|
|
70557
70916
|
const skip = { ...record?.state.skip ?? {} };
|
|
@@ -70648,7 +71007,7 @@ async function processClientCall(input, deps) {
|
|
|
70648
71007
|
To confirm:
|
|
70649
71008
|
${confirmList(gated.held).map((l) => `- ${l}`).join("\n")}
|
|
70650
71009
|
${CONFIRM_INSTRUCTION}`;
|
|
70651
|
-
const clientName = save?.businessName ?? save?.contactName ??
|
|
71010
|
+
const clientName = save?.businessName ?? save?.contactName ?? clean5(input.businessName);
|
|
70652
71011
|
const confirmPass = (input.confirmed?.length ?? 0) > 0;
|
|
70653
71012
|
const clientFile = clientName ? buildClientFile({ clientName, date, source: input.source, gated, analysis, transcript: input.transcript, saveLine, confirmedByOwner: save?.confirmedByOwner }) : null;
|
|
70654
71013
|
if (clientFile && confirmPass) {
|
|
@@ -70690,7 +71049,7 @@ ${body}
|
|
|
70690
71049
|
localCopy
|
|
70691
71050
|
};
|
|
70692
71051
|
}
|
|
70693
|
-
var fs50, path50, RECORDERS, SOLUTION_KINDS,
|
|
71052
|
+
var fs50, path50, RECORDERS, SOLUTION_KINDS, clean5, ANALYSIS_MIN_WORDS, CLIENT_FILES_ROOT, WINDOWS_RESERVED, bullet, q2, CONFIRM_INSTRUCTION, stop2, EXISTING_WITH_CONTACT, UNCLEAN_RECORD, contactLocks, same, plural2;
|
|
70694
71053
|
var init_client_calls = __esm({
|
|
70695
71054
|
"src/command-os/client-calls.ts"() {
|
|
70696
71055
|
"use strict";
|
|
@@ -70702,7 +71061,7 @@ var init_client_calls = __esm({
|
|
|
70702
71061
|
init_assessment();
|
|
70703
71062
|
RECORDERS = ["Plaud", "Zoom", "Other"];
|
|
70704
71063
|
SOLUTION_KINDS = ["GoHighLevel build", "AI tool", "custom build", "process"];
|
|
70705
|
-
|
|
71064
|
+
clean5 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
|
|
70706
71065
|
ANALYSIS_MIN_WORDS = 3;
|
|
70707
71066
|
CLIENT_FILES_ROOT = ["Command OS", "Clients"];
|
|
70708
71067
|
WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
@@ -71397,8 +71756,29 @@ function composePreview(input) {
|
|
|
71397
71756
|
else reuse.push({ key: key2, type: item.type, name: item.name, id: item.existingId });
|
|
71398
71757
|
}
|
|
71399
71758
|
if (input.board) change.push({ key: `${BOARD_KEY_PREFIX}${input.board.id}`, type: "customValues", name: input.board.name, id: input.board.id, changes: [BOARD_CHANGE] });
|
|
71759
|
+
const earlierBuild = [];
|
|
71760
|
+
const earlierSeen = /* @__PURE__ */ new Set();
|
|
71761
|
+
for (const item of input.items) {
|
|
71762
|
+
const hit = item.status === "existing" && item.existingId ? input.otherPlans?.get(item.existingId) : void 0;
|
|
71763
|
+
if (hit && !earlierSeen.has(item.existingId)) {
|
|
71764
|
+
earlierSeen.add(item.existingId);
|
|
71765
|
+
earlierBuild.push({ type: item.type, name: item.name || hit.name, runId: hit.runId });
|
|
71766
|
+
}
|
|
71767
|
+
}
|
|
71768
|
+
const unrecordedDrafts = [...reuse, ...change].filter((e) => e.type === "workflows" && input.workflowStatus?.get(e.id) === "draft").map((e) => e.name);
|
|
71400
71769
|
const core = { locationId: input.locationId, planHash: input.planHash, reuse, change, logins: [...input.logins ?? []] };
|
|
71401
|
-
return {
|
|
71770
|
+
return {
|
|
71771
|
+
v: 1,
|
|
71772
|
+
recordKey: input.recordKey,
|
|
71773
|
+
at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
71774
|
+
createCount,
|
|
71775
|
+
createByType,
|
|
71776
|
+
createNames,
|
|
71777
|
+
...core,
|
|
71778
|
+
...earlierBuild.length ? { earlierBuild } : {},
|
|
71779
|
+
...unrecordedDrafts.length ? { unrecordedDrafts } : {},
|
|
71780
|
+
fingerprint: previewFingerprint(core)
|
|
71781
|
+
};
|
|
71402
71782
|
}
|
|
71403
71783
|
function allowedWorkflowIds(allow) {
|
|
71404
71784
|
return new Set(allow.filter((k) => k.startsWith("workflows:")).map((k) => k.slice("workflows:".length)));
|
|
@@ -71418,6 +71798,7 @@ function boardAllowed(preview, confirm2) {
|
|
|
71418
71798
|
}
|
|
71419
71799
|
function confirmMatches(confirm2, preview) {
|
|
71420
71800
|
if (!previewNamesLogins(preview)) return false;
|
|
71801
|
+
if (preview.earlierBuild?.length) return false;
|
|
71421
71802
|
if (!confirm2 || confirm2.fingerprint !== preview.fingerprint || !Array.isArray(confirm2.allow)) return false;
|
|
71422
71803
|
const changeKeys = new Set(preview.change.map((e) => e.key));
|
|
71423
71804
|
return confirm2.allow.every((k) => changeKeys.has(k));
|
|
@@ -71492,6 +71873,14 @@ function listedValue(raw, id2) {
|
|
|
71492
71873
|
const value = hit && typeof hit === "object" ? Reflect.get(hit, "value") : void 0;
|
|
71493
71874
|
return typeof value === "string" ? value : void 0;
|
|
71494
71875
|
}
|
|
71876
|
+
function earlierBuildText(earlier, clientName) {
|
|
71877
|
+
const runs = [...new Set(earlier.map((e) => e.runId))];
|
|
71878
|
+
const names = earlier.map((e) => `"${e.name}"`).join(", ");
|
|
71879
|
+
return {
|
|
71880
|
+
why: `${clientName} already has ${earlier.length === 1 ? "an item" : `${earlier.length} items`} an earlier build of a different plan made: ${names}. Building now would build on top of ${earlier.length === 1 ? "it" : "them"}, which is how duplicates happen, so Build will not start. Undo the earlier build first, or stop here.`,
|
|
71881
|
+
undo: runs.map((r) => `Undo build run ${r} for ${clientName}`)
|
|
71882
|
+
};
|
|
71883
|
+
}
|
|
71495
71884
|
var import_node_fs17, import_node_path15, BUILD_PAUSED, BUILD_PREVIEW_CHANGED, keyOf4, stableChange, BOARD_VALUE_NAME, BOARD_KEY_PREFIX, BOARD_CHANGE, PREVIEW_KIND;
|
|
71496
71885
|
var init_build_confirm = __esm({
|
|
71497
71886
|
"src/build-confirm.ts"() {
|
|
@@ -71782,6 +72171,116 @@ var init_clinic_prefix_registry = __esm({
|
|
|
71782
72171
|
}
|
|
71783
72172
|
});
|
|
71784
72173
|
|
|
72174
|
+
// src/client-asks.ts
|
|
72175
|
+
function parseAsks(raw) {
|
|
72176
|
+
if (raw === void 0 || raw === null) return { ok: true, asks: {} };
|
|
72177
|
+
if (typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "The answers could not be read. Choose them again, then save." };
|
|
72178
|
+
const r = raw;
|
|
72179
|
+
const out = {};
|
|
72180
|
+
if (r.texting !== void 0) {
|
|
72181
|
+
if (r.texting !== "yes" && r.texting !== "not_yet") return { ok: false, error: "Texting is Yes or Not yet." };
|
|
72182
|
+
out.texting = r.texting;
|
|
72183
|
+
}
|
|
72184
|
+
if (r.callAnswering !== void 0) {
|
|
72185
|
+
if (!Array.isArray(r.callAnswering) || !r.callAnswering.every((c) => CALL_ANSWERING_CHOICES.includes(c))) {
|
|
72186
|
+
return { ok: false, error: "Call answering choices could not be read. Choose them again, then save." };
|
|
72187
|
+
}
|
|
72188
|
+
const chosen = [...new Set(r.callAnswering)];
|
|
72189
|
+
if (chosen.length) out.callAnswering = CALL_ANSWERING_CHOICES.filter((c) => chosen.includes(c));
|
|
72190
|
+
}
|
|
72191
|
+
if (r.reactivation !== void 0) {
|
|
72192
|
+
const x = r.reactivation;
|
|
72193
|
+
if (!x || typeof x !== "object") return { ok: false, error: "Reactivation answers could not be read. Choose them again, then save." };
|
|
72194
|
+
if (!REACTIVATION_LISTS.includes(x.list)) return { ok: false, error: "Choose who reactivation should reach." };
|
|
72195
|
+
const offer = x.offer;
|
|
72196
|
+
const text3 = typeof offer?.text === "string" ? offer.text.trim() : "";
|
|
72197
|
+
if (!offer || offer.from !== "brief" && offer.from !== "typed" || !text3) return { ok: false, error: "Choose the offer that brings them back, or type one." };
|
|
72198
|
+
if (text3.length > MAX_OFFER) return { ok: false, error: `Keep the offer under ${MAX_OFFER} characters.` };
|
|
72199
|
+
if (x.channel !== "email" && x.channel !== "email_and_text") return { ok: false, error: "Choose Email, or Email and text." };
|
|
72200
|
+
const a = { list: x.list, offer: { from: offer.from, text: text3 }, channel: x.channel };
|
|
72201
|
+
if (a.list === "in_account_not_booked") {
|
|
72202
|
+
const d = Number(x.notBookedDays);
|
|
72203
|
+
if (!Number.isInteger(d) || d < 1 || d > 3650) return { ok: false, error: "Not booked in how many days? Use a whole number from 1 to 3650." };
|
|
72204
|
+
a.notBookedDays = d;
|
|
72205
|
+
}
|
|
72206
|
+
if (x.textConsentConfirmed === true) a.textConsentConfirmed = true;
|
|
72207
|
+
out.reactivation = a;
|
|
72208
|
+
}
|
|
72209
|
+
return { ok: true, asks: out };
|
|
72210
|
+
}
|
|
72211
|
+
function effectiveTexting(asks, intakeSms) {
|
|
72212
|
+
if (asks?.texting === "yes") return "yes";
|
|
72213
|
+
if (asks?.texting === "not_yet") return "not_yet";
|
|
72214
|
+
if (intakeSms === true) return "yes";
|
|
72215
|
+
if (intakeSms === false) return "not_yet";
|
|
72216
|
+
return "unanswered";
|
|
72217
|
+
}
|
|
72218
|
+
function asksContradiction(asks, texting) {
|
|
72219
|
+
if (texting !== "yes" && asks.callAnswering?.includes("missed_text")) return "A text back after a missed call needs Texting: Yes.";
|
|
72220
|
+
const re = asks.reactivation;
|
|
72221
|
+
if (re?.channel === "email_and_text") {
|
|
72222
|
+
if (texting !== "yes") return "Email and text needs Texting: Yes.";
|
|
72223
|
+
if (!re.textConsentConfirmed) return "Email and text needs your client to confirm these people agreed to get texts.";
|
|
72224
|
+
}
|
|
72225
|
+
return null;
|
|
72226
|
+
}
|
|
72227
|
+
function heldItems(ticks, asks, texting) {
|
|
72228
|
+
const out = [];
|
|
72229
|
+
const wait = (id2, name2) => ({ id: id2, name: name2, reason: "unanswered", line: `Waiting for your client's answer: ${name2}` });
|
|
72230
|
+
const textsMatter = ticks.includes("sms") || ticks.includes("missed") || ticks.includes("reactivation");
|
|
72231
|
+
if (textsMatter && texting === "unanswered") out.push(wait("texting", "Texting"));
|
|
72232
|
+
if (ticks.includes("sms") && texting === "not_yet") out.push({ id: "texting", name: "Texting", reason: "not_yet", line: "Not built, your client said not yet: Texting" });
|
|
72233
|
+
if (ticks.includes("missed")) {
|
|
72234
|
+
const c = asks?.callAnswering ?? [];
|
|
72235
|
+
if (!c.length) out.push(wait("call_answering", "Call answering"));
|
|
72236
|
+
else {
|
|
72237
|
+
const later = c.filter((x) => x === "live_voice" || x === "chat_bot");
|
|
72238
|
+
if (later.length) out.push({ id: "call_answering", name: "Call answering", reason: "later_update", line: `Not built yet, chosen and coming in a later update: ${later.map((x) => x === "live_voice" ? "an AI that answers the phone live" : "an AI that replies by text or chat and books").join(" and ")}` });
|
|
72239
|
+
if (c.includes("missed_text") && texting !== "yes") out.push(wait("texting", "Texting (the text back after a missed call needs it)"));
|
|
72240
|
+
}
|
|
72241
|
+
}
|
|
72242
|
+
if (ticks.includes("reactivation")) {
|
|
72243
|
+
const re = asks?.reactivation;
|
|
72244
|
+
if (!re) out.push(wait("reactivation", "Reactivation (who it reaches, which offer)"));
|
|
72245
|
+
else if (re.list !== "cold_from_now") out.push({ id: "reactivation", name: "Reactivation", reason: "later_update", line: `Not built yet, chosen and coming in a later update: reactivation for ${re.list === "import" ? "a list your client will give you" : `contacts here with no booking in ${re.notBookedDays} days`}` });
|
|
72246
|
+
}
|
|
72247
|
+
return out.filter((h, i) => out.findIndex((o) => o.id === h.id && o.reason === h.reason) === i);
|
|
72248
|
+
}
|
|
72249
|
+
function featuresAllowedByAsks(features, ticks, asks) {
|
|
72250
|
+
return features.filter((f) => {
|
|
72251
|
+
if (f === "missed_call" && ticks.includes("missed")) return asks?.callAnswering?.includes("missed_text") === true;
|
|
72252
|
+
if (f === "winback" && ticks.includes("reactivation")) return asks?.reactivation?.list === "cold_from_now";
|
|
72253
|
+
return true;
|
|
72254
|
+
});
|
|
72255
|
+
}
|
|
72256
|
+
function briefWithAsks(brief, asks, ticks, presetWorkflows) {
|
|
72257
|
+
const ch = brief.channels && typeof brief.channels === "object" ? brief.channels : {};
|
|
72258
|
+
const texting = effectiveTexting(asks, typeof ch.sms === "boolean" ? ch.sms : void 0);
|
|
72259
|
+
const { sms: _was, ...rest } = ch;
|
|
72260
|
+
let out = { ...brief, channels: { ...rest, ...texting === "unanswered" ? {} : { sms: texting === "yes" } } };
|
|
72261
|
+
if (!ticks) return out;
|
|
72262
|
+
const scope = out.scope;
|
|
72263
|
+
const offered = [...new Set(presetWorkflows.map((w) => featureOfWorkflow(String(w.ref ?? ""), w.feature)).filter((f) => f !== null))];
|
|
72264
|
+
const current = scope ? Array.isArray(scope.features) ? scope.features.map(String) : [] : offered;
|
|
72265
|
+
const allowed = featuresAllowedByAsks(current, ticks, asks);
|
|
72266
|
+
if (allowed.length !== current.length) out = { ...out, scope: { ...scope?.package ? { package: scope.package } : {}, features: allowed } };
|
|
72267
|
+
return out;
|
|
72268
|
+
}
|
|
72269
|
+
function asksBindingHash(asks, ticks) {
|
|
72270
|
+
return contentHash2({ asks: asks && Object.keys(asks).length ? asks : null, ticks: ticks ? [...new Set(ticks)].sort() : null });
|
|
72271
|
+
}
|
|
72272
|
+
var CALL_ANSWERING_CHOICES, REACTIVATION_LISTS, MAX_OFFER;
|
|
72273
|
+
var init_client_asks = __esm({
|
|
72274
|
+
"src/client-asks.ts"() {
|
|
72275
|
+
"use strict";
|
|
72276
|
+
init_features();
|
|
72277
|
+
init_build_contract();
|
|
72278
|
+
CALL_ANSWERING_CHOICES = ["live_voice", "chat_bot", "missed_text"];
|
|
72279
|
+
REACTIVATION_LISTS = ["import", "in_account_not_booked", "cold_from_now"];
|
|
72280
|
+
MAX_OFFER = 300;
|
|
72281
|
+
}
|
|
72282
|
+
});
|
|
72283
|
+
|
|
71785
72284
|
// src/dashboard.ts
|
|
71786
72285
|
var dashboard_exports = {};
|
|
71787
72286
|
__export(dashboard_exports, {
|
|
@@ -71796,6 +72295,7 @@ __export(dashboard_exports, {
|
|
|
71796
72295
|
ASSESSMENT_CHANGED_SINCE_APPROVAL: () => ASSESSMENT_CHANGED_SINCE_APPROVAL,
|
|
71797
72296
|
ASSESSMENT_PIPELINE_MISSING: () => ASSESSMENT_PIPELINE_MISSING,
|
|
71798
72297
|
BRIEF_NEEDS_INTAKE: () => BRIEF_NEEDS_INTAKE2,
|
|
72298
|
+
BUILD_WHAT_WAS_WAITING: () => BUILD_WHAT_WAS_WAITING,
|
|
71799
72299
|
CHOOSE_BEFORE_BUILD: () => CHOOSE_BEFORE_BUILD,
|
|
71800
72300
|
CHOOSE_CLIENT: () => CHOOSE_CLIENT,
|
|
71801
72301
|
CHOOSE_WHAT_THEY_BOUGHT: () => CHOOSE_WHAT_THEY_BOUGHT,
|
|
@@ -71852,6 +72352,7 @@ __export(dashboard_exports, {
|
|
|
71852
72352
|
brandPagesForPro: () => brandPagesForPro,
|
|
71853
72353
|
briefReach: () => briefReach,
|
|
71854
72354
|
buildPreviewsFor: () => buildPreviewsFor,
|
|
72355
|
+
buildStatusFor: () => buildStatusFor,
|
|
71855
72356
|
callClaudeRecorded: () => callClaudeRecorded,
|
|
71856
72357
|
canonicalIndustry: () => canonicalIndustry,
|
|
71857
72358
|
capabilityJobs: () => capabilityJobs,
|
|
@@ -71892,7 +72393,9 @@ __export(dashboard_exports, {
|
|
|
71892
72393
|
forgetVerifiedHandoff: () => forgetVerifiedHandoff,
|
|
71893
72394
|
guarded: () => guarded,
|
|
71894
72395
|
hasChosenScope: () => hasChosenScope,
|
|
72396
|
+
industryChoices: () => industryChoices,
|
|
71895
72397
|
insideDir: () => insideDir,
|
|
72398
|
+
intakeAskFacts: () => intakeAskFacts,
|
|
71896
72399
|
intakeErrorDetail: () => intakeErrorDetail,
|
|
71897
72400
|
intakeInput: () => intakeInput,
|
|
71898
72401
|
intakeMissingMessage: () => intakeMissingMessage,
|
|
@@ -71916,6 +72419,7 @@ __export(dashboard_exports, {
|
|
|
71916
72419
|
memberFacingError: () => memberFacingError,
|
|
71917
72420
|
mergeQueueIntakes: () => mergeQueueIntakes,
|
|
71918
72421
|
newClientFailure: () => newClientFailure,
|
|
72422
|
+
noClientPageHtml: () => noClientPageHtml,
|
|
71919
72423
|
normalizedIntake: () => normalizedIntake,
|
|
71920
72424
|
notPreparedHtml: () => notPreparedHtml,
|
|
71921
72425
|
notSignedInMsg: () => notSignedInMsg,
|
|
@@ -71948,15 +72452,20 @@ __export(dashboard_exports, {
|
|
|
71948
72452
|
rereadQueueIntakes: () => rereadQueueIntakes,
|
|
71949
72453
|
resetCockpitStateRecovery: () => resetCockpitStateRecovery,
|
|
71950
72454
|
resolvePlan: () => resolvePlan2,
|
|
72455
|
+
reviewIsStale: () => reviewIsStale,
|
|
72456
|
+
reviewPromisesFor: () => reviewPromisesFor,
|
|
72457
|
+
reviewPromisesHash: () => reviewPromisesHash,
|
|
71951
72458
|
routeGate: () => routeGate,
|
|
71952
72459
|
runDashboard: () => runDashboard,
|
|
71953
72460
|
runForTests: () => runForTests,
|
|
71954
72461
|
runRefusal: () => runRefusal,
|
|
71955
72462
|
runStopReason: () => runStopReason,
|
|
71956
72463
|
safeDocName: () => safeDocName,
|
|
72464
|
+
savedPlanFitsAnswers: () => savedPlanFitsAnswers,
|
|
71957
72465
|
scanGate: () => scanGate,
|
|
71958
72466
|
scopeHashOf: () => scopeHashOf,
|
|
71959
72467
|
scopePrefillFor: () => scopePrefillFor,
|
|
72468
|
+
scopeWithAsks: () => scopeWithAsks,
|
|
71960
72469
|
secondClientInputHash: () => secondClientInputHash,
|
|
71961
72470
|
setAccountIndustry: () => setAccountIndustry,
|
|
71962
72471
|
setAccountType: () => setAccountType,
|
|
@@ -72147,7 +72656,7 @@ function withdrawIfStaffChanged(state2, locationId2) {
|
|
|
72147
72656
|
return { state: { ...setBuildApproval(state2, locationId2, null), planApprovals: approvals }, withdrawn: true, message: ap.staffHash === void 0 ? APPROVAL_WITHDRAWN_NO_TEAM : APPROVAL_WITHDRAWN_FOR_STAFF };
|
|
72148
72657
|
}
|
|
72149
72658
|
function withBoughtJobs(tasks2, lines) {
|
|
72150
|
-
const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text));
|
|
72659
|
+
const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text) && !HELD_JOB.test(t.text));
|
|
72151
72660
|
const prior = new Map(tasks2.map((t) => [t.id, t]));
|
|
72152
72661
|
return [...kept, ...lines.map((line4) => {
|
|
72153
72662
|
if (typeof line4 === "string") return prior.get(taskId(line4)) ?? { id: taskId(line4), text: line4 };
|
|
@@ -72247,6 +72756,71 @@ function presetWorkflowsFor(state2, recordKey) {
|
|
|
72247
72756
|
}
|
|
72248
72757
|
return null;
|
|
72249
72758
|
}
|
|
72759
|
+
function scopeWithAsks(state2, rk) {
|
|
72760
|
+
const current = state2.scope?.[rk];
|
|
72761
|
+
const ticks = state2.bought?.[rk];
|
|
72762
|
+
if (!current || !Array.isArray(ticks)) return current;
|
|
72763
|
+
const base = current.wholePreset ? offeredFeaturesFor(state2, rk) : current.features;
|
|
72764
|
+
const allowed = featuresAllowedByAsks(base, ticks, state2.asks?.[rk]);
|
|
72765
|
+
return allowed.length === base.length ? current : { features: allowed, setAt: current.setAt };
|
|
72766
|
+
}
|
|
72767
|
+
function intakeAskFacts(answers) {
|
|
72768
|
+
const get2 = (key2, label2) => {
|
|
72769
|
+
const v = answers?.[label2] ?? answers?.[key2];
|
|
72770
|
+
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
72771
|
+
};
|
|
72772
|
+
const { sms } = intakeChannelAnswers(answers);
|
|
72773
|
+
const phone = get2("has_phone_number", "Do you already have a phone number in GoHighLevel?")?.toLowerCase();
|
|
72774
|
+
const offer = get2("core_offer", "Core Offer");
|
|
72775
|
+
return { ...sms !== void 0 ? { sms } : {}, ...phone === "yes" || phone === "no" || phone === "unsure" ? { phone } : {}, ...offer ? { offer: offer.slice(0, 300) } : {} };
|
|
72776
|
+
}
|
|
72777
|
+
function buildStatusFor(st, rk, locationId2) {
|
|
72778
|
+
const ticks = st.bought?.[rk] ?? [];
|
|
72779
|
+
const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
|
|
72780
|
+
const held2 = {};
|
|
72781
|
+
const item = { texting: "sms", call_answering: "missed", reactivation: "reactivation" };
|
|
72782
|
+
for (const h of heldItems(ticks, st.asks?.[rk], effectiveTexting(st.asks?.[rk], intake.sms))) held2[item[h.id]] ??= h.line;
|
|
72783
|
+
const sp = readSavedPlan(rk);
|
|
72784
|
+
const ran = st.outcomes?.[`${rk}:3`]?.ok === true || (sp?.built?.length ?? 0) > 0;
|
|
72785
|
+
if (!ran) return { held: held2 };
|
|
72786
|
+
const featureOf = (ref) => {
|
|
72787
|
+
try {
|
|
72788
|
+
return featureOfWorkflow(ref);
|
|
72789
|
+
} catch {
|
|
72790
|
+
return null;
|
|
72791
|
+
}
|
|
72792
|
+
};
|
|
72793
|
+
const made = /* @__PURE__ */ new Set();
|
|
72794
|
+
for (const o of sp?.planId ? madeByBuild(locationId2, sp.planId) : []) if (o.type === "workflow") {
|
|
72795
|
+
const f = featureOf(o.ref);
|
|
72796
|
+
if (f) made.add(f);
|
|
72797
|
+
}
|
|
72798
|
+
for (const b of sp?.built ?? []) if (b.type === "workflow" && b.realId) {
|
|
72799
|
+
const f = featureOf(String(b.ref));
|
|
72800
|
+
if (f) made.add(f);
|
|
72801
|
+
}
|
|
72802
|
+
return { made, held: held2 };
|
|
72803
|
+
}
|
|
72804
|
+
function reviewPromisesFor(st, rk, locationId2) {
|
|
72805
|
+
const ticks = new Set(st.bought?.[rk] ?? []);
|
|
72806
|
+
const items = boughtItemsFor(offeredFeaturesFor(st, rk), boughtWordingFor(st, rk), buildStatusFor(st, rk, locationId2)).filter((i) => ticks.has(i.id));
|
|
72807
|
+
const willBuild = items.map((i) => /^(?:Will build|Built|Always built): (.+)$/.exec(i.delivery)?.[1]).filter((x) => Boolean(x));
|
|
72808
|
+
const held2 = items.map((i) => i.delivery).filter((d) => HELD_JOB.test(d));
|
|
72809
|
+
const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
|
|
72810
|
+
return { willBuild, held: held2, textingOn: effectiveTexting(st.asks?.[rk], intake.sms) === "yes" };
|
|
72811
|
+
}
|
|
72812
|
+
function reviewPromisesHash(st, rk, locationId2 = rk) {
|
|
72813
|
+
return contentHash2(reviewPromisesFor(st, rk, locationId2));
|
|
72814
|
+
}
|
|
72815
|
+
function reviewIsStale(st, rk, review) {
|
|
72816
|
+
return Boolean(review?.ok) && review.promisesHash !== reviewPromisesHash(st, rk);
|
|
72817
|
+
}
|
|
72818
|
+
function savedPlanFitsAnswers(sp, state2, rk) {
|
|
72819
|
+
if (!sp || !savedPlanMatchesScope(sp, scopeWithAsks(state2, rk))) return false;
|
|
72820
|
+
const asks = state2.asks?.[rk];
|
|
72821
|
+
if (!sp.asksHash) return !(asks && Object.keys(asks).length);
|
|
72822
|
+
return sp.asksHash === asksBindingHash(asks, state2.bought?.[rk]);
|
|
72823
|
+
}
|
|
72250
72824
|
function offeredFeaturesFor(state2, recordKey) {
|
|
72251
72825
|
const workflows = presetWorkflowsFor(state2, recordKey);
|
|
72252
72826
|
return workflows ? featuresInPreset(workflows) : FEATURE_IDS;
|
|
@@ -72456,7 +73030,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
72456
73030
|
// or before Your team decided) is made again, never built with someone the member was not shown.
|
|
72457
73031
|
savedPlan: (() => {
|
|
72458
73032
|
const sp = stage === 3 ? readSavedPlan(rk) : null;
|
|
72459
|
-
return sp && sp.inputHash === meta.inputHash &&
|
|
73033
|
+
return sp && sp.inputHash === meta.inputHash && savedPlanFitsAnswers(sp, state2, rk) && savedPlanTeamIsCurrent(sp, meta.normalizedAnswers, composed) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
|
|
72460
73034
|
})(),
|
|
72461
73035
|
textSnippets: Boolean(state2.textSnippets?.[rk])
|
|
72462
73036
|
};
|
|
@@ -72533,7 +73107,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
72533
73107
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
72534
73108
|
const shared = { at, locationId: locationId2, stage, engineVersion: outcome.engineVersion, engineModel: outcome.engineModel };
|
|
72535
73109
|
if (outcome.engineCheckRan || outcome.engineCheckCostUsd !== void 0) {
|
|
72536
|
-
recordRunCost({ ...shared, ok:
|
|
73110
|
+
recordRunCost({ ...shared, ok: outcome.engineCheckOk !== false, kind: "engine-check", costUsd: outcome.engineCheckCostUsd });
|
|
72537
73111
|
}
|
|
72538
73112
|
recordRunCost({
|
|
72539
73113
|
...shared,
|
|
@@ -72559,6 +73133,9 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
72559
73133
|
outcome.issues = [];
|
|
72560
73134
|
outcome.plain = { summary: BRIEF_NEEDS_INTAKE2, issues: [] };
|
|
72561
73135
|
}
|
|
73136
|
+
const noted = failureNoteFor(stage, outcome, run.lines, scopeRefusal || briefNeedsIntake);
|
|
73137
|
+
const failNote = noted !== null;
|
|
73138
|
+
if (noted) Object.assign(outcome, noted);
|
|
72562
73139
|
if (stage === 3 && !outcome.summary && !outcome.issues?.length) {
|
|
72563
73140
|
try {
|
|
72564
73141
|
const st = readCockpitState();
|
|
@@ -72574,8 +73151,8 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
72574
73151
|
const st = readCockpitState();
|
|
72575
73152
|
writeCockpitState({ ...st, outcomes: { ...st.outcomes ?? {}, [`${rk}:${stage}`]: { ok: outcome.ok, error: outcome.error, summary: outcome.summary, issues: outcome.issues, ...plain && { plain }, ...briefNeedsIntake && { reason: "intake-needed" }, at, inputHash: runInputHash, ...stage === 4 ? { buildHash: runBuildHash, verification: verifyRaw?.verification } : {}, hash: contentHash2({ inputHash: runInputHash, at, ok: outcome.ok, summary: outcome.summary, issues: outcome.issues }) } } });
|
|
72576
73153
|
};
|
|
72577
|
-
save(scopeRefusal || briefNeedsIntake ? outcome.plain : void 0);
|
|
72578
|
-
if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal && !briefNeedsIntake) {
|
|
73154
|
+
save(scopeRefusal || briefNeedsIntake || failNote ? outcome.plain : void 0);
|
|
73155
|
+
if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal && !briefNeedsIntake && !failNote) {
|
|
72579
73156
|
run.lines.push("Writing the plain-English version of what came back\u2026");
|
|
72580
73157
|
const rewritten = await rewriteOutcomePlain(STAGES2[stage], verifyRaw ?? outcome, { onEvent: (l) => run.lines.push(l), onCost: (costUsd) => {
|
|
72581
73158
|
recordRunCost({ at, locationId: locationId2, stage, kind: "plain rewrite", ok: true, costUsd });
|
|
@@ -72729,6 +73306,10 @@ async function cockpitAlreadyRunning(port) {
|
|
|
72729
73306
|
return false;
|
|
72730
73307
|
}
|
|
72731
73308
|
}
|
|
73309
|
+
function industryChoices() {
|
|
73310
|
+
const packs = INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label }));
|
|
73311
|
+
return packs.some((p) => p.slug === "other") ? packs : [...packs, { slug: "other", label: "Other" }];
|
|
73312
|
+
}
|
|
72732
73313
|
function renderCockpitHtml(roster, seatToken = "", screen = "board", cap2) {
|
|
72733
73314
|
return html(roster, seatToken, screen, cap2);
|
|
72734
73315
|
}
|
|
@@ -73090,8 +73671,8 @@ function intakeRequestError(e) {
|
|
|
73090
73671
|
function intakeErrorDetail(e) {
|
|
73091
73672
|
const raw = e instanceof Error ? e.message : typeof e === "string" ? e : "";
|
|
73092
73673
|
if (!raw || INTAKE_OWN_MESSAGES.includes(raw)) return void 0;
|
|
73093
|
-
const
|
|
73094
|
-
return
|
|
73674
|
+
const clean6 = raw.replace(/(?:[A-Za-z]:)?(?:[\\/][^\s"'\\/]+){2,}[\\/]?/g, "[path]").replace(/\b(?:pit|sk|key|token|bearer)[-_ ][A-Za-z0-9._-]{6,}/gi, "[hidden]").replace(/\b[A-Za-z0-9_-]{24,}\b/g, "[hidden]").replace(/\s+/g, " ").trim();
|
|
73675
|
+
return clean6 ? clean6.length > 240 ? clean6.slice(0, 237) + "\u2026" : clean6 : void 0;
|
|
73095
73676
|
}
|
|
73096
73677
|
function intakeRefusal(e) {
|
|
73097
73678
|
const detail = intakeErrorDetail(e);
|
|
@@ -73302,6 +73883,16 @@ ${cockpitHeaderHtml("for", { trail: ["For this client", what] })}
|
|
|
73302
73883
|
${cockpitShellClose()}
|
|
73303
73884
|
<script>${COCKPIT_TYPE_JS}</script></body></html>`;
|
|
73304
73885
|
}
|
|
73886
|
+
function noClientPageHtml(what, why) {
|
|
73887
|
+
const title2 = why === "no-client" ? `Choose a client to see ${what.toLowerCase().replace(/^the /, "their ")}` : "No Blueprint Review yet";
|
|
73888
|
+
const sub = why === "no-client" ? `${what} belongs to one client. Open it from that client's page.` : "Write the Brief for this client first; the Blueprint Review is written with it.";
|
|
73889
|
+
return `${cockpitHeadHtml(`${title2} \xB7 ${productName()}`, { extraCss: "#main{display:grid;gap:var(--space-6);align-content:start;min-width:0}" })}<body data-screen="for">
|
|
73890
|
+
${cockpitHeaderHtml("for", { trail: ["For this client", what] })}
|
|
73891
|
+
<main id="main" class="no-client"><div class="cds-page-head"><div class="txt"><div class="cds-kicker">For this client</div><h1 class="cds-page-title">${title2}</h1><p class="cds-page-sub">${sub}</p></div></div>
|
|
73892
|
+
<div class="cds-empty"><p><a class="cds-btn" href="/">Back to the start</a></p></div></main>
|
|
73893
|
+
${cockpitShellClose()}
|
|
73894
|
+
<script>${COCKPIT_TYPE_JS}</script></body></html>`;
|
|
73895
|
+
}
|
|
73305
73896
|
function unknownCapabilityHtml() {
|
|
73306
73897
|
return `${cockpitHeadHtml("No such page \xB7 Command OS", { extraCss: "#main{display:grid;gap:var(--space-6);align-content:start;min-width:0}" })}<body data-screen="for">
|
|
73307
73898
|
${cockpitHeaderHtml("for", { trail: ["For this client", "Page not found"] })}
|
|
@@ -73527,7 +74118,7 @@ function followRefresh(id,n){if(n>20)return;const tok=clientToken();setTimeout(a
|
|
|
73527
74118
|
async function refreshSnapshots(){const tok=clientToken();const sr=await(await fetch("/api/snapshots")).json();if(!sr||!sr.ok)return;if(sr.cache)ageViews.snapshots=sr.cache;
|
|
73528
74119
|
if(!stillFor(tok))return;
|
|
73529
74120
|
if(JSON.stringify(sr.snapshots)!==JSON.stringify(snapshots)){snapshots=sr.snapshots;if(!userIsBusy()&&!unsavedTyping()&&lastState)renderFor(tok);else redrawPending=true}else paintAges()}
|
|
73530
|
-
const INDUSTRIES=${scriptJson(
|
|
74121
|
+
const INDUSTRIES=${scriptJson(industryChoices())};
|
|
73531
74122
|
const INDUSTRIES2=${scriptJson(INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })))};
|
|
73532
74123
|
/* One account at a time (Jerry, 2026-08-26: "way too busy\u2026 tempting to work on multiple
|
|
73533
74124
|
accounts at once"). One card is ever expanded; every other account is a one-line row. */
|
|
@@ -74203,6 +74794,60 @@ async function profLoad(){if(profLoading)return;profLoading=true;try{profCache=(
|
|
|
74203
74794
|
waits for the Assessment question above the list. */
|
|
74204
74795
|
/* The ticks and package chosen but not yet saved, kept outside the block so a redraw keeps them (review of c09e946). */
|
|
74205
74796
|
let scopeDraft={},boughtNotice={};
|
|
74797
|
+
/* Ask first (Jerry 2026-10-29): texting, call answering and reactivation are asked of the client before Build. The
|
|
74798
|
+
answers save with "Save what they bought" (server: parseAsks, asksContradiction). Nothing is pre-selected: the
|
|
74799
|
+
client's own form answer shows with a Change link; a phone number is only a hint. Blank = held, never built. */
|
|
74800
|
+
let asksDraft={};
|
|
74801
|
+
function asksClean(a){const o={};if(a.texting)o.texting=a.texting;if(a.callAnswering&&a.callAnswering.length)o.callAnswering=[...a.callAnswering];
|
|
74802
|
+
/* W44: typed text counts whenever no other offer was chosen, whatever was clicked first. */
|
|
74803
|
+
const r=a.reactivation;if(r&&r.list){const off=r.offer&&r.offer.text?r.offer:(r._typed?{from:"typed",text:r._typed}:{});const x={list:r.list,offer:off,channel:r.channel};if(r.list==="in_account_not_booked")x.notBookedDays=Number(r.notBookedDays);if(r.textConsentConfirmed)x.textConsentConfirmed=true;o.reactivation=x}return o}
|
|
74804
|
+
function asksBox(id,v,getTicked,keep){
|
|
74805
|
+
const box=mk("div","cds-stack ask-first");box.id="asks-"+id;const ia=v.intakeAsks||{};const savedJson=JSON.stringify(v.asks||{});
|
|
74806
|
+
if(!asksDraft[id]||asksDraft[id].base!==savedJson)asksDraft[id]={base:savedJson,a:JSON.parse(savedJson),change:false};
|
|
74807
|
+
const d=asksDraft[id],a=d.a;
|
|
74808
|
+
const radio=(name,val,lab,on,dis,why,act)=>{const l=mk("label","cds-check");const r=document.createElement("input");r.type="radio";r.name=name;r.checked=on;r.disabled=!!dis;r.onchange=act;l.appendChild(r);const s=mk("span","");s.appendChild(mk("b","",lab));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);return l};
|
|
74809
|
+
function draw(){box.innerHTML="";const t=getTicked();
|
|
74810
|
+
if(!(t.has("sms")||t.has("missed")||t.has("reactivation"))){box.hidden=true;return}box.hidden=false;
|
|
74811
|
+
box.appendChild(mk("h4","cds-panel-label","Confirm with your client before Build"));
|
|
74812
|
+
box.appendChild(mk("p","","Ask your client these, then choose. Nothing is built for a question left blank; it waits for your client's answer."));
|
|
74813
|
+
box.appendChild(mk("p","muted","The answers save with Save what they bought. Changing one after the plan is approved means approving it again."));
|
|
74814
|
+
const texting=a.texting||(ia.sms===true?"yes":ia.sms===false?"not_yet":"");
|
|
74815
|
+
const tx=mk("div","cds-stack");tx.appendChild(mk("b","","Texting"));tx.appendChild(mk("p","","Can this business text leads?"));
|
|
74816
|
+
if(!a.texting&&ia.sms!==undefined&&!d.change){const p=mk("p","","Your client's form said: "+(ia.sms?"Yes":"Not yet")+" ");const ch=mk("button","cds-link","Change");ch.type="button";ch.onclick=()=>{d.change=true;draw()};p.appendChild(ch);tx.appendChild(p)}
|
|
74817
|
+
else{const row=mk("div","cds-row");row.appendChild(radio("tx-"+id,"yes","Yes",a.texting==="yes",false,"",()=>{a.texting="yes";keep();draw()}));row.appendChild(radio("tx-"+id,"not_yet","Not yet",a.texting==="not_yet",false,"",()=>{a.texting="not_yet";keep();draw()}));tx.appendChild(row)}
|
|
74818
|
+
if(ia.phone==="yes")tx.appendChild(mk("small","muted","Hint: your client's form said they have a phone number in GoHighLevel."));
|
|
74819
|
+
box.appendChild(tx);
|
|
74820
|
+
if(t.has("missed")){const ca=mk("div","cds-stack");ca.appendChild(mk("b","","Call answering"));ca.appendChild(mk("p","","What did your client pay for? Choose all that apply."));
|
|
74821
|
+
const cur=new Set(a.callAnswering||[]);
|
|
74822
|
+
const opt=(val,lab,sub,why,greyed)=>{const l=mk("label","cds-check");const c=document.createElement("input");c.type="checkbox";c.checked=cur.has(val);c.disabled=greyed&&!cur.has(val);c.onchange=()=>{if(c.checked)cur.add(val);else cur.delete(val);a.callAnswering=[...cur];keep();draw()};l.appendChild(c);const s=mk("span","");s.appendChild(mk("b","",lab));if(sub)s.appendChild(mk("small","",sub));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);ca.appendChild(l)};
|
|
74823
|
+
opt("live_voice","An AI answers the phone live","Your client connects their number to it afterwards. Once it is connected, GoHighLevel charges for every minute it talks, usually from your agency's wallet.","",false);
|
|
74824
|
+
opt("chat_bot","An AI replies by text or chat and books the visit","",texting==="yes"?"":"Texting replies need Texting: Yes.",false);
|
|
74825
|
+
opt("missed_text","A text back after a missed call","",texting==="yes"?"":"Needs texting: Yes",texting!=="yes");
|
|
74826
|
+
box.appendChild(ca)}
|
|
74827
|
+
if(t.has("reactivation")){const re=a.reactivation||(a.reactivation={});const rv=mk("div","cds-stack");rv.appendChild(mk("b","","Reactivation"));
|
|
74828
|
+
rv.appendChild(mk("p","","Who should it reach?"));
|
|
74829
|
+
rv.appendChild(radio("rl-"+id,"import","A list your client will give you to import",re.list==="import",false,"",()=>{re.list="import";keep();draw()}));
|
|
74830
|
+
/* rc.3 c6 hole 4: the number is in the sentence as text, and its box sits on its own labelled line. */
|
|
74831
|
+
const nDays=re.notBookedDays||90;
|
|
74832
|
+
rv.appendChild(radio("rl-"+id,"in_account_not_booked","Contacts already here who have not booked in "+nDays+" days",re.list==="in_account_not_booked",false,"",()=>{re.list="in_account_not_booked";if(!re.notBookedDays)re.notBookedDays=90;keep();draw()}));
|
|
74833
|
+
if(re.list==="in_account_not_booked"){const row=mk("label","cds-row");row.appendChild(mk("span","","Days with no booking: "));const days=document.createElement("input");days.type="number";days.min="1";days.max="3650";days.className="cds-input";days.style.width="6em";days.value=String(nDays);days.onchange=()=>{const n=Math.round(Number(days.value));if(n>=1&&n<=3650){re.notBookedDays=n;keep();draw()}else days.value=String(nDays)};row.appendChild(days);rv.appendChild(row);
|
|
74834
|
+
rv.appendChild(mk("small","muted","You will see how many people this reaches and approve it before anyone gets a message."))}
|
|
74835
|
+
rv.appendChild(radio("rl-"+id,"cold_from_now","Only leads who go cold from now on",re.list==="cold_from_now",false,"",()=>{re.list="cold_from_now";keep();draw()}));
|
|
74836
|
+
rv.appendChild(mk("p","","What offer brings them back?"));
|
|
74837
|
+
/* W44: with no offer in the brief, typing one is the only choice, so it starts chosen. */
|
|
74838
|
+
if(!ia.offer&&!re.offer)re.offer={from:"typed",text:""};
|
|
74839
|
+
if(ia.offer)rv.appendChild(radio("ro-"+id,"brief","From the brief: \\u201c"+ia.offer+"\\u201d",re.offer&&re.offer.from==="brief",false,"",()=>{re.offer={from:"brief",text:ia.offer};keep();draw()}));
|
|
74840
|
+
const typed=radio("ro-"+id,"typed","Type one:",re.offer&&re.offer.from==="typed",false,"",()=>{re.offer={from:"typed",text:ot.value.trim()};keep();draw()});
|
|
74841
|
+
/* W45 (Jerry, Safari): inside the label the box drew as a tiny oval. It is its own full-width field under its choice. */
|
|
74842
|
+
const ot=document.createElement("textarea");ot.rows=2;ot.maxLength=300;ot.className="cds-input ask-offer";ot.style.width="100%";ot.style.boxSizing="border-box";ot.placeholder="e.g. a free skin check";ot.value=re.offer&&re.offer.from==="typed"?re.offer.text:"";ot.setAttribute("aria-label","The offer, in your client's words");ot.oninput=()=>{re._typed=ot.value.trim();re.offer={from:"typed",text:re._typed};keep();typed.querySelector("input").checked=true};
|
|
74843
|
+
rv.appendChild(typed);rv.appendChild(ot);
|
|
74844
|
+
rv.appendChild(mk("p","","How?"));
|
|
74845
|
+
const canText=texting==="yes"&&re.textConsentConfirmed===true;
|
|
74846
|
+
rv.appendChild(radio("rc-"+id,"email","Email",re.channel==="email",false,"",()=>{re.channel="email";keep();draw()}));
|
|
74847
|
+
rv.appendChild(radio("rc-"+id,"email_and_text","Email and text",re.channel==="email_and_text",!canText&&re.channel!=="email_and_text",texting!=="yes"?"Needs texting: Yes":re.textConsentConfirmed?"":"Needs your client to confirm these people agreed to get texts",()=>{re.channel="email_and_text";keep();draw()}));
|
|
74848
|
+
const cl=mk("label","cds-check");const cc=document.createElement("input");cc.type="checkbox";cc.checked=re.textConsentConfirmed===true;cc.onchange=()=>{if(cc.checked)re.textConsentConfirmed=true;else{delete re.textConsentConfirmed;if(re.channel==="email_and_text")re.channel="email"}keep();draw()};cl.appendChild(cc);cl.appendChild(mk("span","","My client confirmed these people agreed to get texts"));rv.appendChild(cl);
|
|
74849
|
+
box.appendChild(rv)}}
|
|
74850
|
+
draw();box.redraw=draw;return box}
|
|
74206
74851
|
const BOUGHT_ITEMS=${scriptJson(boughtItemsFor(FEATURE_IDS))};
|
|
74207
74852
|
function scopeChosen(state,id){const s=(state.scope||{})[id];const v=(state.boughtViews||{})[id];return Boolean(s&&(s.wholePreset||(Array.isArray(s.features)&&s.features.length)))&&!(v&&v.ready===false)}
|
|
74208
74853
|
function openWhatTheyBought(id){rOpen[id]=true;rSeen[id]=true;const go=()=>{render(lastState,lastRun);requestAnimationFrame(()=>{const t=document.getElementById("wtb-"+id);if(t){t.scrollIntoView({behavior:"smooth",block:"start"});const f=t.querySelector("button,input,select");if(f)f.focus({preventScroll:true})}})};if(!rData[id])rLoad(id).then(go,go);else go()}
|
|
@@ -74225,7 +74870,7 @@ function scopeBlock(id,state){
|
|
|
74225
74870
|
const v=boughtView(state,id);
|
|
74226
74871
|
if(!bsHideAssess)sc.appendChild(assessmentQuestion(id,v));
|
|
74227
74872
|
sc.appendChild(mk("h4","cds-panel-label","What they bought"));
|
|
74228
|
-
sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked
|
|
74873
|
+
sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked Will build get built."));
|
|
74229
74874
|
if(!profCache){profLoad();const p=document.createElement("p");p.className="muted";p.textContent="Loading your packages\u2026";sc.appendChild(p);return sc}
|
|
74230
74875
|
const saved=(state.scope||{})[id]||null;const pk=(profCache.packages||[]);
|
|
74231
74876
|
const sel=document.createElement("select");sel.className="cds-select";sel.setAttribute("aria-label","Package they bought");
|
|
@@ -74236,17 +74881,17 @@ function scopeBlock(id,state){
|
|
|
74236
74881
|
const keepScope=()=>{scopeDraft[id]={ticked:[...ticked],pkg:sel.value,base:base};unsaved.hidden=false};
|
|
74237
74882
|
const everything=document.createElement("button");everything.className="cds-btn ghost";everything.textContent="Tick everything";
|
|
74238
74883
|
everything.title="Ticks all fourteen. Nothing is saved until you press Save what they bought.";
|
|
74239
|
-
everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs()};
|
|
74884
|
+
everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};
|
|
74240
74885
|
sc.appendChild(everything);
|
|
74241
74886
|
const boxes=document.createElement("div");boxes.className="cds-stack";
|
|
74242
74887
|
function drawBoxes(){boxes.innerHTML="";for(const it of v.items){const l=document.createElement("label");l.className="cds-check";
|
|
74243
|
-
l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs()};l.appendChild(c);
|
|
74888
|
+
l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs();if(askEl)askEl.redraw()};l.appendChild(c);
|
|
74244
74889
|
const t=document.createElement("span");const tb=document.createElement("b");tb.textContent=it.name;t.appendChild(tb);t.appendChild(mk("small","",it.means));
|
|
74245
|
-
t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next)t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
|
|
74890
|
+
t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next&&!(((asksDraft[id]&&asksDraft[id].a.callAnswering)||(v.asks||{}).callAnswering||[]).includes("live_voice")))t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
|
|
74246
74891
|
const figs=((state.assessmentFigures||{})[id])||{};
|
|
74247
74892
|
function drawFigs(){for(const l of boxes.querySelectorAll("label")){const it=v.items.find(i=>i.id===l.dataset.k);const old=l.querySelector("em");if(old)old.remove();if(it&&it.feature&&figs[it.feature]){const e=document.createElement("em");e.className="cds-chip"+(ticked.has(it.id)?"":" warn");e.textContent="Assessment: "+figs[it.feature]+(ticked.has(it.id)?".":". Not ticked.");l.querySelector("span").appendChild(e)}}}
|
|
74248
74893
|
/* A package names build features: it ticks their items and unticks the other built ones; the rest stay as they are. */
|
|
74249
|
-
sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs()};drawBoxes();drawFigs();
|
|
74894
|
+
sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};let askEl=null;drawBoxes();drawFigs();askEl=asksBox(id,v,()=>ticked,keepScope);
|
|
74250
74895
|
const note=document.createElement("p");note.className="muted";note.textContent="Always built, whatever is ticked: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing. A ticked item that is not built automatically becomes one of your jobs in the account.";
|
|
74251
74896
|
const sv=document.createElement("button");sv.className="cds-btn ghost";sv.textContent="Save what they bought";
|
|
74252
74897
|
const status=document.createElement("p");status.className="muted";status.setAttribute("role","status");
|
|
@@ -74254,14 +74899,17 @@ function scopeBlock(id,state){
|
|
|
74254
74899
|
if(!v.items.some(it=>it.kind==="built"&&ticked.has(it.id))){status.textContent="None of the ticked items is built for this client's industry. Tick at least one item marked Built.";return}
|
|
74255
74900
|
/* Run 8 finding 9: about 5 seconds with nothing showing it was working. */
|
|
74256
74901
|
sv.disabled=true;const was=sv.textContent;sv.textContent="Saving\u2026";status.textContent="Saving what they bought\u2026";
|
|
74257
|
-
const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;const r=await postJSON("/api/scope",body);
|
|
74258
|
-
|
|
74902
|
+
const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;if(asksDraft[id])body.asks=asksClean(asksDraft[id].a);const r=await postJSON("/api/scope",body);
|
|
74903
|
+
/* OS audit F11: the Review panel kept the copy it read before this save, so it never said the Review was now out of
|
|
74904
|
+
date while the client document refused it. Read it again after every save. */
|
|
74905
|
+
if(r){unsaved.hidden=true;delete asksDraft[id];if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];status.textContent="Saved.";if(rData[id])rLoad(id);load()}
|
|
74259
74906
|
if(sv.isConnected){sv.disabled=false;sv.textContent=was}};
|
|
74260
74907
|
/* Audit 2026-10-28 #9: the always-built pipeline counts as built ("2 bought, 1 built" while it is always built). */
|
|
74261
|
-
|
|
74262
|
-
|
|
74908
|
+
/* rc.3 c6 hole 2: the count follows the labels: what will be built (or was) and what waits on your client's answer. */
|
|
74909
|
+
const tickedItems=v.items.filter(it=>v.ticks.includes(it.id));const nBought=v.ticks.length,nBuilt=tickedItems.filter(it=>/^(Will build|Built|Always built): /.test(it.delivery)).length,nHeld=tickedItems.filter(it=>/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /.test(it.delivery)).length;
|
|
74910
|
+
status.textContent=saved&&v.source==="saved"?"Saved: "+nBought+" of "+v.items.length+" bought. "+nBuilt+(nBuilt===1?" is":" are")+" built automatically"+(nHeld?", "+nHeld+(nHeld===1?" waits":" wait")+" for your client's answer":"")+(saved.package?(pk.find(x=>x.id===saved.package)?", from the "+pk.find(x=>x.id===saved.package).name+" package":", from a package no longer on Your agency"):"")+". The build follows this.":saved?(saved.wholePreset?"Saved: everything in the plan. The build follows this.":"Saved before this list: "+saved.features.length+" features. Save again to record what they bought."):"Not saved yet. Save what they bought before approving.";
|
|
74263
74911
|
const unsaved=mk("span","cds-chip warn","Not saved yet");unsaved.hidden=!scopeDraft[id];
|
|
74264
|
-
sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
|
|
74912
|
+
sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(askEl);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
|
|
74265
74913
|
if(boughtNotice[id]){const n=mk("p","cds-callout warn",boughtNotice[id]);n.setAttribute("role","alert");sc.appendChild(n)}
|
|
74266
74914
|
return sc}
|
|
74267
74915
|
async function rLoad(id){const tok=clientToken();const r=await(await fetch("/api/review?locationId="+id)).json();rData[id]=r;renderFor(tok)}
|
|
@@ -74290,12 +74938,20 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
74290
74938
|
rBusy[id]=false;load()};
|
|
74291
74939
|
if(busy){const p=document.createElement("p");p.className="muted";
|
|
74292
74940
|
p.textContent="Reading their answers and writing the review, usually 1 to 2 minutes. This panel updates itself when it is ready.";d.appendChild(p)}
|
|
74941
|
+
/* rc.3 c6 hole 1: a review written before the answers changed may promise what the build will not make. */
|
|
74942
|
+
/* OS audit F11 (ruling (c)): one plain line, and the two choices on the row below: Approve anyway, or Update the review. */
|
|
74943
|
+
if(r&&r.ok&&r.stale&&!busy){const w=mk("p","cds-callout warn","This review was written before your last change. Approve anyway, or update the review first; the client document waits for an updated one.");w.setAttribute("role","alert");d.appendChild(w)}
|
|
74944
|
+
if(r&&r.ok&&!r.stale&&r.graded!==true&&!busy){const w=mk("p","cds-callout warn","Not checked for over-promises. Regenerate.");w.setAttribute("role","alert");d.appendChild(w)}
|
|
74945
|
+
/* OS audit F12: the trimmed lines were one run-on sentence ("...regular schedule today?.") read first. Now a folded
|
|
74946
|
+
note: how many lines, why, and each line as it was, with nothing added to its punctuation. */
|
|
74947
|
+
if(r&&r.ok&&Array.isArray(r.trimmed)&&r.trimmed.length&&!busy){const n=r.trimmed.length;const det=mk("details","cds-details review-trimmed");det.appendChild(mk("summary","",n+(n===1?" line was":" lines were")+" taken out of this review because "+(n===1?"it":"they")+" promised more than this build makes"));const ul=mk("ul","cds-list");for(const t of r.trimmed){ul.appendChild(mk("li","",String(t)))}det.appendChild(ul);d.appendChild(det)}
|
|
74293
74948
|
if(!r||!r.ok){if(!busy){const p=document.createElement("p");if(r&&r.error&&r.error!=="not generated yet"){p.className="cds-callout warn";p.setAttribute("role","alert")}else p.className="muted";
|
|
74294
74949
|
p.textContent=r&&r.error?r.error:"Turn their intake answers into strategy: who they really sell to, whether the offer holds up, what the account is missing, and what to build first.";
|
|
74295
74950
|
d.appendChild(p)}
|
|
74296
74951
|
d.appendChild(gen);
|
|
74297
|
-
/* What they bought decides the build, so it is here even before the review is written (2026-09-29).
|
|
74298
|
-
|
|
74952
|
+
/* What they bought decides the build, so it is here even before the review is written (2026-09-29). On the steps
|
|
74953
|
+
page it is its own step 6, before the Brief (OS audit F11, ruling (c)), so it is not shown twice. */
|
|
74954
|
+
if(!bsHideAssess)d.appendChild(scopeBlock(id,state));
|
|
74299
74955
|
/* On the steps page Approve never waits for the review: the server does not need it (round-4 review). */
|
|
74300
74956
|
if(bsHideAssess&&scopeChosen(state,id)&&!(state.buildApproved||{})[id]&&!PROTECTED[id])d.appendChild(approveButton(id,name,state,"review","cds-btn"));
|
|
74301
74957
|
return d}
|
|
@@ -74316,18 +74972,19 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
74316
74972
|
if((r.gaps||[]).length){const s3=document.createElement("div");s3.className="cds-stack";const gh=document.createElement("h4");gh.className="cds-panel-label";gh.textContent="What's missing ("+r.gaps.length+")";s3.appendChild(gh);
|
|
74317
74973
|
const gl=mk("ul","cds-list gaps");
|
|
74318
74974
|
for(const g of r.gaps){const gg=mk("li","gap "+(g.priority||"soon"));
|
|
74319
|
-
const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(
|
|
74975
|
+
const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(String(g.cost||""))));/* rc.5: the fix is never the model's words; what we build is listed below, from the build */gg.appendChild(gt);
|
|
74320
74976
|
const ge=mk("span","end");ge.appendChild(chip(pri==="now"?"warn":"",pri==="now"?"Fix now":pri==="later"?"Later":"Soon"));gg.appendChild(ge);gl.appendChild(gg)}
|
|
74321
74977
|
s3.appendChild(gl);
|
|
74322
74978
|
d.appendChild(s3)}
|
|
74323
|
-
|
|
74979
|
+
/* rc.5 (c6 ruling): What we build is the build's own will-build list and held lines, never the model's prose. */
|
|
74980
|
+
{const pr=r.promises;if(pr&&((pr.willBuild||[]).length||(pr.held||[]).length)){const s4=document.createElement("div");s4.className="cds-stack";s4.innerHTML="<h4 class='cds-panel-label'>What we build</h4>";
|
|
74324
74981
|
const bl=document.createElement("ul");bl.className="cds-list";
|
|
74325
|
-
for(const
|
|
74326
|
-
|
|
74327
|
-
s4.appendChild(bl);d.appendChild(s4)}
|
|
74982
|
+
for(const w of (pr.willBuild||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl",String(w)));bl.appendChild(bb)}
|
|
74983
|
+
for(const h of (pr.held||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl muted",String(h)));bl.appendChild(bb)}
|
|
74984
|
+
s4.appendChild(bl);d.appendChild(s4)}}
|
|
74328
74985
|
if((r.unknowns||[]).length){const s5=document.createElement("div");s5.className="cds-stack";s5.innerHTML="<h4 class='cds-panel-label'>Ask them next</h4>";
|
|
74329
74986
|
const p=document.createElement("p");p.textContent=r.unknowns.join(" \xB7 ");s5.appendChild(p);d.appendChild(s5)}
|
|
74330
|
-
d.appendChild(scopeBlock(id,state));
|
|
74987
|
+
if(!bsHideAssess)d.appendChild(scopeBlock(id,state));
|
|
74331
74988
|
d.appendChild(mk("h4","cds-panel-label","The client document: your agency name and color on it"));const brand=document.createElement("div");brand.className="cds-row brand-row";
|
|
74332
74989
|
const an=document.createElement("input");an.placeholder="Your agency name (on the client doc)";an.value=(state.agencyName||"");
|
|
74333
74990
|
an.className="cds-input";an.setAttribute("aria-label","Your agency name on the client document");
|
|
@@ -74349,6 +75006,7 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
74349
75006
|
if((state.buildApproved||{})[id]&&intakeStale){const ap=document.createElement("button");ap.className="cds-btn ghost";ap.textContent="Approve again once the intake is reviewed";ap.disabled=true;row.appendChild(ap)}
|
|
74350
75007
|
else if((state.buildApproved||{})[id]&&!approvalStale){const ap=document.createElement("button");ap.className="cds-btn ghost";ap.textContent="Approved";ap.disabled=true;row.appendChild(ap)}
|
|
74351
75008
|
else if(scopeChosen(state,id)&&!["template","hidden"].includes(typeOf(state,id))&&!PROTECTED[id])row.appendChild(approveButton(id,name,state,"review","cds-btn"));
|
|
75009
|
+
if(r.stale&&!busy){gen.textContent="Update the review (uses Claude)";const ab=row.querySelector("button.cds-btn:not(.ghost)");if(ab&&/^Approve this plan$/.test(ab.textContent))ab.textContent="Approve anyway"}
|
|
74352
75010
|
row.appendChild(gen);d.appendChild(row);
|
|
74353
75011
|
return d}
|
|
74354
75012
|
const NC_AGENCY=${scriptJson(readAgencyProfile().agencyName?.trim() || "in GoHighLevel")};
|
|
@@ -75132,7 +75790,7 @@ function autoBuildClaim(id,b){if(!autoBuild[id])return;queueMicrotask(()=>{const
|
|
|
75132
75790
|
function approveSpec(id,name,state){const sandbox=typeOf(state,id)==="sandbox";
|
|
75133
75791
|
const view=(state.buildViews||{})[id];const repairs=(view&&view.planHash&&view.repairs)||[];
|
|
75134
75792
|
return {title:"Approve this plan for "+name,
|
|
75135
|
-
text:["Command OS then builds in this account what the Blueprint Review lists"+(repairs.length?", and makes the "+repairs.length+" live workflow "+plural(repairs.length,"repair")+" listed with the plan":"")+".","When you approve, step
|
|
75793
|
+
text:["Command OS then builds in this account what the Blueprint Review lists"+(repairs.length?", and makes the "+repairs.length+" live workflow "+plural(repairs.length,"repair")+" listed with the plan":"")+".","When you approve, step 10 opens. Click Start the build there."],
|
|
75136
75794
|
after:async()=>{load()},
|
|
75137
75795
|
fields:[NAME_FIELD()].concat(sandbox?[]:[{id:"client",label:"Type the client name to confirm: "+name,value:""}]),
|
|
75138
75796
|
ok:"Approve",busyLabel:"Approving\u2026",
|
|
@@ -75407,7 +76065,9 @@ function renderCapability(m,state,run){
|
|
|
75407
76065
|
if(CAP.key==="intake"){const go=mk("a","cds-btn cap-build-account","Build this client's account");end.appendChild(asClientLink(go,"/new",c.locationId))}
|
|
75408
76066
|
m.appendChild(screenHead(CAP.label,CAP.what,CAP.groupLabel,end));
|
|
75409
76067
|
const sc=document.getElementById("side-client");if(sc)sc.textContent=c.name;
|
|
75410
|
-
|
|
76068
|
+
/* W43 (Jerry's watched walk, 2026-10-29): he saw no Build button on the page Your clients leads to. The header carries
|
|
76069
|
+
one; this puts the next step at the top of the page itself, where nobody can miss it. */
|
|
76070
|
+
if(CAP.key==="intake"){const nx=mk("div","cds-callout cap-next-build");nx.appendChild(mk("span","ttl","Next step: build this client's account"));nx.appendChild(mk("span","","Answers, approval and the build itself are on the build page."));const go=mk("a","cds-btn cap-build-account-main","Build this client's account");nx.appendChild(asClientLink(go,"/new",c.locationId));m.appendChild(nx);renderIntakeCapability(m,c);return}
|
|
75411
76071
|
const built=capBuiltItems(view,CAP.key);
|
|
75412
76072
|
const toCheck=built.filter(x=>x.status==="uncertain");
|
|
75413
76073
|
const health=HEALTH_PAGES.includes(CAP.key);
|
|
@@ -75434,7 +76094,8 @@ function renderCapability(m,state,run){
|
|
|
75434
76094
|
if(CAP.key==="checkup"){const v=capCheckupPanel(last,bb);capReadsInto(v,c,hasBuild,true);m.appendChild(v)}
|
|
75435
76095
|
else{
|
|
75436
76096
|
/* In this account now, read live from GoHighLevel. */
|
|
75437
|
-
|
|
76097
|
+
/* OS audit F16: a page that reads nothing said "Read live from GoHighLevel" above "This page does not read the account yet". */
|
|
76098
|
+
const card=CAP.reads.length||CAP.key==="handoff"?panel("Read live from GoHighLevel","In this account now"):panel("Not read on this page","Ask Claude below");card.classList.add("cap-reads");
|
|
75438
76099
|
capReadsInto(card,c,hasBuild,false);m.appendChild(card)}
|
|
75439
76100
|
m.appendChild(capDoPanel(c,state,run,words,hasBuild,busy,last));
|
|
75440
76101
|
/* From the last build: only this page's own items. */
|
|
@@ -75448,7 +76109,25 @@ function renderCapability(m,state,run){
|
|
|
75448
76109
|
if(health)m.appendChild(capOwedPanel(owed,(state.jobCounts||{})[c.locationId],((view||{}).handoff||{}).lines))}
|
|
75449
76110
|
/* What this page reads live: one list for one read. With several reads, a read of 3 or fewer shows its rows under its own label,
|
|
75450
76111
|
and a longer one folds under a closed "See the N \u2026". A count read is one row: the number. */
|
|
76112
|
+
/* Sign-off and handoff (command-center 2026-10-29): read from this client's own build record, the same one the build
|
|
76113
|
+
steps show (F2): what is Built, what waits on the client (the held lines), and the jobs still open. Never GoHighLevel's
|
|
76114
|
+
words for it, and never a claim the record does not hold. */
|
|
76115
|
+
const HELD_RE=/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
|
|
76116
|
+
function capHandoffRecord(card,c){
|
|
76117
|
+
const v=((lastState&&lastState.boughtViews)||{})[c.locationId];
|
|
76118
|
+
if(!v){card.appendChild(mk("p","muted","Nothing is on record for this client yet."));return}
|
|
76119
|
+
const ticked=new Set(v.ticks||[]);const items=(v.items||[]).filter(i=>ticked.has(i.id));
|
|
76120
|
+
const built=items.filter(i=>/^Built: /.test(i.delivery)).map(i=>i.delivery.replace(/^Built: /,""));
|
|
76121
|
+
const waiting=items.filter(i=>HELD_RE.test(i.delivery)).map(i=>i.delivery);
|
|
76122
|
+
const open=(((lastState&&lastState.manualTasks)||{})[c.locationId]||[]).filter(t=>!t.claimed&&!HELD_RE.test(String(t.text))).map(t=>String(t.text));
|
|
76123
|
+
const sec=(title,list,none)=>{const b=mk("div","cap-read");b.appendChild(mk("b","",title));if(!list.length)b.appendChild(mk("p","muted",none));else{const u=mk("ul","");for(const x of list.slice(0,8))u.appendChild(mk("li","",x));if(list.length>8)u.appendChild(mk("li","muted","and "+(list.length-8)+" more"));b.appendChild(u)}card.appendChild(b)};
|
|
76124
|
+
card.appendChild(mk("p","muted","From this client's build record on this computer, the same record the build steps show."));
|
|
76125
|
+
sec("Built",built,"Nothing is recorded as built yet.");
|
|
76126
|
+
sec("Waiting on your client",waiting,"Nothing is waiting on your client.");
|
|
76127
|
+
sec("Still open",open,"No open jobs.");
|
|
76128
|
+
}
|
|
75451
76129
|
function capReadsInto(card,c,hasBuild,foldAll){
|
|
76130
|
+
if(CAP.key==="handoff"){capHandoffRecord(card,c);return}
|
|
75452
76131
|
if(!CAP.reads.length){card.appendChild(capEmpty("This page does not read the account yet","Say the words below to Claude, and it reads what this account has for this job, live."));return}
|
|
75453
76132
|
if(capReadsFor!==c.locationId){card.appendChild(mk("p","muted","Reading this account\u2026"));loadCapReads(c.locationId);return}
|
|
75454
76133
|
if(!capReads){card.appendChild(mk("p","muted","Reading this account\u2026"));return}
|
|
@@ -75552,7 +76231,7 @@ function capOwedPanel(owed,counts,hl){if(!counts){counts={before:0,atGoLive:0,la
|
|
|
75552
76231
|
const jobs=owed.filter(x=>x.job),lines=hl||[];
|
|
75553
76232
|
if(!jobs.length&&!lines.length){p.appendChild(capEmpty("Nothing is recorded as owed yet","The handoff checks and the manual tasks for this client show here once the build has been verified."));return p}
|
|
75554
76233
|
const c=counts||{before:0,atGoLive:0,later:0};
|
|
75555
|
-
p.appendChild(mk("p","",c.before+" "+plural(c.before,"job")+" before sign-off, "+c.atGoLive+" "+plural(c.atGoLive,"automation")+" to switch on, "+c.later+" for later: the same jobs as step
|
|
76234
|
+
p.appendChild(mk("p","",c.before+" "+plural(c.before,"job")+" before sign-off, "+c.atGoLive+" "+plural(c.atGoLive,"automation")+" to switch on, "+c.later+" for later: the same jobs as step 12."));
|
|
75556
76235
|
if(lines.length)p.appendChild(mk("p","muted",handoffCountLine(lines,c)));
|
|
75557
76236
|
p.appendChild(capList(jobs.map(x=>capRow(x.text,"",x.done?chip("ok","Done"):chip(x.timing==="before"?"warn":"",x.timing==="before"?"Before sign-off":x.timing==="at-go-live"?"After sign-off":"For later"))),"jobs"));return p}
|
|
75558
76237
|
/* Intake and documents: what is answered, what is still open, the key facts, and the files. */
|
|
@@ -76995,10 +77674,11 @@ async function runDashboard(argv) {
|
|
|
76995
77674
|
const hit = await pickIntakeContact(c, locationId2, byId2, snap.contactId, INTAKE_FORM_NAME2);
|
|
76996
77675
|
const completed = st0.intakeCompleted?.[locationId2]?.answers;
|
|
76997
77676
|
const answers = Object.fromEntries(Object.entries(completed ?? normalizedIntake({ answers: hit?.answers ?? {}, answeredAt: snap.answeredAt }, st0.intakeDrafts?.[locationId2])).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v]));
|
|
76998
|
-
|
|
77677
|
+
const review = await generateReview(
|
|
76999
77678
|
(prompt) => callClaudeRecorded(claudeBin(), ["-p", prompt, "--allowedTools", "", "--disallowedTools", `Bash,Write,Edit,WebFetch,WebSearch,Task,Read,Glob,Grep,${MCP_DENY}`, "--max-turns", "6", ...NO_MCP_ARGS], { locationId: locationId2, stage: 1, kind: "review" }),
|
|
77000
|
-
{ clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {} }
|
|
77679
|
+
{ clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {}, promises: reviewPromisesFor(st0, locationId2, locationId2) }
|
|
77001
77680
|
);
|
|
77681
|
+
return review.ok ? { ...review, promisesHash: contentHash2(review.promises ?? reviewPromisesFor(st0, locationId2, locationId2)) } : review;
|
|
77002
77682
|
};
|
|
77003
77683
|
const currentRoster = () => {
|
|
77004
77684
|
const registered = registry2.listLocations();
|
|
@@ -77097,7 +77777,7 @@ async function runDashboard(argv) {
|
|
|
77097
77777
|
if (other) cockpitCache.invalidate(account2.locationId, ["brief-account"]);
|
|
77098
77778
|
const v = other ? { cold: true, refreshing: v0.refreshing, stale: true } : v0;
|
|
77099
77779
|
seen.push({ account: account2, v });
|
|
77100
|
-
if (v.cold) throw !other && v.lastRefresh?.text ? new Error(v.lastRefresh.text) : new AccountLoading(COLD_TEXT);
|
|
77780
|
+
if (v.cold) throw !other && !v.refreshing && v.lastRefresh?.text ? new Error(v.lastRefresh.text) : new AccountLoading(COLD_TEXT);
|
|
77101
77781
|
return v.data.read;
|
|
77102
77782
|
},
|
|
77103
77783
|
ages: () => {
|
|
@@ -77652,7 +78332,12 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77652
78332
|
const assessmentFigures = {};
|
|
77653
78333
|
for (const loc of Object.keys(st.assessmentAttachments ?? {})) assessmentFigures[loc] = assessmentFiguresFor(st, loc);
|
|
77654
78334
|
const scopeOffered = Object.fromEntries(rosterNow.map((c) => [c.locationId, offeredFeaturesFor(st, c.locationId)]));
|
|
77655
|
-
const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId,
|
|
78335
|
+
const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId, {
|
|
78336
|
+
...boughtViewFor(st, c.locationId, scopeOffered[c.locationId], boughtWordingFor(st, c.locationId), buildStatusFor(st, c.locationId, c.locationId)),
|
|
78337
|
+
// Ask first (2026-10-29): the saved answers and what the client's own form said, for the questions box.
|
|
78338
|
+
asks: st.asks?.[c.locationId] ?? {},
|
|
78339
|
+
intakeAsks: intakeAskFacts(st.intakeCompleted?.[c.locationId]?.answers ?? st.intakeInputs?.[c.locationId]?.answers)
|
|
78340
|
+
}]));
|
|
77656
78341
|
const buildViews = Object.fromEntries(rosterNow.map((c) => {
|
|
77657
78342
|
const saved = readSavedPlan(c.locationId);
|
|
77658
78343
|
const approval = st.planApprovals?.[c.locationId];
|
|
@@ -77880,7 +78565,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77880
78565
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
77881
78566
|
const proClient = gate3.plan === "pro" ? new URL(req.url || "/", "http://x").searchParams.get("locationId") ?? "" : void 0;
|
|
77882
78567
|
const mine = activeRun2 && (proClient === void 0 || activeRun2.locationId === proClient);
|
|
77883
|
-
res.end(JSON.stringify(mine && activeRun2 ? { ...activeRun2, lines: memberProgress(activeRun2.lines, activeRun2.running, activeRun2.ok) } : activeRun2?.running ? { running: true, elsewhere: true, lines: [] } : { running: false, lines: [] }));
|
|
78568
|
+
res.end(JSON.stringify(mine && activeRun2 ? { ...activeRun2, lines: memberProgress(activeRun2.lines, activeRun2.running, activeRun2.ok, activeRun2.capability ? void 0 : activeRun2.stage) } : activeRun2?.running ? { running: true, elsewhere: true, lines: [] } : { running: false, lines: [] }));
|
|
77884
78569
|
return;
|
|
77885
78570
|
}
|
|
77886
78571
|
if (req.method === "GET" && url === "/api/run/secrets") {
|
|
@@ -77926,7 +78611,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77926
78611
|
if (stage === 3) {
|
|
77927
78612
|
assertStaffUnchanged(st0.planApprovals?.[rk], st0.staffRoster?.[locationId2]);
|
|
77928
78613
|
const sp = readSavedPlan(rk);
|
|
77929
|
-
if (!sp || !
|
|
78614
|
+
if (!sp || !savedPlanFitsAnswers(sp, st0, rk)) throw new Error("The features selected for this client, or the answers about them, changed after its plan was prepared. Prepare the plan again, approve it, then build.");
|
|
77930
78615
|
}
|
|
77931
78616
|
if (stage === 4) {
|
|
77932
78617
|
const build = st0.outcomes?.[`${rk}:3`];
|
|
@@ -78398,6 +79083,17 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78398
79083
|
});
|
|
78399
79084
|
return;
|
|
78400
79085
|
}
|
|
79086
|
+
if (req.method === "GET" && url === "/design/result") {
|
|
79087
|
+
const file3 = designResultFile(new URL(req.url || "/", "http://x").searchParams.get("id") || "");
|
|
79088
|
+
if (!file3) {
|
|
79089
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
79090
|
+
res.end("That design is not ready, or it was not made on this computer.");
|
|
79091
|
+
return;
|
|
79092
|
+
}
|
|
79093
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": "sandbox allow-scripts", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
79094
|
+
res.end(fs53.readFileSync(file3, "utf8"));
|
|
79095
|
+
return;
|
|
79096
|
+
}
|
|
78401
79097
|
if (req.method === "GET" && url === "/api/design/run") {
|
|
78402
79098
|
const id2 = new URL(req.url || "/", "http://x").searchParams.get("id");
|
|
78403
79099
|
const payload = id2 ? designRunStatus(id2) : { runs: listDesignRuns() };
|
|
@@ -78521,8 +79217,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78521
79217
|
if (req.method === "GET" && url === "/handoff") {
|
|
78522
79218
|
const locationId2 = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
|
|
78523
79219
|
if (!readableAccount(locationId2)) {
|
|
78524
|
-
res.writeHead(400);
|
|
78525
|
-
res.end("
|
|
79220
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
79221
|
+
res.end(noClientPageHtml("The handoff", "no-client"));
|
|
78526
79222
|
return;
|
|
78527
79223
|
}
|
|
78528
79224
|
try {
|
|
@@ -78536,8 +79232,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78536
79232
|
if (req.method === "GET" && url === "/handoff/detail") {
|
|
78537
79233
|
const locationId2 = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
|
|
78538
79234
|
if (!readableAccount(locationId2)) {
|
|
78539
|
-
res.writeHead(400);
|
|
78540
|
-
res.end("
|
|
79235
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
79236
|
+
res.end(noClientPageHtml("The account detail", "no-client"));
|
|
78541
79237
|
return;
|
|
78542
79238
|
}
|
|
78543
79239
|
try {
|
|
@@ -78647,13 +79343,23 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78647
79343
|
const st = readCockpitState();
|
|
78648
79344
|
const review = st.reviews?.[locationId2];
|
|
78649
79345
|
if (!review?.ok) {
|
|
78650
|
-
res.writeHead(404, { "Content-Type": "text/
|
|
78651
|
-
res.end("
|
|
79346
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
79347
|
+
res.end(noClientPageHtml("The Blueprint Review", readableAccount(locationId2) ? "no-review" : "no-client"));
|
|
79348
|
+
return;
|
|
79349
|
+
}
|
|
79350
|
+
if (reviewIsStale(st, locationId2, review)) {
|
|
79351
|
+
res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
|
|
79352
|
+
res.end("This client's Blueprint Review was written before their answers or what they bought changed. Press Regenerate on the review, then open the client document again.");
|
|
79353
|
+
return;
|
|
79354
|
+
}
|
|
79355
|
+
if (review.graded !== true) {
|
|
79356
|
+
res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
|
|
79357
|
+
res.end("This client's Blueprint Review was not checked for over-promises. Press Regenerate on the review, then open the client document again.");
|
|
78652
79358
|
return;
|
|
78653
79359
|
}
|
|
78654
79360
|
const name2 = clientDisplayName(st, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name || locationId2);
|
|
78655
79361
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
78656
|
-
res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
|
|
79362
|
+
res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2), buildStatusFor(st, locationId2, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
|
|
78657
79363
|
return;
|
|
78658
79364
|
}
|
|
78659
79365
|
if (req.method === "GET" && url === "/api/review") {
|
|
@@ -78661,7 +79367,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78661
79367
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
78662
79368
|
const stR = readCockpitState();
|
|
78663
79369
|
const businessName = clientDisplayName(stR, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name ?? "");
|
|
78664
|
-
|
|
79370
|
+
const savedReview = stR.reviews?.[locationId2];
|
|
79371
|
+
res.end(JSON.stringify({ ...savedReview ?? { ok: false, error: "not generated yet" }, ...businessName ? { businessName } : {}, ...reviewIsStale(stR, locationId2, savedReview) ? { stale: true } : {} }));
|
|
78665
79372
|
return;
|
|
78666
79373
|
}
|
|
78667
79374
|
if (req.method === "POST" && url === "/api/shared-adopt") {
|
|
@@ -78712,9 +79419,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78712
79419
|
const rk = who2.recordKey;
|
|
78713
79420
|
if (PROTECTED_LOCATIONS[who2.locationId]) throw new Error(PROTECTED_LOCATIONS[who2.locationId] + " can never be built.");
|
|
78714
79421
|
const current = readCockpitState();
|
|
78715
|
-
if (!current.buildApproved?.[rk]) throw new Error("Approve building (step
|
|
79422
|
+
if (!current.buildApproved?.[rk]) throw new Error("Approve building (step 9) before confirming the build.");
|
|
78716
79423
|
const preview = readBuildPreview(rk);
|
|
78717
79424
|
if (!preview || !previewNamesLogins(preview) || typeof fingerprint2 !== "string" || preview.fingerprint !== fingerprint2) throw new Error("This list is out of date. Start the build again to see the current one.");
|
|
79425
|
+
if (preview.earlierBuild?.length) throw new Error(earlierBuildText(preview.earlierBuild, who2.name).why);
|
|
78718
79426
|
const changeKeys = new Set(preview.change.map((e) => e.key));
|
|
78719
79427
|
const allowed = Array.isArray(allow) ? [...new Set(allow.filter((k) => typeof k === "string"))] : [];
|
|
78720
79428
|
if (allowed.some((k) => !changeKeys.has(k))) throw new Error("Only items on the list can be changed.");
|
|
@@ -78782,6 +79490,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78782
79490
|
const approval = { inputHash: hash, at, repairHashes: readWorkflowRepairs(who2.locationId).filter((r) => Array.isArray(repairHashes) && repairHashes.includes(r.hash)).map((r) => r.hash), staffHash: staffListHash(current.staffRoster?.[who2.locationId]), scopeHash: approvalScopeHash(current, rk), boughtHash: approvalBoughtHash(current, rk), ...approvedBy };
|
|
78783
79491
|
approvals[rk] = approval;
|
|
78784
79492
|
}
|
|
79493
|
+
if (!revoke && approvals[rk] && reviewIsStale(current, rk, current.reviews?.[rk])) approvals[rk] = { ...approvals[rk], approvedOverStaleReview: true };
|
|
78785
79494
|
const latest = readCockpitState();
|
|
78786
79495
|
writeCockpitState({ ...setBuildApproval(latest, rk, revoke ? null : at), planApprovals: { ...latest.planApprovals, [rk]: approvals[rk] }, clients: revoke ? latest.clients : setStage(latest, who2.locationId, 2, "done", who2.clientKey).clients });
|
|
78787
79496
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -79205,7 +79914,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
79205
79914
|
if (req.method === "POST" && url === "/api/scope") {
|
|
79206
79915
|
readBody(req, res, 1e4, (body) => {
|
|
79207
79916
|
try {
|
|
79208
|
-
const { locationId: locationId2, clientKey, package: pkg2, features, bought } = JSON.parse(body);
|
|
79917
|
+
const { locationId: locationId2, clientKey, package: pkg2, features, bought, asks: asksIn } = JSON.parse(body);
|
|
79209
79918
|
const who2 = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
|
|
79210
79919
|
let ticks;
|
|
79211
79920
|
let derived;
|
|
@@ -79229,21 +79938,36 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
79229
79938
|
else ticks = savedTicks[rk];
|
|
79230
79939
|
nextState = { ...nextState, bought: savedTicks };
|
|
79231
79940
|
if (!Object.keys(savedTicks).length) delete nextState.bought;
|
|
79941
|
+
const intakeSms = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers).sms;
|
|
79942
|
+
if (asksIn !== void 0) {
|
|
79943
|
+
const parsed2 = parseAsks(asksIn);
|
|
79944
|
+
if (!parsed2.ok) throw new Error(parsed2.error);
|
|
79945
|
+
const clash = asksContradiction(parsed2.asks, effectiveTexting(parsed2.asks, intakeSms));
|
|
79946
|
+
if (clash) throw new Error(clash);
|
|
79947
|
+
const all = { ...nextState.asks ?? {} };
|
|
79948
|
+
if (Object.keys(parsed2.asks).length) all[rk] = parsed2.asks;
|
|
79949
|
+
else delete all[rk];
|
|
79950
|
+
nextState = { ...nextState, asks: all };
|
|
79951
|
+
if (!Object.keys(all).length) delete nextState.asks;
|
|
79952
|
+
}
|
|
79232
79953
|
const { state: bound, withdrawn } = withdrawIfBindingChanged(nextState, rk);
|
|
79233
79954
|
nextState = bound;
|
|
79234
79955
|
{
|
|
79235
79956
|
const offered = offeredFeaturesFor(nextState, rk);
|
|
79236
79957
|
const channels = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers);
|
|
79958
|
+
const texting = effectiveTexting(nextState.asks?.[rk], channels.sms);
|
|
79237
79959
|
const scopeNow = nextState.scope?.[rk];
|
|
79238
79960
|
const excluded = ticks ? boughtButExcludedJobs(ticks, offered, !scopeNow || scopeNow.wholePreset ? "all" : scopeNow.features ?? []) : [];
|
|
79239
|
-
const
|
|
79961
|
+
const held2 = ticks ? heldJobs(heldItems(ticks, nextState.asks?.[rk], texting)) : [];
|
|
79962
|
+
const boughtList = ticks ? [...boughtJobs(ticks, offered, { ...channels, sms: texting === "yes" }), ...excluded, ...held2] : [];
|
|
79240
79963
|
replaceBuildObligationsOfType(rk, BOUGHT_JOB_TYPE, boughtList);
|
|
79241
79964
|
const tasks2 = withBoughtJobs(nextState.manualTasks?.[rk] ?? [], ticks ? catalogJobs({ handoffs: [], obligations: boughtList }) : []);
|
|
79242
79965
|
if (tasks2.length || nextState.manualTasks?.[rk]) nextState = { ...nextState, manualTasks: { ...nextState.manualTasks, [rk]: tasks2 } };
|
|
79243
79966
|
}
|
|
79244
79967
|
writeCockpitState(nextState);
|
|
79245
79968
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
79246
|
-
|
|
79969
|
+
const builtBefore = nextState.outcomes?.[`${rk}:3`]?.ok === true;
|
|
79970
|
+
res.end(JSON.stringify({ ok: true, scope: readCockpitState().scope?.[rk] ?? null, ...withdrawn ? { approvalWithdrawn: true, message: builtBefore && asksIn !== void 0 ? BUILD_WHAT_WAS_WAITING : APPROVAL_WITHDRAWN_FOR_SCOPE } : {} }));
|
|
79247
79971
|
} catch (e) {
|
|
79248
79972
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
79249
79973
|
res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "bad request" }));
|
|
@@ -79434,7 +80158,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
79434
80158
|
process.on("SIGTERM", stop3);
|
|
79435
80159
|
});
|
|
79436
80160
|
}
|
|
79437
|
-
var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, runStarting, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
80161
|
+
var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, BUILD_WHAT_WAS_WAITING, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, runStarting, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
79438
80162
|
var init_dashboard = __esm({
|
|
79439
80163
|
"src/dashboard.ts"() {
|
|
79440
80164
|
"use strict";
|
|
@@ -79475,6 +80199,7 @@ var init_dashboard = __esm({
|
|
|
79475
80199
|
init_client_status_publish();
|
|
79476
80200
|
init_handoff_snapshot();
|
|
79477
80201
|
init_member_progress();
|
|
80202
|
+
init_stage_failure();
|
|
79478
80203
|
init_build_obligations();
|
|
79479
80204
|
fs53 = __toESM(require("fs"));
|
|
79480
80205
|
path53 = __toESM(require("path"));
|
|
@@ -79574,6 +80299,7 @@ var init_dashboard = __esm({
|
|
|
79574
80299
|
init_daily_brief2();
|
|
79575
80300
|
init_daily_brief_collect();
|
|
79576
80301
|
init_brief_deadline();
|
|
80302
|
+
init_client_asks();
|
|
79577
80303
|
VIEWER_COCKPIT_MSG = "Your team login on this license is view only, and Command OS runs work that changes accounts, so it did not open. Ask the license owner to make you a team member, then open Command OS again.";
|
|
79578
80304
|
STAGES2 = [
|
|
79579
80305
|
"Intake",
|
|
@@ -79595,19 +80321,20 @@ var init_dashboard = __esm({
|
|
|
79595
80321
|
CHOOSE_BEFORE_BUILD = "Choose what the client bought before building: Blueprint Review, then What they bought, then Save.";
|
|
79596
80322
|
TEXT_SNIPPETS_LOCKED = "Text snippets are chosen before the first build. This client's build plan is already saved, so the choice stays as it was.";
|
|
79597
80323
|
SCOPE_REFUSAL = /Nobody has chosen what|what this client bought|features (are )?selected for this client|No features are selected|features nobody ticked/i;
|
|
79598
|
-
APPROVAL_WITHDRAWN_FOR_STAFF = "Your team changed after you approved, so the approval was taken back. Approve again in step
|
|
79599
|
-
APPROVAL_WITHDRAWN_NO_TEAM = "This approval was given before the team was saved with it, so it was taken back. Approve again in step
|
|
80324
|
+
APPROVAL_WITHDRAWN_FOR_STAFF = "Your team changed after you approved, so the approval was taken back. Approve again in step 9.";
|
|
80325
|
+
APPROVAL_WITHDRAWN_NO_TEAM = "This approval was given before the team was saved with it, so it was taken back. Approve again in step 9.";
|
|
79600
80326
|
HANDOFF_WORKFLOW = (ref) => ref === "handoff.quote_tag" ? "workflow.quote_follow_up" : ref === "handoff.review_link" || ref.startsWith("handoff.review_tag") ? "workflow.review_request" : void 0;
|
|
79601
80327
|
NOTHING_BOUGHT = "Tick at least one item they bought, then save.";
|
|
79602
80328
|
NOTHING_BUILT_FROM_TICKS = "None of the ticked items is built for this client's industry, so nothing was saved. Tick at least one item marked Built.";
|
|
79603
80329
|
ASSESSMENT_CHANGED_SINCE_APPROVAL = "The Assessment answer or the attached Assessment changed after the build was approved, so Build did not start. Approve this plan again, then build.";
|
|
79604
80330
|
APPROVAL_WITHDRAWN_FOR_ASSESSMENT = "The Assessment answer changed, so the build approval was withdrawn. Approve this plan again before building.";
|
|
79605
80331
|
APPROVE_NEEDS_INTAKE = "Approve opens after step 5, Intake, is finished.";
|
|
79606
|
-
APPROVE_NEEDS_BRIEF = "Approve opens after step
|
|
80332
|
+
APPROVE_NEEDS_BRIEF = "Approve opens after step 7, Brief, has run.";
|
|
79607
80333
|
INDUSTRY_FIRST = "Choose what kind of business this is in step 3 first.";
|
|
79608
80334
|
NOTHING_TICKED_IN_PLAN = "None of the ticked features is in the plan for this client's industry, so nothing was saved. Tick features from the list, or choose Everything in the plan.";
|
|
79609
80335
|
SCOPE_CHANGED_SINCE_APPROVAL = "What they bought changed after the build was approved, so Build did not start. Approve this plan again, then build.";
|
|
79610
80336
|
APPROVAL_WITHDRAWN_FOR_SCOPE = "What they bought changed, so the build approval was withdrawn. Approve this plan again before building.";
|
|
80337
|
+
BUILD_WHAT_WAS_WAITING = "Your client's answers changed what gets built, so the build approval was withdrawn. Approve the updated plan, then run Build: it adds only what was waiting and makes nothing twice.";
|
|
79611
80338
|
INACTIVE = "License is not active. Contact support@ghlcommand.com.";
|
|
79612
80339
|
OFF_LICENSE = "This computer is no longer on your license. Free a device on your license, then restart Command OS.";
|
|
79613
80340
|
LICENSE_RECHECK_MS = 4 * 60 * 60 * 1e3;
|
|
@@ -86483,14 +87210,14 @@ function registerDesignStudioTools(server2) {
|
|
|
86483
87210
|
d.provenance.evidence["shape.radius"] = `set by ${stamp}`;
|
|
86484
87211
|
}
|
|
86485
87212
|
if (a.logoSvg) {
|
|
86486
|
-
let
|
|
87213
|
+
let clean6;
|
|
86487
87214
|
try {
|
|
86488
|
-
|
|
87215
|
+
clean6 = sanitizeSvg(a.logoSvg);
|
|
86489
87216
|
} catch (e) {
|
|
86490
87217
|
throw new Error(`${e.message} Nothing was changed.`);
|
|
86491
87218
|
}
|
|
86492
|
-
a.logoSvg =
|
|
86493
|
-
d.logo = { inlineSvg:
|
|
87219
|
+
a.logoSvg = clean6;
|
|
87220
|
+
d.logo = { inlineSvg: clean6, whiteFillFixed: false, source: `set by ${stamp}`, rights: "declared" };
|
|
86494
87221
|
d.provenance.evidence["logo"] = `set by ${stamp}`;
|
|
86495
87222
|
}
|
|
86496
87223
|
if (a.tone) d.voice.tone = a.tone;
|
|
@@ -90167,8 +90894,8 @@ async function listProcs(platform2 = process.platform, exec = execText, timeoutM
|
|
|
90167
90894
|
return listingOf(platform2, await bounded(exec(file3, args, timeoutMs), timeoutMs));
|
|
90168
90895
|
}
|
|
90169
90896
|
function jobMarker(jobDir, platform2 = process.platform) {
|
|
90170
|
-
const
|
|
90171
|
-
return `--user-data-dir=${jobDir.replace(/[\\/]+$/, "")}${
|
|
90897
|
+
const sep7 = platform2 === "win32" ? "\\" : "/";
|
|
90898
|
+
return `--user-data-dir=${jobDir.replace(/[\\/]+$/, "")}${sep7}`;
|
|
90172
90899
|
}
|
|
90173
90900
|
function holdsMarker(command, marker, platform2 = process.platform) {
|
|
90174
90901
|
return namesJob(command, markerDir(marker), platform2);
|
|
@@ -91514,18 +92241,22 @@ function claimBuildLock(locationId2, base) {
|
|
|
91514
92241
|
}
|
|
91515
92242
|
return null;
|
|
91516
92243
|
}
|
|
91517
|
-
function takeOver(lock, seen) {
|
|
92244
|
+
function takeOver(lock, seen, retried = false) {
|
|
91518
92245
|
const mutex = `${lock}.takeover`;
|
|
91519
92246
|
let fd;
|
|
91520
92247
|
try {
|
|
91521
92248
|
fd = fs63.openSync(mutex, "wx", 384);
|
|
91522
92249
|
} catch (e) {
|
|
91523
92250
|
if (e.code !== "EEXIST") throw e;
|
|
92251
|
+
let aged = false;
|
|
91524
92252
|
try {
|
|
91525
|
-
if (Date.now() - fs63.statSync(mutex).mtimeMs > TAKEOVER_STALE_MS)
|
|
92253
|
+
if (Date.now() - fs63.statSync(mutex).mtimeMs > TAKEOVER_STALE_MS) {
|
|
92254
|
+
fs63.unlinkSync(mutex);
|
|
92255
|
+
aged = true;
|
|
92256
|
+
}
|
|
91526
92257
|
} catch {
|
|
91527
92258
|
}
|
|
91528
|
-
return false;
|
|
92259
|
+
return aged && !retried ? takeOver(lock, seen, true) : false;
|
|
91529
92260
|
}
|
|
91530
92261
|
try {
|
|
91531
92262
|
try {
|
|
@@ -94598,6 +95329,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94598
95329
|
const recordKey = clientRecordKey(activeLocation, clientKey);
|
|
94599
95330
|
let planArg = planArgIn;
|
|
94600
95331
|
let composed;
|
|
95332
|
+
let asksHash;
|
|
94601
95333
|
if (fromPreset) {
|
|
94602
95334
|
if (planArg) {
|
|
94603
95335
|
return jsonResponse({ ok: false, phase: "compose", error: "fromPreset composes the plan itself \u2014 pass brief (and optionally preset / copy), not a plan. To run a hand-authored plan, drop fromPreset." });
|
|
@@ -94655,6 +95387,12 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94655
95387
|
if (!chosen.ok) {
|
|
94656
95388
|
return jsonResponse({ ok: false, phase: "compose", error: chosen.error });
|
|
94657
95389
|
}
|
|
95390
|
+
{
|
|
95391
|
+
const board = readBoardState();
|
|
95392
|
+
const ticks = board.bought?.[recordKey];
|
|
95393
|
+
briefForCompose = briefWithAsks(briefForCompose, board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0, chosen.preset.skeleton?.workflows ?? []);
|
|
95394
|
+
asksHash = asksBindingHash(board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0);
|
|
95395
|
+
}
|
|
94658
95396
|
const composition = planFromPreset({ preset: chosen.preset, brief: briefForCompose, locationId: activeLocation, copy: copyArg });
|
|
94659
95397
|
if (!composition.ok) {
|
|
94660
95398
|
return jsonResponse({
|
|
@@ -94785,7 +95523,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94785
95523
|
const { name: locationName, timeZone: locationTimeZone } = await readLocationBasics(client, activeLocation);
|
|
94786
95524
|
const { assets, warnings: scanWarnings } = await scanExistingAssets(client, activeLocation, registry2);
|
|
94787
95525
|
const result = resolvePlan(typedPlan, assets, { metHandoffs });
|
|
94788
|
-
const clientFields = { ...clientKey ? { clientKey } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
|
|
95526
|
+
const clientFields = { ...clientKey ? { clientKey } : {}, ...asksHash ? { asksHash } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
|
|
94789
95527
|
if (clinicPrefix && clinicScoping) {
|
|
94790
95528
|
let checked = false;
|
|
94791
95529
|
let findings = [];
|
|
@@ -94910,7 +95648,16 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94910
95648
|
}
|
|
94911
95649
|
const { changes: workflowChanges, states: workflowStates } = await previewWorkflowChanges(deps, result, ours, saved?.calendarPending);
|
|
94912
95650
|
const logins = (typedPlan.users ?? []).filter((u) => result.items.some((i) => i.ref === u.ref && i.type === "users" && i.status === "would_create")).map((u) => ({ name: [u.firstName, u.lastName === "-" ? "" : u.lastName].filter(Boolean).join(" "), email: u.email, role: u.role }));
|
|
94913
|
-
const
|
|
95651
|
+
const otherPlans = createdByOtherPlans(activeLocation, { planHash: contentHash2(typedPlan), planId: typedPlan.planId });
|
|
95652
|
+
const workflowStatus = /* @__PURE__ */ new Map();
|
|
95653
|
+
try {
|
|
95654
|
+
const listed = workflowInventorySchema.safeParse(await client.get("/workflows/", { params: { locationId: activeLocation } }));
|
|
95655
|
+
if (listed.success) {
|
|
95656
|
+
for (const w of listed.data.workflows) if (typeof w.status === "string") workflowStatus.set(w.id, String(w.status));
|
|
95657
|
+
}
|
|
95658
|
+
} catch {
|
|
95659
|
+
}
|
|
95660
|
+
const preview = composePreview({ locationId: activeLocation, recordKey, planHash: contentHash2(typedPlan), items: result.items, ours, workflowChanges, workflowStates, board, logins, otherPlans, workflowStatus, ...bookingCalendarName(typedPlan, result) && { bookingCalendar: bookingCalendarName(typedPlan, result) } });
|
|
94914
95661
|
try {
|
|
94915
95662
|
saveBuildPreview(preview);
|
|
94916
95663
|
} catch (e) {
|
|
@@ -94921,7 +95668,8 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94921
95668
|
return jsonResponse({
|
|
94922
95669
|
ok: false,
|
|
94923
95670
|
phase: "confirm",
|
|
94924
|
-
error: confirm2 ? BUILD_PREVIEW_CHANGED : BUILD_PAUSED,
|
|
95671
|
+
error: preview.earlierBuild?.length ? earlierBuildText(preview.earlierBuild, locationName ?? "This account").why : confirm2 ? BUILD_PREVIEW_CHANGED : BUILD_PAUSED,
|
|
95672
|
+
...preview.earlierBuild?.length ? { earlierBuild: preview.earlierBuild, undoWithClaude: earlierBuildText(preview.earlierBuild, locationName ?? "This account").undo } : {},
|
|
94925
95673
|
willCreate: preview.createCount,
|
|
94926
95674
|
willUse: preview.reuse.map((e) => `${PREVIEW_KIND[e.type] ?? e.type} "${e.name}"`),
|
|
94927
95675
|
wouldChange: preview.change.map((e) => ({ what: `${PREVIEW_KIND[e.type] ?? e.type} "${e.name}"`, changes: e.changes ?? [] }))
|
|
@@ -94938,7 +95686,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94938
95686
|
const runId = newRunId();
|
|
94939
95687
|
const journalParents = parentRefIndex(typedPlan);
|
|
94940
95688
|
try {
|
|
94941
|
-
appendBuildStart({ runId, locationId: activeLocation, locationName, planId: typedPlan.planId });
|
|
95689
|
+
appendBuildStart({ runId, locationId: activeLocation, locationName, planId: typedPlan.planId, planHash: contentHash2(typedPlan) });
|
|
94942
95690
|
if (notOurs.length) appendBuildNotOurs({ runId, locationId: activeLocation, objects: notOurs.map((e) => ({ type: e.type, name: e.name, ghlId: e.id })) });
|
|
94943
95691
|
if (allowedChanges.length) appendBuildChanges({ runId, locationId: activeLocation, changes: allowedChanges });
|
|
94944
95692
|
} catch (e) {
|
|
@@ -95749,6 +96497,7 @@ var init_intake_to_build = __esm({
|
|
|
95749
96497
|
init_claims_guard();
|
|
95750
96498
|
init_plan_from_preset();
|
|
95751
96499
|
init_board_roster();
|
|
96500
|
+
init_client_asks();
|
|
95752
96501
|
init_features();
|
|
95753
96502
|
init_executor();
|
|
95754
96503
|
init_build_confirm();
|
|
@@ -96324,6 +97073,21 @@ var os11 = __toESM(require("node:os"));
|
|
|
96324
97073
|
function sha2563(buf) {
|
|
96325
97074
|
return (0, import_node_crypto34.createHash)("sha256").update(buf).digest("hex");
|
|
96326
97075
|
}
|
|
97076
|
+
function isLink(p) {
|
|
97077
|
+
try {
|
|
97078
|
+
return fs65.lstatSync(p).isSymbolicLink();
|
|
97079
|
+
} catch {
|
|
97080
|
+
return false;
|
|
97081
|
+
}
|
|
97082
|
+
}
|
|
97083
|
+
function linkOnTheWay(root, rel) {
|
|
97084
|
+
let at = root;
|
|
97085
|
+
for (const part of rel.split(/[\\/]/).filter(Boolean)) {
|
|
97086
|
+
at = path65.join(at, part);
|
|
97087
|
+
if (isLink(at)) return true;
|
|
97088
|
+
}
|
|
97089
|
+
return false;
|
|
97090
|
+
}
|
|
96327
97091
|
function isOlderRelease(candidate, installed) {
|
|
96328
97092
|
const parts = (v) => v.trim().split(".").map((n) => Number.parseInt(n, 10));
|
|
96329
97093
|
const a = parts(candidate), b = parts(installed);
|
|
@@ -96371,6 +97135,7 @@ function installBundledSkills(opts) {
|
|
|
96371
97135
|
updated: [],
|
|
96372
97136
|
skippedUserModified: [],
|
|
96373
97137
|
skippedOlderPackage: [],
|
|
97138
|
+
skippedSymlink: [],
|
|
96374
97139
|
unchanged: [],
|
|
96375
97140
|
errors: [],
|
|
96376
97141
|
targetDir: opts.targetDir ?? defaultSkillsDir(),
|
|
@@ -96385,6 +97150,11 @@ function installBundledSkills(opts) {
|
|
|
96385
97150
|
result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
|
|
96386
97151
|
return result;
|
|
96387
97152
|
}
|
|
97153
|
+
if (isLink(result.targetDir)) {
|
|
97154
|
+
result.targetIsLink = true;
|
|
97155
|
+
result.skippedSymlink.push(...listFiles(bundled));
|
|
97156
|
+
return result;
|
|
97157
|
+
}
|
|
96388
97158
|
const markerPath = path65.join(result.targetDir, ".ghl-command-skills.json");
|
|
96389
97159
|
const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
|
|
96390
97160
|
const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
|
|
@@ -96392,6 +97162,10 @@ function installBundledSkills(opts) {
|
|
|
96392
97162
|
try {
|
|
96393
97163
|
const src = fs65.readFileSync(path65.join(bundled, rel));
|
|
96394
97164
|
const dest = path65.join(result.targetDir, rel);
|
|
97165
|
+
if (linkOnTheWay(result.targetDir, rel)) {
|
|
97166
|
+
result.skippedSymlink.push(rel);
|
|
97167
|
+
continue;
|
|
97168
|
+
}
|
|
96395
97169
|
const srcHash = sha2563(src);
|
|
96396
97170
|
let destBuf = null;
|
|
96397
97171
|
try {
|
|
@@ -96429,6 +97203,7 @@ function installBundledSkills(opts) {
|
|
|
96429
97203
|
}
|
|
96430
97204
|
}
|
|
96431
97205
|
try {
|
|
97206
|
+
if (isLink(markerPath)) throw new Error("the marker file is a symbolic link; not written");
|
|
96432
97207
|
fs65.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
|
|
96433
97208
|
} catch (e) {
|
|
96434
97209
|
result.errors.push(`marker write failed: ${String(e)}`);
|
|
@@ -96449,6 +97224,7 @@ function summarizeInstall(r) {
|
|
|
96449
97224
|
bucket(r.updated, "updated");
|
|
96450
97225
|
bucket(r.skippedUserModified, "kept with your edits");
|
|
96451
97226
|
bucket(r.skippedOlderPackage, "kept, newer than this version");
|
|
97227
|
+
bucket(r.skippedSymlink ?? [], "not touched, linked elsewhere");
|
|
96452
97228
|
if (r.errors.length) bits.push(`${r.errors.length} file error${r.errors.length === 1 ? "" : "s"}`);
|
|
96453
97229
|
if (bits.length === 0) return "";
|
|
96454
97230
|
return `Skills: ${bits.join(", ")} \u2192 ${r.targetDir}`;
|
|
@@ -96476,7 +97252,7 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
|
|
|
96476
97252
|
ok: result.errors.length === 0,
|
|
96477
97253
|
targetDir: result.targetDir,
|
|
96478
97254
|
// Skills, by name, before the per-file detail (BUG-2026-09-14-03).
|
|
96479
|
-
skills: { installed: skillNames(result.installed), updated: skillNames(result.updated), keptYourEdits: skillNames(result.skippedUserModified) },
|
|
97255
|
+
skills: { installed: skillNames(result.installed), updated: skillNames(result.updated), keptYourEdits: skillNames(result.skippedUserModified), linkedElsewhere: skillNames(result.skippedSymlink) },
|
|
96480
97256
|
installed: result.installed,
|
|
96481
97257
|
updated: result.updated,
|
|
96482
97258
|
keptYourEdits: result.skippedUserModified,
|
|
@@ -96485,7 +97261,14 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
|
|
|
96485
97261
|
keptNewerThanThisVersion: result.skippedOlderPackage,
|
|
96486
97262
|
unchanged: result.unchanged.length,
|
|
96487
97263
|
errors: result.errors,
|
|
96488
|
-
|
|
97264
|
+
// Reviewer MED-2 on rc.1: a skills folder (or skill folder) that is a link elsewhere is never written, and the
|
|
97265
|
+
// reply says so instead of "Everything already current."
|
|
97266
|
+
linkedElsewhere: result.skippedSymlink,
|
|
97267
|
+
next: [
|
|
97268
|
+
result.installed.length > 0 || result.updated.length > 0 ? "Fully restart Claude (quit and reopen) so the new/updated skills load." : "",
|
|
97269
|
+
result.skippedSymlink.length ? `Not installed: ${skillNames(result.skippedSymlink).join(", ")}, because ${linkedWhere(result)} is a link to somewhere else, and GHL Command never writes through a link. Replace the link with a real folder to get these skills.` : "",
|
|
97270
|
+
result.installed.length === 0 && result.updated.length === 0 && result.skippedSymlink.length === 0 ? "Everything already current." : ""
|
|
97271
|
+
].filter(Boolean).join(" ")
|
|
96489
97272
|
});
|
|
96490
97273
|
} catch (error) {
|
|
96491
97274
|
return errorResponse(error);
|
|
@@ -96493,6 +97276,10 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
|
|
|
96493
97276
|
}
|
|
96494
97277
|
);
|
|
96495
97278
|
}
|
|
97279
|
+
function linkedWhere(r) {
|
|
97280
|
+
if (r.targetIsLink) return `the skills folder (${r.targetDir})`;
|
|
97281
|
+
return `the folder for ${skillNames(r.skippedSymlink).length === 1 ? "that skill" : "each of those skills"}`;
|
|
97282
|
+
}
|
|
96496
97283
|
|
|
96497
97284
|
// src/index.ts
|
|
96498
97285
|
init_brain();
|