@elitedcs/ghl-mcp 3.84.8 → 3.84.10
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 +10 -0
- package/README.md +1 -1
- package/dist/index.js +181 -37
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.84.10 — confirming a call's numbers no longer files the call twice
|
|
4
|
+
|
|
5
|
+
When you answered "confirm all" after bringing in a client call, the call was filed a second time: a duplicate call file marked "(2)" and the same call added to "Client understanding" twice. Now the confirmation adds one dated "Confirmed by you" section to the call you already filed, and to Client understanding, listing the numbers that saved. If the first call was never filed, the whole call is filed then, so nothing is lost. The reply says the numbers saved only when they actually did.
|
|
6
|
+
|
|
7
|
+
## 3.84.9 — build steps you can read
|
|
8
|
+
|
|
9
|
+
Each client's build now shows a progress bar, the next step across the full width with one clear button, and one readable row per step, instead of seven narrow columns. Nothing about how building works changed: the same steps, checks and typed confirmations. Step descriptions now say what each step really does. Verify checks the links between workflows, pipelines and fields and tests the form, and reports anything it cannot check.
|
|
10
|
+
|
|
11
|
+
Call recordings: Plaud is proven and Zoom should work the same way but has not been checked yet. For any other recorder, export the transcript and attach it.
|
|
12
|
+
|
|
3
13
|
## 3.84.8 — client calls into assessments, and locks you can always reach
|
|
4
14
|
|
|
5
15
|
Bring a recorded client call into their assessment in one step. Your Claude fetches the transcript from its own Plaud or Zoom connection (or you attach the file), proposes answers with the client's exact words, and read_assessment_transcript now saves them to the client's contact and reads them back. Answers in the client's own words save straight away. Every number waits for your OK in one short list: reply "confirm all" or "confirm all except …", and those save marked as confirmed by you. A needs analysis comes with it: what they said they want, what they actually need, problems they didn't name, and solutions they didn't know to ask for, each tied to their own words and written for you, never sent to the client. The reply also lays out a client file for your Google Drive (Clients / the client / Calls, plus a running "Client understanding" document), and can keep a copy in a local folder you choose.
|
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -29058,7 +29058,7 @@ var require_package = __commonJS({
|
|
|
29058
29058
|
"package.json"(exports2, module2) {
|
|
29059
29059
|
module2.exports = {
|
|
29060
29060
|
name: "@elitedcs/ghl-mcp",
|
|
29061
|
-
version: "3.84.
|
|
29061
|
+
version: "3.84.10",
|
|
29062
29062
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
29063
29063
|
description: "GoHighLevel MCP Server for Claude. 250 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
29064
29064
|
main: "dist/index.js",
|
|
@@ -39495,7 +39495,8 @@ __export(client_calls_exports, {
|
|
|
39495
39495
|
gateAnalysis: () => gateAnalysis,
|
|
39496
39496
|
processClientCall: () => processClientCall,
|
|
39497
39497
|
safeName: () => safeName,
|
|
39498
|
-
writeClientFileLocally: () => writeClientFileLocally
|
|
39498
|
+
writeClientFileLocally: () => writeClientFileLocally,
|
|
39499
|
+
writeConfirmationLocally: () => writeConfirmationLocally
|
|
39499
39500
|
});
|
|
39500
39501
|
function gateAnalysis(transcript, analysis) {
|
|
39501
39502
|
const rejected = [];
|
|
@@ -39722,6 +39723,37 @@ function writeClientFileLocally(baseDir, file2) {
|
|
|
39722
39723
|
return { written: false, error: `could not write the client file: ${e?.message ?? String(e)}` };
|
|
39723
39724
|
}
|
|
39724
39725
|
}
|
|
39726
|
+
function writeConfirmationLocally(baseDir, file2) {
|
|
39727
|
+
const c = file2.confirmation;
|
|
39728
|
+
if (!c) return writeClientFileLocally(baseDir, file2);
|
|
39729
|
+
try {
|
|
39730
|
+
if (!path32.isAbsolute(baseDir)) return { written: false, error: `the client files folder must be a full path, not "${baseDir}".` };
|
|
39731
|
+
const base = path32.resolve(baseDir);
|
|
39732
|
+
const callPath = path32.resolve(base, ...file2.call.folderParts, c.callName);
|
|
39733
|
+
const understandingPath = path32.resolve(base, ...file2.understanding.folderParts, file2.understanding.name);
|
|
39734
|
+
if (!inside(base, callPath) || !inside(base, understandingPath)) return { written: false, error: "the client folder name would land outside the client files folder." };
|
|
39735
|
+
let st;
|
|
39736
|
+
try {
|
|
39737
|
+
st = fs34.lstatSync(callPath);
|
|
39738
|
+
} catch {
|
|
39739
|
+
}
|
|
39740
|
+
if (!st) return writeClientFileLocally(baseDir, { ...file2, confirmation: void 0 });
|
|
39741
|
+
if (st.isSymbolicLink() || !st.isFile() || st.nlink > 1) return { written: false, error: `${callPath} is a link, not a plain file, so nothing was added to it.` };
|
|
39742
|
+
if (!inside(fs34.realpathSync(base), fs34.realpathSync(callPath))) return { written: false, error: "the call file resolves outside the client files folder, so nothing was added." };
|
|
39743
|
+
fs34.appendFileSync(callPath, c.appendToCall, { encoding: "utf8", mode: 384 });
|
|
39744
|
+
let u;
|
|
39745
|
+
try {
|
|
39746
|
+
u = fs34.lstatSync(understandingPath);
|
|
39747
|
+
} catch {
|
|
39748
|
+
}
|
|
39749
|
+
if (u && (u.isSymbolicLink() || !u.isFile() || u.nlink > 1)) return { written: false, error: `${understandingPath} is a link, not a plain file, so it was not appended to. The call file was updated: ${callPath}` };
|
|
39750
|
+
if (u) fs34.appendFileSync(understandingPath, c.appendToUnderstanding, { encoding: "utf8", mode: 384 });
|
|
39751
|
+
else fs34.writeFileSync(understandingPath, file2.understanding.headerIfNew + c.appendToUnderstanding, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
39752
|
+
return { written: true, callFile: callPath, transcriptFile: "", understandingFile: understandingPath };
|
|
39753
|
+
} catch (e) {
|
|
39754
|
+
return { written: false, error: `could not add the confirmation to the client file: ${e?.message ?? String(e)}` };
|
|
39755
|
+
}
|
|
39756
|
+
}
|
|
39725
39757
|
function contactNames(raw) {
|
|
39726
39758
|
const c = raw?.contact ?? {};
|
|
39727
39759
|
const s = (k) => typeof c[k] === "string" && c[k].trim() ? c[k].trim() : "";
|
|
@@ -39916,8 +39948,32 @@ To confirm:
|
|
|
39916
39948
|
${confirmList(gated.held).map((l) => `- ${l}`).join("\n")}
|
|
39917
39949
|
${CONFIRM_INSTRUCTION}`;
|
|
39918
39950
|
const clientName = save?.businessName ?? save?.contactName ?? clean(input.businessName);
|
|
39951
|
+
const confirmPass = (input.confirmed?.length ?? 0) > 0;
|
|
39919
39952
|
const clientFile = clientName ? buildClientFile({ clientName, date, source: input.source, gated, analysis, transcript: input.transcript, saveLine, confirmedByOwner: save?.confirmedByOwner }) : null;
|
|
39920
|
-
|
|
39953
|
+
if (clientFile && confirmPass) {
|
|
39954
|
+
const today2 = now.toISOString().slice(0, 10);
|
|
39955
|
+
const confirmedIds = new Set(save?.confirmedByOwner ?? []);
|
|
39956
|
+
const lines = save?.status === "SAVED" ? gated.accepted.filter((a) => confirmedIds.has(a.questionId)).map((a) => `${ASK.get(a.questionId) ?? a.questionId} **${a.value}**. They said: ${q2(a.quote)}`) : [];
|
|
39957
|
+
const body = lines.length ? `Saved on the GoHighLevel contact ${save?.contactId}:
|
|
39958
|
+
${bullet(lines)}` : `Not saved on the contact: ${save?.reason ?? save?.status ?? "no contact was named"}.`;
|
|
39959
|
+
clientFile.confirmation = {
|
|
39960
|
+
callName: clientFile.call.name,
|
|
39961
|
+
appendToCall: `
|
|
39962
|
+
|
|
39963
|
+
## Confirmed by you, ${today2}
|
|
39964
|
+
|
|
39965
|
+
${body}
|
|
39966
|
+
`,
|
|
39967
|
+
appendToUnderstanding: `
|
|
39968
|
+
|
|
39969
|
+
### ${today2}: numbers from the ${date} call confirmed by you
|
|
39970
|
+
|
|
39971
|
+
${body}
|
|
39972
|
+
`
|
|
39973
|
+
};
|
|
39974
|
+
clientFile.howToWrite = `This call confirms answers from a call you have probably already filed. First SEARCH the client's Calls folder for "${clientFile.call.name}". If it is there, append clientFile.confirmation.appendToCall to it and clientFile.confirmation.appendToUnderstanding to Client understanding, and write nothing else. If it is NOT there, the call was never filed: write the whole call as follows. ${clientFile.howToWrite}`;
|
|
39975
|
+
}
|
|
39976
|
+
const localCopy = clientFile && deps.clientFilesDir ? clientFile.confirmation ? writeConfirmationLocally(deps.clientFilesDir, clientFile) : writeClientFileLocally(deps.clientFilesDir, clientFile) : null;
|
|
39921
39977
|
const { applied, ...legacyRest } = legacyOf(gated);
|
|
39922
39978
|
return {
|
|
39923
39979
|
// Never hand back unchecked values beside a failed save (review D-3).
|
|
@@ -39929,7 +39985,7 @@ ${CONFIRM_INSTRUCTION}`;
|
|
|
39929
39985
|
source: noteSource ? { ...noteSource, dateFromRecording: fromRecording } : null,
|
|
39930
39986
|
analysis: analysis ? { ...analysis.kept, leftOutForQuote: analysis.rejected, forTheOwnerOnly: true } : null,
|
|
39931
39987
|
clientFile,
|
|
39932
|
-
clientFileNote: clientFile ?
|
|
39988
|
+
clientFileNote: !clientFile ? "No client name was known (no contact, no businessName), so no client file entry was made." : clientFile.confirmation ? `This call confirms answers from an earlier call. Add only the "Confirmed by you" sections to that call's file and to Client understanding; do not file the call again.${save?.status === "SAVED" ? " The confirmed answers are on the contact." : " The confirmed answers did NOT save on the contact; see the reply."}` : null,
|
|
39933
39989
|
localCopy
|
|
39934
39990
|
};
|
|
39935
39991
|
}
|
|
@@ -39943,7 +39999,7 @@ var init_client_calls = __esm({
|
|
|
39943
39999
|
init_words();
|
|
39944
40000
|
init_store3();
|
|
39945
40001
|
init_assessment();
|
|
39946
|
-
RECORDERS = ["Plaud", "Zoom", "
|
|
40002
|
+
RECORDERS = ["Plaud", "Zoom", "Other"];
|
|
39947
40003
|
SOLUTION_KINDS = ["GoHighLevel build", "AI tool", "custom build", "process"];
|
|
39948
40004
|
clean = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
|
|
39949
40005
|
ANALYSIS_MIN_WORDS = 3;
|
|
@@ -40245,7 +40301,7 @@ function registerAssessmentTools(server2, client) {
|
|
|
40245
40301
|
safeTool(
|
|
40246
40302
|
server2,
|
|
40247
40303
|
"read_assessment_transcript",
|
|
40248
|
-
`Bring a recorded client call (Plaud
|
|
40304
|
+
`Bring a recorded client call (Plaud or Zoom through your own connection, or a transcript file the owner exports from any other recorder and attaches) into the client's assessment, their needs analysis and their client file. The flow: 1) get the transcript yourself, with your own Plaud or Zoom connection or from the attached file; 2) propose assessment answers, each with the client's words copied VERBATIM from the transcript as the quote; for a text question the value is the client's own words taken from that quote, and for a number it is the number they said, written in digits (Plaud writes "a hundred percent"; send "100%"); 3) write the needs analysis, every need, unnamed problem and recommended solution tied to a verbatim quote of at least three words; 4) call this with the client's contactId and the source; 5) write the returned clientFile to the owner's Google Drive with your Drive connection, following clientFile.howToWrite (on the confirmation call below, follow clientFile.howToWrite exactly: it adds a "Confirmed by you" section to the call already filed instead of filing it again); 6) show the owner the reply as given, including the "To confirm" list. EVERY NUMBER waits for the owner's OK and never saves on its own, because no word check can tell a figure that answers the question from one said in passing. Text answers in the client's own words save straight away. When the owner answers the list ("confirm all", or "confirm all except ..."), call again with the same transcript, proposed and contactId, and confirmed holding exactly the approved items (questionId and value from toConfirm); for an item marked replacesSaved, also set replace: true on that proposed answer. Never put an item in confirmed that the owner has not approved in this conversation. Drive allows two folders with the same name, so SEARCH for each folder in the path (and for the client's folder above all) before creating it, and never create a second tree. To append to "Client understanding": read it, add the new dated section at the end, write it back, then read it again and confirm every earlier section is still there. Answers and analysis points whose quote is not in the transcript are thrown out. With a contactId the saved answers are read back: the reply starts SAVED, NOT SAVED, NOT CONFIRMED or NOTHING TO SAVE, and only SAVED means they are on the contact. The needs analysis is advice for the owner and is never sent to the client. Without a contactId nothing is saved.`,
|
|
40249
40305
|
{
|
|
40250
40306
|
transcript: import_zod61.z.string().min(20).describe("The recording, as text. Paste the whole thing."),
|
|
40251
40307
|
proposed: import_zod61.z.array(import_zod61.z.object({
|
|
@@ -40265,7 +40321,7 @@ function registerAssessmentTools(server2, client) {
|
|
|
40265
40321
|
businessName: import_zod61.z.string().optional().describe("The client's business name, used when the contact and its saved assessment carry none."),
|
|
40266
40322
|
createPipeline: import_zod61.z.boolean().optional().describe('Create the "Assessments" pipeline if it is missing. Only when the owner has said yes.'),
|
|
40267
40323
|
source: import_zod61.z.object({
|
|
40268
|
-
recorder: import_zod61.z.enum(["Plaud", "Zoom", "
|
|
40324
|
+
recorder: import_zod61.z.enum(["Plaud", "Zoom", "Other"]).describe("What recorded the call. Use Other for any recorder whose transcript was exported and attached."),
|
|
40269
40325
|
recordedAt: import_zod61.z.string().describe("When the call happened, as an ISO date (2026-09-24) or timestamp, from the recording itself."),
|
|
40270
40326
|
title: import_zod61.z.string().optional().describe("The recording's title, as the recorder shows it."),
|
|
40271
40327
|
fetchedBy: import_zod61.z.enum(["claude", "upload"]).optional().describe("claude when you fetched it with your own connector, upload when the owner attached a file.")
|
|
@@ -41202,17 +41258,46 @@ function html(roster, seatToken, screen = "board", cap) {
|
|
|
41202
41258
|
.sandbox-badge{background:var(--ok-soft);color:var(--ok);border-radius:999px;padding:.14rem .55rem;font-size:var(--fs-label);font-weight:700;letter-spacing:.04em;vertical-align:middle;white-space:nowrap}
|
|
41203
41259
|
|
|
41204
41260
|
/* Stage tiles . same shapes as the hub's 7-stage track, clickable */
|
|
41205
|
-
|
|
41206
|
-
.
|
|
41207
|
-
.
|
|
41208
|
-
.
|
|
41209
|
-
.
|
|
41210
|
-
.
|
|
41211
|
-
.
|
|
41212
|
-
.
|
|
41213
|
-
.
|
|
41214
|
-
.
|
|
41215
|
-
.
|
|
41261
|
+
/* The steps (owner-approved layout, 2026-09-25): progress bar, next-step panel, one row per step. */
|
|
41262
|
+
.sprog{margin:1rem 0 .9rem}
|
|
41263
|
+
.strack{display:grid;grid-template-columns:repeat(7,1fr);gap:.35rem}
|
|
41264
|
+
.sseg{height:.45rem;border-radius:999px;background:var(--wait-soft)}
|
|
41265
|
+
.sseg.done{background:var(--ok)}.sseg.now{background:var(--gold)}.sseg.stale{background:var(--warn)}
|
|
41266
|
+
.splabel{display:flex;justify-content:space-between;flex-wrap:wrap;gap:.3rem 1rem;margin-top:.55rem;font-size:var(--fs-sm)}
|
|
41267
|
+
.splabel b{color:var(--heading);font-weight:600}.splabel span{color:var(--ink-3)}
|
|
41268
|
+
.snext{border:1px solid var(--gold);background:var(--gold-soft);border-radius:var(--radius);padding:1rem 1.1rem;display:grid;grid-template-columns:1fr auto;gap:.6rem 1.2rem;align-items:center}
|
|
41269
|
+
.snext .sk{font-size:var(--fs-label);letter-spacing:.14em;text-transform:uppercase;color:var(--gold-ink);font-weight:700;margin:0 0 .2rem}
|
|
41270
|
+
.snext h3{margin:0 0 .35rem;font-size:var(--fs-lg);color:var(--heading)}
|
|
41271
|
+
.snext p{margin:0;color:var(--ink-2);font-size:var(--fs-sm);max-width:60ch}
|
|
41272
|
+
.snext .swarn{margin-top:.5rem;color:var(--warn);font-weight:500}
|
|
41273
|
+
.snext .snoteline{margin-top:.4rem}
|
|
41274
|
+
.snext .slive{margin-top:.5rem}
|
|
41275
|
+
.sbtns{display:flex;flex-direction:column;align-items:flex-end;gap:.5rem}
|
|
41276
|
+
.sbtns .sbig{padding:.6rem 1.1rem;font-size:var(--fs-body)}
|
|
41277
|
+
.stage button,.snext button{box-sizing:border-box;max-width:100%;white-space:normal;overflow-wrap:anywhere;height:auto}
|
|
41278
|
+
.sbtns .runbtn.sbig{color:var(--on-accent)}
|
|
41279
|
+
.sbtns .swhy{margin:0;font-size:var(--fs-sm);max-width:22rem;text-align:right}
|
|
41280
|
+
.stages{margin-top:1rem;border-top:1px solid var(--line)}
|
|
41281
|
+
.stage{display:grid;grid-template-columns:2rem 9.5rem 7.5rem 1fr auto;gap:.4rem 1rem;align-items:center;padding:.7rem .3rem;border-bottom:1px solid var(--line)}
|
|
41282
|
+
.stage.here{background:var(--gold-soft)}
|
|
41283
|
+
.snum{width:1.7rem;height:1.7rem;border-radius:50%;display:grid;place-items:center;font-size:var(--fs-label);font-weight:700;background:var(--wait-soft);color:var(--wait)}
|
|
41284
|
+
.snum.done{background:var(--ok-soft);color:var(--ok)}.snum.stale{background:var(--warn-soft);color:var(--warn)}.snum.now{background:var(--gold);color:var(--on-accent)}
|
|
41285
|
+
.sname{font-weight:600;color:var(--heading)}
|
|
41286
|
+
.schip{justify-self:start;font-size:var(--fs-label);letter-spacing:.06em;text-transform:uppercase;font-weight:700;padding:.15rem .55rem;border-radius:999px;white-space:nowrap}
|
|
41287
|
+
.schip.done{background:var(--ok-soft);color:var(--ok)}.schip.stale{background:var(--warn-soft);color:var(--warn)}.schip.wait{background:var(--wait-soft);color:var(--wait)}.schip.you,.schip.now{background:var(--gold-soft);color:var(--gold-ink)}
|
|
41288
|
+
.swhat{color:var(--ink-2);font-size:var(--fs-sm);min-width:0;overflow-wrap:anywhere}
|
|
41289
|
+
.swhat .sreason{color:var(--warn)}
|
|
41290
|
+
.snote{display:block;font-size:var(--fs-sm);line-height:1.35}
|
|
41291
|
+
.slive{font-size:var(--fs-sm);line-height:1.35;margin-top:.25rem}
|
|
41292
|
+
.sact{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:.2rem .8rem}
|
|
41293
|
+
.sactbtn{font-size:var(--fs-sm);color:var(--gold-ink);font-weight:600;background:none;border:0;padding:.2rem 0;cursor:pointer;text-align:right}
|
|
41294
|
+
.sactbtn:hover{text-decoration:underline}
|
|
41295
|
+
.sactbtn:disabled{color:var(--ink-3);cursor:default;text-decoration:none}
|
|
41296
|
+
@media (max-width: 52rem){
|
|
41297
|
+
.stage{grid-template-columns:2rem 1fr auto;grid-template-areas:"n name act" ". chip chip" ". what what"}
|
|
41298
|
+
.stage .snum{grid-area:n}.stage .sname{grid-area:name}.stage .schip{grid-area:chip}.stage .swhat{grid-area:what}.stage .sact{grid-area:act}
|
|
41299
|
+
.snext{grid-template-columns:1fr}.sbtns{align-items:flex-start}.sbtns .swhy{text-align:left}
|
|
41300
|
+
}
|
|
41216
41301
|
|
|
41217
41302
|
/* \u2500\u2500 Other accounts: one-line rows. One account at a time is the rule. \u2500\u2500 */
|
|
41218
41303
|
.acct-list{padding:.2rem .9rem}
|
|
@@ -41322,8 +41407,6 @@ function html(roster, seatToken, screen = "board", cap) {
|
|
|
41322
41407
|
|
|
41323
41408
|
@media (max-width: 46rem){
|
|
41324
41409
|
.brand-sub,.localpill{display:none}
|
|
41325
|
-
.stages{gap:.3rem}
|
|
41326
|
-
.stage{min-width:4.2rem}
|
|
41327
41410
|
}
|
|
41328
41411
|
@media (prefers-reduced-motion: reduce){*{transition:none !important}}
|
|
41329
41412
|
` })}<body data-screen="${screen}"${cap ? ` data-capability="${cap.key}"` : ""}>
|
|
@@ -41358,6 +41441,14 @@ const GATES=${scriptJson(HUMAN_GATE_STAGES)};let PROTECTED=${scriptJson(PROTECTE
|
|
|
41358
41441
|
const TYPES=["client","sandbox","template","hidden"];
|
|
41359
41442
|
const TYPE_LABELS={client:"Client",sandbox:"Practice sandbox",template:"Template",hidden:"Hidden"};
|
|
41360
41443
|
const LABELS=["Intake","Brief","Approve","Build","Verify","Manual tasks","Go-live"];
|
|
41444
|
+
// What each step does: [the one line on its row, the fuller sentence in the next-step panel].
|
|
41445
|
+
const STEP_ABOUT=[["Creates the intake form, its fields and one notification workflow.","Creates or repairs this client's intake form, its custom fields and one published notification workflow."],
|
|
41446
|
+
["Reads the intake into a brief.","Reads this client's intake, from the submitted form or the one completed here, and turns it into the brief the plan is made from."],
|
|
41447
|
+
["You approve the proposed build.","Review the intake and the brief, then approve the proposed build for this client."],
|
|
41448
|
+
["Builds the approved plan into this account.","Builds the approved plan into this client's account. Anything it cannot build is listed as an issue."],
|
|
41449
|
+
["Checks the build for broken links.","Checks the account for broken links between workflows, pipelines and fields, and tests the form. It may submit and delete one test lead. Anything it cannot check is reported as unchecked."],
|
|
41450
|
+
["Jobs only a person can do. You confirm them.","The jobs only a person can do, such as A2P, Google sign-in, staff and phone. Confirm them when they are finished."],
|
|
41451
|
+
["You sign this client live.","Sign this client live once Verify is clean and the manual tasks are finished."]];
|
|
41361
41452
|
const STATUS_TEXT={pending:"not started",in_progress:"in progress",done:"done"};
|
|
41362
41453
|
const NEXT={pending:"in_progress",in_progress:"done",done:"pending"};
|
|
41363
41454
|
let polling=false,query="",showParked=false,showNew=false,snapshots=null,snapshotError="";
|
|
@@ -41902,8 +41993,22 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41902
41993
|
if(!response.ok||r.ok!==true){r.ok=false;r.error=r.error||"Command OS could not confirm the result. Refresh this client to read its saved state before retrying."}
|
|
41903
41994
|
if(!r.ok)alert(r.error);else load()
|
|
41904
41995
|
}catch(e){alert("Command OS could not confirm the result. Refresh this client to read its saved state before retrying.")}};
|
|
41905
|
-
|
|
41906
|
-
|
|
41996
|
+
// The steps, laid out as the owner approved on 2026-09-25: a progress bar, the next step in
|
|
41997
|
+
// full, then one row per step. Only the layout changed. Every button below keeps the condition
|
|
41998
|
+
// and the handler it had in the seven-column layout; the next-step panel draws the SAME
|
|
41999
|
+
// buttons again for the current step, so nothing appears there that its row would not offer.
|
|
42000
|
+
const staleOf=i=>state.buildViews?.[c.locationId]?.stale?.find(x=>x.stage===i);
|
|
42001
|
+
const isDone=i=>(st[i]||"pending")==="done"&&!staleOf(i);
|
|
42002
|
+
let curI=-1;for(let i=0;i<STAGES.length;i++){if(!isDone(i)){curI=i;break}}
|
|
42003
|
+
const nDone=STAGES.filter((_,i)=>isDone(i)).length;
|
|
42004
|
+
const prog=document.createElement("div");prog.className="sprog";prog.setAttribute("aria-label","Progress: "+nDone+" of "+STAGES.length+" steps done");
|
|
42005
|
+
const track=document.createElement("div");track.className="strack";prog.appendChild(track);
|
|
42006
|
+
const plabel=document.createElement("div");plabel.className="splabel";prog.appendChild(plabel);
|
|
42007
|
+
div.appendChild(prog);
|
|
42008
|
+
const nextBox=document.createElement("div");nextBox.className="snext";div.appendChild(nextBox);
|
|
42009
|
+
const row=document.createElement("div");row.className="stages";row.setAttribute("role","list");
|
|
42010
|
+
let curLive=false;
|
|
42011
|
+
STAGES.forEach((name,i)=>{const stale=staleOf(i);const el=document.createElement("div");el.className="stage "+(stale?"pending":(st[i]||"pending"))+(i===curI?" here":"");el.setAttribute("role","listitem");
|
|
41907
42012
|
const gate=GATES[String(i)];
|
|
41908
42013
|
const oc0=(state.outcomes||{})[c.locationId+":"+i];const notPassed=(st[i]||"pending")==="pending"&&oc0&&(oc0.issues||[]).length>0;
|
|
41909
42014
|
if(notPassed)el.className+=" notpassed";
|
|
@@ -41913,8 +42018,16 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41913
42018
|
// from before the run (a "stale" warning and an old issue count are both wrong then).
|
|
41914
42019
|
const live=Boolean(run&&run.running&&run.locationId===c.locationId&&run.stage===i);
|
|
41915
42020
|
const anyBusy=Boolean(run&&run.running&&run.locationId===c.locationId);
|
|
41916
|
-
|
|
41917
|
-
|
|
42021
|
+
if(i===curI&&live)curLive=true;
|
|
42022
|
+
const tone=i===curI?"now":isDone(i)?"done":(stale||notPassed)?"stale":"";
|
|
42023
|
+
const seg=document.createElement("span");seg.className="sseg "+tone;track.appendChild(seg);
|
|
42024
|
+
const num=document.createElement("span");num.className="snum "+tone;num.textContent=String(i+1);el.appendChild(num);
|
|
42025
|
+
const nm=document.createElement("b");nm.className="sname";nm.textContent=LABELS[i];el.appendChild(nm);
|
|
42026
|
+
const chipOf=live?["Running\u2026","now"]:stale?["Run again","stale"]:notPassed?["Not passed","stale"]:isDone(i)?["Done","done"]:gate?["Your call","you"]:(st[i]||"pending")==="in_progress"?["In progress","now"]:i===curI?["Up next","now"]:["Waiting","wait"];
|
|
42027
|
+
const chip=document.createElement("span");chip.className="schip "+chipOf[1];chip.textContent=chipOf[0];el.appendChild(chip);
|
|
42028
|
+
const what=document.createElement("div");what.className="swhat";el.appendChild(what);
|
|
42029
|
+
const act=document.createElement("div");act.className="sact";el.appendChild(act);
|
|
42030
|
+
const mkLive=()=>{const box=document.createElement("div");box.className="slive";
|
|
41918
42031
|
const clock=document.createElement("div");clock.style.cssText="font-variant-numeric:tabular-nums;color:var(--ok)";
|
|
41919
42032
|
const began=run.startedAt?new Date(run.startedAt).getTime():0;
|
|
41920
42033
|
const tick=()=>{if(!began)return;const sec=Math.max(0,Math.round((Date.now()-began)/1000));
|
|
@@ -41927,29 +42040,60 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
|
|
|
41927
42040
|
line.textContent=(doneAll?"\u2713 ":g.state==="in_progress"?"\u203A ":" ")+g.label+" "+g.done+"/"+g.total;box.appendChild(line)}}
|
|
41928
42041
|
if(pr&&pr.current){const now=document.createElement("div");now.style.cssText="margin-top:.25rem;color:var(--muted)";
|
|
41929
42042
|
now.textContent=String(pr.current).replace(/^"|"$/g,"").slice(0,110);box.appendChild(now)}
|
|
41930
|
-
|
|
42043
|
+
return box};
|
|
41931
42044
|
// One short line here; the button's confirmation carries the full wording. At 135
|
|
41932
42045
|
// characters this sentence made every stage box 677 px tall (auditor, 2026-09-07).
|
|
41933
|
-
|
|
42046
|
+
let about=document.createElement("span");about.textContent=STEP_ABOUT[i][0];
|
|
42047
|
+
if(i===4){const disclosure=document.createElement("span");disclosure.textContent="Checks links and the form. May submit and delete one test lead.";about=disclosure}
|
|
41934
42048
|
if(gate)el.title=gate;
|
|
41935
|
-
if(stale&&!live){const reason=document.createElement("small");reason.textContent=stale.reason;el.appendChild(reason)}
|
|
41936
42049
|
const oc=(state.outcomes||{})[c.locationId+":"+i];
|
|
41937
|
-
|
|
41938
|
-
|
|
41939
|
-
|
|
42050
|
+
const nIss=oc?(oc.issues||[]).length:0;
|
|
42051
|
+
const toggleIssues=(ev)=>{ev.stopPropagation();oOpen[c.locationId+":"+i]=!oOpen[c.locationId+":"+i];render(lastState,lastRun)};
|
|
42052
|
+
const mkNote=()=>{let note=null;
|
|
42053
|
+
if(!live&&oc&&(oc.summary||(oc.issues||[]).length)){note=document.createElement("small");note.className="snote";
|
|
42054
|
+
note.style.cssText="color:"+(nIss?"var(--warn)":"var(--ok)")+";text-decoration:"+(nIss?"underline":"none")+";cursor:"+(nIss?"pointer":"default");
|
|
41940
42055
|
note.textContent=i===4&&oc.ok===true?(${verifyPassedLine.toString()})((state.manualTasks?.[c.locationId]||[]).length):nIss?("\u26A0 "+nIss+" issue"+(nIss>1?"s":"")+". See details"):((oc.plain&&oc.plain.summary)||oc.summary||"").slice(0,90);
|
|
41941
|
-
if(nIss)note.onclick=
|
|
41942
|
-
|
|
42056
|
+
if(nIss)note.onclick=toggleIssues}
|
|
42057
|
+
return note};
|
|
42058
|
+
// What the row says, in one place: the live run, else why it went stale, else what the last
|
|
42059
|
+
// run found, else what the step does.
|
|
42060
|
+
if(live){const rn=document.createElement("span");rn.textContent="Running now.";what.appendChild(rn);what.appendChild(mkLive())}
|
|
42061
|
+
if(stale&&!live){const reason=document.createElement("span");reason.className="sreason";reason.textContent=stale.reason;what.appendChild(reason)}
|
|
42062
|
+
else if(!live){const note=mkNote();what.appendChild(note||about)}
|
|
41943
42063
|
if(POWERED.includes(i)&&(st[i]||"pending")!=="done"){el.style.cursor="default";
|
|
41944
42064
|
el.title="Command OS runs and checks this step. Its result comes from a run, never from a click."}
|
|
41945
|
-
|
|
42065
|
+
const gateLabel=({2:"Approve this plan",5:"Confirm these tasks are finished",6:"Sign this client live"})[i];
|
|
42066
|
+
const mkGate=cls=>{const action=document.createElement("button");action.className=cls;action.textContent=gateLabel;action.onclick=()=>{if(i===2)return approveBuilding();return markStage(c.locationId,i,"done")};return action};
|
|
41946
42067
|
const canBuild=!parkedType&&(i<=1||sandbox||Boolean((state.buildApproved||{})[c.locationId]));
|
|
41947
|
-
if(
|
|
41948
|
-
|
|
42068
|
+
const runIt=async(ev)=>{ev.stopPropagation();if(i===0&&!confirm("In "+c.name+", Intake writes custom fields, one form, and one published notification workflow."+String.fromCharCode(10,10)+INTAKE_NOTIFICATION_REVIEW.join(String.fromCharCode(10))+String.fromCharCode(10,10)+"Create or repair these intake items?"))return;if(i===4&&!confirm("Verify submits one test lead to "+c.name+"\u2019s funnel, watches it arrive, and deletes the test contact afterwards. It may fire this client\u2019s own automations once. Run the check?"))return;const res=await fetch("/api/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,stage:i,intakeConfirmed:i===0})});const j=await res.json();if(!j.ok)alert(j.error);load()};
|
|
42069
|
+
const mkRun=(cls,label)=>{const b=document.createElement("button");b.className=cls;
|
|
42070
|
+
const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":label;
|
|
41949
42071
|
b.disabled=Boolean(busy)||(!st.some(s=>s==="done")&&!preflights[c.locationId]?.ok);
|
|
41950
|
-
b.onclick=
|
|
41951
|
-
|
|
42072
|
+
b.onclick=runIt;return b};
|
|
42073
|
+
if(!live&&nIss){const si=document.createElement("button");si.className="sactbtn";si.textContent=nIss>1?"See issues":"See issue";si.onclick=toggleIssues;act.appendChild(si)}
|
|
42074
|
+
if(gate&&!anyBusy&&!parkedType)act.appendChild(mkGate("sactbtn"));
|
|
42075
|
+
if(canBuild&&POWERED.includes(i))act.appendChild(mkRun("sactbtn srun",i===curI?"Run "+LABELS[i]:(st[i]||"pending")==="done"?"Run again":"Run this step"));
|
|
42076
|
+
if(i===curI){const main=document.createElement("div");
|
|
42077
|
+
const k=document.createElement("p");k.className="sk";k.textContent=parkedType?"Building is off":"Next step";main.appendChild(k);
|
|
42078
|
+
const h3=document.createElement("h3");h3.textContent=parkedType?"Nothing is built into this account":(i+1)+". "+LABELS[i];main.appendChild(h3);
|
|
42079
|
+
const pa=document.createElement("p");pa.textContent=parkedType?"It opens here so you can lock or unlock it, or change its type to Client to build it.":STEP_ABOUT[i][1];main.appendChild(pa);
|
|
42080
|
+
if(live)main.appendChild(mkLive());
|
|
42081
|
+
if(stale&&!live){const w=document.createElement("p");w.className="swarn";w.textContent=stale.reason;main.appendChild(w)}
|
|
42082
|
+
const pn=mkNote();if(pn){const w=document.createElement("p");w.className="snoteline";w.appendChild(pn);main.appendChild(w)}
|
|
42083
|
+
const btns=document.createElement("div");btns.className="sbtns";
|
|
42084
|
+
if(gate&&!anyBusy&&!parkedType)btns.appendChild(mkGate("intakebtn sbig"));
|
|
42085
|
+
if(canBuild&&POWERED.includes(i))btns.appendChild(mkRun("runbtn sbig","Run "+LABELS[i]));
|
|
42086
|
+
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)}
|
|
42087
|
+
if(!btns.children.length&&!live&&!parkedType){const why=document.createElement("p");why.className="muted swhy";
|
|
42088
|
+
why.textContent=anyBusy?"Another step is running for this client. This one opens when it finishes.":POWERED.includes(i)?"Approve building for this client below, and this step can run.":"";
|
|
42089
|
+
if(why.textContent)btns.appendChild(why)}
|
|
42090
|
+
nextBox.title=el.title||"";nextBox.appendChild(main);nextBox.appendChild(btns)}
|
|
41952
42091
|
row.appendChild(el)});
|
|
42092
|
+
if(curI<0){const main=document.createElement("div");const k=document.createElement("p");k.className="sk";k.textContent="All done";main.appendChild(k);
|
|
42093
|
+
const h3=document.createElement("h3");h3.textContent="All "+STAGES.length+" steps are done.";main.appendChild(h3);nextBox.appendChild(main)}
|
|
42094
|
+
{const b=document.createElement("b");b.textContent=curI<0?"All "+STAGES.length+" steps done":"Step "+(curI+1)+" of "+STAGES.length+": "+LABELS[curI];plabel.appendChild(b);
|
|
42095
|
+
const after=STAGES.length-nDone-(curI<0?0:1);const s=document.createElement("span");
|
|
42096
|
+
s.textContent=nDone+" done"+(curI<0?"":" \xB7 "+(curLive?"1 running now":"1 needs you")+" \xB7 "+after+" after that");plabel.appendChild(s)}
|
|
41953
42097
|
div.appendChild(row);
|
|
41954
42098
|
for(let i=0;i<STAGES.length;i++){if(oOpen[c.locationId+":"+i]){const op=outcomePanel(c.locationId,i,state);if(op)div.appendChild(op)}}
|
|
41955
42099
|
const view=(state.buildViews||{})[c.locationId];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.84.
|
|
3
|
+
"version": "3.84.10",
|
|
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",
|