@elitedcs/ghl-mcp 3.83.5 → 3.83.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 CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.83.7 — the cockpit answers only to its own address, and keeps its key to itself
6
+
7
+ **Security fix. Update to this version.**
8
+
9
+ The cockpit runs on your own computer and only accepts connections from it. Two
10
+ things it should have refused, it did not:
11
+
12
+ - **A design run's details were readable without the cockpit's key.** Anything
13
+ able to reach the cockpit on your computer could list the Design Studio runs it
14
+ had started: the brand, headline, points and audience you typed, the run's log,
15
+ and the folder it saved to, which includes your computer's user name.
16
+ - **The cockpit handed its key to a request that used a different address for
17
+ it.** A web page that tricks your browser into reaching the cockpit under
18
+ another name could have picked up that key, and with it read what the cockpit
19
+ reads for your clients: intake answers, uploaded intake documents, what each
20
+ account already has set up, and your snapshot list. Nothing could be changed
21
+ that way; every change the cockpit makes kept refusing such requests. No
22
+ GoHighLevel key was exposed by either path.
23
+
24
+ Now the cockpit refuses any request that is not addressed to `localhost` or
25
+ `127.0.0.1` on its own port, before it does anything else. It gives its key only
26
+ to its own pages opened on your computer, never to anything else. Design Studio's
27
+ run list and progress need the key, like the cockpit's other private reads. And a
28
+ request that spells a page's address in an unusual way is refused rather than
29
+ guessed at.
30
+
31
+ Nothing changes in how you open it: the address the cockpit opens for you is one of
32
+ the two it accepts.
33
+
34
+ ## 3.83.6 — what a design run actually costs you, measured
35
+
36
+ 3.83.5 gave a running job a clock. This gives it an honest number on it.
37
+
38
+ How long a run takes was written by hand in three places and every one was wrong.
39
+ The brand page said a deck takes "roughly ten minutes". The run API said "twenty
40
+ minutes or more". The clock added in 3.83.5 said "ten to twenty". Nobody had ever
41
+ timed one.
42
+
43
+ Two runs have now been measured. A six-slide deck took 12 minutes 39 seconds and
44
+ $1.74 for a single pass. A conversion page on a real brand took 26 minutes and
45
+ $3.75 across three. So a pass is 9 to 13 minutes and $1.25 to $1.75, and a run may
46
+ use three of them.
47
+
48
+ That sentence now lives in one file and every screen reads it, so the three cannot
49
+ drift apart again.
50
+
51
+ **The cost is now stated before you press the button, not discovered afterwards.**
52
+ These runs use your own Claude, never ours. On a Max plan that is usage rather than
53
+ a bill, which is exactly why the figure belongs on the screen: on pay as you go it
54
+ is real money, and you should know roughly what a button costs before you press it.
55
+
5
56
  ## 3.83.5 — the brand page's buttons work, and a run tells you it is working
6
57
 
7
58
  If you use Design Studio, every button on a brand's own page has been dead since
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.83.5
900
+ **Version:** 3.83.7
901
901
 
902
902
  ---
903
903
 
package/dist/index.js CHANGED
@@ -27187,6 +27187,20 @@ var init_run_jobs = __esm({
27187
27187
  }
27188
27188
  });
27189
27189
 
27190
+ // src/command-os/design/run-expectations.ts
27191
+ var RUN_MAX_PASSES, RUN_PASS_MINUTES, RUN_PASS_COST, RUN_TOTAL_MINUTES, RUN_TOTAL_COST, RUN_EXPECTATION;
27192
+ var init_run_expectations = __esm({
27193
+ "src/command-os/design/run-expectations.ts"() {
27194
+ "use strict";
27195
+ RUN_MAX_PASSES = 3;
27196
+ RUN_PASS_MINUTES = "9 to 13 minutes";
27197
+ RUN_PASS_COST = "$1.25 to $1.75";
27198
+ RUN_TOTAL_MINUTES = "25 to 40 minutes";
27199
+ RUN_TOTAL_COST = "about $5";
27200
+ RUN_EXPECTATION = `Each pass takes ${RUN_PASS_MINUTES} and costs ${RUN_PASS_COST} of your own Claude usage, and a run may take up to ${RUN_MAX_PASSES} passes. Allow ${RUN_TOTAL_MINUTES} and up to ${RUN_TOTAL_COST}.`;
27201
+ }
27202
+ });
27203
+
27190
27204
  // src/run-cost.ts
