@elitedcs/ghl-mcp 3.84.7 → 3.84.9

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/dist/index.js CHANGED
@@ -127,7 +127,7 @@ var init_account_read_retry = __esm({
127
127
  inBuildRequestScope = () => buildScope.getStore() !== void 0;
128
128
  inAccountReadAttempt = () => readScope.getStore() === true;
129
129
  withBuildRequestScope = (run, wait = sleep) => buildScope.run({ wait }, run);
130
- sleep = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms));
130
+ sleep = (ms) => new Promise((resolve13) => setTimeout(resolve13, ms));
131
131
  }
132
132
  });
133
133
 
@@ -446,13 +446,13 @@ function setProtectedAccount(locationId2, name, file2 = accountRulesPath()) {
446
446
  function isWriteMethod(method) {
447
447
  return WRITE_METHODS.has(method.toUpperCase());
448
448
  }
449
- function locationIdsInRequest(path41, opts = {}) {
449
+ function locationIdsInRequest(path42, opts = {}) {
450
450
  const found = /* @__PURE__ */ new Set();
451
451
  const add2 = (v) => {
452
452
  if (typeof v === "string" && v.trim()) found.add(v.trim());
453
453
  else if (Array.isArray(v)) v.forEach(add2);
454
454
  };
455
- const seg = /\/locations\/([^/?#]+)/i.exec(path41);
455
+ const seg = /\/locations\/([^/?#]+)/i.exec(path42);
456
456
  if (seg) add2(decodeURIComponent(seg[1]));
457
457
  for (const src of [opts.params, opts.body]) {
458
458
  if (!src || typeof src !== "object") continue;
@@ -479,7 +479,7 @@ function protectedRefusalFor(locationId2, rulesFile) {
479
479
  const name = rules.protected[id];
480
480
  return name ? protectedRefusalMessage(name, false) : null;
481
481
  }
482
- function assertWriteAllowed(method, path41, opts = {}) {
482
+ function assertWriteAllowed(method, path42, opts = {}) {
483
483
  if (!isWriteMethod(method)) return;
484
484
  let rules;
485
485
  try {
@@ -488,7 +488,7 @@ function assertWriteAllowed(method, path41, opts = {}) {
488
488
  throw new Error(UNREADABLE_RULES_MSG);
489
489
  }
490
490
  if (Object.keys(rules.protected).length === 0) return;
491
- for (const id of locationIdsInRequest(path41, opts)) {
491
+ for (const id of locationIdsInRequest(path42, opts)) {
492
492
  const name = rules.protected[id];
493
493
  if (name) throw new Error(protectedRefusalMessage(name, false));
494
494
  }
@@ -646,8 +646,8 @@ var init_ghl_client = __esm({
646
646
  Version: version || GHL_API_VERSION
647
647
  };
648
648
  }
649
- buildUrl(path41, params) {
650
- const url = new URL(path41, GHL_BASE_URL);
649
+ buildUrl(path42, params) {
650
+ const url = new URL(path42, GHL_BASE_URL);
651
651
  if (params) {
652
652
  for (const [key, value] of Object.entries(params)) {
653
653
  if (value !== void 0 && value !== null) {
@@ -657,19 +657,19 @@ var init_ghl_client = __esm({
657
657
  }
658
658
  return url.toString();
659
659
  }
660
- async request(method, path41, options = {}, attempt = 0) {
660
+ async request(method, path42, options = {}, attempt = 0) {
661
661
  this.signal?.throwIfAborted();
662
662
  if (inBuildRequestScope() && method === "GET" && !inAccountReadAttempt()) {
663
- return retryAccountRead(() => this.request(method, path41, options, attempt), `${method} ${path41}`);
663
+ return retryAccountRead(() => this.request(method, path42, options, attempt), `${method} ${path42}`);
664
664
  }
665
665
  if (attempt === 0) {
666
- assertWriteAllowed(method, path41, {
666
+ assertWriteAllowed(method, path42, {
667
667
  params: options.params,
668
668
  body: options.body,
669
669
  defaultLocationId: this.defaultLocationId
670
670
  });
671
671
  }
672
- const url = this.buildUrl(path41, options.params);
672
+ const url = this.buildUrl(path42, options.params);
673
673
  const headers = this.buildHeaders(options.version);
674
674
  const fetchOptions = {
675
675
  method,
@@ -690,14 +690,14 @@ var init_ghl_client = __esm({
690
690
  clearTimeout(timeout);
691
691
  this.signal?.throwIfAborted();
692
692
  if (error instanceof Error && error.name === "AbortError") {
693
- throw requestError(new Error(`Request timeout (30s): ${method} ${path41}`));
693
+ throw requestError(new Error(`Request timeout (30s): ${method} ${path42}`));
694
694
  }
695
695
  if (!inBuildRequestScope() && !options.noRetry && attempt < MAX_RETRIES) {
696
696
  const delay5 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
697
- process.stderr.write(`[ghl-mcp] Network error on ${method} ${path41}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
697
+ process.stderr.write(`[ghl-mcp] Network error on ${method} ${path42}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
698
698
  `);
699
699
  await new Promise((r) => setTimeout(r, delay5));
700
- return this.request(method, path41, options, attempt + 1);
700
+ return this.request(method, path42, options, attempt + 1);
701
701
  }
702
702
  throw requestError(error);
703
703
  } finally {
@@ -705,10 +705,10 @@ var init_ghl_client = __esm({
705
705
  }
706
706
  if (!inBuildRequestScope() && !options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
707
707
  const delay5 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
708
- process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path41}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
708
+ process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path42}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
709
709
  `);
710
710
  await new Promise((r) => setTimeout(r, delay5));
711
- return this.request(method, path41, options, attempt + 1);
711
+ return this.request(method, path42, options, attempt + 1);
712
712
  }
713
713
  if (!response.ok) {
714
714
  let errorBody = "";
@@ -717,7 +717,7 @@ var init_ghl_client = __esm({
717
717
  } catch {
718
718
  }
719
719
  throw requestError(new Error(
720
- `GHL API Error ${response.status} ${response.statusText}: ${method} ${path41}
720
+ `GHL API Error ${response.status} ${response.statusText}: ${method} ${path42}
721
721
  ${errorBody}`
722
722
  ));
723
723
  }
@@ -738,20 +738,20 @@ ${errorBody}`
738
738
  combined?.dispose();
739
739
  }
740
740
  }
741
- async get(path41, options) {
742
- return this.request("GET", path41, options);
741
+ async get(path42, options) {
742
+ return this.request("GET", path42, options);
743
743
  }
744
- async post(path41, options) {
745
- return this.request("POST", path41, options);
744
+ async post(path42, options) {
745
+ return this.request("POST", path42, options);
746
746
  }
747
- async put(path41, options) {
748
- return this.request("PUT", path41, options);
747
+ async put(path42, options) {
748
+ return this.request("PUT", path42, options);
749
749
  }
750
- async patch(path41, options) {
751
- return this.request("PATCH", path41, options);
750
+ async patch(path42, options) {
751
+ return this.request("PATCH", path42, options);
752
752
  }
753
- async delete(path41, options) {
754
- return this.request("DELETE", path41, options);
753
+ async delete(path42, options) {
754
+ return this.request("DELETE", path42, options);
755
755
  }
756
756
  /**
757
757
  * Helper: resolves locationId from args or falls back to default
@@ -5472,12 +5472,12 @@ function registerFormTools(server2, client) {
5472
5472
  startAt: import_zod16.z.string().optional().describe("Start date filter (ISO string)"),
5473
5473
  endAt: import_zod16.z.string().optional().describe("End date filter (ISO string)")
5474
5474
  },
5475
- async ({ locationId: locationId2, page, limit, formId, q: q2, startAt, endAt }) => {
5475
+ async ({ locationId: locationId2, page, limit, formId, q: q3, startAt, endAt }) => {
5476
5476
  const resolvedLocationId = client.resolveLocationId(locationId2);
5477
5477
  const params = { locationId: resolvedLocationId, limit: limit ?? 20 };
5478
5478
  if (page !== void 0) params.page = page;
5479
5479
  if (formId !== void 0) params.formId = formId;
5480
- if (q2 !== void 0) params.q = q2;
5480
+ if (q3 !== void 0) params.q = q3;
5481
5481
  if (startAt !== void 0) params.startAt = startAt;
5482
5482
  if (endAt !== void 0) params.endAt = endAt;
5483
5483
  return client.get("/forms/submissions", { params });
@@ -5526,13 +5526,13 @@ function registerSurveyTools(server2, client) {
5526
5526
  startAt: import_zod17.z.string().optional().describe("Start date filter (ISO string)"),
5527
5527
  endAt: import_zod17.z.string().optional().describe("End date filter (ISO string)")
5528
5528
  },
5529
- async ({ locationId: locationId2, page, limit, surveyId, q: q2, startAt, endAt }) => {
5529
+ async ({ locationId: locationId2, page, limit, surveyId, q: q3, startAt, endAt }) => {
5530
5530
  const resolvedLocationId = client.resolveLocationId(locationId2);
5531
5531
  const params = { locationId: resolvedLocationId };
5532
5532
  if (page !== void 0) params.page = page;
5533
5533
  if (limit !== void 0) params.limit = limit;
5534
5534
  if (surveyId !== void 0) params.surveyId = surveyId;
5535
- if (q2 !== void 0) params.q = q2;
5535
+ if (q3 !== void 0) params.q = q3;
5536
5536
  if (startAt !== void 0) params.startAt = startAt;
5537
5537
  if (endAt !== void 0) params.endAt = endAt;
5538
5538
  return client.get("/surveys/submissions", { params });
@@ -6192,12 +6192,12 @@ function buildCreateUserBody(args, opts = {}) {
6192
6192
  function generateTemporaryPassword() {
6193
6193
  const upper2 = "ABCDEFGHJKLMNPQRSTUVWXYZ";
6194
6194
  const lower = "abcdefghijkmnopqrstuvwxyz";
6195
- const digits2 = "23456789";
6195
+ const digits = "23456789";
6196
6196
  const symbols = "!@#$%&*?";
6197
- const all = upper2 + lower + digits2 + symbols;
6197
+ const all = upper2 + lower + digits + symbols;
6198
6198
  const bytes = (0, import_node_crypto.randomBytes)(24);
6199
6199
  const pick2 = (set, b) => set[b % set.length];
6200
- const chars = [pick2(upper2, bytes[0]), pick2(lower, bytes[1]), pick2(digits2, bytes[2]), pick2(symbols, bytes[3])];
6200
+ const chars = [pick2(upper2, bytes[0]), pick2(lower, bytes[1]), pick2(digits, bytes[2]), pick2(symbols, bytes[3])];
6201
6201
  for (let i = 4; i < 20; i++) chars.push(pick2(all, bytes[i]));
6202
6202
  for (let i = chars.length - 1; i > 0; i--) {
6203
6203
  const j = bytes[(i + 20) % bytes.length] % (i + 1);
@@ -7105,11 +7105,11 @@ function registerEmailTools(server2, client) {
7105
7105
  function registerEmailBuilderInternalTools(server2, builderClient, publicClient) {
7106
7106
  if (!builderClient) return;
7107
7107
  const client = builderClient;
7108
- async function builderRequest2(method, path41, body) {
7109
- const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path41}`, body ? { body } : {});
7108
+ async function builderRequest2(method, path42, body) {
7109
+ const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path42}`, body ? { body } : {});
7110
7110
  if (!response.ok) {
7111
7111
  const text3 = await response.text();
7112
- throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path41}
7112
+ throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path42}
7113
7113
  ${text3}`);
7114
7114
  }
7115
7115
  const text2 = await response.text();
@@ -8330,18 +8330,18 @@ async function readAgent(client, agentId) {
8330
8330
  return a;
8331
8331
  }
8332
8332
  async function updateAgentFields(client, agentId, change) {
8333
- const clean = {};
8334
- for (const [k, val] of Object.entries(change)) if (val !== void 0) clean[k] = val;
8335
- if (Object.keys(clean).length === 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
8333
+ const clean2 = {};
8334
+ for (const [k, val] of Object.entries(change)) if (val !== void 0) clean2[k] = val;
8335
+ if (Object.keys(clean2).length === 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
8336
8336
  const before = await readAgent(client, agentId);
8337
- const body = { ...promptTrio(before), ...clean };
8337
+ const body = { ...promptTrio(before), ...clean2 };
8338
8338
  const merged = promptTrio(body);
8339
8339
  if (!merged.personality || !merged.goal || !merged.instructions) {
8340
8340
  throw new Error(`Agent ${agentId} read back without its personality/goal/instructions, and GoHighLevel refuses an update that does not carry all three. Pass all three explicitly (get_conversation_agent shows what is stored).`);
8341
8341
  }
8342
8342
  await client.put(`/conversation-ai/agents/${agentId}`, { version: AI_API_VERSION, body });
8343
8343
  const after = await readAgent(client, agentId);
8344
- return { before, changed: Object.keys(clean), after };
8344
+ return { before, changed: Object.keys(clean2), after };
8345
8345
  }
8346
8346
  function registerConversationAiTools(server2, client) {
8347
8347
  const v = { version: AI_API_VERSION };
@@ -8978,7 +8978,7 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
8978
8978
  const utc = (s) => /^\d{4}-\d{2}-\d{2}T[\d:.]+$/.test(s) ? `${s}Z` : s;
8979
8979
  const when = (m) => Date.parse(utc(String(m.dateAdded ?? m.createdAt ?? m.dateUpdated ?? ""))) || 0;
8980
8980
  const start = Date.parse(run.startedAt) || 0;
8981
- const stop = run.endedAt ? Date.parse(run.endedAt) || Infinity : Infinity;
8981
+ const stop2 = run.endedAt ? Date.parse(run.endedAt) || Infinity : Infinity;
8982
8982
  const SKEW_MS = 15e3;
8983
8983
  let beforeStart = 0;
8984
8984
  let afterEnd = 0;
@@ -8993,7 +8993,7 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
8993
8993
  beforeStart++;
8994
8994
  return false;
8995
8995
  }
8996
- if (t > stop + SKEW_MS) {
8996
+ if (t > stop2 + SKEW_MS) {
8997
8997
  afterEnd++;
8998
8998
  return false;
8999
8999
  }
@@ -10919,11 +10919,11 @@ async function readPageFull(client, pageId, fetcher = fetch) {
10919
10919
  const headers = await client.buildHeaders();
10920
10920
  headers.Origin = "https://app.gohighlevel.com";
10921
10921
  headers.Referer = "https://app.gohighlevel.com/";
10922
- const path41 = `/page/${pageId}?locationId=${client.locationId}`;
10923
- const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path41}`, { method: "GET", headers });
10922
+ const path42 = `/page/${pageId}?locationId=${client.locationId}`;
10923
+ const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path42}`, { method: "GET", headers });
10924
10924
  if (!response.ok) {
10925
10925
  const text3 = await response.text();
10926
- throw new Error(`Funnel API Error ${response.status}: GET ${path41}
10926
+ throw new Error(`Funnel API Error ${response.status}: GET ${path42}
10927
10927
  ${text3}`);
10928
10928
  }
10929
10929
  const text2 = await response.text();
@@ -10982,20 +10982,20 @@ function registerFunnelBuilderTools(server2, builderClient) {
10982
10982
  return;
10983
10983
  }
10984
10984
  const client = builderClient;
10985
- async function internalGet(path41) {
10986
- return client.request("GET", path41);
10985
+ async function internalGet(path42) {
10986
+ return client.request("GET", path42);
10987
10987
  }
10988
- async function internalPost(path41, body) {
10989
- return client.request("POST", path41, body);
10988
+ async function internalPost(path42, body) {
10989
+ return client.request("POST", path42, body);
10990
10990
  }
10991
- async function internalPut(path41, body) {
10992
- return client.request("PUT", path41, body);
10991
+ async function internalPut(path42, body) {
10992
+ return client.request("PUT", path42, body);
10993
10993
  }
10994
- async function internalDelete(path41) {
10995
- return client.request("DELETE", path41);
10994
+ async function internalDelete(path42) {
10995
+ return client.request("DELETE", path42);
10996
10996
  }
10997
- async function funnelRequest(method, path41, body) {
10998
- const url = `https://backend.leadconnectorhq.com/funnels${path41}`;
10997
+ async function funnelRequest(method, path42, body) {
10998
+ const url = `https://backend.leadconnectorhq.com/funnels${path42}`;
10999
10999
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
11000
11000
  const response = await builderRequest(client, method, url, {
11001
11001
  ...sendsBody ? { body } : {},
@@ -11008,7 +11008,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
11008
11008
  });
11009
11009
  if (!response.ok) {
11010
11010
  const text3 = await response.text();
11011
- throw new Error(`Funnel API Error ${response.status}: ${method} ${path41}
11011
+ throw new Error(`Funnel API Error ${response.status}: ${method} ${path42}
11012
11012
  ${text3}`);
11013
11013
  }
11014
11014
  const text2 = await response.text();
@@ -11599,30 +11599,30 @@ function requireCompany(api) {
11599
11599
  }
11600
11600
  return company;
11601
11601
  }
11602
- async function call(api, path41, init) {
11602
+ async function call(api, path42, init) {
11603
11603
  const headers = { ...await api.headers(), Origin: "https://app.gohighlevel.com", Referer: "https://app.gohighlevel.com/" };
11604
- if (MUTATING_PATHS.some((mutating) => path41.startsWith(mutating))) {
11605
- assertWriteAllowed("POST", SVC + path41, { defaultLocationId: api.locationId() });
11604
+ if (MUTATING_PATHS.some((mutating) => path42.startsWith(mutating))) {
11605
+ assertWriteAllowed("POST", SVC + path42, { defaultLocationId: api.locationId() });
11606
11606
  }
11607
11607
  const doFetch = api.fetch ?? fetch;
11608
- const response = await doFetch(SVC + path41, { ...init, headers });
11608
+ const response = await doFetch(SVC + path42, { ...init, headers });
11609
11609
  const text2 = await response.text();
11610
11610
  if (!response.ok) {
11611
11611
  const hint = response.status === 401 && /not authorized to access this company/i.test(text2) ? " \u2014 the Firebase session is authenticated as a DIFFERENT company than the one that owns this sub-account. Run switch_location for this sub-account and try again." : response.status === 401 ? " \u2014 the template library needs the workflow-builder Firebase session, not the API key alone. Run enable_workflow_builder for this sub-account." : "";
11612
- throw new Error(`GoHighLevel template library ${response.status} on ${path41}${hint}
11612
+ throw new Error(`GoHighLevel template library ${response.status} on ${path42}${hint}
11613
11613
  ${text2.slice(0, 300)}`);
11614
11614
  }
11615
11615
  return text2 ? JSON.parse(text2) : {};
11616
11616
  }
11617
- async function searchTemplates(api, q2 = {}) {
11618
- if (q2.categories?.length) {
11619
- const wanted = new Set(q2.categories.map((name) => name.trim().toLowerCase()));
11620
- const limit = Math.min(Math.max(q2.limit ?? 24, 1), 200);
11621
- const offset = Math.max(q2.offset ?? 0, 0);
11617
+ async function searchTemplates(api, q3 = {}) {
11618
+ if (q3.categories?.length) {
11619
+ const wanted = new Set(q3.categories.map((name) => name.trim().toLowerCase()));
11620
+ const limit = Math.min(Math.max(q3.limit ?? 24, 1), 200);
11621
+ const offset = Math.max(q3.offset ?? 0, 0);
11622
11622
  const matches = [];
11623
11623
  const seen = /* @__PURE__ */ new Set();
11624
11624
  for (let upstreamOffset = 0; ; upstreamOffset += 200) {
11625
- const page = await searchTemplates(api, { ...q2, categories: [], limit: 200, offset: upstreamOffset });
11625
+ const page = await searchTemplates(api, { ...q3, categories: [], limit: 200, offset: upstreamOffset });
11626
11626
  const fresh = page.filter((row) => !seen.has(row._id));
11627
11627
  if (page.length && !fresh.length) throw new Error("GoHighLevel repeated a template page. The category search could not finish.");
11628
11628
  for (const row of fresh) {
@@ -11637,19 +11637,19 @@ async function searchTemplates(api, q2 = {}) {
11637
11637
  const body = {
11638
11638
  companyId: requireCompany(api),
11639
11639
  locationId: api.locationId(),
11640
- product: q2.product ?? "funnels",
11641
- keyword: q2.keyword ?? "",
11640
+ product: q3.product ?? "funnels",
11641
+ keyword: q3.keyword ?? "",
11642
11642
  type: "all",
11643
- categories: q2.categories ?? [],
11644
- features: q2.features ?? [],
11645
- tags: q2.tags ?? [],
11646
- colors: q2.colors ?? [],
11643
+ categories: q3.categories ?? [],
11644
+ features: q3.features ?? [],
11645
+ tags: q3.tags ?? [],
11646
+ colors: q3.colors ?? [],
11647
11647
  status: [],
11648
11648
  createdBy: [],
11649
11649
  isAdminView: false,
11650
11650
  system: false,
11651
- limit: Math.min(Math.max(q2.limit ?? 24, 1), 200),
11652
- offset: q2.offset ?? 0
11651
+ limit: Math.min(Math.max(q3.limit ?? 24, 1), 200),
11652
+ offset: q3.offset ?? 0
11653
11653
  };
11654
11654
  const out = await call(api, "/templates/list", { method: "POST", body: JSON.stringify(body) });
11655
11655
  return Array.isArray(out) ? out : out.templates ?? [];
@@ -11966,12 +11966,12 @@ function normaliseColour(value) {
11966
11966
  function renderedPalette(envelope) {
11967
11967
  const defs = pageVariables(envelope);
11968
11968
  const paints = [];
11969
- const walk = (node, path41, prop) => {
11970
- if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${path41}[${i}]`, prop));
11969
+ const walk = (node, path42, prop) => {
11970
+ if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${path42}[${i}]`, prop));
11971
11971
  if (isRecord8(node)) {
11972
11972
  for (const [k, x] of Object.entries(node)) {
11973
11973
  if (DEFINITION_KEYS.has(k) || k.startsWith("--")) continue;
11974
- walk(x, `${path41}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
11974
+ walk(x, `${path42}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
11975
11975
  }
11976
11976
  return;
11977
11977
  }
@@ -11981,7 +11981,7 @@ function renderedPalette(envelope) {
11981
11981
  const resolved2 = resolveValue(raw, defs);
11982
11982
  const colour = normaliseColour(resolved2);
11983
11983
  if (colour === null && resolved2 !== null) return;
11984
- paints.push({ path: path41, prop, kind: kindOf(prop), colour });
11984
+ paints.push({ path: path42, prop, kind: kindOf(prop), colour });
11985
11985
  };
11986
11986
  walk(envelope, "", "");
11987
11987
  const face = (name, settingsKey) => {
@@ -12020,8 +12020,8 @@ function repaintReport(before, after) {
12020
12020
  const coloured = (c) => c.startsWith("#") && hexToOklch(c).c >= CARRIES_COLOUR;
12021
12021
  for (const p of a.paints) {
12022
12022
  if (!p.colour || p.colour === "transparent") continue;
12023
- const q2 = afterByPath.get(p.path);
12024
- const to = q2?.colour ?? null;
12023
+ const q3 = afterByPath.get(p.path);
12024
+ const to = q3?.colour ?? null;
12025
12025
  const differs = to !== null && looksDifferent(p.colour, to);
12026
12026
  if (p.kind === "background" || p.kind === "text") {
12027
12027
  const t = p.kind === "text" ? tally.text : coloured(p.colour) ? tally.background : tally.neutral;
@@ -12076,7 +12076,7 @@ var init_rendered_palette = __esm({
12076
12076
  VISIBLE_CHANGE = 0.05;
12077
12077
  BARELY_SHARE = 0.5;
12078
12078
  CARRIES_COLOUR = 0.02;
12079
- paintGroup = (path41) => /^\.(sections|popups)\[\d+\]/.exec(path41)?.[0]?.slice(1) ?? "page";
12079
+ paintGroup = (path42) => /^\.(sections|popups)\[\d+\]/.exec(path42)?.[0]?.slice(1) ?? "page";
12080
12080
  looksDifferent = (a, b) => {
12081
12081
  if (a === b) return false;
12082
12082
  if (a === "transparent" || b === "transparent" || a.startsWith("rgba") || b.startsWith("rgba")) return a !== b;
@@ -15700,12 +15700,12 @@ function deriveBrandDna(raw, opts = {}) {
15700
15700
  fixed = true;
15701
15701
  }
15702
15702
  const vb = svg.match(/viewBox=["']\s*[\d.-]+\s+[\d.-]+\s+([\d.]+)\s+([\d.]+)/i);
15703
- const clean = sanitizeSvgOrDrop(svg);
15704
- if (clean) {
15705
- logoSvg = clean;
15706
- logo = { inlineSvg: clean, whiteFillFixed: fixed, source: bl.where, rights: opts.rights ? "declared" : "inherits", ...vb && { width: parseFloat(vb[1]), height: parseFloat(vb[2]) } };
15703
+ const clean2 = sanitizeSvgOrDrop(svg);
15704
+ if (clean2) {
15705
+ logoSvg = clean2;
15706
+ logo = { inlineSvg: clean2, whiteFillFixed: fixed, source: bl.where, rights: opts.rights ? "declared" : "inherits", ...vb && { width: parseFloat(vb[1]), height: parseFloat(vb[2]) } };
15707
15707
  declared++;
15708
- evidence["logo"] = `${bl.where}${fixed ? "; white fills replaced with currentColor" : ""}${clean !== svg ? "; scripts, handlers and outside references stripped" : ""}`;
15708
+ evidence["logo"] = `${bl.where}${fixed ? "; white fills replaced with currentColor" : ""}${clean2 !== svg ? "; scripts, handlers and outside references stripped" : ""}`;
15709
15709
  } else {
15710
15710
  evidence["logo"] = `${bl.where}: not a plain drawing (or over 60 KB), so it was not kept`;
15711
15711
  }
@@ -16651,10 +16651,10 @@ function removalHoldBack(opts) {
16651
16651
  if (opts.multiPageStepNames.length > 0) return `${opts.multiPageStepNames.length} step(s) carry more than one page (${opts.multiPageStepNames.join(", ")}) and only the first of each was rebuilt`;
16652
16652
  return null;
16653
16653
  }
16654
- function brandedPath(path41, name, taken) {
16655
- const normalize = (s) => s.replace(/^\/+|\/+$/g, "").toLowerCase();
16656
- const base = normalize(path41 ?? "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
16657
- const used = new Set([...taken].map(normalize));
16654
+ function brandedPath(path42, name, taken) {
16655
+ const normalize2 = (s) => s.replace(/^\/+|\/+$/g, "").toLowerCase();
16656
+ const base = normalize2(path42 ?? "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
16657
+ const used = new Set([...taken].map(normalize2));
16658
16658
  let candidate = `${base}-branded`;
16659
16659
  for (let n = 2; used.has(candidate); n++) candidate = `${base}-branded-${n}`;
16660
16660
  taken.add(`/${candidate}`);
@@ -16663,7 +16663,7 @@ function brandedPath(path41, name, taken) {
16663
16663
  function rewriteTemplateLinks(envelope, destinations) {
16664
16664
  const links = [];
16665
16665
  const unmapped = [];
16666
- const normalize = (s) => s.replace(/^\/+|\/+$/g, "");
16666
+ const normalize2 = (s) => s.replace(/^\/+|\/+$/g, "");
16667
16667
  function rewrite(from, kind) {
16668
16668
  if (!from) return from;
16669
16669
  let matches = [];
@@ -16681,9 +16681,9 @@ function rewriteTemplateLinks(envelope, destinations) {
16681
16681
  representation = "page";
16682
16682
  matches = destinations.filter((d2) => d2.pageId === preview[2]);
16683
16683
  } else if (!/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(from) && !from.startsWith("#")) {
16684
- const [path41] = from.split(/[?#]/);
16685
- suffix = from.slice(path41.length);
16686
- matches = destinations.filter((d2) => d2.path && normalize(d2.path) === normalize(path41));
16684
+ const [path42] = from.split(/[?#]/);
16685
+ suffix = from.slice(path42.length);
16686
+ matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(path42));
16687
16687
  }
16688
16688
  }
16689
16689
  if (matches.length !== 1) {
@@ -16695,7 +16695,7 @@ function rewriteTemplateLinks(envelope, destinations) {
16695
16695
  unmapped.push({ from, reason: "the target step was not rebuilt" });
16696
16696
  return from;
16697
16697
  }
16698
- const to = representation === "step" ? d.newStepId : representation === "page" ? `${previewPrefix}${d.newPageId}${suffix}` : `${from.startsWith("/") ? d.newPath : normalize(d.newPath)}${suffix}`;
16698
+ const to = representation === "step" ? d.newStepId : representation === "page" ? `${previewPrefix}${d.newPageId}${suffix}` : `${from.startsWith("/") ? d.newPath : normalize2(d.newPath)}${suffix}`;
16699
16699
  links.push({ from, to });
16700
16700
  return to;
16701
16701
  }
@@ -16766,8 +16766,8 @@ function registerPageStudioTools(server2, builderClient) {
16766
16766
  }
16767
16767
  return { map: out, truncated };
16768
16768
  }
16769
- async function funnelRequest(method, path41, body) {
16770
- const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path41}`, {
16769
+ async function funnelRequest(method, path42, body) {
16770
+ const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path42}`, {
16771
16771
  ...body ? { body } : {},
16772
16772
  headers: {
16773
16773
  // Internal-API calls require the app origin (autosave needs the
@@ -16776,7 +16776,7 @@ function registerPageStudioTools(server2, builderClient) {
16776
16776
  Referer: "https://app.gohighlevel.com/"
16777
16777
  }
16778
16778
  });
16779
- if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path41}
16779
+ if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path42}
16780
16780
  ${await response.text()}`);
16781
16781
  const text2 = await response.text();
16782
16782
  return text2 ? JSON.parse(text2) : {};
@@ -17283,7 +17283,7 @@ ${await response.text()}`);
17283
17283
  }
17284
17284
  const confirmed = /* @__PURE__ */ new Set();
17285
17285
  for (let attempt = 0; accepted.length && attempt < 5; attempt++) {
17286
- if (attempt) await new Promise((resolve12) => setTimeout(resolve12, attempt * 1e3));
17286
+ if (attempt) await new Promise((resolve13) => setTimeout(resolve13, attempt * 1e3));
17287
17287
  try {
17288
17288
  const listed = await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=${FUNNEL_PAGE}`);
17289
17289
  const current = listed.funnels?.find((item) => item._id === funnel._id);
@@ -17645,13 +17645,13 @@ function cleanFormDataForSave(formData) {
17645
17645
  const cleaned = dropRedundantCaseCollidingKeys(formData);
17646
17646
  return isJsonObject(cleaned) ? cleaned : formData;
17647
17647
  }
17648
- async function formApiRequest(client, method, path41, body) {
17649
- const url = `https://backend.leadconnectorhq.com/forms${path41}`;
17648
+ async function formApiRequest(client, method, path42, body) {
17649
+ const url = `https://backend.leadconnectorhq.com/forms${path42}`;
17650
17650
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
17651
17651
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
17652
17652
  if (!response.ok) {
17653
17653
  const text3 = await response.text();
17654
- throw new Error(`Form API Error ${response.status}: ${method} ${path41}
17654
+ throw new Error(`Form API Error ${response.status}: ${method} ${path42}
17655
17655
  ${text3}`);
17656
17656
  }
17657
17657
  const text2 = await response.text();
@@ -17693,7 +17693,7 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
17693
17693
  );
17694
17694
  return;
17695
17695
  }
17696
- const formRequest = (method, path41, body) => formApiRequest(client, method, path41, body);
17696
+ const formRequest = (method, path42, body) => formApiRequest(client, method, path42, body);
17697
17697
  server2.tool(
17698
17698
  "get_form_full",
17699
17699
  "Get a form with full builder data: all fields (labels, types, IDs, validation), conditional logic, auto-responder config, email notification settings, styling, and version history. This is the internal API \u2014 it returns what the form builder UI shows, with one exception: where GoHighLevel stores the same value under two spellings of one key that differ only in case (it writes custom fields with both `Id` and `id`), only the lowercase one is returned. No value is lost, and the result can be edited and passed straight back to update_form.",
@@ -17827,10 +17827,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
17827
17827
  },
17828
17828
  async ({ formId, limit, skip }) => {
17829
17829
  try {
17830
- let path41 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
17831
- if (formId) path41 += `&formId=${formId}`;
17832
- if (skip) path41 += `&skip=${skip}`;
17833
- const result = await formRequest("GET", path41);
17830
+ let path42 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
17831
+ if (formId) path42 += `&formId=${formId}`;
17832
+ if (skip) path42 += `&skip=${skip}`;
17833
+ const result = await formRequest("GET", path42);
17834
17834
  return {
17835
17835
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
17836
17836
  };
@@ -18074,7 +18074,7 @@ function registerFunnelQaTools(server2, client, builderClient) {
18074
18074
  last_name: contact.lastName,
18075
18075
  name: contact.name
18076
18076
  };
18077
- const same = (key, a, b) => key === "phone" ? (a ?? "").replace(/\D/g, "") === (b ?? "").replace(/\D/g, "") : (a ?? "").trim().toLowerCase() === (b ?? "").trim().toLowerCase();
18077
+ const same2 = (key, a, b) => key === "phone" ? (a ?? "").replace(/\D/g, "") === (b ?? "").replace(/\D/g, "") : (a ?? "").trim().toLowerCase() === (b ?? "").trim().toLowerCase();
18078
18078
  for (const k of args.expectStandard ?? []) {
18079
18079
  const key = k.toLowerCase();
18080
18080
  if (!(key in got)) {
@@ -18087,7 +18087,7 @@ function registerFunnelQaTools(server2, client, builderClient) {
18087
18087
  continue;
18088
18088
  }
18089
18089
  if (!got[key]) probs.push(`standard ${k}: not saved`);
18090
- else if (!same(key, got[key], exp)) probs.push(`standard ${k}: saved "${got[key]}" \u2260 sent "${exp}"`);
18090
+ else if (!same2(key, got[key], exp)) probs.push(`standard ${k}: saved "${got[key]}" \u2260 sent "${exp}"`);
18091
18091
  }
18092
18092
  const okCount = (args.expectCustom?.length ?? 0) + (args.expectStandard?.length ?? 0) - probs.length - notes.length;
18093
18093
  checks.push(probs.length ? { id: "field_fidelity", label: "Field-value persistence", status: "fail", detail: [probs.join("; "), notes.join("; ")].filter(Boolean).join(" | ") } : { id: "field_fidelity", label: "Field-value persistence", status: "pass", detail: `${okCount} asserted field value(s) persisted with the exact submitted value.${notes.length ? " " + notes.join("; ") : ""}` });
@@ -18238,13 +18238,13 @@ var init_funnel_qa = __esm({
18238
18238
  function registerPipelineBuilderTools(server2, builderClient) {
18239
18239
  if (!builderClient) return;
18240
18240
  const client = builderClient;
18241
- async function pipelineRequest(method, path41, body) {
18242
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path41}`;
18241
+ async function pipelineRequest(method, path42, body) {
18242
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path42}`;
18243
18243
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT" || method === "PATCH");
18244
18244
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
18245
18245
  if (!response.ok) {
18246
18246
  const text3 = await response.text();
18247
- throw new Error(`Pipeline API Error ${response.status}: ${method} ${path41}
18247
+ throw new Error(`Pipeline API Error ${response.status}: ${method} ${path42}
18248
18248
  ${text3}`);
18249
18249
  }
18250
18250
  const text2 = await response.text();
@@ -18548,6 +18548,22 @@ var init_features = __esm({
18548
18548
  });
18549
18549
 
18550
18550
  // src/agency-profile.ts
18551
+ var agency_profile_exports = {};
18552
+ __export(agency_profile_exports, {
18553
+ AGENCY_PROFILE_ENV: () => AGENCY_PROFILE_ENV,
18554
+ agencyProfilePath: () => agencyProfilePath,
18555
+ demoRailProfile: () => demoRailProfile,
18556
+ mergeAgencyProfile: () => mergeAgencyProfile,
18557
+ normalizeAccent: () => normalizeAccent,
18558
+ profileIsUsable: () => profileIsUsable,
18559
+ readAgencyProfile: () => readAgencyProfile,
18560
+ safeDir: () => safeDir,
18561
+ safeUrl: () => safeUrl2,
18562
+ sanitizeAgencyOs: () => sanitizeAgencyOs,
18563
+ sanitizeProfile: () => sanitizeProfile,
18564
+ saveAgencyProfileAnswer: () => saveAgencyProfileAnswer,
18565
+ writeAgencyProfile: () => writeAgencyProfile
18566
+ });
18551
18567
  function agencyProfilePath() {
18552
18568
  const override = process.env[AGENCY_PROFILE_ENV];
18553
18569
  return override && override.trim() ? override : path8.join(appDataDir(), "agency-profile.json");
@@ -18572,6 +18588,11 @@ function safeUrl2(x, max = MAX_URL) {
18572
18588
  if (u.protocol === "http:" && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(u.hostname)) return raw;
18573
18589
  return void 0;
18574
18590
  }
18591
+ function safeDir(x) {
18592
+ const raw = typeof x === "string" ? x.trim() : "";
18593
+ if (!raw || raw.length > 1024 || raw.includes("\0") || !path8.isAbsolute(raw)) return void 0;
18594
+ return path8.normalize(raw);
18595
+ }
18575
18596
  function normalizeAccent(x) {
18576
18597
  const s = typeof x === "string" ? x.trim() : "";
18577
18598
  return /^#[0-9a-fA-F]{6}$/.test(s) ? s.toLowerCase() : void 0;
@@ -18646,6 +18667,7 @@ function sanitizeProfile(raw) {
18646
18667
  industry: str2(defaults?.industry, 64)
18647
18668
  }),
18648
18669
  agencyOs: sanitizeAgencyOs(r.agencyOs),
18670
+ clientFilesDir: safeDir(r.clientFilesDir),
18649
18671
  updated_at: str2(r.updated_at, 40)
18650
18672
  };
18651
18673
  }
@@ -18790,6 +18812,17 @@ function writeAgencyProfile(patch, file2 = agencyProfilePath()) {
18790
18812
  function profileIsUsable(p) {
18791
18813
  return Boolean(p.agencyName);
18792
18814
  }
18815
+ function demoRailProfile(p = readAgencyProfile(), ghl, demoDays = 7) {
18816
+ if (!profileIsUsable(p)) return void 0;
18817
+ const rec = { agencyName: p.agencyName };
18818
+ if (p.accent) rec.accent = p.accent;
18819
+ if (p.logoUrl) rec.logoUrl = p.logoUrl;
18820
+ if (p.sender) rec.senderIdentity = p.sender;
18821
+ const g = drop({ locationId: str2(ghl?.locationId), triggerTag: str2(ghl?.triggerTag, 60) });
18822
+ if (g) rec.ghl = g;
18823
+ rec.demoDays = num(demoDays) ?? 7;
18824
+ return rec;
18825
+ }
18793
18826
  function sanitizePlanEvidence(raw) {
18794
18827
  if (!raw || typeof raw !== "object") return void 0;
18795
18828
  const r = raw;
@@ -18839,7 +18872,8 @@ var init_agency_profile = __esm({
18839
18872
  "defaults",
18840
18873
  "agencyOs",
18841
18874
  "canCreateSubAccounts",
18842
- "ghlPlanEvidence"
18875
+ "ghlPlanEvidence",
18876
+ "clientFilesDir"
18843
18877
  ];
18844
18878
  NESTED = /* @__PURE__ */ new Set(["sender", "pricing", "delivery", "defaults", "agencyOs"]);
18845
18879
  isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -20487,8 +20521,8 @@ function summarisePrice(sub) {
20487
20521
  unpriced = true;
20488
20522
  continue;
20489
20523
  }
20490
- const q2 = i.quantity;
20491
- const qty = typeof q2 === "number" && isFinite(q2) && q2 > 0 ? Math.round(q2) : 1;
20524
+ const q3 = i.quantity;
20525
+ const qty = typeof q3 === "number" && isFinite(q3) && q3 > 0 ? Math.round(q3) : 1;
20492
20526
  total += unit * qty;
20493
20527
  }
20494
20528
  if (unpriced) {
@@ -20816,7 +20850,7 @@ function calendarSetupDetail() {
20816
20850
  }
20817
20851
  function openBrowser(url) {
20818
20852
  const [file2, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
20819
- return new Promise((resolve12) => (0, import_node_child_process.execFile)(file2, args, { timeout: 5e3 }, (error) => resolve12(!error)));
20853
+ return new Promise((resolve13) => (0, import_node_child_process.execFile)(file2, args, { timeout: 5e3 }, (error) => resolve13(!error)));
20820
20854
  }
20821
20855
  function parsed(schema, value) {
20822
20856
  const result = schema.safeParse(value);
@@ -20913,16 +20947,16 @@ function createGoogleCalendar(deps = {}) {
20913
20947
  requireOnlyCalendar(result.scope);
20914
20948
  return result.access_token;
20915
20949
  }
20916
- async function pages(path41, access, item, query = {}) {
20950
+ async function pages(path42, access, item, query = {}) {
20917
20951
  const result = [];
20918
20952
  const seen = /* @__PURE__ */ new Set();
20919
20953
  let pageToken;
20920
20954
  for (let page = 0; page < 100; page++) {
20921
- const url = new URL(`${API}${path41}`);
20955
+ const url = new URL(`${API}${path42}`);
20922
20956
  for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
20923
20957
  if (pageToken) url.searchParams.set("pageToken", pageToken);
20924
20958
  const data = parsed(
20925
- import_zod52.z.object({ items: import_zod52.z.array(item).optional(), nextPageToken: import_zod52.z.string().optional(), kind: import_zod52.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path41.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
20959
+ import_zod52.z.object({ items: import_zod52.z.array(item).optional(), nextPageToken: import_zod52.z.string().optional(), kind: import_zod52.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path42.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
20926
20960
  await request(url.href, { headers: { Authorization: `Bearer ${access}` } })
20927
20961
  );
20928
20962
  result.push(...data.items ?? []);
@@ -20974,9 +21008,9 @@ function createGoogleCalendar(deps = {}) {
20974
21008
  server2.requestTimeout = 5e3;
20975
21009
  server2.headersTimeout = 5e3;
20976
21010
  try {
20977
- await new Promise((resolve12, reject) => {
21011
+ await new Promise((resolve13, reject) => {
20978
21012
  server2.once("error", reject);
20979
- server2.listen(0, "127.0.0.1", resolve12);
21013
+ server2.listen(0, "127.0.0.1", resolve13);
20980
21014
  });
20981
21015
  } catch {
20982
21016
  server2.close();
@@ -21173,8 +21207,8 @@ function redactConnectionKey(value, key) {
21173
21207
  return value;
21174
21208
  }
21175
21209
  function stripeFetcher(key, signal) {
21176
- return async (path41, query) => {
21177
- const url = new URL(`https://api.stripe.com${path41}`);
21210
+ return async (path42, query) => {
21211
+ const url = new URL(`https://api.stripe.com${path42}`);
21178
21212
  for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
21179
21213
  const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, signal });
21180
21214
  if (!res.ok) {
@@ -21263,11 +21297,11 @@ var init_handlers = __esm({
21263
21297
  });
21264
21298
 
21265
21299
  // src/command-os/connectors/stripe-read.ts
21266
- async function listAll(f, path41, schema, extra = {}) {
21300
+ async function listAll(f, path42, schema, extra = {}) {
21267
21301
  const out = [];
21268
21302
  let after;
21269
21303
  for (let page = 0; page < 20; page++) {
21270
- const result = import_zod53.z.object({ data: import_zod53.z.array(schema), has_more: import_zod53.z.boolean().optional() }).parse(await f(path41, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
21304
+ const result = import_zod53.z.object({ data: import_zod53.z.array(schema), has_more: import_zod53.z.boolean().optional() }).parse(await f(path42, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
21271
21305
  out.push(...result.data);
21272
21306
  if (!result.has_more) return out;
21273
21307
  const last = import_zod53.z.object({ id: import_zod53.z.string().min(1) }).parse(result.data.at(-1));
@@ -21397,8 +21431,8 @@ function sanitizeEngagements(raw) {
21397
21431
  for (const [id, rec] of Object.entries(src).slice(0, MAX_CLIENTS)) {
21398
21432
  const key = str2(id, 64);
21399
21433
  if (!key || FORBIDDEN_KEY.has(key)) continue;
21400
- const clean = sanitizeEngagement(rec);
21401
- if (clean) clients[key] = clean;
21434
+ const clean2 = sanitizeEngagement(rec);
21435
+ if (clean2) clients[key] = clean2;
21402
21436
  }
21403
21437
  }
21404
21438
  return { v: 1, clients: { ...clients }, updated_at: str2(r.updated_at, 40) };
@@ -21879,16 +21913,16 @@ function deriveFieldKey(fieldName) {
21879
21913
  function intakeFieldName(label) {
21880
21914
  return `${INTAKE_FIELD_NAME_PREFIX}${label}`;
21881
21915
  }
21882
- function expectedFieldKey(q2) {
21883
- return deriveFieldKey(intakeFieldName(q2.label));
21916
+ function expectedFieldKey(q3) {
21917
+ return deriveFieldKey(intakeFieldName(q3.label));
21884
21918
  }
21885
21919
  function intakeSectionCount(questions = INTAKE_QUESTIONS) {
21886
21920
  let count2 = 0;
21887
21921
  let last;
21888
- for (const q2 of questions) {
21889
- if (q2.section !== last) {
21922
+ for (const q3 of questions) {
21923
+ if (q3.section !== last) {
21890
21924
  count2++;
21891
- last = q2.section;
21925
+ last = q3.section;
21892
21926
  }
21893
21927
  }
21894
21928
  return count2;
@@ -24902,15 +24936,15 @@ function shimTarget(shim) {
24902
24936
  return null;
24903
24937
  }
24904
24938
  const refs = [...text2.matchAll(/"%~?dp0%?\\([^"\r\n]+)"/g)].map((m) => m[1]);
24905
- const resolve12 = (rel) => path17.join(path17.dirname(shim), ...rel.split(/[\\/]/));
24939
+ const resolve13 = (rel) => path17.join(path17.dirname(shim), ...rel.split(/[\\/]/));
24906
24940
  const script = refs.find((r) => /\.(?:js|cjs|mjs)$/i.test(r));
24907
24941
  if (script) {
24908
- const p = resolve12(script);
24942
+ const p = resolve13(script);
24909
24943
  return fs18.existsSync(p) ? { kind: "script", path: p } : null;
24910
24944
  }
24911
24945
  const program = refs.find((r) => /\.exe$/i.test(r) && !/(^|[\\/])node\.exe$/i.test(r));
24912
24946
  if (program) {
24913
- const p = resolve12(program);
24947
+ const p = resolve13(program);
24914
24948
  return fs18.existsSync(p) ? { kind: "program", path: p } : null;
24915
24949
  }
24916
24950
  return null;
@@ -25032,12 +25066,12 @@ function noteObservedEngine(observed, file2 = enginePath()) {
25032
25066
  return true;
25033
25067
  }
25034
25068
  function runSmoke(bin, timeoutMs = 9e4) {
25035
- return new Promise((resolve12) => {
25069
+ return new Promise((resolve13) => {
25036
25070
  let settled = false;
25037
25071
  const done = (r) => {
25038
25072
  if (!settled) {
25039
25073
  settled = true;
25040
- resolve12(r);
25074
+ resolve13(r);
25041
25075
  }
25042
25076
  };
25043
25077
  const cmd = claudeCommand(bin, [
@@ -25262,13 +25296,13 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
25262
25296
  const locationName = SANDBOX_ALLOWLIST[locationId2] || opts.name || locationId2;
25263
25297
  const spec = opts.spec ?? STAGE_SPECS[stage];
25264
25298
  if (!spec) return Promise.reject(new Error(`Stage ${stage} is not powered yet.`));
25265
- return new Promise((resolve12) => {
25299
+ return new Promise((resolve13) => {
25266
25300
  void (async () => {
25267
25301
  const bin = claudeBin();
25268
25302
  const engineCheck = await ensureEngineReady(bin, (line3) => onEvent({ kind: "status", line: line3 }));
25269
25303
  if (!engineCheck.ok) {
25270
25304
  onEvent({ kind: "error", line: engineCheck.reason });
25271
- resolve12({ ok: false, error: engineCheck.reason, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
25305
+ resolve13({ ok: false, error: engineCheck.reason, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
25272
25306
  return;
25273
25307
  }
25274
25308
  onEvent({ kind: "status", line: `Starting ${spec.name} for ${locationName}.` });
@@ -25360,7 +25394,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
25360
25394
  if (code !== 0 && !lastText) {
25361
25395
  const outcome2 = { ok: false, error: `claude exited ${code}: ${stderrTail.trim().slice(-200) || "no output"}`, ...engine };
25362
25396
  onEvent({ kind: "error", line: outcome2.error });
25363
- resolve12(outcome2);
25397
+ resolve13(outcome2);
25364
25398
  return;
25365
25399
  }
25366
25400
  const outcome = { ...parseResultLine(lastText), ...engine };
@@ -25373,19 +25407,19 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
25373
25407
  const detail = outcome.error || (outcome.issues?.length ? outcome.issues.join(" \xB7 ") : "") || outcome.summary || "no detail reported";
25374
25408
  const success = outcome.formId ? `Done: form "${outcome.formName}" (${outcome.formId})` : `Done: ${outcome.summary ?? "stage complete"}`;
25375
25409
  onEvent({ kind: "result", line: outcome.ok ? success : `Not passed: ${detail}` });
25376
- resolve12(outcome);
25410
+ resolve13(outcome);
25377
25411
  });
25378
25412
  child.on("error", (err) => {
25379
25413
  clearTimeout(timeout);
25380
25414
  if (progressPoll) clearInterval(progressPoll);
25381
25415
  const outcome = { ok: false, error: `could not start claude: ${err.message}` };
25382
25416
  onEvent({ kind: "error", line: outcome.error });
25383
- resolve12(outcome);
25417
+ resolve13(outcome);
25384
25418
  });
25385
25419
  })().catch((err) => {
25386
25420
  const line3 = err instanceof Error ? err.message : String(err);
25387
25421
  onEvent({ kind: "error", line: line3 });
25388
- resolve12({ ok: false, error: line3 });
25422
+ resolve13({ ok: false, error: line3 });
25389
25423
  });
25390
25424
  });
25391
25425
  }
@@ -26245,8 +26279,8 @@ function lintDesignContract(html2, medium) {
26245
26279
  for (let i = 0; i < sectionTags.length; i++) {
26246
26280
  const start = sectionTags[i].index;
26247
26281
  const end = i + 1 < sectionTags.length ? sectionTags[i + 1].index : html2.length;
26248
- const inside2 = opens.filter((t) => t.index > start && t.index < end);
26249
- headlinesPerSection.push(inside2.filter((t) => t.attrs["data-role"] === "headline").length);
26282
+ const inside3 = opens.filter((t) => t.index > start && t.index < end);
26283
+ headlinesPerSection.push(inside3.filter((t) => t.attrs["data-role"] === "headline").length);
26250
26284
  }
26251
26285
  for (const t of opens) {
26252
26286
  const r = t.attrs["data-role"];
@@ -26584,13 +26618,13 @@ var init_handoff_pack = __esm({
26584
26618
 
26585
26619
  // src/command-os/handoff-run.ts
26586
26620
  function askNoTools(prompt, opts = {}) {
26587
- return new Promise((resolve12) => {
26621
+ return new Promise((resolve13) => {
26588
26622
  let bin;
26589
26623
  try {
26590
26624
  bin = claudeBin();
26591
26625
  } catch {
26592
26626
  opts.onCost?.();
26593
- resolve12("");
26627
+ resolve13("");
26594
26628
  return;
26595
26629
  }
26596
26630
  const cmd = claudeCommand(bin, ["-p", prompt, "--output-format", "stream-json", "--verbose", "--max-turns", String(opts.maxTurns ?? 3), "--allowedTools", "", "--disallowedTools", NO_TOOLS_DENY]);
@@ -26628,12 +26662,12 @@ function askNoTools(prompt, opts = {}) {
26628
26662
  child.on("error", () => {
26629
26663
  clearTimeout(timeout);
26630
26664
  report2();
26631
- resolve12(lastText);
26665
+ resolve13(lastText);
26632
26666
  });
26633
26667
  child.on("close", () => {
26634
26668
  clearTimeout(timeout);
26635
26669
  report2();
26636
- resolve12(lastText);
26670
+ resolve13(lastText);
26637
26671
  });
26638
26672
  });
26639
26673
  }
@@ -27726,8 +27760,8 @@ function dailyBrief(input) {
27726
27760
  } else if (renewals.length) {
27727
27761
  headline3 = `${renewals[0].client}'s agreement renews ${renewals[0].inWords}.`;
27728
27762
  } else if (payingButQuiet.length) {
27729
- const q2 = payingButQuiet[0];
27730
- headline3 = payingButQuiet.length === 1 ? `${q2.client} is paying you and has booked nothing in ${q2.days} days.` : `${payingButQuiet.length} clients are paying you and have booked nothing in ${q2.days} days. The largest is ${q2.client}.`;
27763
+ const q3 = payingButQuiet[0];
27764
+ headline3 = payingButQuiet.length === 1 ? `${q3.client} is paying you and has booked nothing in ${q3.days} days.` : `${payingButQuiet.length} clients are paying you and have booked nothing in ${q3.days} days. The largest is ${q3.client}.`;
27731
27765
  } else if (underFloor.length) {
27732
27766
  headline3 = `Nothing is waiting, but ${underFloor.length} client${underFloor.length === 1 ? " is" : "s are"} billed below your own floor.`;
27733
27767
  } else if (newLeads.length) {
@@ -28057,7 +28091,7 @@ function stopReasonSentence(reason3, fetched, cap) {
28057
28091
  }
28058
28092
  }
28059
28093
  function delay(ms) {
28060
- return new Promise((resolve12) => setTimeout(resolve12, ms));
28094
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
28061
28095
  }
28062
28096
  async function fetchAllContacts(client, locationId2, options = {}) {
28063
28097
  const cap = options.maxContacts ?? DEFAULT_MAX_CONTACTS;
@@ -28763,7 +28797,7 @@ var init_client_capabilities = __esm({
28763
28797
  { key: "reputation", label: "Reputation and content", blurb: "Reviews, social posts and the blog." },
28764
28798
  { key: "health", label: "Health and handoff", blurb: "Proving it works, keeping it clean, and handing it over." }
28765
28799
  ];
28766
- N = (label, path41, params, key, nameField, version) => ({ label, path: path41, params, key, nameField, version });
28800
+ N = (label, path42, params, key, nameField, version) => ({ label, path: path42, params, key, nameField, version });
28767
28801
  AI2 = "2021-04-15";
28768
28802
  CAPABILITIES = [
28769
28803
  // ── Set up ──────────────────────────────────────────────────────────────
@@ -29024,7 +29058,7 @@ var require_package = __commonJS({
29024
29058
  "package.json"(exports2, module2) {
29025
29059
  module2.exports = {
29026
29060
  name: "@elitedcs/ghl-mcp",
29027
- version: "3.84.7",
29061
+ version: "3.84.9",
29028
29062
  mcpName: "io.github.drjerryrelth/ghl-command",
29029
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.",
29030
29064
  main: "dist/index.js",
@@ -31395,20 +31429,20 @@ function clientHeadline(items) {
31395
31429
  const live = items.filter((i) => i.state === "in-place").length;
31396
31430
  const notYet = items.filter((i) => i.state === "not-yet").length;
31397
31431
  const waiting = items.filter((i) => i.state === "waiting").length;
31398
- const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
31432
+ const plural2 = (n, one, many) => `${n} ${n === 1 ? one : many}`;
31399
31433
  if (!items.length) return { headline: "Your account has not been checked yet", sub: "As soon as we run the first check, this page fills in on its own." };
31400
31434
  if (!notYet && !waiting) {
31401
31435
  return {
31402
31436
  headline: "Everything we set out to build is live",
31403
- sub: `All ${plural(live, "item was", "items were")} read back from your live account, not assumed. This page re-checks itself and updates here, so it is always current.`
31437
+ sub: `All ${plural2(live, "item was", "items were")} read back from your live account, not assumed. This page re-checks itself and updates here, so it is always current.`
31404
31438
  };
31405
31439
  }
31406
31440
  const parts = [];
31407
- if (live) parts.push(`${plural(live, "thing is", "things are")} live in your account`);
31408
- if (notYet) parts.push(`${plural(notYet, "is", "are")} built but not confirmed yet`);
31409
- if (waiting) parts.push(`${plural(waiting, "is", "are")} waiting on someone else`);
31441
+ if (live) parts.push(`${plural2(live, "thing is", "things are")} live in your account`);
31442
+ if (notYet) parts.push(`${plural2(notYet, "is", "are")} built but not confirmed yet`);
31443
+ if (waiting) parts.push(`${plural2(waiting, "is", "are")} waiting on someone else`);
31410
31444
  return {
31411
- headline: notYet ? `${plural(notYet, "item", "items")} still to confirm` : `${plural(waiting, "item is", "items are")} waiting on someone else`,
31445
+ headline: notYet ? `${plural2(notYet, "item", "items")} still to confirm` : `${plural2(waiting, "item is", "items are")} waiting on someone else`,
31412
31446
  sub: `${parts.join(", ").replace(/, ([^,]*)$/, " and $1")}. Everything below was read back from your live account.`
31413
31447
  };
31414
31448
  }
@@ -31912,9 +31946,9 @@ function intakeInput(answers, industry, questions) {
31912
31946
  }
31913
31947
  function shortenPlanLine(text2, limit = 360) {
31914
31948
  if (text2.length <= limit) return text2;
31915
- const tokens = text2.match(/(?:\[[^\]]*\]|[^\s[])+/g) ?? [];
31949
+ const tokens2 = text2.match(/(?:\[[^\]]*\]|[^\s[])+/g) ?? [];
31916
31950
  let end = "";
31917
- for (const token2 of tokens) {
31951
+ for (const token2 of tokens2) {
31918
31952
  const next = end ? `${end} ${token2}` : token2;
31919
31953
  if (next.length > limit - 1) {
31920
31954
  if (!end) return token2.length === text2.length ? text2 : `${token2}\u2026`;
@@ -31982,8 +32016,8 @@ function executablePlanLines(saved) {
31982
32016
  const refs = [...names.keys()].sort((a, b) => b.length - a.length);
31983
32017
  const refPattern = refs.length ? new RegExp(`(?<![\\w./:@])(?:${refs.map(escapeRE).join("|")})(?![\\w]|\\.[\\w])`, "g") : void 0;
31984
32018
  const missingRefPattern = /(?<![\w./:@])(user|pipeline|stage|field|tag|workflow|form|calendar|email_template|sms_template|email|sms|cv|handoff|funnel)\.[a-z0-9_]+(?![\w]|\.[\w])/g;
31985
- const resolve12 = (s) => s.replace(missingRefPattern, (ref) => names.get(ref) || `Unnamed ${kindOf2(ref.split(".")[0])}`);
31986
- const renderRefs = (s) => resolve12(refPattern ? s.replace(refPattern, (ref) => names.get(ref)) : s);
32019
+ const resolve13 = (s) => s.replace(missingRefPattern, (ref) => names.get(ref) || `Unnamed ${kindOf2(ref.split(".")[0])}`);
32020
+ const renderRefs = (s) => resolve13(refPattern ? s.replace(refPattern, (ref) => names.get(ref)) : s);
31987
32021
  const mergeLabel = (field) => {
31988
32022
  const [group, ...parts] = field.trim().split(".");
31989
32023
  const key = parts.join(".");
@@ -32954,7 +32988,7 @@ var init_run_cost = __esm({
32954
32988
  // src/claude-call.ts
32955
32989
  function callClaudeRecorded(bin, argv, context, onCost = () => {
32956
32990
  }) {
32957
- return new Promise((resolve12, reject) => {
32991
+ return new Promise((resolve13, reject) => {
32958
32992
  let text2 = "";
32959
32993
  let buffer = "";
32960
32994
  let stderr = "";
@@ -32973,7 +33007,7 @@ function callClaudeRecorded(bin, argv, context, onCost = () => {
32973
33007
  onCost("The cost record could not be saved.");
32974
33008
  }
32975
33009
  onCost(`${context.kind}: ${facts.costUsd === void 0 ? "cost not reported" : formatUsd(facts.costUsd)}.`);
32976
- if (ok) resolve12(text2);
33010
+ if (ok) resolve13(text2);
32977
33011
  else reject(new Error("Check your Claude authentication in Your agency before reading this source."));
32978
33012
  };
32979
33013
  const consume = (line3) => {
@@ -33082,9 +33116,9 @@ function unknownKeys(layer, against) {
33082
33116
  return referenced.filter((k) => !have.has(k));
33083
33117
  }
33084
33118
  function installQuestionSet(opts = {}) {
33085
- return composeQuestionSet(opts).map((q2) => {
33086
- const base = INTAKE_QUESTIONS.find((b) => b.key === q2.key);
33087
- return base && base.label !== q2.label ? { ...q2, label: base.label } : q2;
33119
+ return composeQuestionSet(opts).map((q3) => {
33120
+ const base = INTAKE_QUESTIONS.find((b) => b.key === q3.key);
33121
+ return base && base.label !== q3.label ? { ...q3, label: base.label } : q3;
33088
33122
  });
33089
33123
  }
33090
33124
  function validatePacks() {
@@ -33391,7 +33425,7 @@ function parsePricePoints(text2) {
33391
33425
  const out = [];
33392
33426
  const segments = text2.split(/\r?\n|;|\.\s+/);
33393
33427
  const ENTRY = /^(.*?)\s*[-–—:|]?\s*\$?\s*([\d,]+(?:\.\d+)?)\s*(?:[-–—]\s*\$?\s*([\d,]+(?:\.\d+)?))?\s*(\/\s*[a-z]+|(?:per|an?|each)\s+[a-z]+)?\s*$/i;
33394
- const clean = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
33428
+ const clean2 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
33395
33429
  const num3 = (s) => s.replace(/,/g, "");
33396
33430
  const recur = (s) => s ? "/" + s.replace(/^\/|^(?:per|an?|each)\b/i, "").trim().toLowerCase() : "";
33397
33431
  for (const segment of segments) {
@@ -33399,7 +33433,7 @@ function parsePricePoints(text2) {
33399
33433
  if (!t) continue;
33400
33434
  const m = t.match(ENTRY);
33401
33435
  if (m && m[2]) {
33402
- const name = clean(m[1]);
33436
+ const name = clean2(m[1]);
33403
33437
  const price = (m[3] ? `${num3(m[2])}-${num3(m[3])}` : num3(m[2])) + recur(m[4]);
33404
33438
  out.push({ name: name || t, price });
33405
33439
  } else {
@@ -33529,11 +33563,11 @@ function splitOnMarkers(line3) {
33529
33563
  }
33530
33564
  function splitOnAnd(entry) {
33531
33565
  if (countDurations(entry) < 2) return [entry];
33532
- const tokens = entry.split(/,?\s+and\s+/i);
33533
- if (tokens.length < 2) return [entry];
33566
+ const tokens2 = entry.split(/,?\s+and\s+/i);
33567
+ if (tokens2.length < 2) return [entry];
33534
33568
  const out = [];
33535
33569
  let acc = "";
33536
- for (const tok of tokens) {
33570
+ for (const tok of tokens2) {
33537
33571
  if (acc && countDurations(acc) > 0 && countDurations(tok) > 0) {
33538
33572
  out.push(acc);
33539
33573
  acc = tok;
@@ -33748,9 +33782,9 @@ function presetForBusinessType(type) {
33748
33782
  return "generic";
33749
33783
  }
33750
33784
  }
33751
- function setPath(target, path41, value) {
33785
+ function setPath(target, path42, value) {
33752
33786
  if (value === void 0) return;
33753
- const parts = path41.split(".");
33787
+ const parts = path42.split(".");
33754
33788
  let node = target;
33755
33789
  for (let i = 0; i < parts.length - 1; i++) {
33756
33790
  const k = parts[i];
@@ -33785,10 +33819,10 @@ function buildFieldMapFromFormFields(fields, questions = INTAKE_QUESTIONS) {
33785
33819
  if (fieldKey && id) byFieldKey.set(fieldKey.toLowerCase(), id);
33786
33820
  }
33787
33821
  const map = {};
33788
- for (const q2 of questions) {
33789
- if (q2.field.kind !== "custom") continue;
33790
- const id = byFieldKey.get(expectedFieldKey(q2).toLowerCase());
33791
- if (id) map[q2.key] = id;
33822
+ for (const q3 of questions) {
33823
+ if (q3.field.kind !== "custom") continue;
33824
+ const id = byFieldKey.get(expectedFieldKey(q3).toLowerCase());
33825
+ if (id) map[q3.key] = id;
33792
33826
  }
33793
33827
  return map;
33794
33828
  }
@@ -33850,11 +33884,11 @@ function normalizeSubmissionToBrief(opts) {
33850
33884
  goal: {}
33851
33885
  };
33852
33886
  const warnings = [];
33853
- const rawFor = (q2) => {
33854
- const rawKey = q2.field.kind === "standard" ? q2.field.tag : opts.fieldMap[q2.key];
33887
+ const rawFor = (q3) => {
33888
+ const rawKey = q3.field.kind === "standard" ? q3.field.tag : opts.fieldMap[q3.key];
33855
33889
  return rawKey ? opts.others[rawKey] : void 0;
33856
33890
  };
33857
- const staffQ = questions.find((q2) => q2.briefKind === "staffList" && q2.briefPath);
33891
+ const staffQ = questions.find((q3) => q3.briefKind === "staffList" && q3.briefPath);
33858
33892
  let staff;
33859
33893
  if (staffQ) {
33860
33894
  const s = firstString(rawFor(staffQ));
@@ -33865,22 +33899,22 @@ function normalizeSubmissionToBrief(opts) {
33865
33899
  }
33866
33900
  }
33867
33901
  const staffNames = (staff ?? []).map((m) => m.name);
33868
- for (const q2 of questions) {
33869
- if (!q2.briefPath || q2.briefKind === "identity") continue;
33902
+ for (const q3 of questions) {
33903
+ if (!q3.briefPath || q3.briefKind === "identity") continue;
33870
33904
  let value;
33871
- if (q2 === staffQ) {
33905
+ if (q3 === staffQ) {
33872
33906
  value = staff;
33873
- } else if (q2.briefKind === "calendarList") {
33874
- const s = firstString(rawFor(q2));
33907
+ } else if (q3.briefKind === "calendarList") {
33908
+ const s = firstString(rawFor(q3));
33875
33909
  if (s) {
33876
33910
  const r = parseCalendarLines(s, staffNames);
33877
33911
  warnings.push(...r.warnings);
33878
33912
  value = r.calendars.length ? r.calendars : void 0;
33879
33913
  }
33880
33914
  } else {
33881
- value = coerceValue(rawFor(q2), q2.briefKind, warnings);
33915
+ value = coerceValue(rawFor(q3), q3.briefKind, warnings);
33882
33916
  }
33883
- setPath(brief, q2.briefPath, value);
33917
+ setPath(brief, q3.briefPath, value);
33884
33918
  }
33885
33919
  distillAssignments(brief, staffNames, warnings);
33886
33920
  if (warnings.length) brief.warnings = warnings;
@@ -34790,7 +34824,7 @@ function compute(ans, skip, asm) {
34790
34824
  var monthly = F.reduce(function(s, f) {
34791
34825
  return s + f.monthly;
34792
34826
  }, 0);
34793
- var stop = monthly * a("recovery");
34827
+ var stop2 = monthly * a("recovery");
34794
34828
  var oneoff = null;
34795
34829
  if (core && raw("dormant_count") !== null && a("reactivation") > 0) {
34796
34830
  oneoff = raw("dormant_count") * a("reactivation") * c * v;
@@ -34823,8 +34857,8 @@ function compute(ans, skip, asm) {
34823
34857
  core,
34824
34858
  monthly,
34825
34859
  annual: monthly * 12,
34826
- stop,
34827
- annualStop: stop * 12,
34860
+ stop: stop2,
34861
+ annualStop: stop2 * 12,
34828
34862
  oneoff,
34829
34863
  hrs,
34830
34864
  hrsWk,
@@ -34926,8 +34960,8 @@ var init_engine = __esm({
34926
34960
  show_rate: "How many booked appointments actually happen"
34927
34961
  };
34928
34962
  UNIT = {};
34929
- Q.forEach(function(q2) {
34930
- UNIT[q2[0]] = q2[4];
34963
+ Q.forEach(function(q3) {
34964
+ UNIT[q3[0]] = q3[4];
34931
34965
  });
34932
34966
  }
34933
34967
  });
@@ -34946,7 +34980,7 @@ function stageFor(opts) {
34946
34980
  function tagFor(stage) {
34947
34981
  return "assessment:" + stage.toLowerCase().replace(/\s+/g, "-");
34948
34982
  }
34949
- function renderHuman(state, computed, q2, today2) {
34983
+ function renderHuman(state, computed, q3, today2) {
34950
34984
  const lines = [];
34951
34985
  lines.push(`${NOTE_TITLE} \u2014 ${state.biz || "unnamed"}${state.trade ? " \u2014 " + state.trade : ""} \u2014 ${today2}`);
34952
34986
  if (computed) {
@@ -34961,8 +34995,8 @@ function renderHuman(state, computed, q2, today2) {
34961
34995
  const byArea = /* @__PURE__ */ new Map();
34962
34996
  const ids = [.../* @__PURE__ */ new Set([...Object.keys(state.ans), ...Object.keys(state.skip)])];
34963
34997
  for (const id of ids) {
34964
- const ask = q2.ask(id);
34965
- const area = q2.area(id);
34998
+ const ask = q3.ask(id);
34999
+ const area = q3.area(id);
34966
35000
  if (!ask || !area) continue;
34967
35001
  const skipped = state.skip[id] === true;
34968
35002
  const raw = state.ans[id];
@@ -34974,7 +35008,7 @@ function renderHuman(state, computed, q2, today2) {
34974
35008
  ${value}`);
34975
35009
  byArea.set(area, bucket);
34976
35010
  }
34977
- for (const area of q2.areaOrder()) {
35011
+ for (const area of q3.areaOrder()) {
34978
35012
  const bucket = byArea.get(area);
34979
35013
  if (!bucket || bucket.length === 0) continue;
34980
35014
  lines.push("");
@@ -34986,7 +35020,7 @@ function renderHuman(state, computed, q2, today2) {
34986
35020
  lines.push("");
34987
35021
  lines.push("ASSUMPTIONS THE OPERATOR SET");
34988
35022
  for (const [k, v] of asm) {
34989
- const label = q2.assumption?.(k) ?? k.replace(/_/g, " ");
35023
+ const label = q3.assumption?.(k) ?? k.replace(/_/g, " ");
34990
35024
  lines.push(` ${label}: ${Math.round((Number(v) || 0) * 100)}%${Number(v) > 0 ? "" : " (set to zero \u2014 its finding was dropped, not estimated)"}`);
34991
35025
  }
34992
35026
  }
@@ -35007,9 +35041,23 @@ function freshSalt() {
35007
35041
  function sanitiseHuman(text2) {
35008
35042
  return text2.split(MACHINE_MARKER).join("--- (marker text removed) ---");
35009
35043
  }
35010
- function buildNote(state, computed, q2, today2) {
35011
- const payload = JSON.stringify({ v: 1, savedAt: today2, state, computed });
35012
- return `${sanitiseHuman(renderHuman(state, computed, q2, today2))}
35044
+ function describeSource(src) {
35045
+ const title2 = src.title?.trim() ? ` "${src.title.trim()}"` : "";
35046
+ const by = src.fetchedBy === "claude" ? ", fetched by Claude" : src.fetchedBy === "upload" ? ", uploaded as a file" : "";
35047
+ return `${src.recorder} recording${title2}, recorded ${src.recordedAt}${by}`;
35048
+ }
35049
+ function buildNote(state, computed, q3, today2, source, confirmedByOwner) {
35050
+ const confirmed = confirmedByOwner?.length ? [...confirmedByOwner] : void 0;
35051
+ const payload = JSON.stringify({ v: 1, savedAt: today2, state, computed, ...source ? { source } : {}, ...confirmed ? { confirmedByOwner: confirmed } : {} });
35052
+ const human = renderHuman(state, computed, q3, today2);
35053
+ const lines = [
35054
+ ...source ? [`Source: ${describeSource(source)}`] : [],
35055
+ ...confirmed ? [`Confirmed by the owner, not heard word for word: ${confirmed.map((id) => q3.ask(id) ?? id).join("; ")}`] : []
35056
+ ];
35057
+ const withSource = lines.length ? human.replace(/\n/, `
35058
+ ${lines.join("\n")}
35059
+ `) : human;
35060
+ return `${sanitiseHuman(withSource)}
35013
35061
 
35014
35062
  ${MACHINE_MARKER}
35015
35063
  ${payload}`;
@@ -35034,7 +35082,9 @@ function parseNote(body) {
35034
35082
  if (!s.ans || typeof s.ans !== "object") return null;
35035
35083
  return {
35036
35084
  state: { biz: s.biz, trade: s.trade, ans: s.ans, skip: s.skip ?? {}, asm: s.asm ?? {}, ...s.client && typeof s.client === "object" ? { client: s.client } : {} },
35037
- computed: p.computed ?? null
35085
+ computed: p.computed ?? null,
35086
+ // Which answers the owner confirmed rather than were heard (BUG-2026-09-24-04), so the next save can carry the mark.
35087
+ ...Array.isArray(p.confirmedByOwner) ? { confirmedByOwner: p.confirmedByOwner.filter((x) => typeof x === "string") } : {}
35038
35088
  };
35039
35089
  }
35040
35090
  function buildContact(state, stage, contact) {
@@ -35111,7 +35161,16 @@ function parseNotes(bodies) {
35111
35161
  ${parts.map((p) => p.text).join("")}`);
35112
35162
  }
35113
35163
  function pickLatest(notes) {
35114
- if (!Array.isArray(notes)) return null;
35164
+ for (const parsed2 of latestFirst(notes)) if (parsed2) return parsed2;
35165
+ return null;
35166
+ }
35167
+ function pickLatestStrict(notes) {
35168
+ const all = latestFirst(notes);
35169
+ if (all.length === 0) return { record: null, clean: true };
35170
+ return { record: all[0], clean: all[0] !== null };
35171
+ }
35172
+ function latestFirst(notes) {
35173
+ if (!Array.isArray(notes)) return [];
35115
35174
  const groups = /* @__PURE__ */ new Map();
35116
35175
  notes.forEach((note, i) => {
35117
35176
  const body = typeof note?.body === "string" ? note.body : null;
@@ -35129,18 +35188,21 @@ function pickLatest(notes) {
35129
35188
  groups.set(key, g);
35130
35189
  });
35131
35190
  const ordered = [...groups.values()].sort((a, b) => a.when === b.when ? 0 : a.when > b.when ? -1 : 1);
35191
+ const out = [];
35132
35192
  for (const g of ordered) {
35133
35193
  const newestPerK = /* @__PURE__ */ new Map();
35134
35194
  for (const p of g.parts) {
35135
35195
  const seen = newestPerK.get(p.k);
35136
35196
  if (!seen || p.when > seen.when) newestPerK.set(p.k, p);
35137
35197
  }
35138
- if (newestPerK.size !== g.n) continue;
35198
+ if (newestPerK.size !== g.n) {
35199
+ out.push(null);
35200
+ continue;
35201
+ }
35139
35202
  const bodies = [...newestPerK.values()].sort((a, b) => a.k - b.k).map((p) => p.body);
35140
- const parsed2 = bodies.length === 1 ? parseNote(bodies[0]) : parseNotes(bodies);
35141
- if (parsed2) return parsed2;
35203
+ out.push(bodies.length === 1 ? parseNote(bodies[0]) : parseNotes(bodies));
35142
35204
  }
35143
- return null;
35205
+ return out;
35144
35206
  }
35145
35207
  var PIPELINE_NAME, NOTE_TITLE, MACHINE_MARKER, STAGES, MARKER_LINE, NOTE_LIMIT;
35146
35208
  var init_store3 = __esm({
@@ -35216,13 +35278,13 @@ function count(value) {
35216
35278
  const n = Number(value);
35217
35279
  return Number.isSafeInteger(n) ? n : void 0;
35218
35280
  }
35219
- function matchesQuestionChoices(q2, value) {
35220
- const fixed = q2.options !== void 0 || q2.field.kind === "custom" && ["SINGLE_OPTIONS", "MULTIPLE_OPTIONS", "CHECKBOX"].includes(q2.field.dataType);
35281
+ function matchesQuestionChoices(q3, value) {
35282
+ const fixed = q3.options !== void 0 || q3.field.kind === "custom" && ["SINGLE_OPTIONS", "MULTIPLE_OPTIONS", "CHECKBOX"].includes(q3.field.dataType);
35221
35283
  if (!fixed) return true;
35222
35284
  if (Array.isArray(value)) {
35223
- return q2.field.kind === "custom" && ["MULTIPLE_OPTIONS", "CHECKBOX"].includes(q2.field.dataType) && value.every((choice) => q2.options?.includes(choice) === true);
35285
+ return q3.field.kind === "custom" && ["MULTIPLE_OPTIONS", "CHECKBOX"].includes(q3.field.dataType) && value.every((choice) => q3.options?.includes(choice) === true);
35224
35286
  }
35225
- return q2.options?.includes(value) === true;
35287
+ return q3.options?.includes(value) === true;
35226
35288
  }
35227
35289
  function mapAssessmentToIntake(s, questions, contact) {
35228
35290
  const values = {
@@ -35248,14 +35310,14 @@ function mapAssessmentToIntake(s, questions, contact) {
35248
35310
  const answers = {};
35249
35311
  const filled = [], leftForMember = [];
35250
35312
  for (const mapping of ASSESSMENT_INTAKE_MAPPING) {
35251
- const q2 = questions.find((q3) => q3.key === mapping.target);
35313
+ const q3 = questions.find((q4) => q4.key === mapping.target);
35252
35314
  const value = values[mapping.target]?.trim();
35253
- if (!q2 || !value || !matchesQuestionChoices(q2, value)) {
35254
- leftForMember.push({ key: mapping.target, question: q2?.label ?? mapping.target, reason: !q2 ? "This question is not in this intake." : !value ? "The assessment does not give this answer." : "The answer does not match one of this intake\u2019s choices." });
35315
+ if (!q3 || !value || !matchesQuestionChoices(q3, value)) {
35316
+ leftForMember.push({ key: mapping.target, question: q3?.label ?? mapping.target, reason: !q3 ? "This question is not in this intake." : !value ? "The assessment does not give this answer." : "The answer does not match one of this intake\u2019s choices." });
35255
35317
  continue;
35256
35318
  }
35257
- answers[q2.label] = q2.field.kind === "custom" && q2.field.dataType === "MULTIPLE_OPTIONS" ? [value] : value;
35258
- filled.push(q2.label);
35319
+ answers[q3.label] = q3.field.kind === "custom" && q3.field.dataType === "MULTIPLE_OPTIONS" ? [value] : value;
35320
+ filled.push(q3.label);
35259
35321
  }
35260
35322
  return { answers, filled, leftForMember };
35261
35323
  }
@@ -35273,19 +35335,19 @@ function fillFromSnapshot(state, locationId2, questions) {
35273
35335
  const result = mapAssessmentToIntake(snapshot.assessment, questions, snapshot.contact);
35274
35336
  const answers = { ...state.intakeInputs?.[locationId2]?.answers, ...state.intakeDrafts?.[locationId2]?.answers };
35275
35337
  for (const [label, value] of Object.entries(result.answers)) {
35276
- const q2 = questions.find((q3) => q3.label === label);
35277
- const previous = answers[label] ?? answers[q2.key];
35338
+ const q3 = questions.find((q4) => q4.label === label);
35339
+ const previous = answers[label] ?? answers[q3.key];
35278
35340
  let merged = value;
35279
- if ((q2.key === "anything_else" || q2.key === "price_points") && previous && !String(previous).includes(String(value))) merged = `${previous}
35341
+ if ((q3.key === "anything_else" || q3.key === "price_points") && previous && !String(previous).includes(String(value))) merged = `${previous}
35280
35342
  ${value}`;
35281
- else if ((q2.key === "anything_else" || q2.key === "price_points") && previous) merged = previous;
35282
- if (!matchesQuestionChoices(q2, merged)) {
35343
+ else if ((q3.key === "anything_else" || q3.key === "price_points") && previous) merged = previous;
35344
+ if (!matchesQuestionChoices(q3, merged)) {
35283
35345
  result.filled = result.filled.filter((question) => question !== label);
35284
- result.leftForMember.push({ key: q2.key, question: label, reason: "The combined answer does not match this intake\u2019s choices. Your saved answer was left unchanged." });
35346
+ result.leftForMember.push({ key: q3.key, question: label, reason: "The combined answer does not match this intake\u2019s choices. Your saved answer was left unchanged." });
35285
35347
  continue;
35286
35348
  }
35287
35349
  answers[label] = merged;
35288
- if (q2.key !== label) delete answers[q2.key];
35350
+ if (q3.key !== label) delete answers[q3.key];
35289
35351
  }
35290
35352
  return { state: { ...state, intakeDrafts: { ...state.intakeDrafts, [locationId2]: { answers, savedAt: (/* @__PURE__ */ new Date()).toISOString() } } }, filled: result.filled, leftForMember: result.leftForMember };
35291
35353
  }
@@ -35373,10 +35435,10 @@ var init_attachment = __esm({
35373
35435
  async function pickIntakeContact(client, locationId2, fieldIdToLabel, knownContactId, formName) {
35374
35436
  const ids = /* @__PURE__ */ new Set();
35375
35437
  if (knownContactId) ids.add(knownContactId);
35376
- for (const q2 of [...INTAKE_TAGS, ...formName ? [formName] : []]) {
35438
+ for (const q3 of [...INTAKE_TAGS, ...formName ? [formName] : []]) {
35377
35439
  try {
35378
- const found = await client.get("/contacts/", { params: { locationId: locationId2, query: q2, limit: 20 } });
35379
- for (const c of found.contacts ?? []) if ((c.tags ?? []).some((t) => INTAKE_TAGS.includes(t)) || q2 === formName) ids.add(c.id);
35440
+ const found = await client.get("/contacts/", { params: { locationId: locationId2, query: q3, limit: 20 } });
35441
+ for (const c of found.contacts ?? []) if ((c.tags ?? []).some((t) => INTAKE_TAGS.includes(t)) || q3 === formName) ids.add(c.id);
35380
35442
  } catch {
35381
35443
  }
35382
35444
  }
@@ -35459,17 +35521,17 @@ function buildHeaderField(title2, index) {
35459
35521
  weight: 400
35460
35522
  };
35461
35523
  }
35462
- function buildStandardFormField(q2) {
35463
- if (q2.field.kind !== "standard") throw new Error(`buildStandardFormField: ${q2.key} is not standard`);
35464
- const tag = q2.field.tag;
35524
+ function buildStandardFormField(q3) {
35525
+ if (q3.field.kind !== "standard") throw new Error(`buildStandardFormField: ${q3.key} is not standard`);
35526
+ const tag = q3.field.tag;
35465
35527
  const isEmail = tag === "email";
35466
35528
  const isPhone = tag === "phone";
35467
35529
  const base = {
35468
35530
  fieldWidthPercentage: 100,
35469
35531
  hiddenFieldQueryKey: tag,
35470
- label: q2.label,
35471
- placeholder: q2.placeholder ?? q2.label,
35472
- required: q2.required,
35532
+ label: q3.label,
35533
+ placeholder: q3.placeholder ?? q3.label,
35534
+ required: q3.required,
35473
35535
  standard: true,
35474
35536
  tag,
35475
35537
  type: isEmail ? "email" : "text",
@@ -35478,7 +35540,7 @@ function buildStandardFormField(q2) {
35478
35540
  if (isPhone) base.enableCountryPicker = false;
35479
35541
  return base;
35480
35542
  }
35481
- function buildCustomFormField(q2, record, locationId2, position2) {
35543
+ function buildCustomFormField(q3, record, locationId2, position2) {
35482
35544
  const type = formFieldType(record.dataType);
35483
35545
  const field = {
35484
35546
  Id: record.id,
@@ -35486,7 +35548,7 @@ function buildCustomFormField(q2, record, locationId2, position2) {
35486
35548
  tag: record.id,
35487
35549
  active: false,
35488
35550
  allowCustomOption: false,
35489
- customFieldLabel: q2.label,
35551
+ customFieldLabel: q3.label,
35490
35552
  dataType: record.dataType,
35491
35553
  dateAdded: record.dateAdded ?? "",
35492
35554
  description: "",
@@ -35496,19 +35558,19 @@ function buildCustomFormField(q2, record, locationId2, position2) {
35496
35558
  fieldWidthPercentage: 100,
35497
35559
  fieldsCount: 0,
35498
35560
  hiddenFieldQueryKey: slug(record.name),
35499
- label: q2.label,
35561
+ label: q3.label,
35500
35562
  locationId: locationId2,
35501
35563
  model: record.model ?? "contact",
35502
35564
  name: record.name,
35503
35565
  parentId: record.parentId ?? "",
35504
- placeholder: q2.placeholder ?? "",
35566
+ placeholder: q3.placeholder ?? "",
35505
35567
  position: position2,
35506
- required: q2.required,
35568
+ required: q3.required,
35507
35569
  showInForms: true,
35508
35570
  standard: false,
35509
35571
  type
35510
35572
  };
35511
- const options = record.picklistOptions ?? q2.options ?? void 0;
35573
+ const options = record.picklistOptions ?? q3.options ?? void 0;
35512
35574
  if (type === "single_options" || type === "multiple_options" || type === "checkbox") {
35513
35575
  field.picklistOptions = options ? [...options] : [];
35514
35576
  }
@@ -35557,17 +35619,17 @@ function buildIntakeFormData(opts) {
35557
35619
  let lastSection = "";
35558
35620
  let headerIndex = 0;
35559
35621
  let position2 = 0;
35560
- for (const q2 of questions) {
35561
- if (q2.section !== lastSection) {
35562
- fields.push(buildHeaderField(q2.sectionLabel, headerIndex++));
35563
- lastSection = q2.section;
35622
+ for (const q3 of questions) {
35623
+ if (q3.section !== lastSection) {
35624
+ fields.push(buildHeaderField(q3.sectionLabel, headerIndex++));
35625
+ lastSection = q3.section;
35564
35626
  }
35565
- if (q2.field.kind === "standard") {
35566
- fields.push(buildStandardFormField(q2));
35627
+ if (q3.field.kind === "standard") {
35628
+ fields.push(buildStandardFormField(q3));
35567
35629
  } else {
35568
- const record = opts.resolved.get(q2.key);
35630
+ const record = opts.resolved.get(q3.key);
35569
35631
  if (!record) continue;
35570
- fields.push(buildCustomFormField(q2, record, opts.locationId, position2 += 50));
35632
+ fields.push(buildCustomFormField(q3, record, opts.locationId, position2 += 50));
35571
35633
  }
35572
35634
  }
35573
35635
  fields.push(buildSubmitButton("Submit"));
@@ -35597,33 +35659,67 @@ var init_form_template = __esm({
35597
35659
  }
35598
35660
  });
35599
35661
 
35662
+ // src/command-os/assessment/words.ts
35663
+ function tokens(s) {
35664
+ return String(s ?? "").toLowerCase().replace(/[‘’ʼ]/g, "'").replace(/[–—]/g, "-").replace(/%/g, " percent ").split(/[^a-z0-9.']+/).map((w) => w.replace(/^\.+|\.+$/g, "")).filter(Boolean);
35665
+ }
35666
+ function numericValue(value) {
35667
+ const v = String(value ?? "").trim().toLowerCase().replace(/[$,\s]/g, "");
35668
+ const m = /^(-?\d+(?:\.\d+)?)(%|k|m)?$/.exec(v);
35669
+ if (!m) return null;
35670
+ const n = Number(m[1]) * (m[2] === "k" ? 1e3 : m[2] === "m" ? 1e6 : 1);
35671
+ return { n, percent: m[2] === "%" };
35672
+ }
35673
+ var init_words = __esm({
35674
+ "src/command-os/assessment/words.ts"() {
35675
+ "use strict";
35676
+ }
35677
+ });
35678
+
35600
35679
  // src/command-os/assessment/transcript.ts
35601
35680
  function loose(s) {
35602
35681
  return String(s ?? "").replace(/[‘’ʼ]/g, "'").replace(/[“”]/g, '"').replace(/[–—]/g, "-").replace(/\s+/g, " ").trim().toLowerCase();
35603
35682
  }
35604
- function digits(s) {
35605
- return String(s ?? "").replace(/[^0-9]/g, "");
35606
- }
35607
- function valueIsInQuote(value, quote) {
35608
- const v = loose(value);
35609
- const q2 = loose(quote);
35610
- if (v.length === 0) return false;
35611
- if (q2.includes(v)) return true;
35612
- const dv = digits(value);
35613
- if (dv.length > 0 && digits(quote).includes(dv)) return true;
35614
- return false;
35683
+ function valueHeard(value, quote, unit) {
35684
+ const v = String(value ?? "").trim();
35685
+ if (unit ? NUMERIC_UNITS.has(unit) : numericValue(v) !== null) return { ok: false, because: NUMBER_HELD };
35686
+ const tv = tokens(v).join(" ");
35687
+ if (tv && ` ${tokens(quote).join(" ")} `.includes(` ${tv} `)) return { ok: true };
35688
+ return { ok: false, because: `"${v}" is not in what they said, so it was worked out rather than heard. Confirm it before it counts.` };
35615
35689
  }
35616
- function citationCheck(material, value, quote) {
35690
+ function citationCheck(material, value, quote, unit) {
35617
35691
  const v = String(value ?? "").trim();
35618
- const q2 = String(quote ?? "").trim();
35692
+ const q3 = String(quote ?? "").trim();
35619
35693
  if (v === "") return { verdict: "refused", because: "no value was given." };
35620
- if (q2 === "") return { verdict: "refused", because: "no quote was given, so there is nothing tying this answer to what they said." };
35621
- if (q2.length > MAX_QUOTE) return { verdict: "refused", because: `the quote is ${q2.length} characters. A quote that long is a summary, not a citation.` };
35694
+ const said = quoteCheck(material, q3);
35695
+ if (said.verdict === "refused") return said;
35696
+ const heard = valueHeard(v, q3, unit);
35697
+ if (!heard.ok) return { verdict: "held", because: heard.because };
35698
+ return { verdict: "ok" };
35699
+ }
35700
+ function quoteCheck(material, quote, opts = {}) {
35701
+ const q3 = String(quote ?? "").trim();
35702
+ if (q3 === "") return { verdict: "refused", because: "no quote was given, so there is nothing tying this answer to what they said." };
35703
+ if (q3.length > MAX_QUOTE) return { verdict: "refused", because: `the quote is ${q3.length} characters. A quote that long is a summary, not a citation.` };
35704
+ const needle = loose(q3);
35705
+ const words = needle.split(" ").filter((w) => WORDCHAR.test(w)).length;
35706
+ if (opts.minWords && words < opts.minWords) {
35707
+ return { verdict: "refused", because: `the quote is ${words === 1 ? "one word" : `${words} words`}. It needs at least ${opts.minWords} words to show they actually said it.` };
35708
+ }
35622
35709
  const hay = loose(material);
35623
- if (hay.length === 0 || !hay.includes(loose(q2))) return { verdict: "refused", because: "that quote does not appear in the transcript." };
35624
- if (!valueIsInQuote(v, q2)) return { verdict: "held", because: `"${v}" is not in what they said, so it was worked out rather than heard. Confirm it before it counts.` };
35710
+ if (hay.length === 0 || !containsAsWords(hay, needle)) return { verdict: "refused", because: "that quote does not appear in the transcript." };
35625
35711
  return { verdict: "ok" };
35626
35712
  }
35713
+ function containsAsWords(hay, needle) {
35714
+ const checkStart = WORDCHAR.test(needle[0] ?? "");
35715
+ const checkEnd = WORDCHAR.test(needle[needle.length - 1] ?? "");
35716
+ for (let i = hay.indexOf(needle); i !== -1; i = hay.indexOf(needle, i + 1)) {
35717
+ const before = i > 0 ? hay[i - 1] : "";
35718
+ const after = hay[i + needle.length] ?? "";
35719
+ if ((!checkStart || !WORDCHAR.test(before)) && (!checkEnd || !WORDCHAR.test(after))) return true;
35720
+ }
35721
+ return false;
35722
+ }
35627
35723
  function fitsItsUnit(value, unit) {
35628
35724
  if (unit === void 0 || unit === "text") return true;
35629
35725
  const n = parseTyped(value, unit);
@@ -35659,7 +35755,7 @@ function gateExtraction(transcript, proposed, questions) {
35659
35755
  rejected.push({ ...item, because: "the same question was answered twice; only the first was taken." });
35660
35756
  continue;
35661
35757
  }
35662
- const cite = citationCheck(transcript, item.value, item.quote);
35758
+ const cite = citationCheck(transcript, item.value, item.quote, questions.get(item.questionId));
35663
35759
  if (cite.verdict === "refused") {
35664
35760
  rejected.push({ ...item, because: cite.because });
35665
35761
  continue;
@@ -35685,12 +35781,16 @@ function gateExtraction(transcript, proposed, questions) {
35685
35781
  function mergeAccepted(ans, result) {
35686
35782
  return { ...ans, ...result.patch };
35687
35783
  }
35688
- var MAX_QUOTE;
35784
+ var NUMERIC_UNITS, NUMBER_HELD, MAX_QUOTE, WORDCHAR;
35689
35785
  var init_transcript = __esm({
35690
35786
  "src/command-os/assessment/transcript.ts"() {
35691
35787
  "use strict";
35692
35788
  init_engine();
35789
+ init_words();
35790
+ NUMERIC_UNITS = /* @__PURE__ */ new Set(["count", "currency", "rate", "minutes", "hours"]);
35791
+ NUMBER_HELD = "a number: confirm it. Numbers always wait for the owner's OK, because no word check can tell a figure that answers the question from one said in passing.";
35693
35792
  MAX_QUOTE = 400;
35793
+ WORDCHAR = /[\p{L}\p{N}]/u;
35694
35794
  }
35695
35795
  });
35696
35796
 
@@ -35767,8 +35867,8 @@ async function readIntakeSnapshot(client, locationId2, sentMarker) {
35767
35867
  if (!submissions) {
35768
35868
  try {
35769
35869
  const seen = /* @__PURE__ */ new Map();
35770
- for (const q2 of [INTAKE_CONTACT_TAG, INTAKE_FORM_NAME2]) {
35771
- const found = await client.get("/contacts/", { params: { locationId: locationId2, query: q2, limit: 20 } });
35870
+ for (const q3 of [INTAKE_CONTACT_TAG, INTAKE_FORM_NAME2]) {
35871
+ const found = await client.get("/contacts/", { params: { locationId: locationId2, query: q3, limit: 20 } });
35772
35872
  for (const c of found.contacts ?? []) seen.set(c.id, c);
35773
35873
  }
35774
35874
  const isFormSubmission = (c) => c.source === INTAKE_FORM_NAME2 || (c.attributions ?? []).some((a) => a.mediumId === form.id || a.medium === "form");
@@ -35902,15 +36002,15 @@ async function runPrefill(call2, labels, material) {
35902
36002
  }
35903
36003
  function validatePrefillAnswers(questions, proposed) {
35904
36004
  const result = {};
35905
- for (const q2 of questions) {
35906
- const raw = proposed[q2.label];
36005
+ for (const q3 of questions) {
36006
+ const raw = proposed[q3.label];
35907
36007
  if (raw === void 0) continue;
35908
36008
  const values = (Array.isArray(raw) ? raw : [raw]).map((x) => x.trim()).filter(Boolean);
35909
36009
  if (!values.length) continue;
35910
- if (q2.options?.length) {
35911
- if (values.some((v) => !q2.options.includes(v)) || !q2.multi && values.length !== 1) continue;
35912
- result[q2.label] = q2.multi ? [...new Set(values)] : values[0];
35913
- } else if (values.length === 1) result[q2.label] = values[0];
36010
+ if (q3.options?.length) {
36011
+ if (values.some((v) => !q3.options.includes(v)) || !q3.multi && values.length !== 1) continue;
36012
+ result[q3.label] = q3.multi ? [...new Set(values)] : values[0];
36013
+ } else if (values.length === 1) result[q3.label] = values[0];
35914
36014
  }
35915
36015
  return result;
35916
36016
  }
@@ -39232,9 +39332,9 @@ function renderReport2(opts) {
39232
39332
  if (opts.answered && opts.answered.length) {
39233
39333
  out.push('<h2>What we started from, in your words</h2><table class="qa">');
39234
39334
  for (var a = 0; a < opts.answered.length; a++) {
39235
- var q2 = opts.answered[a];
39236
- var numeric = q2.unit !== void 0 ? q2.unit !== "text" : isFigure(q2.value);
39237
- out.push("<tr><td>" + esc7(q2.ask) + '</td><td class="' + (numeric ? "num" : "said") + '">' + (q2.unknown ? '<span class="unk">you did not know</span>' : esc7(q2.value)) + "</td></tr>");
39335
+ var q3 = opts.answered[a];
39336
+ var numeric = q3.unit !== void 0 ? q3.unit !== "text" : isFigure(q3.value);
39337
+ out.push("<tr><td>" + esc7(q3.ask) + '</td><td class="' + (numeric ? "num" : "said") + '">' + (q3.unknown ? '<span class="unk">you did not know</span>' : esc7(q3.value)) + "</td></tr>");
39238
39338
  }
39239
39339
  out.push("</table>");
39240
39340
  }
@@ -39379,6 +39479,491 @@ var init_report = __esm({
39379
39479
  }
39380
39480
  });
39381
39481
 
39482
+ // src/command-os/client-calls.ts
39483
+ var client_calls_exports = {};
39484
+ __export(client_calls_exports, {
39485
+ ANALYSIS_MIN_WORDS: () => ANALYSIS_MIN_WORDS,
39486
+ CLIENT_FILES_ROOT: () => CLIENT_FILES_ROOT,
39487
+ CONFIRM_INSTRUCTION: () => CONFIRM_INSTRUCTION,
39488
+ EXISTING_WITH_CONTACT: () => EXISTING_WITH_CONTACT,
39489
+ RECORDERS: () => RECORDERS,
39490
+ SOLUTION_KINDS: () => SOLUTION_KINDS,
39491
+ UNCLEAN_RECORD: () => UNCLEAN_RECORD,
39492
+ buildClientFile: () => buildClientFile,
39493
+ callDate: () => callDate,
39494
+ confirmList: () => confirmList,
39495
+ gateAnalysis: () => gateAnalysis,
39496
+ processClientCall: () => processClientCall,
39497
+ safeName: () => safeName,
39498
+ writeClientFileLocally: () => writeClientFileLocally
39499
+ });
39500
+ function gateAnalysis(transcript, analysis) {
39501
+ const rejected = [];
39502
+ const kept = {
39503
+ saidTheyWant: (analysis.saidTheyWant ?? []).map(clean).filter(Boolean),
39504
+ actuallyNeed: [],
39505
+ unnamedProblems: [],
39506
+ recommendedSolutions: [],
39507
+ nextCallQuestions: (analysis.nextCallQuestions ?? []).map(clean).filter(Boolean)
39508
+ };
39509
+ const check = (section2, item, quote) => {
39510
+ if (!item) {
39511
+ rejected.push({ section: section2, item, quote, why: "the point itself was empty." });
39512
+ return false;
39513
+ }
39514
+ const said = quoteCheck(transcript, quote, { minWords: ANALYSIS_MIN_WORDS });
39515
+ if (said.verdict === "refused") {
39516
+ rejected.push({ section: section2, item, quote, why: said.because });
39517
+ return false;
39518
+ }
39519
+ return true;
39520
+ };
39521
+ for (const section2 of ["actuallyNeed", "unnamedProblems"]) {
39522
+ for (const p of analysis[section2] ?? []) {
39523
+ const point = clean(p?.point), quote = String(p?.quote ?? "").trim();
39524
+ if (check(section2, point, quote)) kept[section2].push({ point, quote });
39525
+ }
39526
+ }
39527
+ for (const s of analysis.recommendedSolutions ?? []) {
39528
+ const solution = clean(s?.solution), quote = String(s?.quote ?? "").trim();
39529
+ if (!SOLUTION_KINDS.includes(s?.kind)) {
39530
+ rejected.push({ section: "recommendedSolutions", item: solution, quote, why: `"${String(s?.kind)}" is not a kind; use ${SOLUTION_KINDS.join(", ")}.` });
39531
+ continue;
39532
+ }
39533
+ if (check("recommendedSolutions", solution, quote)) kept.recommendedSolutions.push({ solution, kind: s.kind, why: clean(s?.why), quote });
39534
+ }
39535
+ return { kept, rejected };
39536
+ }
39537
+ function safeName(raw, max = 80) {
39538
+ let s = String(raw ?? "").normalize("NFC").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/[/\\:*?"<>|]/g, " ").replace(/(^|\s)\.+(?=\s|$)/g, " ").replace(/\s+/g, " ").trim().replace(/^[.\s]+|[.\s]+$/g, "");
39539
+ const points = Array.from(s);
39540
+ if (points.length > max) s = points.slice(0, max).join("").replace(/[.\s]+$/g, "");
39541
+ if (WINDOWS_RESERVED.test(s)) return void 0;
39542
+ return s || void 0;
39543
+ }
39544
+ function callDate(source, now) {
39545
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(source?.recordedAt ?? "").trim());
39546
+ if (m) {
39547
+ const d = /* @__PURE__ */ new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00Z`);
39548
+ if (!isNaN(d.getTime()) && d.toISOString().slice(0, 10) === `${m[1]}-${m[2]}-${m[3]}`) return { date: d.toISOString().slice(0, 10), fromRecording: true };
39549
+ }
39550
+ return { date: now.toISOString().slice(0, 10), fromRecording: false };
39551
+ }
39552
+ function confirmList(held) {
39553
+ return held.map((h) => {
39554
+ const saved = /already has "([^"]*)" saved/.exec(h.because)?.[1];
39555
+ return `${ASK.get(h.questionId) ?? h.questionId} \u2192 ${h.value} (their words: ${q2(h.quote)})${saved !== void 0 ? ` [replaces the saved "${saved}"]` : ""}`;
39556
+ });
39557
+ }
39558
+ function analysisMarkdown(a, level) {
39559
+ return [
39560
+ `${level} What they said they want`,
39561
+ bullet(a.saidTheyWant),
39562
+ "",
39563
+ `${level} What they actually need`,
39564
+ bullet(a.actuallyNeed.map((p) => `${stop(p.point)} They said: ${q2(p.quote)}`)),
39565
+ "",
39566
+ `${level} Problems they did not name`,
39567
+ bullet(a.unnamedProblems.map((p) => `${stop(p.point)} They said: ${q2(p.quote)}`)),
39568
+ "",
39569
+ `${level} Recommended solutions`,
39570
+ bullet(a.recommendedSolutions.map((s) => `**${s.solution}** (${s.kind}).${s.why ? ` ${stop(s.why)}` : ""} They said: ${q2(s.quote)}`)),
39571
+ "",
39572
+ `${level} Questions for the next call`,
39573
+ bullet(a.nextCallQuestions)
39574
+ ].join("\n");
39575
+ }
39576
+ function buildClientFile(args) {
39577
+ const client = safeName(args.clientName) ?? "Unnamed client";
39578
+ const folderParts = [...CLIENT_FILES_ROOT, client];
39579
+ const recorder = args.source?.recorder ?? "Call";
39580
+ const title2 = args.source?.title?.trim() || "call";
39581
+ const stem = safeName(`${args.date} ${recorder} ${title2}`, 120) ?? `${args.date} call`;
39582
+ const name = `${stem}.md`;
39583
+ const transcriptName = `${stem} transcript.md`;
39584
+ const ask = (id) => ASK.get(id) ?? id;
39585
+ const src = args.source ? describeSource({ ...args.source, recordedAt: args.date }) : "Not recorded (no source was given)";
39586
+ const call2 = [
39587
+ `# Call with ${client}, ${args.date}`,
39588
+ "",
39589
+ `Source: ${src}.`,
39590
+ `Assessment: ${args.saveLine}`,
39591
+ "",
39592
+ "## Answers saved or ready to save",
39593
+ bullet(args.gated.accepted.map((a) => `${ask(a.questionId)} **${a.value}**${args.confirmedByOwner?.includes(a.questionId) ? " (confirmed by you)" : ""}. They said: ${q2(a.quote)}`)),
39594
+ "",
39595
+ "## Waiting for your confirmation",
39596
+ ...args.gated.held.length ? [confirmList(args.gated.held).map((l) => `- ${l}`).join("\n"), "", CONFIRM_INSTRUCTION] : ["- (none)"],
39597
+ "",
39598
+ "## Thrown out",
39599
+ bullet(args.gated.rejected.map((r) => `${ask(r.questionId)} ${r.value}: ${r.because}`)),
39600
+ "",
39601
+ ...args.analysis ? [
39602
+ "## Needs analysis (for you, not for the client)",
39603
+ "",
39604
+ analysisMarkdown(args.analysis.kept, "###"),
39605
+ "",
39606
+ ...args.analysis.rejected.length ? [
39607
+ "### Left out: the quote was not in the transcript",
39608
+ bullet(args.analysis.rejected.map((r) => `${r.item || "(empty)"}: ${r.why}`)),
39609
+ ""
39610
+ ] : []
39611
+ ] : [],
39612
+ "## Transcript",
39613
+ "",
39614
+ `Kept in its own file: Calls/${transcriptName}. If that file was not written, the recording in ${args.source?.recorder ?? "the recorder"} is the copy.`,
39615
+ ""
39616
+ ].join("\n");
39617
+ const transcript = [
39618
+ `# Transcript: call with ${client}, ${args.date}`,
39619
+ "",
39620
+ `Source: ${src}.`,
39621
+ "",
39622
+ args.transcript.trim(),
39623
+ ""
39624
+ ].join("\n");
39625
+ const understanding = [
39626
+ "",
39627
+ `## ${args.date}: ${recorder} call${args.source?.title?.trim() ? `, ${q2(args.source.title)}` : ""}`,
39628
+ "",
39629
+ ...args.analysis ? [analysisMarkdown(args.analysis.kept, "###"), ""] : ["No needs analysis was written for this call.", ""],
39630
+ `Assessment: ${args.saveLine}`,
39631
+ `Full call: Calls/${name}`,
39632
+ ""
39633
+ ].join("\n");
39634
+ return {
39635
+ clientName: client,
39636
+ folder: folderParts.join("/"),
39637
+ folderParts,
39638
+ call: { folderParts: [...folderParts, "Calls"], name, mode: "create-new", content: call2 },
39639
+ transcript: { folderParts: [...folderParts, "Calls"], name: transcriptName, mode: "create-new", optional: true, characters: args.transcript.trim().length, content: transcript },
39640
+ understanding: {
39641
+ folderParts,
39642
+ name: "Client understanding.md",
39643
+ mode: "append",
39644
+ headerIfNew: `# Client understanding: ${client}
39645
+
39646
+ What we know about this client, newest call last. Advice for the agency, never sent to the client.
39647
+ `,
39648
+ append: understanding
39649
+ },
39650
+ howToWrite: `Search for each folder in folderParts before creating it, one level at a time, and use the one that exists; create it only when the search finds none, and never make a second copy of the tree. Write call.content as a NEW file named call.name in the Calls folder; if that name is taken, add " (2)" rather than replacing it. Write transcript.content as its own new file, transcript.name, beside it; it is optional: when it is too large to write in one go (see transcript.characters), skip it, and a link to the recording or the recorder's own copy is enough. For understanding: read "Client understanding.md", add understanding.append at the end, write it back, then read it again and check every earlier dated section is still there. If it does not exist yet, create it with headerIfNew followed by the append. Never drop what was there: it is the running record of every call.`
39651
+ };
39652
+ }
39653
+ function inside(base, p) {
39654
+ return p.startsWith(base.endsWith(path32.sep) ? base : base + path32.sep);
39655
+ }
39656
+ function writeClientFileLocally(baseDir, file2) {
39657
+ try {
39658
+ if (!path32.isAbsolute(baseDir)) return { written: false, error: `the client files folder must be a full path, not "${baseDir}".` };
39659
+ const base = path32.resolve(baseDir);
39660
+ let st;
39661
+ try {
39662
+ st = fs34.statSync(base);
39663
+ } catch {
39664
+ return { written: false, error: `the client files folder ${base} does not exist. If it is your Google Drive folder, Google Drive for desktop needs to be installed and signed in.` };
39665
+ }
39666
+ if (!st.isDirectory()) return { written: false, error: `${base} is not a folder.` };
39667
+ const realBase = fs34.realpathSync(base);
39668
+ const callsDir = path32.resolve(base, ...file2.call.folderParts);
39669
+ const clientDir = path32.resolve(base, ...file2.understanding.folderParts);
39670
+ if (!inside(base, callsDir) || !inside(base, clientDir)) return { written: false, error: "the client folder name would land outside the client files folder." };
39671
+ let cur = base;
39672
+ for (const part of file2.call.folderParts) {
39673
+ cur = path32.join(cur, part);
39674
+ let here;
39675
+ try {
39676
+ here = fs34.lstatSync(cur);
39677
+ } catch {
39678
+ }
39679
+ if (here && (here.isSymbolicLink() || !here.isDirectory())) {
39680
+ return { written: false, error: `${cur} is a link or a file, not a folder, so nothing was written.` };
39681
+ }
39682
+ if (!here) fs34.mkdirSync(cur, { mode: 448 });
39683
+ }
39684
+ if (!inside(realBase, fs34.realpathSync(callsDir))) return { written: false, error: "the client folder resolves outside the client files folder, so nothing was written." };
39685
+ const stem = file2.call.name.replace(/\.md$/, "");
39686
+ const tStem = file2.transcript.name.replace(/\.md$/, "");
39687
+ let callFile = "", transcriptFile = "";
39688
+ for (let n = 1; n <= 50 && !callFile; n++) {
39689
+ const suffix = n === 1 ? "" : ` (${n})`;
39690
+ const callPath = path32.join(callsDir, `${stem}${suffix}.md`);
39691
+ const tName = `${tStem.slice(0, tStem.length - " transcript".length)}${suffix} transcript.md`;
39692
+ const tPath = path32.join(callsDir, tName);
39693
+ if (fs34.existsSync(callPath) || fs34.existsSync(tPath)) continue;
39694
+ try {
39695
+ fs34.writeFileSync(tPath, file2.transcript.content, { encoding: "utf8", flag: "wx", mode: 384 });
39696
+ fs34.writeFileSync(callPath, file2.call.content.split(`Calls/${file2.transcript.name}`).join(`Calls/${tName}`), { encoding: "utf8", flag: "wx", mode: 384 });
39697
+ callFile = callPath;
39698
+ transcriptFile = tPath;
39699
+ } catch (e) {
39700
+ if (e.code !== "EEXIST") throw e;
39701
+ }
39702
+ }
39703
+ if (!callFile) return { written: false, error: `fifty calls named "${stem}" already exist; nothing was overwritten.` };
39704
+ fs34.chmodSync(callFile, 384);
39705
+ fs34.chmodSync(transcriptFile, 384);
39706
+ const understandingFile = path32.join(clientDir, file2.understanding.name);
39707
+ let created = false;
39708
+ try {
39709
+ fs34.writeFileSync(understandingFile, file2.understanding.headerIfNew + file2.understanding.append, { encoding: "utf8", flag: "wx", mode: 384 });
39710
+ created = true;
39711
+ } catch (e) {
39712
+ if (e.code !== "EEXIST") throw e;
39713
+ }
39714
+ if (!created) {
39715
+ const st2 = fs34.lstatSync(understandingFile);
39716
+ if (st2.isSymbolicLink() || !st2.isFile() || st2.nlink > 1) return { written: false, error: `${understandingFile} is a link, not a plain file, so it was not appended to. The call file was written: ${callFile}` };
39717
+ fs34.appendFileSync(understandingFile, file2.understanding.append, { encoding: "utf8", mode: 384 });
39718
+ }
39719
+ fs34.chmodSync(understandingFile, 384);
39720
+ return { written: true, callFile, transcriptFile, understandingFile };
39721
+ } catch (e) {
39722
+ return { written: false, error: `could not write the client file: ${e?.message ?? String(e)}` };
39723
+ }
39724
+ }
39725
+ function contactNames(raw) {
39726
+ const c = raw?.contact ?? {};
39727
+ const s = (k) => typeof c[k] === "string" && c[k].trim() ? c[k].trim() : "";
39728
+ const name = s("name") || [s("firstName"), s("lastName")].filter(Boolean).join(" ") || s("contactName");
39729
+ return { name: name || null, business: s("companyName") || null };
39730
+ }
39731
+ async function withContactLock(key, fn) {
39732
+ const before = contactLocks.get(key) ?? Promise.resolve();
39733
+ let release;
39734
+ const mine = before.then(() => new Promise((r) => {
39735
+ release = r;
39736
+ }));
39737
+ contactLocks.set(key, mine);
39738
+ await before;
39739
+ try {
39740
+ return await fn();
39741
+ } finally {
39742
+ release();
39743
+ if (contactLocks.get(key) === mine) contactLocks.delete(key);
39744
+ }
39745
+ }
39746
+ async function readStrict(client, contactId) {
39747
+ const raw = await client.get(`/contacts/${encodeURIComponent(contactId)}/notes`);
39748
+ return pickLatestStrict(raw?.notes ?? []);
39749
+ }
39750
+ async function saveToContact(input, deps, gated, source) {
39751
+ const contactId = input.contactId;
39752
+ let effective = gated;
39753
+ const out = {
39754
+ status: "NOT SAVED",
39755
+ contactId,
39756
+ contactName: null,
39757
+ businessName: null,
39758
+ savedAnswers: 0,
39759
+ confirmedByOwner: [],
39760
+ confirmationsNotUsed: [],
39761
+ confirmedOnContact: [],
39762
+ reason: null,
39763
+ answersOnContact: null,
39764
+ notesWritten: 0,
39765
+ opportunityNote: null
39766
+ };
39767
+ const done = (o) => ({ outcome: { ...out, ...o }, effective });
39768
+ const fail = (reason3) => done({ status: "NOT SAVED", savedAnswers: 0, reason: reason3 });
39769
+ const msg3 = (e) => e?.message ?? String(e);
39770
+ if (input.existing && Object.keys(input.existing).length) return fail(EXISTING_WITH_CONTACT);
39771
+ try {
39772
+ const who = contactNames(await deps.client.get(`/contacts/${encodeURIComponent(contactId)}`));
39773
+ out.contactName = who.name;
39774
+ out.businessName = who.business;
39775
+ } catch (e) {
39776
+ return fail(`could not open contact ${contactId} in this account (${msg3(e)}).`);
39777
+ }
39778
+ let record;
39779
+ try {
39780
+ const got = await readStrict(deps.client, contactId);
39781
+ if (!got.clean) return fail(UNCLEAN_RECORD);
39782
+ record = got.record;
39783
+ } catch (e) {
39784
+ return fail(`could not read the answers already saved on this contact, and saving without them would hide them (${msg3(e)}).`);
39785
+ }
39786
+ if (record) out.businessName = out.businessName ?? record.state.biz;
39787
+ const sameValue = (a, b) => {
39788
+ const na = numericValue(a), nb = numericValue(b);
39789
+ return a.trim() === b.trim() || Boolean(na && nb && na.n === nb.n && na.percent === nb.percent);
39790
+ };
39791
+ const confirmedIds = /* @__PURE__ */ new Set();
39792
+ const heldAfterConfirm = gated.held.filter((h) => {
39793
+ const c = (input.confirmed ?? []).find((x) => String(x?.questionId) === h.questionId);
39794
+ if (c && sameValue(String(c.value), h.value)) {
39795
+ confirmedIds.add(h.questionId);
39796
+ return false;
39797
+ }
39798
+ return true;
39799
+ });
39800
+ for (const c of input.confirmed ?? []) {
39801
+ const id = String(c?.questionId), value = String(c?.value ?? "");
39802
+ if (confirmedIds.has(id)) continue;
39803
+ const held = gated.held.find((h) => h.questionId === id);
39804
+ out.confirmationsNotUsed.push({ questionId: id, value, why: held ? `the held answer is "${held.value}", not "${value}". Propose "${value}" with its quote, then confirm it.` : gated.rejected.some((r) => r.questionId === id) ? "that answer was thrown out: its quote is not in the transcript." : "no answer to this question is waiting for confirmation." });
39805
+ }
39806
+ const candidates = [...gated.accepted, ...gated.held.filter((h) => confirmedIds.has(h.questionId)).map(({ because: _b, ...a }) => a)];
39807
+ const prior = record?.state.ans ?? {};
39808
+ const replace = new Set(input.proposed.filter((p) => p?.replace === true).map((p) => String(p.questionId)));
39809
+ const conflicts = [];
39810
+ const accepted = candidates.filter((a) => {
39811
+ const had = prior[a.questionId];
39812
+ if (had === void 0 || String(had).trim() === "" || String(had) === a.value || replace.has(a.questionId)) return true;
39813
+ conflicts.push({ ...a, because: `the contact already has "${had}" saved for this question and this call says "${a.value}". Confirm before it replaces the saved answer (send it again with replace: true).` });
39814
+ return false;
39815
+ });
39816
+ out.confirmedByOwner = accepted.filter((a) => confirmedIds.has(a.questionId)).map((a) => a.questionId);
39817
+ effective = { ...gated, accepted, held: [...heldAfterConfirm, ...conflicts], patch: Object.fromEntries(accepted.map((a) => [a.questionId, a.value])) };
39818
+ if (accepted.length === 0) {
39819
+ return done({ status: "NOTHING TO SAVE", reason: effective.held.length ? "every answer is waiting for your OK (numbers always do), so nothing new was written yet." : "no new answer passed the quote check, so nothing new was written.", answersOnContact: record?.state.ans ?? null });
39820
+ }
39821
+ const biz = record?.state.biz || clean(input.businessName) || out.businessName || out.contactName;
39822
+ if (!biz) return fail("neither the contact nor a saved assessment has a business name. Give businessName and try again.");
39823
+ out.businessName = out.businessName ?? biz;
39824
+ const skip = { ...record?.state.skip ?? {} };
39825
+ for (const a of accepted) delete skip[a.questionId];
39826
+ const expected = { ...prior, ...effective.patch };
39827
+ const confirmedAll = [.../* @__PURE__ */ new Set([
39828
+ ...(record?.confirmedByOwner ?? []).filter((id) => !(id in effective.patch) || out.confirmedByOwner.includes(id)),
39829
+ ...out.confirmedByOwner
39830
+ ])];
39831
+ const state = { ...record?.state ?? { asm: {} }, biz, ans: expected, skip, asm: record?.state.asm ?? {} };
39832
+ let failure2 = null;
39833
+ try {
39834
+ const saved = await saveAssessment(deps.client, deps.locationId, {
39835
+ assessment: state,
39836
+ contactId,
39837
+ source,
39838
+ createPipeline: input.createPipeline,
39839
+ confirmedByOwner: confirmedAll
39840
+ });
39841
+ if (!saved.saved) failure2 = saved.error ?? "GoHighLevel did not accept the save.";
39842
+ out.notesWritten = saved.notesWritten ?? 0;
39843
+ out.opportunityNote = saved.opportunityNote ?? null;
39844
+ } catch (e) {
39845
+ failure2 = `GoHighLevel reported an error (${msg3(e)}).`;
39846
+ }
39847
+ let back;
39848
+ try {
39849
+ const got = await readStrict(deps.client, contactId);
39850
+ back = got.clean ? got.record : null;
39851
+ } catch (e) {
39852
+ return done({ status: "NOT CONFIRMED", reason: `${failure2 ? failure2 + " " : ""}The contact could not be read back (${msg3(e)}). Treat the answers as not saved until you open the contact.` });
39853
+ }
39854
+ const off = back ? same(back.state.ans, expected) : null;
39855
+ const marks = back?.confirmedByOwner ?? [];
39856
+ if (off && off.length === 0 && marks.length === confirmedAll.length && confirmedAll.every((id) => marks.includes(id))) {
39857
+ return done({ status: "SAVED", savedAnswers: accepted.length, answersOnContact: back.state.ans, confirmedOnContact: marks, reason: failure2 ? `${failure2} The answers did land and read back correctly.` : null });
39858
+ }
39859
+ const unchanged = back ? same(back.state.ans, prior).length === 0 : !record;
39860
+ if (failure2 && unchanged) return done({ status: "NOT SAVED", savedAnswers: 0, reason: failure2, answersOnContact: back?.state.ans ?? null });
39861
+ return done({
39862
+ status: "NOT CONFIRMED",
39863
+ answersOnContact: back?.state.ans ?? null,
39864
+ reason: `${failure2 ? failure2 + " " : ""}Reading the contact back ${back ? `did not match what was saved (${off.length ? off.join(", ") : "the confirmed-by-you marks"}); the contact may have changed at the same time` : "found no complete assessment"}. Treat the answers as not saved until you open the contact.`
39865
+ });
39866
+ }
39867
+ async function processClientCall(input, deps) {
39868
+ let gated = gateExtraction(input.transcript, input.proposed, QUESTION_UNITS);
39869
+ const legacyOf = (gated2) => ({
39870
+ applied: gated2.accepted.length,
39871
+ answers: mergeAccepted(input.existing ?? {}, gated2),
39872
+ confirmBeforeTheseCount: gated2.held.map((h) => ({
39873
+ question: ASK.get(h.questionId) ?? h.questionId,
39874
+ proposedValue: h.value,
39875
+ theyActuallySaid: h.quote,
39876
+ howItWasWorkedOut: h.reasoning ?? null,
39877
+ why: h.because
39878
+ })),
39879
+ thrownOut: gated2.rejected.map((r) => ({ questionId: r.questionId, value: r.value, why: r.because })),
39880
+ note: gated2.rejected.some((r) => /does not appear in the transcript/.test(r.because)) ? "One or more answers cited words that are not in the transcript. Those were discarded, not stored. That check is the only thing standing between a recording and an invented figure in a client's report." : null
39881
+ });
39882
+ const legacy = legacyOf(gated);
39883
+ if (!input.contactId && !input.analysis && !input.source) return legacy;
39884
+ const now = deps.now();
39885
+ const { date, fromRecording } = callDate(input.source, now);
39886
+ const noteSource = input.source ? { recorder: input.source.recorder, recordedAt: date, title: input.source.title?.trim() || void 0, fetchedBy: input.source.fetchedBy } : void 0;
39887
+ const saving = input.contactId ? await withContactLock(`${deps.locationId}:${input.contactId}`, () => saveToContact(input, deps, gated, noteSource)) : null;
39888
+ const save = saving?.outcome ?? null;
39889
+ if (saving) gated = saving.effective;
39890
+ const analysis = input.analysis ? gateAnalysis(input.transcript, input.analysis) : null;
39891
+ const counts = `${plural(gated.held.length, "answer needs", "answers need")} your confirmation, ${plural(gated.rejected.length, "was", "were")} thrown out`;
39892
+ const who = save ? `${save.businessName ?? save.contactName ?? "the contact"}${save.contactName && save.businessName ? ` (${save.contactName})` : ""}, contact ${save.contactId}` : "";
39893
+ let reply;
39894
+ let saveLine;
39895
+ if (!save) {
39896
+ reply = `NOT SAVED: no contact was named, so these answers are only proposed. ${gated.accepted.length} passed the check, ${counts}. Call again with the client's contactId to save them.`;
39897
+ saveLine = "not saved to GoHighLevel (no contact was named).";
39898
+ } else if (save.status === "SAVED") {
39899
+ const byYou = save.confirmedByOwner.length ? ` (${save.confirmedByOwner.length} confirmed by you)` : "";
39900
+ reply = `SAVED ${plural(save.savedAnswers, "answer", "answers")}${byYou} to ${who}. ${counts[0].toUpperCase()}${counts.slice(1)}.${save.reason ? ` Note: ${save.reason}` : ""}`;
39901
+ saveLine = `saved ${plural(save.savedAnswers, "answer", "answers")}${byYou} on the GoHighLevel contact ${save.contactId}.`;
39902
+ } else if (save.status === "NOTHING TO SAVE") {
39903
+ const why = gated.held.length ? "every answer is waiting for your OK (numbers always do, and any that differ from a saved answer)" : "no answer passed the quote check";
39904
+ reply = `NOTHING TO SAVE for ${who}: ${why}. ${counts[0].toUpperCase()}${counts.slice(1)}.`;
39905
+ saveLine = `nothing new saved (${why}).`;
39906
+ } else {
39907
+ reply = `${save.status} for ${who}: ${save.reason} ${save.status === "NOT SAVED" ? "Nothing from this call is on the contact." : ""} Of the proposed answers, ${counts}.`.replace(/\s+/g, " ");
39908
+ saveLine = `${save.status}. ${save.reason}`;
39909
+ }
39910
+ if (save?.opportunityNote) reply += ` ${save.opportunityNote}`;
39911
+ if (save?.confirmationsNotUsed.length) reply += ` ${plural(save.confirmationsNotUsed.length, "confirmation was", "confirmations were")} not used: ${save.confirmationsNotUsed.map((c) => `${c.questionId} (${c.why})`).join("; ")}`;
39912
+ if (analysis?.rejected.length) reply += ` ${plural(analysis.rejected.length, "analysis point was", "analysis points were")} left out because the quote is not in the transcript.`;
39913
+ if (gated.held.length) reply += `
39914
+
39915
+ To confirm:
39916
+ ${confirmList(gated.held).map((l) => `- ${l}`).join("\n")}
39917
+ ${CONFIRM_INSTRUCTION}`;
39918
+ const clientName = save?.businessName ?? save?.contactName ?? clean(input.businessName);
39919
+ 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;
39921
+ const { applied, ...legacyRest } = legacyOf(gated);
39922
+ return {
39923
+ // Never hand back unchecked values beside a failed save (review D-3).
39924
+ ...save ? (({ answers: _unchecked, ...rest }) => rest)(legacyRest) : { applied, ...legacyRest },
39925
+ ...save?.answersOnContact ? { answers: save.answersOnContact } : {},
39926
+ reply,
39927
+ toConfirm: gated.held.map((h) => ({ questionId: h.questionId, question: ASK.get(h.questionId) ?? h.questionId, value: h.value, theirWords: h.quote, replacesSaved: /already has "/.test(h.because) })),
39928
+ saved: save,
39929
+ source: noteSource ? { ...noteSource, dateFromRecording: fromRecording } : null,
39930
+ analysis: analysis ? { ...analysis.kept, leftOutForQuote: analysis.rejected, forTheOwnerOnly: true } : null,
39931
+ clientFile,
39932
+ clientFileNote: clientFile ? null : "No client name was known (no contact, no businessName), so no client file entry was made.",
39933
+ localCopy
39934
+ };
39935
+ }
39936
+ var fs34, path32, RECORDERS, SOLUTION_KINDS, clean, ANALYSIS_MIN_WORDS, CLIENT_FILES_ROOT, WINDOWS_RESERVED, bullet, q2, CONFIRM_INSTRUCTION, stop, EXISTING_WITH_CONTACT, UNCLEAN_RECORD, contactLocks, same, plural;
39937
+ var init_client_calls = __esm({
39938
+ "src/command-os/client-calls.ts"() {
39939
+ "use strict";
39940
+ fs34 = __toESM(require("node:fs"));
39941
+ path32 = __toESM(require("node:path"));
39942
+ init_transcript();
39943
+ init_words();
39944
+ init_store3();
39945
+ init_assessment();
39946
+ RECORDERS = ["Plaud", "Zoom", "Other"];
39947
+ SOLUTION_KINDS = ["GoHighLevel build", "AI tool", "custom build", "process"];
39948
+ clean = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
39949
+ ANALYSIS_MIN_WORDS = 3;
39950
+ CLIENT_FILES_ROOT = ["Command OS", "Clients"];
39951
+ WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
39952
+ bullet = (xs) => xs.length ? xs.map((x) => `- ${x}`).join("\n") : "- (none)";
39953
+ q2 = (s) => `"${s.replace(/\s+/g, " ").trim()}"`;
39954
+ CONFIRM_INSTRUCTION = "Reply 'confirm all', or 'confirm all except \u2026' naming the ones to leave out.";
39955
+ stop = (s) => s && !/[.!?]["')\]]?$/.test(s) ? `${s}.` : s;
39956
+ EXISTING_WITH_CONTACT = "existing cannot be used with a contact: the contact's saved answers are the starting point, and only answers quoted from this call are added. Leave existing out and call again.";
39957
+ UNCLEAN_RECORD = "the newest saved assessment on this contact is incomplete or could not be read, so nothing was changed. It may have been edited by hand, or a save may have stopped partway; saving the assessment again with save_assessment writes a complete one, and then this call can be added.";
39958
+ contactLocks = /* @__PURE__ */ new Map();
39959
+ same = (a, b) => {
39960
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
39961
+ return [...keys].filter((k) => String(a[k] ?? "") !== String(b[k] ?? ""));
39962
+ };
39963
+ plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
39964
+ }
39965
+ });
39966
+
39382
39967
  // src/tools/assessment.ts
39383
39968
  function assessmentValidationMessage(error) {
39384
39969
  if (!(error instanceof import_zod61.z.ZodError)) return void 0;
@@ -39461,19 +40046,34 @@ async function saveAssessment(client, locationId2, args, onOpportunityNoteFailur
39461
40046
  const state = normalise(args.assessment);
39462
40047
  const computed = computedFor(state);
39463
40048
  const stage = stageFor({ verdict: computed.verdict, proposalSent: args.proposalSent, outcome: args.outcome });
39464
- const { payload: contactBody, dedupable } = buildContact(state, stage, {
40049
+ const { payload: contactBody, dedupable: dedupableByDetails } = buildContact(state, stage, {
39465
40050
  name: args.contactName,
39466
40051
  email: args.email,
39467
40052
  phone: args.phone
39468
40053
  });
39469
- const upserted = await client.post("/contacts/upsert", {
39470
- body: { ...contactBody, locationId: locationId2 }
39471
- });
39472
- const contactId = upserted?.contact?.id ?? upserted?.id;
40054
+ let contactId;
40055
+ if (args.contactId) {
40056
+ const found = await client.get(`/contacts/${encodeURIComponent(args.contactId)}`);
40057
+ const existingContact = found?.contact;
40058
+ contactId = existingContact?.id;
40059
+ if (!contactId) {
40060
+ return { saved: false, error: `GoHighLevel has no contact ${args.contactId} in this account, so nothing was written.` };
40061
+ }
40062
+ if (!existingContact?.companyName?.trim() && contactBody.companyName) {
40063
+ await client.put(`/contacts/${encodeURIComponent(contactId)}`, { body: { companyName: contactBody.companyName } });
40064
+ }
40065
+ await client.post(`/contacts/${encodeURIComponent(contactId)}/tags`, { body: { tags: contactBody.tags } });
40066
+ } else {
40067
+ const upserted = await client.post("/contacts/upsert", {
40068
+ body: { ...contactBody, locationId: locationId2 }
40069
+ });
40070
+ contactId = upserted?.contact?.id ?? upserted?.id;
40071
+ }
39473
40072
  if (!contactId) {
39474
40073
  return { saved: false, error: "GoHighLevel did not return a contact id, so nothing was written. Nothing partial was left behind." };
39475
40074
  }
39476
- const bodies = buildNoteBodies(buildNote(state, computed, LOOKUP, today()));
40075
+ const dedupable = dedupableByDetails || Boolean(args.contactId);
40076
+ const bodies = buildNoteBodies(buildNote(state, computed, LOOKUP, today(), args.source, args.confirmedByOwner));
39477
40077
  const noteIds = [];
39478
40078
  for (const body of bodies) {
39479
40079
  const note = await client.post(`/contacts/${contactId}/notes`, { body: { body } });
@@ -39645,33 +40245,56 @@ function registerAssessmentTools(server2, client) {
39645
40245
  safeTool(
39646
40246
  server2,
39647
40247
  "read_assessment_transcript",
39648
- "Turn a recorded conversation into assessment answers, WITHOUT letting anything into the record that the owner did not say. You read the transcript and propose answers; each one must carry the verbatim words it came from. This checks every quote actually appears in the transcript and throws out the ones that do not, and holds back anything you worked out rather than heard so the operator can confirm it. Use it after a coffee-shop meeting, a Zoom call or a Plaud recording.",
40248
+ `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; 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.`,
39649
40249
  {
39650
40250
  transcript: import_zod61.z.string().min(20).describe("The recording, as text. Paste the whole thing."),
39651
40251
  proposed: import_zod61.z.array(import_zod61.z.object({
39652
40252
  questionId: import_zod61.z.string().describe("The assessment question this answers."),
39653
40253
  value: import_zod61.z.string().describe("What to store, in the unit the question wants."),
39654
40254
  quote: import_zod61.z.string().describe("The owner's own words, VERBATIM from the transcript. Do not tidy it up \u2014 if it is not in the transcript the answer is thrown out."),
39655
- reasoning: import_zod61.z.string().optional().describe("How you got from the quote to the value, when they are not the same.")
40255
+ reasoning: import_zod61.z.string().optional().describe("How you got from the quote to the value, when they are not the same."),
40256
+ replace: import_zod61.z.boolean().optional().describe("Only after the owner confirmed it: let this answer replace a different one already saved on the contact.")
39656
40257
  })).describe("One entry per question you could answer. Leave out anything the conversation did not cover."),
39657
- existing: import_zod61.z.record(import_zod61.z.string(), import_zod61.z.union([import_zod61.z.string(), import_zod61.z.number()])).optional().describe("Answers already captured, so this adds to them rather than replacing them.")
40258
+ confirmed: import_zod61.z.array(import_zod61.z.object({
40259
+ questionId: import_zod61.z.string(),
40260
+ value: import_zod61.z.union([import_zod61.z.string(), import_zod61.z.number()]).describe("The exact value the owner said yes to.")
40261
+ })).optional().describe("Held answers the owner has said yes to, that specific value, in THIS conversation. Never add one the owner has not confirmed. Its quote must still be in the transcript, and it does not replace a different saved answer without replace: true."),
40262
+ existing: import_zod61.z.record(import_zod61.z.string(), import_zod61.z.union([import_zod61.z.string(), import_zod61.z.number()])).optional().describe("Answers already captured, so this adds to them rather than replacing them. Not with contactId: there the contact's saved answers are the starting point."),
40263
+ contactId: import_zod61.z.string().optional().describe("The client's GoHighLevel contact. Given, the checked answers are added to the assessment already saved on it and saved in this same call. Find it with search_contacts first."),
40264
+ locationId: import_zod61.z.string().optional().describe("The sub-account the contact is in. Defaults to the active one."),
40265
+ businessName: import_zod61.z.string().optional().describe("The client's business name, used when the contact and its saved assessment carry none."),
40266
+ createPipeline: import_zod61.z.boolean().optional().describe('Create the "Assessments" pipeline if it is missing. Only when the owner has said yes.'),
40267
+ source: import_zod61.z.object({
40268
+ 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
+ recordedAt: import_zod61.z.string().describe("When the call happened, as an ISO date (2026-09-24) or timestamp, from the recording itself."),
40270
+ title: import_zod61.z.string().optional().describe("The recording's title, as the recorder shows it."),
40271
+ 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.")
40272
+ }).optional().describe("Where the transcript came from. Written onto the saved note and into the client file."),
40273
+ analysis: import_zod61.z.object({
40274
+ saidTheyWant: import_zod61.z.array(import_zod61.z.string()).optional().describe("What the client asked for, in plain words."),
40275
+ actuallyNeed: import_zod61.z.array(import_zod61.z.object({ point: import_zod61.z.string(), quote: import_zod61.z.string().describe("VERBATIM from the transcript.") })).optional().describe("What they really need, each with the words that show it."),
40276
+ unnamedProblems: import_zod61.z.array(import_zod61.z.object({ point: import_zod61.z.string(), quote: import_zod61.z.string().describe("VERBATIM from the transcript.") })).optional().describe("Problems they described without naming, each with the words that show it."),
40277
+ recommendedSolutions: import_zod61.z.array(import_zod61.z.object({
40278
+ solution: import_zod61.z.string(),
40279
+ kind: import_zod61.z.enum(["GoHighLevel build", "AI tool", "custom build", "process"]),
40280
+ why: import_zod61.z.string().describe("Why it fits this client."),
40281
+ quote: import_zod61.z.string().describe("VERBATIM: what they said that this answers.")
40282
+ })).optional().describe("What you would build or change, including things they did not ask for."),
40283
+ nextCallQuestions: import_zod61.z.array(import_zod61.z.string()).optional().describe("What to ask on the next call.")
40284
+ }).optional().describe("Your needs analysis of the call. For the owner only; it is never sent to the client. Every quote is checked against the transcript like the answers are.")
39658
40285
  },
39659
- async ({ transcript, proposed, existing }) => {
39660
- const gated = gateExtraction(transcript, proposed, QUESTION_UNITS);
39661
- const merged = mergeAccepted(existing ?? {}, gated);
39662
- return {
39663
- applied: gated.accepted.length,
39664
- answers: merged,
39665
- confirmBeforeTheseCount: gated.held.map((h) => ({
39666
- question: ASK.get(h.questionId) ?? h.questionId,
39667
- proposedValue: h.value,
39668
- theyActuallySaid: h.quote,
39669
- howItWasWorkedOut: h.reasoning ?? null,
39670
- why: h.because
39671
- })),
39672
- thrownOut: gated.rejected.map((r) => ({ questionId: r.questionId, value: r.value, why: r.because })),
39673
- note: gated.rejected.some((r) => /does not appear in the transcript/.test(r.because)) ? "One or more answers cited words that are not in the transcript. Those were discarded, not stored. That check is the only thing standing between a recording and an invented figure in a client's report." : null
39674
- };
40286
+ async (args) => {
40287
+ const [{ processClientCall: processClientCall2 }, { readAgencyProfile: readAgencyProfile2 }] = await Promise.all([
40288
+ Promise.resolve().then(() => (init_client_calls(), client_calls_exports)),
40289
+ Promise.resolve().then(() => (init_agency_profile(), agency_profile_exports))
40290
+ ]);
40291
+ const wantsFiles = Boolean(args.contactId || args.analysis || args.source);
40292
+ return processClientCall2(args, {
40293
+ client,
40294
+ locationId: args.contactId ? client.resolveLocationId(args.locationId) : "",
40295
+ now: () => /* @__PURE__ */ new Date(),
40296
+ clientFilesDir: wantsFiles ? readAgencyProfile2().clientFilesDir : void 0
40297
+ });
39675
40298
  }
39676
40299
  );
39677
40300
  safeTool(
@@ -39695,10 +40318,10 @@ function registerAssessmentTools(server2, client) {
39695
40318
  const r = compute(state.ans, state.skip, state.asm);
39696
40319
  const labels = {};
39697
40320
  for (const a of ASSUMPTIONS) labels[a[0]] = a[3];
39698
- const answered = Q.filter((q2) => {
39699
- const id = q2[0];
40321
+ const answered = Q.filter((q3) => {
40322
+ const id = q3[0];
39700
40323
  return state.skip[id] === true || state.ans[id] !== void 0 && String(state.ans[id]).trim() !== "";
39701
- }).map((q2) => state.skip[q2[0]] ? { ask: q2[2], unknown: true, unit: q2[4] } : { ask: q2[2], value: String(state.ans[q2[0]]), unit: q2[4] });
40324
+ }).map((q3) => state.skip[q3[0]] ? { ask: q3[2], unknown: true, unit: q3[4] } : { ask: q3[2], value: String(state.ans[q3[0]]), unit: q3[4] });
39702
40325
  const html2 = renderReport2({
39703
40326
  state,
39704
40327
  computed: r,
@@ -39759,16 +40382,15 @@ var init_assessment = __esm({
39759
40382
  init_tool_helpers();
39760
40383
  init_store3();
39761
40384
  init_engine();
39762
- init_transcript();
39763
40385
  init_report();
39764
40386
  import_node_crypto11 = require("node:crypto");
39765
40387
  import_node_fs6 = require("node:fs");
39766
40388
  import_node_os = require("node:os");
39767
40389
  import_node_path6 = require("node:path");
39768
- QUESTION_UNITS = new Map(Q.map((q2) => [q2[0], q2[4]]));
40390
+ QUESTION_UNITS = new Map(Q.map((q3) => [q3[0], q3[4]]));
39769
40391
  AREA_TITLE = new Map(AREAS.map((a) => [a[0], a[1]]));
39770
- ASK = new Map(Q.map((q2) => [q2[0], q2[2]]));
39771
- AREA_OF = new Map(Q.map((q2) => [q2[0], q2[1]]));
40392
+ ASK = new Map(Q.map((q3) => [q3[0], q3[2]]));
40393
+ AREA_OF = new Map(Q.map((q3) => [q3[0], q3[1]]));
39772
40394
  ASSUMPTION_LABEL = new Map(ASSUMPTIONS.map((a) => [a[0], a[1]]));
39773
40395
  LOOKUP = {
39774
40396
  ask: (id) => ASK.get(id),
@@ -39876,6 +40498,7 @@ __export(dashboard_exports, {
39876
40498
  INTAKE_OWN_MESSAGES: () => INTAKE_OWN_MESSAGES,
39877
40499
  INTAKE_UNREADABLE: () => INTAKE_UNREADABLE,
39878
40500
  NOT_SIGNED_IN_MSG: () => NOT_SIGNED_IN_MSG,
40501
+ PARKED_NOT_BUILT: () => PARKED_NOT_BUILT,
39879
40502
  SEAT_GUARDED_READS: () => SEAT_GUARDED_READS,
39880
40503
  SEAT_TOKEN_HEADER: () => SEAT_TOKEN_HEADER,
39881
40504
  STAGES: () => STAGES2,
@@ -39947,11 +40570,11 @@ function recoverStuckStages(state) {
39947
40570
  return { state: { ...state, clients }, recovered };
39948
40571
  }
39949
40572
  function cockpitStatePath() {
39950
- return path32.join(appDataDir(), "cockpit-state.json");
40573
+ return path33.join(appDataDir(), "cockpit-state.json");
39951
40574
  }
39952
40575
  function readCockpitState() {
39953
40576
  try {
39954
- const raw = JSON.parse(fs34.readFileSync(cockpitStatePath(), "utf8"));
40577
+ const raw = JSON.parse(fs35.readFileSync(cockpitStatePath(), "utf8"));
39955
40578
  if (raw && raw.v === 1 && raw.clients && typeof raw.clients === "object") return raw;
39956
40579
  } catch {
39957
40580
  }
@@ -39959,7 +40582,7 @@ function readCockpitState() {
39959
40582
  }
39960
40583
  function writeCockpitState(state) {
39961
40584
  ensureAppDataDir();
39962
- fs34.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
40585
+ fs35.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
39963
40586
  }
39964
40587
  function setStage(state, locationId2, stageIndex, status) {
39965
40588
  if (!Number.isInteger(stageIndex) || stageIndex < 0 || stageIndex >= STAGES2.length) throw new Error("bad stage index");
@@ -40103,11 +40726,11 @@ function startRun(locationId2, stage, onDone, meta = {}) {
40103
40726
  industry,
40104
40727
  manualObligations: stage === 4 ? readBuildObligations(locationId2) : void 0,
40105
40728
  intakeSummary: describeComposition({ industry, agency }),
40106
- customLabels: composed.filter((q2) => {
40107
- const base = INTAKE_QUESTIONS.find((b) => b.key === q2.key);
40108
- return base && base.label !== q2.label;
40109
- }).map((q2) => ({ key: q2.key, label: q2.label, from: INTAKE_QUESTIONS.find((b) => b.key === q2.key)?.label })),
40110
- removedKeys: INTAKE_QUESTIONS.filter((b) => !composed.some((q2) => q2.key === b.key)).map((b) => b.key),
40729
+ customLabels: composed.filter((q3) => {
40730
+ const base = INTAKE_QUESTIONS.find((b) => b.key === q3.key);
40731
+ return base && base.label !== q3.label;
40732
+ }).map((q3) => ({ key: q3.key, label: q3.label, from: INTAKE_QUESTIONS.find((b) => b.key === q3.key)?.label })),
40733
+ removedKeys: INTAKE_QUESTIONS.filter((b) => !composed.some((q3) => q3.key === b.key)).map((b) => b.key),
40111
40734
  // A build re-run reuses the approved plan already saved for this account (finding 10).
40112
40735
  // ...and only while it still builds the scope the member has confirmed now: a
40113
40736
  // changed selection composes the plan fresh from the preset (delta round 3, T-M4).
@@ -40290,7 +40913,7 @@ function notify(message) {
40290
40913
  `);
40291
40914
  try {
40292
40915
  ensureAppDataDir();
40293
- fs34.appendFileSync(path32.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
40916
+ fs35.appendFileSync(path33.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
40294
40917
  `);
40295
40918
  } catch {
40296
40919
  }
@@ -40360,20 +40983,20 @@ function withCockpitShell(page, screen, extraHead = "") {
40360
40983
  function clientDocsDir(locationId2, base = appDataDir()) {
40361
40984
  const safeLoc = String(locationId2).replace(/[^A-Za-z0-9_-]/g, "");
40362
40985
  if (!safeLoc) throw new Error("no client");
40363
- return path32.join(base, "clients", safeLoc, "documents");
40986
+ return path33.join(base, "clients", safeLoc, "documents");
40364
40987
  }
40365
40988
  function safeDocName(name) {
40366
- const cleaned = path32.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, " ").trim();
40367
- const ext = path32.extname(cleaned).toLowerCase();
40989
+ const cleaned = path33.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, " ").trim();
40990
+ const ext = path33.extname(cleaned).toLowerCase();
40368
40991
  if (!cleaned || cleaned.startsWith(".") || !DOC_EXTS.has(ext)) throw new Error("Only plain text (.txt, .md, .csv), .vtt and .srt files can be saved here.");
40369
40992
  const stem = cleaned.slice(0, cleaned.length - ext.length).slice(0, 120 - ext.length).trim();
40370
40993
  return `${stem || "document"}${ext}`;
40371
40994
  }
40372
40995
  function listClientDocs(locationId2, base = appDataDir()) {
40373
40996
  const dir = clientDocsDir(locationId2, base);
40374
- if (!fs34.existsSync(dir)) return [];
40375
- return fs34.readdirSync(dir).filter((f) => DOC_EXTS.has(path32.extname(f).toLowerCase())).map((f) => {
40376
- const st = fs34.statSync(path32.join(dir, f));
40997
+ if (!fs35.existsSync(dir)) return [];
40998
+ return fs35.readdirSync(dir).filter((f) => DOC_EXTS.has(path33.extname(f).toLowerCase())).map((f) => {
40999
+ const st = fs35.statSync(path33.join(dir, f));
40377
41000
  return { name: f, bytes: st.size, savedAt: st.mtime.toISOString() };
40378
41001
  }).sort((a, b) => b.savedAt.localeCompare(a.savedAt));
40379
41002
  }
@@ -40391,7 +41014,7 @@ function readFailureText(e) {
40391
41014
  function logQuietly(message) {
40392
41015
  try {
40393
41016
  ensureAppDataDir();
40394
- fs34.appendFileSync(path32.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
41017
+ fs35.appendFileSync(path33.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
40395
41018
  `);
40396
41019
  } catch {
40397
41020
  }
@@ -40464,40 +41087,40 @@ function newClientFailure(error, surface = "command-os", account2) {
40464
41087
  }
40465
41088
  function insideDir(dir, p) {
40466
41089
  try {
40467
- const real = fs34.realpathSync(p);
40468
- const base = fs34.realpathSync(dir);
40469
- return real === base || real.startsWith(base + path32.sep);
41090
+ const real = fs35.realpathSync(p);
41091
+ const base = fs35.realpathSync(dir);
41092
+ return real === base || real.startsWith(base + path33.sep);
40470
41093
  } catch {
40471
41094
  return false;
40472
41095
  }
40473
41096
  }
40474
41097
  function uniqueDocName(dir, name) {
40475
- const ext = path32.extname(name), stem = name.slice(0, name.length - ext.length);
41098
+ const ext = path33.extname(name), stem = name.slice(0, name.length - ext.length);
40476
41099
  let candidate = name;
40477
- for (let n = 2; fs34.existsSync(path32.join(dir, candidate)); n++) candidate = `${stem} (${n})${ext}`;
41100
+ for (let n = 2; fs35.existsSync(path33.join(dir, candidate)); n++) candidate = `${stem} (${n})${ext}`;
40478
41101
  return candidate;
40479
41102
  }
40480
41103
  function writeDocAtomically(dir, name, bytes) {
40481
- fs34.mkdirSync(dir, { recursive: true, mode: 448 });
41104
+ fs35.mkdirSync(dir, { recursive: true, mode: 448 });
40482
41105
  const finalName = uniqueDocName(dir, name);
40483
- const tmp = path32.join(dir, `.${finalName}.${process.pid}.tmp`);
40484
- const fd = fs34.openSync(tmp, fs34.constants.O_WRONLY | fs34.constants.O_CREAT | fs34.constants.O_EXCL | (fs34.constants.O_NOFOLLOW ?? 0), 384);
41106
+ const tmp = path33.join(dir, `.${finalName}.${process.pid}.tmp`);
41107
+ const fd = fs35.openSync(tmp, fs35.constants.O_WRONLY | fs35.constants.O_CREAT | fs35.constants.O_EXCL | (fs35.constants.O_NOFOLLOW ?? 0), 384);
40485
41108
  try {
40486
- fs34.writeSync(fd, bytes);
40487
- fs34.fsyncSync(fd);
41109
+ fs35.writeSync(fd, bytes);
41110
+ fs35.fsyncSync(fd);
40488
41111
  } finally {
40489
- fs34.closeSync(fd);
41112
+ fs35.closeSync(fd);
40490
41113
  }
40491
- const dest = path32.join(dir, finalName);
41114
+ const dest = path33.join(dir, finalName);
40492
41115
  try {
40493
- if (fs34.lstatSync(dest).isSymbolicLink()) {
40494
- fs34.unlinkSync(tmp);
41116
+ if (fs35.lstatSync(dest).isSymbolicLink()) {
41117
+ fs35.unlinkSync(tmp);
40495
41118
  throw new Error("that name is taken by a link");
40496
41119
  }
40497
41120
  } catch (e) {
40498
41121
  if (e.code !== "ENOENT") throw e;
40499
41122
  }
40500
- fs34.renameSync(tmp, dest);
41123
+ fs35.renameSync(tmp, dest);
40501
41124
  return finalName;
40502
41125
  }
40503
41126
  function readableAccount(locationId2, roster = currentRosterRef(), state = readCockpitState()) {
@@ -40579,17 +41202,46 @@ function html(roster, seatToken, screen = "board", cap) {
40579
41202
  .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}
40580
41203
 
40581
41204
  /* Stage tiles . same shapes as the hub's 7-stage track, clickable */
40582
- .stages{display:flex;gap:.4rem;flex-wrap:wrap}
40583
- .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)}
40584
- .stage button{box-sizing:border-box;max-width:100%;white-space:normal;overflow-wrap:anywhere;height:auto}
40585
- .stage b{display:block;font-weight:600;color:var(--heading);font-size:var(--fs-label)}
40586
- .stage small{display:block;color:var(--ink-3);font-size:var(--fs-sm);margin-top:.15rem}
40587
- .stage.in_progress{border-color:var(--gold);background:var(--gold-soft)}
40588
- .stage.in_progress small{color:var(--gold-ink)}
40589
- .stage.done{border-color:var(--ok);background:var(--ok-soft)}
40590
- .stage.done small,.stage.done b{color:var(--ok)}
40591
- .stage.notpassed{border-color:var(--warn);background:var(--warn-soft)}
40592
- .stage.notpassed small{color:var(--warn)}
41205
+ /* The steps (owner-approved layout, 2026-09-25): progress bar, next-step panel, one row per step. */
41206
+ .sprog{margin:1rem 0 .9rem}
41207
+ .strack{display:grid;grid-template-columns:repeat(7,1fr);gap:.35rem}
41208
+ .sseg{height:.45rem;border-radius:999px;background:var(--wait-soft)}
41209
+ .sseg.done{background:var(--ok)}.sseg.now{background:var(--gold)}.sseg.stale{background:var(--warn)}
41210
+ .splabel{display:flex;justify-content:space-between;flex-wrap:wrap;gap:.3rem 1rem;margin-top:.55rem;font-size:var(--fs-sm)}
41211
+ .splabel b{color:var(--heading);font-weight:600}.splabel span{color:var(--ink-3)}
41212
+ .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}
41213
+ .snext .sk{font-size:var(--fs-label);letter-spacing:.14em;text-transform:uppercase;color:var(--gold-ink);font-weight:700;margin:0 0 .2rem}
41214
+ .snext h3{margin:0 0 .35rem;font-size:var(--fs-lg);color:var(--heading)}
41215
+ .snext p{margin:0;color:var(--ink-2);font-size:var(--fs-sm);max-width:60ch}
41216
+ .snext .swarn{margin-top:.5rem;color:var(--warn);font-weight:500}
41217
+ .snext .snoteline{margin-top:.4rem}
41218
+ .snext .slive{margin-top:.5rem}
41219
+ .sbtns{display:flex;flex-direction:column;align-items:flex-end;gap:.5rem}
41220
+ .sbtns .sbig{padding:.6rem 1.1rem;font-size:var(--fs-body)}
41221
+ .stage button,.snext button{box-sizing:border-box;max-width:100%;white-space:normal;overflow-wrap:anywhere;height:auto}
41222
+ .sbtns .runbtn.sbig{color:var(--on-accent)}
41223
+ .sbtns .swhy{margin:0;font-size:var(--fs-sm);max-width:22rem;text-align:right}
41224
+ .stages{margin-top:1rem;border-top:1px solid var(--line)}
41225
+ .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)}
41226
+ .stage.here{background:var(--gold-soft)}
41227
+ .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)}
41228
+ .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)}
41229
+ .sname{font-weight:600;color:var(--heading)}
41230
+ .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}
41231
+ .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)}
41232
+ .swhat{color:var(--ink-2);font-size:var(--fs-sm);min-width:0;overflow-wrap:anywhere}
41233
+ .swhat .sreason{color:var(--warn)}
41234
+ .snote{display:block;font-size:var(--fs-sm);line-height:1.35}
41235
+ .slive{font-size:var(--fs-sm);line-height:1.35;margin-top:.25rem}
41236
+ .sact{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:.2rem .8rem}
41237
+ .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}
41238
+ .sactbtn:hover{text-decoration:underline}
41239
+ .sactbtn:disabled{color:var(--ink-3);cursor:default;text-decoration:none}
41240
+ @media (max-width: 52rem){
41241
+ .stage{grid-template-columns:2rem 1fr auto;grid-template-areas:"n name act" ". chip chip" ". what what"}
41242
+ .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}
41243
+ .snext{grid-template-columns:1fr}.sbtns{align-items:flex-start}.sbtns .swhy{text-align:left}
41244
+ }
40593
41245
 
40594
41246
  /* \u2500\u2500 Other accounts: one-line rows. One account at a time is the rule. \u2500\u2500 */
40595
41247
  .acct-list{padding:.2rem .9rem}
@@ -40699,8 +41351,6 @@ function html(roster, seatToken, screen = "board", cap) {
40699
41351
 
40700
41352
  @media (max-width: 46rem){
40701
41353
  .brand-sub,.localpill{display:none}
40702
- .stages{gap:.3rem}
40703
- .stage{min-width:4.2rem}
40704
41354
  }
40705
41355
  @media (prefers-reduced-motion: reduce){*{transition:none !important}}
40706
41356
  ` })}<body data-screen="${screen}"${cap ? ` data-capability="${cap.key}"` : ""}>
@@ -40735,6 +41385,14 @@ const GATES=${scriptJson(HUMAN_GATE_STAGES)};let PROTECTED=${scriptJson(PROTECTE
40735
41385
  const TYPES=["client","sandbox","template","hidden"];
40736
41386
  const TYPE_LABELS={client:"Client",sandbox:"Practice sandbox",template:"Template",hidden:"Hidden"};
40737
41387
  const LABELS=["Intake","Brief","Approve","Build","Verify","Manual tasks","Go-live"];
41388
+ // What each step does: [the one line on its row, the fuller sentence in the next-step panel].
41389
+ 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."],
41390
+ ["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."],
41391
+ ["You approve the proposed build.","Review the intake and the brief, then approve the proposed build for this client."],
41392
+ ["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."],
41393
+ ["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."],
41394
+ ["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."],
41395
+ ["You sign this client live.","Sign this client live once Verify is clean and the manual tasks are finished."]];
40738
41396
  const STATUS_TEXT={pending:"not started",in_progress:"in progress",done:"done"};
40739
41397
  const NEXT={pending:"in_progress",in_progress:"done",done:"pending"};
40740
41398
  let polling=false,query="",showParked=false,showNew=false,snapshots=null,snapshotError="";
@@ -41252,6 +41910,9 @@ function preflightCard(c){const box=document.createElement("section");box.classN
41252
41910
  const a=document.createElement("a");a.href="/agency#connections";a.textContent="Open Your agency";box.appendChild(a);return box}
41253
41911
  function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES.map(()=>"pending")}).stages;
41254
41912
  const sandbox=typeOf(state,c.locationId)==="sandbox";
41913
+ // A template or hidden account opens only so its owner can lock or unlock it and change its
41914
+ // type: nothing on its card builds into it (review L-1; a template can be the live snapshot source).
41915
+ const parkedType=["template","hidden"].includes(typeOf(state,c.locationId));
41255
41916
  const div=document.createElement("div");div.className="client";
41256
41917
  const head=document.createElement("div");head.className="client-head";
41257
41918
  const av=document.createElement("span");av.className="avatar lg";av.textContent=initials(c.name);head.appendChild(av);
@@ -41269,14 +41930,29 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
41269
41930
  isel.onchange=async()=>{await postJSON("/api/industry",{locationId:c.locationId,industry:isel.value});load()};
41270
41931
  sels.appendChild(isel);head.appendChild(sels);div.appendChild(head);
41271
41932
  const costs=(state.costRecords||[]).filter(r=>r.locationId===c.locationId);if(costs.length){const details=document.createElement("details");const title=document.createElement("summary");title.textContent="Claude usage for this client";details.appendChild(title);for(const cost of costs){const p=document.createElement("p");p.textContent=costLabel(cost.kind)+": "+(typeof cost.costUsd==="number"?"$"+cost.costUsd.toFixed(4):"cost not reported")+". "+new Date(cost.at).toLocaleString();details.appendChild(p)}div.appendChild(details)}
41272
- if(!st.some(s=>s==="done"))div.appendChild(preflightCard(c));
41933
+ if(parkedType){const pn=document.createElement("p");pn.className="muted";pn.textContent="This is a "+typeOf(state,c.locationId)+" account, so nothing is built into it from here. You can lock or unlock it below, or change its type to Client to build it.";div.appendChild(pn)}
41934
+ else if(!st.some(s=>s==="done"))div.appendChild(preflightCard(c));
41273
41935
  const approveBuilding=async()=>{const typed=prompt('Approve the Blueprint Review for "'+c.name+'" and let Command OS build in this account.'+String.fromCharCode(10,10)+'Type the client name exactly to confirm:');if(typed===null)return;
41274
41936
  try{const response=await fetch("/api/build-approval",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,confirmName:typed})});const r=await response.json();
41275
41937
  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."}
41276
41938
  if(!r.ok)alert(r.error);else load()
41277
41939
  }catch(e){alert("Command OS could not confirm the result. Refresh this client to read its saved state before retrying.")}};
41278
- const row=document.createElement("div");row.className="stages";
41279
- 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"));
41940
+ // The steps, laid out as the owner approved on 2026-09-25: a progress bar, the next step in
41941
+ // full, then one row per step. Only the layout changed. Every button below keeps the condition
41942
+ // and the handler it had in the seven-column layout; the next-step panel draws the SAME
41943
+ // buttons again for the current step, so nothing appears there that its row would not offer.
41944
+ const staleOf=i=>state.buildViews?.[c.locationId]?.stale?.find(x=>x.stage===i);
41945
+ const isDone=i=>(st[i]||"pending")==="done"&&!staleOf(i);
41946
+ let curI=-1;for(let i=0;i<STAGES.length;i++){if(!isDone(i)){curI=i;break}}
41947
+ const nDone=STAGES.filter((_,i)=>isDone(i)).length;
41948
+ const prog=document.createElement("div");prog.className="sprog";prog.setAttribute("aria-label","Progress: "+nDone+" of "+STAGES.length+" steps done");
41949
+ const track=document.createElement("div");track.className="strack";prog.appendChild(track);
41950
+ const plabel=document.createElement("div");plabel.className="splabel";prog.appendChild(plabel);
41951
+ div.appendChild(prog);
41952
+ const nextBox=document.createElement("div");nextBox.className="snext";div.appendChild(nextBox);
41953
+ const row=document.createElement("div");row.className="stages";row.setAttribute("role","list");
41954
+ let curLive=false;
41955
+ 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");
41280
41956
  const gate=GATES[String(i)];
41281
41957
  const oc0=(state.outcomes||{})[c.locationId+":"+i];const notPassed=(st[i]||"pending")==="pending"&&oc0&&(oc0.issues||[]).length>0;
41282
41958
  if(notPassed)el.className+=" notpassed";
@@ -41286,8 +41962,16 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
41286
41962
  // from before the run (a "stale" warning and an old issue count are both wrong then).
41287
41963
  const live=Boolean(run&&run.running&&run.locationId===c.locationId&&run.stage===i);
41288
41964
  const anyBusy=Boolean(run&&run.running&&run.locationId===c.locationId);
41289
- 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>";
41290
- if(live){const box=document.createElement("div");box.style.cssText="margin-top:.35rem;font-size:var(--fs-sm);line-height:1.35";
41965
+ if(i===curI&&live)curLive=true;
41966
+ const tone=i===curI?"now":isDone(i)?"done":(stale||notPassed)?"stale":"";
41967
+ const seg=document.createElement("span");seg.className="sseg "+tone;track.appendChild(seg);
41968
+ const num=document.createElement("span");num.className="snum "+tone;num.textContent=String(i+1);el.appendChild(num);
41969
+ const nm=document.createElement("b");nm.className="sname";nm.textContent=LABELS[i];el.appendChild(nm);
41970
+ 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"];
41971
+ const chip=document.createElement("span");chip.className="schip "+chipOf[1];chip.textContent=chipOf[0];el.appendChild(chip);
41972
+ const what=document.createElement("div");what.className="swhat";el.appendChild(what);
41973
+ const act=document.createElement("div");act.className="sact";el.appendChild(act);
41974
+ const mkLive=()=>{const box=document.createElement("div");box.className="slive";
41291
41975
  const clock=document.createElement("div");clock.style.cssText="font-variant-numeric:tabular-nums;color:var(--ok)";
41292
41976
  const began=run.startedAt?new Date(run.startedAt).getTime():0;
41293
41977
  const tick=()=>{if(!began)return;const sec=Math.max(0,Math.round((Date.now()-began)/1000));
@@ -41300,29 +41984,60 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
41300
41984
  line.textContent=(doneAll?"\u2713 ":g.state==="in_progress"?"\u203A ":" ")+g.label+" "+g.done+"/"+g.total;box.appendChild(line)}}
41301
41985
  if(pr&&pr.current){const now=document.createElement("div");now.style.cssText="margin-top:.25rem;color:var(--muted)";
41302
41986
  now.textContent=String(pr.current).replace(/^"|"$/g,"").slice(0,110);box.appendChild(now)}
41303
- el.appendChild(box)}
41987
+ return box};
41304
41988
  // One short line here; the button's confirmation carries the full wording. At 135
41305
41989
  // characters this sentence made every stage box 677 px tall (auditor, 2026-09-07).
41306
- 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)}
41990
+ let about=document.createElement("span");about.textContent=STEP_ABOUT[i][0];
41991
+ if(i===4){const disclosure=document.createElement("span");disclosure.textContent="Checks links and the form. May submit and delete one test lead.";about=disclosure}
41307
41992
  if(gate)el.title=gate;
41308
- if(stale&&!live){const reason=document.createElement("small");reason.textContent=stale.reason;el.appendChild(reason)}
41309
41993
  const oc=(state.outcomes||{})[c.locationId+":"+i];
41310
- if(!live&&oc&&(oc.summary||(oc.issues||[]).length)){const note=document.createElement("small");
41311
- const nIss=(oc.issues||[]).length;
41312
- 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");
41994
+ const nIss=oc?(oc.issues||[]).length:0;
41995
+ const toggleIssues=(ev)=>{ev.stopPropagation();oOpen[c.locationId+":"+i]=!oOpen[c.locationId+":"+i];render(lastState,lastRun)};
41996
+ const mkNote=()=>{let note=null;
41997
+ if(!live&&oc&&(oc.summary||(oc.issues||[]).length)){note=document.createElement("small");note.className="snote";
41998
+ note.style.cssText="color:"+(nIss?"var(--warn)":"var(--ok)")+";text-decoration:"+(nIss?"underline":"none")+";cursor:"+(nIss?"pointer":"default");
41313
41999
  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);
41314
- if(nIss)note.onclick=(ev)=>{ev.stopPropagation();oOpen[c.locationId+":"+i]=!oOpen[c.locationId+":"+i];render(lastState,lastRun)};
41315
- el.appendChild(note)}
42000
+ if(nIss)note.onclick=toggleIssues}
42001
+ return note};
42002
+ // What the row says, in one place: the live run, else why it went stale, else what the last
42003
+ // run found, else what the step does.
42004
+ if(live){const rn=document.createElement("span");rn.textContent="Running now.";what.appendChild(rn);what.appendChild(mkLive())}
42005
+ if(stale&&!live){const reason=document.createElement("span");reason.className="sreason";reason.textContent=stale.reason;what.appendChild(reason)}
42006
+ else if(!live){const note=mkNote();what.appendChild(note||about)}
41316
42007
  if(POWERED.includes(i)&&(st[i]||"pending")!=="done"){el.style.cursor="default";
41317
42008
  el.title="Command OS runs and checks this step. Its result comes from a run, never from a click."}
41318
- if(gate&&!anyBusy){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)}
41319
- const canBuild=i<=1||sandbox||Boolean((state.buildApproved||{})[c.locationId]);
41320
- if(canBuild&&POWERED.includes(i)){const b=document.createElement("button");b.className="runbtn";
41321
- const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":"Run this step";
42009
+ const gateLabel=({2:"Approve this plan",5:"Confirm these tasks are finished",6:"Sign this client live"})[i];
42010
+ 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};
42011
+ const canBuild=!parkedType&&(i<=1||sandbox||Boolean((state.buildApproved||{})[c.locationId]));
42012
+ 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()};
42013
+ const mkRun=(cls,label)=>{const b=document.createElement("button");b.className=cls;
42014
+ const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":label;
41322
42015
  b.disabled=Boolean(busy)||(!st.some(s=>s==="done")&&!preflights[c.locationId]?.ok);
41323
- 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()};
41324
- el.appendChild(document.createElement("br"));el.appendChild(b)}
42016
+ b.onclick=runIt;return b};
42017
+ 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)}
42018
+ if(gate&&!anyBusy&&!parkedType)act.appendChild(mkGate("sactbtn"));
42019
+ if(canBuild&&POWERED.includes(i))act.appendChild(mkRun("sactbtn srun",i===curI?"Run "+LABELS[i]:(st[i]||"pending")==="done"?"Run again":"Run this step"));
42020
+ if(i===curI){const main=document.createElement("div");
42021
+ const k=document.createElement("p");k.className="sk";k.textContent=parkedType?"Building is off":"Next step";main.appendChild(k);
42022
+ const h3=document.createElement("h3");h3.textContent=parkedType?"Nothing is built into this account":(i+1)+". "+LABELS[i];main.appendChild(h3);
42023
+ 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);
42024
+ if(live)main.appendChild(mkLive());
42025
+ if(stale&&!live){const w=document.createElement("p");w.className="swarn";w.textContent=stale.reason;main.appendChild(w)}
42026
+ const pn=mkNote();if(pn){const w=document.createElement("p");w.className="snoteline";w.appendChild(pn);main.appendChild(w)}
42027
+ const btns=document.createElement("div");btns.className="sbtns";
42028
+ if(gate&&!anyBusy&&!parkedType)btns.appendChild(mkGate("intakebtn sbig"));
42029
+ if(canBuild&&POWERED.includes(i))btns.appendChild(mkRun("runbtn sbig","Run "+LABELS[i]));
42030
+ 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)}
42031
+ if(!btns.children.length&&!live&&!parkedType){const why=document.createElement("p");why.className="muted swhy";
42032
+ 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.":"";
42033
+ if(why.textContent)btns.appendChild(why)}
42034
+ nextBox.title=el.title||"";nextBox.appendChild(main);nextBox.appendChild(btns)}
41325
42035
  row.appendChild(el)});
42036
+ if(curI<0){const main=document.createElement("div");const k=document.createElement("p");k.className="sk";k.textContent="All done";main.appendChild(k);
42037
+ const h3=document.createElement("h3");h3.textContent="All "+STAGES.length+" steps are done.";main.appendChild(h3);nextBox.appendChild(main)}
42038
+ {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);
42039
+ const after=STAGES.length-nDone-(curI<0?0:1);const s=document.createElement("span");
42040
+ s.textContent=nDone+" done"+(curI<0?"":" \xB7 "+(curLive?"1 running now":"1 needs you")+" \xB7 "+after+" after that");plabel.appendChild(s)}
41326
42041
  div.appendChild(row);
41327
42042
  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)}}
41328
42043
  const view=(state.buildViews||{})[c.locationId];
@@ -41337,7 +42052,7 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
41337
42052
  for(const stale of view.stale){const p=document.createElement("p");p.textContent=LABELS[stale.stage]+": stale. "+stale.reason;div.appendChild(p)}
41338
42053
  if(view.approvedAt){const p=document.createElement("p");p.textContent="Building approved on "+new Date(view.approvedAt).toLocaleString()+".";div.appendChild(p)}
41339
42054
  for(const repair of view.repairs){const p=document.createElement("p");p.textContent="Live workflow repair: "+repair.name+". "+repair.changes.join(" ");div.appendChild(p)}
41340
- if(view.planHash){const approve=document.createElement("button");approve.textContent="Approve building\u2026";approve.onclick=async()=>{const typed=sandbox?c.name:prompt("Approve building and the listed live workflow repairs for "+c.name+". Type the client name to confirm.");if(typed===null)return;if(await postJSON("/api/build-approval",{locationId:c.locationId,confirmName:typed,repairHashes:view.repairs.map(r=>r.hash)}))load()};div.appendChild(approve)}
42055
+ if(view.planHash&&!parkedType){const approve=document.createElement("button");approve.textContent="Approve building\u2026";approve.onclick=async()=>{const typed=sandbox?c.name:prompt("Approve building and the listed live workflow repairs for "+c.name+". Type the client name to confirm.");if(typed===null)return;if(await postJSON("/api/build-approval",{locationId:c.locationId,confirmName:typed,repairHashes:view.repairs.map(r=>r.hash)}))load()};div.appendChild(approve)}
41341
42056
  }else{const p=document.createElement("p");p.textContent="The executable plan is prepared when Build runs.";div.appendChild(p)}
41342
42057
  if(view?.handoff)div.appendChild(handoffPanel(c,view));
41343
42058
  const _prot=PROTECTED[c.locationId];
@@ -41354,6 +42069,7 @@ function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES
41354
42069
  const rv=document.createElement("button");rv.className="qdel";rv.style.marginLeft=".5rem";rv.textContent="revoke";
41355
42070
  rv.onclick=async()=>{await postJSON("/api/build-approval",{locationId:c.locationId,revoke:true});load()};
41356
42071
  ab.appendChild(s1);ab.appendChild(rv);ab.appendChild(protectBtn(c))}
42072
+ else if(parkedType){ab.appendChild(protectBtn(c))}
41357
42073
  else{const w=document.createElement("span");w.className="muted";w.textContent="Automated building is off for this client. ";
41358
42074
  const btn=document.createElement("button");btn.className="intakebtn";btn.style.padding=".25rem .55rem";btn.textContent="Approve building\u2026";
41359
42075
  btn.onclick=approveBuilding;
@@ -41734,10 +42450,12 @@ function render(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...sta
41734
42450
  const parked=sorted.filter(c=>match(c)&&["template","hidden"].includes(typeOf(state,c.locationId)));
41735
42451
  if(!board.length&&!parked.length){const d=document.createElement("div");d.className="client";d.innerHTML="<h2>No matches</h2>";m.appendChild(d);return}
41736
42452
  /* One card open at a time. Default: the account being built, else the busiest one. */
41737
- let act=(activeId&&board.some(c=>c.locationId===activeId))?activeId:null;
42453
+ /* A template or hidden account opens too: its card carries the lock, and an owner must never be locked out of their own account (BUG-2026-09-24-07). */
42454
+ const openable=[...board,...parked];
42455
+ let act=(activeId&&openable.some(c=>c.locationId===activeId))?activeId:null;
41738
42456
  if(!act&&run&&run.running&&board.some(c=>c.locationId===run.locationId))act=run.locationId;
41739
42457
  if(!act&&board.length)act=board[0].locationId;
41740
- const actC=board.find(c=>c.locationId===act);
42458
+ const actC=openable.find(c=>c.locationId===act);
41741
42459
  if(run&&run.running&&actC&&run.locationId!==actC.locationId){
41742
42460
  const other=ROSTER.find(c=>c.locationId===run.locationId);
41743
42461
  if(other){const ban=document.createElement("div");ban.className="card";ban.style.cssText="display:flex;align-items:center;gap:.6rem;padding:.55rem .9rem;margin-bottom:1rem;border-color:var(--gold)";
@@ -41755,11 +42473,12 @@ function render(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...sta
41755
42473
  const hint=document.createElement("p");hint.className="switchnote";
41756
42474
  hint.textContent="One account at a time. Opening another puts this one back in the list. Nothing is lost.";
41757
42475
  m.appendChild(hint)}
41758
- if(parked.length){const gh=document.createElement("div");gh.className="grouphead";
41759
- gh.textContent=(showParked?"\u25BE":"\u25B8")+" Templates & hidden ("+parked.length+")";
42476
+ const parkedRest=parked.filter(c=>c.locationId!==act);
42477
+ if(parkedRest.length){const gh=document.createElement("div");gh.className="grouphead";
42478
+ gh.textContent=(showParked?"\u25BE":"\u25B8")+" Templates & hidden ("+parkedRest.length+")";
41760
42479
  gh.onclick=()=>{showParked=!showParked;render(state,run)};m.appendChild(gh);
41761
42480
  if(showParked){const list2=document.createElement("div");list2.className="card acct-list";
41762
- for(const c of parked)list2.appendChild(acctRow(c,state,run));m.appendChild(list2)}}}
42481
+ for(const c of parkedRest)list2.appendChild(acctRow(c,state,run));m.appendChild(list2)}}}
41763
42482
  ${COCKPIT_TYPE_JS}
41764
42483
  load();
41765
42484
  </script></body></html>`;
@@ -42301,8 +43020,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42301
43020
  if (!bytes.length) throw new Error("that file is empty");
42302
43021
  if (bytes.length > DOC_MAX_BYTES) throw new Error("that file is over 20 MB");
42303
43022
  const dir = clientDocsDir(args.locationId);
42304
- fs34.mkdirSync(dir, { recursive: true, mode: 448 });
42305
- if (!insideDir(path32.join(appDataDir(), "clients"), dir)) throw new Error("This client's documents folder is not where it should be, so nothing was saved.");
43023
+ fs35.mkdirSync(dir, { recursive: true, mode: 448 });
43024
+ if (!insideDir(path33.join(appDataDir(), "clients"), dir)) throw new Error("This client's documents folder is not where it should be, so nothing was saved.");
42306
43025
  const saved = writeDocAtomically(dir, name, bytes);
42307
43026
  res.end(JSON.stringify({ ok: true, name: saved, files: listClientDocs(args.locationId) }));
42308
43027
  } catch (e) {
@@ -42477,6 +43196,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42477
43196
  try {
42478
43197
  const { locationId: locationId2, stage, intakeConfirmed } = JSON.parse(body);
42479
43198
  if (activeRun2?.running) throw new Error("a run is already in progress");
43199
+ const runType = readCockpitState().accounts?.[locationId2]?.type;
43200
+ if (runType === "template" || runType === "hidden") throw new Error(PARKED_NOT_BUILT);
42480
43201
  let st0 = readCockpitState();
42481
43202
  let inputHash;
42482
43203
  let normalizedAnswers;
@@ -42505,8 +43226,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42505
43226
  const snap = await readIntakeSnapshot(c, locationId2);
42506
43227
  if (snap.status === "unavailable") throw new Error(INTAKE_UNREADABLE);
42507
43228
  if (snap.status !== "received") throw new Error("Open the real form, submit it, then refresh.");
42508
- const required = composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }).filter((q2) => q2.required && !String(normalizedAnswers?.[q2.label] ?? normalizedAnswers?.[q2.key] ?? "").trim());
42509
- if (required.length) throw new Error("Complete these required intake answers: " + required.map((q2) => q2.label).join(", ") + ".");
43229
+ const required = composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }).filter((q3) => q3.required && !String(normalizedAnswers?.[q3.label] ?? normalizedAnswers?.[q3.key] ?? "").trim());
43230
+ if (required.length) throw new Error("Complete these required intake answers: " + required.map((q3) => q3.label).join(", ") + ".");
42510
43231
  }
42511
43232
  if (stage === 0 && intakeConfirmed !== true) throw new Error("Confirm the custom fields, form and published notification workflow on this client\u2019s Intake button.");
42512
43233
  if (!st0.accounts?.[locationId2]?.industry) throw new Error("Choose the client industry before running Intake.");
@@ -42552,7 +43273,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42552
43273
  industries: INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })),
42553
43274
  overlay: agency,
42554
43275
  warnings: unknownKeys(agency, INTAKE_QUESTIONS).map((k) => `Your overlay references "${k}", which isn't in the base intake.`),
42555
- questions: composed.map((q2) => ({ key: q2.key, label: q2.label, sectionLabel: q2.sectionLabel, required: q2.required, base: INTAKE_QUESTIONS.some((b) => b.key === q2.key) }))
43276
+ questions: composed.map((q3) => ({ key: q3.key, label: q3.label, sectionLabel: q3.sectionLabel, required: q3.required, base: INTAKE_QUESTIONS.some((b) => b.key === q3.key) }))
42556
43277
  }));
42557
43278
  return;
42558
43279
  }
@@ -42588,14 +43309,14 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42588
43309
  const snap = c ? await readIntakeSnapshot(c, locationId2, Boolean(st.intakeSent?.[locationId2])) : { status: "not_installed", fields: {}, queryKeys: {}, submissions: 0 };
42589
43310
  if (snap.status === "unavailable") throw new Error(INTAKE_UNREADABLE);
42590
43311
  const industry = st.accounts?.[locationId2]?.industry;
42591
- const questions = composeQuestionSet({ industry, agency: readOverlay() }).map((q2) => ({ key: q2.key, contactTag: q2.field.kind === "standard" ? q2.field.tag : void 0, label: q2.label, sectionLabel: q2.sectionLabel, required: q2.required, options: q2.options ?? null, placeholder: q2.placeholder ?? "", multi: q2.field.kind === "custom" && q2.field.dataType === "MULTIPLE_OPTIONS" }));
43312
+ 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" }));
42592
43313
  const answers = normalizedIntake(snap, st.intakeDrafts?.[locationId2]);
42593
43314
  const submittedAnswers = normalizedIntake(snap);
42594
43315
  const hash = intakeInput(submittedAnswers, industry, composeQuestionSet({ industry, agency: readOverlay() }));
42595
43316
  const latestState = readCockpitState();
42596
43317
  writeCockpitState({ ...latestState, intakeInputs: { ...latestState.intakeInputs, [locationId2]: { hash, answers: submittedAnswers, status: snap.status } } });
42597
43318
  res.writeHead(200, { "Content-Type": "application/json" });
42598
- 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 }));
43319
+ res.end(JSON.stringify({ ok: true, ...snap, questions, industry, inputHash: hash, unansweredRequired: questions.filter((q3) => q3.required && !String(answers[q3.label] ?? answers[q3.key] ?? "").trim()).map((q3) => q3.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 }));
42599
43320
  } catch (e) {
42600
43321
  res.writeHead(400, { "Content-Type": "application/json" });
42601
43322
  logQuietly(`client-intake ${new URL(req.url || "/", "http://x").searchParams.get("locationId") || ""}: ${e instanceof Error ? e.message : String(e)}`);
@@ -42614,7 +43335,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42614
43335
  const snap = await readIntakeSnapshot(c, args.locationId, false);
42615
43336
  if (!snap.formId || !snap.formUrl) throw new Error(INTAKE_FORM_REQUIRED);
42616
43337
  res.writeHead(200, { "Content-Type": "application/json" });
42617
- const identityLabels = new Set(composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).filter((q2) => q2.field.kind === "standard").map((q2) => q2.label));
43338
+ const identityLabels = new Set(composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).filter((q3) => q3.field.kind === "standard").map((q3) => q3.label));
42618
43339
  const customAnswers = Object.fromEntries(Object.entries(args.answers ?? {}).filter(([label]) => !identityLabels.has(label)));
42619
43340
  const pf = prefillFormUrl(snap.formUrl, snap.queryKeys, customAnswers, args.contact ?? {}, snap.altQueryKeys ?? {});
42620
43341
  res.end(JSON.stringify({ ok: true, url: pf.url, leftovers: pf.leftovers }));
@@ -42656,28 +43377,28 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42656
43377
  if (args.file) {
42657
43378
  const name = safeDocName(args.file);
42658
43379
  const dir = clientDocsDir(args.locationId);
42659
- const p = path32.join(dir, name);
42660
- if (!fs34.existsSync(p)) throw new Error("that document is not saved for this client");
42661
- if (fs34.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error("that document is not saved for this client");
42662
- const text2 = fs34.readFileSync(p, "utf8");
43380
+ const p = path33.join(dir, name);
43381
+ if (!fs35.existsSync(p)) throw new Error("that document is not saved for this client");
43382
+ if (fs35.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error("that document is not saved for this client");
43383
+ const text2 = fs35.readFileSync(p, "utf8");
42663
43384
  if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
42664
43385
  fileSource = { kind: "text", text: text2 };
42665
43386
  }
42666
43387
  const source = fileSource ? fileSource : args.url ? { kind: "url", url: String(args.url) } : args.instruction ? { kind: "recorder", instruction: String(args.instruction) } : { kind: "text", text: String(args.transcript ?? "") };
42667
43388
  let recorderPatterns = [];
42668
43389
  if (source.kind === "recorder") {
42669
- recorderPatterns = await new Promise((resolve12) => {
43390
+ recorderPatterns = await new Promise((resolve13) => {
42670
43391
  const mcpList = claudeCommand(claudeBin(), ["mcp", "list"]);
42671
43392
  const ls = (0, import_child_process5.spawn)(mcpList.command, mcpList.args, { stdio: ["ignore", "pipe", "ignore"] });
42672
43393
  let buf = "";
42673
43394
  ls.stdout.on("data", (d) => {
42674
43395
  buf += d.toString();
42675
43396
  });
42676
- ls.on("close", () => resolve12(recorderAllowPatterns(buf)));
42677
- ls.on("error", () => resolve12([]));
43397
+ ls.on("close", () => resolve13(recorderAllowPatterns(buf)));
43398
+ ls.on("error", () => resolve13([]));
42678
43399
  setTimeout(() => {
42679
43400
  ls.kill("SIGKILL");
42680
- resolve12(recorderAllowPatterns(buf));
43401
+ resolve13(recorderAllowPatterns(buf));
42681
43402
  }, 9e4).unref?.();
42682
43403
  });
42683
43404
  if (!recorderPatterns.length) {
@@ -42688,10 +43409,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42688
43409
  }
42689
43410
  const out = await prefillFromSource(
42690
43411
  (prompt, tools) => callClaudeRecorded(bin, ["-p", prompt, "--max-turns", "25", "--disallowedTools", tools.deny, "--allowedTools", tools.allow || ""], { locationId: args.locationId, stage: 0, kind: "intake extraction" }),
42691
- composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((q2) => q2.label),
43412
+ composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((q3) => q3.label),
42692
43413
  source,
42693
43414
  recorderPatterns,
42694
- composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((q2) => ({ label: q2.label, options: q2.options, multi: q2.field.kind === "custom" && q2.field.dataType === "MULTIPLE_OPTIONS", dataType: q2.field.kind === "custom" ? q2.field.dataType : q2.field.tag }))
43415
+ composeQuestionSet({ industry: readCockpitState().accounts?.[args.locationId]?.industry, agency: readOverlay() }).map((q3) => ({ label: q3.label, options: q3.options, multi: q3.field.kind === "custom" && q3.field.dataType === "MULTIPLE_OPTIONS", dataType: q3.field.kind === "custom" ? q3.field.dataType : q3.field.tag }))
42695
43416
  );
42696
43417
  res.writeHead(200, { "Content-Type": "application/json" });
42697
43418
  res.end(JSON.stringify({ ...out, costs: readRunCosts().filter((r) => r.locationId === args.locationId).slice(-2) }));
@@ -42733,14 +43454,14 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42733
43454
  }
42734
43455
  if (req.method === "GET" && /^\/brand\/[a-z0-9-]+\/screenshot\.png$/.test(url)) {
42735
43456
  const slug3 = url.split("/")[2];
42736
- const file2 = path32.join(brandDir(slug3), "screenshot.png");
42737
- if (!fs34.existsSync(file2)) {
43457
+ const file2 = path33.join(brandDir(slug3), "screenshot.png");
43458
+ if (!fs35.existsSync(file2)) {
42738
43459
  res.writeHead(404);
42739
43460
  res.end();
42740
43461
  return;
42741
43462
  }
42742
43463
  res.writeHead(200, { "Content-Type": "image/png", "Cache-Control": "no-store" });
42743
- res.end(fs34.readFileSync(file2));
43464
+ res.end(fs35.readFileSync(file2));
42744
43465
  return;
42745
43466
  }
42746
43467
  if (req.method === "GET" && /^\/brand\/[a-z0-9-]+$/.test(url)) {
@@ -42749,7 +43470,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42749
43470
  const dna = houseLooks().find((d) => d.slug === slug3) ?? readBrand(slug3);
42750
43471
  const dir = brandExists(slug3) ? brandDir(slug3) : void 0;
42751
43472
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
42752
- res.end(brandCardHtml(dna, { screenshot: dir ? fs34.existsSync(path32.join(dir, "screenshot.png")) : false, dir, seatToken: mayServeSeat(req, port) ? seatToken : "" }));
43473
+ res.end(brandCardHtml(dna, { screenshot: dir ? fs35.existsSync(path33.join(dir, "screenshot.png")) : false, dir, seatToken: mayServeSeat(req, port) ? seatToken : "" }));
42753
43474
  } catch (e) {
42754
43475
  res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
42755
43476
  res.end(e.message);
@@ -42911,7 +43632,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
42911
43632
  }
42912
43633
  try {
42913
43634
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
42914
- res.end(fs34.readFileSync(localCopyPath(locationId2), "utf8"));
43635
+ res.end(fs35.readFileSync(localCopyPath(locationId2), "utf8"));
42915
43636
  } catch {
42916
43637
  res.end("The handoff is not prepared yet. Run Verify first.");
42917
43638
  }
@@ -43011,8 +43732,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
43011
43732
  return;
43012
43733
  }
43013
43734
  if (req.method === "GET" && url === "/blueprint") {
43014
- const q2 = new URL(req.url || "/", "http://x").searchParams;
43015
- const locationId2 = q2.get("locationId") || "";
43735
+ const q3 = new URL(req.url || "/", "http://x").searchParams;
43736
+ const locationId2 = q3.get("locationId") || "";
43016
43737
  const st = readCockpitState();
43017
43738
  const review = st.reviews?.[locationId2];
43018
43739
  if (!review?.ok) {
@@ -43022,7 +43743,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
43022
43743
  }
43023
43744
  const name = currentRoster().find((r) => r.locationId === locationId2)?.name || locationId2;
43024
43745
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
43025
- res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name, agencyName: q2.get("agency") || st.agencyName, accent: q2.get("accent") || st.agencyAccent }));
43746
+ res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
43026
43747
  return;
43027
43748
  }
43028
43749
  if (req.method === "GET" && url === "/api/review") {
@@ -43483,9 +44204,9 @@ ${opportunityNoteFailure?.detail ?? ""}`);
43483
44204
  res.end();
43484
44205
  }));
43485
44206
  try {
43486
- await new Promise((resolve12, reject) => {
44207
+ await new Promise((resolve13, reject) => {
43487
44208
  server2.once("error", reject);
43488
- server2.listen(port, "127.0.0.1", resolve12);
44209
+ server2.listen(port, "127.0.0.1", resolve13);
43489
44210
  });
43490
44211
  } catch (e) {
43491
44212
  const msg3 = e?.code === "EADDRINUSE" ? `Port ${port} is busy with another program. Close it, or start Command OS on a different port.` : `Command OS could not start: ${e instanceof Error ? e.message : String(e)}`;
@@ -43511,13 +44232,13 @@ ${opportunityNoteFailure?.detail ?? ""}`);
43511
44232
  if (adopted) process.stderr.write(` Synced ${adopted} client${adopted === 1 ? "" : "s"} from your GHL (teammate updates).
43512
44233
  `);
43513
44234
  });
43514
- return await new Promise((resolve12) => {
43515
- const stop = () => server2.close(() => resolve12(0));
43516
- process.on("SIGINT", stop);
43517
- process.on("SIGTERM", stop);
44235
+ return await new Promise((resolve13) => {
44236
+ const stop2 = () => server2.close(() => resolve13(0));
44237
+ process.on("SIGINT", stop2);
44238
+ process.on("SIGTERM", stop2);
43518
44239
  });
43519
44240
  }
43520
- var import_zod62, import_node_util4, fs34, path32, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, BOARD_ROUTES, BOARD_TITLES, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest;
44241
+ var import_zod62, import_node_util4, fs35, path33, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, BOARD_ROUTES, BOARD_TITLES, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest;
43521
44242
  var init_dashboard = __esm({
43522
44243
  "src/dashboard.ts"() {
43523
44244
  "use strict";
@@ -43545,8 +44266,8 @@ var init_dashboard = __esm({
43545
44266
  init_handoff_snapshot();
43546
44267
  init_member_progress();
43547
44268
  init_build_obligations();
43548
- fs34 = __toESM(require("fs"));
43549
- path32 = __toESM(require("path"));
44269
+ fs35 = __toESM(require("fs"));
44270
+ path33 = __toESM(require("path"));
43550
44271
  http = __toESM(require("http"));
43551
44272
  import_child_process4 = require("child_process");
43552
44273
  init_credentials_store();
@@ -43624,6 +44345,7 @@ var init_dashboard = __esm({
43624
44345
  UPGRADE_MSG = "\n The cockpit is not included in this licence, so it did not start.\n Nothing was changed.\n\n";
43625
44346
  DOC_EXTS = /* @__PURE__ */ new Set([".md", ".txt", ".vtt", ".srt", ".csv"]);
43626
44347
  DOC_MAX_BYTES = 20 * 1024 * 1024;
44348
+ PARKED_NOT_BUILT = "Template and hidden accounts are not built from Command OS. Change the account's type to Client first.";
43627
44349
  NEEDS_AGENCY_KEY = "No agency key on file. Run register_agency_key in GHL Command first.";
43628
44350
  INTAKE_UNREADABLE = "Command OS could not read this client's account. Paste its key again in Build a Client Account.";
43629
44351
  INTAKE_FORM_REQUIRED = "Run the Intake stage in Client Builds to create the form, then come back and submit.";
@@ -44115,12 +44837,12 @@ ${companies.map((c) => ` ${c.name || c.companyId} (${c.companyId})`).join("\n")
44115
44837
  ]
44116
44838
  };
44117
44839
  }
44118
- const tokens = registry2.listLocations().map((loc) => ({
44840
+ const tokens2 = registry2.listLocations().map((loc) => ({
44119
44841
  ...loc,
44120
44842
  companyId: registry2.getToken(loc.locationId)?.companyId
44121
44843
  }));
44122
44844
  const currentId = client.defaultLocationId;
44123
- const lines = tokens.map((loc) => {
44845
+ const lines = tokens2.map((loc) => {
44124
44846
  const marker = loc.locationId === currentId ? "\u2192 " : " ";
44125
44847
  const company = loc.companyId ? ` [company ${loc.companyId}${companiesWithFirebase.has(loc.companyId) ? ", Firebase \u2713" : ", no Firebase"}]` : "";
44126
44848
  return `${marker}${loc.name} (${loc.locationId})${company}`;
@@ -44133,7 +44855,7 @@ ${companies.map((c) => ` ${c.name || c.companyId} (${c.companyId})`).join("\n")
44133
44855
  content: [
44134
44856
  {
44135
44857
  type: "text",
44136
- text: `Token Registry (${tokens.length} locations):
44858
+ text: `Token Registry (${tokens2.length} locations):
44137
44859
  ${lines.join("\n")}
44138
44860
 
44139
44861
  \u2192 = currently active${fbSection}`
@@ -44288,7 +45010,7 @@ var init_location_switcher = __esm({
44288
45010
 
44289
45011
  // src/tools/bulk-operations.ts
44290
45012
  function delay2(ms) {
44291
- return new Promise((resolve12) => setTimeout(resolve12, ms));
45013
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
44292
45014
  }
44293
45015
  function formatResults(op, results, total) {
44294
45016
  return `${op}: ${results.success} success, ${results.failed} failed out of ${total}.${results.errors.length ? "\nErrors:\n" + results.errors.join("\n") : ""}`;
@@ -44533,7 +45255,7 @@ var init_asset_ownership = __esm({
44533
45255
  function sectionItems(section2) {
44534
45256
  return Array.isArray(section2) ? section2 : [];
44535
45257
  }
44536
- function bullet(lines) {
45258
+ function bullet2(lines) {
44537
45259
  return lines.length ? lines.map((l) => `- ${l}`).join("\n") : "- None found.";
44538
45260
  }
44539
45261
  function plainReason(reason3) {
@@ -44555,7 +45277,7 @@ function sectionList(section2, render) {
44555
45277
  if (!Array.isArray(section2)) {
44556
45278
  return `**Not read.** ${plainReason(section2.unavailable)}. This section is unknown, which is NOT the same as empty. Check it by hand before you rely on this document.`;
44557
45279
  }
44558
- return bullet(section2.map(render));
45280
+ return bullet2(section2.map(render));
44559
45281
  }
44560
45282
  function buildHandoffDoc(input) {
44561
45283
  const {
@@ -44577,13 +45299,13 @@ function buildHandoffDoc(input) {
44577
45299
  if (!items.length) return "";
44578
45300
  return `**${kind}** ${OWNER_MEANING[kind]}
44579
45301
 
44580
- ${bullet(
45302
+ ${bullet2(
44581
45303
  items.map((i) => i.note ? `${i.asset} (${i.note})` : i.asset)
44582
45304
  )}`;
44583
45305
  }).filter(Boolean).join("\n\n") : "Nothing has been recorded yet. Every asset below is unanswered.";
44584
45306
  const unknownSection = ownership.unknown.length ? `These exist in the account but nobody has said who owns them. Answer them once and they stay answered:
44585
45307
 
44586
- ${bullet(ownership.unknown)}` : "Every asset found in this account has a recorded owner.";
45308
+ ${bullet2(ownership.unknown)}` : "Every asset found in this account has a recorded owner.";
44587
45309
  const webhookSection = sectionList(webhooks, (w) => `${w.name} sends to ${w.url}${w.events.length ? ` when: ${w.events.join(", ")}` : ""}`) + (webhookScanPartial ? `
44588
45310
 
44589
45311
  **This list may be incomplete.** ${webhookScanPartial}` : "");
@@ -44660,7 +45382,7 @@ ${ownership.unreadable.length ? `
44660
45382
 
44661
45383
  Somebody edited these by hand and the answer no longer parses. They need a human:
44662
45384
 
44663
- ${bullet(ownership.unreadable)}
45385
+ ${bullet2(ownership.unreadable)}
44664
45386
  ` : ""}
44665
45387
  ## Rotate these on the way out
44666
45388
 
@@ -44693,7 +45415,7 @@ var init_handoff_doc = __esm({
44693
45415
 
44694
45416
  // src/tools/account-export.ts
44695
45417
  function delay3(ms) {
44696
- return new Promise((resolve12) => setTimeout(resolve12, ms));
45418
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
44697
45419
  }
44698
45420
  function builderSectionAvailability(builderLocationId, requestedLocationId) {
44699
45421
  if (!builderLocationId) {
@@ -45408,13 +46130,13 @@ function contactServiceNote(payload) {
45408
46130
  function registerSmartListTools(server2, builderClient) {
45409
46131
  if (!builderClient) return;
45410
46132
  const client = builderClient;
45411
- async function smartListRequest(method, path41, body) {
45412
- const url = `${SMARTLIST_BASE}${path41}`;
46133
+ async function smartListRequest(method, path42, body) {
46134
+ const url = `${SMARTLIST_BASE}${path42}`;
45413
46135
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
45414
46136
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
45415
46137
  if (!response.ok) {
45416
46138
  const text3 = await response.text();
45417
- throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path41}
46139
+ throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path42}
45418
46140
  ${text3}`);
45419
46141
  }
45420
46142
  const text2 = await response.text();
@@ -45659,11 +46381,11 @@ var init_smart_lists = __esm({
45659
46381
  function registerReputationTools(server2, builderClient) {
45660
46382
  if (!builderClient) return;
45661
46383
  const client = builderClient;
45662
- async function reputationRequest(method, path41) {
45663
- const response = await builderRequest(client, method, `${REPUTATION_BASE}${path41}`);
46384
+ async function reputationRequest(method, path42) {
46385
+ const response = await builderRequest(client, method, `${REPUTATION_BASE}${path42}`);
45664
46386
  if (!response.ok) {
45665
46387
  const text3 = await response.text();
45666
- throw new Error(`Reputation API Error ${response.status}: ${method} ${path41}
46388
+ throw new Error(`Reputation API Error ${response.status}: ${method} ${path42}
45667
46389
  ${text3}`);
45668
46390
  }
45669
46391
  const text2 = await response.text();
@@ -45705,7 +46427,7 @@ ${text3}`);
45705
46427
  );
45706
46428
  }
45707
46429
  function buildReviewsQuery(locationId2, opts = {}) {
45708
- const q2 = [
46430
+ const q3 = [
45709
46431
  `filterParams[locationId][0][value]=${locationId2}`,
45710
46432
  `filterParams[locationId][0][condition]=eq`,
45711
46433
  `filterParams[deleted][0][value]=${opts.includeDeleted ? "true" : "false"}`,
@@ -45719,13 +46441,13 @@ function buildReviewsQuery(locationId2, opts = {}) {
45719
46441
  if (opts.maxRating !== void 0) bounds.push([opts.maxRating, "lte"]);
45720
46442
  }
45721
46443
  bounds.forEach(([value, condition], i) => {
45722
- q2.push(`filterParams[starRating][${i}][value]=${value}`);
45723
- q2.push(`filterParams[starRating][${i}][condition]=${condition}`);
46444
+ q3.push(`filterParams[starRating][${i}][value]=${value}`);
46445
+ q3.push(`filterParams[starRating][${i}][condition]=${condition}`);
45724
46446
  });
45725
- q2.push(`sortParams[dateAdded]=-1`);
45726
- q2.push(`pageNumber=${opts.pageNumber ?? 1}`);
45727
- q2.push(`pageSize=${opts.pageSize ?? 10}`);
45728
- const query = q2.map((p) => {
46447
+ q3.push(`sortParams[dateAdded]=-1`);
46448
+ q3.push(`pageNumber=${opts.pageNumber ?? 1}`);
46449
+ q3.push(`pageSize=${opts.pageSize ?? 10}`);
46450
+ const query = q3.map((p) => {
45729
46451
  const i = p.indexOf("=");
45730
46452
  return `${encodeURIComponent(p.slice(0, i))}=${encodeURIComponent(p.slice(i + 1))}`;
45731
46453
  }).join("&");
@@ -45801,11 +46523,11 @@ var init_email_campaigns = __esm({
45801
46523
  function registerMembershipTools(server2, builderClient) {
45802
46524
  if (!builderClient) return;
45803
46525
  const client = builderClient;
45804
- async function membershipRequest(path41, method = "GET", body) {
45805
- const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path41}`, body ? { body } : {});
46526
+ async function membershipRequest(path42, method = "GET", body) {
46527
+ const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path42}`, body ? { body } : {});
45806
46528
  if (!response.ok) {
45807
46529
  const text3 = await response.text();
45808
- throw new Error(`Membership API Error ${response.status}: ${method} ${path41}
46530
+ throw new Error(`Membership API Error ${response.status}: ${method} ${path42}
45809
46531
  ${text3}`);
45810
46532
  }
45811
46533
  const text2 = await response.text();
@@ -45991,28 +46713,28 @@ var init_memberships = __esm({
45991
46713
 
45992
46714
  // src/tools/template-deployer.ts
45993
46715
  function delay4(ms) {
45994
- return new Promise((resolve12) => setTimeout(resolve12, ms));
46716
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
45995
46717
  }
45996
46718
  function isAlreadyExistsError(message) {
45997
46719
  return /already\s+exists?\b/i.test(message) || /duplicate/i.test(message);
45998
46720
  }
45999
46721
  function registerTemplateDeployerTools(server2, client) {
46000
46722
  const builderClient = WorkflowBuilderClient.fromEnv();
46001
- const templatesDir = path33.resolve(__dirname, "..", "templates");
46723
+ const templatesDir = path34.resolve(__dirname, "..", "templates");
46002
46724
  function validateTemplatePath(templateFile) {
46003
- const resolved2 = path33.resolve(templateFile);
46004
- const realPath = fs35.existsSync(resolved2) ? fs35.realpathSync(resolved2) : resolved2;
46725
+ const resolved2 = path34.resolve(templateFile);
46726
+ const realPath = fs36.existsSync(resolved2) ? fs36.realpathSync(resolved2) : resolved2;
46005
46727
  const allowedDirs = [
46006
46728
  templatesDir,
46007
- path33.resolve(process.cwd(), "templates"),
46008
- path33.resolve(__dirname, "..", "..", "templates")
46009
- ].map((d) => fs35.existsSync(d) ? fs35.realpathSync(d) : d);
46010
- const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir + path33.sep));
46729
+ path34.resolve(process.cwd(), "templates"),
46730
+ path34.resolve(__dirname, "..", "..", "templates")
46731
+ ].map((d) => fs36.existsSync(d) ? fs36.realpathSync(d) : d);
46732
+ const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir + path34.sep));
46011
46733
  if (!isAllowed) {
46012
46734
  throw new Error(`Template file must be inside a templates/ directory. Got: ${realPath}`);
46013
46735
  }
46014
- if (path33.relative(allowedDirs[0], realPath).startsWith("..")) {
46015
- const otherMatch = allowedDirs.slice(1).some((d) => !path33.relative(d, realPath).startsWith(".."));
46736
+ if (path34.relative(allowedDirs[0], realPath).startsWith("..")) {
46737
+ const otherMatch = allowedDirs.slice(1).some((d) => !path34.relative(d, realPath).startsWith(".."));
46016
46738
  if (!otherMatch) {
46017
46739
  throw new Error(`Template path escapes allowed directory.`);
46018
46740
  }
@@ -46027,26 +46749,26 @@ function registerTemplateDeployerTools(server2, client) {
46027
46749
  try {
46028
46750
  const dirs = [
46029
46751
  templatesDir,
46030
- path33.resolve(process.cwd(), "templates"),
46031
- path33.resolve(__dirname, "..", "..", "templates")
46752
+ path34.resolve(process.cwd(), "templates"),
46753
+ path34.resolve(__dirname, "..", "..", "templates")
46032
46754
  ];
46033
46755
  const templates = [];
46034
46756
  const seen = /* @__PURE__ */ new Set();
46035
46757
  for (const dir of dirs) {
46036
- if (!fs35.existsSync(dir)) continue;
46037
- const files = fs35.readdirSync(dir).filter((f) => f.endsWith(".json"));
46758
+ if (!fs36.existsSync(dir)) continue;
46759
+ const files = fs36.readdirSync(dir).filter((f) => f.endsWith(".json"));
46038
46760
  for (const file2 of files) {
46039
- const full = path33.join(dir, file2);
46761
+ const full = path34.join(dir, file2);
46040
46762
  let real;
46041
46763
  try {
46042
- real = fs35.realpathSync(full);
46764
+ real = fs36.realpathSync(full);
46043
46765
  } catch {
46044
46766
  continue;
46045
46767
  }
46046
46768
  if (seen.has(real)) continue;
46047
46769
  seen.add(real);
46048
46770
  try {
46049
- const content = JSON.parse(fs35.readFileSync(full, "utf-8"));
46771
+ const content = JSON.parse(fs36.readFileSync(full, "utf-8"));
46050
46772
  if (typeof content.templateName !== "string" || !content.templateName) continue;
46051
46773
  const parsed2 = TemplateSchema.safeParse(content);
46052
46774
  templates.push({
@@ -46083,7 +46805,7 @@ function registerTemplateDeployerTools(server2, client) {
46083
46805
  async ({ templateFile }) => {
46084
46806
  try {
46085
46807
  const safePath = validateTemplatePath(templateFile);
46086
- const content = JSON.parse(fs35.readFileSync(safePath, "utf-8"));
46808
+ const content = JSON.parse(fs36.readFileSync(safePath, "utf-8"));
46087
46809
  return {
46088
46810
  content: [
46089
46811
  {
@@ -46123,8 +46845,8 @@ function registerTemplateDeployerTools(server2, client) {
46123
46845
  try {
46124
46846
  const locId = client.resolveLocationId(locationId2);
46125
46847
  const safePath = validateTemplatePath(templateFile);
46126
- const template = TemplateSchema.parse(JSON.parse(fs35.readFileSync(safePath, "utf-8")));
46127
- const resolve12 = (text2) => {
46848
+ const template = TemplateSchema.parse(JSON.parse(fs36.readFileSync(safePath, "utf-8")));
46849
+ const resolve13 = (text2) => {
46128
46850
  if (typeof text2 !== "string") return text2;
46129
46851
  let result = text2;
46130
46852
  for (const [key, value] of Object.entries(answers)) {
@@ -46137,7 +46859,7 @@ function registerTemplateDeployerTools(server2, client) {
46137
46859
  return result;
46138
46860
  };
46139
46861
  const resolveObj = (obj) => {
46140
- if (typeof obj === "string") return resolve12(obj);
46862
+ if (typeof obj === "string") return resolve13(obj);
46141
46863
  if (Array.isArray(obj)) return obj.map(resolveObj);
46142
46864
  if (obj && typeof obj === "object") {
46143
46865
  const result = {};
@@ -46495,15 +47217,15 @@ ${errors.join("\n")}` : dryRun ? "\nNo errors!" : "\nNo errors \u2014 every item
46495
47217
  }
46496
47218
  );
46497
47219
  }
46498
- var import_zod71, fs35, path33, TemplateSchema;
47220
+ var import_zod71, fs36, path34, TemplateSchema;
46499
47221
  var init_template_deployer = __esm({
46500
47222
  "src/tools/template-deployer.ts"() {
46501
47223
  "use strict";
46502
47224
  import_zod71 = require("zod");
46503
47225
  init_workflow_builder_client();
46504
47226
  init_tool_helpers();
46505
- fs35 = __toESM(require("fs"));
46506
- path33 = __toESM(require("path"));
47227
+ fs36 = __toESM(require("fs"));
47228
+ path34 = __toESM(require("path"));
46507
47229
  init_workflow_builder_client();
46508
47230
  TemplateSchema = import_zod71.z.object({
46509
47231
  templateName: import_zod71.z.string(),
@@ -46824,10 +47546,10 @@ async function confirm(run) {
46824
47546
  async function runProbes(client, loc) {
46825
47547
  return Promise.all(
46826
47548
  PROBES.map((probe) => confirm(async () => {
46827
- const path41 = probe.path.replace("{loc}", loc);
47549
+ const path42 = probe.path.replace("{loc}", loc);
46828
47550
  const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
46829
47551
  try {
46830
- const raw = await client.get(path41, { params, noRetry: true });
47552
+ const raw = await client.get(path42, { params, noRetry: true });
46831
47553
  const count2 = countRows(raw, probe.listKeys);
46832
47554
  return {
46833
47555
  key: probe.key,
@@ -47103,14 +47825,14 @@ var init_checkup = __esm({
47103
47825
 
47104
47826
  // src/tools/user-guide.ts
47105
47827
  function resolveGuidePath() {
47106
- const bundled = path34.resolve(__dirname, "..", "guide", "guide.html");
47107
- if (fs36.existsSync(bundled)) return bundled;
47108
- const devRoot = path34.resolve(__dirname, "..", "..");
47828
+ const bundled = path35.resolve(__dirname, "..", "guide", "guide.html");
47829
+ if (fs37.existsSync(bundled)) return bundled;
47830
+ const devRoot = path35.resolve(__dirname, "..", "..");
47109
47831
  try {
47110
- const pkg2 = JSON.parse(fs36.readFileSync(path34.join(devRoot, "package.json"), "utf8"));
47832
+ const pkg2 = JSON.parse(fs37.readFileSync(path35.join(devRoot, "package.json"), "utf8"));
47111
47833
  if (pkg2.name === "@elitedcs/ghl-mcp") {
47112
- const dev = path34.join(devRoot, "guide", "guide.html");
47113
- if (fs36.existsSync(dev)) return dev;
47834
+ const dev = path35.join(devRoot, "guide", "guide.html");
47835
+ if (fs37.existsSync(dev)) return dev;
47114
47836
  }
47115
47837
  } catch {
47116
47838
  }
@@ -47119,7 +47841,7 @@ function resolveGuidePath() {
47119
47841
  function guideContents(file2) {
47120
47842
  let html2 = "";
47121
47843
  try {
47122
- html2 = fs36.readFileSync(file2, "utf8");
47844
+ html2 = fs37.readFileSync(file2, "utf8");
47123
47845
  } catch {
47124
47846
  return "The guide is installed but could not be read.";
47125
47847
  }
@@ -47143,7 +47865,7 @@ function registerUserGuideTools(server2) {
47143
47865
  { open: import_zod73.z.boolean().optional().describe("Open the guide in the browser. Takes over the person's screen \u2014 only when they asked for it.") },
47144
47866
  async ({ open }) => {
47145
47867
  const guidePath = resolveGuidePath();
47146
- if (!fs36.existsSync(guidePath)) {
47868
+ if (!fs37.existsSync(guidePath)) {
47147
47869
  return {
47148
47870
  content: [{
47149
47871
  type: "text",
@@ -47176,14 +47898,14 @@ ${guidePath}`;
47176
47898
  }
47177
47899
  );
47178
47900
  }
47179
- var import_child_process6, import_zod73, fs36, path34;
47901
+ var import_child_process6, import_zod73, fs37, path35;
47180
47902
  var init_user_guide = __esm({
47181
47903
  "src/tools/user-guide.ts"() {
47182
47904
  "use strict";
47183
47905
  import_child_process6 = require("child_process");
47184
47906
  import_zod73 = require("zod");
47185
- fs36 = __toESM(require("fs"));
47186
- path34 = __toESM(require("path"));
47907
+ fs37 = __toESM(require("fs"));
47908
+ path35 = __toESM(require("path"));
47187
47909
  }
47188
47910
  });
47189
47911
 
@@ -47244,7 +47966,8 @@ function registerAgencyProfileTools(server2) {
47244
47966
  defaults: import_zod74.z.object({
47245
47967
  timezone: import_zod74.z.string().optional(),
47246
47968
  industry: import_zod74.z.string().optional()
47247
- }).optional()
47969
+ }).optional(),
47970
+ clientFilesDir: import_zod74.z.string().optional().describe("A full folder path on this machine where client call files are also written, usually your Google Drive for desktop folder so Drive syncs them. A relative path is refused.")
47248
47971
  },
47249
47972
  async (args) => {
47250
47973
  const patch = Object.fromEntries(
@@ -47822,7 +48545,7 @@ function blockContrast(t, fallbackBg) {
47822
48545
  if (!fg || !bg) return null;
47823
48546
  return Math.round(contrastHex(fg, bg) * 100) / 100;
47824
48547
  }
47825
- function inside(inner, outer, inset = 0) {
48548
+ function inside2(inner, outer, inset = 0) {
47826
48549
  return inner.x >= outer.x + inset - 0.5 && inner.y >= outer.y + inset - 0.5 && inner.x + inner.w <= outer.x + outer.w - inset + 0.5 && inner.y + inner.h <= outer.y + outer.h - inset + 0.5;
47827
48550
  }
47828
48551
  function clusterBoxes(boxes, gap = 16) {
@@ -47917,7 +48640,7 @@ function checkSlides(probe, pixels, ctx, rendered) {
47917
48640
  const bad = [];
47918
48641
  for (const s of slides) {
47919
48642
  const inset = Math.round(s.box.w * 0.05);
47920
- const outside = [...s.textBlocks.filter((t) => t.words > 0).map((t) => ({ box: t.box, what: `"${t.text.slice(0, 20)}"` })), ...s.images.map((i) => ({ box: i.box, what: i.tag }))].filter((e) => !inside(e.box, s.box, inset));
48643
+ const outside = [...s.textBlocks.filter((t) => t.words > 0).map((t) => ({ box: t.box, what: `"${t.text.slice(0, 20)}"` })), ...s.images.map((i) => ({ box: i.box, what: i.tag }))].filter((e) => !inside2(e.box, s.box, inset));
47921
48644
  if (outside.length) bad.push(`${slideName(s)}: ${outside.slice(0, 2).map((o) => o.what).join(", ")} inside the ${inset} px band`);
47922
48645
  }
47923
48646
  out.push(graded(rule("S5"), bad.length === 0, bad.length ? fails3(bad) : `all content \u2265${Math.round(slides[0].box.w * 0.05)} px from every edge`));
@@ -48139,7 +48862,7 @@ function checkSlides(probe, pixels, ctx, rendered) {
48139
48862
  const l = logos[0];
48140
48863
  if (l) {
48141
48864
  if (l.box.h > 28) bad.push(`${slideName(s)} logo ${Math.round(l.box.h)} px tall`);
48142
- if (!inside(l.box, s.box, inset)) bad.push(`${slideName(s)} logo inside the safe band`);
48865
+ if (!inside2(l.box, s.box, inset)) bad.push(`${slideName(s)} logo inside the safe band`);
48143
48866
  } else if (byHook && ctx.logoSig) bad.push(`${slideName(s)} has a data-role="logo" element but not the stored logo's bytes (conformance)`);
48144
48867
  }
48145
48868
  out.push(graded(rule("S21"), bad.length === 0, bad.length ? fails3(bad) : "stored logo on every slide, \u226424 px, inside the safe zone", void 0, ctx.logoSig ? void 0 : 'No stored logo to match; only the data-role="logo" hook was available.'));
@@ -48699,20 +49422,20 @@ function checkConversion(p, ctx, rendered) {
48699
49422
  invented.push(`quote attributed to ${t.name} that they did not give`);
48700
49423
  }
48701
49424
  }
48702
- const digits2 = (x) => x.replace(/[,\s]/g, "");
49425
+ const digits = (x) => x.replace(/[,\s]/g, "");
48703
49426
  const STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "from", "our", "over", "of", "in", "on", "a", "an", "we", "us", "have", "been"]);
48704
49427
  const unitAfter = (text2, at) => norm4(text2.slice(at)).split(" ").slice(1).filter((w) => w && !STOP.has(w) && !/^[\d,.]+$/.test(w)).slice(0, 2);
48705
49428
  const briefFigures = b.proof.aggregates.flatMap((agg) => {
48706
49429
  const line3 = `${agg.value} ${agg.label}`;
48707
- return [...line3.matchAll(/\d[\d,.]*/g)].map((m) => ({ n: digits2(m[0]), units: /* @__PURE__ */ new Set([...unitAfter(line3, m.index ?? 0), ...norm4(agg.label).split(" ").filter((w) => w && !STOP.has(w))]) }));
49430
+ return [...line3.matchAll(/\d[\d,.]*/g)].map((m) => ({ n: digits(m[0]), units: /* @__PURE__ */ new Set([...unitAfter(line3, m.index ?? 0), ...norm4(agg.label).split(" ").filter((w) => w && !STOP.has(w))]) }));
48708
49431
  });
48709
49432
  const seenClaims = /* @__PURE__ */ new Set();
48710
49433
  const claimed = (text2) => {
48711
49434
  for (const m of text2.matchAll(/\d[\d,.]*/g)) {
48712
- const key = `${digits2(m[0])}@${norm4(text2).slice(0, 40)}`;
49435
+ const key = `${digits(m[0])}@${norm4(text2).slice(0, 40)}`;
48713
49436
  if (seenClaims.has(key)) continue;
48714
49437
  seenClaims.add(key);
48715
- const hit = briefFigures.find((f) => f.n === digits2(m[0]));
49438
+ const hit = briefFigures.find((f) => f.n === digits(m[0]));
48716
49439
  if (!hit) {
48717
49440
  invented.push(`figure "${m[0]}" in "${text2.slice(0, 30)}"`);
48718
49441
  continue;
@@ -49393,9 +50116,9 @@ async function settle(page) {
49393
50116
  }
49394
50117
  async function readTarget(target) {
49395
50118
  if (isUrl(target)) return { html: null, url: target };
49396
- const abs = path35.resolve(target);
49397
- if (!fs37.existsSync(abs)) throw new Error(`No file at ${abs}.`);
49398
- return { html: fs37.readFileSync(abs, "utf8"), url: (0, import_node_url.pathToFileURL)(abs).toString() };
50119
+ const abs = path36.resolve(target);
50120
+ if (!fs38.existsSync(abs)) throw new Error(`No file at ${abs}.`);
50121
+ return { html: fs38.readFileSync(abs, "utf8"), url: (0, import_node_url.pathToFileURL)(abs).toString() };
49399
50122
  }
49400
50123
  function slidePixels(index, png, file2, sec, brand) {
49401
50124
  let img;
@@ -49443,7 +50166,7 @@ async function verifyDesign(opts) {
49443
50166
  const { html: html2, url } = await readTarget(opts.target);
49444
50167
  const placement = opts.placement ?? "html";
49445
50168
  const medium = opts.medium;
49446
- fs37.mkdirSync(opts.outDir, { recursive: true });
50169
+ fs38.mkdirSync(opts.outDir, { recursive: true });
49447
50170
  const files = [];
49448
50171
  const screenshots = [];
49449
50172
  const at = (/* @__PURE__ */ new Date()).toISOString();
@@ -49488,8 +50211,8 @@ async function verifyDesign(opts) {
49488
50211
  try {
49489
50212
  await loc.scrollIntoViewIfNeeded({ timeout: 5e3 });
49490
50213
  const png = await loc.screenshot({ type: "png", timeout: 15e3 });
49491
- fs37.writeFileSync(path35.join(opts.outDir, file2), png);
49492
- files.push(path35.join(opts.outDir, file2));
50214
+ fs38.writeFileSync(path36.join(opts.outDir, file2), png);
50215
+ files.push(path36.join(opts.outDir, file2));
49493
50216
  screenshots.push(file2);
49494
50217
  pixels.push(slidePixels(sec.index, png, file2, sec, opts.brand));
49495
50218
  } catch {
@@ -49521,8 +50244,8 @@ async function verifyDesign(opts) {
49521
50244
  const shot = "page-1440.png";
49522
50245
  try {
49523
50246
  const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
49524
- fs37.writeFileSync(path35.join(opts.outDir, shot), png);
49525
- files.push(path35.join(opts.outDir, shot));
50247
+ fs38.writeFileSync(path36.join(opts.outDir, shot), png);
50248
+ files.push(path36.join(opts.outDir, shot));
49526
50249
  screenshots.push(shot);
49527
50250
  } catch {
49528
50251
  }
@@ -49530,8 +50253,8 @@ async function verifyDesign(opts) {
49530
50253
  await page.setViewportSize({ width: 390, height: 844 });
49531
50254
  await page.evaluate(SETTLE_SCROLL_JS).catch(() => void 0);
49532
50255
  const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
49533
- fs37.writeFileSync(path35.join(opts.outDir, "page-390.png"), png);
49534
- files.push(path35.join(opts.outDir, "page-390.png"));
50256
+ fs38.writeFileSync(path36.join(opts.outDir, "page-390.png"), png);
50257
+ files.push(path36.join(opts.outDir, "page-390.png"));
49535
50258
  screenshots.push("page-390.png");
49536
50259
  } catch {
49537
50260
  }
@@ -49604,21 +50327,21 @@ async function verifyDesign(opts) {
49604
50327
  screenshots
49605
50328
  };
49606
50329
  const checked = VerificationSchema.parse(report2);
49607
- const jsonPath = path35.join(opts.outDir, "verification.json");
49608
- fs37.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
50330
+ const jsonPath = path36.join(opts.outDir, "verification.json");
50331
+ fs38.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
49609
50332
  files.push(jsonPath);
49610
50333
  const reportHtml = renderVerificationHtml(checked, opts.branding);
49611
- const htmlPath = path35.join(opts.outDir, "verification.html");
49612
- fs37.writeFileSync(htmlPath, reportHtml);
50334
+ const htmlPath = path36.join(opts.outDir, "verification.html");
50335
+ fs38.writeFileSync(htmlPath, reportHtml);
49613
50336
  files.push(htmlPath);
49614
50337
  return { report: checked, files, reportHtml };
49615
50338
  }
49616
- var fs37, path35, import_node_url, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS;
50339
+ var fs38, path36, import_node_url, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS;
49617
50340
  var init_verify = __esm({
49618
50341
  "src/command-os/design/verify.ts"() {
49619
50342
  "use strict";
49620
- fs37 = __toESM(require("node:fs"));
49621
- path35 = __toESM(require("node:path"));
50343
+ fs38 = __toESM(require("node:fs"));
50344
+ path36 = __toESM(require("node:path"));
49622
50345
  import_node_url = require("node:url");
49623
50346
  init_lint();
49624
50347
  init_probe();
@@ -50037,7 +50760,7 @@ function registerDesignStudioTools(server2) {
50037
50760
  const { dna, logoSvg, logoPng } = deriveBrandDna(raw, { name, slug: slug3, rights: { declaration: a.rights, at: (/* @__PURE__ */ new Date()).toISOString() }, browser, firecrawl: fc });
50038
50761
  if (a.rights === "none" && dna.logo) notes.push("Rights declared as none: the logo is stored for reference only; do not place it on a client-facing page.");
50039
50762
  const written = writeBrand(dna, { logoSvg, logoPng, screenshotPng: browser?.screenshotPng });
50040
- return { ok: true, brand: summarise2(dna), passes: { static: true, browser: Boolean(browser), firecrawl: Boolean(fc) }, spaShell: raw.spaShell, dir: written.dir, files: written.files.map((f) => path36.basename(f)), notes };
50763
+ return { ok: true, brand: summarise2(dna), passes: { static: true, browser: Boolean(browser), firecrawl: Boolean(fc) }, spaShell: raw.spaShell, dir: written.dir, files: written.files.map((f) => path37.basename(f)), notes };
50041
50764
  }
50042
50765
  );
50043
50766
  safeTool(
@@ -50141,14 +50864,14 @@ function registerDesignStudioTools(server2) {
50141
50864
  d.provenance.evidence["shape.radius"] = `set by ${stamp}`;
50142
50865
  }
50143
50866
  if (a.logoSvg) {
50144
- let clean;
50867
+ let clean2;
50145
50868
  try {
50146
- clean = sanitizeSvg(a.logoSvg);
50869
+ clean2 = sanitizeSvg(a.logoSvg);
50147
50870
  } catch (e) {
50148
50871
  throw new Error(`${e.message} Nothing was changed.`);
50149
50872
  }
50150
- a.logoSvg = clean;
50151
- d.logo = { inlineSvg: clean, whiteFillFixed: false, source: `set by ${stamp}`, rights: "declared" };
50873
+ a.logoSvg = clean2;
50874
+ d.logo = { inlineSvg: clean2, whiteFillFixed: false, source: `set by ${stamp}`, rights: "declared" };
50152
50875
  d.provenance.evidence["logo"] = `set by ${stamp}`;
50153
50876
  }
50154
50877
  if (a.tone) d.voice.tone = a.tone;
@@ -50204,9 +50927,9 @@ function registerDesignStudioTools(server2) {
50204
50927
  "Check a written HTML file honours the output contract before grading: section and role markers present and consistent, no outside scripts, one h1 on a page, every image described. Deterministic; no browser. A file that fails this is not graded by verify_design.",
50205
50928
  { file: import_zod76.z.string().describe("Path to the .html file."), medium: import_zod76.z.enum(MEDIA) },
50206
50929
  async ({ file: file2, medium }) => {
50207
- const abs = path36.resolve(file2);
50208
- if (!fs38.existsSync(abs)) throw new Error(`No file at ${abs}.`);
50209
- const r = lintDesignContract(fs38.readFileSync(abs, "utf8"), medium);
50930
+ const abs = path37.resolve(file2);
50931
+ if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
50932
+ const r = lintDesignContract(fs39.readFileSync(abs, "utf8"), medium);
50210
50933
  return { ok: r.ok, problems: r.problems, warnings: r.warnings, facts: r.facts };
50211
50934
  }
50212
50935
  );
@@ -50227,7 +50950,7 @@ function registerDesignStudioTools(server2) {
50227
50950
  async (a) => {
50228
50951
  const brand = a.brandSlug ? houseLooks().find((d) => d.slug === a.brandSlug) ?? readBrand(slugify(a.brandSlug)) : void 0;
50229
50952
  const isUrl2 = /^https?:\/\//i.test(a.target);
50230
- const outDir = a.outDir ?? (isUrl2 ? path36.resolve(`verify-${new URL(a.target).hostname.replace(/[^a-z0-9.-]/gi, "_")}`) : path36.join(path36.dirname(path36.resolve(a.target)), `verify-${path36.basename(a.target, ".html")}`));
50953
+ const outDir = a.outDir ?? (isUrl2 ? path37.resolve(`verify-${new URL(a.target).hostname.replace(/[^a-z0-9.-]/gi, "_")}`) : path37.join(path37.dirname(path37.resolve(a.target)), `verify-${path37.basename(a.target, ".html")}`));
50231
50954
  const r = await verifyDesign({ target: a.target, medium: a.medium, brand, outDir, placement: a.placement, mode: a.mode, ratio: a.ratio, logo: a.logo, branding: readAgencyBranding() });
50232
50955
  const v = r.report;
50233
50956
  const failed = v.rules.filter((x) => x.state === "MEASURED-FAIL").map((x) => `${x.ruleId} ${x.name}: ${x.measured ?? ""}`);
@@ -50242,7 +50965,7 @@ function registerDesignStudioTools(server2) {
50242
50965
  judgments: v.rules.filter((x) => x.state === "ASSERTED").map((x) => `${x.ruleId} ${x.name}: ${x.detail ?? ""}`),
50243
50966
  notApplicable: v.rules.filter((x) => x.state === "N-A").map((x) => `${x.ruleId}: ${x.detail ?? ""}`),
50244
50967
  brandMatch: v.brandMatch,
50245
- report: path36.join(outDir, "verification.html"),
50968
+ report: path37.join(outDir, "verification.html"),
50246
50969
  files: r.files,
50247
50970
  ...a.placement === "ghl" && { builderOwned: builderOwnedRules().map((b) => `${b.rule.id} ${b.rule.name}: ${b.why}`) }
50248
50971
  };
@@ -50270,9 +50993,9 @@ function registerDesignStudioTools(server2) {
50270
50993
  if (!a.pageDataDownloadUrl && !a.file) throw new Error("Pass pageDataDownloadUrl (from get_page_full) or file.");
50271
50994
  let page;
50272
50995
  if (a.file) {
50273
- const abs = path36.resolve(a.file);
50274
- if (!fs38.existsSync(abs)) throw new Error(`No file at ${abs}.`);
50275
- page = JSON.parse(fs38.readFileSync(abs, "utf8"));
50996
+ const abs = path37.resolve(a.file);
50997
+ if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
50998
+ page = JSON.parse(fs39.readFileSync(abs, "utf8"));
50276
50999
  } else {
50277
51000
  const response = await fetch(a.pageDataDownloadUrl, { headers: { Accept: "application/json,*/*" } });
50278
51001
  if (!response.ok) throw new Error(`Could not download the page data: HTTP ${response.status}. Re-read get_page_full \u2014 the download URL expires.`);
@@ -50313,9 +51036,9 @@ function registerDesignStudioTools(server2) {
50313
51036
  if (!a.file && !a.text) throw new Error("Pass file or text.");
50314
51037
  let html2 = a.text ?? "";
50315
51038
  if (a.file) {
50316
- const abs = path36.resolve(a.file);
50317
- if (!fs38.existsSync(abs)) throw new Error(`No file at ${abs}.`);
50318
- html2 = fs38.readFileSync(abs, "utf8");
51039
+ const abs = path37.resolve(a.file);
51040
+ if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
51041
+ html2 = fs39.readFileSync(abs, "utf8");
50319
51042
  }
50320
51043
  const r = await gradeCopy(html2, claudeGenerate(a.model), { icp: a.icp });
50321
51044
  if (!r.grade) {
@@ -50350,13 +51073,13 @@ function registerDesignStudioTools(server2) {
50350
51073
  }
50351
51074
  );
50352
51075
  }
50353
- var import_zod76, fs38, path36, summarise2;
51076
+ var import_zod76, fs39, path37, summarise2;
50354
51077
  var init_design_studio = __esm({
50355
51078
  "src/tools/design-studio.ts"() {
50356
51079
  "use strict";
50357
51080
  import_zod76 = require("zod");
50358
- fs38 = __toESM(require("node:fs"));
50359
- path36 = __toESM(require("node:path"));
51081
+ fs39 = __toESM(require("node:fs"));
51082
+ path37 = __toESM(require("node:path"));
50360
51083
  init_tool_helpers();
50361
51084
  init_extract();
50362
51085
  init_extract_browser();
@@ -51080,11 +51803,11 @@ var init_audit = __esm({
51080
51803
  // src/command-os/delivery-audit/store.ts
51081
51804
  function deliveryAuditPath() {
51082
51805
  const override = process.env[DELIVERY_AUDIT_ENV];
51083
- return override && override.trim() ? override : path37.join(appDataDir(), "delivery-audit.json");
51806
+ return override && override.trim() ? override : path38.join(appDataDir(), "delivery-audit.json");
51084
51807
  }
51085
51808
  function readDeliveryAudit() {
51086
51809
  try {
51087
- const parsed2 = JSON.parse(fs39.readFileSync(deliveryAuditPath(), "utf-8"));
51810
+ const parsed2 = JSON.parse(fs40.readFileSync(deliveryAuditPath(), "utf-8"));
51088
51811
  if (!parsed2 || typeof parsed2 !== "object" || !parsed2.answers || typeof parsed2.answers !== "object") return { ...EMPTY3 };
51089
51812
  return { v: 1, answers: parsed2.answers, updated_at: parsed2.updated_at };
51090
51813
  } catch {
@@ -51095,7 +51818,7 @@ function readForWrite2() {
51095
51818
  const target = deliveryAuditPath();
51096
51819
  let raw;
51097
51820
  try {
51098
- raw = fs39.readFileSync(target, "utf-8");
51821
+ raw = fs40.readFileSync(target, "utf-8");
51099
51822
  } catch (e) {
51100
51823
  if (e?.code === "ENOENT") return { ...EMPTY3 };
51101
51824
  throw new Error("Your saved audit could not be read, so nothing was changed. Nothing was lost.");
@@ -51107,7 +51830,7 @@ function readForWrite2() {
51107
51830
  } catch {
51108
51831
  const kept = `${target}.unreadable-${Date.now()}`;
51109
51832
  try {
51110
- fs39.copyFileSync(target, kept);
51833
+ fs40.copyFileSync(target, kept);
51111
51834
  } catch {
51112
51835
  }
51113
51836
  throw new Error(`Your saved audit is damaged, so nothing was written over it. A copy is at ${kept}. Start again with reset:true when you are ready.`);
@@ -51119,13 +51842,13 @@ function writeFile2(f) {
51119
51842
  const tmp = `${target}.${process.pid}.tmp`;
51120
51843
  let renamed = false;
51121
51844
  try {
51122
- fs39.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
51123
- fs39.renameSync(tmp, target);
51845
+ fs40.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
51846
+ fs40.renameSync(tmp, target);
51124
51847
  renamed = true;
51125
51848
  } finally {
51126
51849
  if (!renamed) {
51127
51850
  try {
51128
- fs39.unlinkSync(tmp);
51851
+ fs40.unlinkSync(tmp);
51129
51852
  } catch {
51130
51853
  }
51131
51854
  }
@@ -51146,12 +51869,12 @@ function resetDeliveryAudit(now = () => /* @__PURE__ */ new Date()) {
51146
51869
  writeFile2(next);
51147
51870
  return next;
51148
51871
  }
51149
- var fs39, path37, EMPTY3, DELIVERY_AUDIT_ENV;
51872
+ var fs40, path38, EMPTY3, DELIVERY_AUDIT_ENV;
51150
51873
  var init_store4 = __esm({
51151
51874
  "src/command-os/delivery-audit/store.ts"() {
51152
51875
  "use strict";
51153
- fs39 = __toESM(require("node:fs"));
51154
- path37 = __toESM(require("node:path"));
51876
+ fs40 = __toESM(require("node:fs"));
51877
+ path38 = __toESM(require("node:path"));
51155
51878
  init_credentials_store();
51156
51879
  EMPTY3 = { v: 1, answers: {} };
51157
51880
  DELIVERY_AUDIT_ENV = "GHL_MCP_DELIVERY_AUDIT";
@@ -51472,7 +52195,7 @@ function position(a) {
51472
52195
  const cyclic = a.steps.length > 0 && check.order.length === 0;
51473
52196
  const order = check.order.length ? check.order : a.steps.map((s) => s.id);
51474
52197
  const lastAttempt = /* @__PURE__ */ new Map();
51475
- for (const x of [...a.applied].sort((p, q2) => p.at < q2.at ? -1 : p.at > q2.at ? 1 : 0)) {
52198
+ for (const x of [...a.applied].sort((p, q3) => p.at < q3.at ? -1 : p.at > q3.at ? 1 : 0)) {
51476
52199
  lastAttempt.set(x.stepId, x);
51477
52200
  }
51478
52201
  const doneIds = new Set([...lastAttempt.values()].filter((x) => x.state === "applied").map((x) => x.stepId));
@@ -51515,7 +52238,7 @@ function position(a) {
51515
52238
  line3 = `STALLED before it started: this plan's steps wait on each other (${names}), so there is no order that works. It cannot be run as it stands and needs planning again.`;
51516
52239
  } else if (failed.length) {
51517
52240
  phase = "stalled";
51518
- const f = failed.reduce((p, q2) => q2.at > p.at ? q2 : p);
52241
+ const f = failed.reduce((p, q3) => q3.at > p.at ? q3 : p);
51519
52242
  stalledOn = { step: f.stepId, because: f.because ?? "no reason was recorded" };
51520
52243
  const rest = total - done - 1;
51521
52244
  line3 = `STALLED on ${f.stepId} (${byId2.get(f.stepId)?.why ?? "step"}): ${f.because ?? "no reason was recorded"}. ${done} of ${total} steps are done and the remaining ${rest === 1 ? "step is" : `${rest} steps are`} on hold \u2014 nothing else runs until this one is sorted, even the steps that do not depend on it, because the plan's order is part of what was approved. Fix what went wrong and record ${f.stepId} again with retry, or abandon the build.`;
@@ -51607,7 +52330,7 @@ var init_automation = __esm({
51607
52330
  // src/command-os/build/store.ts
51608
52331
  function automationsPath() {
51609
52332
  const override = process.env[AUTOMATIONS_ENV];
51610
- return override && override.trim() ? override : path38.join(appDataDir(), "automations.json");
52333
+ return override && override.trim() ? override : path39.join(appDataDir(), "automations.json");
51611
52334
  }
51612
52335
  function parse4(raw) {
51613
52336
  const p = JSON.parse(raw);
@@ -51616,7 +52339,7 @@ function parse4(raw) {
51616
52339
  }
51617
52340
  function readAutomations() {
51618
52341
  try {
51619
- return parse4(fs40.readFileSync(automationsPath(), "utf-8")).builds;
52342
+ return parse4(fs41.readFileSync(automationsPath(), "utf-8")).builds;
51620
52343
  } catch {
51621
52344
  return { ...EMPTY4.builds };
51622
52345
  }
@@ -51625,7 +52348,7 @@ function readForWrite3() {
51625
52348
  const target = automationsPath();
51626
52349
  let raw;
51627
52350
  try {
51628
- raw = fs40.readFileSync(target, "utf-8");
52351
+ raw = fs41.readFileSync(target, "utf-8");
51629
52352
  } catch (e) {
51630
52353
  if (e?.code === "ENOENT") return { v: 1, builds: {} };
51631
52354
  throw new Error("Your builds file could not be read, so nothing was changed. Nothing was lost.");
@@ -51635,7 +52358,7 @@ function readForWrite3() {
51635
52358
  } catch {
51636
52359
  const kept = `${target}.unreadable-${Date.now()}`;
51637
52360
  try {
51638
- fs40.copyFileSync(target, kept);
52361
+ fs41.copyFileSync(target, kept);
51639
52362
  } catch {
51640
52363
  }
51641
52364
  throw new Error(
@@ -51649,13 +52372,13 @@ function writeFile3(f) {
51649
52372
  const tmp = `${target}.${process.pid}.tmp`;
51650
52373
  let renamed = false;
51651
52374
  try {
51652
- fs40.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
51653
- fs40.renameSync(tmp, target);
52375
+ fs41.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
52376
+ fs41.renameSync(tmp, target);
51654
52377
  renamed = true;
51655
52378
  } finally {
51656
52379
  if (!renamed) {
51657
52380
  try {
51658
- fs40.unlinkSync(tmp);
52381
+ fs41.unlinkSync(tmp);
51659
52382
  } catch {
51660
52383
  }
51661
52384
  }
@@ -51681,12 +52404,12 @@ function newAutomationId(now = () => /* @__PURE__ */ new Date()) {
51681
52404
  }
51682
52405
  return id;
51683
52406
  }
51684
- var fs40, path38, AUTOMATIONS_ENV, EMPTY4;
52407
+ var fs41, path39, AUTOMATIONS_ENV, EMPTY4;
51685
52408
  var init_store5 = __esm({
51686
52409
  "src/command-os/build/store.ts"() {
51687
52410
  "use strict";
51688
- fs40 = __toESM(require("node:fs"));
51689
- path38 = __toESM(require("node:path"));
52411
+ fs41 = __toESM(require("node:fs"));
52412
+ path39 = __toESM(require("node:path"));
51690
52413
  init_credentials_store();
51691
52414
  AUTOMATIONS_ENV = "GHL_MCP_AUTOMATIONS";
51692
52415
  EMPTY4 = { v: 1, builds: {} };
@@ -52220,15 +52943,15 @@ var init_agency_build = __esm({
52220
52943
 
52221
52944
  // src/tools/client-engagements.ts
52222
52945
  function resolveClient(who, roster) {
52223
- const q2 = who.trim();
52224
- if (!q2) return { error: "Name who you mean \u2014 either the way you refer to them, or their account id." };
52225
- const byId2 = roster.find((r) => r.locationId === q2);
52946
+ const q3 = who.trim();
52947
+ if (!q3) return { error: "Name who you mean \u2014 either the way you refer to them, or their account id." };
52948
+ const byId2 = roster.find((r) => r.locationId === q3);
52226
52949
  if (byId2) return { locationId: byId2.locationId, name: byId2.name, onTheBoard: true };
52227
- const hits = roster.filter((r) => r.name?.trim().toLowerCase() === q2.toLowerCase());
52228
- const loose2 = hits.length ? hits : roster.filter((r) => r.name?.toLowerCase().includes(q2.toLowerCase()));
52950
+ const hits = roster.filter((r) => r.name?.trim().toLowerCase() === q3.toLowerCase());
52951
+ const loose2 = hits.length ? hits : roster.filter((r) => r.name?.toLowerCase().includes(q3.toLowerCase()));
52229
52952
  if (loose2.length === 1) return { locationId: loose2[0].locationId, name: loose2[0].name, onTheBoard: true };
52230
- if (loose2.length > 1) return { error: `More than one match for "${q2}": ${loose2.map((r) => r.name).join(", ")}. Use the full name.` };
52231
- return { error: `Nothing called "${q2}" is on your board or in your records.` };
52953
+ if (loose2.length > 1) return { error: `More than one match for "${q3}": ${loose2.map((r) => r.name).join(", ")}. Use the full name.` };
52954
+ return { error: `Nothing called "${q3}" is on your board or in your records.` };
52232
52955
  }
52233
52956
  function line2(locationId2, e, roster, profile) {
52234
52957
  const boardName = roster.find((r) => r.locationId === locationId2)?.name;
@@ -52483,9 +53206,9 @@ function looksLikeRawId(word) {
52483
53206
  if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(word)) return true;
52484
53207
  if (/^[0-9a-f]{24}$/.test(word)) return true;
52485
53208
  if (!/^[A-Za-z0-9]{17,}$/.test(word)) return false;
52486
- const digits2 = (word.match(/\d/g) ?? []).length;
53209
+ const digits = (word.match(/\d/g) ?? []).length;
52487
53210
  const letters = (word.match(/[A-Za-z]/g) ?? []).length;
52488
- if (digits2 < 2 || letters < 2) return false;
53211
+ if (digits < 2 || letters < 2) return false;
52489
53212
  const longestLetterRun = Math.max(0, ...(word.match(/[a-z]+|[A-Z]+/g) ?? []).map((r) => r.length));
52490
53213
  return longestLetterRun <= 3;
52491
53214
  }
@@ -53532,7 +54255,7 @@ function planRevert(record, opts) {
53532
54255
  function errText(e) {
53533
54256
  return e instanceof Error ? e.message : String(e);
53534
54257
  }
53535
- async function executeRevert(plan, deps, wait = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms))) {
54258
+ async function executeRevert(plan, deps, wait = (ms) => new Promise((resolve13) => setTimeout(resolve13, ms))) {
53536
54259
  const removed = [];
53537
54260
  const refused = [];
53538
54261
  const attempted = [];
@@ -53651,8 +54374,8 @@ function renderDryRun(plan, opts = {}) {
53651
54374
  const children2 = withParent.filter((w) => w.object.parentRef && g.items.some((i) => i.object.ref === w.object.parentRef));
53652
54375
  if (children2.length) {
53653
54376
  const kind = children2[0].object.type;
53654
- const inside2 = g.items.length === 1 ? "inside it" : "inside them";
53655
- L.push(` (plus the ${children2.length} ${PLURAL[kind].toLowerCase()} ${inside2}, which GoHighLevel removes at the same time)`);
54377
+ const inside3 = g.items.length === 1 ? "inside it" : "inside them";
54378
+ L.push(` (plus the ${children2.length} ${PLURAL[kind].toLowerCase()} ${inside3}, which GoHighLevel removes at the same time)`);
53656
54379
  }
53657
54380
  }
53658
54381
  const kept = [...bound, ...cannot, ...gone];
@@ -54845,11 +55568,11 @@ function extractFormFields(formFull) {
54845
55568
  const fields = Array.isArray(inner.fields) ? inner.fields : [];
54846
55569
  return fields.filter((f) => !!f && typeof f === "object");
54847
55570
  }
54848
- function findRecordForQuestion(q2, records) {
54849
- const wantKey = expectedFieldKey(q2).toLowerCase();
55571
+ function findRecordForQuestion(q3, records) {
55572
+ const wantKey = expectedFieldKey(q3).toLowerCase();
54850
55573
  const byKey = records.find((r) => r.fieldKey.toLowerCase() === wantKey);
54851
55574
  if (byKey) return byKey;
54852
- const wantName = intakeFieldName(q2.label).toLowerCase();
55575
+ const wantName = intakeFieldName(q3.label).toLowerCase();
54853
55576
  return records.find((r) => r.name.toLowerCase() === wantName);
54854
55577
  }
54855
55578
  function isFormNotYetPropagated(error) {
@@ -55074,13 +55797,13 @@ ${text3}`);
55074
55797
  };
55075
55798
  }
55076
55799
  function makePipelineApi(builderClient) {
55077
- return async (method, path41, body) => {
55078
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path41}`;
55800
+ return async (method, path42, body) => {
55801
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path42}`;
55079
55802
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
55080
55803
  const response = await builderRequest(builderClient, method, url, sendsBody ? { body } : {});
55081
55804
  if (!response.ok) {
55082
55805
  const text3 = await response.text();
55083
- throw new Error(`Pipeline API ${response.status}: ${method} ${path41}
55806
+ throw new Error(`Pipeline API ${response.status}: ${method} ${path42}
55084
55807
  ${text3}`);
55085
55808
  }
55086
55809
  const text2 = await response.text();
@@ -55093,8 +55816,8 @@ ${text3}`);
55093
55816
  };
55094
55817
  }
55095
55818
  function makeFunnelApi(builderClient) {
55096
- return async (method, path41, body) => {
55097
- const url = `https://backend.leadconnectorhq.com/funnels${path41}`;
55819
+ return async (method, path42, body) => {
55820
+ const url = `https://backend.leadconnectorhq.com/funnels${path42}`;
55098
55821
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
55099
55822
  const response = await builderRequest(builderClient, method, url, {
55100
55823
  ...sendsBody ? { body } : {},
@@ -55107,7 +55830,7 @@ function makeFunnelApi(builderClient) {
55107
55830
  });
55108
55831
  if (!response.ok) {
55109
55832
  const text3 = await response.text();
55110
- throw new Error(`Funnel API ${response.status}: ${method} ${path41}
55833
+ throw new Error(`Funnel API ${response.status}: ${method} ${path42}
55111
55834
  ${text3.slice(0, 300)}`);
55112
55835
  }
55113
55836
  const text2 = await response.text();
@@ -55474,17 +56197,17 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
55474
56197
  industry: industryPack(industry)?.slug,
55475
56198
  intake: describeComposition({ industry, agency }),
55476
56199
  count: questions.length,
55477
- questions: questions.map((q2) => ({
55478
- key: q2.key,
55479
- label: q2.label,
55480
- section: q2.sectionLabel,
55481
- required: q2.required,
55482
- fieldKind: q2.field.kind,
55483
- dataType: q2.field.kind === "custom" ? q2.field.dataType : void 0,
55484
- standardTag: q2.field.kind === "standard" ? q2.field.tag : void 0,
55485
- expectedFieldKey: q2.field.kind === "custom" ? expectedFieldKey(q2) : void 0,
55486
- options: q2.options,
55487
- briefPath: q2.briefPath
56200
+ questions: questions.map((q3) => ({
56201
+ key: q3.key,
56202
+ label: q3.label,
56203
+ section: q3.sectionLabel,
56204
+ required: q3.required,
56205
+ fieldKind: q3.field.kind,
56206
+ dataType: q3.field.kind === "custom" ? q3.field.dataType : void 0,
56207
+ standardTag: q3.field.kind === "standard" ? q3.field.tag : void 0,
56208
+ expectedFieldKey: q3.field.kind === "custom" ? expectedFieldKey(q3) : void 0,
56209
+ options: q3.options,
56210
+ briefPath: q3.briefPath
55488
56211
  }))
55489
56212
  };
55490
56213
  }
@@ -56088,26 +56811,26 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
56088
56811
  if (!builderClient) return;
56089
56812
  const bc = builderClient;
56090
56813
  async function resolveCustomFields(locationId2, dryRun, questions) {
56091
- const cqs = questions.filter((q2) => q2.field.kind === "custom");
56814
+ const cqs = questions.filter((q3) => q3.field.kind === "custom");
56092
56815
  const existing = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
56093
56816
  const created = [];
56094
56817
  const reused = [];
56095
- for (const q2 of cqs) {
56096
- const found = findRecordForQuestion(q2, existing);
56818
+ for (const q3 of cqs) {
56819
+ const found = findRecordForQuestion(q3, existing);
56097
56820
  if (found) {
56098
- reused.push(q2.label);
56821
+ reused.push(q3.label);
56099
56822
  } else {
56100
- created.push(q2.label);
56823
+ created.push(q3.label);
56101
56824
  if (!dryRun) {
56102
56825
  const body = {
56103
- name: intakeFieldName(q2.label),
56104
- dataType: q2.field.kind === "custom" ? q2.field.dataType : "TEXT",
56826
+ name: intakeFieldName(q3.label),
56827
+ dataType: q3.field.kind === "custom" ? q3.field.dataType : "TEXT",
56105
56828
  model: "contact"
56106
56829
  };
56107
- if (q2.options && q2.field.kind === "custom") {
56108
- const dt = q2.field.dataType;
56830
+ if (q3.options && q3.field.kind === "custom") {
56831
+ const dt = q3.field.dataType;
56109
56832
  if (dt === "SINGLE_OPTIONS" || dt === "MULTIPLE_OPTIONS" || dt === "CHECKBOX") {
56110
- body.options = [...q2.options];
56833
+ body.options = [...q3.options];
56111
56834
  }
56112
56835
  }
56113
56836
  await client.post(`/locations/${locationId2}/customFields`, { body });
@@ -56121,13 +56844,13 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
56121
56844
  const after = parseCustomFields(await client.get(`/locations/${locationId2}/customFields`));
56122
56845
  resolved2.clear();
56123
56846
  missing = void 0;
56124
- for (const q2 of cqs) {
56125
- const rec = findRecordForQuestion(q2, after);
56847
+ for (const q3 of cqs) {
56848
+ const rec = findRecordForQuestion(q3, after);
56126
56849
  if (!rec) {
56127
- missing = q2;
56850
+ missing = q3;
56128
56851
  break;
56129
56852
  }
56130
- resolved2.set(q2.key, rec);
56853
+ resolved2.set(q3.key, rec);
56131
56854
  }
56132
56855
  if (!missing) break;
56133
56856
  if (attempt < 6) await sleep6(700 * attempt);
@@ -56177,7 +56900,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
56177
56900
  industry: pack?.slug,
56178
56901
  intake,
56179
56902
  questionCount: questions.length,
56180
- standardFields: questions.filter((q2) => q2.field.kind === "standard").map((q2) => q2.label),
56903
+ standardFields: questions.filter((q3) => q3.field.kind === "standard").map((q3) => q3.label),
56181
56904
  customFieldsToCreate: created,
56182
56905
  customFieldsToReuse: reused,
56183
56906
  note: "No changes written. Re-run without dryRun to install."
@@ -56422,7 +57145,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env2 = process
56422
57145
  return findings.map((finding) => ({ finding, because: `"${finding.id}" cannot be checked: no saved key for sub-account ${loc}. Register it (register_location) or point the server at it first.` }));
56423
57146
  }
56424
57147
  const reader = token2 ? new GHLClient({ apiKey: token2.apiKey, locationId: loc }) : client;
56425
- return refuseUnseenFindings2((path41, params) => reader.get(path41, params ? { params } : void 0), loc, findings);
57148
+ return refuseUnseenFindings2((path42, params) => reader.get(path42, params ? { params } : void 0), loc, findings);
56426
57149
  }, () => client.defaultLocationId);
56427
57150
  registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
56428
57151
  registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
@@ -56842,8 +57565,8 @@ __export(index_exports, {
56842
57565
  });
56843
57566
  module.exports = __toCommonJS(index_exports);
56844
57567
  var dotenv2 = __toESM(require("dotenv"));
56845
- var path40 = __toESM(require("path"));
56846
- var fs42 = __toESM(require("fs"));
57568
+ var path41 = __toESM(require("path"));
57569
+ var fs43 = __toESM(require("fs"));
56847
57570
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
56848
57571
 
56849
57572
  // src/version-compare.ts
@@ -56915,8 +57638,8 @@ var import_zod87 = require("zod");
56915
57638
 
56916
57639
  // src/skill-installer.ts
56917
57640
  var import_node_crypto13 = require("node:crypto");
56918
- var fs41 = __toESM(require("node:fs"));
56919
- var path39 = __toESM(require("node:path"));
57641
+ var fs42 = __toESM(require("node:fs"));
57642
+ var path40 = __toESM(require("node:path"));
56920
57643
  var os7 = __toESM(require("node:os"));
56921
57644
  function sha2562(buf) {
56922
57645
  return (0, import_node_crypto13.createHash)("sha256").update(buf).digest("hex");
@@ -56931,9 +57654,9 @@ function isOlderRelease(candidate, installed) {
56931
57654
  return false;
56932
57655
  }
56933
57656
  function bundledSkillsDir(baseDir) {
56934
- const candidate = path39.resolve(baseDir, "..", "skills");
57657
+ const candidate = path40.resolve(baseDir, "..", "skills");
56935
57658
  try {
56936
- return fs41.statSync(candidate).isDirectory() ? candidate : null;
57659
+ return fs42.statSync(candidate).isDirectory() ? candidate : null;
56937
57660
  } catch {
56938
57661
  return null;
56939
57662
  }
@@ -56941,10 +57664,10 @@ function bundledSkillsDir(baseDir) {
56941
57664
  function listFiles(root) {
56942
57665
  const out = [];
56943
57666
  const walk = (dir) => {
56944
- for (const entry of fs41.readdirSync(dir, { withFileTypes: true })) {
56945
- const full = path39.join(dir, entry.name);
57667
+ for (const entry of fs42.readdirSync(dir, { withFileTypes: true })) {
57668
+ const full = path40.join(dir, entry.name);
56946
57669
  if (entry.isDirectory()) walk(full);
56947
- else if (entry.isFile()) out.push(path39.relative(root, full));
57670
+ else if (entry.isFile()) out.push(path40.relative(root, full));
56948
57671
  }
56949
57672
  };
56950
57673
  walk(root);
@@ -56952,7 +57675,7 @@ function listFiles(root) {
56952
57675
  }
56953
57676
  function readMarker(markerPath) {
56954
57677
  try {
56955
- const parsed2 = JSON.parse(fs41.readFileSync(markerPath, "utf8"));
57678
+ const parsed2 = JSON.parse(fs42.readFileSync(markerPath, "utf8"));
56956
57679
  return parsed2 && typeof parsed2 === "object" && parsed2.files ? parsed2 : null;
56957
57680
  } catch {
56958
57681
  return null;
@@ -56966,35 +57689,35 @@ function installBundledSkills(opts) {
56966
57689
  skippedOlderPackage: [],
56967
57690
  unchanged: [],
56968
57691
  errors: [],
56969
- targetDir: opts.targetDir ?? path39.join(os7.homedir(), ".claude", "skills"),
57692
+ targetDir: opts.targetDir ?? path40.join(os7.homedir(), ".claude", "skills"),
56970
57693
  bundledDir: null
56971
57694
  };
56972
57695
  const bundled = bundledSkillsDir(opts.baseDir);
56973
57696
  result.bundledDir = bundled;
56974
57697
  if (!bundled) return result;
56975
57698
  try {
56976
- fs41.mkdirSync(result.targetDir, { recursive: true });
57699
+ fs42.mkdirSync(result.targetDir, { recursive: true });
56977
57700
  } catch (e) {
56978
57701
  result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
56979
57702
  return result;
56980
57703
  }
56981
- const markerPath = path39.join(result.targetDir, ".ghl-command-skills.json");
57704
+ const markerPath = path40.join(result.targetDir, ".ghl-command-skills.json");
56982
57705
  const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
56983
57706
  const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
56984
57707
  for (const rel of listFiles(bundled)) {
56985
57708
  try {
56986
- const src = fs41.readFileSync(path39.join(bundled, rel));
56987
- const dest = path39.join(result.targetDir, rel);
57709
+ const src = fs42.readFileSync(path40.join(bundled, rel));
57710
+ const dest = path40.join(result.targetDir, rel);
56988
57711
  const srcHash = sha2562(src);
56989
57712
  let destBuf = null;
56990
57713
  try {
56991
- destBuf = fs41.readFileSync(dest);
57714
+ destBuf = fs42.readFileSync(dest);
56992
57715
  } catch {
56993
57716
  destBuf = null;
56994
57717
  }
56995
57718
  if (destBuf === null) {
56996
- fs41.mkdirSync(path39.dirname(dest), { recursive: true });
56997
- fs41.writeFileSync(dest, src);
57719
+ fs42.mkdirSync(path40.dirname(dest), { recursive: true });
57720
+ fs42.writeFileSync(dest, src);
56998
57721
  newMarker.files[rel] = srcHash;
56999
57722
  result.installed.push(rel);
57000
57723
  continue;
@@ -57011,7 +57734,7 @@ function installBundledSkills(opts) {
57011
57734
  newMarker.packageVersion = marker.packageVersion;
57012
57735
  result.skippedOlderPackage.push(rel);
57013
57736
  } else if (lastWritten && destHash === lastWritten) {
57014
- fs41.writeFileSync(dest, src);
57737
+ fs42.writeFileSync(dest, src);
57015
57738
  newMarker.files[rel] = srcHash;
57016
57739
  result.updated.push(rel);
57017
57740
  } else {
@@ -57022,7 +57745,7 @@ function installBundledSkills(opts) {
57022
57745
  }
57023
57746
  }
57024
57747
  try {
57025
- fs41.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
57748
+ fs42.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
57026
57749
  } catch (e) {
57027
57750
  result.errors.push(`marker write failed: ${String(e)}`);
57028
57751
  }
@@ -57178,7 +57901,7 @@ dotenv2.config();
57178
57901
  setPkgVersion(pkg.version);
57179
57902
  {
57180
57903
  const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
57181
- if (configDirOverride && !path40.isAbsolute(configDirOverride)) {
57904
+ if (configDirOverride && !path41.isAbsolute(configDirOverride)) {
57182
57905
  process.stderr.write(
57183
57906
  `[ghl-mcp] GHL_MCP_CONFIG_DIR must be an absolute path (got "${configDirOverride}"). Use e.g. /data/ghl-mcp in a container, with a volume mounted at /data.
57184
57907
  `
@@ -57191,20 +57914,20 @@ process.on("unhandledRejection", (reason3) => {
57191
57914
  `);
57192
57915
  });
57193
57916
  function hardenSecretFilePerms() {
57194
- const repoDir = path40.resolve(__dirname, "..");
57917
+ const repoDir = path41.resolve(__dirname, "..");
57195
57918
  const candidates = [
57196
- { file: path40.join(repoDir, "start-mcp.sh"), mode: 448 },
57919
+ { file: path41.join(repoDir, "start-mcp.sh"), mode: 448 },
57197
57920
  // Legacy registry location (pre-migration); new location lives in app-data.
57198
- { file: path40.join(repoDir, ".ghl-tokens.json"), mode: 384 },
57921
+ { file: path41.join(repoDir, ".ghl-tokens.json"), mode: 384 },
57199
57922
  { file: tokenRegistryPath(), mode: 384 }
57200
57923
  ];
57201
57924
  for (const { file: file2, mode } of candidates) {
57202
57925
  let current;
57203
57926
  try {
57204
- if (!fs42.existsSync(file2)) continue;
57205
- current = fs42.statSync(file2).mode & 511;
57927
+ if (!fs43.existsSync(file2)) continue;
57928
+ current = fs43.statSync(file2).mode & 511;
57206
57929
  if (current !== mode) {
57207
- fs42.chmodSync(file2, mode);
57930
+ fs43.chmodSync(file2, mode);
57208
57931
  }
57209
57932
  } catch (error) {
57210
57933
  const message = error instanceof Error ? error.message : String(error);