@elitedcs/ghl-mcp 3.90.0 → 3.90.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.90.1: Add them for me and Confirm and build add exactly the people you were shown
4
+
5
+ This release is everything in 3.90.0, plus the fix below. It fixes behavior that shipped in 3.89.0 and 3.90.0.
6
+
7
+ **Add them for me and Confirm and build now add exactly the people shown, mark anyone getting an admin login, and refuse if the team changed.** Step 7, Staff, lists each person with their job, marks anyone whose login will be an admin with "Admin login", and says where the list came from. Confirm and build names each login it will make, the same way, before anything is made. When you press, the cockpit reads the team again first. If it changed since you looked, nobody is added and the list is shown as it is now.
8
+
9
+ **One team, the same everywhere.** The team is Your team in step 5 when it has anyone on it, otherwise the client's intake. Your own intake draft counts only when the client has not submitted the intake. In 3.89.0 and 3.90.0 an older draft could add someone who had left, with an admin login, and Build could make logins from the intake while step 7 checked Your team.
10
+
11
+ **Someone already in the account keeps their login as it is.** If the list would give them a different level, step 7 says so, and you change it in GoHighLevel.
12
+
13
+ **After Add them for me, step 7 says what happened:** who was added, who already had a login, and anyone who got a login but is no longer on the team, so you can remove it in GoHighLevel.
14
+
15
+ **A new login is never made twice while GoHighLevel catches up.** GoHighLevel can take a minute to list a login it has just made. Until it does, step 7 and the board say the person "is still being set up in GoHighLevel", and Add them for me does not send them again, from any page or window, for up to 10 minutes. If GoHighLevel insisted on a password, step 7 now shows the one-time password, as the board does.
16
+
17
+ **Known limits, fixed in the next release.** The board's staff panel does not show when someone's login level differs from the list. If someone is taken off the team while their login is being made and the connection drops, step 7 can still say they could not be added. Without an agency key, step 7 says your plan does not let you add people. The staff step says Intake waits for it (only Build does), and says calendars and lead alerts will use the people found, which it does not check. Blueprint Review says staff are not in the account when it could not read them. When nobody's job says owner, Build makes the first person listed an admin, while Add them for me makes them a user; Build's confirmation shows it before anything is made.
18
+
19
+ After Add them for me or Confirm and build, check the client's GoHighLevel Users list.
20
+
3
21
  ## 3.90.0: the cockpit never acts on the wrong client, and refuses a change made from an old page
4
22
 
5
23
  This release is everything in 3.89.0, plus the changes below.
package/README.md CHANGED
@@ -899,7 +899,7 @@ Built by **[Elite DCs, LLC](https://elitedcs.com)** — a digital marketing and
899
899
 
900
900
  **Tech stack:** TypeScript, Node.js, esbuild, MCP SDK, Zod, GHL API v2, Firebase Auth
901
901
 
902
- **Version:** 3.90.0
902
+ **Version:** 3.90.1
903
903
 
904
904
  ---
905
905
 
package/dist/index.js CHANGED
@@ -8255,10 +8255,16 @@ function pickUsers(raw) {
8255
8255
  const last = typeof o.lastName === "string" ? o.lastName : "";
8256
8256
  const name = typeof o.name === "string" && o.name ? o.name : `${first2} ${last}`.trim();
8257
8257
  const email = typeof o.email === "string" ? o.email : void 0;
8258
- out.push({ id, name: name || id, email });
8258
+ const roles = o.roles && typeof o.roles === "object" ? o.roles : void 0;
8259
+ const role = roles && typeof roles.role === "string" ? roles.role : void 0;
8260
+ out.push({ id, name: name || id, email, ...role ? { role } : {} });
8259
8261
  }
8260
8262
  return out;
8261
8263
  }
8264
+ function isDefinitiveRefusal(error) {
8265
+ const m = /^GHL API Error (\d{3})\b/.exec(error);
8266
+ return Boolean(m && m[1].startsWith("4") && m[1] !== "408");
8267
+ }
8262
8268
  function classifyCreateUserError(error) {
8263
8269
  return error.toLowerCase().includes(FEATURE_UNAVAILABLE_TEXT) ? "feature_unavailable" : "other";
8264
8270
  }
