@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 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
@@ -897,7 +897,7 @@ Built by **[Elite DCs, LLC](https://elitedcs.com)** — a digital marketing and
897
897
 
898
898
  **Tech stack:** TypeScript, Node.js, esbuild, MCP SDK, Zod, GHL API v2, Firebase Auth
899
899
 
900
- **Version:** 3.84.8
900
+ **Version:** 3.84.10
901
901
 
902
902
  ---
903
903
 
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.8",
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
- const localCopy = clientFile && deps.clientFilesDir ? writeClientFileLocally(deps.clientFilesDir, clientFile) : null;
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 ? null : "No client name was known (no contact, no businessName), so no client file entry was made.",
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", "Pocket", "Fathom", "Other"];
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, Zoom, Pocket, Fathom, or any transcript file the owner attaches) into the client's assessment, their needs analysis and their client file. The flow: 1) get the transcript yourself, with your own Plaud or Zoom connection or from the attached file; 2) propose assessment answers, each with the client's words copied VERBATIM from the transcript as the quote; for a text question the value is the client's own words taken from that quote, and for a number it is the number they said, written in digits (Plaud writes "a hundred percent"; send "100%"); 3) write the needs analysis, every need, unnamed problem and recommended solution tied to a verbatim quote of at least three words; 4) call this with the client's contactId and the source; 5) write the returned clientFile to the owner's Google Drive with your Drive connection, following clientFile.howToWrite; 6) show the owner the reply as given, including the "To confirm" list. EVERY NUMBER waits for the owner's OK and never saves on its own, because no word check can tell a figure that answers the question from one said in passing. Text answers in the client's own words save straight away. When the owner answers the list ("confirm all", or "confirm all except ..."), call again with the same transcript, proposed and contactId, and confirmed holding exactly the approved items (questionId and value from toConfirm); for an item marked replacesSaved, also set replace: true on that proposed answer. Never put an item in confirmed that the owner has not approved in this conversation. Drive allows two folders with the same name, so SEARCH for each folder in the path (and for the client's folder above all) before creating it, and never create a second tree. To append to "Client understanding": read it, add the new dated section at the end, write it back, then read it again and confirm every earlier section is still there. Answers and analysis points whose quote is not in the transcript are thrown out. With a contactId the saved answers are read back: the reply starts SAVED, NOT SAVED, NOT CONFIRMED or NOTHING TO SAVE, and only SAVED means they are on the contact. The needs analysis is advice for the owner and is never sent to the client. Without a contactId nothing is saved.`,
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", "Pocket", "Fathom", "Other"]).describe("What recorded the call."),
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
- .stages{display:flex;gap:.4rem;flex-wrap:wrap}
41206
- .stage{flex:1;min-width:5.2rem;border:1px solid var(--line);border-radius:var(--radius-sm);padding:.45rem .5rem;text-align:center;cursor:pointer;user-select:none;font-size:var(--fs-label);background:var(--surface-2);color:var(--ink-2)}
41207
- .stage button{box-sizing:border-box;max-width:100%;white-space:normal;overflow-wrap:anywhere;height:auto}
41208
- .stage b{display:block;font-weight:600;color:var(--heading);font-size:var(--fs-label)}
41209
- .stage small{display:block;color:var(--ink-3);font-size:var(--fs-sm);margin-top:.15rem}
41210
- .stage.in_progress{border-color:var(--gold);background:var(--gold-soft)}
41211
- .stage.in_progress small{color:var(--gold-ink)}
41212
- .stage.done{border-color:var(--ok);background:var(--ok-soft)}
41213
- .stage.done small,.stage.done b{color:var(--ok)}
41214
- .stage.notpassed{border-color:var(--warn);background:var(--warn-soft)}
41215
- .stage.notpassed small{color:var(--warn)}
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
- const row=document.createElement("div");row.className="stages";
41906
- STAGES.forEach((name,i)=>{const stale=state.buildViews?.[c.locationId]?.stale?.find(x=>x.stage===i);const el=document.createElement("div");el.className="stage "+(stale?"pending":(st[i]||"pending"));
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
- el.innerHTML="<b>"+(i+1)+". "+LABELS[i]+"</b><small>"+(live?"running now":stale?"stale":notPassed?"not passed":STATUS_TEXT[st[i]||"pending"])+(gate&&!live?" \xB7 your call":"")+"</small>";
41917
- if(live){const box=document.createElement("div");box.style.cssText="margin-top:.35rem;font-size:var(--fs-sm);line-height:1.35";
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
- el.appendChild(box)}
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
- if(i===4){const disclosure=document.createElement("small");disclosure.textContent="Reads every planned item back. May submit and delete one test lead.";el.appendChild(disclosure)}
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
- if(!live&&oc&&(oc.summary||(oc.issues||[]).length)){const note=document.createElement("small");
41938
- const nIss=(oc.issues||[]).length;
41939
- note.style.cssText="display:block;margin-top:.25rem;color:"+(nIss?"var(--warn)":"var(--ok)")+";font-size:var(--fs-sm);line-height:1.25;text-decoration:"+(nIss?"underline":"none")+";cursor:"+(nIss?"pointer":"default");
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=(ev)=>{ev.stopPropagation();oOpen[c.locationId+":"+i]=!oOpen[c.locationId+":"+i];render(lastState,lastRun)};
41942
- el.appendChild(note)}
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
- if(gate&&!anyBusy&&!parkedType){const action=document.createElement("button");action.className="intakebtn";action.textContent=({2:"Approve this plan",5:"Confirm these tasks are finished",6:"Sign this client live"})[i];action.onclick=()=>{if(i===2)return approveBuilding();return markStage(c.locationId,i,"done")};el.appendChild(action)}
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(canBuild&&POWERED.includes(i)){const b=document.createElement("button");b.className="runbtn";
41948
- const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":"Run this step";
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=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()};
41951
- el.appendChild(document.createElement("br"));el.appendChild(b)}
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.8",
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",