@elitedcs/ghl-mcp 3.89.0-beta.6 → 3.89.0-beta.7
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 +24 -0
- package/README.md +1 -1
- package/dist/index.js +289 -70
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.89.0-beta.7: the intake fills what your notes say, and the cockpit shows you where to start
|
|
4
|
+
|
|
5
|
+
Found by clicking through a full client build in the cockpit.
|
|
6
|
+
|
|
7
|
+
**Filling the intake from a document you added twice works again.** Adding a document with the same name as one already saved, such as updated notes, stores it as a second copy, and filling the intake from that copy always failed. It now reads. When filling the intake does fail, a Details line under the message says why.
|
|
8
|
+
|
|
9
|
+
**Complete the intake takes you there.** Clicking Complete the intake in the Next step box now scrolls the page to the intake it opens.
|
|
10
|
+
|
|
11
|
+
**Brief tells you how to fill the intake.** If you run Brief before the intake is in, it now says "First complete the intake. It fills most answers from a call or your notes." with a Complete the intake button that opens the intake and takes you to it. It used to say only to open the real form, with no link.
|
|
12
|
+
|
|
13
|
+
**The intake is right under the steps when it is next.** While Brief is the next step and no intake is in, the Next step box says to complete the intake first and has a Complete the intake button, and the intake opens right under the steps instead of far down the page. It stays there while it is open.
|
|
14
|
+
|
|
15
|
+
**Typing on Build a client account is kept.** A sub-account id or client detail you type while the page is still reading your accounts is no longer cleared when it finishes. For your security, the key is never kept.
|
|
16
|
+
|
|
17
|
+
**Filling the intake from your notes fills what your notes say.** Answers your notes state plainly are now filled in, such as business hours, staff, team size, the trial price, deposit and balance, the trial length and how many trials fit in a day. An answer is filled only when the exact words it came from give it; anything worked out from other facts is left as a suggestion. Yes or no answers always wait for your click.
|
|
18
|
+
|
|
19
|
+
**Use all suggestions.** A new button above the suggestions takes every suggestion that would not replace an answer you already have. Any that would replace one stay beside their field for you to decide, and so do the answers about trial spots running out and a 72-hour program price, because they decide what your texts may promise.
|
|
20
|
+
|
|
21
|
+
**Clearer intake counts.** After filling from a call or notes, the intake says how many answers were filled, how many are suggestions waiting for your click, and which questions your notes did not answer, by name.
|
|
22
|
+
|
|
23
|
+
**A quieter Verify report.** The line about check notes that did not match the account is now a small note at the end of the report instead of in its headline.
|
|
24
|
+
|
|
25
|
+
**Smaller fixes.** The light and dark switch in the account menu is read out as "Color theme".
|
|
26
|
+
|
|
3
27
|
## 3.89.0-beta.6: Build only states what the account shows, and calendars open when the client is open
|
|
4
28
|
|
|
5
29
|
This is a beta, for testers only. Found by running a full Wellness clinic client build in the cockpit.
|
package/README.md
CHANGED
|
@@ -899,7 +899,7 @@ Built by **[Elite DCs, LLC](https://elitedcs.com)** — a digital marketing and
|
|
|
899
899
|
|
|
900
900
|
**Tech stack:** TypeScript, Node.js, esbuild, MCP SDK, Zod, GHL API v2, Firebase Auth
|
|
901
901
|
|
|
902
|
-
**Version:** 3.89.0-beta.
|
|
902
|
+
**Version:** 3.89.0-beta.7
|
|
903
903
|
|
|
904
904
|
---
|
|
905
905
|
|
package/dist/index.js
CHANGED
|
@@ -30165,7 +30165,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
30165
30165
|
});
|
|
30166
30166
|
});
|
|
30167
30167
|
}
|
|
30168
|
-
var import_child_process3, fs21, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
|
|
30168
|
+
var import_child_process3, fs21, BRIEF_NEEDS_INTAKE, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
|
|
30169
30169
|
var init_stage_runner = __esm({
|
|
30170
30170
|
"src/stage-runner.ts"() {
|
|
30171
30171
|
"use strict";
|
|
@@ -30186,6 +30186,7 @@ var init_stage_runner = __esm({
|
|
|
30186
30186
|
init_account_rules();
|
|
30187
30187
|
init_client_record();
|
|
30188
30188
|
init_workflow_builder_client();
|
|
30189
|
+
BRIEF_NEEDS_INTAKE = "First complete the intake. It fills most answers from a call or your notes.";
|
|
30189
30190
|
STAFF_NOTIFICATION_RULE = `Staff notifications ALWAYS stay in the workflow. Call get_users on THIS account and use only a user id it returns as the recipient. If get_users returns no users, leave the recipient EMPTY ("") \u2014 never use a user id from another account, from the agency, from an earlier conversation or from memory (a foreign id looks like it worked and notifies nobody). An empty recipient can make publish fail: then keep the workflow as a draft with the notification step still in it, and list that step in the RESULT issues as "waiting for a staff member: add one in ${GHL_ADD_STAFF_CLICK_PATH}, then pick them on the notification step and publish".`;
|
|
30190
30191
|
STAFF_PAUSE_RULE = `If apply_build_plan answers with staffPause, copy its reason into issues word for word and do not call those calendars built.`;
|
|
30191
30192
|
BUILD_REPLY_RULE = `A long apply_build_plan reply comes back shortened, with replyTrimmed on it; it still holds everything your report needs (summary, missingSteps, workflowsNotBuilt, staffPause, calendars, countsByType, notClean, templates, nextManualSteps, customValues and, on a dry run, composed.copySlots), so report from it. If you need a section it lists under sectionsNotShownInFull, read it with apply_build_plan detail:{of:"dry_run" or "execute",section:"<name>"}; that call builds nothing.`;
|
|
@@ -30266,7 +30267,7 @@ If anything fails, finish with: RESULT: {"ok":false,"error":"<short reason>"}`
|
|
|
30266
30267
|
3. Turn whichever you found into a Brief with normalize_submission_to_brief (for a form submission), then check it with validate_brief. If the answers came from the tagged contact, build the Brief from those field values directly and call validate_brief on it too. Booking calendars go at the TOP level of the Brief as calendars:[{"name":"...","type":"one_on_one"|"round_robin"|"class","staffNames":["..."],"durationMinutes":30}], one entry per calendar, never inside goal. Put each real gap validate_brief reports in issues, in plain words, and finish ok:true. When validate_brief finds nothing, issues is [] \u2014 never report the clean result itself as an issue.
|
|
30267
30268
|
Use these authoritative normalized intake answers when provided: ${JSON.stringify(ctx?.normalizedAnswers ?? {})}.
|
|
30268
30269
|
Do NOT create, update or delete anything \u2014 this step only reads and normalizes.
|
|
30269
|
-
Only if BOTH paths come up empty, finish with RESULT: {"ok":false,"error"
|
|
30270
|
+
Only if BOTH paths come up empty, finish with RESULT: {"ok":false,"error":${JSON.stringify(BRIEF_NEEDS_INTAKE)}}.
|
|
30270
30271
|
${NO_INVENTED_CAUSE_RULE}
|
|
30271
30272
|
Finish with EXACTLY one final line:
|
|
30272
30273
|
RESULT: {"ok":true,"summary":"<one sentence: who the client is and what they want built>","issues":["<validate_brief gaps, if any>"]}`
|
|
@@ -31761,17 +31762,35 @@ function renderVerifyReport(records, notes) {
|
|
|
31761
31762
|
const summary = [
|
|
31762
31763
|
`${records.buildOk === true ? verifyPassedLine(records.memberJobs ?? rows2.filter((r) => r.status === "pending").length + issues.length) : `${present} of ${rows2.length} planned items are in the account; ${attention} need attention.`}${drafts ? `
|
|
31763
31764
|
${drafts}` : ""}${liveLine ? `
|
|
31764
|
-
${liveLine}` : ""}
|
|
31765
|
-
${removedClaimNote(removedClaims.size)}` : ""}`,
|
|
31765
|
+
${liveLine}` : ""}`,
|
|
31766
31766
|
["What is not right", ...diagnosis.length ? diagnosis : ["No planned items need attention."]].join("\n"),
|
|
31767
31767
|
["Every planned item", ...[...grouped].flatMap(([type, items]) => [typeLabels[type], ...items.map((r) => ` ${r.line}`)])].join("\n"),
|
|
31768
|
-
|
|
31768
|
+
// Test 01 (BUG-2026-09-30-36): the left-out note is a quiet line in the notes, after the lists, not in the headline.
|
|
31769
|
+
...summaryNotes.length || removedClaims.size ? [["Notes from the check", ...summaryNotes, ...removedClaims.size ? [removedClaimNote(removedClaims.size)] : []].join("\n")] : []
|
|
31769
31770
|
].join("\n\n");
|
|
31770
31771
|
const cvNames = customValueDisplayNames(rows2, records.deps);
|
|
31771
31772
|
return { summary: plainCustomValueNames(summary, cvNames), issues: issues.map((i) => plainCustomValueNames(i, cvNames)) };
|
|
31772
31773
|
}
|
|
31773
31774
|
function removedClaimNote(count2) {
|
|
31774
|
-
return `${count2 === 1 ? "One
|
|
31775
|
+
return `${count2 === 1 ? "One check note did not match the account and is not shown" : `${count2} check notes did not match the account and are not shown`}. The lists above are what the account holds.`;
|
|
31776
|
+
}
|
|
31777
|
+
function quietRemovedClaimLine(summary) {
|
|
31778
|
+
if (!summary) return summary;
|
|
31779
|
+
const blocks = summary.split("\n\n");
|
|
31780
|
+
const head4 = blocks[0].split("\n");
|
|
31781
|
+
const old = /^(One thing the check reported was|(\d+) things the check reported were) left out\b/;
|
|
31782
|
+
const at = head4.findIndex((l) => old.test(l.trim()));
|
|
31783
|
+
if (at < 0) return summary;
|
|
31784
|
+
const m = old.exec(head4[at].trim());
|
|
31785
|
+
const note = removedClaimNote(m[2] ? Number(m[2]) : 1);
|
|
31786
|
+
head4.splice(at, 1);
|
|
31787
|
+
blocks[0] = head4.join("\n");
|
|
31788
|
+
const notesAt = blocks.findIndex((b) => b.startsWith("Notes from the check"));
|
|
31789
|
+
if (notesAt >= 0) blocks[notesAt] = `${blocks[notesAt]}
|
|
31790
|
+
${note}`;
|
|
31791
|
+
else blocks.push(`Notes from the check
|
|
31792
|
+
${note}`);
|
|
31793
|
+
return blocks.join("\n\n");
|
|
31775
31794
|
}
|
|
31776
31795
|
function customValueDisplayName(key) {
|
|
31777
31796
|
const words2 = key.trim().replace(/^cv\./, "").replace(/[_\s]+/g, " ").trim();
|
|
@@ -31833,7 +31852,7 @@ var init_member_progress = __esm({
|
|
|
31833
31852
|
jobText = (text2) => text2.trim().replace(/\s+/g, " ").replace(/([^.!?])$/, "$1.");
|
|
31834
31853
|
countWord = (text2) => /^\d+$/.test(text2) ? Number(text2) : ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"].indexOf(text2.toLowerCase());
|
|
31835
31854
|
normalJob = (text2) => text2.toLowerCase().replace(/^manual action:\s*/, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
31836
|
-
REMOVED_CLAIM_LINE = /^(?:One thing the check reported was|\d+ things the check reported were) left out\b|^Removed a claim\b/i;
|
|
31855
|
+
REMOVED_CLAIM_LINE = /^(?:One check note did not match|\d+ check notes did not match) the account\b|^(?:One thing the check reported was|\d+ things the check reported were) left out\b|^Removed a claim\b/i;
|
|
31837
31856
|
}
|
|
31838
31857
|
});
|
|
31839
31858
|
|
|
@@ -34084,7 +34103,7 @@ var require_package = __commonJS({
|
|
|
34084
34103
|
"package.json"(exports2, module2) {
|
|
34085
34104
|
module2.exports = {
|
|
34086
34105
|
name: "@elitedcs/ghl-mcp",
|
|
34087
|
-
version: "3.89.0-beta.
|
|
34106
|
+
version: "3.89.0-beta.7",
|
|
34088
34107
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
34089
34108
|
description: "GoHighLevel MCP Server for Claude. 250 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
34090
34109
|
main: "dist/index.js",
|
|
@@ -34317,7 +34336,7 @@ ${cockpitSidebarHtml(active, opts.capability)}
|
|
|
34317
34336
|
<a class="cds-menu-item" role="menuitem" href="/connections">Connections</a>
|
|
34318
34337
|
<a class="cds-menu-item" role="menuitem" href="${HUB_URL}#do">Members Hub</a>
|
|
34319
34338
|
<div class="cds-menu-sep" role="separator"></div>
|
|
34320
|
-
<div class="cds-menu-row"><span>Appearance</span><div class="cds-seg" role="group" aria-label="
|
|
34339
|
+
<div class="cds-menu-row"><span>Appearance</span><div class="cds-seg" role="group" aria-label="Color theme"><button type="button" data-theme-choice="auto" aria-pressed="true">Auto</button><button type="button" data-theme-choice="light" aria-pressed="false">Light</button><button type="button" data-theme-choice="dark" aria-pressed="false">Dark</button></div></div>
|
|
34321
34340
|
</div>
|
|
34322
34341
|
</details>
|
|
34323
34342
|
</div>
|
|
@@ -47001,6 +47020,10 @@ function prefillPrompt(labels, transcript) {
|
|
|
47001
47020
|
"Answer ONLY from the material provided. Never invent a fact, a number, or a name.",
|
|
47002
47021
|
"Every answer must carry a QUOTE: the exact words from the material it comes from, copied verbatim, under 300 characters. An answer with no quote will be thrown away, and a quote that is not in the material will be thrown away.",
|
|
47003
47022
|
'If the material does not clearly answer a question, leave it out and list it under "unknown".',
|
|
47023
|
+
"When the material STATES a fact, answer it. Do not leave a stated fact out.",
|
|
47024
|
+
'Quote ONLY the words that give that one answer, not the whole line or paragraph around them. For a number, quote just the words with that one number, for example "$79 total" or "No more than 6 trials a day". For the staff and team size, quote the lines that name the people.',
|
|
47025
|
+
"Copy the material's own words into the value wherever the question allows it, rather than rewording them.",
|
|
47026
|
+
'Give every answer a "basis": "stated" when the material says it, or "inferred" when the material does not say it and you are working it out from other facts.',
|
|
47004
47027
|
"Keep answers short and concrete \u2014 this feeds an automated build, not a report.",
|
|
47005
47028
|
"",
|
|
47006
47029
|
"QUESTIONS (use these labels verbatim as keys):",
|
|
@@ -47010,7 +47033,7 @@ function prefillPrompt(labels, transcript) {
|
|
|
47010
47033
|
transcript.slice(0, 6e4),
|
|
47011
47034
|
"",
|
|
47012
47035
|
"Reply with ONE line of JSON and nothing else:",
|
|
47013
|
-
'RESULT: {"answers":{"<label>":{"value":"<answer>","quote":"<their exact words>"}},"unknown":["<label>"]}'
|
|
47036
|
+
'RESULT: {"answers":{"<label>":{"value":"<answer>","quote":"<their exact words>","basis":"stated or inferred"}},"unknown":["<label>"]}'
|
|
47014
47037
|
].join("\n");
|
|
47015
47038
|
}
|
|
47016
47039
|
function prefillPromptForSource(labels, source) {
|
|
@@ -47032,7 +47055,8 @@ async function prefillFromSource(spawnClaude, labels, source, toolContext = { se
|
|
|
47032
47055
|
if (empty) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "nothing to read" };
|
|
47033
47056
|
if (source.kind === "url") return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: LINK_READING_OFF_MESSAGE };
|
|
47034
47057
|
if (source.kind === "text" && source.text.length > 6e4) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "The limit is 60,000 characters. Paste an excerpt or shorten this text file." };
|
|
47035
|
-
const
|
|
47058
|
+
const byLabel = new Map(questions.map((q3) => [q3.label, q3]));
|
|
47059
|
+
const result = await runPrefill(() => spawnClaude(prefillPromptForSource(labels, source) + "\nQuestion schema (use only these choices; multiple choices use an array in the cited answer object\u2019s value):\n" + JSON.stringify(questions), prefillTools(source, toolContext)), labels, source.kind === "text" ? source.text : void 0, byLabel);
|
|
47036
47060
|
result.answers = validatePrefillAnswers(questions, result.answers);
|
|
47037
47061
|
for (const [label2, held] of Object.entries(result.held)) {
|
|
47038
47062
|
const checked = validatePrefillAnswers(questions, { [label2]: held.value });
|
|
@@ -47043,7 +47067,87 @@ async function prefillFromSource(spawnClaude, labels, source, toolContext = { se
|
|
|
47043
47067
|
result.unknown = labels.filter((label2) => !(label2 in result.answers) && !(label2 in result.held));
|
|
47044
47068
|
return result;
|
|
47045
47069
|
}
|
|
47046
|
-
|
|
47070
|
+
function numbersIn(text2) {
|
|
47071
|
+
return [...String(text2 ?? "").matchAll(/\d+(?:,\d{3})*(?:\.\d+)?/g)].map((m) => Number(m[0].replace(/,/g, ""))).filter(Number.isFinite);
|
|
47072
|
+
}
|
|
47073
|
+
function isYesNo(options) {
|
|
47074
|
+
return Boolean(options?.includes("Yes") && options?.includes("No"));
|
|
47075
|
+
}
|
|
47076
|
+
function wordsInQuote(value, quote) {
|
|
47077
|
+
const q3 = ` ${tokens(quote).join(" ")} `;
|
|
47078
|
+
const lines = value.split(/\n+/).map((l) => tokens(l).join(" ")).filter(Boolean);
|
|
47079
|
+
return lines.length > 0 && lines.every((l) => q3.includes(` ${l} `));
|
|
47080
|
+
}
|
|
47081
|
+
function onlyNumber(quote) {
|
|
47082
|
+
const nums = [...new Set(numbersIn(quote))];
|
|
47083
|
+
return nums.length === 1 ? nums[0] : null;
|
|
47084
|
+
}
|
|
47085
|
+
function amountBefore(quote, word) {
|
|
47086
|
+
const m = new RegExp(`${AMOUNT}\\s*${word}\\b`, "i").exec(quote) ?? new RegExp(`\\b${word}\\b[^0-9$]{0,12}${AMOUNT}`, "i").exec(quote);
|
|
47087
|
+
return m ? Number(m[1].replace(/,/g, "")) : null;
|
|
47088
|
+
}
|
|
47089
|
+
function depositIn(quote) {
|
|
47090
|
+
return amountBefore(quote, "deposit");
|
|
47091
|
+
}
|
|
47092
|
+
function totalIn(quote) {
|
|
47093
|
+
return amountBefore(quote, "total");
|
|
47094
|
+
}
|
|
47095
|
+
function staffNamedIn(quote) {
|
|
47096
|
+
return new Set((String(quote).match(/[^\s@()<>,;]+@[^\s@()<>,;]+\.[a-z]{2,}/gi) ?? []).map((e) => e.toLowerCase())).size;
|
|
47097
|
+
}
|
|
47098
|
+
function teamRange(n) {
|
|
47099
|
+
return n <= 1 ? "Just me" : n <= 5 ? "2-5" : "6+";
|
|
47100
|
+
}
|
|
47101
|
+
function leadRange(n) {
|
|
47102
|
+
return n < 100 ? "Under 100" : n < 500 ? "100-500" : n < 1e3 ? "500-1000" : "1000+";
|
|
47103
|
+
}
|
|
47104
|
+
function quoteGives(value, quote, label2 = "", options) {
|
|
47105
|
+
const n = numericValue(value);
|
|
47106
|
+
if (/^Balance due/i.test(label2)) {
|
|
47107
|
+
const t = totalIn(quote), d = depositIn(quote);
|
|
47108
|
+
return Boolean(n && !n.percent && t !== null && d !== null && t > d && t - d === n.n);
|
|
47109
|
+
}
|
|
47110
|
+
if (/^Deposit/i.test(label2)) return Boolean(n && !n.percent && depositIn(quote) === n.n);
|
|
47111
|
+
if (options?.length) {
|
|
47112
|
+
if (!options.includes(value)) return false;
|
|
47113
|
+
if (/[a-z]/i.test(value) && (/\d/.test(value) || tokens(value).length >= 3) && wordsInQuote(value, quote)) return true;
|
|
47114
|
+
if (/^Team Size$/i.test(label2)) {
|
|
47115
|
+
const named = staffNamedIn(quote);
|
|
47116
|
+
if (named) return named >= 2 && teamRange(named) === value;
|
|
47117
|
+
const one = onlyNumber(quote);
|
|
47118
|
+
return one !== null && /\b(?:staff|team|people|employees?|providers?)\b/i.test(quote) && teamRange(one) === value;
|
|
47119
|
+
}
|
|
47120
|
+
if (/Lead Volume/i.test(label2)) {
|
|
47121
|
+
const one = onlyNumber(quote);
|
|
47122
|
+
return one !== null && /\bleads?\b/i.test(quote) && leadRange(one) === value;
|
|
47123
|
+
}
|
|
47124
|
+
return false;
|
|
47125
|
+
}
|
|
47126
|
+
if (n) {
|
|
47127
|
+
if (n.percent || onlyNumber(quote) !== n.n) return false;
|
|
47128
|
+
if (/price|cost|value|\$/i.test(label2) && !/\$/.test(quote)) return false;
|
|
47129
|
+
if (/^Trial price/i.test(label2) && /deposit/i.test(quote)) return false;
|
|
47130
|
+
if (/How long is the trial/i.test(label2) && (!/\btrials?\b/i.test(quote) || !/minute/i.test(quote) || /\$/.test(quote))) return false;
|
|
47131
|
+
if (/in one day|per day/i.test(label2) && !/\b(?:trials?|visits?|clients?)\s+(?:a|per)\s+day\b/i.test(quote)) return false;
|
|
47132
|
+
if (/^Average program value/i.test(label2) && !/\baverage\b|\bmost popular\b|\bprogram\b/i.test(quote)) return false;
|
|
47133
|
+
return true;
|
|
47134
|
+
}
|
|
47135
|
+
return wordsInQuote(value, quote);
|
|
47136
|
+
}
|
|
47137
|
+
function intakeCitation(material, value, quote, basis, question) {
|
|
47138
|
+
const v = String(value ?? "").trim();
|
|
47139
|
+
if (v === "") return { verdict: "refused", because: "no value was given." };
|
|
47140
|
+
const said = quoteCheck(material, quote);
|
|
47141
|
+
if (said.verdict === "refused") return said;
|
|
47142
|
+
const shown = quote.length > 140 ? quote.slice(0, 137).trimEnd() + "\u2026" : quote;
|
|
47143
|
+
const based = `Based on \u201C${shown}\u201D.`;
|
|
47144
|
+
if (question && CLAIMS_GUARD_QUESTION.test(question.label)) return { verdict: "held", because: `This answer decides what the texts may promise, so it always waits for your click. ${based}`, claims: true };
|
|
47145
|
+
if (question && isYesNo(question.options)) return { verdict: "held", because: `Yes or no answers always wait for your click. ${based}` };
|
|
47146
|
+
if (!quoteGives(v, quote, question?.label, question?.options)) return { verdict: "held", because: based };
|
|
47147
|
+
if (basis === "inferred") return { verdict: "held", because: `Your Claude marked this as worked out. ${based}` };
|
|
47148
|
+
return { verdict: "ok" };
|
|
47149
|
+
}
|
|
47150
|
+
async function runPrefill(call2, labels, material, questions = /* @__PURE__ */ new Map()) {
|
|
47047
47151
|
const fail = (error) => ({ ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error });
|
|
47048
47152
|
try {
|
|
47049
47153
|
const out = await call2();
|
|
@@ -47058,16 +47162,18 @@ async function runPrefill(call2, labels, material) {
|
|
|
47058
47162
|
const candidate = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.value : raw;
|
|
47059
47163
|
const value = typeof candidate === "string" ? candidate.trim() : Array.isArray(candidate) && candidate.every((v) => typeof v === "string") ? candidate.map((v) => v.trim()).filter(Boolean) : "";
|
|
47060
47164
|
const quote = typeof raw?.quote === "string" ? String(raw.quote).trim() : "";
|
|
47165
|
+
const basis = raw?.basis === "inferred" ? "inferred" : void 0;
|
|
47061
47166
|
if (!value.length) continue;
|
|
47062
47167
|
if (material === void 0) {
|
|
47063
47168
|
answers[k] = value;
|
|
47064
47169
|
unverified.push(k);
|
|
47065
47170
|
continue;
|
|
47066
47171
|
}
|
|
47067
|
-
const
|
|
47172
|
+
const q3 = questions.get(k);
|
|
47173
|
+
const checks = (Array.isArray(value) ? value : [value]).map((v) => intakeCitation(material, v, quote, basis, q3 ? { label: k, options: q3.options } : void 0));
|
|
47068
47174
|
const cite = checks.find((c) => c.verdict === "refused") ?? checks.find((c) => c.verdict === "held") ?? checks[0];
|
|
47069
47175
|
if (cite.verdict === "ok") answers[k] = value;
|
|
47070
|
-
else if (cite.verdict === "held") held[k] = { value, because: cite.because };
|
|
47176
|
+
else if (cite.verdict === "held") held[k] = { value, because: cite.because, ...cite.claims ? { claims: true } : {} };
|
|
47071
47177
|
}
|
|
47072
47178
|
const unknown = labels.filter((l) => !(l in answers) && !(l in held));
|
|
47073
47179
|
return { ok: true, answers, unknown, held, unverified };
|
|
@@ -47089,7 +47195,7 @@ function validatePrefillAnswers(questions, proposed) {
|
|
|
47089
47195
|
}
|
|
47090
47196
|
return result;
|
|
47091
47197
|
}
|
|
47092
|
-
var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, FICTIONAL_PHONE_REASON, RECORDER_HINTS, RECORDER_READ_VERBS, RECORDER_WRITE_VERBS, PREFILL_BUILTIN_DENY, LINK_READING_OFF_MESSAGE, WORD_PDF_MESSAGE, READABLE_DOCUMENT;
|
|
47198
|
+
var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, FICTIONAL_PHONE_REASON, RECORDER_HINTS, RECORDER_READ_VERBS, RECORDER_WRITE_VERBS, PREFILL_BUILTIN_DENY, LINK_READING_OFF_MESSAGE, WORD_PDF_MESSAGE, READABLE_DOCUMENT, CLAIMS_GUARD_QUESTION, AMOUNT;
|
|
47093
47199
|
var init_client_intake = __esm({
|
|
47094
47200
|
"src/client-intake.ts"() {
|
|
47095
47201
|
"use strict";
|
|
@@ -47100,6 +47206,7 @@ var init_client_intake = __esm({
|
|
|
47100
47206
|
init_intake_overlay();
|
|
47101
47207
|
init_form_template();
|
|
47102
47208
|
init_transcript();
|
|
47209
|
+
init_words();
|
|
47103
47210
|
init_result_text();
|
|
47104
47211
|
init_links();
|
|
47105
47212
|
INTAKE_FORM_NAME2 = "GHL Command Blueprint \u2014 Client Intake";
|
|
@@ -47156,6 +47263,8 @@ var init_client_intake = __esm({
|
|
|
47156
47263
|
LINK_READING_OFF_MESSAGE = "Reading a link isn't available in this beta. Open the page, copy its text, and paste it into Paste a transcript.";
|
|
47157
47264
|
WORD_PDF_MESSAGE = "Word and PDF files can't be read yet. Open the file, copy the text, and paste it into Paste a transcript.";
|
|
47158
47265
|
READABLE_DOCUMENT = /[.](txt|md|csv|vtt|srt)$/i;
|
|
47266
|
+
CLAIMS_GUARD_QUESTION = /really run out|only good for 72 hours/i;
|
|
47267
|
+
AMOUNT = String.raw`\$?\s*(\d[\d,]*(?:\.\d+)?)`;
|
|
47159
47268
|
}
|
|
47160
47269
|
});
|
|
47161
47270
|
|
|
@@ -52224,9 +52333,11 @@ __export(dashboard_exports, {
|
|
|
52224
52333
|
APPROVAL_WITHDRAWN_FOR_ASSESSMENT: () => APPROVAL_WITHDRAWN_FOR_ASSESSMENT,
|
|
52225
52334
|
APPROVAL_WITHDRAWN_FOR_SCOPE: () => APPROVAL_WITHDRAWN_FOR_SCOPE,
|
|
52226
52335
|
ASSESSMENT_CHANGED_SINCE_APPROVAL: () => ASSESSMENT_CHANGED_SINCE_APPROVAL,
|
|
52336
|
+
BRIEF_NEEDS_INTAKE: () => BRIEF_NEEDS_INTAKE2,
|
|
52227
52337
|
CHOOSE_BEFORE_BUILD: () => CHOOSE_BEFORE_BUILD,
|
|
52228
52338
|
CHOOSE_WHAT_THEY_BOUGHT: () => CHOOSE_WHAT_THEY_BOUGHT,
|
|
52229
52339
|
CLAUDE_REPAIR_MESSAGES: () => CLAUDE_REPAIR_MESSAGES,
|
|
52340
|
+
DOC_NOT_SAVED: () => DOC_NOT_SAVED,
|
|
52230
52341
|
INTAKE_ANSWERS_MISSING: () => INTAKE_ANSWERS_MISSING,
|
|
52231
52342
|
INTAKE_CONNECT_REQUIRED: () => INTAKE_CONNECT_REQUIRED,
|
|
52232
52343
|
INTAKE_FORM_REQUIRED: () => INTAKE_FORM_REQUIRED,
|
|
@@ -52277,10 +52388,13 @@ __export(dashboard_exports, {
|
|
|
52277
52388
|
guarded: () => guarded,
|
|
52278
52389
|
hasChosenScope: () => hasChosenScope,
|
|
52279
52390
|
insideDir: () => insideDir,
|
|
52391
|
+
intakeErrorDetail: () => intakeErrorDetail,
|
|
52280
52392
|
intakeInput: () => intakeInput,
|
|
52393
|
+
intakeRefusal: () => intakeRefusal,
|
|
52281
52394
|
intakeRequestError: () => intakeRequestError,
|
|
52282
52395
|
intakeUnavailableError: () => intakeUnavailableError,
|
|
52283
52396
|
isAllowedCockpitHost: () => isAllowedCockpitHost,
|
|
52397
|
+
isBriefNeedsIntake: () => isBriefNeedsIntake,
|
|
52284
52398
|
isLoopback: () => isLoopback,
|
|
52285
52399
|
isUpstreamFailure: () => isUpstreamFailure,
|
|
52286
52400
|
launcherAlreadyOffered: () => launcherAlreadyOffered,
|
|
@@ -52302,6 +52416,7 @@ __export(dashboard_exports, {
|
|
|
52302
52416
|
readFailureText: () => readFailureText,
|
|
52303
52417
|
readOverlay: () => readOverlay,
|
|
52304
52418
|
readRunCosts: () => readRunCosts,
|
|
52419
|
+
readSavedClientDoc: () => readSavedClientDoc,
|
|
52305
52420
|
readableAccount: () => readableAccount,
|
|
52306
52421
|
recordLauncherOffered: () => recordLauncherOffered,
|
|
52307
52422
|
recoverStuckStages: () => recoverStuckStages,
|
|
@@ -52560,7 +52675,8 @@ function verifiedStaffNames(check) {
|
|
|
52560
52675
|
return (check?.matched ?? []).map((m) => String(m.name ?? "").trim()).filter(Boolean);
|
|
52561
52676
|
}
|
|
52562
52677
|
function verifyPanelOutcome(value) {
|
|
52563
|
-
|
|
52678
|
+
const shown = value.plain?.summary?.trim() ? value.plain : { summary: value.summary, issues: value.issues };
|
|
52679
|
+
return { ...shown, summary: quietRemovedClaimLine(shown.summary) };
|
|
52564
52680
|
}
|
|
52565
52681
|
function startRun(locationId2, stage, onDone, meta2 = {}) {
|
|
52566
52682
|
const rk = clientRecordKey(locationId2, meta2.clientKey);
|
|
@@ -52671,6 +52787,12 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
|
|
|
52671
52787
|
outcome.issues = [];
|
|
52672
52788
|
outcome.plain = { summary: outcome.error, issues: [] };
|
|
52673
52789
|
}
|
|
52790
|
+
const briefNeedsIntake = stage === 1 && !outcome.ok && isBriefNeedsIntake(outcome.error);
|
|
52791
|
+
if (briefNeedsIntake) {
|
|
52792
|
+
outcome.summary = BRIEF_NEEDS_INTAKE2;
|
|
52793
|
+
outcome.issues = [];
|
|
52794
|
+
outcome.plain = { summary: BRIEF_NEEDS_INTAKE2, issues: [] };
|
|
52795
|
+
}
|
|
52674
52796
|
if (stage === 3 && !outcome.summary && !outcome.issues?.length) {
|
|
52675
52797
|
try {
|
|
52676
52798
|
const st = readCockpitState();
|
|
@@ -52684,10 +52806,10 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
|
|
|
52684
52806
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
52685
52807
|
const save = (plain) => {
|
|
52686
52808
|
const st = readCockpitState();
|
|
52687
|
-
writeCockpitState({ ...st, outcomes: { ...st.outcomes ?? {}, [`${rk}:${stage}`]: { ok: outcome.ok, error: outcome.error, summary: outcome.summary, issues: outcome.issues, ...plain && { plain }, at, inputHash: runInputHash, ...stage === 4 ? { buildHash: runBuildHash, verification: verifyRaw?.verification } : {}, hash: contentHash({ inputHash: runInputHash, at, ok: outcome.ok, summary: outcome.summary, issues: outcome.issues }) } } });
|
|
52809
|
+
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: contentHash({ inputHash: runInputHash, at, ok: outcome.ok, summary: outcome.summary, issues: outcome.issues }) } } });
|
|
52688
52810
|
};
|
|
52689
|
-
save(scopeRefusal ? outcome.plain : void 0);
|
|
52690
|
-
if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal) {
|
|
52811
|
+
save(scopeRefusal || briefNeedsIntake ? outcome.plain : void 0);
|
|
52812
|
+
if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal && !briefNeedsIntake) {
|
|
52691
52813
|
run.lines.push("Writing the plain-English version of what came back\u2026");
|
|
52692
52814
|
const rewritten = await rewriteOutcomePlain(STAGES2[stage], verifyRaw ?? outcome, { onEvent: (l) => run.lines.push(l), onCost: (costUsd) => {
|
|
52693
52815
|
recordRunCost({ at, locationId: locationId2, stage, kind: "plain rewrite", ok: true, costUsd });
|
|
@@ -52862,7 +52984,7 @@ function clientDocsDir(locationId2, base = appDataDir()) {
|
|
|
52862
52984
|
return path40.join(base, "clients", safeLoc, "documents");
|
|
52863
52985
|
}
|
|
52864
52986
|
function safeDocName(name) {
|
|
52865
|
-
const cleaned = path40.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, " ").trim();
|
|
52987
|
+
const cleaned = path40.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ ()-]/g, "_").replace(/\s+/g, " ").trim();
|
|
52866
52988
|
const ext = path40.extname(cleaned).toLowerCase();
|
|
52867
52989
|
if (!cleaned || cleaned.startsWith(".") || !DOC_EXTS.has(ext)) throw new Error("Only plain text (.txt, .md, .csv), .vtt and .srt files can be saved here.");
|
|
52868
52990
|
const stem = cleaned.slice(0, cleaned.length - ext.length).slice(0, 120 - ext.length).trim();
|
|
@@ -53023,6 +53145,16 @@ function insideDir(dir, p) {
|
|
|
53023
53145
|
return false;
|
|
53024
53146
|
}
|
|
53025
53147
|
}
|
|
53148
|
+
function readSavedClientDoc(locationId2, file2, base = appDataDir()) {
|
|
53149
|
+
const name = safeDocName(file2);
|
|
53150
|
+
const dir = clientDocsDir(locationId2, base);
|
|
53151
|
+
const p = path40.join(dir, name);
|
|
53152
|
+
if (!fs42.existsSync(p)) throw new Error(DOC_NOT_SAVED);
|
|
53153
|
+
if (fs42.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error(DOC_NOT_SAVED);
|
|
53154
|
+
const text2 = fs42.readFileSync(p, "utf8");
|
|
53155
|
+
if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
|
|
53156
|
+
return text2;
|
|
53157
|
+
}
|
|
53026
53158
|
function uniqueDocName(dir, name) {
|
|
53027
53159
|
const ext = path40.extname(name), stem = name.slice(0, name.length - ext.length);
|
|
53028
53160
|
let candidate = name;
|
|
@@ -53061,9 +53193,12 @@ function intakeUnavailableError(snap) {
|
|
|
53061
53193
|
const reason3 = snap.unavailableReason;
|
|
53062
53194
|
return new Error(reason3 === "auth" ? INTAKE_UNREADABLE : reason3 === "missing" ? INTAKE_ANSWERS_MISSING : reason3 === "error" ? INTAKE_READ_FAILED : INTAKE_READ_TRANSIENT);
|
|
53063
53195
|
}
|
|
53196
|
+
function isBriefNeedsIntake(error) {
|
|
53197
|
+
return typeof error === "string" && /^\s*First complete the intake\b/i.test(error);
|
|
53198
|
+
}
|
|
53064
53199
|
function runRefusal(e) {
|
|
53065
53200
|
const error = e && typeof e.message === "string" ? e.message : "bad request";
|
|
53066
|
-
return error === INTAKE_ANSWERS_MISSING ? { ok: false, error, reason: "intake-missing" } : error === RUN_IN_PROGRESS ? { ok: false, error, reason: "busy" } : { ok: false, error };
|
|
53201
|
+
return error === INTAKE_ANSWERS_MISSING ? { ok: false, error, reason: "intake-missing" } : error === RUN_IN_PROGRESS ? { ok: false, error, reason: "busy" } : error === BRIEF_NEEDS_INTAKE2 ? { ok: false, error, reason: "intake-needed" } : { ok: false, error };
|
|
53067
53202
|
}
|
|
53068
53203
|
function planClaimChecks(saved) {
|
|
53069
53204
|
const plan = saved?.plan && typeof saved.plan === "object" ? saved.plan : void 0;
|
|
@@ -53096,6 +53231,16 @@ function intakeRequestError(e) {
|
|
|
53096
53231
|
const text2 = e instanceof Error ? e.message : "";
|
|
53097
53232
|
return INTAKE_OWN_MESSAGES.includes(text2) ? text2 : "The intake request did not finish. Retry this step.";
|
|
53098
53233
|
}
|
|
53234
|
+
function intakeErrorDetail(e) {
|
|
53235
|
+
const raw = e instanceof Error ? e.message : typeof e === "string" ? e : "";
|
|
53236
|
+
if (!raw || INTAKE_OWN_MESSAGES.includes(raw)) return void 0;
|
|
53237
|
+
const clean4 = 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();
|
|
53238
|
+
return clean4 ? clean4.length > 240 ? clean4.slice(0, 237) + "\u2026" : clean4 : void 0;
|
|
53239
|
+
}
|
|
53240
|
+
function intakeRefusal(e) {
|
|
53241
|
+
const detail = intakeErrorDetail(e);
|
|
53242
|
+
return { ok: false, error: intakeRequestError(e), ...detail ? { detail } : {} };
|
|
53243
|
+
}
|
|
53099
53244
|
function escHtml2(s) {
|
|
53100
53245
|
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
53101
53246
|
}
|
|
@@ -53452,7 +53597,8 @@ function outcomePanel(id,i,state){const oc=(state.outcomes||{})[id+":"+i];if(!oc
|
|
|
53452
53597
|
t.textContent="Every planned item: "+part.split(NL).filter(l=>l.startsWith(" ")&&l.trim().length>0).length+" read back from the account";box.appendChild(t);
|
|
53453
53598
|
const body=document.createElement("p");body.className="runlog";body.textContent=part.split(NL).slice(1).join(NL);
|
|
53454
53599
|
box.appendChild(body);d.appendChild(box);continue}
|
|
53455
|
-
|
|
53600
|
+
/* Notes from the check are background, drawn quietly (test 01, BUG-2026-09-30-36). */
|
|
53601
|
+
const p=document.createElement("p");if(i===4)p.className=/^Notes from the check/.test(part)?"runlog muted small vnotes":"runlog";p.textContent=part;d.appendChild(p)}}
|
|
53456
53602
|
for(const x of iss){const g=document.createElement("div");g.className="cds-callout warn pendpanel";
|
|
53457
53603
|
g.textContent=x;d.appendChild(g)}
|
|
53458
53604
|
const when=document.createElement("p");when.className="cds-src";
|
|
@@ -53516,7 +53662,7 @@ let iFieldN=0;
|
|
|
53516
53662
|
function iField(id,q,val,prefilled){const w=document.createElement("div");w.className="cds-field ifield"+(prefilled?" prefilled":"");
|
|
53517
53663
|
const fid="ifield-"+(++iFieldN);
|
|
53518
53664
|
const l=document.createElement("label");l.htmlFor=fid;l.textContent=q.label+(q.required?" *":"");if(prefilled)l.setAttribute("data-src",iPreSrc[id]||"your call");w.appendChild(l);
|
|
53519
|
-
if(prefilled)w.appendChild(mk("div","hint","Filled in from "+(
|
|
53665
|
+
if(prefilled)w.appendChild(mk("div","hint","Filled in from "+iSrcWords(id)+". Check it."));
|
|
53520
53666
|
let inp;
|
|
53521
53667
|
if(q.multi&&q.options&&q.options.length){
|
|
53522
53668
|
// Several answers allowed (the GHL field is multi-choice) . checkboxes, stored as a list. (2026-08-25, finding 8)
|
|
@@ -53610,17 +53756,13 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53610
53756
|
:"Your own Claude asks the recorder you connected (Plaud, Otter, Fireflies\u2026) for that call; the transcript then goes to your Claude account and nowhere else.";
|
|
53611
53757
|
const r=await iWork(id,async()=>(await clientPageRequest("/api/intake-prefill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)})).json());
|
|
53612
53758
|
b.disabled=false;b.textContent="Fill the intake from this";
|
|
53613
|
-
if(!r.ok){st
|
|
53614
|
-
|
|
53759
|
+
if(!r.ok){iFailNote(st,r);return}
|
|
53760
|
+
for(const n of ["transcript","url","recorder"])delete iDraft[id+":"+n];
|
|
53615
53761
|
/* Codex F2 (2026-09-06): a HELD answer (the words were said, the value was worked out) is a
|
|
53616
53762
|
suggestion beside the field, never an answer. It reaches iAnswers only when the operator
|
|
53617
|
-
clicks "Use it". Unverified answers (read by their Claude, not
|
|
53618
|
-
but the banner names them. */
|
|
53619
|
-
|
|
53620
|
-
iHeld[id]=r.held||{};iUnv[id]=r.unverified||[];
|
|
53621
|
-
const heldKeys=Object.keys(iHeld[id]),unv=iUnv[id];
|
|
53622
|
-
iNote[id]=(heldKeys.length?heldKeys.length+" answer"+(heldKeys.length===1?" was":"s were")+" worked out rather than heard and NOT filled in: "+heldKeys.join(", ")+". Each is shown beside its field with a Use it button. ":"")+(unv.length?unv.length+" could not be checked against the material because your Claude read it rather than you pasting it here: "+unv.join(", ")+". Check them.":"");
|
|
53623
|
-
iMode[id]="fill";render(lastState,lastRun)};
|
|
53763
|
+
clicks "Use it" or "Use all suggestions". Unverified answers (read by their Claude, not
|
|
53764
|
+
pasted here) are answers, but the banner names them. */
|
|
53765
|
+
iAfterFill(id,r,sub==="url"?"the link":"your call")};
|
|
53624
53766
|
b.onclick=()=>readIt(false);icAfter(b,limitKey);
|
|
53625
53767
|
const urlIn=document.createElement("input");urlIn.placeholder="https://\u2026 a shared transcript or recording page";
|
|
53626
53768
|
urlIn.className="cds-input";urlIn.setAttribute("aria-label","Link to the transcript or recording");keepDraft(urlIn,id,"url");
|
|
@@ -53656,8 +53798,8 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53656
53798
|
rb.onclick=async()=>{rb.disabled=true;rb.textContent="Reading it\u2026";st.textContent="The text of "+f.name+" goes to your own Claude account to be read, and nowhere else.";
|
|
53657
53799
|
const r=await iWork(id,async()=>(await clientPageRequest("/api/intake-prefill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:id,labels:d.questions.map(q=>q.label),file:f.name})})).json());
|
|
53658
53800
|
rb.disabled=false;rb.textContent="Fill the intake from this";
|
|
53659
|
-
if(!r.ok){st
|
|
53660
|
-
|
|
53801
|
+
if(!r.ok){iFailNote(st,r);return}
|
|
53802
|
+
iAfterFill(id,r,"the document")};
|
|
53661
53803
|
const end=mk("span","end");end.appendChild(rb);row.appendChild(end);list.appendChild(row)}
|
|
53662
53804
|
|
|
53663
53805
|
w.appendChild(list);
|
|
@@ -53667,9 +53809,7 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53667
53809
|
if(iSub[id]!=="doc"){w.appendChild(b);w.appendChild(st)}box.appendChild(w)}
|
|
53668
53810
|
if(mode==="fill"||mode==="interview"){
|
|
53669
53811
|
const qs=d.questions;const ans=iAnswers[id]||{};const pre=iPre[id]||[];
|
|
53670
|
-
if(
|
|
53671
|
-
banner.textContent=pre.length+" answers pre-filled from "+(iPreSrc[id]||"your call")+". Correct anything wrong; "+(qs.length-pre.length)+" still need you.";box.appendChild(banner)}
|
|
53672
|
-
if(mode==="fill"&&iNote[id]){const nb=document.createElement("div");nb.className="cds-callout warn pendpanel";nb.textContent=iNote[id];box.appendChild(nb)}
|
|
53812
|
+
if(mode==="fill")iFillBanners(id,qs,box);
|
|
53673
53813
|
if(mode==="interview"){const i=Math.min(iIdx[id]||0,qs.length-1);const q=qs[i];
|
|
53674
53814
|
const prog=document.createElement("p");prog.className="cds-panel-label";prog.textContent="Question "+(i+1)+" of "+qs.length+" \xB7 "+q.sectionLabel;
|
|
53675
53815
|
const qq=document.createElement("div");qq.className="cds-panel-title iq";qq.textContent=q.label;
|
|
@@ -53689,13 +53829,13 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53689
53829
|
{const rp=iReplacePanel(id,q.label);if(rp)box.appendChild(rp)}
|
|
53690
53830
|
const held=(iHeld[id]||{})[q.label];
|
|
53691
53831
|
if(held){const hr=document.createElement("div");hr.className="cds-callout warn cds-row";
|
|
53692
|
-
const ht=document.createElement("span");ht.textContent="
|
|
53832
|
+
const ht=document.createElement("span");ht.textContent="Suggested from "+iSrcWords(id)+": \u201C"+iShowVal(held.value)+"\u201D. "+(held.because||"");hr.appendChild(ht);
|
|
53693
53833
|
const use=document.createElement("button");use.className="cds-btn ghost";use.textContent="Use it";
|
|
53694
53834
|
const heldKey="replaceheld:"+id+":"+q.label;
|
|
53695
53835
|
const dropHeld=()=>{const h=Object.assign({},iHeld[id]);delete h[q.label];iHeld[id]=h};
|
|
53696
53836
|
use.onclick=()=>{const cur=(iAnswers[id]||{})[q.label];
|
|
53697
53837
|
if(iConflicts(iAnswers[id]||{},q.label,held.value))return icShow(hr,heldKey,iReplaceSpec(id,q.label,cur,held.value,dropHeld));
|
|
53698
|
-
|
|
53838
|
+
iUseHeld(id,{[q.label]:held.value});dropHeld();render(lastState,lastRun)};
|
|
53699
53839
|
hr.appendChild(use);box.appendChild(icAfter(hr,heldKey))}}
|
|
53700
53840
|
if(hasForm){const save=document.createElement("button");save.className=d.connectionPending?"cds-btn ghost":"cds-btn";
|
|
53701
53841
|
save.textContent="Review and submit the real form";save.disabled=Boolean(d.connectionPending);
|
|
@@ -53722,6 +53862,45 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53722
53862
|
if(hasForm&&iPrepared[id])preparedFormBlock(box,id)}}
|
|
53723
53863
|
return box}
|
|
53724
53864
|
let iNote={},iHeld={},iUnv={};
|
|
53865
|
+
/* A failed read says the plain sentence, and the real reason under Details (BUG-2026-09-30-38). */
|
|
53866
|
+
function iFailNote(st,r){st.textContent=clientPageError(r&&r.error,"Check the source and try reading it again.");
|
|
53867
|
+
if(r&&r.detail){const d=mk("details","cds-details ifaildetail");d.appendChild(mk("summary","","Details"));d.appendChild(mk("p","small",r.detail));st.appendChild(document.createTextNode(" "));st.appendChild(d)}}
|
|
53868
|
+
/* What the intake was filled from, in the banner's words. */
|
|
53869
|
+
/* One word for the source in every banner line (review nit): your notes for a document, the transcript otherwise. */
|
|
53870
|
+
function iSrcWords(id){return (iPreSrc[id]||"your call")==="the document"?"your notes":"the transcript"}
|
|
53871
|
+
/* After Fill the intake from this (test 01 walkthrough, BUG-2026-09-30-35): the stated answers land, the
|
|
53872
|
+
suggestions wait beside their fields, and the banners are drawn from what is on screen at each redraw. */
|
|
53873
|
+
function iAfterFill(id,r,src){if(r.costs)iData[id].costs=r.costs;iPre[id]=iApplyProposals(id,r.answers);iPreSrc[id]=src;
|
|
53874
|
+
iHeld[id]=r.held||{};iHeldLeft[id]=0;iUnv[id]=r.unverified||[];const unv=iUnv[id];
|
|
53875
|
+
iNote[id]=unv.length?unv.length+" could not be checked against the material because your Claude read it rather than you pasting it here: "+unv.join(", ")+". Check them.":"";
|
|
53876
|
+
iMode[id]="fill";render(lastState,lastRun)}
|
|
53877
|
+
/* A suggestion the operator took counts as filled from the source, and is marked to check like the rest. */
|
|
53878
|
+
function iUseHeld(id,proposals){const applied=iApplyProposals(id,proposals);const pre=iPre[id]||(iPre[id]=[]);for(const l of applied)if(!pre.includes(l))pre.push(l);return applied}
|
|
53879
|
+
function iHasAnswer(v){return Array.isArray(v)?v.length>0:String(v==null?"":v).trim()!==""}
|
|
53880
|
+
/* Use all suggestions (BUG-2026-09-30-34): every suggestion that would not replace an answer already there.
|
|
53881
|
+
The ones that would replace one stay beside their fields for the person to decide. Returns how many stayed. */
|
|
53882
|
+
function iUseAllHeld(id){const held=iHeld[id]||{};const answers=iAnswers[id]||{};const keep={};const take={};
|
|
53883
|
+
/* The claims guard's questions (spots running out, the 72-hour price) are never taken in bulk (review): each waits for its own Use it. */
|
|
53884
|
+
for(const [label,h] of Object.entries(held)){if(h.claims||iConflicts(answers,label,h.value))keep[label]=h;else take[label]=h.value}
|
|
53885
|
+
iUseHeld(id,take);iHeld[id]=keep;return Object.keys(keep).length}
|
|
53886
|
+
/* The fill banners, counted from what is on screen: filled, suggestions waiting for a click, and what the
|
|
53887
|
+
source did not answer at all, by name. */
|
|
53888
|
+
function iFillBanners(id,qs,box){const pre=iPre[id]||[];const held=iHeld[id]||{};const ans=iAnswers[id]||{};const heldN=Object.keys(held).length;
|
|
53889
|
+
if(!pre.length&&!heldN){if(iNote[id]){const nb=mk("div","cds-callout warn pendpanel",iNote[id]);box.appendChild(nb)}return}
|
|
53890
|
+
const missing=qs.filter(q=>!iHasAnswer(ans[q.label])&&!held[q.label]).map(q=>q.label);
|
|
53891
|
+
const src=iSrcWords(id);
|
|
53892
|
+
const banner=mk("div","cds-callout info pendpanel ifillbanner");
|
|
53893
|
+
banner.appendChild(mk("p","",[pre.length+" filled from "+src+". Correct anything wrong.",heldN?heldN+" suggested, waiting for your click.":"",missing.length?missing.length+" not in "+src+": listed below.":"Nothing else is missing."].filter(Boolean).join(" ")));
|
|
53894
|
+
if(missing.length){const ul=mk("ul","cds-list imissing");for(const l of missing)ul.appendChild(mk("li","",l));banner.appendChild(ul)}
|
|
53895
|
+
box.appendChild(banner);
|
|
53896
|
+
if(heldN){const nb=mk("div","cds-callout warn pendpanel cds-stack iheldbanner");
|
|
53897
|
+
nb.appendChild(mk("p","",heldN+" more "+(heldN===1?"answer was":"answers were")+" suggested from "+src+". Check "+(heldN===1?"it":"them")+" and click Use all suggestions, or Use it on each."));
|
|
53898
|
+
const all=mk("button","cds-btn","Use all suggestions");all.type="button";
|
|
53899
|
+
all.onclick=()=>{const left=iUseAllHeld(id);iHeldLeft[id]=left;render(lastState,lastRun)};nb.appendChild(all);
|
|
53900
|
+
if(iHeldLeft[id])nb.appendChild(mk("p","muted",iHeldLeft[id]+" "+(iHeldLeft[id]===1?"suggestion was":"suggestions were")+" not used because "+(iHeldLeft[id]===1?"it":"they")+" would replace an answer already there, or decide"+(iHeldLeft[id]===1?"s":"")+" what the texts may promise. Use it on each to choose."));
|
|
53901
|
+
box.appendChild(nb)}
|
|
53902
|
+
if(iNote[id])box.appendChild(mk("div","cds-callout warn pendpanel",iNote[id]))}
|
|
53903
|
+
let iHeldLeft={};
|
|
53725
53904
|
${ASSESSMENT_PANEL_JS}
|
|
53726
53905
|
let rOpen={},rData={},rBusy={},rSeen={};
|
|
53727
53906
|
let profCache=null,profLoading=false;
|
|
@@ -53909,6 +54088,13 @@ b.textContent=answer?"Yes":"No";
|
|
|
53909
54088
|
let agencyReady=false;
|
|
53910
54089
|
function ncField(label,control,hint,full){const w=mk("div","cds-field"+(full?" ncfull":""));const l=mk("label","",label);l.htmlFor=control.id;w.appendChild(l);
|
|
53911
54090
|
if(hint){const h=mk("div","hint",hint);h.id=control.id+"-hint";control.setAttribute("aria-describedby",h.id);w.appendChild(h)}w.appendChild(control);return w}
|
|
54091
|
+
/* What was typed on Build a client account, by field id (test 01 walkthrough, BUG-2026-09-30-32). The screen
|
|
54092
|
+
draws again when late data arrives (the snapshot list, the agency check), and a sub-account id typed in the
|
|
54093
|
+
first seconds was wiped. Each field takes its typed value back when drawn; a successful create or register
|
|
54094
|
+
clears it. */
|
|
54095
|
+
let ncDraft={};
|
|
54096
|
+
function ncKeep(el){const k=el.id;if(ncDraft[k]!=null)el.value=ncDraft[k];const save=()=>{ncDraft[k]=el.value};el.addEventListener("input",save);el.addEventListener("change",save);return el}
|
|
54097
|
+
function ncForget(ids){for(const k of ids)delete ncDraft[k]}
|
|
53912
54098
|
function newClientForm(){const f=document.createElement("div");f.className="cds-panel ncform";
|
|
53913
54099
|
f.innerHTML="<h2 class='cds-panel-title'>Create the sub-account</h2><p>GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and paste its key into Command OS.</p>";
|
|
53914
54100
|
const g=document.createElement("div");g.className="ncgrid";
|
|
@@ -53918,6 +54104,7 @@ function newClientForm(){const f=document.createElement("div");f.className="cds-
|
|
|
53918
54104
|
const snap=document.createElement("select");snap.id="nc-snapshot";snap.innerHTML='<option value="">No snapshot (blank account)</option>';snap.className="cds-select nccontrol";
|
|
53919
54105
|
if(snapshots)for(const sn of snapshots){const e=document.createElement("option");e.value=sn.id;e.textContent=(sn.type==="own"?"Yours: ":"")+sn.name;snap.appendChild(e)}
|
|
53920
54106
|
const fn=mkIn("nc-first"),ln=mkIn("nc-last"),em=mkIn("nc-email"),city=mkIn("nc-city"),st=mkIn("nc-state"),tz=mkIn("nc-timezone");em.type="email";tz.value=${scriptJson(readAgencyProfile().defaults?.timezone || "")};
|
|
54107
|
+
for(const el of [name,ind,snap,fn,ln,em,city,st,tz])ncKeep(el);
|
|
53921
54108
|
for(const [label,el,hint,full] of [["Business name *",name,"",true],["Industry *",ind,"Tailors this client's intake."],["Snapshot",snap,"",false],["Owner first name",fn],["Owner last name",ln],["Owner email",em,"",true],["City",city],["State",st],["Timezone",tz,"Confirm this client\u2019s timezone for appointment times. For example America/Phoenix.",true]])g.appendChild(ncField(label,el,hint,full));
|
|
53922
54109
|
f.appendChild(g);
|
|
53923
54110
|
if(snapshotError)showClientError(snapshotError,f);
|
|
@@ -53925,7 +54112,7 @@ function newClientForm(){const f=document.createElement("div");f.className="cds-
|
|
|
53925
54112
|
b.onclick=async()=>{if(!ind.value){showClientError("Choose the client industry.",f);return}if(!name.value.trim()){showClientError("Enter the business name, then create the sub-account.",f);return}
|
|
53926
54113
|
b.disabled=true;b.textContent="Creating\u2026";
|
|
53927
54114
|
const r=await(await clientPageRequest("/api/new-client",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:name.value.trim(),industry:ind.value,snapshot_id:snap.value||undefined,first_name:fn.value.trim()||undefined,last_name:ln.value.trim()||undefined,email:em.value.trim()||undefined,city:city.value.trim()||undefined,state:st.value.trim()||undefined,timezone:tz.value.trim()||undefined})})).json();
|
|
53928
|
-
if(r.ok){canCreateSubAccounts=true;planNotice="";registerNotice="";chooseIntakeClient({locationId:r.locationId,name:r.name,pending:!r.registered})}
|
|
54115
|
+
if(r.ok){ncForget(["nc-name","nc-industry","nc-snapshot","nc-first","nc-last","nc-email","nc-city","nc-state","nc-timezone"]);canCreateSubAccounts=true;planNotice="";registerNotice="";chooseIntakeClient({locationId:r.locationId,name:r.name,pending:!r.registered})}
|
|
53929
54116
|
else{if(r.canCreateSubAccounts===false){canCreateSubAccounts=false;planNotice=r.error;render(lastState,lastRun)}
|
|
53930
54117
|
else showClientError(r.error,f,"GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and paste its key into Command OS.");b.disabled=false;b.textContent="Create sub-account"}};
|
|
53931
54118
|
b.disabled=!agencyReady;
|
|
@@ -53936,7 +54123,8 @@ function pasteKeyCard(quiet){const card=document.createElement("section");card.c
|
|
|
53936
54123
|
const help=document.createElement("p");help.textContent="Create the sub-account in GoHighLevel, then paste its Private Integration key here.";card.appendChild(help);
|
|
53937
54124
|
const grid=document.createElement("div");grid.className="ncgrid";card.appendChild(grid);
|
|
53938
54125
|
const field=(text,id)=>{const input=document.createElement("input");input.id=id;input.className="cds-input nccontrol";input.setAttribute("aria-label",text);grid.appendChild(ncField(text,input));return input};
|
|
53939
|
-
|
|
54126
|
+
/* The id is kept across a redraw; the key never is, in memory or anywhere else (review LOW 9). */
|
|
54127
|
+
const id=ncKeep(field("Sub-account id","register-client-id")),key=field("Private Integration key","register-client-key");key.type="password";key.autocomplete="off";key.setAttribute("spellcheck","false");key.setAttribute("data-lpignore","true");key.disabled=registerBusy;id.disabled=registerBusy;
|
|
53940
54128
|
// Paths live in ghl-click-paths.ts (key path checked 2026-09-06 through Basic info, and 2026-09-29 with the
|
|
53941
54129
|
// Location ID in Business Profile). Scopes step exact labels: [to confirm live]; do not name them here.
|
|
53942
54130
|
const helpDetails=document.createElement("details");helpDetails.className="cds-details";
|
|
@@ -53951,7 +54139,7 @@ function pasteKeyCard(quiet){const card=document.createElement("section");card.c
|
|
|
53951
54139
|
const r=await(await clientPageRequest("/api/register-client",{method:"POST",headers:{"Content-Type":"application/json"},body:body})).json();
|
|
53952
54140
|
key.value="";key.disabled=false;id.disabled=false;registerBusy=false;registerFailed=!r.ok;
|
|
53953
54141
|
registerNotice=r.ok?r.message:(r.error||"The sub-account could not be registered; check the connection and try again.");
|
|
53954
|
-
if(r.ok){planNotice="";registerNextId=String(r.locationId||"");chooseIntakeClient({locationId:r.locationId,name:r.name})}
|
|
54142
|
+
if(r.ok){ncForget(["register-client-id"]);planNotice="";registerNextId=String(r.locationId||"");chooseIntakeClient({locationId:r.locationId,name:r.name})}
|
|
53955
54143
|
else{const shown=document.getElementById("register-client-status")||status,action=document.getElementById("register-client-button")||button;shown.setAttribute("role","alert");shown.textContent=registerNotice;const shownKey=document.getElementById("register-client-key"),shownId=document.getElementById("register-client-id");if(shownKey){shownKey.value="";shownKey.disabled=false}if(shownId)shownId.disabled=false;action.disabled=false;action.textContent="Register"}};
|
|
53956
54144
|
/* Enter in either field submits, as a form would (rehearsal 5, BUG-2026-09-30-18): the key was
|
|
53957
54145
|
pasted, Register never clicked, and nothing said so. The button's own handler does the work. */
|
|
@@ -54033,6 +54221,32 @@ function staffCard(c,state){const box=document.createElement("section");box.clas
|
|
|
54033
54221
|
for(const line of staffSaid[c.locationId])said.appendChild(mk("p","",line));
|
|
54034
54222
|
const done=mk("button","cds-btn ghost","Done");done.type="button";done.onclick=()=>{delete staffSaid[c.locationId];said.remove()};said.appendChild(done);box.appendChild(said)}
|
|
54035
54223
|
return box}
|
|
54224
|
+
/* Is a submitted intake in for this client? From the intake as last read here, else as the server last read it. */
|
|
54225
|
+
function intakeIsIn(id,state){const d=iData[id];if(d&&d.ok)return d.status==="received";return (((state||{}).intakeInputs||{})[id]||{}).status==="received"}
|
|
54226
|
+
/* The Complete the intake button, its status chip and, when open, the intake itself (one copy, drawn where the card puts it). */
|
|
54227
|
+
let iUnder={};
|
|
54228
|
+
function intakeEntry(c,div,under){const ir=document.createElement("div");ir.className="cds-row";ir.id="intake-"+c.locationId;
|
|
54229
|
+
const ib2=document.createElement("button");ib2.className="cds-btn ghost";ib2.textContent=iOpen[c.locationId]?"Close the intake":"Complete the intake";ib2.setAttribute("aria-expanded",String(Boolean(iOpen[c.locationId])));
|
|
54230
|
+
ib2.onclick=async()=>{iOpen[c.locationId]=!iOpen[c.locationId];if(iOpen[c.locationId]&&under)iUnder[c.locationId]=true;if(!iOpen[c.locationId]){iRetryStop(c.locationId);delete iUnder[c.locationId]}if(iOpen[c.locationId]&&!iData[c.locationId])await iLoad(c.locationId);else render(lastState,lastRun)};
|
|
54231
|
+
ir.appendChild(ib2);
|
|
54232
|
+
const dd=iData[c.locationId];
|
|
54233
|
+
if(dd&&dd.ok){const ic=document.createElement("span");ic.className="cds-chip istatus "+(dd.status==="received"?"ok":dd.status==="sent"?"warn":"");
|
|
54234
|
+
ic.textContent=dd.status==="received"?("ANSWERS IN"+(dd.channel==="form"?" \xB7 FORM":dd.channel==="cockpit"?" \xB7 SAVED HERE":"")):dd.status==="sent"?"SENT":"NOT STARTED";
|
|
54235
|
+
ic.title=dd.channel==="form"?"A real form submission, visible in GHL's Submissions and re-readable later.":dd.channel==="cockpit"?"Saved from Command OS onto a tagged contact (no form submission).":"";ir.appendChild(ic)}
|
|
54236
|
+
div.appendChild(ir);
|
|
54237
|
+
if(iOpen[c.locationId])div.appendChild(unnest(intakeWorkspace(c.locationId,c.name)))}
|
|
54238
|
+
/* Opens the intake where the card draws it and scrolls to it (test 01, BUG-2026-09-30-30/-31). A first opening
|
|
54239
|
+
goes straight to From a call or a document, the fastest way to fill it. */
|
|
54240
|
+
/* The scroll waits for the redraw (BUG-2026-09-30-39): a smooth scroll started before the intake had drawn,
|
|
54241
|
+
and the redraw that followed (its document list arriving) put the page back where it was. For a moment
|
|
54242
|
+
after the click, every redraw ends by bringing the intake into view, instantly. */
|
|
54243
|
+
let iScrollTo=null;const I_SCROLL_MS=2500;
|
|
54244
|
+
function iScrollAfterRender(){const p=iScrollTo;if(!p)return;if(Date.now()>p.until){iScrollTo=null;return}
|
|
54245
|
+
const t=document.getElementById("intake-"+p.id);if(!t)return;try{t.scrollIntoView({block:"start"})}catch(e){}
|
|
54246
|
+
if(!p.focused){p.focused=true;const f=t.querySelector("button");if(f)try{f.focus({preventScroll:true})}catch(e){}}}
|
|
54247
|
+
function openIntake(id){iOpen[id]=true;iUnder[id]=true;if(!iMode[id])iMode[id]="transcript";iScrollTo={id:id,until:Date.now()+I_SCROLL_MS};
|
|
54248
|
+
const go=()=>{render(lastState,lastRun);requestAnimationFrame(iScrollAfterRender)};
|
|
54249
|
+
if(!iData[id])iLoad(id).then(go,go);else go()}
|
|
54036
54250
|
function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES.map(()=>"pending")}).stages;
|
|
54037
54251
|
const sandbox=typeOf(state,c.locationId)==="sandbox";
|
|
54038
54252
|
// A template or hidden account opens only so its owner can lock or unlock it and change its
|
|
@@ -54070,6 +54284,12 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
54070
54284
|
const liveI=run&&run.running&&run.locationId===c.locationId&&typeof run.stage==="number"?run.stage:-1;
|
|
54071
54285
|
const isDone=i=>(st[i]||"pending")==="done"&&!staleOf(i)&&i!==liveI;
|
|
54072
54286
|
let curI=-1;for(let i=0;i<STAGES.length;i++){if(!isDone(i)){curI=i;break}}
|
|
54287
|
+
/* Test 01 walkthrough (BUG-2026-09-30-31): while Brief is next and no intake is in, the intake is the
|
|
54288
|
+
next job, so its entry point sits right under the steps instead of four screens down. */
|
|
54289
|
+
const intakeNext=!parkedType&&curI===1&&st[0]==="done"&&!intakeIsIn(c.locationId,state);
|
|
54290
|
+
/* Opened under the steps, it stays there while it is open, after a submit too (review LOW 7). */
|
|
54291
|
+
const intakeUp=!parkedType&&st[0]==="done"&&(intakeNext||Boolean(iUnder[c.locationId]&&iOpen[c.locationId]));
|
|
54292
|
+
const intakeFirst=intakeNext;
|
|
54073
54293
|
/* A step still waiting on the owner's approval is not counted as done, so the count matches its "Your call" chip. */
|
|
54074
54294
|
const nDone=STAGES.filter((_,i)=>isDone(i)&&!(i===2&&!(state.buildApproved||{})[c.locationId])).length;
|
|
54075
54295
|
// Where it stands, in words; each row's pin repeats its state and its chip names it.
|
|
@@ -54137,6 +54357,10 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
54137
54357
|
/* What the last run found, in owner words: a warn chip counting the issues with its See details toggle;
|
|
54138
54358
|
Verify's own verdict; the plain summary; a raw system note only under Details. */
|
|
54139
54359
|
const mkNote=()=>{let note=null;
|
|
54360
|
+
/* Brief's own run found no intake (review LOW 8): the gate's sentence, and its button unless the next step already shows one. */
|
|
54361
|
+
if(!live&&oc&&i===1&&oc.reason==="intake-needed"){note=mk("span","snote sintakenote",${scriptJson(BRIEF_NEEDS_INTAKE2)});
|
|
54362
|
+
if(!intakeFirst){const go=mk("button","cds-btn ghost srunintake","Complete the intake");go.type="button";go.onclick=e=>{e.stopPropagation();openIntake(c.locationId)};note.appendChild(document.createTextNode(" "));note.appendChild(go)}
|
|
54363
|
+
return note}
|
|
54140
54364
|
if(!live&&oc&&(oc.summary||(oc.issues||[]).length)){
|
|
54141
54365
|
const passed=i===4&&oc.ok===true,plainSaid=Boolean(oc.plain&&oc.plain.summary);
|
|
54142
54366
|
if(!passed&&nIss){note=mk("span","snote");if(!doneWithIssues&&!staleIssues)note.appendChild(chip("warn",nIss+" "+plural(nIss,"issue")));const see=mk("button","cds-link snote-see","See details");see.type="button";see.onclick=toggleIssues;note.appendChild(see);return note}
|
|
@@ -54182,6 +54406,8 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
54182
54406
|
rnShow(btn,runKeyOf,null);const j=await postRun();
|
|
54183
54407
|
if(j&&j.ok){load();return}
|
|
54184
54408
|
if(i===1&&j&&j.reason==="intake-missing"){rnShow(btn,runKeyOf,{wait:true,waitId:c.locationId,text:BRIEF_WAIT_TEXT});await briefWait(c.locationId,runKeyOf,postRun,j.error);return}
|
|
54409
|
+
/* One Complete the intake button (review nit): the line carries it only when the next step does not. */
|
|
54410
|
+
if(i===1&&j&&j.reason==="intake-needed"){rnShow(btn,runKeyOf,intakeFirst?{text:runFailText(j)}:{text:runFailText(j),openIntake:c.locationId});load();return}
|
|
54185
54411
|
rnShow(btn,runKeyOf,{text:runFailText(j),wasDone:wasDone});load()};
|
|
54186
54412
|
const mkRun=(cls,label)=>{const b=document.createElement("button");b.className=cls;
|
|
54187
54413
|
const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":label;
|
|
@@ -54197,11 +54423,13 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
54197
54423
|
const h3=document.createElement("h3");h3.className="cds-panel-title";h3.textContent=parkedType?"Nothing is built into this account":(i+1)+". "+LABELS[i];main.appendChild(h3);
|
|
54198
54424
|
const pa=document.createElement("p");pa.textContent=parkedType?"It opens here so you can lock or unlock it, or change its type to Client to build it.":stepAbout(i,c,true);main.appendChild(pa);
|
|
54199
54425
|
if(mustChoose&&!parkedType){const pc=document.createElement("p");pc.className="swarn";pc.textContent="First choose what the client bought. Only those parts get built.";main.appendChild(pc)}
|
|
54426
|
+
if(i===1&&intakeFirst&&!live&&!(oc&&oc.reason==="intake-needed")){main.appendChild(mk("p","swarn sintake",${scriptJson(BRIEF_NEEDS_INTAKE2)}))}
|
|
54200
54427
|
if(live)main.appendChild(mkLive());
|
|
54201
54428
|
if(stale&&!live){const w=document.createElement("div");w.className="cds-row";w.appendChild(chip(chipOf[1],chipOf[0]));w.appendChild(mk("span","swarn",stale.reason));main.appendChild(w)}
|
|
54202
54429
|
const pn=mkNote();if(pn){const w=document.createElement("div");w.className="snoteline";w.appendChild(pn);main.appendChild(w)}
|
|
54203
54430
|
const btns=document.createElement("div");btns.className="sbtns";
|
|
54204
54431
|
if(gateOpen&&!anyBusy&&!parkedType)btns.appendChild(mkGate("cds-btn gatebtn sbig"));
|
|
54432
|
+
if(i===1&&intakeFirst&&!live&&!iOpen[c.locationId]){const ib=mk("button","cds-btn sbig sintakebtn","Complete the intake");ib.type="button";ib.onclick=ev=>{ev.stopPropagation();openIntake(c.locationId)};btns.appendChild(ib)}
|
|
54205
54433
|
if(canBuild&&POWERED.includes(i))btns.appendChild(mkRun("cds-btn runbtn sbig","Run "+LABELS[i]));
|
|
54206
54434
|
if(!parkedType&&canBuild&&POWERED.includes(i)&&run&&run.running&&run.locationId!==c.locationId){const why=document.createElement("p");why.className="muted swhy";why.textContent="A step is running for another client. This one can start when it finishes.";btns.appendChild(why)}
|
|
54207
54435
|
if(!btns.children.length&&!live&&!parkedType){const why=document.createElement("p");why.className="muted swhy";
|
|
@@ -54219,6 +54447,8 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
54219
54447
|
const needsN=(curI<0||curLive?0:1)+approvalWaits;const afterN=Math.max(0,after-approvalWaits);
|
|
54220
54448
|
s.textContent=nDone+" done"+(curI<0&&!approvalWaits?"":(curLive?" \xB7 1 running now":"")+(needsN?" \xB7 "+needsN+" needs you":"")+(curI<0?"":" \xB7 "+afterN+" after that"));plabel.appendChild(s)}
|
|
54221
54449
|
div.appendChild(row);
|
|
54450
|
+
/* Closed, the next step's own Complete the intake button is the one way in (review nit: one button); open, it sits here. */
|
|
54451
|
+
if(intakeUp&&iOpen[c.locationId])intakeEntry(c,div,true);
|
|
54222
54452
|
for(let i=0;i<STAGES.length;i++){if(oOpen[c.locationId+":"+i]){const op=outcomePanel(c.locationId,i,state);if(op)div.appendChild(unnest(op))}}
|
|
54223
54453
|
const view=(state.buildViews||{})[c.locationId];
|
|
54224
54454
|
// The plan is reference material, not the page. Printed open it was 33,759 px of
|
|
@@ -54324,16 +54554,7 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
54324
54554
|
rb.onclick=async()=>{rOpen[c.locationId]=!rOpen[c.locationId];rSeen[c.locationId]=true;if(rOpen[c.locationId]&&!rData[c.locationId])await rLoad(c.locationId);else render(lastState,lastRun)};
|
|
54325
54555
|
rr.appendChild(rb);div.appendChild(rr);
|
|
54326
54556
|
if(rOpen[c.locationId])div.appendChild(unnest(reviewPanel(c.locationId,c.name,state,run)))}
|
|
54327
|
-
if(st[0]==="done")
|
|
54328
|
-
const ib2=document.createElement("button");ib2.className="cds-btn ghost";ib2.textContent=iOpen[c.locationId]?"Close the intake":"Complete the intake";ib2.setAttribute("aria-expanded",String(Boolean(iOpen[c.locationId])));
|
|
54329
|
-
ib2.onclick=async()=>{iOpen[c.locationId]=!iOpen[c.locationId];if(!iOpen[c.locationId])iRetryStop(c.locationId);if(iOpen[c.locationId]&&!iData[c.locationId])await iLoad(c.locationId);else render(lastState,lastRun)};
|
|
54330
|
-
ir.appendChild(ib2);
|
|
54331
|
-
const dd=iData[c.locationId];
|
|
54332
|
-
if(dd&&dd.ok){const ic=document.createElement("span");ic.className="cds-chip istatus "+(dd.status==="received"?"ok":dd.status==="sent"?"warn":"");
|
|
54333
|
-
ic.textContent=dd.status==="received"?("ANSWERS IN"+(dd.channel==="form"?" \xB7 FORM":dd.channel==="cockpit"?" \xB7 SAVED HERE":"")):dd.status==="sent"?"SENT":"NOT STARTED";
|
|
54334
|
-
ic.title=dd.channel==="form"?"A real form submission, visible in GHL's Submissions and re-readable later.":dd.channel==="cockpit"?"Saved from Command OS onto a tagged contact (no form submission).":"";ir.appendChild(ic)}
|
|
54335
|
-
div.appendChild(ir);
|
|
54336
|
-
if(iOpen[c.locationId])div.appendChild(unnest(intakeWorkspace(c.locationId,c.name)))}
|
|
54557
|
+
if(st[0]==="done"&&!intakeUp)intakeEntry(c,div,false);
|
|
54337
54558
|
if(run&&run.locationId===c.locationId&&(run.running||run.lines&&run.lines.length)){
|
|
54338
54559
|
const bp=buildPanel(run);
|
|
54339
54560
|
if(bp){div.appendChild(unnest(bp));
|
|
@@ -54431,6 +54652,8 @@ function icLive(key){return [...document.querySelectorAll(".ic-panel")].filter(x
|
|
|
54431
54652
|
let runNote={},rnQueue=[];
|
|
54432
54653
|
function runFailText(j){return (j&&j.error)||"Command OS could not confirm the result. Refresh this client to read its saved state before retrying."}
|
|
54433
54654
|
function rnEl(key){const n=runNote[key];const p=mk("p",n.wait?"muted srunnote":"cds-callout warn srunnote",n.text);p.dataset.rn=key;p.setAttribute("role",n.wait?"status":"alert");
|
|
54655
|
+
/* Brief refused for want of an intake (test 01, BUG-2026-09-30-30): the line carries the way to the intake panel. */
|
|
54656
|
+
if(n.openIntake){const go=mk("button","cds-btn ghost srunintake","Complete the intake");go.type="button";go.onclick=e=>{e.stopPropagation();openIntake(n.openIntake)};p.appendChild(document.createTextNode(" "));p.appendChild(go)}
|
|
54434
54657
|
if(n.wait&&n.waitId){const stop=mk("button","cds-link srunstop","Stop waiting");stop.type="button";stop.onclick=e=>{e.stopPropagation();briefStop(n.waitId,key)};p.appendChild(document.createTextNode(" "));p.appendChild(stop)}
|
|
54435
54658
|
return p}
|
|
54436
54659
|
function rnLive(key){return [...document.querySelectorAll(".srunnote")].filter(x=>x.dataset.rn===key)}
|
|
@@ -54947,7 +55170,7 @@ function restoreView(v){const m=document.getElementById("main");if(!v||!m)return
|
|
|
54947
55170
|
if(v.focus){const el=[...m.querySelectorAll(v.focus.tag)].filter(x=>fieldKey(x,m)===v.focus.key)[v.focus.n];
|
|
54948
55171
|
if(el&&document.activeElement!==el){try{el.focus({preventScroll:true})}catch(e){}try{if(v.focus.st!=null)el.setSelectionRange(v.focus.st,v.focus.en)}catch(e){}}}
|
|
54949
55172
|
if(window.scrollX!==v.x||window.scrollY!==v.y){try{window.scrollTo(v.x,v.y)}catch(e){}}}
|
|
54950
|
-
function render(state,run){const v=captureView();try{if(state)icPrune(state);rnPrune(state,run);renderPage(state,run);icFlush();rnFlush()}finally{lastDrawnSig=dataSig(state,run);restoreView(v)}}
|
|
55173
|
+
function render(state,run){const v=captureView();try{if(state)icPrune(state);rnPrune(state,run);renderPage(state,run);icFlush();rnFlush()}finally{lastDrawnSig=dataSig(state,run);restoreView(v);iScrollAfterRender()}}
|
|
54951
55174
|
function renderPage(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...state.roster)}syncReviews(state);lastState=state;lastRun=run;
|
|
54952
55175
|
if(state&&state.protectedAccounts)PROTECTED=state.protectedAccounts;const m=document.getElementById("main");m.innerHTML="";
|
|
54953
55176
|
/* Four screens share this page's data and differ only in what they show. */
|
|
@@ -56362,7 +56585,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
56362
56585
|
if (!c) throw new Error(INTAKE_UNREADABLE);
|
|
56363
56586
|
const snap = await readIntakeSnapshot(c, locationId2);
|
|
56364
56587
|
if (snap.status === "unavailable") throw intakeUnavailableError(snap);
|
|
56365
|
-
if (snap.status !== "received") throw new Error(
|
|
56588
|
+
if (snap.status !== "received") throw new Error(BRIEF_NEEDS_INTAKE2);
|
|
56366
56589
|
const required = composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }).filter((q3) => q3.required && !String(normalizedAnswers?.[q3.label] ?? normalizedAnswers?.[q3.key] ?? "").trim());
|
|
56367
56590
|
if (required.length) throw new Error("Complete these required intake answers: " + required.map((q3) => q3.label).join(", ") + ".");
|
|
56368
56591
|
}
|
|
@@ -56461,7 +56684,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
56461
56684
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
56462
56685
|
logQuietly(`client-intake ${new URL(req.url || "/", "http://x").searchParams.get("locationId") || ""}: ${e instanceof Error ? e.message : String(e)}`);
|
|
56463
56686
|
if (e instanceof Error && e.message === INTAKE_READ_TRANSIENT) res.end(JSON.stringify({ ok: false, transient: true, error: INTAKE_REFRESH_RETRYING }));
|
|
56464
|
-
else res.end(JSON.stringify(
|
|
56687
|
+
else res.end(JSON.stringify(intakeRefusal(e)));
|
|
56465
56688
|
}
|
|
56466
56689
|
})();
|
|
56467
56690
|
return;
|
|
@@ -56482,7 +56705,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
56482
56705
|
res.end(JSON.stringify({ ok: true, url: pf.url, leftovers: pf.leftovers }));
|
|
56483
56706
|
} catch (e) {
|
|
56484
56707
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
56485
|
-
|
|
56708
|
+
logQuietly(`${url}: ${e instanceof Error ? e.message : String(e)}`);
|
|
56709
|
+
res.end(JSON.stringify(intakeRefusal(e)));
|
|
56486
56710
|
}
|
|
56487
56711
|
});
|
|
56488
56712
|
return;
|
|
@@ -56519,17 +56743,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
56519
56743
|
if (ready.smoked) recordRunCost({ at: (/* @__PURE__ */ new Date()).toISOString(), locationId: args.locationId, stage: 0, kind: "engine-check", ok: ready.ok, costUsd: ready.smokeCostUsd });
|
|
56520
56744
|
if (!ready.ok) throw new Error(engineRepair(ready.version));
|
|
56521
56745
|
if (typeof args.transcript === "string" && args.transcript.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
|
|
56522
|
-
|
|
56523
|
-
if (args.file) {
|
|
56524
|
-
const name = safeDocName(args.file);
|
|
56525
|
-
const dir = clientDocsDir(args.locationId);
|
|
56526
|
-
const p = path40.join(dir, name);
|
|
56527
|
-
if (!fs42.existsSync(p)) throw new Error("that document is not saved for this client");
|
|
56528
|
-
if (fs42.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error("that document is not saved for this client");
|
|
56529
|
-
const text2 = fs42.readFileSync(p, "utf8");
|
|
56530
|
-
if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
|
|
56531
|
-
fileSource = { kind: "text", text: text2 };
|
|
56532
|
-
}
|
|
56746
|
+
const fileSource = args.file ? { kind: "text", text: readSavedClientDoc(args.locationId, args.file) } : null;
|
|
56533
56747
|
const source = fileSource ? fileSource : args.url ? { kind: "url", url: String(args.url) } : args.instruction ? { kind: "recorder", instruction: String(args.instruction) } : { kind: "text", text: String(args.transcript ?? "") };
|
|
56534
56748
|
const toolContext = { servers: [] };
|
|
56535
56749
|
if (source.kind === "recorder") {
|
|
@@ -56613,7 +56827,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
56613
56827
|
return;
|
|
56614
56828
|
} catch (e) {
|
|
56615
56829
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
56616
|
-
|
|
56830
|
+
logQuietly(`${url}: ${e instanceof Error ? e.message : String(e)}`);
|
|
56831
|
+
res.end(JSON.stringify(intakeRefusal(e)));
|
|
56617
56832
|
}
|
|
56618
56833
|
});
|
|
56619
56834
|
return;
|
|
@@ -57526,7 +57741,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
57526
57741
|
process.on("SIGTERM", stop2);
|
|
57527
57742
|
});
|
|
57528
57743
|
}
|
|
57529
|
-
var import_zod67, import_node_util5, fs42, path40, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, SCOPE_REFUSAL, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanOnce, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
57744
|
+
var import_zod67, import_node_util5, fs42, path40, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, SCOPE_REFUSAL, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanOnce, DOC_NOT_SAVED, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, 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, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
57530
57745
|
var init_dashboard = __esm({
|
|
57531
57746
|
"src/dashboard.ts"() {
|
|
57532
57747
|
"use strict";
|
|
@@ -57668,6 +57883,7 @@ var init_dashboard = __esm({
|
|
|
57668
57883
|
DOC_EXTS = /* @__PURE__ */ new Set([".md", ".txt", ".vtt", ".srt", ".csv"]);
|
|
57669
57884
|
DOC_MAX_BYTES = 20 * 1024 * 1024;
|
|
57670
57885
|
queueScanOnce = scanGate();
|
|
57886
|
+
DOC_NOT_SAVED = "That document is not saved for this client any more. Choose it again from the list, or add it again.";
|
|
57671
57887
|
PARKED_NOT_BUILT = "Template and hidden accounts are not built from Command OS. Change the account's type to Client first.";
|
|
57672
57888
|
NEEDS_AGENCY_KEY = "No agency key on file. Run register_agency_key in GHL Command first.";
|
|
57673
57889
|
INTAKE_UNREADABLE = "Command OS could not read this client's account. Paste its key again in Build a client account.";
|
|
@@ -57676,6 +57892,7 @@ var init_dashboard = __esm({
|
|
|
57676
57892
|
INTAKE_ANSWERS_MISSING = "The intake form shows a submission, but no contact in this client's account has the answers yet. If the client has not submitted the form, send it again. If they have, press Refresh status in a minute.";
|
|
57677
57893
|
INTAKE_READ_FAILED = "The intake could not be read. Press Refresh status to try again.";
|
|
57678
57894
|
RUN_IN_PROGRESS = "a run is already in progress";
|
|
57895
|
+
BRIEF_NEEDS_INTAKE2 = BRIEF_NEEDS_INTAKE;
|
|
57679
57896
|
BUSINESS_TYPE_FOR_INDUSTRY = { "med-spa": "Med spa", "wellness-clinic": "Wellness clinic", clinic: "Clinic / practice", coach: "Coach / consultant", "local-service": "Local service", ecommerce: "Ecommerce", agency: "Agency" };
|
|
57680
57897
|
INTAKE_FORM_REQUIRED = "Run the intake step in Client builds to create the form, then come back and submit.";
|
|
57681
57898
|
INTAKE_CONNECT_REQUIRED = "Connect this sub-account by pasting its Private Integration key in Command OS.";
|
|
@@ -57691,7 +57908,9 @@ var init_dashboard = __esm({
|
|
|
57691
57908
|
INTAKE_FORM_REQUIRED,
|
|
57692
57909
|
"Confirm the contact creation and email before sending the questionnaire.",
|
|
57693
57910
|
"Choose a registered client before saving the intake.",
|
|
57694
|
-
"Enter intake answers as text or a list of choices."
|
|
57911
|
+
"Enter intake answers as text or a list of choices.",
|
|
57912
|
+
DOC_NOT_SAVED,
|
|
57913
|
+
"Only plain text (.txt, .md, .csv), .vtt and .srt files can be saved here."
|
|
57695
57914
|
];
|
|
57696
57915
|
currentRosterRef = () => [];
|
|
57697
57916
|
staffGateRef = async () => null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.89.0-beta.
|
|
3
|
+
"version": "3.89.0-beta.7",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
5
|
"description": "GoHighLevel MCP Server for Claude. 250 tools — full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
6
6
|
"main": "dist/index.js",
|