@@ -8301,6 +8307,7 @@ async function createOrBindUserWith(args, deps) {
8301
8307
  lastError = msg(e);
8302
8308
  if (inBuildRequestScope() && isRetryableAccountError(e)) break;
8303
8309
  if (classifyCreateUserError(lastError) === "feature_unavailable") break;
8310
+ if (!isDefinitiveRefusal(lastError)) break;
8304
8311
  if (/password/i.test(lastError) && !password) {
8305
8312
  password = generateTemporaryPassword();
8306
8313
  generatedPassword = password;
@@ -36471,7 +36478,7 @@ var require_package = __commonJS({
36471
36478
  "package.json"(exports2, module2) {
36472
36479
  module2.exports = {
36473
36480
  name: "@elitedcs/ghl-mcp",
36474
- version: "3.90.0",
36481
+ version: "3.90.1",
36475
36482
  mcpName: "io.github.drjerryrelth/ghl-command",
36476
36483
  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.",
36477
36484
  main: "dist/index.js",
@@ -44293,6 +44300,64 @@ var init_normalizer = __esm({
44293
44300
  });
44294
44301
 
44295
44302
  // src/staff-first.ts
44303
+ function staffWaitingLine(name) {
44304
+ return `${name} is still being set up in GoHighLevel. Add them for me will not add them again for up to 10 minutes.`;
44305
+ }
44306
+ function createMadeNothing(outcome) {
44307
+ if (outcome.ok) return false;
44308
+ if (outcome.stage === "verify") return false;
44309
+ if (outcome.stage !== "create") return true;
44310
+ return isDefinitiveRefusal(outcome.error ?? "");
44311
+ }
44312
+ function pendingSeen(pending, check) {
44313
+ if (check.status === "unreadable") return { ...pending ?? {} };
44314
+ const listed = new Set(check.matched.map((m) => normEmail(m.email)));
44315
+ return Object.fromEntries(Object.entries(pending ?? {}).filter(([email]) => !listed.has(normEmail(email))));
44316
+ }
44317
+ function settleResults(results, check) {
44318
+ const unreadable = check.status === "unreadable";
44319
+ const listed = new Set(check.matched.map((m) => normEmail(m.email)));
44320
+ const onTeam = new Set(check.listed.map((m) => normEmail(m.email)));
44321
+ for (const r of results) {
44322
+ const e = normEmail(r.email);
44323
+ if (!unreadable && !onTeam.has(e)) continue;
44324
+ if (unreadable && r.status === "existing") continue;
44325
+ if (!unreadable && listed.has(e)) {
44326
+ r.ok = true;
44327
+ if (r.status !== "existing") r.status = "created";
44328
+ r.said = r.status === "existing" ? `${r.name} already had a login. Nothing was created.` : `${r.name} was added. GoHighLevel should email them an invitation.`;
44329
+ } else if (r.refused) {
44330
+ r.ok = false;
44331
+ delete r.status;
44332
+ r.said = r.refusedSaid ?? r.said;
44333
+ } else {
44334
+ r.ok = false;
44335
+ r.said = staffWaitingLine(r.name);
44336
+ }
44337
+ }
44338
+ return results;
44339
+ }
44340
+ function livePending(pending, nowMs) {
44341
+ const out = {};
44342
+ for (const [email, v] of Object.entries(pending ?? {})) {
44343
+ const at = Date.parse(v.at);
44344
+ if (Number.isFinite(at) && nowMs - at >= 0 && nowMs - at < STAFF_PENDING_MS) out[normEmail(email)] = v;
44345
+ }
44346
+ return out;
44347
+ }
44348
+ function applyStaffPending(check, pending, nowMs) {
44349
+ const held = livePending(pending, nowMs);
44350
+ const waiting = check.missing.filter((m) => held[normEmail(m.email)]);
44351
+ if (!waiting.length) return check;
44352
+ const isHeld = (email) => Boolean(held[normEmail(email)]);
44353
+ const lines = new Map(waiting.map((m) => [normEmail(m.email), staffWaitingLine(m.name)]));
44354
+ return {
44355
+ ...check,
44356
+ creations: (check.creations ?? []).filter((c) => !isHeld(c.email)),
44357
+ steps: check.missing.map((m, i) => lines.get(normEmail(m.email)) ?? check.steps[i]).filter((x) => typeof x === "string"),
44358
+ waiting: waiting.map((m) => ({ name: m.name, email: m.email, line: lines.get(normEmail(m.email)) }))
44359
+ };
44360
+ }
44296
44361
  function personLabel(p) {
44297
44362
  return `${p.name} (${p.email})`;
44298
44363
  }
@@ -44332,22 +44397,11 @@ function intakeAnswer(answers, key2, label2) {
44332
44397
  if (Array.isArray(v)) return v.map(String).join("\n");
44333
44398
  return void 0;
44334
44399
  }
44335
- function prefillTeamAnswers(answers, roster, questions) {
44336
- const out = { ...answers };
44337
- if (!roster) return out;
44338
- const pairs = [
44339
- [STAFF_INTAKE_KEYS.staff, roster.lines],
44340
- [STAFF_INTAKE_KEYS.notify, roster.notifyName],
44341
- [STAFF_INTAKE_KEYS.calls, roster.callsName]
44342
- ];
44343
- for (const [key2, value] of pairs) {
44344
- const label2 = questions.find((q3) => q3.key === key2)?.label;
44345
- if (label2 && value && value.trim() && !String(out[label2] ?? "").trim()) out[label2] = value;
44346
- }
44347
- return out;
44400
+ function teamBoxDecides(roster) {
44401
+ return Boolean(roster && String(roster.lines ?? "").trim());
44348
44402
  }
44349
44403
  function teamAnswersFromRoster(answers, roster, questions) {
44350
- if (!roster || !String(roster.lines ?? "").trim()) return { ...answers };
44404
+ if (!roster || !teamBoxDecides(roster)) return { ...answers };
44351
44405
  const out = { ...answers };
44352
44406
  for (const [key2, value] of [[STAFF_INTAKE_KEYS.staff, roster.lines], [STAFF_INTAKE_KEYS.notify, roster.notifyName], [STAFF_INTAKE_KEYS.calls, roster.callsName]]) {
44353
44407
  const label2 = questions.find((q3) => q3.key === key2)?.label;
@@ -44385,11 +44439,20 @@ function checkStaff(listed, users, opts = { canAddForYou: false }) {
44385
44439
  }
44386
44440
  const matched = [];
44387
44441
  const missing = [];
44442
+ const levelDiffs = [];
44388
44443
  for (const p of listed.people) {
44389
44444
  const u = findUserByEmail(users, p.email);
44390
- if (u) matched.push({ ...p, userId: u.id });
44391
- else missing.push(p);
44445
+ if (u) {
44446
+ matched.push({ ...p, userId: u.id });
44447
+ const listedRole = personToCreateInput(p).role;
44448
+ if (u.role && u.role !== listedRole) {
44449
+ const d = { name: p.name, email: p.email, ...p.role ? { job: p.role } : {}, listed: listedRole, account: u.role };
44450
+ levelDiffs.push({ ...d, line: staffLevelDiffLine(d) });
44451
+ }
44452
+ } else missing.push(p);
44392
44453
  }
44454
+ if (levelDiffs.length) Object.assign(base, { levelDiffs });
44455
+ Object.assign(base, { creations: creationsFor(missing) });
44393
44456
  if (missing.length === 0) {
44394
44457
  return {
44395
44458
  ...base,
@@ -44451,17 +44514,23 @@ function personToCreateInput(p) {
44451
44514
  ...p.mobile ? { phone: p.mobile } : {}
44452
44515
  };
44453
44516
  }
44454
- async function addMissingStaff(deps, listed, now) {
44455
- const before = await readStaffCheck(deps, listed, now);
44517
+ async function addMissingStaff(deps, listed, now, compared) {
44518
+ const before = compared ?? await readStaffCheck(deps, listed, now);
44456
44519
  if (before.status !== "needs_staff") return { results: [], check: before };
44457
44520
  if (!deps.canCreateUsers()) {
44458
44521
  return { results: [], check: before };
44459
44522
  }
44460
44523
  const results = [];
44461
44524
  let refused = false;
44525
+ const waiting = new Set((before.waiting ?? []).map((w) => normEmail(w.email)));
44462
44526
  for (const p of before.missing) {
44527
+ if (waiting.has(normEmail(p.email))) {
44528
+ results.push({ name: p.name, email: p.email, ok: false, said: staffWaitingLine(p.name) });
44529
+ continue;
44530
+ }
44463
44531
  if (refused) {
44464
- results.push({ name: p.name, email: p.email, ok: false, said: `${p.name} has to be added in GoHighLevel itself, for the same reason.` });
44532
+ const said = `${p.name} has to be added in GoHighLevel itself, for the same reason.`;
44533
+ results.push({ name: p.name, email: p.email, ok: false, refused: true, refusedSaid: said, said });
44465
44534
  continue;
44466
44535
  }
44467
44536
  let outcome;
@@ -44475,36 +44544,86 @@ async function addMissingStaff(deps, listed, now) {
44475
44544
  name: p.name,
44476
44545
  email: p.email,
44477
44546
  ok: true,
44478
- said: outcome.status === "existing" ? `${p.name} already had a login. Nothing was created.` : `${p.name} was added. GoHighLevel sends them an invitation email.`,
44547
+ status: outcome.status,
44548
+ said: outcome.status === "existing" ? `${p.name} already had a login. Nothing was created.` : `${p.name} was added. GoHighLevel should email them an invitation.`,
44479
44549
  ...outcome.generatedPassword ? { generatedPassword: outcome.generatedPassword } : {}
44480
44550
  });
44481
44551
  } else {
44482
44552
  if (outcome.errorKind === "feature_unavailable") refused = true;
44553
+ const said = outcome.errorKind === "feature_unavailable" ? `GoHighLevel does not let this agency add people from outside GoHighLevel, so ${p.name} has to be added in GoHighLevel itself.` : `${p.name} could not be added. GoHighLevel said: ${outcome.error}`;
44483
44554
  results.push({
44484
44555
  name: p.name,
44485
44556
  email: p.email,
44486
44557
  ok: false,
44487
- said: outcome.errorKind === "feature_unavailable" ? `GoHighLevel does not let this agency add people from outside GoHighLevel, so ${p.name} has to be added in GoHighLevel itself.` : `${p.name} could not be added. GoHighLevel said: ${outcome.error}`
44558
+ ...createMadeNothing(outcome) ? { refused: true, refusedSaid: said } : {},
44559
+ said
44488
44560
  });
44489
44561
  }
44490
44562
  }
44491
44563
  const after = await readStaffCheck(refused ? { listUsers: () => deps.listUsers(), canCreateUsers: () => false } : deps, listed, now);
44492
44564
  if (refused) after.createRefused = true;
44493
- for (const r of results) {
44494
- if (r.ok && after.missing.some((m) => normEmail(m.email) === normEmail(r.email))) {
44495
- r.ok = false;
44496
- r.said = `${r.name} was sent to GoHighLevel but is not on this account's staff list yet. Wait a minute, then click Check again.`;
44497
- }
44565
+ return { results: settleResults(results, after), check: after };
44566
+ }
44567
+ function effectiveStaffList(roster, intake, draftText) {
44568
+ if (roster && teamBoxDecides(roster)) return { ...listedStaff(roster), source: "team" };
44569
+ if (intake?.submitted) return { ...listedStaff(void 0, intake.text), source: "intake" };
44570
+ if (draftText && draftText.trim()) return { ...listedStaff(void 0, draftText), source: "draft" };
44571
+ return { people: [], skipped: [] };
44572
+ }
44573
+ function creationsFor(people) {
44574
+ return people.map((p) => {
44575
+ const c = personToCreateInput(p);
44576
+ return { email: normEmail(c.email), firstName: c.firstName, lastName: c.lastName, role: c.role, ...p.role ? { job: p.role } : {} };
44577
+ });
44578
+ }
44579
+ function parseShownCreations(raw) {
44580
+ if (!Array.isArray(raw)) return void 0;
44581
+ const out = [];
44582
+ for (const r of raw.slice(0, 500)) {
44583
+ if (!r || typeof r !== "object") continue;
44584
+ const x = r;
44585
+ if (typeof x.email !== "string" || !x.email.trim() || x.role !== "admin" && x.role !== "user") continue;
44586
+ out.push({ email: normEmail(x.email), firstName: typeof x.firstName === "string" ? x.firstName : "", lastName: typeof x.lastName === "string" ? x.lastName : "", role: x.role });
44498
44587
  }
44499
- return { results, check: after };
44588
+ return out;
44589
+ }
44590
+ function sameCreations(shown, live) {
44591
+ const key2 = (c) => [normEmail(c.email), fullName(c), c.role].join("|");
44592
+ const a = [...shown].map(key2).sort(), b = [...live].map(key2).sort();
44593
+ return a.length === b.length && a.every((x, i) => x === b[i]);
44594
+ }
44595
+ function staffLevelDiffLine(d) {
44596
+ return `${d.name} is in this account as ${d.account === "admin" ? "an admin" : "a user"} login. The list${d.job ? ` (${d.job})` : ""} would make ${d.listed === "admin" ? "an admin" : "a user"} login. Nothing is changed here: change it in GoHighLevel if it should be different.`;
44597
+ }
44598
+ function staffAddSaid(results) {
44599
+ const made = results.filter((r) => r.status === "created" && r.ok).map((r) => r.name);
44600
+ const bound = results.filter((r) => r.status === "existing" && r.ok).map((r) => r.name);
44601
+ const out = [];
44602
+ if (made.length) out.push(`Added: ${listSentence(made)}.`);
44603
+ if (bound.length) out.push(`${listSentence(bound)} already had a login.`);
44604
+ for (const r of results) if (!r.ok) out.push(r.said);
44605
+ return out;
44606
+ }
44607
+ function staffNotOnTeamLine(name) {
44608
+ return `${name} got a login but is not on the team now: remove it in GoHighLevel if it should not be there.`;
44609
+ }
44610
+ function staffUncheckedLine(results) {
44611
+ const made = results.filter((r) => r.status === "created" && r.ok).map((r) => r.name);
44612
+ return `${made.length ? `Added: ${listSentence(made)}. ` : ""}The team could not be checked again; look at the team list before adding more.`;
44613
+ }
44614
+ function planTeamMatches(planUsers2, people) {
44615
+ const a = [...new Set((planUsers2 ?? []).map((u) => normEmail(u.email)))].sort();
44616
+ const b = [...new Set(people.map((p) => normEmail(p.email)))].sort();
44617
+ return a.length === b.length && a.every((x, i) => x === b[i]);
44500
44618
  }
44501
- var STAFF_INTAKE_KEYS, STAFF_STEP_ABOUT, STAFF_LIST_HELP, STAFF_HOW_TO_ADD, STAFF_REVIEW_WARNING, STAFF_REVIEW_UNCHECKED, STAFF_GATED_STAGES, ADMIN_ROLE_RE;
44619
+ var STAFF_PENDING_MS, STAFF_INTAKE_KEYS, STAFF_STEP_ABOUT, STAFF_LIST_HELP, STAFF_HOW_TO_ADD, STAFF_REVIEW_WARNING, STAFF_REVIEW_UNCHECKED, STAFF_GATED_STAGES, ADMIN_ROLE_RE, fullName, STAFF_ADMIN_MARK, STAFF_SOURCE_WORDS;
44502
44620
  var init_staff_first = __esm({
44503
44621
  "src/staff-first.ts"() {
44504
44622
  "use strict";
44505
44623
  init_normalizer();
44506
44624
  init_user_provisioning();
44507
44625
  init_ghl_click_paths();
44626
+ STAFF_PENDING_MS = 10 * 60 * 1e3;
44508
44627
  STAFF_INTAKE_KEYS = { staff: "staff_members", notify: "notify_name", calls: "calls_name" };
44509
44628
  STAFF_STEP_ABOUT = "Calendars and lead alerts need real people. List everyone who takes appointments and everyone who should hear about new leads. We check this client's account for each email address before anything is built, so no calendar or alert is built with a blank name to fix later.";
44510
44629
  STAFF_LIST_HELP = "One person per line: name, email, role. For example: Jane Smith, jane@yourclinic.com, Front desk. If it is just you, add yourself.";
@@ -44513,6 +44632,13 @@ var init_staff_first = __esm({
44513
44632
  STAFF_REVIEW_UNCHECKED = "We have not checked this client's staff yet. Build will not start until everyone who takes appointments is in the account. Step 7, Staff, checks them.";
44514
44633
  STAFF_GATED_STAGES = /* @__PURE__ */ new Set([3]);
44515
44634
  ADMIN_ROLE_RE = /\b(owner|founder|admin|administrator|ceo|principal|director)\b/i;
44635
+ fullName = (c) => [c.firstName, c.lastName === "-" ? "" : c.lastName].filter(Boolean).join(" ");
44636
+ STAFF_ADMIN_MARK = "Admin login";
44637
+ STAFF_SOURCE_WORDS = {
44638
+ team: "This list is from Your team in step 5.",
44639
+ intake: "This list is from the client's intake.",
44640
+ draft: "This list is from your intake draft: the client's intake has no team on it yet."
44641
+ };
44516
44642
  }
44517
44643
  });
44518
44644
 
@@ -50704,12 +50830,12 @@ var init_build_steps = __esm({
50704
50830
  "Command OS has built and checked the account. You sign off here. The list below says what you can switch on now and what waits until its approval comes through."
50705
50831
  ];
50706
50832
  BUILD_STEPS_SCRIPT = String.raw`
50707
- let bsChange={},bsErr={},bsBusy={},bsStaff={},bsSeen={},bsDraft={},bsNote={},bsInSteps=false,bsMoreOpen={},bsInfo={};
50833
+ let bsStaffSaid={},bsChange={},bsErr={},bsBusy={},bsStaff={},bsSeen={},bsDraft={},bsNote={},bsInSteps=false,bsMoreOpen={},bsInfo={};
50708
50834
  /* A link with ?client=<id>&reconnect=1 (or the older #client=<id>&reconnect=1) opens that client's reconnect box (round-6 review). */
50709
50835
  try{for(const hp of [new URLSearchParams(location.search),new URLSearchParams(location.hash.slice(1))])if(hp.get("reconnect")==="1"&&hp.get("client"))bsMoreOpen[hp.get("client")]=true}catch(e){}
50710
50836
  async function bsPost(url,body){let r,j=null;try{r=await fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)})}catch(e){return {ok:false,error:"Command OS did not answer. Check it is still running, then try again."}}
50711
50837
  try{j=await r.json()}catch(e){}
50712
- if(!r.ok||!j||j.ok===false)return {ok:false,error:(j&&j.error)||"That did not go through, and nothing was changed.",missing:j&&j.missing,reason:j&&j.reason};
50838
+ if(!r.ok||!j||j.ok===false)return {ok:false,error:(j&&j.error)||"That did not go through, and nothing was changed.",missing:j&&j.missing,reason:j&&j.reason,check:j&&j.check,said:j&&j.said,results:j&&j.results};
50713
50839
  return j}
50714
50840
  function bsInputs(c,state,run){const id=c.locationId;const st=(((state.clients||{})[id])||{stages:[]}).stages||[];
50715
50841
  const stale=((((state.buildViews||{})[id])||{}).stale||[]).map(x=>x.stage);const acc=((state.accounts||{})[id])||{};
@@ -50870,6 +50996,8 @@ function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
50870
50996
  const BS_KIND={users:"staff member",pipelines:"pipeline",customFields:"custom field",tags:"tag",customValues:"custom value",calendars:"calendar",forms:"form",workflows:"workflow",emails:"email",sms:"text message",emailTemplates:"email template",smsTemplates:"text message template"};
50871
50997
  let bsAllow={};
50872
50998
  function bsPause(c,state,pv,box){const id=c.locationId;const key="confirm:"+id;
50999
+ /* A list from before 3.90.1 names no logins: out of date, made again before anything can be confirmed. */
51000
+ if(!Array.isArray(pv.logins)){box.appendChild(mk("p","cds-callout warn","This list is out of date. Start the build again to see the current one."));bsRun(c,state,lastRun||{},3,"Start the build",box);return}
50873
51001
  /* Ticks belong to one list: a new list starts unticked, and only keys on the list shown are sent (review of a38ebe1). */
50874
51002
  if(!bsAllow[id]||bsAllow[id].fp!==pv.fingerprint)bsAllow[id]={fp:pv.fingerprint,keys:{}};const allow=bsAllow[id].keys;
50875
51003
  box.appendChild(mk("h3","","Before Build writes anything"));
@@ -50877,6 +51005,10 @@ function bsPause(c,state,pv,box){const id=c.locationId;const key="confirm:"+id;
50877
51005
  const kinds=Object.entries(pv.createByType||{}).filter(x=>x[1]>0).map(x=>x[1]+" "+(BS_KIND[x[0]]||x[0])+(x[1]===1?"":"s"));
50878
51006
  box.appendChild(mk("h4","","Will create: "+pv.createCount+" new "+plural(pv.createCount,"item")));
50879
51007
  if(kinds.length)box.appendChild(mk("p","muted",kinds.join(", ")+"."));
51008
+ /* The logins it would make, each named with the job the team list gives and "Admin login" where it is one
51009
+ (3.90.1, Codex on ad610635), before Confirm and build can be pressed. */
51010
+ if((pv.logins||[]).length){const sc=(state.staffChecks||{})[id]||{};const jobOf=e=>{const p=[...(sc.listed||[]),...(sc.matched||[]),...(sc.missing||[])].find(x=>String(x.email).trim().toLowerCase()===String(e).trim().toLowerCase());return p&&p.role?p.role:""};
51011
+ const ul=mk("ul","cds-list bs-logins");for(const u of pv.logins){const li=mk("li","");li.appendChild(mk("span","",u.name+" ("+u.email+")"+(jobOf(u.email)?" · "+jobOf(u.email):"")));if(u.role==="admin")li.appendChild(chip("info",BS_ADMIN_MARK));ul.appendChild(li)}box.appendChild(ul)}
50880
51012
  box.appendChild(mk("h4","","Will use what is already there: "+pv.reuse.length));
50881
51013
  if(pv.reuse.length){const ul=mk("ul","cds-list");for(const e of pv.reuse)ul.appendChild(mk("li",""," "+(BS_KIND[e.type]||e.type)+" \u201c"+e.name+"\u201d, used as it is"));box.appendChild(ul)}
50882
51014
  else box.appendChild(mk("p","muted","Nothing already in the account is used."));
@@ -50985,22 +51117,28 @@ function bsReconnect(c,pr){const id=c.locationId;const key="reconnect:"+id;const
50985
51117
  function bsOpenReconnect(id){bsMoreOpen[id]=true;render(lastState,lastRun);const el=document.getElementById("bs-more-"+id);if(el)try{el.scrollIntoView({block:"start"})}catch(e){}}
50986
51118
  /* Step 7: everyone from step 5 must be a user in the client's GoHighLevel account. */
50987
51119
  function bsStaffStep(c,state,box){const id=c.locationId;const sc=(state.staffChecks||{})[id]||bsStaff[id];const key="staff:"+id;
51120
+ /* What the last Add them for me did (3.90.1): who was added, who already had a login, any login made for someone
51121
+ no longer on the team. Shown in every staff state (Codex on 516081d8); the list below is the team as it is now. */
51122
+ if((bsStaffSaid[id]||[]).length){const said=mk("div","cds-callout info cds-stack bs-staffsaid");said.setAttribute("role","status");for(const line of bsStaffSaid[id])said.appendChild(mk("p","",line));box.appendChild(said)}
50988
51123
  if(!sc&&bsErr[key]&&!bsBusy[key]){bsErrLine(key,box);box.appendChild(bsBtn("Check again",()=>{bsBusy[key]=true;delete bsErr[key];bsCheckStaff(id);render(lastState,lastRun)}));return}
50989
51124
  if(!sc&&!bsBusy[key]){bsBusy[key]=true;bsCheckStaff(id)}
50990
51125
  if(!sc){box.appendChild(mk("p","muted","Checking this client's GoHighLevel account for your team…"));return}
50991
- if(sc.status==="needs_list"){box.appendChild(mk("p","","Nobody is on the team yet. Add them under Your team in step 5, Intake."));const go=bsBtn("Open step 5",()=>{bsChange[id+":4"]=true;render(lastState,lastRun)},true);box.appendChild(go);return}
51126
+ if(sc.status==="needs_list"){box.appendChild(mk("p","","Nobody is on the team yet. Add them under Your team in step 5, Intake."));const go=bsBtn("Open step 5",()=>{bsChange[id+":4"]=true;render(lastState,lastRun)},true);box.appendChild(go);bsErrLine(key,box);return}
50992
51127
  if(sc.status==="unreadable"){box.appendChild(mk("p","cds-callout warn","We could not read this client's staff in GoHighLevel. Try Check again in a minute. If it keeps happening, reconnect this client."));box.appendChild(bsBtn("Reconnect this client",()=>bsOpenReconnect(id),true))}
51128
+ if(sc.source&&BS_STAFF_SOURCE[sc.source])box.appendChild(mk("p","muted bs-source",BS_STAFF_SOURCE[sc.source]));
50993
51129
  const ul=mk("ul","cds-list bs-people");
50994
- for(const m of (sc.matched||[])){const li=mk("li","");li.appendChild(chip("ok","In the account"));li.appendChild(mk("span",""," "+m.name+" · "+m.email));ul.appendChild(li)}
50995
- for(const p of (sc.missing||[])){const li=mk("li","");li.appendChild(chip("warn","Not added yet"));li.appendChild(mk("span",""," "+p.name+" · "+p.email+" "));
51130
+ for(const m of (sc.matched||[])){const li=mk("li","");li.appendChild(chip("ok","In the account"));li.appendChild(mk("span",""," "+m.name+" · "+m.email+(m.role?" · "+m.role:"")));ul.appendChild(li)}
51131
+ for(const p of (sc.missing||[])){const li=mk("li","");const w=(sc.waiting||[]).find(x=>x.email===p.email);if(!w)li.appendChild(chip("warn","Not added yet"));const cr=(sc.creations||[]).find(x=>x.email===String(p.email).trim().toLowerCase());if(cr&&cr.role==="admin")li.appendChild(chip("info",BS_ADMIN_MARK));li.appendChild(mk("span",""," "+p.name+" · "+p.email+(p.role?" · "+p.role:"")+" "));if(w)li.appendChild(mk("span","muted",w.line+" "));
50996
51132
  for(const [lab,val] of [["Copy name",p.name],["Copy email",p.email]]){const cp=mk("button","cds-link",lab);cp.type="button";cp.onclick=async()=>{try{await navigator.clipboard.writeText(val);cp.textContent="Copied"}catch(e){cp.textContent=val}};li.appendChild(cp);li.appendChild(document.createTextNode(" "))}
50997
51133
  ul.appendChild(li)}
50998
51134
  box.appendChild(ul);
51135
+ for(const d of (sc.levelDiffs||[]))box.appendChild(mk("p","cds-callout warn bs-level",d.line));
51136
+
50999
51137
  if((sc.missing||[]).length){
51000
- if(sc.canAddForYou&&!sc.createRefused){box.appendChild(mk("p","","We can add them to this client's GoHighLevel account for you. Each person gets a login email from GoHighLevel, so let your client know."));
51001
- box.appendChild(bsBtn(bsBusy[key+":add"]?"Adding…":"Add them for me",async()=>{bsBusy[key+":add"]=true;delete bsErr[key];render(lastState,lastRun);const j=await bsPost("/api/staff/add",{locationId:id});bsBusy[key+":add"]=false;if(!j.ok)bsErr[key]=j.error;else bsStaff[id]=j.check;load(true)}))}
51138
+ if(sc.canAddForYou&&!sc.createRefused){if((sc.creations||[]).length){box.appendChild(mk("p","","We can add them to this client's GoHighLevel account for you. GoHighLevel should email each new person an invitation. Let your client know, and check with them that it arrived."));
51139
+ box.appendChild(bsBtn(bsBusy[key+":add"]?"Adding…":"Add them for me",async()=>{bsBusy[key+":add"]=true;delete bsErr[key];render(lastState,lastRun);const j=await bsPost("/api/staff/add",{locationId:id,shown:sc.creations||[]});bsBusy[key+":add"]=false;bsStaffSaid[id]=[...(j.ok?(j.said||[]):[]),...(j.results||[]).filter(x=>x.generatedPassword).map(x=>x.said+" Their one-time password is "+x.generatedPassword+". Give it to them privately; it is not saved anywhere.")];if(!j.ok){bsErr[key]=j.error;if(j.check)bsStaff[id]=j.check}else bsStaff[id]=j.check;load(true)}))}}
51002
51140
  else{const how=mk("ol","bs-how");for(const t of BS_HOW_TO_ADD)how.appendChild(mk("li","",t));box.appendChild(mk("p","","GoHighLevel only lets you add people in its own screens on your plan. Do this once for each person marked Not added yet:"));box.appendChild(how)}}
51003
- const again=bsBtn(bsBusy[key]?"Checking…":"Check again",()=>{bsBusy[key]=true;bsCheckStaff(id);render(lastState,lastRun)},sc.status==="ready");box.appendChild(again);
51141
+ const again=bsBtn(bsBusy[key]?"Checking…":"Check again",()=>{bsBusy[key]=true;delete bsStaffSaid[id];bsCheckStaff(id);render(lastState,lastRun)},sc.status==="ready");box.appendChild(again);
51004
51142
  bsErrLine(key,box)}
51005
51143
  async function bsCheckStaff(id){const key="staff:"+id;let j=null;try{j=await(await fetch("/api/staff/check",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:id})})).json()}catch(e){}
51006
51144
  bsBusy[key]=false;if(j&&j.ok){bsStaff[id]=j.check;delete bsErr[key]}else bsErr[key]=(j&&j.error)||"We could not check this client's staff. Try Check again.";load(true)}
@@ -51096,7 +51234,9 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
51096
51234
  for(let i=0;i<n;i++){const ck=id+":"+i;
51097
51235
  /* A Change closes itself once that step is done with a different answer (review of 64da59d, LOW 15). */
51098
51236
  if(bsChange[ck]&&pr.done[i]&&typeof bsChange[ck]==="string"&&bsSummary(i,c,state)!==bsChange[ck])delete bsChange[ck];
51099
- const li=mk("li","bs-step");li.id="step-"+(i+1);const open=(i===pr.current)||Boolean(bsChange[ck]);
51237
+ /* Step 7 stays open while what the last Add them for me said, or its refusal, is there to read (3.90.1). */
51238
+ const staffNews=i===6&&(((bsStaffSaid[id]||[]).length>0)||Boolean(bsErr["staff:"+id]));
51239
+ const li=mk("li","bs-step");li.id="step-"+(i+1);const open=(i===pr.current)||Boolean(bsChange[ck])||staffNews;
51100
51240
  const num=mk("span","bs-num",String(i+1));num.setAttribute("aria-hidden","true");li.appendChild(num);const main=mk("div","bs-main");li.appendChild(main);
51101
51241
  if(open){li.className+=" bs-now";main.appendChild(mk("div","bs-kicker","Step "+(i+1)+" of "+n+(i===pr.current?" · do this now":" · changing")));main.appendChild(mk("h3","bs-title",BS_NAMES[i]));main.appendChild(mk("p","muted",BS_ABOUT[i]));
51102
51242
  if(inp.parked&&i>=2){main.appendChild(mk("p","cds-callout info","This is a "+TYPE_LABELS[typeOf(state,id)].toLowerCase()+" account, so nothing is built into it. Change step 2 to Client or Practice sandbox to build it."))}
@@ -56513,9 +56653,14 @@ function previewFingerprint(p) {
56513
56653
  locationId: p.locationId,
56514
56654
  planHash: p.planHash,
56515
56655
  reuse: p.reuse.map((e) => e.key).sort(),
56516
- change: p.change.map((e) => [e.key, (e.changes ?? []).map(stableChange).sort(), e.state ?? ""]).sort((a, b) => String(a[0]).localeCompare(String(b[0])))
56656
+ change: p.change.map((e) => [e.key, (e.changes ?? []).map(stableChange).sort(), e.state ?? ""]).sort((a, b) => String(a[0]).localeCompare(String(b[0]))),
56657
+ // 3.90.1 (Codex on e4734bc4): the logins the confirmation names are part of what was confirmed.
56658
+ logins: (p.logins ?? []).map((l) => `${l.email.trim().toLowerCase()}|${l.name}|${l.role}`).sort()
56517
56659
  });
56518
56660
  }
56661
+ function previewNamesLogins(preview) {
56662
+ return Boolean(preview && Array.isArray(preview.logins));
56663
+ }
56519
56664
  function composePreview(input) {
56520
56665
  const reuse = [];
56521
56666
  const change = [];
@@ -56540,7 +56685,7 @@ function composePreview(input) {
56540
56685
  else reuse.push({ key: key2, type: item.type, name: item.name, id: item.existingId });
56541
56686
  }
56542
56687
  if (input.board) change.push({ key: `${BOARD_KEY_PREFIX}${input.board.id}`, type: "customValues", name: input.board.name, id: input.board.id, changes: [BOARD_CHANGE] });
56543
- const core = { locationId: input.locationId, planHash: input.planHash, reuse, change };
56688
+ const core = { locationId: input.locationId, planHash: input.planHash, reuse, change, logins: [...input.logins ?? []] };
56544
56689
  return { v: 1, recordKey: input.recordKey, at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(), createCount, createByType, ...core, fingerprint: previewFingerprint(core) };
56545
56690
  }
56546
56691
  function allowedWorkflowIds(allow) {
@@ -56560,6 +56705,7 @@ function boardAllowed(preview, confirm2) {
56560
56705
  return Boolean(confirm2 && confirm2.fingerprint === preview.fingerprint && confirm2.allow.includes(board.key));
56561
56706
  }
56562
56707
  function confirmMatches(confirm2, preview) {
56708
+ if (!previewNamesLogins(preview)) return false;
56563
56709
  if (!confirm2 || confirm2.fingerprint !== preview.fingerprint || !Array.isArray(confirm2.allow)) return false;
56564
56710
  const changeKeys = new Set(preview.change.map((e) => e.key));
56565
56711
  return confirm2.allow.every((k) => changeKeys.has(k));
@@ -57046,7 +57192,6 @@ __export(dashboard_exports, {
57046
57192
  overlayPath: () => overlayPath,
57047
57193
  parseIntakeShape: () => parseIntakeShape,
57048
57194
  planClaimChecks: () => planClaimChecks,
57049
- prefillTeamAnswers: () => prefillTeamAnswers,
57050
57195
  proPickerHtml: () => proPickerHtml,
57051
57196
  queueScanFilter: () => queueScanFilter,
57052
57197
  queueScanOnce: () => queueScanOnce,
@@ -57092,6 +57237,7 @@ __export(dashboard_exports, {
57092
57237
  stateSaveFs: () => stateSaveFs,
57093
57238
  statusFromThisRun: () => statusFromThisRun,
57094
57239
  syncVerifyVerdict: () => syncVerifyVerdict,
57240
+ teamAnswersFromRoster: () => teamAnswersFromRoster,
57095
57241
  uniqueDocName: () => uniqueDocName,
57096
57242
  unknownCapabilityHtml: () => unknownCapabilityHtml,
57097
57243
  unverifiedStaffNames: () => unverifiedStaffNames,
@@ -57365,6 +57511,12 @@ function offeredFeaturesFor(state2, recordKey) {
57365
57511
  function boughtWordingFor(state2, recordKey) {
57366
57512
  return { programFollowUp: programFollowUpName(presetWorkflowsFor(state2, recordKey)) };
57367
57513
  }
57514
+ function savedPlanTeamIsCurrent(sp, answers, questions) {
57515
+ const users = sp.plan && typeof sp.plan === "object" ? sp.plan.users : void 0;
57516
+ const planUsers2 = Array.isArray(users) ? users.filter((u) => Boolean(u) && typeof u.email === "string") : [];
57517
+ const label2 = questions.find((q3) => q3.key === STAFF_INTAKE_KEYS.staff)?.label;
57518
+ return planTeamMatches(planUsers2, listedStaff(void 0, intakeAnswer(answers, STAFF_INTAKE_KEYS.staff, label2)).people);
57519
+ }
57368
57520
  function documentScopeFor(state2, locationId2, savedPlan) {
57369
57521
  const sc = state2.scope?.[locationId2];
57370
57522
  const offered = offeredFeaturesFor(state2, locationId2);
@@ -57530,9 +57682,11 @@ function startRun(locationId2, stage, onDone, meta = {}) {
57530
57682
  // changed selection composes the plan fresh from the preset (delta round 3, T-M4).
57531
57683
  // Rehearsal 5 (BUG-2026-09-30-10): Build is told who the staff check matched in this account.
57532
57684
  ...stage === 3 ? { verifiedStaff: verifiedStaffNames(state2.staffChecks?.[locationId2]) } : {},
57685
+ // A saved plan is reused only for the team the rule gives now (3.90.1): one saved under an older team (a draft,
57686
+ // or before Your team decided) is made again, never built with someone the member was not shown.
57533
57687
  savedPlan: (() => {
57534
57688
  const sp = stage === 3 ? readSavedPlan(rk) : null;
57535
- return sp && sp.inputHash === meta.inputHash && savedPlanMatchesScope(sp, state2.scope?.[rk]) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
57689
+ return sp && sp.inputHash === meta.inputHash && savedPlanMatchesScope(sp, state2.scope?.[rk]) && savedPlanTeamIsCurrent(sp, meta.normalizedAnswers, composed) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
57536
57690
  })(),
57537
57691
  textSnippets: Boolean(state2.textSnippets?.[rk])
57538
57692
  };
@@ -59411,13 +59565,19 @@ a.textContent="Open Your agency";box.appendChild(a);return box}
59411
59565
  person listed here is found in the account by email. The server reads the account every
59412
59566
  time; nothing on this panel is taken as proof on its own. */
59413
59567
  let staffBusy={},staffSaid={},staffErr={};
59568
+ /* Whether Add them for me would make this person an admin (the check's own creations, never guessed here). */
59569
+ function staffAdminFor(check,m){const cr=((check&&check.creations)||[]).find(x=>x.email===String(m.email).trim().toLowerCase());return Boolean(cr&&cr.role==="admin")}
59414
59570
  function staffCard(c,state){const box=document.createElement("section");box.className="cds-panel ipanel staffstep";box.id="staff-"+c.locationId;
59415
59571
  const check=(state.staffChecks||{})[c.locationId];const roster=(state.staffRoster||{})[c.locationId]||{};
59416
59572
  const h=document.createElement("h3");h.className="cds-panel-title";h.textContent=check&&check.status==="ready"?"Your staff are in place":"Add your staff";box.appendChild(h);
59417
59573
  box.appendChild(mk("p","",check?check.headline+". "+check.body:"Calendars and lead alerts need real people. List everyone who takes appointments and everyone who should hear about new leads. We check this client's account for each email address before anything is built."));
59418
- if(check&&(check.matched||[]).length){const ul=mk("ul","cds-list");for(const m of check.matched){const li=mk("li");li.appendChild(mk("span","ttl",m.name+" ("+m.email+")"));const end=mk("span","end");end.appendChild(chip("ok","In the account"));li.appendChild(end);ul.appendChild(li)}
59419
- for(const m of check.missing||[]){const li=mk("li");li.appendChild(mk("span","ttl",m.name+" ("+m.email+")"));const end=mk("span","end");end.appendChild(chip("warn","Not in the account yet"));li.appendChild(end);ul.appendChild(li)}box.appendChild(ul)}
59420
- else if(check&&(check.missing||[]).length){const ul=mk("ul","cds-list");for(const m of check.missing){const li=mk("li");li.appendChild(mk("span","ttl",m.name+" ("+m.email+")"));const end=mk("span","end");end.appendChild(chip("warn","Not in the account yet"));li.appendChild(end);ul.appendChild(li)}box.appendChild(ul)}
59574
+ /* One listing in every state (3.90.1, Codex on 516081d8): where the list came from, then each person with their
59575
+ job, and "Admin login" on anyone Add them for me would make an admin, before it can be pressed. */
59576
+ if(check&&((check.matched||[]).length||(check.missing||[]).length)){if(check.source&&BS_STAFF_SOURCE[check.source])box.appendChild(mk("p","small",BS_STAFF_SOURCE[check.source]));
59577
+ const ul=mk("ul","cds-list");
59578
+ for(const m of check.matched||[]){const li=mk("li");li.appendChild(mk("span","ttl",m.name+" ("+m.email+")"+(m.role?" \xB7 "+m.role:"")));const end=mk("span","end");end.appendChild(chip("ok","In the account"));li.appendChild(end);ul.appendChild(li)}
59579
+ for(const m of check.missing||[]){const li=mk("li");li.appendChild(mk("span","ttl",m.name+" ("+m.email+")"+(m.role?" \xB7 "+m.role:"")));const end=mk("span","end");const w=(check.waiting||[]).find(x=>x.email===m.email);if(staffAdminFor(check,m))end.appendChild(chip("info",BS_ADMIN_MARK));end.appendChild(w?mk("span","small",w.line):chip("warn","Not in the account yet"));li.appendChild(end);ul.appendChild(li)}
59580
+ box.appendChild(ul)}
59421
59581
  for(const line of (check&&check.steps)||[])box.appendChild(mk("p","small",line));
59422
59582
  if(check&&check.createRefused)box.appendChild(mk("p","small","GoHighLevel does not let this agency add people from outside GoHighLevel, so please add them yourself."));
59423
59583
  if(check&&check.howToAdd)box.appendChild(mk("p","small",check.howToAdd));
@@ -59434,13 +59594,18 @@ function staffCard(c,state){const box=document.createElement("section");box.clas
59434
59594
  /* Rehearsal 3: creating the logins asks in a panel under the button, and what GoHighLevel said (with any
59435
59595
  one-time password) stays on the page until Done, never in confirm() or alert(). The passwords live
59436
59596
  only in this page's memory. */
59437
- if(check&&check.status==="needs_staff"&&check.canAddForYou){const add=document.createElement("button");add.className="cds-btn";add.textContent="Add them for me";const addKey="staffadd:"+c.locationId;
59597
+ if(check&&check.status==="needs_staff"&&check.canAddForYou&&(check.creations||[]).length){const add=document.createElement("button");add.className="cds-btn";add.textContent="Add them for me";const addKey="staffadd:"+c.locationId;
59438
59598
  add.onclick=()=>{let r=null;icShow(add,addKey,{title:"Create GoHighLevel logins in "+c.name+"?",
59439
- text:[(check.missing||[]).map(m=>m.name+" ("+m.email+")").join(", ")+".","GoHighLevel sends each person an invitation email. We then read the account back to confirm they arrived."],
59599
+ text:[(check.missing||[]).filter(m=>!(check.waiting||[]).some(x=>x.email===m.email)).map(m=>m.name+" ("+m.email+")"+(m.role?" \xB7 "+m.role:"")+(staffAdminFor(check,m)?" \xB7 "+BS_ADMIN_MARK:"")).join(", ")+".","GoHighLevel should email each new person an invitation. We then read the account back to confirm they arrived."],
59440
59600
  ok:"Create the logins",busyLabel:"Creating the logins\u2026",
59441
- run:async()=>{r=await(await clientPageRequest("/api/staff/add",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId})})).json();
59442
- return !r||r.ok===false?clientPageError((r&&r.error)||"That did not go through. Click Check again to see where things stand."):""},
59443
- after:async()=>{const said=(r.results||[]).map(x=>x.said+(x.generatedPassword?" Their one-time password is "+x.generatedPassword+". Give it to them privately; it is not saved anywhere.":""));if(said.length)staffSaid[c.locationId]=said;load()}})};
59601
+ run:async()=>{r=await(await clientPageRequest("/api/staff/add",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,shown:check.creations||[]})})).json();
59602
+ /* Any answer closes this confirmation (it showed the list as it was) and its lines go to the panel's own box,
59603
+ which stays through the redraw until Done (3.90.1, Codex on e4734bc4); a refusal's team is drawn at once. */
59604
+ const lines=!r?[clientPageError("That did not go through. Click Check again to see where things stand.")]:[...(r.ok===false?[clientPageError(r.error||"That did not go through. Click Check again to see where things stand.")]:[]),...(r.said||[]),...(r.results||[]).filter(x=>x.generatedPassword).map(x=>x.said+" Their one-time password is "+x.generatedPassword+". Give it to them privately; it is not saved anywhere.")];
59605
+ if(lines.length)staffSaid[c.locationId]=lines;
59606
+ if(r&&r.check&&lastState)lastState.staffChecks=Object.assign({},lastState.staffChecks,{[c.locationId]:r.check});
59607
+ return ""},
59608
+ after:async()=>{delete icOpen[addKey];load()}})};
59444
59609
  btns.appendChild(icAfter(add,addKey))}
59445
59610
  box.appendChild(btns);
59446
59611
  if(staffErr[c.locationId])showClientError(staffErr[c.locationId],box);
@@ -60529,7 +60694,7 @@ function renderPage(state,run){if(state.roster)mergeRoster(state.roster);syncRev
60529
60694
  d.appendChild(mk("summary","","Show the "+parked.length+" template and hidden "+plural(parked.length,"account")));
60530
60695
  d.appendChild(listRows(rows.slice(board.length),"clients-parked"));list.appendChild(d)}
60531
60696
  m.appendChild(list)}
60532
- const BS_TEAM_KEYS=${scriptJson(Object.values(STAFF_INTAKE_KEYS))};const BS_TOPICS=${scriptJson(Object.fromEntries(GO_LIVE_TOPICS.map((g) => [g.key, g.title])))};const BS_NAMES=${scriptJson(STEP_NAMES)};const BS_ABOUT=${scriptJson(STEP_ABOUT)};const BS_HOW_TO_ADD=${scriptJson(BS_HOW_TO_ADD)};const BS_TYPE_HELP=${scriptJson(BS_TYPE_HELP)};
60697
+ const BS_TEAM_KEYS=${scriptJson(Object.values(STAFF_INTAKE_KEYS))};const BS_ADMIN_MARK=${scriptJson(STAFF_ADMIN_MARK)};const BS_STAFF_SOURCE=${scriptJson(STAFF_SOURCE_WORDS)};const BS_TOPICS=${scriptJson(Object.fromEntries(GO_LIVE_TOPICS.map((g) => [g.key, g.title])))};const BS_NAMES=${scriptJson(STEP_NAMES)};const BS_ABOUT=${scriptJson(STEP_ABOUT)};const BS_HOW_TO_ADD=${scriptJson(BS_HOW_TO_ADD)};const BS_TYPE_HELP=${scriptJson(BS_TYPE_HELP)};
60533
60698
  const stepProgress=(${stepProgress.toString()});
60534
60699
  let bsHideAssess=false;
60535
60700
  ${BUILD_STEPS_SCRIPT}
@@ -60959,15 +61124,31 @@ async function runDashboard(argv) {
60959
61124
  const tok = registry2.getToken(locationId2);
60960
61125
  return tok?.apiKey ? new GHLClient({ apiKey: tok.apiKey, locationId: locationId2 }) : void 0;
60961
61126
  };
60962
- const staffListedFor = (locationId2) => {
61127
+ const staffListedSaved = (locationId2) => {
60963
61128
  const st = readCockpitState();
60964
61129
  const questions = composeQuestionSet({ industry: st.accounts?.[locationId2]?.industry, agency: readOverlay() });
60965
61130
  const label2 = questions.find((q3) => q3.key === STAFF_INTAKE_KEYS.staff)?.label;
61131
+ const saved = st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers;
61132
+ return effectiveStaffList(
61133
+ st.staffRoster?.[locationId2],
61134
+ saved ? { submitted: true, text: intakeAnswer(saved, STAFF_INTAKE_KEYS.staff, label2) } : void 0,
61135
+ intakeAnswer(st.intakeDrafts?.[locationId2]?.answers, STAFF_INTAKE_KEYS.staff, label2)
61136
+ );
61137
+ };
61138
+ void staffListedSaved;
61139
+ const staffListedLive = async (locationId2) => {
61140
+ const st = readCockpitState();
60966
61141
  const roster2 = st.staffRoster?.[locationId2];
60967
- if (roster2 && String(roster2.lines ?? "").trim()) return listedStaff(roster2);
60968
- return listedStaff(
60969
- roster2,
60970
- intakeAnswer(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers, STAFF_INTAKE_KEYS.staff, label2),
61142
+ if (roster2 && String(roster2.lines ?? "").trim()) return effectiveStaffList(roster2, void 0, void 0);
61143
+ const c = clientFor(locationId2);
61144
+ if (!c) throw new Error("This client is not connected yet.");
61145
+ const snap = await readIntakeSnapshot(c, locationId2);
61146
+ if (snap.status === "unavailable") throw new Error("We could not check this client's staff. Try Check again.");
61147
+ const questions = composeQuestionSet({ industry: st.accounts?.[locationId2]?.industry, agency: readOverlay() });
61148
+ const label2 = questions.find((q3) => q3.key === STAFF_INTAKE_KEYS.staff)?.label;
61149
+ return effectiveStaffList(
61150
+ void 0,
61151
+ { submitted: snap.status === "received", text: intakeAnswer(snap.answers, STAFF_INTAKE_KEYS.staff, label2) },
60971
61152
  intakeAnswer(st.intakeDrafts?.[locationId2]?.answers, STAFF_INTAKE_KEYS.staff, label2)
60972
61153
  );
60973
61154
  };
@@ -60988,18 +61169,47 @@ async function runDashboard(argv) {
60988
61169
  }
60989
61170
  };
60990
61171
  };
61172
+ const staffAddChain = /* @__PURE__ */ new Map();
61173
+ const oneStaffAddAtATime = (locationId2, run) => {
61174
+ const tail = (staffAddChain.get(locationId2) ?? Promise.resolve()).then(run, run).catch(() => void 0);
61175
+ staffAddChain.set(locationId2, tail);
61176
+ void tail.then(() => {
61177
+ if (staffAddChain.get(locationId2) === tail) staffAddChain.delete(locationId2);
61178
+ });
61179
+ return tail;
61180
+ };
61181
+ const holdStaff = (locationId2, email, v) => {
61182
+ const latest = readCockpitState();
61183
+ const mine = { ...latest.staffPending?.[locationId2] ?? {} };
61184
+ if (v) mine[normEmail(email)] = v;
61185
+ else delete mine[normEmail(email)];
61186
+ writeCockpitState({ ...latest, staffPending: { ...latest.staffPending, [locationId2]: mine } });
61187
+ };
61188
+ const endSeenHolds = (locationId2, check) => {
61189
+ const latest = readCockpitState();
61190
+ const was = latest.staffPending?.[locationId2];
61191
+ if (!was) return;
61192
+ const now = pendingSeen(was, check);
61193
+ if (Object.keys(now).length !== Object.keys(was).length) writeCockpitState({ ...latest, staffPending: { ...latest.staffPending, [locationId2]: now } });
61194
+ };
60991
61195
  const saveStaffCheck = (locationId2, check) => {
60992
61196
  const latest = readCockpitState();
60993
61197
  writeCockpitState({ ...latest, staffChecks: { ...latest.staffChecks, [locationId2]: check } });
60994
61198
  };
60995
61199
  const staffCheckFor = async (locationId2, save = true) => {
60996
- const check = await readStaffCheck(staffDepsFor(locationId2), staffListedFor(locationId2));
60997
- if (save) saveStaffCheck(locationId2, check);
61200
+ const listed = await staffListedLive(locationId2);
61201
+ const check = applyStaffPending(await readStaffCheck(staffDepsFor(locationId2), listed), readCockpitState().staffPending?.[locationId2], Date.now());
61202
+ if (listed.source) check.source = listed.source;
61203
+ if (save) {
61204
+ saveStaffCheck(locationId2, check);
61205
+ endSeenHolds(locationId2, check);
61206
+ }
60998
61207
  return check;
60999
61208
  };
61000
61209
  staffGateRef = async (locationId2, stage) => {
61001
61210
  if (!STAFF_GATED_STAGES.has(stage)) return null;
61002
- return staffGateReason(await staffCheckFor(locationId2), STAGES2[stage] === "Staged Build" ? "Build" : STAGES2[stage]);
61211
+ const check = await staffCheckFor(locationId2).catch(() => checkStaff({ people: [], skipped: [] }, { error: "unreadable" }, { canAddForYou: false }));
61212
+ return staffGateReason(check, STAGES2[stage] === "Staged Build" ? "Build" : STAGES2[stage]);
61003
61213
  };
61004
61214
  const readAgencySnapshots = async () => {
61005
61215
  const agencyKey = registry2.getAgencyKey();
@@ -61900,25 +62110,82 @@ ${opportunityNoteFailure?.detail ?? ""}`);
61900
62110
  return;
61901
62111
  }
61902
62112
  if (req.method === "POST" && url === "/api/staff/add") {
61903
- readBody(req, res, 2e3, async (body) => {
62113
+ readBody(req, res, 2e4, async (body) => {
62114
+ let queued = "";
61904
62115
  try {
61905
- const { locationId: locationId2 } = JSON.parse(body);
61906
- if (typeof locationId2 !== "string" || !readableAccount(locationId2)) throw new Error("Choose a registered client.");
61907
- const perm = buildPermission(locationId2, { name: currentRoster().find((r) => r.locationId === locationId2)?.name, approvedAt: (/* @__PURE__ */ new Date()).toISOString() });
61908
- if (!perm.allowed) throw new Error(perm.reason);
61909
- const out = await addMissingStaff(staffDepsFor(locationId2), staffListedFor(locationId2));
61910
- if (out.check.createRefused) {
61911
- const latest = readCockpitState();
61912
- writeCockpitState({ ...latest, staffAddRefused: { ...latest.staffAddRefused, [locationId2]: (/* @__PURE__ */ new Date()).toISOString() } });
61913
- }
61914
- saveStaffCheck(locationId2, out.check);
61915
- writeCockpitState(recordStaffAdd(readCockpitState(), locationId2, currentActor()));
61916
- res.writeHead(200, { "Content-Type": "application/json" });
61917
- res.end(JSON.stringify({ ok: true, ...out }));
61918
- } catch (e) {
61919
- res.writeHead(400, { "Content-Type": "application/json" });
61920
- res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "bad request" }));
62116
+ const id = JSON.parse(body).locationId;
62117
+ if (typeof id === "string") queued = id;
62118
+ } catch {
61921
62119
  }
62120
+ await oneStaffAddAtATime(queued, async () => {
62121
+ try {
62122
+ const { locationId: locationId2, shown: shownRaw } = JSON.parse(body);
62123
+ if (typeof locationId2 !== "string" || !readableAccount(locationId2)) throw new Error("Choose a registered client.");
62124
+ const perm = buildPermission(locationId2, { name: currentRoster().find((r) => r.locationId === locationId2)?.name, approvedAt: (/* @__PURE__ */ new Date()).toISOString() });
62125
+ if (!perm.allowed) throw new Error(perm.reason);
62126
+ const deps = staffDepsFor(locationId2);
62127
+ const listed = await staffListedLive(locationId2);
62128
+ const live = applyStaffPending(await readStaffCheck(deps, listed), readCockpitState().staffPending?.[locationId2], Date.now());
62129
+ if (listed.source) live.source = listed.source;
62130
+ const shown = parseShownCreations(shownRaw);
62131
+ if (live.status === "unreadable") throw new Error("We could not check this client's staff. Try Check again.");
62132
+ const changed = shown ? sameCreations(shown, live.creations ?? []) ? [] : ["the team changed"] : ["this page did not send the list it showed; reload the page"];
62133
+ if (!shown || changed.length) {
62134
+ saveStaffCheck(locationId2, live);
62135
+ const refusal2 = staleRefusal("the team", changed);
62136
+ res.writeHead(refusal2.status, { "Content-Type": "application/json" });
62137
+ res.end(JSON.stringify({ ...refusal2.body, check: live }));
62138
+ return;
62139
+ }
62140
+ const out = await addMissingStaff({ ...deps, createUser: async (u) => {
62141
+ const at = (/* @__PURE__ */ new Date()).toISOString();
62142
+ const name = [u.firstName, u.lastName === "-" ? "" : u.lastName].filter(Boolean).join(" ");
62143
+ holdStaff(locationId2, u.email, { name, at, phase: "creating" });
62144
+ const outcome = await deps.createUser(u);
62145
+ if (outcome.ok && outcome.status === "created") holdStaff(locationId2, u.email, { name, at, phase: "created" });
62146
+ else if (outcome.ok || createMadeNothing(outcome)) holdStaff(locationId2, u.email, void 0);
62147
+ return outcome;
62148
+ } }, listed, void 0, live);
62149
+ out.check = applyStaffPending(out.check, readCockpitState().staffPending?.[locationId2], Date.now());
62150
+ if (out.check.createRefused) {
62151
+ const latest = readCockpitState();
62152
+ writeCockpitState({ ...latest, staffAddRefused: { ...latest.staffAddRefused, [locationId2]: (/* @__PURE__ */ new Date()).toISOString() } });
62153
+ }
62154
+ writeCockpitState(recordStaffAdd(readCockpitState(), locationId2, currentActor()));
62155
+ let said = staffAddSaid(out.results);
62156
+ const createdOf = () => out.results.filter((r) => r.status === "created").map((r) => normEmail(r.email));
62157
+ let created = createdOf();
62158
+ const after = await staffListedLive(locationId2).catch(() => null);
62159
+ const reply = (status, body2) => {
62160
+ res.writeHead(status, { "Content-Type": "application/json" });
62161
+ res.end(JSON.stringify(body2));
62162
+ };
62163
+ if (!after) {
62164
+ saveStaffCheck(locationId2, out.check);
62165
+ reply(409, { ok: false, written: created.length > 0, error: staffUncheckedLine(out.results), said, results: out.results, created, check: out.check });
62166
+ return;
62167
+ }
62168
+ const fresh = applyStaffPending(await readStaffCheck(deps, after), readCockpitState().staffPending?.[locationId2], Date.now());
62169
+ if (fresh.status === "unreadable") {
62170
+ saveStaffCheck(locationId2, out.check);
62171
+ reply(409, { ok: false, written: created.length > 0, error: staffUncheckedLine(out.results), said, results: out.results, created, check: out.check });
62172
+ return;
62173
+ }
62174
+ if (after.source) fresh.source = after.source;
62175
+ if (out.check.createRefused) fresh.createRefused = true;
62176
+ fresh.canAddForYou = fresh.canAddForYou && out.check.canAddForYou;
62177
+ saveStaffCheck(locationId2, fresh);
62178
+ endSeenHolds(locationId2, fresh);
62179
+ said = staffAddSaid(settleResults(out.results, fresh));
62180
+ created = createdOf();
62181
+ const listedNow = new Set(after.people.map((p) => normEmail(p.email)));
62182
+ for (const r of out.results) if (r.status === "created" && !listedNow.has(normEmail(r.email))) said.push(staffNotOnTeamLine(r.name));
62183
+ reply(200, { ok: true, said, results: out.results, created, check: fresh });
62184
+ } catch (e) {
62185
+ res.writeHead(400, { "Content-Type": "application/json" });
62186
+ res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "bad request" }));
62187
+ }
62188
+ });
61922
62189
  });
61923
62190
  return;
61924
62191
  }
@@ -62099,7 +62366,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
62099
62366
  inputHash = intakeInput(normalizedAnswers, st0.accounts?.[locationId2]?.industry, composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }));
62100
62367
  st0 = { ...st0, intakeInputs: { ...st0.intakeInputs, [locationId2]: { hash: inputHash, answers: normalizedAnswers, status: bound.status, shape: intakeShapeOf(snap) } } };
62101
62368
  writeCockpitState(st0);
62102
- if (stage !== 4) normalizedAnswers = prefillTeamAnswers(normalizedAnswers, st0.staffRoster?.[locationId2], composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }));
62369
+ if (stage !== 4) normalizedAnswers = teamAnswersFromRoster(normalizedAnswers, st0.staffRoster?.[locationId2], composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }));
62103
62370
  if (stage >= 3) {
62104
62371
  assertBuildApproval(st0.planApprovals?.[locationId2], inputHash);
62105
62372
  if (stage === 3) assertStaffUnchanged(st0.planApprovals?.[locationId2], st0.staffRoster?.[locationId2]);
@@ -62235,7 +62502,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
62235
62502
  const industry = st.accounts?.[locationId2]?.industry;
62236
62503
  const questions = composeQuestionSet({ industry, agency: readOverlay() }).map((q3) => ({ key: q3.key, contactTag: q3.field.kind === "standard" ? q3.field.tag : void 0, label: q3.label, sectionLabel: q3.sectionLabel, required: q3.required, options: q3.options ?? null, placeholder: q3.placeholder ?? "", multi: q3.field.kind === "custom" && q3.field.dataType === "MULTIPLE_OPTIONS" }));
62237
62504
  const completed = st.intakeCompleted?.[locationId2];
62238
- const answers = completed ? completed.answers : fillBusinessType(prefillTeamAnswers(normalizedIntake(snap, st.intakeDrafts?.[locationId2]), st.staffRoster?.[locationId2], questions), industry, questions);
62505
+ const answers = completed ? completed.answers : fillBusinessType(teamAnswersFromRoster(normalizedIntake(snap, st.intakeDrafts?.[locationId2]), st.staffRoster?.[locationId2], questions), industry, questions);
62239
62506
  const latestState = readCockpitState();
62240
62507
  const bound = boundIntake(latestState, locationId2, snap);
62241
62508
  const submittedAnswers = bound.answers;
@@ -62841,7 +63108,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
62841
63108
  const current = readCockpitState();
62842
63109
  if (!current.buildApproved?.[rk]) throw new Error("Approve building (step 8) before confirming the build.");
62843
63110
  const preview = readBuildPreview(rk);
62844
- if (!preview || typeof fingerprint !== "string" || preview.fingerprint !== fingerprint) throw new Error("This list is out of date. Start the build again to see the current one.");
63111
+ if (!preview || !previewNamesLogins(preview) || typeof fingerprint !== "string" || preview.fingerprint !== fingerprint) throw new Error("This list is out of date. Start the build again to see the current one.");
62845
63112
  const changeKeys = new Set(preview.change.map((e) => e.key));
62846
63113
  const allowed = Array.isArray(allow) ? [...new Set(allow.filter((k) => typeof k === "string"))] : [];
62847
63114
  if (allowed.some((k) => !changeKeys.has(k))) throw new Error("Only items on the list can be changed.");
@@ -78723,7 +78990,8 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
78723
78990
  return jsonResponse({ ok: false, phase: "confirm", error: `Command OS could not read this account's custom values (${e instanceof Error ? e.message : String(e)}), so it cannot list what Build would change. Nothing was written. Run Build again.` });
78724
78991
  }
78725
78992
  const { changes: workflowChanges, states: workflowStates } = await previewWorkflowChanges(deps, result, ours, saved?.calendarPending);
78726
- const preview = composePreview({ locationId: activeLocation, recordKey, planHash: contentHash(typedPlan), items: result.items, ours, workflowChanges, workflowStates, board });
78993
+ const logins = (typedPlan.users ?? []).filter((u) => result.items.some((i) => i.ref === u.ref && i.type === "users" && i.status === "would_create")).map((u) => ({ name: [u.firstName, u.lastName === "-" ? "" : u.lastName].filter(Boolean).join(" "), email: u.email, role: u.role }));
78994
+ const preview = composePreview({ locationId: activeLocation, recordKey, planHash: contentHash(typedPlan), items: result.items, ours, workflowChanges, workflowStates, board, logins });
78727
78995
  try {
78728
78996
  saveBuildPreview(preview);
78729
78997
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.90.0",
3
+ "version": "3.90.1",
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",