27191
27205
  function runCostPath() {
27192
27206
  return path18.join(appDataDir(), "command-os-run-cost.jsonl");
@@ -28591,7 +28605,7 @@ var require_package = __commonJS({
28591
28605
  "package.json"(exports2, module2) {
28592
28606
  module2.exports = {
28593
28607
  name: "@elitedcs/ghl-mcp",
28594
- version: "3.83.5",
28608
+ version: "3.83.7",
28595
28609
  mcpName: "io.github.drjerryrelth/ghl-command",
28596
28610
  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.",
28597
28611
  main: "dist/index.js",
@@ -32369,7 +32383,7 @@ ${d.voice.samples.length ? `<h2>Voice</h2><ul>${d.voice.samples.map((s) => `<li>
32369
32383
  </div>
32370
32384
  ${opts.screenshot ? `<h2>Their site, as rendered</h2><img class="shot" src="/brand/${encodeURIComponent(d.slug)}/screenshot.png" alt="Screenshot of ${esc5(d.name)}'s website">` : ""}
32371
32385
  <h2>Make something in this brand</h2>
32372
- <p class="small">Runs on <b>your own Claude</b>, on this computer \u2014 never our key. A deck takes roughly ten minutes, a page twenty or more; the run keeps going if you close this tab. Write the headline and the points you want made, not instructions.</p>
32386
+ <p class="small">Runs on <b>your own Claude</b>, on this computer, never our key. ${esc5(RUN_EXPECTATION)} The run keeps going if you close this tab. Write the headline and the points you want made, not instructions.</p>
32373
32387
  <form id="make"><div class="two">
32374
32388
  <div><label for="mk-medium">What</label><select id="mk-medium"><option value="deck">A deck</option><option value="site">A landing page</option><option value="report">A report</option></select></div>
32375
32389
  <div><label for="mk-slides">Slides (a deck only)</label><input id="mk-slides" type="number" min="1" max="40" value="6"></div>
@@ -32398,6 +32412,7 @@ ${opts.dir ? `<p class="small">Stored at ${esc5(opts.dir)}</p>` : ""}
32398
32412
  ${foot(SHELL_JS(opts.seatToken ?? "") + `
32399
32413
  (function(){
32400
32414
  var poll=null,runId=null,started=0,ticker=null,passes=0;
32415
+ var EXPECT=${scriptJson(RUN_EXPECTATION)};
32401
32416
  function stopPolling(){if(poll){clearInterval(poll);poll=null}if(ticker){clearInterval(ticker);ticker=null}}
32402
32417
  function elapsed(ms){var s=Math.floor(ms/1000),m=Math.floor(s/60);s=s%60;return m?(m+"m "+(s<10?"0":"")+s+"s"):(s+"s")}
32403
32418
  /* The run prints NOTHING while Claude writes - it answers in one go, so the log
@@ -32407,7 +32422,7 @@ function elapsed(ms){var s=Math.floor(ms/1000),m=Math.floor(s/60);s=s%60;return
32407
32422
  from the child process, and it ticks every second, not every poll. */
32408
32423
  function tick(){
32409
32424
  if(!started)return;
32410
- say("mk-status","Working on your Claude: "+elapsed(Date.now()-started)+" so far"+(passes?(", on pass "+(passes+1)):"")+". Claude writes the whole file in one answer, so nothing appears below until a pass finishes. Ten to twenty minutes is normal. Closing this tab does not stop it.");
32425
+ say("mk-status","Working on your Claude: "+elapsed(Date.now()-started)+" so far"+(passes?(", on pass "+(passes+1)):"")+". Claude writes the whole file in one answer, so nothing appears below until a pass finishes. "+EXPECT+" Closing this tab does not stop it.");
32411
32426
  }
32412
32427
  function show(t){var el=document.getElementById("mk-log");el.hidden=false;el.textContent=t}
32413
32428
  document.getElementById("make").addEventListener("submit",async function(e){
@@ -32427,7 +32442,7 @@ document.getElementById("make").addEventListener("submit",async function(e){
32427
32442
  document.getElementById("mk-cancel").hidden=false;
32428
32443
  started=Date.now();passes=0;tick();ticker=setInterval(tick,1000);
32429
32444
  poll=setInterval(async function(){
32430
- var s=await (await fetch("/api/design/run?id="+encodeURIComponent(runId))).json();
32445
+ var s=await (await fetch("/api/design/run?id="+encodeURIComponent(runId),{headers:{"x-command-os-seat":KEY}})).json();
32431
32446
  var lines=s.tail||[];
32432
32447
  show(lines.join("\\n"));
32433
32448
  passes=lines.filter(function(l){return l.trim().slice(0,5)==="pass "}).length;
@@ -32453,6 +32468,7 @@ var esc5, STYLE, SHELL_JS, foot;
32453
32468
  var init_brand_page = __esm({
32454
32469
  "src/command-os/design/brand-page.ts"() {
32455
32470
  "use strict";
32471
+ init_run_expectations();
32456
32472
  init_fonts();
32457
32473
  init_cockpit_shell();
32458
32474
  init_script_json();
@@ -33969,9 +33985,11 @@ __export(dashboard_exports, {
33969
33985
  insideDir: () => insideDir,
33970
33986
  intakeInput: () => intakeInput,
33971
33987
  intakeRequestError: () => intakeRequestError,
33988
+ isAllowedCockpitHost: () => isAllowedCockpitHost,
33972
33989
  isLoopback: () => isLoopback,
33973
33990
  launcherAlreadyOffered: () => launcherAlreadyOffered,
33974
33991
  listClientDocs: () => listClientDocs,
33992
+ mayServeSeat: () => mayServeSeat,
33975
33993
  memberFacingError: () => memberFacingError,
33976
33994
  newClientFailure: () => newClientFailure,
33977
33995
  normalizedIntake: () => normalizedIntake,
@@ -35736,22 +35754,45 @@ function checkSeatToken(req, expected) {
35736
35754
  }
35737
35755
  return null;
35738
35756
  }
35739
- function checkMutationGuard(req, port) {
35740
- const method = (req.method || "GET").toUpperCase();
35741
- if (method === "GET" || method === "HEAD" || method === "OPTIONS") return null;
35742
- const header = (name) => {
35743
- const v = req.headers[name];
35744
- return (Array.isArray(v) ? v[0] : v ?? "").trim();
35745
- };
35746
- const allowedHosts = [`127.0.0.1:${port}`, `localhost:${port}`];
35747
- if (!allowedHosts.includes(header("host").toLowerCase())) {
35748
- return { status: 403, error: "host not allowed" };
35757
+ function cockpitHeader(req, name) {
35758
+ const value = req.headers[name];
35759
+ return Array.isArray(value) ? value.join(",") : value ?? "";
35760
+ }
35761
+ function isAllowedCockpitHost(req, port) {
35762
+ const host = cockpitHeader(req, "host").toLowerCase();
35763
+ return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
35764
+ }
35765
+ function mayServeSeat(req, port) {
35766
+ return isLoopback(req) && isAllowedCockpitHost(req, port);
35767
+ }
35768
+ function cockpitPath(req) {
35769
+ return (req.url || "/").split("?")[0];
35770
+ }
35771
+ function checkCockpitPath(req) {
35772
+ const pathname = cockpitPath(req);
35773
+ if (!pathname.startsWith("/") || /\/\/|\\|#|[\x00-\x20\x7f]|%(?:2e|2f|5c)|%(?![0-9a-f]{2})/i.test(pathname) || pathname.split("/").some((segment) => segment === "." || segment === "..")) {
35774
+ return { status: 400, error: "Use a canonical cockpit path." };
35749
35775
  }
35750
- const origin = header("origin").toLowerCase();
35751
- if (origin && !allowedHosts.some((h) => origin === `http://${h}`)) {
35776
+ return null;
35777
+ }
35778
+ function checkCockpitOrigin(req, port) {
35779
+ const origin = cockpitHeader(req, "origin").toLowerCase();
35780
+ if (req.headers.origin !== void 0 && origin !== `http://127.0.0.1:${port}` && origin !== `http://localhost:${port}`) {
35752
35781
  return { status: 403, error: "cross-origin request rejected" };
35753
35782
  }
35754
- if (header("content-type").split(";")[0].trim().toLowerCase() !== "application/json") {
35783
+ return null;
35784
+ }
35785
+ function checkMutationGuard(req, port) {
35786
+ if (!isAllowedCockpitHost(req, port)) {
35787
+ return { status: 403, error: "This Host is not allowed to access the cockpit." };
35788
+ }
35789
+ const pathDenied = checkCockpitPath(req);
35790
+ if (pathDenied) return pathDenied;
35791
+ const method = (req.method || "GET").toUpperCase();
35792
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") return null;
35793
+ const originDenied = checkCockpitOrigin(req, port);
35794
+ if (originDenied) return originDenied;
35795
+ if (cockpitHeader(req, "content-type").split(";")[0].trim().toLowerCase() !== "application/json") {
35755
35796
  return { status: 415, error: "expected application/json" };
35756
35797
  }
35757
35798
  return null;
@@ -35759,8 +35800,13 @@ function checkMutationGuard(req, port) {
35759
35800
  function cockpitGate(req, seatToken, port) {
35760
35801
  const denied = checkMutationGuard(req, port) ?? checkSeatToken(req, seatToken);
35761
35802
  if (denied) return denied;
35762
- const pathname = (req.url ?? "/").split("?")[0];
35763
- if (SEAT_GUARDED_READS.includes(pathname)) return checkSeatToken({ method: "POST", headers: req.headers }, seatToken);
35803
+ const pathname = cockpitPath(req);
35804
+ if (SEAT_GUARDED_READS.includes(pathname) || pathname === "/api/design" || pathname.startsWith("/api/design/")) {
35805
+ if (cockpitHeader(req, "sec-fetch-site").toLowerCase() === "cross-site") {
35806
+ return { status: 403, error: "cross-site request rejected" };
35807
+ }
35808
+ return checkCockpitOrigin(req, port) ?? checkSeatToken({ method: "POST", headers: req.headers }, seatToken);
35809
+ }
35764
35810
  return null;
35765
35811
  }
35766
35812
  function guarded(seatToken, port, routes) {
@@ -35885,7 +35931,7 @@ async function runDashboard(argv) {
35885
35931
  questions: (id) => composeQuestionSet({ industry: readCockpitState().accounts?.[id]?.industry, agency: readOverlay() })
35886
35932
  };
35887
35933
  const server2 = http.createServer(guarded(seatToken, port, (req, res) => {
35888
- const url = req.url || "/";
35934
+ const url = cockpitPath(req);
35889
35935
  if (req.method === "POST" && url === "/api/client-assessment") {
35890
35936
  readBody(req, res, 2e4, async (body) => {
35891
35937
  try {
@@ -35907,10 +35953,10 @@ async function runDashboard(argv) {
35907
35953
  return;
35908
35954
  }
35909
35955
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
35910
- res.end(html(currentRoster(), isLoopback(req) ? seatToken : "", "for", cap));
35956
+ res.end(html(currentRoster(), mayServeSeat(req, port) ? seatToken : "", "for", cap));
35911
35957
  return;
35912
35958
  }
35913
- if (req.method === "GET" && url.startsWith("/api/capability")) {
35959
+ if (req.method === "GET" && url === "/api/capability") {
35914
35960
  const u = new URL(req.url ?? "/", "http://127.0.0.1");
35915
35961
  const cap = capabilityByKey(u.searchParams.get("key") ?? "");
35916
35962
  const locationId2 = u.searchParams.get("locationId") ?? "";
@@ -36045,7 +36091,7 @@ async function runDashboard(argv) {
36045
36091
  });
36046
36092
  return;
36047
36093
  }
36048
- if (req.method === "GET" && url.startsWith("/api/intake-documents")) {
36094
+ if (req.method === "GET" && url === "/api/intake-documents") {
36049
36095
  const u = new URL(req.url ?? "/", "http://127.0.0.1");
36050
36096
  const seat = checkSeatToken({ method: "POST", headers: req.headers }, seatToken);
36051
36097
  if (seat) {
@@ -36093,7 +36139,7 @@ async function runDashboard(argv) {
36093
36139
  const boardScreen = BOARD_ROUTES[url];
36094
36140
  if (req.method === "GET" && boardScreen) {
36095
36141
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
36096
- res.end(html(currentRoster(), isLoopback(req) ? seatToken : "", boardScreen));
36142
+ res.end(html(currentRoster(), mayServeSeat(req, port) ? seatToken : "", boardScreen));
36097
36143
  return;
36098
36144
  }
36099
36145
  if ((req.method === "GET" || req.method === "HEAD") && (url === "/assessment" || url === "/assessment.html")) {
@@ -36105,7 +36151,7 @@ async function runDashboard(argv) {
36105
36151
  "X-Robots-Tag": "noindex, nofollow"
36106
36152
  });
36107
36153
  const roster2 = currentRoster().map((r) => ({ locationId: r.locationId, name: r.name }));
36108
- const clientsTag = `<script>window.COMMAND_OS_CLIENTS=${scriptJson(roster2)};window.COMMAND_OS_SEAT=${scriptJson(isLoopback(req) ? seatToken : "")};</script>`;
36154
+ const clientsTag = `<script>window.COMMAND_OS_CLIENTS=${scriptJson(roster2)};window.COMMAND_OS_SEAT=${scriptJson(mayServeSeat(req, port) ? seatToken : "")};</script>`;
36109
36155
  res.end(req.method === "HEAD" ? void 0 : withCockpitShell(ASSESSMENT_HTML, "assessment", assessmentAgencyTag() + clientsTag));
36110
36156
  return;
36111
36157
  }
@@ -36148,14 +36194,14 @@ async function runDashboard(argv) {
36148
36194
  });
36149
36195
  return;
36150
36196
  }
36151
- if (req.method === "GET" && url.startsWith("/api/client-preflight?")) {
36197
+ if (req.method === "GET" && url === "/api/client-preflight") {
36152
36198
  const seat = checkSeatToken({ method: "POST", headers: req.headers }, seatToken);
36153
36199
  if (seat) {
36154
36200
  res.writeHead(seat.status);
36155
36201
  res.end(JSON.stringify({ ok: false, error: seat.error }));
36156
36202
  return;
36157
36203
  }
36158
- const id = new URL(url, "http://x").searchParams.get("locationId") || "";
36204
+ const id = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
36159
36205
  if (!readableAccount(id)) {
36160
36206
  res.writeHead(400);
36161
36207
  res.end(JSON.stringify({ ok: false, error: "Choose a registered client." }));
@@ -36311,8 +36357,8 @@ async function runDashboard(argv) {
36311
36357
  });
36312
36358
  return;
36313
36359
  }
36314
- if (req.method === "GET" && url.startsWith("/api/intake")) {
36315
- const industry = new URL(url, "http://x").searchParams.get("industry") || void 0;
36360
+ if (req.method === "GET" && url === "/api/intake") {
36361
+ const industry = new URL(req.url || "/", "http://x").searchParams.get("industry") || void 0;
36316
36362
  const agency = readOverlay();
36317
36363
  const composed = composeQuestionSet({ industry, agency });
36318
36364
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -36341,10 +36387,10 @@ async function runDashboard(argv) {
36341
36387
  });
36342
36388
  return;
36343
36389
  }
36344
- if (req.method === "GET" && url.startsWith("/api/client-intake")) {
36390
+ if (req.method === "GET" && url === "/api/client-intake") {
36345
36391
  (async () => {
36346
36392
  try {
36347
- const locationId2 = new URL(url, "http://x").searchParams.get("locationId") || "";
36393
+ const locationId2 = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
36348
36394
  const seat = checkSeatToken({ method: "POST", headers: req.headers }, seatToken);
36349
36395
  if (seat) {
36350
36396
  res.writeHead(seat.status, { "Content-Type": "application/json" });
@@ -36368,7 +36414,7 @@ async function runDashboard(argv) {
36368
36414
  res.end(JSON.stringify({ ok: true, ...snap, questions, industry, inputHash: hash, unansweredRequired: questions.filter((q2) => q2.required && !String(answers[q2.label] ?? answers[q2.key] ?? "").trim()).map((q2) => q2.label), costs: readRunCosts().filter((r) => r.locationId === locationId2 && (r.kind === "intake extraction" || r.kind === "engine-check")).slice(-10), submittedAnswers: snap.answers ?? {}, answers, savedAt: st.intakeDrafts?.[locationId2]?.savedAt, connectionPending: !c }));
36369
36415
  } catch (e) {
36370
36416
  res.writeHead(400, { "Content-Type": "application/json" });
36371
- logQuietly(`client-intake ${new URL(url, "http://x").searchParams.get("locationId") || ""}: ${e instanceof Error ? e.message : String(e)}`);
36417
+ logQuietly(`client-intake ${new URL(req.url || "/", "http://x").searchParams.get("locationId") || ""}: ${e instanceof Error ? e.message : String(e)}`);
36372
36418
  res.end(JSON.stringify({ ok: false, error: intakeRequestError(e) }));
36373
36419
  }
36374
36420
  })();
@@ -36492,12 +36538,12 @@ async function runDashboard(argv) {
36492
36538
  if (!rows.some((r) => r.locationId === locationId2)) rows.push({ locationId: locationId2, name: e.clientName || "(no account connected)", engagement: e });
36493
36539
  }
36494
36540
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
36495
- res.end(agencyProfilePageHtml(readAgencyProfile(), rows, isLoopback(req) ? seatToken : ""));
36541
+ res.end(agencyProfilePageHtml(readAgencyProfile(), rows, mayServeSeat(req, port) ? seatToken : ""));
36496
36542
  return;
36497
36543
  }
36498
36544
  if (req.method === "GET" && url === "/brands") {
36499
36545
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
36500
- res.end(brandsPageHtml(listBrands(), houseLooks(), listReferenceLooks(), isLoopback(req) ? seatToken : ""));
36546
+ res.end(brandsPageHtml(listBrands(), houseLooks(), listReferenceLooks(), mayServeSeat(req, port) ? seatToken : ""));
36501
36547
  return;
36502
36548
  }
36503
36549
  if (req.method === "GET" && /^\/brand\/[a-z0-9-]+\/screenshot\.png$/.test(url)) {
@@ -36518,7 +36564,7 @@ async function runDashboard(argv) {
36518
36564
  const dna = houseLooks().find((d) => d.slug === slug3) ?? readBrand(slug3);
36519
36565
  const dir = brandExists(slug3) ? brandDir(slug3) : void 0;
36520
36566
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
36521
- res.end(brandCardHtml(dna, { screenshot: dir ? fs27.existsSync(path26.join(dir, "screenshot.png")) : false, dir, seatToken: isLoopback(req) ? seatToken : "" }));
36567
+ res.end(brandCardHtml(dna, { screenshot: dir ? fs27.existsSync(path26.join(dir, "screenshot.png")) : false, dir, seatToken: mayServeSeat(req, port) ? seatToken : "" }));
36522
36568
  } catch (e) {
36523
36569
  res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
36524
36570
  res.end(e.message);
@@ -36536,7 +36582,7 @@ async function runDashboard(argv) {
36536
36582
  }
36537
36583
  const job = startDesignRun(checked.value);
36538
36584
  res.writeHead(200, { "Content-Type": "application/json" });
36539
- res.end(JSON.stringify({ ok: true, id: job.id, outDir: job.outDir, note: "Started on your own Claude. This takes twenty minutes or more; poll /api/design/run?id= for progress." }));
36585
+ res.end(JSON.stringify({ ok: true, id: job.id, outDir: job.outDir, note: `Started on your own Claude. ${RUN_EXPECTATION} Poll /api/design/run?id= for progress.` }));
36540
36586
  } catch (e) {
36541
36587
  res.writeHead(400, { "Content-Type": "application/json" });
36542
36588
  res.end(JSON.stringify({ ok: false, error: e.message }));
@@ -36544,8 +36590,8 @@ async function runDashboard(argv) {
36544
36590
  });
36545
36591
  return;
36546
36592
  }
36547
- if (req.method === "GET" && url.startsWith("/api/design/run")) {
36548
- const id = new URL(url, "http://x").searchParams.get("id");
36593
+ if (req.method === "GET" && url === "/api/design/run") {
36594
+ const id = new URL(req.url || "/", "http://x").searchParams.get("id");
36549
36595
  const payload = id ? designRunStatus(id) : { runs: listDesignRuns() };
36550
36596
  res.writeHead(payload ? 200 : 404, { "Content-Type": "application/json", "Cache-Control": "no-store" });
36551
36597
  res.end(JSON.stringify(payload ?? { ok: false, error: "no such run" }));
@@ -36671,8 +36717,8 @@ async function runDashboard(argv) {
36671
36717
  });
36672
36718
  return;
36673
36719
  }
36674
- if (req.method === "GET" && url.startsWith("/handoff?")) {
36675
- const locationId2 = new URL(url, "http://x").searchParams.get("locationId") || "";
36720
+ if (req.method === "GET" && url === "/handoff") {
36721
+ const locationId2 = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
36676
36722
  if (!readableAccount(locationId2)) {
36677
36723
  res.writeHead(400);
36678
36724
  res.end("Choose a registered client.");
@@ -36775,8 +36821,8 @@ async function runDashboard(argv) {
36775
36821
  });
36776
36822
  return;
36777
36823
  }
36778
- if (req.method === "GET" && url.startsWith("/blueprint")) {
36779
- const q2 = new URL(url, "http://x").searchParams;
36824
+ if (req.method === "GET" && url === "/blueprint") {
36825
+ const q2 = new URL(req.url || "/", "http://x").searchParams;
36780
36826
  const locationId2 = q2.get("locationId") || "";
36781
36827
  const st = readCockpitState();
36782
36828
  const review = st.reviews?.[locationId2];
@@ -36790,8 +36836,8 @@ async function runDashboard(argv) {
36790
36836
  res.end(reviewDocumentHtml(review, { assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: st.assessmentPurchases?.[locationId2], planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name, agencyName: q2.get("agency") || st.agencyName, accent: q2.get("accent") || st.agencyAccent }));
36791
36837
  return;
36792
36838
  }
36793
- if (req.method === "GET" && url.startsWith("/api/review")) {
36794
- const locationId2 = new URL(url, "http://x").searchParams.get("locationId") || "";
36839
+ if (req.method === "GET" && url === "/api/review") {
36840
+ const locationId2 = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
36795
36841
  res.writeHead(200, { "Content-Type": "application/json" });
36796
36842
  res.end(JSON.stringify(readCockpitState().reviews?.[locationId2] ?? { ok: false, error: "not generated yet" }));
36797
36843
  return;
@@ -37294,6 +37340,7 @@ var init_dashboard = __esm({
37294
37340
  init_claude_cli();
37295
37341
  init_token_registry();
37296
37342
  init_run_jobs();
37343
+ init_run_expectations();
37297
37344
  init_stage_runner();
37298
37345
  init_account_rules();
37299
37346
  init_claude_call();
@@ -37382,7 +37429,7 @@ var init_dashboard = __esm({
37382
37429
  anthropicKeyWrites = Promise.resolve();
37383
37430
  SEAT_TOKEN_HEADER = "x-command-os-seat";
37384
37431
  NOT_SIGNED_IN_MSG = "This browser isn't signed in to Command OS on this computer. Open Command OS on the machine it runs on, and use the page it opens for you.";
37385
- SEAT_GUARDED_READS = ["/api/capability", "/api/client-intake", "/api/intake-documents", "/api/snapshots"];
37432
+ SEAT_GUARDED_READS = ["/api/capability", "/api/client-intake", "/api/intake-documents", "/api/snapshots", "/api/agency-connection", "/api/client-preflight"];
37386
37433
  }
37387
37434
  });
37388
37435
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.83.5",
3
+ "version": "3.83.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",