@elitedcs/ghl-mcp 3.85.0 → 3.86.0

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
@@ -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(path43, opts = {}) {
449
+ function locationIdsInRequest(path44, 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(path43);
455
+ const seg = /\/locations\/([^/?#]+)/i.exec(path44);
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, path43, opts = {}) {
482
+ function assertWriteAllowed(method, path44, opts = {}) {
483
483
  if (!isWriteMethod(method)) return;
484
484
  let rules;
485
485
  try {
@@ -488,7 +488,7 @@ function assertWriteAllowed(method, path43, 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(path43, opts)) {
491
+ for (const id of locationIdsInRequest(path44, 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(path43, params) {
650
- const url = new URL(path43, GHL_BASE_URL);
649
+ buildUrl(path44, params) {
650
+ const url = new URL(path44, 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, path43, options = {}, attempt = 0) {
660
+ async request(method, path44, options = {}, attempt = 0) {
661
661
  this.signal?.throwIfAborted();
662
662
  if (inBuildRequestScope() && method === "GET" && !inAccountReadAttempt()) {
663
- return retryAccountRead(() => this.request(method, path43, options, attempt), `${method} ${path43}`);
663
+ return retryAccountRead(() => this.request(method, path44, options, attempt), `${method} ${path44}`);
664
664
  }
665
665
  if (attempt === 0) {
666
- assertWriteAllowed(method, path43, {
666
+ assertWriteAllowed(method, path44, {
667
667
  params: options.params,
668
668
  body: options.body,
669
669
  defaultLocationId: this.defaultLocationId
670
670
  });
671
671
  }
672
- const url = this.buildUrl(path43, options.params);
672
+ const url = this.buildUrl(path44, 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} ${path43}`));
693
+ throw requestError(new Error(`Request timeout (30s): ${method} ${path44}`));
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} ${path43}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
697
+ process.stderr.write(`[ghl-mcp] Network error on ${method} ${path44}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
698
698
  `);
699
699
  await new Promise((r) => setTimeout(r, delay5));
700
- return this.request(method, path43, options, attempt + 1);
700
+ return this.request(method, path44, 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} ${path43}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
708
+ process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path44}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay5}ms
709
709
  `);
710
710
  await new Promise((r) => setTimeout(r, delay5));
711
- return this.request(method, path43, options, attempt + 1);
711
+ return this.request(method, path44, 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} ${path43}
720
+ `GHL API Error ${response.status} ${response.statusText}: ${method} ${path44}
721
721
  ${errorBody}`
722
722
  ));
723
723
  }
@@ -738,20 +738,20 @@ ${errorBody}`
738
738
  combined?.dispose();
739
739
  }
740
740
  }
741
- async get(path43, options) {
742
- return this.request("GET", path43, options);
741
+ async get(path44, options) {
742
+ return this.request("GET", path44, options);
743
743
  }
744
- async post(path43, options) {
745
- return this.request("POST", path43, options);
744
+ async post(path44, options) {
745
+ return this.request("POST", path44, options);
746
746
  }
747
- async put(path43, options) {
748
- return this.request("PUT", path43, options);
747
+ async put(path44, options) {
748
+ return this.request("PUT", path44, options);
749
749
  }
750
- async patch(path43, options) {
751
- return this.request("PATCH", path43, options);
750
+ async patch(path44, options) {
751
+ return this.request("PATCH", path44, options);
752
752
  }
753
- async delete(path43, options) {
754
- return this.request("DELETE", path43, options);
753
+ async delete(path44, options) {
754
+ return this.request("DELETE", path44, options);
755
755
  }
756
756
  /**
757
757
  * Helper: resolves locationId from args or falls back to default
@@ -1045,9 +1045,9 @@ var init_token_registry = __esm({
1045
1045
  /**
1046
1046
  * Update the home Firebase refresh token (called on rotation)
1047
1047
  */
1048
- updateFirebaseRefreshToken(newToken) {
1048
+ updateFirebaseRefreshToken(newToken2) {
1049
1049
  if (this.data.firebase) {
1050
- this.data.firebase.refreshToken = newToken;
1050
+ this.data.firebase.refreshToken = newToken2;
1051
1051
  this.save();
1052
1052
  }
1053
1053
  }
@@ -1102,10 +1102,10 @@ var init_token_registry = __esm({
1102
1102
  * Update a company's Firebase refresh token (called on rotation while that
1103
1103
  * company is the active one). No-op if the company isn't registered.
1104
1104
  */
1105
- updateCompanyFirebaseRefreshToken(companyId, newToken) {
1105
+ updateCompanyFirebaseRefreshToken(companyId, newToken2) {
1106
1106
  const cfg = this.data.firebaseByCompany?.[_TokenRegistry.normalizeCompanyId(companyId)];
1107
1107
  if (cfg) {
1108
- cfg.refreshToken = newToken;
1108
+ cfg.refreshToken = newToken2;
1109
1109
  this.save();
1110
1110
  }
1111
1111
  }
@@ -2339,21 +2339,21 @@ var init_workflow_builder_client = __esm({
2339
2339
  * keep the in-memory home token in sync so resetToHomeFirebase() restores
2340
2340
  * the fresh value rather than a stale one.
2341
2341
  */
2342
- persistRotatedToken(newToken) {
2342
+ persistRotatedToken(newToken2) {
2343
2343
  const company = this.currentCompanyId;
2344
2344
  const hasCompanySlot = company !== void 0 && this.registry?.getCompanyFirebase(company) !== void 0;
2345
2345
  if (hasCompanySlot) {
2346
- this.registry?.updateCompanyFirebaseRefreshToken(company, newToken);
2346
+ this.registry?.updateCompanyFirebaseRefreshToken(company, newToken2);
2347
2347
  if (company === this.homeCompanyId) {
2348
- this.homeRefreshToken = newToken;
2348
+ this.homeRefreshToken = newToken2;
2349
2349
  }
2350
2350
  return;
2351
2351
  }
2352
2352
  if (company === void 0 || company === this.homeCompanyId) {
2353
- this.homeRefreshToken = newToken;
2353
+ this.homeRefreshToken = newToken2;
2354
2354
  }
2355
- this.persistRefreshToken(newToken);
2356
- this.registry?.updateFirebaseRefreshToken(newToken);
2355
+ this.persistRefreshToken(newToken2);
2356
+ this.registry?.updateFirebaseRefreshToken(newToken2);
2357
2357
  }
2358
2358
  /**
2359
2359
  * Persist a rotated refresh token to every store that backs it.
@@ -2366,11 +2366,11 @@ var init_workflow_builder_client = __esm({
2366
2366
  * credentials.json. Both writes are independent and non-fatal — the token
2367
2367
  * still works for the current session even if persistence fails.
2368
2368
  */
2369
- persistRefreshToken(newToken) {
2369
+ persistRefreshToken(newToken2) {
2370
2370
  try {
2371
2371
  const creds = readCredentials();
2372
2372
  if (creds) {
2373
- writeCredentials({ ...creds, ghl_firebase_refresh_token: newToken });
2373
+ writeCredentials({ ...creds, ghl_firebase_refresh_token: newToken2 });
2374
2374
  }
2375
2375
  } catch {
2376
2376
  process.stderr.write("[ghl-mcp] Warning: Could not persist rotated refresh token to credentials file\n");
@@ -2381,7 +2381,7 @@ var init_workflow_builder_client = __esm({
2381
2381
  let content = fs4.readFileSync(envPath, "utf-8");
2382
2382
  content = content.replace(
2383
2383
  /^GHL_FIREBASE_REFRESH_TOKEN=.*/m,
2384
- `GHL_FIREBASE_REFRESH_TOKEN=${newToken}`
2384
+ `GHL_FIREBASE_REFRESH_TOKEN=${newToken2}`
2385
2385
  );
2386
2386
  const tmpPath = envPath + ".tmp";
2387
2387
  fs4.writeFileSync(tmpPath, content);
@@ -3022,8 +3022,8 @@ function buildEvent(event, opts) {
3022
3022
  }
3023
3023
  function ephemeralId() {
3024
3024
  if (!processEphemeralId) {
3025
- const crypto8 = require("node:crypto");
3026
- processEphemeralId = crypto8.randomBytes(16).toString("hex");
3025
+ const crypto9 = require("node:crypto");
3026
+ processEphemeralId = crypto9.randomBytes(16).toString("hex");
3027
3027
  }
3028
3028
  return processEphemeralId;
3029
3029
  }
@@ -3469,8 +3469,8 @@ function textResponse(text2) {
3469
3469
  content: [{ type: "text", text: text2 }]
3470
3470
  };
3471
3471
  }
3472
- function escapeRegex(str6) {
3473
- return str6.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3472
+ function escapeRegex(str8) {
3473
+ return str8.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3474
3474
  }
3475
3475
  function errorMessage(error) {
3476
3476
  return error instanceof Error ? error.message : String(error);
@@ -7108,11 +7108,11 @@ function registerEmailTools(server2, client) {
7108
7108
  function registerEmailBuilderInternalTools(server2, builderClient, publicClient) {
7109
7109
  if (!builderClient) return;
7110
7110
  const client = builderClient;
7111
- async function builderRequest2(method, path43, body) {
7112
- const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path43}`, body ? { body } : {});
7111
+ async function builderRequest2(method, path44, body) {
7112
+ const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path44}`, body ? { body } : {});
7113
7113
  if (!response.ok) {
7114
7114
  const text3 = await response.text();
7115
- throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path43}
7115
+ throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path44}
7116
7116
  ${text3}`);
7117
7117
  }
7118
7118
  const text2 = await response.text();
@@ -8563,22 +8563,22 @@ function readDemoJournal(locationId2, base) {
8563
8563
  const raw = JSON.parse(fs7.readFileSync(p, "utf8"));
8564
8564
  return { locationId: locationId2, install: raw.install, runs: Array.isArray(raw.runs) ? raw.runs : [] };
8565
8565
  }
8566
- function writeDemoJournal(journal, base) {
8567
- const p = demoJournalPath(journal.locationId, base);
8566
+ function writeDemoJournal(journal2, base) {
8567
+ const p = demoJournalPath(journal2.locationId, base);
8568
8568
  fs7.mkdirSync(path6.dirname(p), { recursive: true });
8569
8569
  const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
8570
- fs7.writeFileSync(tmp, JSON.stringify(journal, null, 2));
8570
+ fs7.writeFileSync(tmp, JSON.stringify(journal2, null, 2));
8571
8571
  fs7.renameSync(tmp, p);
8572
8572
  return p;
8573
8573
  }
8574
- function activeRun(journal, agentId) {
8575
- return journal.runs.find((r) => HOLDS_BOT.has(r.status) && (agentId === void 0 || r.agentId === agentId));
8574
+ function activeRun(journal2, agentId) {
8575
+ return journal2.runs.find((r) => HOLDS_BOT.has(r.status) && (agentId === void 0 || r.agentId === agentId));
8576
8576
  }
8577
- function findRun(journal, runId) {
8578
- if (runId) return journal.runs.find((r) => r.runId === runId);
8579
- const open = journal.runs.filter((r) => HOLDS_BOT.has(r.status));
8577
+ function findRun(journal2, runId) {
8578
+ if (runId) return journal2.runs.find((r) => r.runId === runId);
8579
+ const open = journal2.runs.filter((r) => HOLDS_BOT.has(r.status));
8580
8580
  if (open.length > 1) throw new Error(`${open.length} demo runs are open on this sub-account (${open.map((r) => `${r.runId} on bot ${r.agentId}`).join("; ")}). Pass runId to say which one.`);
8581
- return open[0] ?? [...journal.runs].sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
8581
+ return open[0] ?? [...journal2.runs].sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
8582
8582
  }
8583
8583
  async function withDemoLock(locationId2, fn, base, opts = {}) {
8584
8584
  const lock = `${demoJournalPath(locationId2, base)}.lock`;
@@ -8736,9 +8736,9 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
8736
8736
  );
8737
8737
  async function installBody(client, loc, agentId, demoPhoneNumber, dryRun) {
8738
8738
  {
8739
- const journal = journalOf(loc);
8740
- if (journal.install && !agentId && !dryRun) {
8741
- throw new Error(`This sub-account already has a demo bot recorded: ${journal.install.agentName} (${journal.install.agentId}, ${journal.install.origin} ${journal.install.installedAt}). Pass agentId to re-point the install at a specific bot; a second bot would not be primary and would answer nobody.`);
8739
+ const journal2 = journalOf(loc);
8740
+ if (journal2.install && !agentId && !dryRun) {
8741
+ throw new Error(`This sub-account already has a demo bot recorded: ${journal2.install.agentName} (${journal2.install.agentId}, ${journal2.install.origin} ${journal2.install.installedAt}). Pass agentId to re-point the install at a specific bot; a second bot would not be primary and would answer nobody.`);
8742
8742
  }
8743
8743
  const existing = await customFields(client, loc);
8744
8744
  const byKey = new Map(existing.map((f) => [f.fieldKey, f]));
@@ -8761,7 +8761,7 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
8761
8761
  phoneNumbers: numbers,
8762
8762
  demoNumber: demoNumber ?? (numbers.length > 1 ? "choose with demoPhoneNumber" : null),
8763
8763
  manualStep: PRIMARY_NOTE,
8764
- alreadyInstalled: journal.install ?? null
8764
+ alreadyInstalled: journal2.install ?? null
8765
8765
  };
8766
8766
  }
8767
8767
  const created = [];
@@ -8794,8 +8794,8 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
8794
8794
  bot = await readAgent(client, made.id);
8795
8795
  origin = "created";
8796
8796
  }
8797
- journal.install = { agentId: String(bot.id), agentName: String(bot.name ?? DEMO_BOT_NAME), installedAt: now().toISOString(), fieldIds, origin, demoNumber, designated: "DEMO ACCOUNT", keyFingerprint: fingerprint(client) };
8798
- save(journal);
8797
+ journal2.install = { agentId: String(bot.id), agentName: String(bot.name ?? DEMO_BOT_NAME), installedAt: now().toISOString(), fieldIds, origin, demoNumber, designated: "DEMO ACCOUNT", keyFingerprint: fingerprint(client) };
8798
+ save(journal2);
8799
8799
  const primary = await primaryOnSms(client, String(bot.id));
8800
8800
  return {
8801
8801
  location: loc,
@@ -8832,14 +8832,14 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
8832
8832
  const t0 = now();
8833
8833
  const { loc, client } = await designatedDemoLocation(demoLocationId);
8834
8834
  const { botId, run } = await locked(loc, async () => {
8835
- const journal = journalOf(loc);
8836
- const botId2 = agentId ?? journal.install?.agentId;
8835
+ const journal2 = journalOf(loc);
8836
+ const botId2 = agentId ?? journal2.install?.agentId;
8837
8837
  if (!botId2) throw new Error("No demo bot is recorded for this sub-account and no agentId was given. Run install_ai_employee_demo first (or pass agentId). This tool never guesses which bot to re-point.");
8838
- const running = activeRun(journal, botId2);
8838
+ const running = activeRun(journal2, botId2);
8839
8839
  if (running) throw new Error(`A demo is already ${running.status} on this bot: run ${running.runId} for ${running.businessName} (${running.website}), started ${running.startedAt}. End it with end_ai_employee_demo before preparing another; two before-states for one bot is one lost before-state.`);
8840
8840
  const run2 = { runId: newDemoRunId(now()), startedAt: t0.toISOString(), status: "active", agentId: botId2, website, businessName, contactId: "", contactPhone: prospectPhone, kbId: "", before: {}, armed: false };
8841
- journal.runs.push(run2);
8842
- save(journal);
8841
+ journal2.runs.push(run2);
8842
+ save(journal2);
8843
8843
  return { botId: botId2, run: run2 };
8844
8844
  });
8845
8845
  const persist = (patch) => {
@@ -8957,8 +8957,8 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
8957
8957
  },
8958
8958
  async ({ demoLocationId, runId, messages }) => {
8959
8959
  const { loc, client } = await designatedDemoLocation(demoLocationId);
8960
- const journal = journalOf(loc);
8961
- const run = findRun(journal, runId);
8960
+ const journal2 = journalOf(loc);
8961
+ const run = findRun(journal2, runId);
8962
8962
  if (!run) throw new Error("No demo run is recorded for this sub-account. prepare_ai_employee_demo first.");
8963
8963
  let convId = run.conversationId;
8964
8964
  if (!convId) {
@@ -8970,7 +8970,7 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
8970
8970
  if (best && typeof best.id === "string") {
8971
8971
  convId = best.id;
8972
8972
  run.conversationId = convId;
8973
- save(journal);
8973
+ save(journal2);
8974
8974
  }
8975
8975
  }
8976
8976
  if (!convId) return { runId: run.runId, prospect: run.contactPhone, exchange: [], note: "The prospect has not texted yet \u2014 no conversation exists for their contact." };
@@ -9035,8 +9035,8 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
9035
9035
  },
9036
9036
  async ({ demoLocationId, runId, keepKnowledgeBase, acceptDifferences }) => {
9037
9037
  const { loc, client } = await designatedDemoLocation(demoLocationId);
9038
- const journal = journalOf(loc);
9039
- const run = findRun(journal, runId);
9038
+ const journal2 = journalOf(loc);
9039
+ const run = findRun(journal2, runId);
9040
9040
  if (!run) throw new Error("No demo run is recorded for this sub-account.");
9041
9041
  if (run.status === "ended" || run.status === "abandoned") return { runId: run.runId, status: run.status, note: `This run was already ${run.status} at ${run.endedAt ?? "?"}.`, diff: run.diff ?? [] };
9042
9042
  const b = run.before;
@@ -9060,14 +9060,14 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
9060
9060
  else {
9061
9061
  await client.delete(`/knowledge-bases/${run.kbId}`, { ...AI, noRetry: true });
9062
9062
  run.kbDeleted = run.kbId;
9063
- save(journal);
9063
+ save(journal2);
9064
9064
  knowledgeBase = `deleted: ${run.kbId}`;
9065
9065
  }
9066
9066
  const restored = diff.length === 0;
9067
9067
  run.status = restored || acceptDifferences ? "ended" : "restore-failed";
9068
9068
  run.endedAt = now().toISOString();
9069
9069
  run.diff = diff;
9070
- save(journal);
9070
+ save(journal2);
9071
9071
  const upd = { after };
9072
9072
  return {
9073
9073
  runId: run.runId,
@@ -9272,27 +9272,27 @@ function actionNameProblem(name) {
9272
9272
  function voiceActionBodyProblem(type, p) {
9273
9273
  const gap = undefinedPath2(p);
9274
9274
  if (gap) return `${gap} is undefined. JSON would drop that key silently and GoHighLevel would see a different body than you wrote \u2014 leave the key out or give it a value.`;
9275
- const str6 = (k) => typeof p[k] === "string" && p[k].trim().length > 0;
9275
+ const str8 = (k) => typeof p[k] === "string" && p[k].trim().length > 0;
9276
9276
  const num3 = (k) => typeof p[k] === "number" && Number.isFinite(p[k]);
9277
- const needTrigger = () => !str6("triggerPrompt") ? `${type} needs triggerPrompt: when, in plain words, the agent should do this ("When the caller asks for a person").` : !str6("triggerMessage") ? `${type} needs triggerMessage: what the agent says as it does it ("Let me get someone for you").` : null;
9277
+ const needTrigger = () => !str8("triggerPrompt") ? `${type} needs triggerPrompt: when, in plain words, the agent should do this ("When the caller asks for a person").` : !str8("triggerMessage") ? `${type} needs triggerMessage: what the agent says as it does it ("Let me get someone for you").` : null;
9278
9278
  switch (type) {
9279
9279
  case "CALL_TRANSFER": {
9280
9280
  const t = needTrigger();
9281
9281
  if (t) return t;
9282
9282
  if (p.transferToType !== "number") return 'CALL_TRANSFER needs transferToType: "number" (the only type GoHighLevel supports today).';
9283
- if (!str6("transferToValue") || !TRANSFER_NUMBER.test(p.transferToValue)) return "CALL_TRANSFER needs transferToValue as a full number with country code and no punctuation, e.g. +16025550100 (GoHighLevel refuses anything else with a 422).";
9283
+ if (!str8("transferToValue") || !TRANSFER_NUMBER.test(p.transferToValue)) return "CALL_TRANSFER needs transferToValue as a full number with country code and no punctuation, e.g. +16025550100 (GoHighLevel refuses anything else with a 422).";
9284
9284
  return null;
9285
9285
  }
9286
9286
  case "SMS": {
9287
9287
  const t = needTrigger();
9288
9288
  if (t) return t;
9289
- if (!str6("messageBody")) return "SMS needs messageBody: the text the caller receives.";
9289
+ if (!str8("messageBody")) return "SMS needs messageBody: the text the caller receives.";
9290
9290
  return null;
9291
9291
  }
9292
9292
  case "DATA_EXTRACTION":
9293
9293
  case "IN_CALL_DATA_EXTRACTION": {
9294
- if (!str6("contactFieldId")) return `${type} needs contactFieldId: the id of the contact field to fill (get_custom_fields). GoHighLevel refuses an unknown id with a 400 naming it.`;
9295
- if (!str6("description")) return `${type} needs description: what to listen for, in plain words.`;
9294
+ if (!str8("contactFieldId")) return `${type} needs contactFieldId: the id of the contact field to fill (get_custom_fields). GoHighLevel refuses an unknown id with a 400 naming it.`;
9295
+ if (!str8("description")) return `${type} needs description: what to listen for, in plain words.`;
9296
9296
  const ex = p.examples;
9297
9297
  if (!Array.isArray(ex) || ex.length < 1 || ex.length > 5 || ex.some((x) => typeof x !== "string" || !x.trim())) return `${type} needs examples: 1 to 5 example values (GoHighLevel refuses an empty list with a 422).`;
9298
9298
  return null;
@@ -9300,19 +9300,19 @@ function voiceActionBodyProblem(type, p) {
9300
9300
  case "WORKFLOW_TRIGGER": {
9301
9301
  const t = needTrigger();
9302
9302
  if (t) return t;
9303
- if (!str6("workflowId")) return "WORKFLOW_TRIGGER needs workflowId. It must be PUBLISHED \u2014 GoHighLevel refuses a draft or unknown id with a 400.";
9303
+ if (!str8("workflowId")) return "WORKFLOW_TRIGGER needs workflowId. It must be PUBLISHED \u2014 GoHighLevel refuses a draft or unknown id with a 400.";
9304
9304
  return null;
9305
9305
  }
9306
9306
  case "APPOINTMENT_BOOKING": {
9307
- if (!str6("calendarId")) return "APPOINTMENT_BOOKING needs calendarId (get_calendars). GoHighLevel refuses an unknown id with a 400 naming it.";
9307
+ if (!str8("calendarId")) return "APPOINTMENT_BOOKING needs calendarId (get_calendars). GoHighLevel refuses an unknown id with a 400 naming it.";
9308
9308
  for (const k of ["daysOfOfferingDates", "slotsPerDay", "hoursBetweenSlots"]) {
9309
9309
  if (!num3(k) || p[k] < 1) return `APPOINTMENT_BOOKING needs ${k} as a positive number (how many days ahead to offer, how many slots a day, hours between slots).`;
9310
9310
  }
9311
9311
  return null;
9312
9312
  }
9313
9313
  case "KNOWLEDGE_BASE": {
9314
- if (!str6("triggerPrompt")) return 'KNOWLEDGE_BASE needs triggerPrompt: when the agent should look something up ("When the caller asks about services, hours or pricing").';
9315
- if (!str6("knowledgeBaseId")) return "KNOWLEDGE_BASE needs knowledgeBaseId from THIS sub-account (list_knowledge_bases). GoHighLevel refuses an unknown id with a 400.";
9314
+ if (!str8("triggerPrompt")) return 'KNOWLEDGE_BASE needs triggerPrompt: when the agent should look something up ("When the caller asks about services, hours or pricing").';
9315
+ if (!str8("knowledgeBaseId")) return "KNOWLEDGE_BASE needs knowledgeBaseId from THIS sub-account (list_knowledge_bases). GoHighLevel refuses an unknown id with a 400.";
9316
9316
  return null;
9317
9317
  }
9318
9318
  case "CUSTOM_ACTION": {
@@ -10922,11 +10922,11 @@ async function readPageFull(client, pageId, fetcher = fetch) {
10922
10922
  const headers = await client.buildHeaders();
10923
10923
  headers.Origin = "https://app.gohighlevel.com";
10924
10924
  headers.Referer = "https://app.gohighlevel.com/";
10925
- const path43 = `/page/${pageId}?locationId=${client.locationId}`;
10926
- const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path43}`, { method: "GET", headers });
10925
+ const path44 = `/page/${pageId}?locationId=${client.locationId}`;
10926
+ const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path44}`, { method: "GET", headers });
10927
10927
  if (!response.ok) {
10928
10928
  const text3 = await response.text();
10929
- throw new Error(`Funnel API Error ${response.status}: GET ${path43}
10929
+ throw new Error(`Funnel API Error ${response.status}: GET ${path44}
10930
10930
  ${text3}`);
10931
10931
  }
10932
10932
  const text2 = await response.text();
@@ -10985,20 +10985,20 @@ function registerFunnelBuilderTools(server2, builderClient) {
10985
10985
  return;
10986
10986
  }
10987
10987
  const client = builderClient;
10988
- async function internalGet(path43) {
10989
- return client.request("GET", path43);
10988
+ async function internalGet(path44) {
10989
+ return client.request("GET", path44);
10990
10990
  }
10991
- async function internalPost(path43, body) {
10992
- return client.request("POST", path43, body);
10991
+ async function internalPost(path44, body) {
10992
+ return client.request("POST", path44, body);
10993
10993
  }
10994
- async function internalPut(path43, body) {
10995
- return client.request("PUT", path43, body);
10994
+ async function internalPut(path44, body) {
10995
+ return client.request("PUT", path44, body);
10996
10996
  }
10997
- async function internalDelete(path43) {
10998
- return client.request("DELETE", path43);
10997
+ async function internalDelete(path44) {
10998
+ return client.request("DELETE", path44);
10999
10999
  }
11000
- async function funnelRequest(method, path43, body) {
11001
- const url = `https://backend.leadconnectorhq.com/funnels${path43}`;
11000
+ async function funnelRequest(method, path44, body) {
11001
+ const url = `https://backend.leadconnectorhq.com/funnels${path44}`;
11002
11002
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
11003
11003
  const response = await builderRequest(client, method, url, {
11004
11004
  ...sendsBody ? { body } : {},
@@ -11011,7 +11011,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
11011
11011
  });
11012
11012
  if (!response.ok) {
11013
11013
  const text3 = await response.text();
11014
- throw new Error(`Funnel API Error ${response.status}: ${method} ${path43}
11014
+ throw new Error(`Funnel API Error ${response.status}: ${method} ${path44}
11015
11015
  ${text3}`);
11016
11016
  }
11017
11017
  const text2 = await response.text();
@@ -11602,17 +11602,17 @@ function requireCompany(api) {
11602
11602
  }
11603
11603
  return company;
11604
11604
  }
11605
- async function call(api, path43, init) {
11605
+ async function call(api, path44, init) {
11606
11606
  const headers = { ...await api.headers(), Origin: "https://app.gohighlevel.com", Referer: "https://app.gohighlevel.com/" };
11607
- if (MUTATING_PATHS.some((mutating) => path43.startsWith(mutating))) {
11608
- assertWriteAllowed("POST", SVC + path43, { defaultLocationId: api.locationId() });
11607
+ if (MUTATING_PATHS.some((mutating) => path44.startsWith(mutating))) {
11608
+ assertWriteAllowed("POST", SVC + path44, { defaultLocationId: api.locationId() });
11609
11609
  }
11610
11610
  const doFetch = api.fetch ?? fetch;
11611
- const response = await doFetch(SVC + path43, { ...init, headers });
11611
+ const response = await doFetch(SVC + path44, { ...init, headers });
11612
11612
  const text2 = await response.text();
11613
11613
  if (!response.ok) {
11614
11614
  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." : "";
11615
- throw new Error(`GoHighLevel template library ${response.status} on ${path43}${hint}
11615
+ throw new Error(`GoHighLevel template library ${response.status} on ${path44}${hint}
11616
11616
  ${text2.slice(0, 300)}`);
11617
11617
  }
11618
11618
  return text2 ? JSON.parse(text2) : {};
@@ -11969,12 +11969,12 @@ function normaliseColour(value) {
11969
11969
  function renderedPalette(envelope) {
11970
11970
  const defs = pageVariables(envelope);
11971
11971
  const paints = [];
11972
- const walk = (node, path43, prop) => {
11973
- if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${path43}[${i}]`, prop));
11972
+ const walk = (node, path44, prop) => {
11973
+ if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${path44}[${i}]`, prop));
11974
11974
  if (isRecord8(node)) {
11975
11975
  for (const [k, x] of Object.entries(node)) {
11976
11976
  if (DEFINITION_KEYS.has(k) || k.startsWith("--")) continue;
11977
- walk(x, `${path43}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
11977
+ walk(x, `${path44}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
11978
11978
  }
11979
11979
  return;
11980
11980
  }
@@ -11984,7 +11984,7 @@ function renderedPalette(envelope) {
11984
11984
  const resolved2 = resolveValue(raw, defs);
11985
11985
  const colour = normaliseColour(resolved2);
11986
11986
  if (colour === null && resolved2 !== null) return;
11987
- paints.push({ path: path43, prop, kind: kindOf(prop), colour });
11987
+ paints.push({ path: path44, prop, kind: kindOf(prop), colour });
11988
11988
  };
11989
11989
  walk(envelope, "", "");
11990
11990
  const face = (name, settingsKey) => {
@@ -12079,7 +12079,7 @@ var init_rendered_palette = __esm({
12079
12079
  VISIBLE_CHANGE = 0.05;
12080
12080
  BARELY_SHARE = 0.5;
12081
12081
  CARRIES_COLOUR = 0.02;
12082
- paintGroup = (path43) => /^\.(sections|popups)\[\d+\]/.exec(path43)?.[0]?.slice(1) ?? "page";
12082
+ paintGroup = (path44) => /^\.(sections|popups)\[\d+\]/.exec(path44)?.[0]?.slice(1) ?? "page";
12083
12083
  looksDifferent = (a, b) => {
12084
12084
  if (a === b) return false;
12085
12085
  if (a === "transparent" || b === "transparent" || a.startsWith("rgba") || b.startsWith("rgba")) return a !== b;
@@ -16654,9 +16654,9 @@ function removalHoldBack(opts) {
16654
16654
  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`;
16655
16655
  return null;
16656
16656
  }
16657
- function brandedPath(path43, name, taken) {
16657
+ function brandedPath(path44, name, taken) {
16658
16658
  const normalize2 = (s) => s.replace(/^\/+|\/+$/g, "").toLowerCase();
16659
- const base = normalize2(path43 ?? "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
16659
+ const base = normalize2(path44 ?? "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
16660
16660
  const used = new Set([...taken].map(normalize2));
16661
16661
  let candidate = `${base}-branded`;
16662
16662
  for (let n = 2; used.has(candidate); n++) candidate = `${base}-branded-${n}`;
@@ -16684,9 +16684,9 @@ function rewriteTemplateLinks(envelope, destinations) {
16684
16684
  representation = "page";
16685
16685
  matches = destinations.filter((d2) => d2.pageId === preview[2]);
16686
16686
  } else if (!/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(from) && !from.startsWith("#")) {
16687
- const [path43] = from.split(/[?#]/);
16688
- suffix = from.slice(path43.length);
16689
- matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(path43));
16687
+ const [path44] = from.split(/[?#]/);
16688
+ suffix = from.slice(path44.length);
16689
+ matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(path44));
16690
16690
  }
16691
16691
  }
16692
16692
  if (matches.length !== 1) {
@@ -16769,8 +16769,8 @@ function registerPageStudioTools(server2, builderClient) {
16769
16769
  }
16770
16770
  return { map: out, truncated };
16771
16771
  }
16772
- async function funnelRequest(method, path43, body) {
16773
- const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path43}`, {
16772
+ async function funnelRequest(method, path44, body) {
16773
+ const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path44}`, {
16774
16774
  ...body ? { body } : {},
16775
16775
  headers: {
16776
16776
  // Internal-API calls require the app origin (autosave needs the
@@ -16779,7 +16779,7 @@ function registerPageStudioTools(server2, builderClient) {
16779
16779
  Referer: "https://app.gohighlevel.com/"
16780
16780
  }
16781
16781
  });
16782
- if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path43}
16782
+ if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path44}
16783
16783
  ${await response.text()}`);
16784
16784
  const text2 = await response.text();
16785
16785
  return text2 ? JSON.parse(text2) : {};
@@ -17648,13 +17648,13 @@ function cleanFormDataForSave(formData) {
17648
17648
  const cleaned = dropRedundantCaseCollidingKeys(formData);
17649
17649
  return isJsonObject(cleaned) ? cleaned : formData;
17650
17650
  }
17651
- async function formApiRequest(client, method, path43, body) {
17652
- const url = `https://backend.leadconnectorhq.com/forms${path43}`;
17651
+ async function formApiRequest(client, method, path44, body) {
17652
+ const url = `https://backend.leadconnectorhq.com/forms${path44}`;
17653
17653
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
17654
17654
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
17655
17655
  if (!response.ok) {
17656
17656
  const text3 = await response.text();
17657
- throw new Error(`Form API Error ${response.status}: ${method} ${path43}
17657
+ throw new Error(`Form API Error ${response.status}: ${method} ${path44}
17658
17658
  ${text3}`);
17659
17659
  }
17660
17660
  const text2 = await response.text();
@@ -17696,7 +17696,7 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
17696
17696
  );
17697
17697
  return;
17698
17698
  }
17699
- const formRequest = (method, path43, body) => formApiRequest(client, method, path43, body);
17699
+ const formRequest = (method, path44, body) => formApiRequest(client, method, path44, body);
17700
17700
  server2.tool(
17701
17701
  "get_form_full",
17702
17702
  "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.",
@@ -17830,10 +17830,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
17830
17830
  },
17831
17831
  async ({ formId, limit, skip }) => {
17832
17832
  try {
17833
- let path43 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
17834
- if (formId) path43 += `&formId=${formId}`;
17835
- if (skip) path43 += `&skip=${skip}`;
17836
- const result = await formRequest("GET", path43);
17833
+ let path44 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
17834
+ if (formId) path44 += `&formId=${formId}`;
17835
+ if (skip) path44 += `&skip=${skip}`;
17836
+ const result = await formRequest("GET", path44);
17837
17837
  return {
17838
17838
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
17839
17839
  };
@@ -18241,13 +18241,13 @@ var init_funnel_qa = __esm({
18241
18241
  function registerPipelineBuilderTools(server2, builderClient) {
18242
18242
  if (!builderClient) return;
18243
18243
  const client = builderClient;
18244
- async function pipelineRequest(method, path43, body) {
18245
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path43}`;
18244
+ async function pipelineRequest(method, path44, body) {
18245
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path44}`;
18246
18246
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT" || method === "PATCH");
18247
18247
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
18248
18248
  if (!response.ok) {
18249
18249
  const text3 = await response.text();
18250
- throw new Error(`Pipeline API Error ${response.status}: ${method} ${path43}
18250
+ throw new Error(`Pipeline API Error ${response.status}: ${method} ${path44}
18251
18251
  ${text3}`);
18252
18252
  }
18253
18253
  const text2 = await response.text();
@@ -19235,9 +19235,9 @@ function planFromPayload(payload) {
19235
19235
  }
19236
19236
  function legacyDeviceFingerprint() {
19237
19237
  const os8 = require("node:os");
19238
- const crypto8 = require("node:crypto");
19238
+ const crypto9 = require("node:crypto");
19239
19239
  const raw = `${os8.hostname()}:${os8.userInfo().username}:${os8.platform()}:${os8.arch()}`;
19240
- return crypto8.createHash("sha256").update(raw).digest("hex").slice(0, 16);
19240
+ return crypto9.createHash("sha256").update(raw).digest("hex").slice(0, 16);
19241
19241
  }
19242
19242
  function deviceFingerprint(opts) {
19243
19243
  if (opts?.stored) return opts.stored;
@@ -19245,14 +19245,14 @@ function deviceFingerprint(opts) {
19245
19245
  opts.persist?.(opts.cached);
19246
19246
  return opts.cached;
19247
19247
  }
19248
- const crypto8 = require("node:crypto");
19248
+ const crypto9 = require("node:crypto");
19249
19249
  if (!opts) {
19250
19250
  try {
19251
19251
  const store = (init_credentials_store(), __toCommonJS(credentials_store_exports));
19252
19252
  const creds = store.readCredentials();
19253
19253
  if (creds?.device_id) return creds.device_id;
19254
19254
  const adopted = creds?.signed_attestation ? parseAttestationPayload(creds.signed_attestation)?.device_fingerprint : void 0;
19255
- const id = adopted || crypto8.randomBytes(8).toString("hex");
19255
+ const id = adopted || crypto9.randomBytes(8).toString("hex");
19256
19256
  if (creds) {
19257
19257
  try {
19258
19258
  store.writeCredentials({ ...creds, device_id: id });
@@ -19264,7 +19264,7 @@ function deviceFingerprint(opts) {
19264
19264
  return legacyDeviceFingerprint();
19265
19265
  }
19266
19266
  }
19267
- const fresh = crypto8.randomBytes(8).toString("hex");
19267
+ const fresh = crypto9.randomBytes(8).toString("hex");
19268
19268
  opts.persist?.(fresh);
19269
19269
  return fresh;
19270
19270
  }
@@ -20950,16 +20950,16 @@ function createGoogleCalendar(deps = {}) {
20950
20950
  requireOnlyCalendar(result.scope);
20951
20951
  return result.access_token;
20952
20952
  }
20953
- async function pages(path43, access, item, query = {}) {
20953
+ async function pages(path44, access, item, query = {}) {
20954
20954
  const result = [];
20955
20955
  const seen = /* @__PURE__ */ new Set();
20956
20956
  let pageToken;
20957
20957
  for (let page = 0; page < 100; page++) {
20958
- const url = new URL(`${API}${path43}`);
20958
+ const url = new URL(`${API}${path44}`);
20959
20959
  for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
20960
20960
  if (pageToken) url.searchParams.set("pageToken", pageToken);
20961
20961
  const data = parsed(
20962
- 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 === (path43.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
20962
+ 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 === (path44.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
20963
20963
  await request(url.href, { headers: { Authorization: `Bearer ${access}` } })
20964
20964
  );
20965
20965
  result.push(...data.items ?? []);
@@ -21210,8 +21210,8 @@ function redactConnectionKey(value, key) {
21210
21210
  return value;
21211
21211
  }
21212
21212
  function stripeFetcher(key, signal) {
21213
- return async (path43, query) => {
21214
- const url = new URL(`https://api.stripe.com${path43}`);
21213
+ return async (path44, query) => {
21214
+ const url = new URL(`https://api.stripe.com${path44}`);
21215
21215
  for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
21216
21216
  const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, signal });
21217
21217
  if (!res.ok) {
@@ -21300,11 +21300,11 @@ var init_handlers = __esm({
21300
21300
  });
21301
21301
 
21302
21302
  // src/command-os/connectors/stripe-read.ts
21303
- async function listAll(f, path43, schema, extra = {}) {
21303
+ async function listAll(f, path44, schema, extra = {}) {
21304
21304
  const out = [];
21305
21305
  let after;
21306
21306
  for (let page = 0; page < 20; page++) {
21307
- const result = import_zod53.z.object({ data: import_zod53.z.array(schema), has_more: import_zod53.z.boolean().optional() }).parse(await f(path43, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
21307
+ const result = import_zod53.z.object({ data: import_zod53.z.array(schema), has_more: import_zod53.z.boolean().optional() }).parse(await f(path44, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
21308
21308
  out.push(...result.data);
21309
21309
  if (!result.has_more) return out;
21310
21310
  const last = import_zod53.z.object({ id: import_zod53.z.string().min(1) }).parse(result.data.at(-1));
@@ -28800,7 +28800,7 @@ var init_client_capabilities = __esm({
28800
28800
  { key: "reputation", label: "Reputation and content", blurb: "Reviews, social posts and the blog." },
28801
28801
  { key: "health", label: "Health and handoff", blurb: "Proving it works, keeping it clean, and handing it over." }
28802
28802
  ];
28803
- N = (label, path43, params, key, nameField, version) => ({ label, path: path43, params, key, nameField, version });
28803
+ N = (label, path44, params, key, nameField, version) => ({ label, path: path44, params, key, nameField, version });
28804
28804
  AI2 = "2021-04-15";
28805
28805
  CAPABILITIES = [
28806
28806
  // ── Set up ──────────────────────────────────────────────────────────────
@@ -29061,7 +29061,7 @@ var require_package = __commonJS({
29061
29061
  "package.json"(exports2, module2) {
29062
29062
  module2.exports = {
29063
29063
  name: "@elitedcs/ghl-mcp",
29064
- version: "3.85.0",
29064
+ version: "3.86.0",
29065
29065
  mcpName: "io.github.drjerryrelth/ghl-command",
29066
29066
  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.",
29067
29067
  main: "dist/index.js",
@@ -29474,14 +29474,15 @@ function resultHtml(run, now) {
29474
29474
  const watching = run.status === "watching";
29475
29475
  const minutesLeft = Math.max(0, Math.ceil((run.closesAt - now) / 6e4));
29476
29476
  const planned = run.planNote.source !== "none" && run.expect.length > 0;
29477
- const head3 = !f ? "Watching what the test lead receives" : f.sent === 0 ? `Nothing arrived in ${watching ? "the time so far" : `${run.watchMinutes} minutes`}` : f.problems.length || f.missing.length ? "What a new lead actually received" : planned ? "A new lead got exactly what you planned" : "No problems in what the lead received";
29477
+ const held = f?.notYetDue.filter((m) => m.heldByWait) ?? [];
29478
+ const head3 = !f ? "Watching what the test lead receives" : f.sent === 0 ? `Nothing arrived in ${watching ? "the time so far" : `${run.watchMinutes} minutes`}` : f.problems.length || f.missing.length ? "What a new lead actually received" : held.length ? "Not finished: GoHighLevel was still holding the test lead" : planned ? "A new lead got exactly what you planned" : "No problems in what the lead received";
29478
29479
  const worst = f?.problems[0];
29479
- const quote = f && f.sent === 0 ? `<blockquote>The test lead received no text and no email.<cite>The workflows may not have fired, may still be drafts, or send later than the window. Check that they are published, or watch for longer.</cite></blockquote>` : worst ? `<blockquote>${worst.kind === "blank_field" ? "A new lead with no first name on file got" : "A new lead got"} <b>"${marked(worst.quote, "")}"</b><cite>${esc2(worst.title)}${f && f.problems.length > 1 ? `. The first of ${f.problems.length}.` : "."}</cite></blockquote>` : f && f.missing.length ? `<blockquote>${f.missing.length} planned message${f.missing.length === 1 ? " never" : "s never"} arrived. <cite>Everything that did arrive read correctly.</cite></blockquote>` : f ? `<blockquote class="good">Every message read correctly${planned ? " and arrived when planned" : ""}.<cite>Checked for the first ${run.watchMinutes} minutes. Later messages are listed below as not yet checked.</cite></blockquote>` : "";
29480
+ const quote = f && f.sent === 0 ? `<blockquote>The test lead received no text and no email.<cite>The workflows may not have fired, may still be drafts, or send later than the window. Check that they are published, or watch for longer.</cite></blockquote>` : worst ? `<blockquote>${worst.kind === "blank_field" ? "A new lead with no first name on file got" : "A new lead got"} <b>"${marked(worst.quote, "")}"</b><cite>${esc2(worst.title)}${f && f.problems.length > 1 ? `. The first of ${f.problems.length}.` : "."}</cite></blockquote>` : f && held.length && !f.missing.length ? `<blockquote>${held.length} planned message${held.length === 1 ? " was" : "s were"} still waiting behind a wait GoHighLevel had not let go of.<cite>Not judged. Run the check again.</cite></blockquote>` : f && f.missing.length ? `<blockquote>${f.missing.length} planned message${f.missing.length === 1 ? " never" : "s never"} arrived. <cite>Everything that did arrive read correctly.</cite></blockquote>` : f ? `<blockquote class="good">Every message read correctly${planned ? " and arrived when planned" : ""}.<cite>Checked for the first ${run.watchMinutes} minutes. Later messages are listed below as not yet checked.</cite></blockquote>` : "";
29480
29481
  const stats = f ? `<div class="stats">
29481
29482
  <div class="stat"><div class="big">${f.sent}</div><div class="lbl">Messages sent</div></div>
29482
29483
  <div class="stat ${f.problems.length ? "bad" : "ok"}"><div class="big">${f.problems.length}</div><div class="lbl">A customer would notice</div></div>
29483
29484
  ${run.steps?.read ? `<div class="stat ${run.steps.problems.length ? "bad" : run.steps.rows.length && !run.steps.unreadWorkflows?.length && !run.steps.partialWorkflows?.length && !run.steps.counts.unclear ? "ok" : "warn"}"><div class="big">${run.steps.rows.length ? run.steps.problems.length : "\u2013"}</div><div class="lbl">Steps failed, skipped or did nothing</div></div>` : ""}
29484
- <div class="stat ${f.missing.length ? "warn" : "ok"}"><div class="big">${run.planNote.source === "none" ? "\u2013" : f.missing.length}</div><div class="lbl">Planned, never sent</div></div>
29485
+ <div class="stat ${f.missing.length || held.length ? "warn" : "ok"}"><div class="big">${run.planNote.source === "none" ? "\u2013" : f.missing.length}</div><div class="lbl">Planned, never sent</div></div>
29485
29486
  ${(() => {
29486
29487
  const opened = f.links.total - f.links.skipped;
29487
29488
  return opened ? `<div class="stat ${f.links.broken ? "bad" : "ok"}"><div class="big">${f.links.ok} of ${opened}</div><div class="lbl">Links open${f.links.skipped ? ` \xB7 ${f.links.skipped} not opened` : ""}</div></div>` : `<div class="stat"><div class="big">0</div><div class="lbl">Links opened${f.links.skipped ? ` \xB7 ${f.links.skipped} not` : ""}</div></div>`;
@@ -29501,20 +29502,28 @@ ${stats}${quote}</div>`;
29501
29502
  ${f.timeline.length || missingRows ? `<ol class="timeline">${f.timeline.map(timelineRow).join("")}${missingRows}</ol>` : `<p class="empty">Nothing has arrived yet.</p>`}</section>`;
29502
29503
  const plan = run.planNote.source === "workflows" ? `Planned messages were read from the workflows: ${run.expect.length}.${run.planNote.conditional ? ` ${run.planNote.conditional} left out because they depend on what the lead does (an if/else or a wait for a reply).` : ""}${run.planNote.noWording ? ` ${run.planNote.noWording} left out because their wording is all merge fields.` : ""}${run.planNote.unread?.length ? ` ${run.planNote.unread.length} workflow${run.planNote.unread.length === 1 ? "" : "s"} could not be read.` : ""}${run.planNote.noPhone ? ` ${run.planNote.noPhone} text-message step${run.planNote.noPhone === 1 ? " was" : "s were"} not checked: no test phone was given.` : ""}` : run.planNote.source === "given" ? `Planned messages were given for this run: ${run.expect.length}.` : "No plan was known for this run, so nothing is reported as missing.";
29503
29504
  const notYet = [
29504
- ...f.notYetDue.map((m) => `${esc2(m.label)}, due after the window closed.`),
29505
+ ...f.notYetDue.map((m) => m.heldByWait ? `${esc2(m.label)}: GoHighLevel was still holding the test lead at ${esc2(run.heldAt?.map((h) => `"${h.stepName}"`).join(", ") ?? "a wait")} after it should have ended${run.extendedMinutes ? `, even after the check waited ${run.extendedMinutes} extra minutes` : ""}. Not judged; run the check again.` : `${esc2(m.label)}, due after the window closed.`),
29505
29506
  `Messages after the ${run.watchMinutes}-minute window. Choose a longer watch on the next run.`,
29506
29507
  "Booking an actual time, or replying as the lead.",
29507
29508
  "Phone calls and voice agents.",
29508
29509
  "Whether the wording persuades. This checks that it's correct, not that it sells.",
29509
29510
  ...f.links.skipped ? [`${f.links.skipped} link${f.links.skipped === 1 ? "" : "s"} deliberately not opened: only links on the client's own website are opened, because a tracked link counts as the lead clicking and an unsubscribe link opts them out.`] : [],
29510
29511
  ...run.testPhone ? [] : ["Text messages: no test phone was given, so the lead could not receive any."],
29512
+ ...run.fastForward ? [
29513
+ `Real timing: every wait was cut to 1 minute. In real life: ${run.fastForward.copies.flatMap((c) => c.waits).join("; ") || "no waits"}.`,
29514
+ ...run.fastForward.copies.filter((c) => c.window).map((c) => `${c.sourceName} ${c.window} in real life; the copy ran at any hour.`),
29515
+ "That staff receive alerts, tasks and assigned leads: during the check they came to you instead.",
29516
+ ...run.fastForward.copies.length ? ["Where a webhook's receiving service forwards the lead next: only the first address is checked."] : [],
29517
+ 'Wording that depends on time ("see you tomorrow") reads oddly after a 1-minute wait; it is not flagged.'
29518
+ ] : [],
29511
29519
  ...run.websiteHost ? [] : ["Links: this account has no website on file, so no link was opened. Add the website in the account's business profile to have them checked."]
29512
29520
  ];
29513
29521
  const coverage = `<section><div class="section-head"><div><div class="label">Coverage</div><h2>What this check looked at</h2></div><div class="right">So a clean result means something</div></div>
29514
29522
  <ul class="checks">${run.steps?.read ? `<li><span class="dot"></span>Every workflow step: ran, did nothing, failed or waiting, from GoHighLevel's own log</li>` : ""}<li><span class="dot"></span>Blank names and empty fields in every message</li><li><span class="dot"></span>Template code left showing, like {{ \u2026 }}</li><li><span class="dot"></span>Another of your accounts' business names</li>${f.links.total - f.links.skipped ? `<li><span class="dot"></span>Links on the client's own website, opened to see they load</li>` : ""}<li><span class="dot"></span>Messages the plan promised that never came</li><li><span class="dot"></span>The same message sent twice</li></ul>
29515
29523
  <p class="plan">${esc2(plan)}</p>
29516
29524
  <details><summary>Not checked (${notYet.length})</summary><ul>${notYet.map((n) => `<li>${n}</li>`).join("")}</ul></details></section>`;
29517
- const cleanup = run.cleanup ? `<section><div class="callout"><b>${run.cleanup.removed ? "Cleaned up:" : "Test lead:"}</b> ${esc2(run.cleanup.detail)} Anything that looked like a password or key in a message was hidden before it reached this page.</div></section>` : "";
29525
+ const ffLine = run.fastForward ? run.fastForward.removed ? " The temporary fast-forward copies were removed." : run.fastForward.problems?.length ? ` ${run.fastForward.problems.join(" ")}` : ` Temporary copies still in the account: ${run.fastForward.copies.map((c) => `"${c.copyName}"`).join(", ")}. The cockpit retries every 30 minutes while it runs; if they are still there tomorrow, delete them in GoHighLevel.` : "";
29526
+ const cleanup = run.cleanup ? `<section><div class="callout"><b>${run.cleanup.removed ? "Cleaned up:" : "Test lead:"}</b> ${esc2(run.cleanup.detail + ffLine)} Anything that looked like a password or key in a message was hidden before it reached this page.</div></section>` : "";
29518
29527
  return hero + draftNote + fixes + stepsHtml(run.steps) + timeline + coverage + cleanup;
29519
29528
  }
29520
29529
  function startForm(d) {
@@ -29529,6 +29538,10 @@ function startForm(d) {
29529
29538
  <label for="watch">Watch for</label>
29530
29539
  <select id="watch" name="watchMinutes"><option value="auto" selected>Automatic: until the last planned message, plus 2 minutes</option><option value="10">10 minutes</option><option value="30">30 minutes</option><option value="60">1 hour</option><option value="240">4 hours</option><option value="1440">24 hours</option></select>
29531
29540
  <label class="inline"><input type="checkbox" id="keep" name="keepLead"> Keep the test lead afterwards so I can look at it in GoHighLevel</label>
29541
+ <label class="inline"><input type="checkbox" id="ff" name="fastForward"> Fast-forward: test long workflows in minutes. A temporary copy with every step kept and every wait cut to 1 minute; staff alerts, tasks and lead assignments go to one person you choose, marked TEST; the copy is deleted afterwards.</label>
29542
+ <div id="alert-to-wrap" hidden><label for="alert-to">Staff alerts, tasks and assignments go to (instead of your staff, marked TEST)</label>
29543
+ <select id="alert-to" name="alertUserId"></select></div>
29544
+ <fieldset id="hooks" hidden><legend>Outside services these workflows send the lead to</legend><div id="hooks-list"></div></fieldset>
29532
29545
  <p class="warnline">The test lead has no first name, on purpose. The workflows you tick will really send to your address and may alert your staff once.</p>
29533
29546
  <button class="btn" type="submit" disabled>Start the secret shopper</button>
29534
29547
  <p class="result" id="start-result" role="status" aria-live="polite"></p>
@@ -29560,11 +29573,50 @@ ${COCKPIT_TYPE_JS}
29560
29573
  var ids=[].slice.call(form.querySelectorAll('input[name="wf"]:checked')).map(function(i){return i.value});
29561
29574
  var out=document.getElementById("start-result");
29562
29575
  if(!ids.length){out.textContent="Tick at least one workflow for the test lead to enter.";return}
29576
+ if(document.getElementById("ff").checked&&!document.getElementById("alert-to").value&&document.getElementById("alert-to").getAttribute("data-loaded")==="1"){out.textContent="Choose who gets the staff alerts, tasks and assignments during the test.";return}
29577
+ if(document.getElementById("ff").checked&&form.querySelector('[data-hook-blocked]')){out.textContent="One of these workflows sends the lead to an address built from a merge field, so fast-forward cannot test it yet. Untick that workflow, or run without fast-forward.";return}
29563
29578
  var btn=form.querySelector("button[type=submit]");btn.disabled=true;out.textContent="Starting\u2026";
29564
- post("/api/shopper/start",{client:client,workflowIds:ids,ownerEmail:document.getElementById("owner-email").value,testPhone:document.getElementById("test-phone").value||undefined,watchMinutes:(document.getElementById("watch").value==="auto"?undefined:Number(document.getElementById("watch").value)),keepLead:document.getElementById("keep").checked})
29579
+ var hosts=[].slice.call(form.querySelectorAll('input[name="hook"]:checked')).map(function(i){return i.value});
29580
+ post("/api/shopper/start",{client:client,workflowIds:ids,ownerEmail:document.getElementById("owner-email").value,testPhone:document.getElementById("test-phone").value||undefined,watchMinutes:(document.getElementById("watch").value==="auto"?undefined:Number(document.getElementById("watch").value)),keepLead:document.getElementById("keep").checked,fastForward:document.getElementById("ff").checked,allowedHosts:hosts,alertUserId:(document.getElementById("alert-to").value||undefined)})
29565
29581
  .then(function(r){if(r.ok){location.reload()}else{btn.disabled=false;out.textContent=r.error||"That did not start."}})
29566
29582
  .catch(function(){btn.disabled=false;out.textContent="That did not start. Check that Command OS is still running."});
29567
29583
  })}
29584
+ // Fast-forward sends the test lead to the workflows' outside services for real:
29585
+ // read them from the chosen workflows and let the owner tick each one.
29586
+ var hookSeq=0, hooks=document.getElementById("hooks"), hooksList=document.getElementById("hooks-list"), ff=document.getElementById("ff");
29587
+ function refreshHooks(){
29588
+ if(!hooks||!form)return;
29589
+ var ids=[].slice.call(form.querySelectorAll('input[name="wf"]:checked')).map(function(i){return i.value});
29590
+ if(!ff.checked||!ids.length){hooks.hidden=true;hooksList.innerHTML="";return}
29591
+ var kept={};[].slice.call(hooksList.querySelectorAll('input[name="hook"]:checked')).forEach(function(i){kept[i.value]=true});
29592
+ var mine=++hookSeq;
29593
+ hooks.hidden=false;hooksList.innerHTML='<p class="empty">Reading the chosen workflows\u2026</p>';
29594
+ fetch("/api/shopper/webhooks?client="+encodeURIComponent(client)+"&ids="+encodeURIComponent(ids.join(",")),{headers:hdrs()}).then(function(r){return r.json()}).then(function(r){
29595
+ if(mine!==hookSeq)return; // an older answer arriving late: the newer one wins
29596
+ if(!r.ok){hooksList.innerHTML='<p class="err">'+esc(r.error||"The workflows could not be read.")+'</p>';return}
29597
+ if(!r.destinations.length){hooksList.innerHTML='<p class="empty">None. These workflows do not send the lead to any outside service, so nothing leaves GoHighLevel during the test.</p>';return}
29598
+ var byHost={};r.destinations.forEach(function(d){var k=d.host||("!"+(d.problem||"merge_field"));(byHost[k]=byHost[k]||[]).push(d)});
29599
+ hooksList.innerHTML='<p class="note">During a fast-forward check these steps run for real, so the test lead (no first name, your email) is sent to each service below and may appear there as a new record. Tick each one you are happy to receive it. Leave one unticked and the check stops before it starts and names the step.</p>'+Object.keys(byHost).map(function(h){var steps=byHost[h].map(function(d){return '"'+esc(d.stepName)+'" in '+esc(d.workflowName)}).join(", ");
29600
+ return h.charAt(0)!=="!"?'<label class="inline"><input type="checkbox" name="hook" value="'+esc(h)+'"'+(kept[h]?" checked":"")+'> <b>'+esc(h)+'</b>, used by '+steps+'</label>':'<p class="err" data-hook-blocked>'+steps+(h==="!no_address"?' has no address filled in, so it cannot run. Fix the step, or untick this workflow.':' sends the lead to an address filled in from a merge field, so where it goes cannot be checked in advance. Fast-forward cannot test this workflow yet.')+'</p>'}).join("");
29601
+ }).catch(function(){hooksList.innerHTML='<p class="err">The workflows could not be read. Check that Command OS is still running.</p>'});
29602
+ }
29603
+ if(ff&&list){ff.addEventListener("change",refreshHooks);list.addEventListener("change",refreshHooks)}
29604
+ // Who stands in for the staff: this account's users, the owner's own login pre-selected when the email matches.
29605
+ var alertWrap=document.getElementById("alert-to-wrap"), alertTo=document.getElementById("alert-to"), usersLoaded=false;
29606
+ function base(e){e=String(e||"").trim().toLowerCase();var p=e.split("@");return p[0].split("+")[0]+"@"+(p[1]||"")}
29607
+ function refreshAlertTo(){
29608
+ if(!alertWrap)return;
29609
+ alertWrap.hidden=!ff.checked;
29610
+ if(!ff.checked||usersLoaded)return;usersLoaded=true;
29611
+ alertTo.innerHTML='<option value="">Loading the people in this account\u2026</option>';
29612
+ fetch("/api/shopper/users?client="+encodeURIComponent(client),{headers:hdrs()}).then(function(r){return r.json()}).then(function(r){
29613
+ if(!r.ok){alertTo.innerHTML='<option value="">'+esc(r.error||"The users could not be read.")+' Your own login is used if your email matches one.</option>';usersLoaded=false;return}
29614
+ alertTo.setAttribute("data-loaded","1");
29615
+ var me=base(document.getElementById("owner-email").value);
29616
+ alertTo.innerHTML='<option value="">Choose a person</option>'+r.users.map(function(u){return '<option value="'+esc(u.id)+'"'+(u.email&&base(u.email)===me?" selected":"")+'>'+esc(u.name)+(u.email?" ("+esc(u.email)+")":"")+'</option>'}).join("");
29617
+ }).catch(function(){usersLoaded=false;alertTo.innerHTML='<option value="">The users could not be read. Your own login is used if your email matches one.</option>'});
29618
+ }
29619
+ if(ff)ff.addEventListener("change",refreshAlertTo);
29568
29620
  var look=document.getElementById("look-now");
29569
29621
  if(look&&run){look.addEventListener("click",function(){look.disabled=true;post("/api/shopper/finish",{client:client,runId:run.runId}).then(function(){location.reload()}).catch(function(){look.disabled=false})})}
29570
29622
  if(run&&run.status==="watching"){setTimeout(function(){location.reload()},30000)}
@@ -29663,7 +29715,7 @@ main.shopper{--sh-bad:#B91C1C;--sh-ok:#15803D;--sh-warn:#B45309;--sh-accent-ink:
29663
29715
  .shopper .checks .dot{flex:none;width:.7em;height:.7em;border-radius:50%;background:var(--sh-ok)}
29664
29716
  .shopper .plan{font-size:15px;color:var(--sh-muted);margin:.8rem 0 0}
29665
29717
  .shopper .startform{display:grid;gap:.5rem;max-width:44rem}
29666
- .shopper .startform input[type=email],.shopper .startform input[type=tel],.shopper .startform select{font:inherit;padding:.5rem .6rem;border:1px solid var(--sh-line);border-radius:10px;background:var(--sh-surface)}
29718
+ .shopper .startform input[type=email],.shopper .startform input[type=tel],.shopper .startform input[type=text],.shopper .startform select{font:inherit;padding:.5rem .6rem;border:1px solid var(--sh-line);border-radius:10px;background:var(--sh-surface)}
29667
29719
  .shopper fieldset{border:1px solid var(--sh-line);border-radius:12px;padding:.6rem .9rem}
29668
29720
  .shopper legend{font-weight:600;padding:0 .3rem}
29669
29721
  .shopper .wf{display:grid;gap:.25rem;max-height:18rem;overflow:auto}
@@ -29807,7 +29859,7 @@ function analyzeMessages(messages, ctx) {
29807
29859
  const want = normal(e.match);
29808
29860
  const got = sorted.some((m) => (!e.channel || e.channel === m.channel) && want && normal(messageText(m)).includes(want));
29809
29861
  if (got) continue;
29810
- const row = { label: e.label, channel: e.channel, byMinute: e.byMinute };
29862
+ const row = { label: e.label, channel: e.channel, byMinute: e.byMinute, workflowId: e.workflowId, stepId: e.stepId, afterWaits: e.afterWaits };
29811
29863
  (e.byMinute <= ctx.windowMinutes ? missing : notYetDue).push(row);
29812
29864
  }
29813
29865
  const sentBy = /* @__PURE__ */ new Map();
@@ -30009,7 +30061,7 @@ function judgeSteps(rawRows, contactId, workflowNames) {
30009
30061
  else if (outcome === "unclear") detail = `GoHighLevel logged "${status || "no status"}" for this step, which this version does not know how to read. Check it in GoHighLevel's Execution logs.`;
30010
30062
  else if (outcome === "ran") detail = type === "wait_finished" ? "The wait ended." : reason3 && !/^\{/.test(reason3) ? reason3 : "Done.";
30011
30063
  else detail = translate(type, outcome, reason3, retryAt).detail;
30012
- rows2.push({ ...base, detail, retryAt: outcome === "failed" && !gaveUp ? retryAt : void 0, reason: reason3 || void 0, ...gaveUp ? { gaveUp: true } : {} });
30064
+ rows2.push({ ...base, detail, retryAt: outcome === "failed" && !gaveUp ? retryAt : void 0, ...outcome === "waiting" && retryAt ? { waitUntil: retryAt } : {}, reason: reason3 || void 0, ...gaveUp ? { gaveUp: true } : {} });
30013
30065
  }
30014
30066
  const sameStep = (a, b) => a.workflowId === b.workflowId && a.pass === b.pass && (a.stepId ?? a.stepName) === (b.stepId ?? b.stepName);
30015
30067
  const problems = [];
@@ -30131,41 +30183,452 @@ var init_steps = __esm({
30131
30183
  }
30132
30184
  });
30133
30185
 
30186
+ // src/command-os/shopper/fastforward.ts
30187
+ var fastforward_exports = {};
30188
+ __export(fastforward_exports, {
30189
+ FF_PREFIX: () => FF_PREFIX,
30190
+ SWEEP_MIN_AGE_MS: () => SWEEP_MIN_AGE_MS,
30191
+ builderAdmin: () => builderAdmin,
30192
+ journal: () => journal,
30193
+ makeCopies: () => makeCopies,
30194
+ newToken: () => newToken,
30195
+ normalizeHost: () => normalizeHost,
30196
+ openCopies: () => openCopies,
30197
+ planCopy: () => planCopy,
30198
+ remapSelf: () => remapSelf,
30199
+ remapWorkflows: () => remapWorkflows,
30200
+ removeCopies: () => removeCopies,
30201
+ staffRecipients: () => staffRecipients,
30202
+ sweep: () => sweep,
30203
+ toSource: () => toSource,
30204
+ webhookDestinations: () => webhookDestinations
30205
+ });
30206
+ function windowText(raw) {
30207
+ const w = asObj3(raw);
30208
+ if (!w.start || !w.end) return void 0;
30209
+ const days = asArr2(w.days).map((d) => typeof d === "number" ? DAY_NAMES[d] ?? String(d) : String(d));
30210
+ const cond = str4(w.condition);
30211
+ return `runs only ${str4(w.start)}\u2013${str4(w.end)}${days.length ? ` on ${days.join(", ")}` : ""}${cond ? ` (${cond})` : ""}`;
30212
+ }
30213
+ function rerouteAlert(attrs, owner) {
30214
+ const channel = str4(attrs.type) ?? "notification";
30215
+ if (channel === "notification") {
30216
+ const n = asObj3(attrs.notification);
30217
+ attrs.notification = { body: n.body ?? "", title: `TEST: ${str4(n.title) ?? ""}`.trim(), userType: "user", redirectPage: n.redirectPage ?? "contact", type: n.type ?? "send_notification", selectedUser: owner };
30218
+ delete attrs.email;
30219
+ delete attrs.sms;
30220
+ return void 0;
30221
+ }
30222
+ if (channel === "email") {
30223
+ const e = asObj3(attrs.email);
30224
+ attrs.email = { isCloned: false, html: e.html ?? "", subject: `TEST: ${str4(e.subject) ?? ""}`.trim(), selectedUser: [owner], userType: "user", attachments: [] };
30225
+ delete attrs.notification;
30226
+ delete attrs.sms;
30227
+ return void 0;
30228
+ }
30229
+ return channel;
30230
+ }
30231
+ function staffRecipients(step) {
30232
+ const type = str4(step.type) ?? "";
30233
+ const a = asObj3(step.attributes);
30234
+ if (type === "internal_notification") {
30235
+ const out = [];
30236
+ for (const k of ["notification", "email", "sms"]) {
30237
+ const ch = asObj3(a[k]);
30238
+ if (!Object.keys(ch).length) continue;
30239
+ if (str4(ch.userType) !== "user") out.push(`everyone (${k})`);
30240
+ for (const u of Array.isArray(ch.selectedUser) ? ch.selectedUser : [ch.selectedUser]) if (typeof u === "string") out.push(u);
30241
+ }
30242
+ return out;
30243
+ }
30244
+ if (type === "task-notification") return [str4(a.assignedTo) ?? "nobody"];
30245
+ if (type === "assign_user") return [...asArr2(a.user_list), ...asArr2(a.customUserList)].map((u) => typeof u === "string" ? u : JSON.stringify(u));
30246
+ if (type === "internal_create_opportunity" || type === "internal_update_opportunity") {
30247
+ return [...a.assignedTo !== void 0 ? [String(a.assignedTo)] : [], ...asArr2(a.__customInputFields__).map(asObj3).filter((f) => /assign/i.test(str4(f.filterField) ?? "")).map((f) => String(f.value))];
30248
+ }
30249
+ return [];
30250
+ }
30251
+ function waitLength(a) {
30252
+ const s = asObj3(a.startAfter);
30253
+ const v = Number(s.value);
30254
+ const unit = (str4(s.type) ?? "").toLowerCase();
30255
+ const word = unit.startsWith("min") ? "minute" : unit.startsWith("hour") ? "hour" : unit.startsWith("day") ? "day" : unit;
30256
+ return `${v} ${word}${v === 1 ? "" : "s"}`;
30257
+ }
30258
+ function webhookDestinations(sources) {
30259
+ const out = [];
30260
+ for (const s of sources) {
30261
+ for (const n of s.templates.map(asObj3)) {
30262
+ if (str4(n.type) !== "webhook") continue;
30263
+ const url = str4(asObj3(n.attributes).url) ?? "";
30264
+ const h = hostOf(url);
30265
+ const merged = /\{\{|%7b/i.test(url.split(/[/?#]/).slice(0, 3).join("/")) || h !== void 0 && /[{}]|%7b|%7d/i.test(h);
30266
+ const problem = merged ? "merge_field" : !h ? "no_address" : void 0;
30267
+ out.push({ ...problem ? { problem } : { host: h }, stepName: str4(n.name) ?? "Webhook", workflowName: s.name });
30268
+ }
30269
+ }
30270
+ return out;
30271
+ }
30272
+ function planCopy(src, opts) {
30273
+ const allowed = opts.allowedHosts.map(normalizeHost).filter(Boolean);
30274
+ const nodes = src.templates.map(asObj3);
30275
+ if (nodes.length === 0) return { ok: false, refusal: `"${src.name}" has no steps to test.` };
30276
+ if (nodes.length > 40) return { ok: false, refusal: `"${src.name}" has ${nodes.length} steps; fast-forward copies at most 40.` };
30277
+ if (str4(src.status)?.toLowerCase() === "draft") return { ok: false, refusal: `"${src.name}" is still a draft, so real leads never go through it. Publish it first, or test it as it is.` };
30278
+ const idMap = /* @__PURE__ */ new Map();
30279
+ for (const n of nodes) {
30280
+ const id = str4(n.id);
30281
+ if (id) idMap.set(id, import_node_crypto7.default.randomUUID());
30282
+ }
30283
+ const remap = (id) => typeof id === "string" ? idMap.get(id) ?? id : id;
30284
+ const waits = [];
30285
+ const hosts = /* @__PURE__ */ new Set();
30286
+ const actions = [];
30287
+ for (const n of nodes) {
30288
+ const type = str4(n.type) ?? "";
30289
+ const name = str4(n.name) ?? type;
30290
+ if (type === "if_else" || str4(n.nodeType)?.startsWith("branch") || str4(n.cat) === "conditions") {
30291
+ return { ok: false, refusal: `"${src.name}" has an if/else branch ("${name}"). Fast-forward handles straight-line workflows for now; branches come next, with test leads that book, reply or stay silent.` };
30292
+ }
30293
+ if (type === "add_to_workflow" || type === "goto") {
30294
+ return { ok: false, refusal: `"${src.name}" hands the lead to another place ("${name}"). Following a lead into other workflows is the next piece of fast-forward.` };
30295
+ }
30296
+ if (!ALLOWED.has(type)) {
30297
+ return { ok: false, refusal: `"${src.name}" has a "${type}" step ("${name}") that fast-forward does not know how to copy safely yet, so nothing was copied.` };
30298
+ }
30299
+ const attrs = { ...asObj3(n.attributes) };
30300
+ if (type === "wait") {
30301
+ if (str4(attrs.type) !== "time" || !asObj3(attrs.startAfter).type) {
30302
+ return { ok: false, refusal: `"${src.name}" has a wait that is not a plain time wait ("${name}"). Fast-forward handles time waits only for now.` };
30303
+ }
30304
+ waits.push(`${name}: ${waitLength(attrs)} in real life`);
30305
+ attrs.startAfter = { type: "minutes", value: 1, when: "after" };
30306
+ }
30307
+ if (type === "internal_notification") {
30308
+ if (!opts.ownerUserId) return { ok: false, refusal: 'During a fast-forward check, staff alerts go to one person instead of your staff, but nobody was chosen and your email is not a user in this account. Choose who should get them (on the Secret shopper screen, "Staff alerts, tasks and assignments go to"; in chat, name the person).' };
30309
+ const unknown = rerouteAlert(attrs, opts.ownerUserId);
30310
+ if (unknown) return { ok: false, refusal: `"${src.name}" has a staff alert sent by ${unknown === "sms" ? "text" : `"${unknown}"`} ("${name}"). Fast-forward cannot yet send that kind to you instead of your staff, so nothing was copied.` };
30311
+ }
30312
+ if (type === "assign_user") {
30313
+ if (!opts.ownerUserId) return { ok: false, refusal: 'During a fast-forward check, leads are assigned to one person instead of your staff, but nobody was chosen and your email is not a user in this account. Choose who should get them (on the Secret shopper screen, "Staff alerts, tasks and assignments go to"; in chat, name the person).' };
30314
+ if (asArr2(attrs.customUserList).length) return { ok: false, refusal: `"${src.name}" assigns the lead from a custom value ("${name}"). Fast-forward cannot yet send that assignment to you instead of your staff, so nothing was copied.` };
30315
+ const o = opts.ownerUserId;
30316
+ Object.assign(attrs, { user_list: [o], traffic_split: "equally", traffic_weightage: { [o]: 1 }, traffic_index: [{ id: o, indexes: [1] }], total_index: 1 });
30317
+ }
30318
+ if (type === "internal_create_opportunity" || type === "internal_update_opportunity") {
30319
+ const assigns = attrs.assignedTo !== void 0 || asArr2(attrs.__customInputFields__).map(asObj3).some((f) => /assign/i.test(str4(f.filterField) ?? ""));
30320
+ if (assigns) {
30321
+ if (!opts.ownerUserId) return { ok: false, refusal: 'During a fast-forward check, cards are assigned to one person instead of your staff, but nobody was chosen and your email is not a user in this account. Choose who should get them (on the Secret shopper screen, "Staff alerts, tasks and assignments go to"; in chat, name the person).' };
30322
+ if (attrs.assignedTo !== void 0) attrs.assignedTo = opts.ownerUserId;
30323
+ attrs.__customInputFields__ = asArr2(attrs.__customInputFields__).map(asObj3).map((f) => /assign/i.test(str4(f.filterField) ?? "") ? { ...f, value: opts.ownerUserId } : f);
30324
+ }
30325
+ }
30326
+ if (type === "task-notification") {
30327
+ if (!opts.ownerUserId) return { ok: false, refusal: 'During a fast-forward check, tasks go to one person instead of your staff, but nobody was chosen and your email is not a user in this account. Choose who should get them (on the Secret shopper screen, "Staff alerts, tasks and assignments go to"; in chat, name the person).' };
30328
+ attrs.assignedTo = opts.ownerUserId;
30329
+ attrs.title = `TEST: ${str4(attrs.title) ?? ""}`.trim();
30330
+ }
30331
+ if (type === "webhook") {
30332
+ const url = str4(attrs.url) ?? "";
30333
+ if (/^[^/]*\/\/[^/?#]*\{\{/.test(url) || /^\{\{/.test(url)) return { ok: false, refusal: `"${src.name}" has a webhook ("${name}") whose address is filled in from a merge field, so where the test lead would go cannot be checked. Nothing was copied.` };
30334
+ const h = hostOf(url);
30335
+ if (h && /[{}]|%7b|%7d/i.test(h)) return { ok: false, refusal: `"${src.name}" has a webhook ("${name}") whose address is filled in from a merge field, so where the test lead would go cannot be checked. Nothing was copied.` };
30336
+ if (!h) return { ok: false, refusal: `"${src.name}" has a webhook ("${name}") without a readable address.` };
30337
+ hosts.add(h);
30338
+ if (!allowed.includes(h)) {
30339
+ return { ok: false, refusal: `"${src.name}" sends the lead to ${h} ("${name}"). A fast-forward check sends the test lead there for real, so tick ${h} to allow it.` };
30340
+ }
30341
+ }
30342
+ const copy = { ...n, id: remap(n.id), attributes: attrs };
30343
+ if (n.next !== void 0) copy.next = Array.isArray(n.next) ? n.next.map(remap) : remap(n.next);
30344
+ if (n.parentKey !== void 0) copy.parentKey = remap(n.parentKey);
30345
+ if (n.parent !== void 0) copy.parent = remap(n.parent);
30346
+ if (Array.isArray(n.sibling)) copy.sibling = n.sibling.map(remap);
30347
+ actions.push(copy);
30348
+ }
30349
+ const w = asObj3(src.window);
30350
+ return {
30351
+ ok: true,
30352
+ plan: {
30353
+ sourceId: src.id,
30354
+ sourceName: src.name,
30355
+ actions,
30356
+ waits,
30357
+ window: windowText(w),
30358
+ webhookHosts: [...hosts],
30359
+ settings: { stopOnResponse: src.stopOnResponse, allowMultiple: src.allowMultiple, removeContactFromLastStep: src.removeContactFromLastStep }
30360
+ }
30361
+ };
30362
+ }
30363
+ function remapWorkflows(value, sourceToCopy, key) {
30364
+ const REF = /* @__PURE__ */ new Set(["workflow_id", "workflowId", "targetWorkflowId"]);
30365
+ if (typeof value === "string") return key !== void 0 && REF.has(key) ? sourceToCopy.get(value) ?? value : value;
30366
+ if (Array.isArray(value)) return value.map((v) => remapWorkflows(v, sourceToCopy, key));
30367
+ if (value && typeof value === "object") {
30368
+ const out = {};
30369
+ for (const [k, v] of Object.entries(value)) out[k] = remapWorkflows(v, sourceToCopy, k);
30370
+ return out;
30371
+ }
30372
+ return value;
30373
+ }
30374
+ function journal(e, base) {
30375
+ const f = journalFile(base);
30376
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(f), { recursive: true, mode: 448 });
30377
+ import_node_fs2.default.appendFileSync(f, JSON.stringify(e) + "\n", { encoding: "utf8", mode: 384 });
30378
+ }
30379
+ function openCopies(locationId2, base) {
30380
+ let lines = [];
30381
+ try {
30382
+ lines = import_node_fs2.default.readFileSync(journalFile(base), "utf8").split("\n").filter(Boolean);
30383
+ } catch {
30384
+ return [];
30385
+ }
30386
+ const byName = /* @__PURE__ */ new Map();
30387
+ for (const l of lines) {
30388
+ let e;
30389
+ try {
30390
+ e = JSON.parse(l);
30391
+ } catch {
30392
+ continue;
30393
+ }
30394
+ if (e.locationId !== locationId2) continue;
30395
+ const cur = byName.get(e.name) ?? { name: e.name, at: e.at, deleted: false };
30396
+ if (e.copyId) cur.copyId = e.copyId;
30397
+ if (e.event === "deleted") cur.deleted = true;
30398
+ byName.set(e.name, cur);
30399
+ }
30400
+ return [...byName.values()].filter((x) => !x.deleted).map(({ deleted: _d, ...x }) => x);
30401
+ }
30402
+ function toSource(id, raw) {
30403
+ const wf = asObj3(raw);
30404
+ return {
30405
+ id,
30406
+ name: str4(wf.name) ?? id,
30407
+ status: str4(wf.status),
30408
+ templates: asArr2(asObj3(wf.workflowData).templates),
30409
+ triggers: asArr2(wf.triggers),
30410
+ stopOnResponse: typeof wf.stopOnResponse === "boolean" ? wf.stopOnResponse : void 0,
30411
+ allowMultiple: typeof wf.allowMultiple === "boolean" ? wf.allowMultiple : void 0,
30412
+ removeContactFromLastStep: typeof wf.removeContactFromLastStep === "boolean" ? wf.removeContactFromLastStep : void 0,
30413
+ window: wf.window
30414
+ };
30415
+ }
30416
+ async function makeCopies(admin, locationId2, sourceIds, opts) {
30417
+ const now = opts.now ?? Date.now;
30418
+ if (sourceIds.length > 5) return { ok: false, refusal: "A fast-forward check copies at most 5 workflows.", copies: [] };
30419
+ const plans = [];
30420
+ for (const id of sourceIds) {
30421
+ const r = planCopy(toSource(id, await admin.get(id)), opts);
30422
+ if (!r.ok) return { ok: false, refusal: r.refusal, copies: [] };
30423
+ plans.push(r.plan);
30424
+ }
30425
+ if (!/^[0-9a-z]{6}$/.test(opts.token)) return { ok: false, refusal: "Internal: a fast-forward token must be 6 letters or digits.", copies: [] };
30426
+ const copies = [];
30427
+ const fail = (e) => ({ ok: false, refusal: `The fast-forward copy could not be set up: ${e instanceof Error ? e.message.split("\n")[0] : String(e)} The copies made so far are being removed.`, copies });
30428
+ for (const p of plans) {
30429
+ const name = Array.from(`${FF_PREFIX} ${opts.token} ${p.sourceName}`).slice(0, 120).join("");
30430
+ journal({ at: now(), locationId: locationId2, event: "creating", name }, opts.base);
30431
+ try {
30432
+ const copyId = (await admin.create(name)).id;
30433
+ if (!copyId || sourceIds.includes(copyId)) throw new Error("GoHighLevel did not return a new workflow id.");
30434
+ journal({ at: now(), locationId: locationId2, event: "created", name, copyId }, opts.base);
30435
+ copies.push({ copyId, copyName: name, sourceId: p.sourceId, sourceName: p.sourceName, waits: p.waits, window: p.window });
30436
+ } catch (e) {
30437
+ return fail(e);
30438
+ }
30439
+ }
30440
+ const sourceToCopy = new Map(copies.map((c) => [c.sourceId, c.copyId]));
30441
+ for (const [i, p] of plans.entries()) {
30442
+ const { copyId, copyName } = copies[i];
30443
+ try {
30444
+ await admin.save(copyId, { actions: remapWorkflows(p.actions, sourceToCopy), triggers: [], status: "draft", window: null, ...p.settings });
30445
+ const back = asObj3(await admin.get(copyId));
30446
+ const steps = asArr2(asObj3(back.workflowData).templates).map(asObj3);
30447
+ const longWait = steps.find((s) => str4(s.type) === "wait" && Number(asObj3(asObj3(s.attributes).startAfter).value) !== 1);
30448
+ if (asArr2(back.triggers).length !== 0) throw new Error(`the copy of "${p.sourceName}" came back with a trigger, so it was not switched on.`);
30449
+ if (steps.length !== p.actions.length) throw new Error(`the copy of "${p.sourceName}" came back with ${steps.length} steps instead of ${p.actions.length}.`);
30450
+ if (longWait) throw new Error(`a wait in the copy of "${p.sourceName}" did not shorten.`);
30451
+ if (!(str4(back.name) ?? "").startsWith(tokenPrefix(copyName))) throw new Error("the copy's name did not save.");
30452
+ const stray = steps.flatMap(staffRecipients).find((u) => u !== opts.ownerUserId);
30453
+ if (stray) throw new Error(`a staff step in the copy of "${p.sourceName}" still reaches ${stray} instead of the person chosen, so it was not switched on.`);
30454
+ if (steps.some((s) => sourceIds.some((id) => JSON.stringify(s).includes(`"${id}"`)))) throw new Error(`a step in the copy of "${p.sourceName}" still points at an original workflow, so it was not switched on.`);
30455
+ await admin.publish(copyId);
30456
+ } catch (e) {
30457
+ return fail(e);
30458
+ }
30459
+ }
30460
+ return { ok: true, copies };
30461
+ }
30462
+ async function removeCopies(admin, locationId2, copies, base, now = Date.now) {
30463
+ const problems = [];
30464
+ for (const c of copies) {
30465
+ if (!c.copyId) continue;
30466
+ try {
30467
+ const wf = asObj3(await admin.get(c.copyId));
30468
+ const name = str4(wf.name) ?? "";
30469
+ const ours = TOKEN_NAME.test(c.name) && (c.matchedByName ? name === c.name : name.startsWith(tokenPrefix(c.name)));
30470
+ if (!ours) {
30471
+ problems.push(`Left alone: workflow ${c.copyId} is not a fast-forward copy this check made (named "${name}").`);
30472
+ continue;
30473
+ }
30474
+ const parked = await admin.parked(c.copyId);
30475
+ if (parked > (c.matchedByName ? 0 : 1)) {
30476
+ problems.push(`Not removed: ${parked} contact${parked === 1 ? " is" : "s are"} in "${name}", more than our test lead. Check it in GoHighLevel.`);
30477
+ continue;
30478
+ }
30479
+ await admin.remove(c.copyId, parked || void 0);
30480
+ journal({ at: now(), locationId: locationId2, event: "deleted", name: c.name, copyId: c.copyId }, base);
30481
+ } catch (e) {
30482
+ const msg3 = e instanceof Error ? e.message : String(e);
30483
+ if (/\b404\b|not found/i.test(msg3)) {
30484
+ journal({ at: now(), locationId: locationId2, event: "deleted", name: c.name, copyId: c.copyId }, base);
30485
+ continue;
30486
+ }
30487
+ problems.push(`The copy "${c.name}" could not be removed (${msg3.split("\n")[0]}). Delete it in GoHighLevel.`);
30488
+ }
30489
+ }
30490
+ return problems;
30491
+ }
30492
+ async function sweep(admin, locationId2, base, now = Date.now, keep = []) {
30493
+ const open = openCopies(locationId2, base).filter((o) => (!o.copyId || !keep.includes(o.copyId)) && now() - o.at >= SWEEP_MIN_AGE_MS && TOKEN_NAME.test(o.name));
30494
+ if (open.length === 0) return [];
30495
+ const targets = open.filter((o) => o.copyId);
30496
+ const unnamed = open.filter((o) => !o.copyId);
30497
+ if (unnamed.length) {
30498
+ try {
30499
+ const all = await admin.list();
30500
+ for (const o of unnamed) {
30501
+ const hits = all.filter((w) => w.name === o.name && !keep.includes(w.id));
30502
+ if (hits.length === 1) targets.push({ copyId: hits[0].id, name: o.name, matchedByName: true });
30503
+ }
30504
+ } catch {
30505
+ }
30506
+ }
30507
+ return removeCopies(admin, locationId2, targets, base, now);
30508
+ }
30509
+ function builderAdmin(b) {
30510
+ return {
30511
+ get: (id) => b.getWorkflow(id),
30512
+ list: async () => {
30513
+ const out = [];
30514
+ for (let skip = 0; skip < 1e3; skip += 100) {
30515
+ const rows2 = asArr2(asObj3(await b.listWorkflows(100, skip)).rows).map(asObj3);
30516
+ for (const r of rows2) {
30517
+ const id = str4(r._id) ?? str4(r.id);
30518
+ if (id) out.push({ id, name: str4(r.name) ?? "", createdAt: str4(r.createdAt) });
30519
+ }
30520
+ if (rows2.length < 100) break;
30521
+ }
30522
+ return out;
30523
+ },
30524
+ create: async (name) => {
30525
+ const w = asObj3(await b.createWorkflow(name));
30526
+ return { id: str4(w._id) ?? str4(w.id) ?? "" };
30527
+ },
30528
+ save: (id, body) => b.updateWorkflow(id, body),
30529
+ publish: (id) => b.publishWorkflow(id),
30530
+ parked: async (id) => (await b.parkedContacts(id)).reduce((sum, p) => sum + (Number(p.total) || 0), 0),
30531
+ remove: (id, ack) => b.deleteWorkflow(id, ack ? { acknowledgeContacts: ack } : void 0)
30532
+ };
30533
+ }
30534
+ var import_node_fs2, import_node_path2, import_node_crypto7, asObj3, asArr2, str4, FF_PREFIX, ALLOWED, hostOf, normalizeHost, newToken, TOKEN_NAME, tokenPrefix, DAY_NAMES, remapSelf, journalFile, SWEEP_MIN_AGE_MS;
30535
+ var init_fastforward = __esm({
30536
+ "src/command-os/shopper/fastforward.ts"() {
30537
+ "use strict";
30538
+ import_node_fs2 = __toESM(require("node:fs"));
30539
+ import_node_path2 = __toESM(require("node:path"));
30540
+ import_node_crypto7 = __toESM(require("node:crypto"));
30541
+ init_run();
30542
+ asObj3 = (v) => v && typeof v === "object" ? v : {};
30543
+ asArr2 = (v) => Array.isArray(v) ? v : [];
30544
+ str4 = (v) => typeof v === "string" ? v : void 0;
30545
+ FF_PREFIX = "ZZ TEST FF";
30546
+ ALLOWED = /* @__PURE__ */ new Set([
30547
+ "email",
30548
+ "sms",
30549
+ "wait",
30550
+ "add_contact_tag",
30551
+ "remove_contact_tag",
30552
+ "update_contact_field",
30553
+ "internal_create_opportunity",
30554
+ "internal_update_opportunity",
30555
+ "assign_user",
30556
+ "internal_notification",
30557
+ "task-notification",
30558
+ "add_notes",
30559
+ "webhook",
30560
+ "remove_from_workflow"
30561
+ ]);
30562
+ hostOf = (url) => {
30563
+ try {
30564
+ return new URL(url).hostname.toLowerCase();
30565
+ } catch {
30566
+ return void 0;
30567
+ }
30568
+ };
30569
+ normalizeHost = (h) => {
30570
+ const t = h.trim().toLowerCase();
30571
+ if (!t) return "";
30572
+ return hostOf(/^[a-z][a-z0-9+.-]*:\/\//.test(t) ? t : `https://${t}`) ?? t;
30573
+ };
30574
+ newToken = () => {
30575
+ let t = "";
30576
+ while (t.length < 6) t += (import_node_crypto7.default.randomBytes(1)[0] % 36).toString(36);
30577
+ return t;
30578
+ };
30579
+ TOKEN_NAME = new RegExp(`^${FF_PREFIX} [0-9a-z]{6} `);
30580
+ tokenPrefix = (name) => name.match(TOKEN_NAME)?.[0] ?? "\0never";
30581
+ DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
30582
+ remapSelf = (value, sourceId, copyId) => remapWorkflows(value, /* @__PURE__ */ new Map([[sourceId, copyId]]));
30583
+ journalFile = (base) => import_node_path2.default.join(shopperDir(base), "fast-forward-journal.jsonl");
30584
+ SWEEP_MIN_AGE_MS = 15 * 6e4;
30585
+ }
30586
+ });
30587
+
30134
30588
  // src/command-os/shopper/run.ts
30589
+ function heldWaits(rows2) {
30590
+ const out = [];
30591
+ for (const r of rows2) {
30592
+ if (r.outcome !== "waiting") continue;
30593
+ const later = rows2.some((x) => x !== r && x.workflowId === r.workflowId && x.at >= r.at && (x.stepId === r.stepId || x.outcome === "finished"));
30594
+ if (!later) out.push(r);
30595
+ }
30596
+ return out;
30597
+ }
30135
30598
  function shopperDir(base = appDataDir()) {
30136
- return import_node_path2.default.join(base, "secret-shopper");
30599
+ return import_node_path3.default.join(base, "secret-shopper");
30137
30600
  }
30138
30601
  function runPath(locationId2, runId, base) {
30139
30602
  if (!SAFE_ID.test(locationId2) || !SAFE_ID.test(runId)) throw new Error("That client or run id is not valid.");
30140
- return import_node_path2.default.join(shopperDir(base), locationId2, `${runId}.json`);
30603
+ return import_node_path3.default.join(shopperDir(base), locationId2, `${runId}.json`);
30141
30604
  }
30142
30605
  function saveRun(run, base) {
30143
30606
  const file2 = runPath(run.locationId, run.runId, base);
30144
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file2), { recursive: true, mode: 448 });
30607
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(file2), { recursive: true, mode: 448 });
30145
30608
  const tmp = `${file2}.${process.pid}.${Date.now()}.tmp`;
30146
- import_node_fs2.default.writeFileSync(tmp, JSON.stringify(run, null, 1), { mode: 384 });
30147
- import_node_fs2.default.renameSync(tmp, file2);
30609
+ import_node_fs3.default.writeFileSync(tmp, JSON.stringify(run, null, 1), { mode: 384 });
30610
+ import_node_fs3.default.renameSync(tmp, file2);
30148
30611
  }
30149
30612
  function readRun(locationId2, runId, base) {
30150
30613
  try {
30151
- return JSON.parse(import_node_fs2.default.readFileSync(runPath(locationId2, runId, base), "utf8"));
30614
+ return JSON.parse(import_node_fs3.default.readFileSync(runPath(locationId2, runId, base), "utf8"));
30152
30615
  } catch {
30153
30616
  return void 0;
30154
30617
  }
30155
30618
  }
30156
30619
  function listRuns(locationId2, base) {
30157
30620
  if (!SAFE_ID.test(locationId2)) return [];
30158
- const dir = import_node_path2.default.join(shopperDir(base), locationId2);
30621
+ const dir = import_node_path3.default.join(shopperDir(base), locationId2);
30159
30622
  let names = [];
30160
30623
  try {
30161
- names = import_node_fs2.default.readdirSync(dir).filter((n) => n.endsWith(".json"));
30624
+ names = import_node_fs3.default.readdirSync(dir).filter((n) => n.endsWith(".json"));
30162
30625
  } catch {
30163
30626
  return [];
30164
30627
  }
30165
30628
  const runs = [];
30166
30629
  for (const n of names) {
30167
30630
  try {
30168
- runs.push(JSON.parse(import_node_fs2.default.readFileSync(import_node_path2.default.join(dir, n), "utf8")));
30631
+ runs.push(JSON.parse(import_node_fs3.default.readFileSync(import_node_path3.default.join(dir, n), "utf8")));
30169
30632
  } catch {
30170
30633
  }
30171
30634
  }
@@ -30195,7 +30658,7 @@ function clampMinutes(n) {
30195
30658
  return Math.min(1440, Math.max(1, Math.round(n)));
30196
30659
  }
30197
30660
  function channelOf(msg3) {
30198
- const mt = (str4(msg3.messageType) ?? "").toUpperCase();
30661
+ const mt = (str5(msg3.messageType) ?? "").toUpperCase();
30199
30662
  if (mt === "TYPE_SMS") return "sms";
30200
30663
  if (mt === "TYPE_EMAIL") return "email";
30201
30664
  if (!mt) {
@@ -30206,30 +30669,30 @@ function channelOf(msg3) {
30206
30669
  return null;
30207
30670
  }
30208
30671
  async function readSentMessages(client, locationId2, contactId, since) {
30209
- const convs = asArr2(asObj3(await client.get("/conversations/search", { params: { locationId: locationId2, contactId, limit: 20 } })).conversations);
30672
+ const convs = asArr3(asObj4(await client.get("/conversations/search", { params: { locationId: locationId2, contactId, limit: 20 } })).conversations);
30210
30673
  const out = [];
30211
30674
  for (const c of convs) {
30212
- const convId = str4(asObj3(c).id);
30675
+ const convId = str5(asObj4(c).id);
30213
30676
  if (!convId) continue;
30214
- const raw = asObj3(await client.get(`/conversations/${convId}/messages`, { params: { limit: 100 } }));
30215
- const list2 = asArr2(raw.messages).length ? asArr2(raw.messages) : asArr2(asObj3(raw.messages).messages);
30677
+ const raw = asObj4(await client.get(`/conversations/${convId}/messages`, { params: { limit: 100 } }));
30678
+ const list2 = asArr3(raw.messages).length ? asArr3(raw.messages) : asArr3(asObj4(raw.messages).messages);
30216
30679
  for (const item of list2) {
30217
- const m = asObj3(item);
30218
- if (str4(m.direction)?.toLowerCase() !== "outbound") continue;
30680
+ const m = asObj4(item);
30681
+ if (str5(m.direction)?.toLowerCase() !== "outbound") continue;
30219
30682
  const channel = channelOf(m);
30220
30683
  if (!channel) continue;
30221
- const at = Date.parse(str4(m.dateAdded) ?? "");
30684
+ const at = Date.parse(str5(m.dateAdded) ?? "");
30222
30685
  if (Number.isNaN(at) || at < since - 5e3) continue;
30223
- const id = str4(m.id);
30686
+ const id = str5(m.id);
30224
30687
  if (!id) continue;
30225
30688
  out.push({
30226
30689
  id,
30227
30690
  channel,
30228
30691
  at,
30229
- subject: str4(asObj3(asObj3(m.meta).email).subject),
30230
- body: htmlToText(str4(m.body) ?? ""),
30231
- raw: str4(m.body) ?? "",
30232
- source: str4(m.source)
30692
+ subject: str5(asObj4(asObj4(m.meta).email).subject),
30693
+ body: htmlToText(str5(m.body) ?? ""),
30694
+ raw: str5(m.body) ?? "",
30695
+ source: str5(m.source)
30233
30696
  });
30234
30697
  }
30235
30698
  }
@@ -30237,16 +30700,16 @@ async function readSentMessages(client, locationId2, contactId, since) {
30237
30700
  }
30238
30701
  async function findContactIdByEmail(client, locationId2, email) {
30239
30702
  const raw = await client.get("/contacts/", { params: { locationId: locationId2, query: email, limit: 10 } });
30240
- for (const c of asArr2(asObj3(raw).contacts)) {
30241
- const co = asObj3(c);
30242
- if (str4(co.email)?.toLowerCase() === email) return str4(co.id);
30703
+ for (const c of asArr3(asObj4(raw).contacts)) {
30704
+ const co = asObj4(c);
30705
+ if (str5(co.email)?.toLowerCase() === email) return str5(co.id);
30243
30706
  }
30244
30707
  return void 0;
30245
30708
  }
30246
30709
  async function startShopper(input, deps) {
30247
30710
  const now = deps.now ?? Date.now;
30248
30711
  const sleep7 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
30249
- const token2 = import_node_crypto7.default.randomUUID().replace(/-/g, "").slice(0, 8);
30712
+ const token2 = import_node_crypto8.default.randomUUID().replace(/-/g, "").slice(0, 8);
30250
30713
  const leadEmail = plusAddress(input.ownerEmail, token2);
30251
30714
  if (!SAFE_ID.test(input.locationId)) throw new Error("That client id is not valid.");
30252
30715
  if (input.entry.kind === "workflows" && input.entry.workflowIds.length === 0) throw new Error("Choose at least one workflow for the test lead to enter.");
@@ -30260,9 +30723,9 @@ async function startShopper(input, deps) {
30260
30723
  const refused = protectedRefusalFor(input.locationId);
30261
30724
  if (refused) throw new Error(refused);
30262
30725
  }
30263
- const loc = asObj3(asObj3(await deps.client.get(`/locations/${input.locationId}`)).location);
30264
- const businessName = str4(loc.name) ?? "";
30265
- const websiteHost = siteHost(str4(loc.website));
30726
+ const loc = asObj4(asObj4(await deps.client.get(`/locations/${input.locationId}`)).location);
30727
+ const businessName = str5(loc.name) ?? "";
30728
+ const websiteHost = siteHost(str5(loc.website));
30266
30729
  const testPhone = input.testPhone?.trim() || void 0;
30267
30730
  let expect = input.expect ?? [];
30268
30731
  const planNote = { ...input.planNote ?? { source: expect.length ? "given" : "none" } };
@@ -30277,7 +30740,7 @@ async function startShopper(input, deps) {
30277
30740
  const watchMinutes = auto ? auto.minutes : clampMinutes(input.watchMinutes);
30278
30741
  const watchReason = auto ? auto.reason : `You chose ${watchMinutes} minute${watchMinutes === 1 ? "" : "s"}.`;
30279
30742
  const firstName = (input.firstName ?? "").trim();
30280
- const entryNote = input.entry.kind === "workflows" ? `Added straight to ${input.entry.workflowIds.length} workflow${input.entry.workflowIds.length === 1 ? "" : "s"}. The sign-up form itself was not submitted.` : input.entry.kind === "tag" ? `Created with the tag "${input.entry.tag}", which starts the workflows that listen for it. The sign-up form itself was not submitted.` : "Sent in through the lead-capture address, the way a form sends it.";
30743
+ const entryNote = input.fastForward?.length ? `Fast-forward: added to temporary copies of ${input.fastForward.length} workflow${input.fastForward.length === 1 ? "" : "s"} with every wait cut to 1 minute and every step kept; the originals were only read. Staff alerts, tasks and lead assignments came to you, marked TEST. The sign-up form itself was not submitted.` : input.entry.kind === "workflows" ? `Added straight to ${input.entry.workflowIds.length} workflow${input.entry.workflowIds.length === 1 ? "" : "s"}. The sign-up form itself was not submitted.` : input.entry.kind === "tag" ? `Created with the tag "${input.entry.tag}", which starts the workflows that listen for it. The sign-up form itself was not submitted.` : "Sent in through the lead-capture address, the way a form sends it.";
30281
30744
  const run = {
30282
30745
  v: 1,
30283
30746
  runId: `${new Date(now()).toISOString().replace(/[-:T]/g, "").slice(0, 14)}-${token2}`,
@@ -30297,6 +30760,7 @@ async function startShopper(input, deps) {
30297
30760
  planNote,
30298
30761
  workflowNames: input.workflowNames,
30299
30762
  draftWorkflows: input.draftWorkflows?.length ? input.draftWorkflows : void 0,
30763
+ fastForward: input.fastForward?.length ? { copies: input.fastForward } : void 0,
30300
30764
  keepLead: Boolean(input.keepLead),
30301
30765
  watchReason,
30302
30766
  status: "watching"
@@ -30326,8 +30790,8 @@ async function startShopper(input, deps) {
30326
30790
  };
30327
30791
  if (firstName) body.firstName = firstName;
30328
30792
  if (testPhone) body.phone = testPhone;
30329
- const created = asObj3(await deps.client.post("/contacts/", { body, noRetry: true }));
30330
- run.contactId = str4(asObj3(created.contact).id) ?? str4(created.id);
30793
+ const created = asObj4(await deps.client.post("/contacts/", { body, noRetry: true }));
30794
+ run.contactId = str5(asObj4(created.contact).id) ?? str5(created.id);
30331
30795
  if (!run.contactId) throw new Error("GoHighLevel did not return the new test lead's id.");
30332
30796
  if (input.entry.kind === "workflows") {
30333
30797
  for (const wid of input.entry.workflowIds) {
@@ -30359,13 +30823,13 @@ async function removeOwnLead(client, run) {
30359
30823
  if (!id) return { removed: false, detail: "No test lead was recorded for this check." };
30360
30824
  let contact;
30361
30825
  try {
30362
- contact = asObj3(asObj3(await client.get(`/contacts/${id}`)).contact);
30826
+ contact = asObj4(asObj4(await client.get(`/contacts/${id}`)).contact);
30363
30827
  } catch (e) {
30364
30828
  if (notFound(e)) return { removed: true, detail: "The test lead is already gone from the account." };
30365
30829
  return { removed: false, detail: `The test lead ${run.leadEmail} could not be checked before removal (${e instanceof Error ? e.message.split("\n")[0] : String(e)}). Delete it in GoHighLevel.` };
30366
30830
  }
30367
- const email = str4(contact.email)?.toLowerCase();
30368
- const added = Date.parse(str4(contact.dateAdded) ?? "");
30831
+ const email = str5(contact.email)?.toLowerCase();
30832
+ const added = Date.parse(str5(contact.dateAdded) ?? "");
30369
30833
  if (email !== run.leadEmail || !Number.isNaN(added) && added < run.startedAt - 10 * 6e4) {
30370
30834
  return { removed: false, detail: `Left alone on purpose: the contact this check ended up with is not the test lead it created (its address or creation time does not match ${run.leadEmail}). Look for the Secret Shopper contact in GoHighLevel.` };
30371
30835
  }
@@ -30388,20 +30852,20 @@ function pidAlive(pid) {
30388
30852
  }
30389
30853
  function claimRun(locationId2, runId, base) {
30390
30854
  const lock = `${runPath(locationId2, runId, base)}.lock`;
30391
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(lock), { recursive: true, mode: 448 });
30392
- const token2 = import_node_crypto7.default.randomUUID();
30855
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(lock), { recursive: true, mode: 448 });
30856
+ const token2 = import_node_crypto8.default.randomUUID();
30393
30857
  const mine = JSON.stringify({ pid: process.pid, token: token2 });
30394
30858
  for (let attempt = 0; attempt < 3; attempt++) {
30395
30859
  try {
30396
- const fd = import_node_fs2.default.openSync(lock, "wx", 384);
30860
+ const fd = import_node_fs3.default.openSync(lock, "wx", 384);
30397
30861
  try {
30398
- import_node_fs2.default.writeSync(fd, mine);
30862
+ import_node_fs3.default.writeSync(fd, mine);
30399
30863
  } finally {
30400
- import_node_fs2.default.closeSync(fd);
30864
+ import_node_fs3.default.closeSync(fd);
30401
30865
  }
30402
30866
  return () => {
30403
30867
  try {
30404
- if (import_node_fs2.default.readFileSync(lock, "utf8") === mine) import_node_fs2.default.unlinkSync(lock);
30868
+ if (import_node_fs3.default.readFileSync(lock, "utf8") === mine) import_node_fs3.default.unlinkSync(lock);
30405
30869
  } catch {
30406
30870
  }
30407
30871
  };
@@ -30410,8 +30874,8 @@ function claimRun(locationId2, runId, base) {
30410
30874
  let held = "";
30411
30875
  let age = 0;
30412
30876
  try {
30413
- held = import_node_fs2.default.readFileSync(lock, "utf8");
30414
- age = Date.now() - import_node_fs2.default.statSync(lock).mtimeMs;
30877
+ held = import_node_fs3.default.readFileSync(lock, "utf8");
30878
+ age = Date.now() - import_node_fs3.default.statSync(lock).mtimeMs;
30415
30879
  } catch {
30416
30880
  continue;
30417
30881
  }
@@ -30423,7 +30887,7 @@ function claimRun(locationId2, runId, base) {
30423
30887
  const stale = age > 10 * 6e4 || pid > 0 && !pidAlive(pid);
30424
30888
  if (!stale) return null;
30425
30889
  try {
30426
- if (import_node_fs2.default.readFileSync(lock, "utf8") === held) import_node_fs2.default.unlinkSync(lock);
30890
+ if (import_node_fs3.default.readFileSync(lock, "utf8") === held) import_node_fs3.default.unlinkSync(lock);
30427
30891
  } catch {
30428
30892
  }
30429
30893
  }
@@ -30459,10 +30923,35 @@ async function finishShopper(locationId2, runId, deps) {
30459
30923
  if (deps.stepLog && run.entry.kind === "workflows") {
30460
30924
  run.steps = await auditSteps(deps.stepLog, run.entry.workflowIds, run.workflowNames ?? {}, run.contactId, run.startedAt, t);
30461
30925
  }
30926
+ const held = closed && run.steps?.read ? heldWaits(run.steps.rows) : [];
30927
+ const base = run.originalClosesAt ?? run.closesAt;
30928
+ const cap = base + MAX_EXTEND_MINUTES * 6e4;
30929
+ const soon = held.filter((h) => h.waitUntil !== void 0 && h.waitUntil <= t + 5 * 6e4);
30930
+ if (soon.length && t < cap) {
30931
+ run.originalClosesAt = base;
30932
+ run.closesAt = Math.min(cap, Math.max(t, ...soon.map((h) => h.waitUntil)) + 3 * 6e4);
30933
+ run.extendedMinutes = Math.round((run.closesAt - base) / 6e4);
30934
+ saveRun(run, deps.base);
30935
+ return run;
30936
+ }
30937
+ const late = held.filter((h) => h.waitUntil !== void 0 && h.waitUntil <= t);
30938
+ if (closed && late.length) {
30939
+ const rows2 = run.steps.rows;
30940
+ const stillHeld = run.findings.missing.filter((m) => m.workflowId && m.stepId && late.some((h) => h.workflowId === m.workflowId && h.stepId !== void 0 && (m.afterWaits ?? []).includes(h.stepId)) && !rows2.some((r) => r.workflowId === m.workflowId && r.stepId === m.stepId));
30941
+ if (stillHeld.length) run.heldAt = late.map((h) => ({ workflowName: h.workflowName, stepName: h.stepName, until: h.waitUntil }));
30942
+ run.findings.missing = run.findings.missing.filter((m) => !stillHeld.includes(m));
30943
+ run.findings.notYetDue = [...run.findings.notYetDue, ...stillHeld.map((m) => ({ ...m, heldByWait: true }))];
30944
+ }
30462
30945
  if (closed) {
30463
30946
  run.status = "done";
30464
30947
  run.finishedAt = t;
30465
30948
  run.cleanup = run.keepLead ? { removed: false, detail: `Kept on request: the test lead ${run.leadEmail} is still in the account for you to look at. Delete it when you're done.` } : await removeOwnLead(deps.client, run);
30949
+ if (run.fastForward && deps.ffAdmin) {
30950
+ const { removeCopies: removeCopies2 } = await Promise.resolve().then(() => (init_fastforward(), fastforward_exports));
30951
+ const problems = await removeCopies2(deps.ffAdmin, run.locationId, run.fastForward.copies.map((c) => ({ copyId: c.copyId, name: c.copyName })), deps.base, now);
30952
+ run.fastForward.removed = problems.length === 0;
30953
+ run.fastForward.problems = problems.length ? problems : void 0;
30954
+ }
30466
30955
  }
30467
30956
  saveRun(run, deps.base);
30468
30957
  return run;
@@ -30477,7 +30966,8 @@ function summarizeRun(run) {
30477
30966
  if (run.status === "failed") return `The secret shopper could not run: ${run.error ?? "unknown error"}`;
30478
30967
  const f = run.findings;
30479
30968
  const st = run.steps;
30480
- const stepNote = (st?.read && st.problems.length ? ` ${st.problems.length} workflow step${st.problems.length === 1 ? "" : "s"} failed, skipped or did nothing.` : "") + (st && !st.read ? " The workflow steps could not be read, so only the messages were checked." : "") + (st?.read && st.rows.length === 0 ? " No workflow steps were logged for the test lead, so the steps are not checked." : "") + (st?.unreadWorkflows?.length ? ` Could not read the steps of: ${st.unreadWorkflows.join(", ")}.` : "") + (st?.partialWorkflows?.length ? ` Only partly read (very busy): ${st.partialWorkflows.join(", ")}.` : "") + (st?.counts.unclear ? ` ${st.counts.unclear} step${st.counts.unclear === 1 ? " has" : "s have"} a status this version cannot read.` : "") + (run.draftWorkflows?.length ? ` ${run.draftWorkflows.length === 1 ? "A chosen workflow is" : `${run.draftWorkflows.length} chosen workflows are`} still a draft, so GoHighLevel took the lead and ran nothing: ${run.draftWorkflows.join(", ")}.` : "");
30969
+ const heldNote = run.heldAt?.length && f?.notYetDue.some((m) => m.heldByWait) ? ` GoHighLevel was still holding the test lead at a wait that should have ended, so ${f.notYetDue.filter((m) => m.heldByWait).length} later message(s) were not judged; run the check again.` : "";
30970
+ const stepNote = heldNote + (st?.read && st.problems.length ? ` ${st.problems.length} workflow step${st.problems.length === 1 ? "" : "s"} failed, skipped or did nothing.` : "") + (st && !st.read ? " The workflow steps could not be read, so only the messages were checked." : "") + (st?.read && st.rows.length === 0 ? " No workflow steps were logged for the test lead, so the steps are not checked." : "") + (st?.unreadWorkflows?.length ? ` Could not read the steps of: ${st.unreadWorkflows.join(", ")}.` : "") + (st?.partialWorkflows?.length ? ` Only partly read (very busy): ${st.partialWorkflows.join(", ")}.` : "") + (st?.counts.unclear ? ` ${st.counts.unclear} step${st.counts.unclear === 1 ? " has" : "s have"} a status this version cannot read.` : "") + (run.draftWorkflows?.length ? ` ${run.draftWorkflows.length === 1 ? "A chosen workflow is" : `${run.draftWorkflows.length} chosen workflows are`} still a draft, so GoHighLevel took the lead and ran nothing: ${run.draftWorkflows.join(", ")}.` : "");
30481
30971
  const draftNow = run.draftWorkflows?.length ? ` Warning: ${run.draftWorkflows.join(", ")} ${run.draftWorkflows.length === 1 ? "is" : "are"} still a draft, so GoHighLevel will take the lead and run nothing there.` : "";
30482
30972
  if (!f) return `Watching: the test lead went in at ${new Date(run.startedAt).toISOString()}. The window closes at ${new Date(run.closesAt).toISOString()}.${draftNow}`;
30483
30973
  const n = f.problems.length;
@@ -30490,22 +30980,23 @@ function summarizeRun(run) {
30490
30980
  const head3 = `${f.sent} message${f.sent === 1 ? "" : "s"} sent, ${n === 0 ? "no problems a customer would notice" : `${n} problem${n === 1 ? "" : "s"} a customer would notice`}${f.missing.length ? `, ${f.missing.length} planned message${f.missing.length === 1 ? "" : "s"} never sent` : ""}.`;
30491
30981
  return run.status === "watching" ? `So far: ${head3}${stepNote} Still watching until ${new Date(run.closesAt).toISOString()}.` : head3 + stepNote;
30492
30982
  }
30493
- var import_node_fs2, import_node_path2, import_node_crypto7, asObj3, asArr2, str4, SAFE_ID, notFound;
30983
+ var import_node_fs3, import_node_path3, import_node_crypto8, MAX_EXTEND_MINUTES, asObj4, asArr3, str5, SAFE_ID, notFound;
30494
30984
  var init_run = __esm({
30495
30985
  "src/command-os/shopper/run.ts"() {
30496
30986
  "use strict";
30497
- import_node_fs2 = __toESM(require("node:fs"));
30498
- import_node_path2 = __toESM(require("node:path"));
30499
- import_node_crypto7 = __toESM(require("node:crypto"));
30987
+ import_node_fs3 = __toESM(require("node:fs"));
30988
+ import_node_path3 = __toESM(require("node:path"));
30989
+ import_node_crypto8 = __toESM(require("node:crypto"));
30500
30990
  init_credentials_store();
30501
30991
  init_analyze();
30502
30992
  init_links();
30503
30993
  init_mask();
30504
30994
  init_account_rules();
30505
30995
  init_steps();
30506
- asObj3 = (v) => v && typeof v === "object" ? v : {};
30507
- asArr2 = (v) => Array.isArray(v) ? v : [];
30508
- str4 = (v) => typeof v === "string" ? v : void 0;
30996
+ MAX_EXTEND_MINUTES = 10;
30997
+ asObj4 = (v) => v && typeof v === "object" ? v : {};
30998
+ asArr3 = (v) => Array.isArray(v) ? v : [];
30999
+ str5 = (v) => typeof v === "string" ? v : void 0;
30509
31000
  SAFE_ID = /^[A-Za-z0-9_-]{1,64}$/;
30510
31001
  notFound = (e) => {
30511
31002
  const m = e instanceof Error ? e.message : String(e);
@@ -30516,12 +31007,12 @@ var init_run = __esm({
30516
31007
 
30517
31008
  // src/command-os/shopper/plan.ts
30518
31009
  function waitMinutes(node) {
30519
- const a = asObj4(node.attributes);
30520
- if (str5(a.type) && str5(a.type) !== "time") return null;
30521
- const s = asObj4(a.startAfter);
31010
+ const a = asObj5(node.attributes);
31011
+ if (str6(a.type) && str6(a.type) !== "time") return null;
31012
+ const s = asObj5(a.startAfter);
30522
31013
  const v = Number(s.value);
30523
31014
  if (!Number.isFinite(v) || v < 0) return null;
30524
- const unit = (str5(s.type) ?? "").toLowerCase();
31015
+ const unit = (str6(s.type) ?? "").toLowerCase();
30525
31016
  if (unit.startsWith("min")) return v;
30526
31017
  if (unit.startsWith("hour")) return v * 60;
30527
31018
  if (unit.startsWith("day")) return v * 1440;
@@ -30539,47 +31030,49 @@ function plannedMessages(workflows) {
30539
31030
  for (const wf of workflows) {
30540
31031
  const nodes = /* @__PURE__ */ new Map();
30541
31032
  for (const t of wf.templates) {
30542
- const n = asObj4(t);
30543
- const id = str5(n.id);
31033
+ const n = asObj5(t);
31034
+ const id = str6(n.id);
30544
31035
  if (id) nodes.set(id, n);
30545
31036
  }
30546
31037
  for (const node of nodes.values()) {
30547
- const type = str5(node.type);
31038
+ const type = str6(node.type);
30548
31039
  if (type !== "sms" && type !== "email") continue;
30549
31040
  let minutes = 0;
30550
- let conditional = Boolean(str5(node.parent) && nodes.get(str5(node.parent) ?? "") && isBranch(nodes.get(str5(node.parent) ?? "")));
31041
+ let conditional = Boolean(str6(node.parent) && nodes.get(str6(node.parent) ?? "") && isBranch(nodes.get(str6(node.parent) ?? "")));
30551
31042
  const seen = /* @__PURE__ */ new Set();
30552
- let cur = str5(node.parentKey);
31043
+ const afterWaits = [];
31044
+ let cur = str6(node.parentKey);
30553
31045
  while (cur && !conditional && !seen.has(cur)) {
30554
31046
  seen.add(cur);
30555
31047
  const p = nodes.get(cur);
30556
31048
  if (!p) break;
30557
- if (isBranch(p) || str5(p.parent) && isBranch(nodes.get(str5(p.parent) ?? "") ?? {})) {
31049
+ if (isBranch(p) || str6(p.parent) && isBranch(nodes.get(str6(p.parent) ?? "") ?? {})) {
30558
31050
  conditional = true;
30559
31051
  break;
30560
31052
  }
30561
- if (str5(p.type) === "wait") {
31053
+ if (str6(p.type) === "wait") {
30562
31054
  const m = waitMinutes(p);
30563
31055
  if (m === null) {
30564
31056
  conditional = true;
30565
31057
  break;
30566
31058
  }
30567
31059
  minutes += m;
31060
+ afterWaits.push(cur);
30568
31061
  }
30569
- cur = str5(p.parentKey);
31062
+ cur = str6(p.parentKey);
30570
31063
  }
30571
31064
  if (conditional) {
30572
31065
  leftOut.conditional++;
30573
31066
  continue;
30574
31067
  }
30575
- const a = asObj4(node.attributes);
30576
- const key = type === "email" ? wordingKey(str5(a.subject) ?? "") || wordingKey(str5(a.html) ?? "") : wordingKey(str5(a.body) ?? "");
31068
+ const a = asObj5(node.attributes);
31069
+ const key = type === "email" ? wordingKey(str6(a.subject) ?? "") || wordingKey(str6(a.html) ?? "") : wordingKey(str6(a.body) ?? "");
30577
31070
  if (!key) {
30578
31071
  leftOut.noWording++;
30579
31072
  continue;
30580
31073
  }
30581
- const stepName = str5(node.name) || (type === "sms" ? "text" : "email");
30582
- expect.push({ label: `${wf.name}: ${stepName}`, channel: type, match: key, byMinute: minutes + 1 });
31074
+ const stepName = str6(node.name) || (type === "sms" ? "text" : "email");
31075
+ expect.push({ label: `${wf.name}: ${stepName}`, channel: type, match: key, byMinute: minutes + 1, stepName, ...wf.id ? { workflowId: wf.id } : {}, ...str6(node.id) ? { stepId: str6(node.id) } : {}, afterWaits });
30583
31076
  }
30584
31077
  }
30585
31078
  return { expect, leftOut };
@@ -30591,26 +31084,105 @@ async function planFromWorkflowIds(reader, ids) {
30591
31084
  const drafts = [];
30592
31085
  for (const id of ids) {
30593
31086
  try {
30594
- const wf = asObj4(await reader.getWorkflow(id));
30595
- const name = str5(wf.name) ?? id;
31087
+ const wf = asObj5(await reader.getWorkflow(id));
31088
+ const name = str6(wf.name) ?? id;
30596
31089
  names[id] = name;
30597
- if (str5(wf.status)?.toLowerCase() === "draft") drafts.push(name);
30598
- workflows.push({ name, templates: asArr3(asObj4(wf.workflowData).templates) });
31090
+ if (str6(wf.status)?.toLowerCase() === "draft") drafts.push(name);
31091
+ workflows.push({ id, name, templates: asArr4(asObj5(wf.workflowData).templates) });
30599
31092
  } catch {
30600
31093
  unread.push(id);
30601
31094
  }
30602
31095
  }
30603
31096
  return { ...plannedMessages(workflows), unread, names, drafts };
30604
31097
  }
30605
- var asObj4, asArr3, str5, isBranch;
31098
+ var asObj5, asArr4, str6, isBranch;
30606
31099
  var init_plan2 = __esm({
30607
31100
  "src/command-os/shopper/plan.ts"() {
30608
31101
  "use strict";
30609
31102
  init_links();
30610
- asObj4 = (v) => v && typeof v === "object" ? v : {};
30611
- asArr3 = (v) => Array.isArray(v) ? v : [];
30612
- str5 = (v) => typeof v === "string" ? v : void 0;
30613
- isBranch = (n) => /^branch-/.test(str5(n.nodeType) ?? "") || str5(n.type) === "if_else";
31103
+ asObj5 = (v) => v && typeof v === "object" ? v : {};
31104
+ asArr4 = (v) => Array.isArray(v) ? v : [];
31105
+ str6 = (v) => typeof v === "string" ? v : void 0;
31106
+ isBranch = (n) => /^branch-/.test(str6(n.nodeType) ?? "") || str6(n.type) === "if_else";
31107
+ }
31108
+ });
31109
+
31110
+ // src/command-os/shopper/ff-start.ts
31111
+ async function ownerUserId(client, locationId2, ownerEmail) {
31112
+ const raw = asObj6(await client.get("/users/", { params: { locationId: locationId2 } }));
31113
+ const want = baseEmail(ownerEmail);
31114
+ for (const u of asArr5(raw.users).map(asObj6)) if (str7(u.email) && baseEmail(str7(u.email)) === want) return str7(u.id);
31115
+ return void 0;
31116
+ }
31117
+ async function accountUsers(client, locationId2) {
31118
+ const raw = asObj6(await client.get("/users/", { params: { locationId: locationId2 } }));
31119
+ return asArr5(raw.users).map(asObj6).filter((u) => str7(u.id) && !u.deleted).map((u) => ({
31120
+ id: str7(u.id),
31121
+ name: str7(u.name) ?? ([str7(u.firstName), str7(u.lastName)].filter(Boolean).join(" ") || str7(u.email) || str7(u.id)),
31122
+ email: str7(u.email)
31123
+ }));
31124
+ }
31125
+ async function sweepLeftovers(admin, locationId2, base, now) {
31126
+ const keep = listRuns(locationId2, base).filter((r) => r.status === "watching").flatMap((r) => r.fastForward?.copies.map((c) => c.copyId) ?? []);
31127
+ return sweep(admin, locationId2, base, now, keep);
31128
+ }
31129
+ async function startFastForward(s) {
31130
+ await sweepLeftovers(s.admin, s.locationId, s.base, s.now);
31131
+ let owner;
31132
+ if (s.alertUserId) {
31133
+ if (!(await accountUsers(s.client, s.locationId)).some((u) => u.id === s.alertUserId)) return { ok: false, error: "The person chosen to receive staff alerts is not a user of this account any more. Pick someone from the list again." };
31134
+ owner = s.alertUserId;
31135
+ } else owner = await ownerUserId(s.client, s.locationId, s.ownerEmail);
31136
+ const token2 = newToken();
31137
+ const made = await makeCopies(s.admin, s.locationId, s.workflowIds, { ownerUserId: owner, allowedHosts: s.allowedHosts, token: token2, base: s.base, now: s.now });
31138
+ const undo = async (copies = made.copies) => removeCopies(s.admin, s.locationId, copies.map((c) => ({ copyId: c.copyId, name: c.copyName })), s.base, s.now);
31139
+ if (!made.ok) {
31140
+ const left = await undo();
31141
+ return { ok: false, error: made.refusal + (left.length ? ` ${left.join(" ")}` : "") };
31142
+ }
31143
+ try {
31144
+ const copyIds = made.copies.map((c) => c.copyId);
31145
+ const plan = await planFromWorkflowIds(s.reader, copyIds);
31146
+ const names = Object.fromEntries(made.copies.map((c) => [c.copyId, c.sourceName]));
31147
+ const run = await startShopper({
31148
+ locationId: s.locationId,
31149
+ ownerEmail: s.ownerEmail,
31150
+ testPhone: s.testPhone,
31151
+ keepLead: s.keepLead,
31152
+ entry: { kind: "workflows", workflowIds: copyIds },
31153
+ // Findings name the ORIGINAL workflow; the copy's name never reaches the card.
31154
+ expect: plan.expect.map((e) => {
31155
+ const c = made.copies.find((x) => x.copyId === e.workflowId);
31156
+ return c && e.stepName ? { ...e, label: `${c.sourceName}: ${e.stepName}` } : e;
31157
+ }),
31158
+ planNote: { source: "workflows", conditional: plan.leftOut.conditional, noWording: plan.leftOut.noWording, unread: plan.unread },
31159
+ workflowNames: names,
31160
+ fastForward: made.copies
31161
+ }, { client: s.client, otherBusinessNames: s.otherBusinessNames, base: s.base, now: s.now });
31162
+ if (run.status === "failed") {
31163
+ await undo();
31164
+ return { ok: false, error: run.error ?? "The check did not start." };
31165
+ }
31166
+ return { ok: true, run };
31167
+ } catch (e) {
31168
+ await undo();
31169
+ return { ok: false, error: e instanceof Error ? e.message.split("\n")[0] : String(e) };
31170
+ }
31171
+ }
31172
+ var asObj6, asArr5, str7, baseEmail;
31173
+ var init_ff_start = __esm({
31174
+ "src/command-os/shopper/ff-start.ts"() {
31175
+ "use strict";
31176
+ init_run();
31177
+ init_fastforward();
31178
+ init_plan2();
31179
+ asObj6 = (v) => v && typeof v === "object" ? v : {};
31180
+ asArr5 = (v) => Array.isArray(v) ? v : [];
31181
+ str7 = (v) => typeof v === "string" ? v : void 0;
31182
+ baseEmail = (e) => {
31183
+ const [l, d] = e.trim().toLowerCase().split("@");
31184
+ return `${(l ?? "").split("+")[0]}@${d ?? ""}`;
31185
+ };
30614
31186
  }
30615
31187
  });
30616
31188
 
@@ -32524,7 +33096,7 @@ function isHex(value) {
32524
33096
  }
32525
33097
  function readAgencyBranding(base = appDataDir()) {
32526
33098
  try {
32527
- const st = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(base, "cockpit-state.json"), "utf8"));
33099
+ const st = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(base, "cockpit-state.json"), "utf8"));
32528
33100
  const out = {};
32529
33101
  if (typeof st.agencyName === "string" && st.agencyName.trim()) out.agencyName = st.agencyName.trim();
32530
33102
  if (isHex(st.agencyAccent)) out.accent = st.agencyAccent;
@@ -32541,12 +33113,12 @@ function resolveBranding(overrides = {}, base) {
32541
33113
  const logoDataUri = isRasterDataUri(overrides.logoDataUri) ? overrides.logoDataUri : saved.logoDataUri;
32542
33114
  return { ...agencyName && { agencyName }, accent, ...logoDataUri && { logoDataUri } };
32543
33115
  }
32544
- var import_node_fs3, import_node_path3, DEFAULT_ACCENT;
33116
+ var import_node_fs4, import_node_path4, DEFAULT_ACCENT;
32545
33117
  var init_branding = __esm({
32546
33118
  "src/command-os/branding.ts"() {
32547
33119
  "use strict";
32548
- import_node_fs3 = require("node:fs");
32549
- import_node_path3 = require("node:path");
33120
+ import_node_fs4 = require("node:fs");
33121
+ import_node_path4 = require("node:path");
32550
33122
  init_credentials_store();
32551
33123
  init_svg_sanitize();
32552
33124
  DEFAULT_ACCENT = "#B8934A";
@@ -32952,11 +33524,11 @@ var init_client_status = __esm({
32952
33524
 
32953
33525
  // src/command-os/client-status-placement.ts
32954
33526
  function statusStorePath(base = appDataDir()) {
32955
- return path22.join(base, "client-status.json");
33527
+ return path23.join(base, "client-status.json");
32956
33528
  }
32957
33529
  function readStatusStore(base) {
32958
33530
  try {
32959
- const raw = JSON.parse(fs24.readFileSync(statusStorePath(base), "utf8"));
33531
+ const raw = JSON.parse(fs25.readFileSync(statusStorePath(base), "utf8"));
32960
33532
  if (raw && raw.v === 1 && raw.clients) return raw;
32961
33533
  } catch {
32962
33534
  }
@@ -32964,8 +33536,8 @@ function readStatusStore(base) {
32964
33536
  }
32965
33537
  function writeStatusStore(store, base) {
32966
33538
  const file2 = statusStorePath(base);
32967
- fs24.mkdirSync(path22.dirname(file2), { recursive: true, mode: 448 });
32968
- fs24.writeFileSync(file2, JSON.stringify(store, null, 2), { mode: 384 });
33539
+ fs25.mkdirSync(path23.dirname(file2), { recursive: true, mode: 448 });
33540
+ fs25.writeFileSync(file2, JSON.stringify(store, null, 2), { mode: 384 });
32969
33541
  }
32970
33542
  function readStatusRecord(locationId2, base) {
32971
33543
  return readStatusStore(base).clients[locationId2] ?? { locationId: locationId2 };
@@ -32997,9 +33569,9 @@ function describePlacement(placement) {
32997
33569
  return `A real page inside the client's own GoHighLevel account, at /${placement.slug ?? STATUS_SLUG}${placement.publicUrl ? ` \u2014 ${placement.publicUrl}` : ""}. Every re-check replaces that one page; it never adds a second copy.`;
32998
33570
  }
32999
33571
  if (placement.kind === "agency-site") {
33000
- return `A file on your own site: ${path22.join(placement.directory, placement.fileName ?? "status.html")}${placement.publicUrl ? `, which you publish at ${placement.publicUrl}` : ""}. The same file is rewritten every time, so the address never changes.`;
33572
+ return `A file on your own site: ${path23.join(placement.directory, placement.fileName ?? "status.html")}${placement.publicUrl ? `, which you publish at ${placement.publicUrl}` : ""}. The same file is rewritten every time, so the address never changes.`;
33001
33573
  }
33002
- return `A page written to ${path22.join(placement.directory, placement.fileName ?? "status.html")} for you to publish as an Artifact from your own Claude${placement.publicUrl ? `, currently at ${placement.publicUrl}` : ""}.`;
33574
+ return `A page written to ${path23.join(placement.directory, placement.fileName ?? "status.html")} for you to publish as an Artifact from your own Claude${placement.publicUrl ? `, currently at ${placement.publicUrl}` : ""}.`;
33003
33575
  }
33004
33576
  function placementReadiness(placement) {
33005
33577
  if (!placement) return ["Choose where this client's page should live: their own GoHighLevel account, your own site, or an Artifact from your own Claude."];
@@ -33058,12 +33630,12 @@ function artifactPublishSteps(filePath, existingUrl) {
33058
33630
  "Send the client the link once. From then on they just revisit it."
33059
33631
  ];
33060
33632
  }
33061
- var fs24, path22, STATUS_SLUG, OUR_HOSTS;
33633
+ var fs25, path23, STATUS_SLUG, OUR_HOSTS;
33062
33634
  var init_client_status_placement = __esm({
33063
33635
  "src/command-os/client-status-placement.ts"() {
33064
33636
  "use strict";
33065
- fs24 = __toESM(require("node:fs"));
33066
- path22 = __toESM(require("node:path"));
33637
+ fs25 = __toESM(require("node:fs"));
33638
+ path23 = __toESM(require("node:path"));
33067
33639
  init_credentials_store();
33068
33640
  init_client_status();
33069
33641
  STATUS_SLUG = "status";
@@ -33083,7 +33655,7 @@ var init_client_status_placement = __esm({
33083
33655
  // src/build-contract.ts
33084
33656
  function contentHash(value) {
33085
33657
  const canonical2 = (v) => Array.isArray(v) ? v.map(canonical2) : v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().filter((k) => v[k] !== void 0).map((k) => [k, canonical2(v[k])])) : v;
33086
- return (0, import_node_crypto8.createHash)("sha256").update(JSON.stringify(canonical2(value)) ?? "null").digest("hex");
33658
+ return (0, import_node_crypto9.createHash)("sha256").update(JSON.stringify(canonical2(value)) ?? "null").digest("hex");
33087
33659
  }
33088
33660
  function assertBuildApproval(approval, inputHash) {
33089
33661
  if (!approval?.at) throw new Error("Approve building before running Build.");
@@ -33098,7 +33670,7 @@ function assertApprovedPlan(saved, approval, inputHash, build) {
33098
33670
  }
33099
33671
  function readPlanApproval(locationId2, base = appDataDir()) {
33100
33672
  try {
33101
- return JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(base, "cockpit-state.json"), "utf8")).planApprovals?.[locationId2];
33673
+ return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(base, "cockpit-state.json"), "utf8")).planApprovals?.[locationId2];
33102
33674
  } catch {
33103
33675
  return void 0;
33104
33676
  }
@@ -33258,13 +33830,13 @@ function executablePlanLines(saved) {
33258
33830
  for (const w of preview?.workflows ?? []) for (const m of [...w.manual ?? [], ...w.needsContent ?? []]) lines.push(`Unresolved: ${w.name || names.get(w.ref) || "Unnamed workflow"}. ${m.reason}`);
33259
33831
  return lines.map(sentence);
33260
33832
  }
33261
- var import_node_crypto8, import_node_fs4, import_node_path4, INTAKE_CHANGED;
33833
+ var import_node_crypto9, import_node_fs5, import_node_path5, INTAKE_CHANGED;
33262
33834
  var init_build_contract = __esm({
33263
33835
  "src/build-contract.ts"() {
33264
33836
  "use strict";
33265
- import_node_crypto8 = require("node:crypto");
33266
- import_node_fs4 = require("node:fs");
33267
- import_node_path4 = require("node:path");
33837
+ import_node_crypto9 = require("node:crypto");
33838
+ import_node_fs5 = require("node:fs");
33839
+ import_node_path5 = require("node:path");
33268
33840
  init_credentials_store();
33269
33841
  INTAKE_CHANGED = "The intake changed. Run Brief again and approve the new review.";
33270
33842
  }
@@ -33522,30 +34094,30 @@ var init_client_status_page = __esm({
33522
34094
 
33523
34095
  // src/command-os/client-status-publish.ts
33524
34096
  function handoffSnapshotDir(base = appDataDir()) {
33525
- return path23.join(base, "handoffs");
34097
+ return path24.join(base, "handoffs");
33526
34098
  }
33527
34099
  function handoffSnapshotPath(locationId2, base) {
33528
- return path23.join(handoffSnapshotDir(base), `${locationId2}-handoff.json`);
34100
+ return path24.join(handoffSnapshotDir(base), `${locationId2}-handoff.json`);
33529
34101
  }
33530
34102
  function saveHandoffSnapshot(saved, base) {
33531
34103
  const file2 = handoffSnapshotPath(saved.locationId, base);
33532
- fs25.mkdirSync(path23.dirname(file2), { recursive: true, mode: 448 });
33533
- fs25.writeFileSync(file2, JSON.stringify(saved, null, 2), { mode: 384 });
34104
+ fs26.mkdirSync(path24.dirname(file2), { recursive: true, mode: 448 });
34105
+ fs26.writeFileSync(file2, JSON.stringify(saved, null, 2), { mode: 384 });
33534
34106
  return file2;
33535
34107
  }
33536
34108
  function readHandoffSnapshot(locationId2, base) {
33537
34109
  try {
33538
- return JSON.parse(fs25.readFileSync(handoffSnapshotPath(locationId2, base), "utf8"));
34110
+ return JSON.parse(fs26.readFileSync(handoffSnapshotPath(locationId2, base), "utf8"));
33539
34111
  } catch {
33540
34112
  return null;
33541
34113
  }
33542
34114
  }
33543
34115
  function localCopyPath(locationId2, base = appDataDir()) {
33544
- return path23.join(base, "client-status", `${locationId2}-status.html`);
34116
+ return path24.join(base, "client-status", `${locationId2}-status.html`);
33545
34117
  }
33546
34118
  function readVerifyOutcome(locationId2, base = appDataDir()) {
33547
34119
  try {
33548
- const st = JSON.parse(fs25.readFileSync(path23.join(base, "cockpit-state.json"), "utf8"));
34120
+ const st = JSON.parse(fs26.readFileSync(path24.join(base, "cockpit-state.json"), "utf8"));
33549
34121
  const o = st.outcomes?.[`${locationId2}:4`];
33550
34122
  if (!o) return null;
33551
34123
  const src = o.plain ?? { summary: o.summary, issues: o.issues };
@@ -33555,9 +34127,9 @@ function readVerifyOutcome(locationId2, base = appDataDir()) {
33555
34127
  }
33556
34128
  }
33557
34129
  function writeFileFor(dir, fileName, html2) {
33558
- fs25.mkdirSync(dir, { recursive: true });
33559
- const file2 = path23.join(dir, fileName);
33560
- fs25.writeFileSync(file2, html2);
34130
+ fs26.mkdirSync(dir, { recursive: true });
34131
+ const file2 = path24.join(dir, fileName);
34132
+ fs26.writeFileSync(file2, html2);
33561
34133
  return file2;
33562
34134
  }
33563
34135
  async function refreshClientStatus(opts) {
@@ -33593,7 +34165,7 @@ async function refreshClientStatus(opts) {
33593
34165
  record.preparedSentences = input.items.map((i) => i.sentence);
33594
34166
  if (!opts.dryRun) saveStatusRecord({ ...record, locationId: opts.locationId }, opts.base);
33595
34167
  const localPath = localCopyPath(opts.locationId, opts.base);
33596
- if (!opts.dryRun) writeFileFor(path23.dirname(localPath), path23.basename(localPath), html2);
34168
+ if (!opts.dryRun) writeFileFor(path24.dirname(localPath), path24.basename(localPath), html2);
33597
34169
  const placement = record.placement;
33598
34170
  const missing = placementReadiness(placement);
33599
34171
  if (!placement || missing.length) {
@@ -33640,7 +34212,7 @@ async function publishTo(placement, input, html2, opts) {
33640
34212
  if (placement.kind === "agency-site") {
33641
34213
  assertNotOurHost(placement.publicUrl, "The address you chose for this page");
33642
34214
  const fileName = placement.fileName ?? "status.html";
33643
- const destinationPath = opts.dryRun ? path23.join(placement.directory, fileName) : writeFileFor(placement.directory, fileName, html2);
34215
+ const destinationPath = opts.dryRun ? path24.join(placement.directory, fileName) : writeFileFor(placement.directory, fileName, html2);
33644
34216
  return {
33645
34217
  ...placement.publicUrl && { url: placement.publicUrl },
33646
34218
  destinationPath,
@@ -33654,7 +34226,7 @@ async function publishTo(placement, input, html2, opts) {
33654
34226
  const safety = artifactSafety(html2);
33655
34227
  if (!safety.ok) throw new Error(safety.problems.join(" "));
33656
34228
  const fileName = placement.fileName ?? "status.html";
33657
- const destinationPath = opts.dryRun ? path23.join(placement.directory, fileName) : writeFileFor(placement.directory, fileName, html2);
34229
+ const destinationPath = opts.dryRun ? path24.join(placement.directory, fileName) : writeFileFor(placement.directory, fileName, html2);
33658
34230
  assertNotOurHost(placement.publicUrl, "The Artifact link saved for this client");
33659
34231
  return {
33660
34232
  ...placement.publicUrl && { url: placement.publicUrl },
@@ -33695,13 +34267,13 @@ async function refreshAfterVerify(locationId2, accountName, emit2, base, approve
33695
34267
  emit2(`The client's status page was not updated: ${e instanceof Error ? e.message : String(e)}`);
33696
34268
  }
33697
34269
  }
33698
- var fs25, path23;
34270
+ var fs26, path24;
33699
34271
  var init_client_status_publish = __esm({
33700
34272
  "src/command-os/client-status-publish.ts"() {
33701
34273
  "use strict";
33702
34274
  init_publication_proof();
33703
- fs25 = __toESM(require("node:fs"));
33704
- path23 = __toESM(require("node:path"));
34275
+ fs26 = __toESM(require("node:fs"));
34276
+ path24 = __toESM(require("node:path"));
33705
34277
  init_credentials_store();
33706
34278
  init_branding();
33707
34279
  init_client_status();
@@ -33713,7 +34285,7 @@ var init_client_status_publish = __esm({
33713
34285
  // src/workflow-repair-approval.ts
33714
34286
  function readWorkflowRepairs(id, base = appDataDir()) {
33715
34287
  try {
33716
- return JSON.parse((0, import_node_fs5.readFileSync)(file(id, base), "utf8"));
34288
+ return JSON.parse((0, import_node_fs6.readFileSync)(file(id, base), "utf8"));
33717
34289
  } catch {
33718
34290
  return [];
33719
34291
  }
@@ -33727,19 +34299,19 @@ function reviewWorkflowRepair(locationId2, workflowId, name, before, after, base
33727
34299
  ];
33728
34300
  const repair = { workflowId, name, beforeHash: contentHash(before), afterHash: contentHash(after), hash: contentHash({ workflowId, before, after }), changes };
33729
34301
  if (readPlanApproval(locationId2, base)?.repairHashes?.includes(repair.hash)) return true;
33730
- (0, import_node_fs5.mkdirSync)((0, import_node_path5.join)(base, "workflow-repairs"), { recursive: true, mode: 448 });
33731
- (0, import_node_fs5.writeFileSync)(file(locationId2, base), JSON.stringify([...readWorkflowRepairs(locationId2, base).filter((r) => r.workflowId !== workflowId), repair]), { mode: 384 });
34302
+ (0, import_node_fs6.mkdirSync)((0, import_node_path6.join)(base, "workflow-repairs"), { recursive: true, mode: 448 });
34303
+ (0, import_node_fs6.writeFileSync)(file(locationId2, base), JSON.stringify([...readWorkflowRepairs(locationId2, base).filter((r) => r.workflowId !== workflowId), repair]), { mode: 384 });
33732
34304
  return false;
33733
34305
  }
33734
- var import_node_fs5, import_node_path5, file;
34306
+ var import_node_fs6, import_node_path6, file;
33735
34307
  var init_workflow_repair_approval = __esm({
33736
34308
  "src/workflow-repair-approval.ts"() {
33737
34309
  "use strict";
33738
- import_node_fs5 = require("node:fs");
33739
- import_node_path5 = require("node:path");
34310
+ import_node_fs6 = require("node:fs");
34311
+ import_node_path6 = require("node:path");
33740
34312
  init_credentials_store();
33741
34313
  init_build_contract();
33742
- file = (id, base) => (0, import_node_path5.join)(base, "workflow-repairs", encodeURIComponent(id) + ".json");
34314
+ file = (id, base) => (0, import_node_path6.join)(base, "workflow-repairs", encodeURIComponent(id) + ".json");
33743
34315
  }
33744
34316
  });
33745
34317
 
@@ -33780,11 +34352,11 @@ var init_handoff_snapshot = __esm({
33780
34352
  // src/build-obligations.ts
33781
34353
  function fileFor(id) {
33782
34354
  if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw new Error("Choose a registered client.");
33783
- return path24.join(appDataDir(), "build-obligations", id + ".json");
34355
+ return path25.join(appDataDir(), "build-obligations", id + ".json");
33784
34356
  }
33785
34357
  function readBuildObligations(id) {
33786
34358
  try {
33787
- const data = JSON.parse(fs26.readFileSync(fileFor(id), "utf8"));
34359
+ const data = JSON.parse(fs27.readFileSync(fileFor(id), "utf8"));
33788
34360
  return Array.isArray(data) ? data : [];
33789
34361
  } catch (e) {
33790
34362
  if (e.code === "ENOENT") return [];
@@ -33795,9 +34367,9 @@ function saveBuildObligations(id, items) {
33795
34367
  const merged = new Map(readBuildObligations(id).map((m) => [`${m.type}:${m.ref}`, m]));
33796
34368
  for (const item of items) merged.set(`${item.type}:${item.ref}`, item);
33797
34369
  const file2 = fileFor(id);
33798
- fs26.mkdirSync(path24.dirname(file2), { recursive: true });
33799
- fs26.writeFileSync(file2 + ".tmp", JSON.stringify([...merged.values()], null, 2), { mode: 384 });
33800
- fs26.renameSync(file2 + ".tmp", file2);
34370
+ fs27.mkdirSync(path25.dirname(file2), { recursive: true });
34371
+ fs27.writeFileSync(file2 + ".tmp", JSON.stringify([...merged.values()], null, 2), { mode: 384 });
34372
+ fs27.renameSync(file2 + ".tmp", file2);
33801
34373
  }
33802
34374
  function planObjectRefs(plan) {
33803
34375
  const refs = /* @__PURE__ */ new Set();
@@ -33822,16 +34394,16 @@ function reconcileBuildObligations(id, checks, currentPlan) {
33822
34394
  item.type === "workflow-manual" && Array.isArray(workflows) && workflows.some((workflow) => item.ref.startsWith(`${workflow.name}:`) && /^\d+$/.test(item.ref.slice(workflow.name.length + 1)));
33823
34395
  const remaining = readBuildObligations(id).filter((item) => stillPlanned(item) && !checks.some((c) => c.ref === item.ref && c.type === item.type && c.ok === true && typeof c.evidence === "string" && c.evidence.trim()));
33824
34396
  const file2 = fileFor(id);
33825
- fs26.mkdirSync(path24.dirname(file2), { recursive: true });
33826
- fs26.writeFileSync(file2 + ".tmp", JSON.stringify(remaining, null, 2), { mode: 384 });
33827
- fs26.renameSync(file2 + ".tmp", file2);
34397
+ fs27.mkdirSync(path25.dirname(file2), { recursive: true });
34398
+ fs27.writeFileSync(file2 + ".tmp", JSON.stringify(remaining, null, 2), { mode: 384 });
34399
+ fs27.renameSync(file2 + ".tmp", file2);
33828
34400
  }
33829
- var fs26, path24;
34401
+ var fs27, path25;
33830
34402
  var init_build_obligations = __esm({
33831
34403
  "src/build-obligations.ts"() {
33832
34404
  "use strict";
33833
- fs26 = __toESM(require("node:fs"));
33834
- path24 = __toESM(require("node:path"));
34405
+ fs27 = __toESM(require("node:fs"));
34406
+ path25 = __toESM(require("node:path"));
33835
34407
  init_credentials_store();
33836
34408
  }
33837
34409
  });
@@ -33841,7 +34413,7 @@ function engineRepair(version) {
33841
34413
  return !version ? CLAUDE_REPAIR_MESSAGES[0] : !meetsFloor(version) ? CLAUDE_REPAIR_MESSAGES[1] : CLAUDE_REPAIR_MESSAGES[2];
33842
34414
  }
33843
34415
  function keyFingerprint(key) {
33844
- return (0, import_node_crypto9.createHash)("sha256").update(key).digest("hex");
34416
+ return (0, import_node_crypto10.createHash)("sha256").update(key).digest("hex");
33845
34417
  }
33846
34418
  async function clientPreflight(registry2, locationId2) {
33847
34419
  const lines = [];
@@ -33908,7 +34480,7 @@ async function agencyConnectionReady(registry2, companyId) {
33908
34480
  return false;
33909
34481
  }
33910
34482
  }
33911
- var import_node_crypto9, CLAUDE_REPAIR_MESSAGES;
34483
+ var import_node_crypto10, CLAUDE_REPAIR_MESSAGES;
33912
34484
  var init_client_preflight = __esm({
33913
34485
  "src/client-preflight.ts"() {
33914
34486
  "use strict";
@@ -33918,7 +34490,7 @@ var init_client_preflight = __esm({
33918
34490
  init_stage_runner();
33919
34491
  init_tool_filter();
33920
34492
  init_credentials_store();
33921
- import_node_crypto9 = require("node:crypto");
34493
+ import_node_crypto10 = require("node:crypto");
33922
34494
  CLAUDE_REPAIR_MESSAGES = [
33923
34495
  "Install Claude Code from Your agency before reading this source.",
33924
34496
  "Update Claude Code to version 2.1.0 or newer from Your agency.",
@@ -33961,25 +34533,25 @@ var init_brief_validation = __esm({
33961
34533
  // src/command-os/design/run-jobs.ts
33962
34534
  function validateRunRequest(input) {
33963
34535
  const a = input ?? {};
33964
- const str6 = (k) => typeof a[k] === "string" ? a[k].trim() : "";
33965
- const brand = str6("brand");
34536
+ const str8 = (k) => typeof a[k] === "string" ? a[k].trim() : "";
34537
+ const brand = str8("brand");
33966
34538
  if (!SLUG2.test(brand)) return { ok: false, error: "brand must be a stored brand's slug (lowercase letters, digits and dashes)." };
33967
- const medium = str6("medium");
34539
+ const medium = str8("medium");
33968
34540
  if (!MEDIA.includes(medium)) return { ok: false, error: `medium must be one of ${MEDIA.join(", ")}.` };
33969
- const headline3 = str6("headline");
34541
+ const headline3 = str8("headline");
33970
34542
  if (headline3.length < 3 || headline3.length > 300) return { ok: false, error: "headline must be between 3 and 300 characters." };
33971
34543
  const points = Array.isArray(a.points) ? a.points.filter((p) => typeof p === "string" && p.trim().length > 0).map((p) => p.trim()) : [];
33972
34544
  if (points.length === 0) return { ok: false, error: "points must be a non-empty list." };
33973
34545
  if (points.length > DESIGN_FORM_LIMITS.pointsMax) return { ok: false, error: POINTS_MESSAGE };
33974
34546
  if (points.some((p) => p.includes("|"))) return { ok: false, error: "a point may not contain the | character; it separates them on the command line." };
33975
34547
  if (points.some((p) => p.length > 400)) return { ok: false, error: "a point may be 400 characters at most." };
33976
- const model = str6("model");
34548
+ const model = str8("model");
33977
34549
  if (model && !MODEL.test(model)) return { ok: false, error: "model must be a plain engine name." };
33978
34550
  const slides = a.slides;
33979
34551
  if (slides !== void 0 && (typeof slides !== "number" || !Number.isInteger(slides) || slides < DESIGN_FORM_LIMITS.slidesMin || slides > DESIGN_FORM_LIMITS.slidesMax)) return { ok: false, error: SLIDES_MESSAGE };
33980
34552
  let refusal = null;
33981
34553
  const optional = (k, max = 400) => {
33982
- const v = str6(k);
34554
+ const v = str8(k);
33983
34555
  if (!v) return void 0;
33984
34556
  if (v.length > max) {
33985
34557
  if (refusal === null) refusal = `${k} must be ${max} characters at most.`;
@@ -34004,8 +34576,8 @@ function validateRunRequest(input) {
34004
34576
  return { ok: true, value };
34005
34577
  }
34006
34578
  function designRunsDir() {
34007
- const dir = (0, import_node_path6.join)(appDataDir(), "design-runs");
34008
- (0, import_node_fs6.mkdirSync)(dir, { recursive: true });
34579
+ const dir = (0, import_node_path7.join)(appDataDir(), "design-runs");
34580
+ (0, import_node_fs7.mkdirSync)(dir, { recursive: true });
34009
34581
  return dir;
34010
34582
  }
34011
34583
  function argvFor(r, outDir) {
@@ -34025,9 +34597,9 @@ function startDesignRun(request, opts = {}) {
34025
34597
  const checked = validateRunRequest(request);
34026
34598
  if (!checked.ok) throw new Error(checked.error);
34027
34599
  request = checked.value;
34028
- const id = (0, import_node_crypto10.randomUUID)();
34029
- const outDir = (0, import_node_path6.join)(designRunsDir(), id);
34030
- (0, import_node_fs6.mkdirSync)(outDir, { recursive: true });
34600
+ const id = (0, import_node_crypto11.randomUUID)();
34601
+ const outDir = (0, import_node_path7.join)(designRunsDir(), id);
34602
+ (0, import_node_fs7.mkdirSync)(outDir, { recursive: true });
34031
34603
  const job = { id, status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), outDir, request, tail: [] };
34032
34604
  jobs.set(id, job);
34033
34605
  const entry = opts.entry ?? process.argv[1];
@@ -34038,11 +34610,11 @@ function startDesignRun(request, opts = {}) {
34038
34610
  env: { ...process.env, CLAUDECODE: "", CLAUDE_CODE_ENTRYPOINT: "" }
34039
34611
  });
34040
34612
  children.set(id, child);
34041
- const log = (0, import_node_path6.join)(outDir, "run.log");
34613
+ const log = (0, import_node_path7.join)(outDir, "run.log");
34042
34614
  const take = (chunk) => {
34043
34615
  const text2 = String(chunk);
34044
34616
  try {
34045
- (0, import_node_fs6.appendFileSync)(log, text2);
34617
+ (0, import_node_fs7.appendFileSync)(log, text2);
34046
34618
  } catch {
34047
34619
  }
34048
34620
  for (const line3 of text2.split("\n").map((l) => l.trimEnd()).filter(Boolean)) {
@@ -34069,10 +34641,10 @@ function startDesignRun(request, opts = {}) {
34069
34641
  function designRunStatus(id) {
34070
34642
  const job = jobs.get(id);
34071
34643
  if (!job) return null;
34072
- const runJson = (0, import_node_path6.join)(job.outDir, "run.json");
34073
- if (!(0, import_node_fs6.existsSync)(runJson)) return { ...job };
34644
+ const runJson = (0, import_node_path7.join)(job.outDir, "run.json");
34645
+ if (!(0, import_node_fs7.existsSync)(runJson)) return { ...job };
34074
34646
  try {
34075
- return { ...job, run: JSON.parse((0, import_node_fs6.readFileSync)(runJson, "utf8")) };
34647
+ return { ...job, run: JSON.parse((0, import_node_fs7.readFileSync)(runJson, "utf8")) };
34076
34648
  } catch {
34077
34649
  return { ...job };
34078
34650
  }
@@ -34092,14 +34664,14 @@ function cancelDesignRun(id) {
34092
34664
  }
34093
34665
  return true;
34094
34666
  }
34095
- var import_node_child_process3, import_node_fs6, import_node_path6, import_node_crypto10, SLUG2, MODEL, JOINED_FLAG, jobs, children;
34667
+ var import_node_child_process3, import_node_fs7, import_node_path7, import_node_crypto11, SLUG2, MODEL, JOINED_FLAG, jobs, children;
34096
34668
  var init_run_jobs = __esm({
34097
34669
  "src/command-os/design/run-jobs.ts"() {
34098
34670
  "use strict";
34099
34671
  import_node_child_process3 = require("node:child_process");
34100
- import_node_fs6 = require("node:fs");
34101
- import_node_path6 = require("node:path");
34102
- import_node_crypto10 = require("node:crypto");
34672
+ import_node_fs7 = require("node:fs");
34673
+ import_node_path7 = require("node:path");
34674
+ import_node_crypto11 = require("node:crypto");
34103
34675
  init_credentials_store();
34104
34676
  init_schema();
34105
34677
  init_brief_validation();
@@ -34127,20 +34699,20 @@ var init_run_expectations = __esm({
34127
34699
 
34128
34700
  // src/run-cost.ts
34129
34701
  function runCostPath() {
34130
- return path25.join(appDataDir(), "command-os-run-cost.jsonl");
34702
+ return path26.join(appDataDir(), "command-os-run-cost.jsonl");
34131
34703
  }
34132
34704
  function recordRunCost(rec, file2 = runCostPath()) {
34133
- fs27.mkdirSync(path25.dirname(file2), { recursive: true });
34134
- fs27.appendFileSync(file2, `${JSON.stringify(rec)}
34705
+ fs28.mkdirSync(path26.dirname(file2), { recursive: true });
34706
+ fs28.appendFileSync(file2, `${JSON.stringify(rec)}
34135
34707
  `, { mode: 384 });
34136
34708
  try {
34137
- fs27.chmodSync(file2, 384);
34709
+ fs28.chmodSync(file2, 384);
34138
34710
  } catch {
34139
34711
  }
34140
34712
  }
34141
34713
  function readRunCosts(file2 = runCostPath()) {
34142
34714
  try {
34143
- return fs27.readFileSync(file2, "utf8").split("\n").filter((l) => l.trim()).map((l) => {
34715
+ return fs28.readFileSync(file2, "utf8").split("\n").filter((l) => l.trim()).map((l) => {
34144
34716
  try {
34145
34717
  return JSON.parse(l);
34146
34718
  } catch {
@@ -34171,12 +34743,12 @@ function costLines(o) {
34171
34743
  if (o.engineCheckRan && o.engineCheckCostUsd === void 0) out.push("Claude readiness check: cost not reported.");
34172
34744
  return out;
34173
34745
  }
34174
- var fs27, path25;
34746
+ var fs28, path26;
34175
34747
  var init_run_cost = __esm({
34176
34748
  "src/run-cost.ts"() {
34177
34749
  "use strict";
34178
- fs27 = __toESM(require("fs"));
34179
- path25 = __toESM(require("path"));
34750
+ fs28 = __toESM(require("fs"));
34751
+ path26 = __toESM(require("path"));
34180
34752
  init_credentials_store();
34181
34753
  }
34182
34754
  });
@@ -34978,9 +35550,9 @@ function presetForBusinessType(type) {
34978
35550
  return "generic";
34979
35551
  }
34980
35552
  }
34981
- function setPath(target, path43, value) {
35553
+ function setPath(target, path44, value) {
34982
35554
  if (value === void 0) return;
34983
- const parts = path43.split(".");
35555
+ const parts = path44.split(".");
34984
35556
  let node = target;
34985
35557
  for (let i = 0; i < parts.length - 1; i++) {
34986
35558
  const k = parts[i];
@@ -35779,17 +36351,17 @@ function planFromPreset(input) {
35779
36351
  }
35780
36352
  function presetsRoot() {
35781
36353
  const candidates = [
35782
- path26.join(__dirname, "..", "skills", "blueprint", "presets"),
36354
+ path27.join(__dirname, "..", "skills", "blueprint", "presets"),
35783
36355
  // dist/ → package root
35784
- path26.join(__dirname, "..", "..", "skills", "blueprint", "presets"),
36356
+ path27.join(__dirname, "..", "..", "skills", "blueprint", "presets"),
35785
36357
  // src/intake-to-build in dev
35786
- path26.join(process.cwd(), "skills", "blueprint", "presets")
36358
+ path27.join(process.cwd(), "skills", "blueprint", "presets")
35787
36359
  ];
35788
- return candidates.find((c) => fs28.existsSync(c)) ?? null;
36360
+ return candidates.find((c) => fs29.existsSync(c)) ?? null;
35789
36361
  }
35790
36362
  function readPresetFile(dir, base) {
35791
36363
  try {
35792
- const raw = JSON.parse(fs28.readFileSync(path26.join(dir, `${base}.preset.json`), "utf8"));
36364
+ const raw = JSON.parse(fs29.readFileSync(path27.join(dir, `${base}.preset.json`), "utf8"));
35793
36365
  if (raw && typeof raw === "object" && typeof raw.presetId === "string" && raw.skeleton) return raw;
35794
36366
  return null;
35795
36367
  } catch {
@@ -35801,7 +36373,7 @@ function loadPresetChoice(requested, briefPreset) {
35801
36373
  if (!dir) {
35802
36374
  return { ok: false, error: "The preset library is not installed with this copy of the tool (skills/blueprint/presets is missing) \u2014 reinstall, or author the plan by hand." };
35803
36375
  }
35804
- const bases = fs28.readdirSync(dir).filter((f) => f.endsWith(".preset.json")).map((f) => f.replace(/\.preset\.json$/, ""));
36376
+ const bases = fs29.readdirSync(dir).filter((f) => f.endsWith(".preset.json")).map((f) => f.replace(/\.preset\.json$/, ""));
35805
36377
  const tryMatch = (want) => {
35806
36378
  const norm6 = slugify2(want);
35807
36379
  for (const base of bases) {
@@ -35832,12 +36404,12 @@ function loadPresetChoice(requested, briefPreset) {
35832
36404
  if (generic) return { ok: true, preset: generic, file: "generic-client.preset.json", note: "Using the generic preset." };
35833
36405
  return { ok: false, error: "No usable preset file was found in the library." };
35834
36406
  }
35835
- var fs28, path26, PRESET_TOKEN_ROOTS, FLAG_ALIASES, INLINE_TOKEN_RE, ADMIN_ROLE_RE, BRIEF_CALENDAR_TYPE_MAP;
36407
+ var fs29, path27, PRESET_TOKEN_ROOTS, FLAG_ALIASES, INLINE_TOKEN_RE, ADMIN_ROLE_RE, BRIEF_CALENDAR_TYPE_MAP;
35836
36408
  var init_plan_from_preset = __esm({
35837
36409
  "src/intake-to-build/plan-from-preset.ts"() {
35838
36410
  "use strict";
35839
- fs28 = __toESM(require("fs"));
35840
- path26 = __toESM(require("path"));
36411
+ fs29 = __toESM(require("fs"));
36412
+ path27 = __toESM(require("path"));
35841
36413
  init_plan();
35842
36414
  init_brief2();
35843
36415
  init_normalizer();
@@ -37226,13 +37798,13 @@ var init_client_intake = __esm({
37226
37798
 
37227
37799
  // src/plan-store.ts
37228
37800
  function plansDir(base = appDataDir()) {
37229
- return path27.join(base, "plans");
37801
+ return path28.join(base, "plans");
37230
37802
  }
37231
37803
  function savePlanRecord(rec, base) {
37232
37804
  const dir = plansDir(base);
37233
- fs29.mkdirSync(dir, { recursive: true, mode: 448 });
37234
- const file2 = path27.join(dir, `${rec.locationId}.json`);
37235
- fs29.writeFileSync(file2, JSON.stringify({ ...rec, contentHash: contentHash(rec.plan), inputHash: rec.inputHash ?? process.env.GHL_COMMAND_INTAKE_HASH, dependencies: rec.dependencies ?? planDependencies(rec.plan) }, null, 2), { mode: 384 });
37805
+ fs30.mkdirSync(dir, { recursive: true, mode: 448 });
37806
+ const file2 = path28.join(dir, `${rec.locationId}.json`);
37807
+ fs30.writeFileSync(file2, JSON.stringify({ ...rec, contentHash: contentHash(rec.plan), inputHash: rec.inputHash ?? process.env.GHL_COMMAND_INTAKE_HASH, dependencies: rec.dependencies ?? planDependencies(rec.plan) }, null, 2), { mode: 384 });
37236
37808
  return file2;
37237
37809
  }
37238
37810
  function savedPlanMatchesScope(saved, current) {
@@ -37247,7 +37819,7 @@ function savedPlanMatchesScope(saved, current) {
37247
37819
  }
37248
37820
  function readSavedPlan(locationId2, base) {
37249
37821
  try {
37250
- return JSON.parse(fs29.readFileSync(path27.join(plansDir(base), `${locationId2}.json`), "utf8"));
37822
+ return JSON.parse(fs30.readFileSync(path28.join(plansDir(base), `${locationId2}.json`), "utf8"));
37251
37823
  } catch {
37252
37824
  return null;
37253
37825
  }
@@ -37270,12 +37842,12 @@ function planSummary(rec) {
37270
37842
  if (wfs.length) parts.push(`workflows: ${wfs.map((w) => `${w.name} (${Array.isArray(w.actions) ? w.actions.length : "?"} actions)`).join("; ")}`);
37271
37843
  return parts.join("\n") || "(plan has no recognisable sections)";
37272
37844
  }
37273
- var fs29, path27;
37845
+ var fs30, path28;
37274
37846
  var init_plan_store = __esm({
37275
37847
  "src/plan-store.ts"() {
37276
37848
  "use strict";
37277
- fs29 = __toESM(require("fs"));
37278
- path27 = __toESM(require("path"));
37849
+ fs30 = __toESM(require("fs"));
37850
+ path28 = __toESM(require("path"));
37279
37851
  init_credentials_store();
37280
37852
  init_build_contract();
37281
37853
  }
@@ -38938,7 +39510,7 @@ function launchCommand(port = 7300) {
38938
39510
  return `npx -y @elitedcs/ghl-mcp@latest dashboard --port=${port}`;
38939
39511
  }
38940
39512
  function installingEntry() {
38941
- return path28.join(__dirname, "index.js");
39513
+ return path29.join(__dirname, "index.js");
38942
39514
  }
38943
39515
  function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
38944
39516
  const cmd = launchCommand(port);
@@ -38946,12 +39518,12 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
38946
39518
  const systemApps = "/Applications";
38947
39519
  let base = home;
38948
39520
  try {
38949
- fs30.accessSync(systemApps, fs30.constants.W_OK);
39521
+ fs31.accessSync(systemApps, fs31.constants.W_OK);
38950
39522
  base = "";
38951
39523
  } catch {
38952
39524
  }
38953
- const appDir = base ? path28.join(home, "Applications", "Command OS.app") : path28.join(systemApps, "Command OS.app");
38954
- const macOSDir = path28.join(appDir, "Contents", "MacOS");
39525
+ const appDir = base ? path29.join(home, "Applications", "Command OS.app") : path29.join(systemApps, "Command OS.app");
39526
+ const macOSDir = path29.join(appDir, "Contents", "MacOS");
38955
39527
  const script = [
38956
39528
  "#!/bin/bash",
38957
39529
  "# GHL Command \u2014 Command OS launcher (regenerate: ghl-mcp install-launcher)",
@@ -38980,13 +39552,13 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
38980
39552
  targetPath: appDir,
38981
39553
  humanLocation: base ? "your personal Applications folder at ~/Applications (Finder \u2192 Go \u2192 Home \u2192 Applications)" : "your Applications folder \u2014 open Finder \u2192 Applications and look for \u201CCommand OS\u201D",
38982
39554
  files: [
38983
- { path: path28.join(macOSDir, "command-os"), contents: script, executable: true },
38984
- { path: path28.join(appDir, "Contents", "Info.plist"), contents: plist, executable: false }
39555
+ { path: path29.join(macOSDir, "command-os"), contents: script, executable: true },
39556
+ { path: path29.join(appDir, "Contents", "Info.plist"), contents: plist, executable: false }
38985
39557
  ]
38986
39558
  };
38987
39559
  }
38988
39560
  if (platform2 === "win32") {
38989
- const target2 = path28.join(home, "Desktop", "Command OS.cmd");
39561
+ const target2 = path29.join(home, "Desktop", "Command OS.cmd");
38990
39562
  return {
38991
39563
  platform: platform2,
38992
39564
  targetPath: target2,
@@ -38999,7 +39571,7 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
38999
39571
  ].join("\r\n"), executable: false }]
39000
39572
  };
39001
39573
  }
39002
- const target = path28.join(home, ".local", "share", "applications", "command-os.desktop");
39574
+ const target = path29.join(home, ".local", "share", "applications", "command-os.desktop");
39003
39575
  return {
39004
39576
  platform: platform2,
39005
39577
  targetPath: target,
@@ -39023,7 +39595,7 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
39023
39595
  function launcherInstalled(port = 7300, platform2 = process.platform, home = os4.homedir()) {
39024
39596
  try {
39025
39597
  const plan = planLauncher(platform2, home, port, installingEntry());
39026
- return plan.files.every((f) => fs30.existsSync(f.path));
39598
+ return plan.files.every((f) => fs31.existsSync(f.path));
39027
39599
  } catch {
39028
39600
  return false;
39029
39601
  }
@@ -39034,8 +39606,8 @@ function installLauncher(argv = []) {
39034
39606
  const plan = planLauncher(process.platform, os4.homedir(), port, installingEntry());
39035
39607
  try {
39036
39608
  for (const f of plan.files) {
39037
- fs30.mkdirSync(path28.dirname(f.path), { recursive: true });
39038
- fs30.writeFileSync(f.path, f.contents, { mode: f.executable ? 493 : 420 });
39609
+ fs31.mkdirSync(path29.dirname(f.path), { recursive: true });
39610
+ fs31.writeFileSync(f.path, f.contents, { mode: f.executable ? 493 : 420 });
39039
39611
  }
39040
39612
  } catch (e) {
39041
39613
  process.stderr.write(`
@@ -39055,47 +39627,47 @@ function installLauncher(argv = []) {
39055
39627
  ].join("\n"));
39056
39628
  return 0;
39057
39629
  }
39058
- var fs30, os4, path28;
39630
+ var fs31, os4, path29;
39059
39631
  var init_launcher = __esm({
39060
39632
  "src/launcher.ts"() {
39061
39633
  "use strict";
39062
- fs30 = __toESM(require("fs"));
39634
+ fs31 = __toESM(require("fs"));
39063
39635
  os4 = __toESM(require("os"));
39064
- path28 = __toESM(require("path"));
39636
+ path29 = __toESM(require("path"));
39065
39637
  }
39066
39638
  });
39067
39639
 
39068
39640
  // src/npx-cache-check.ts
39069
39641
  function defaultNpxCacheDir(env2 = process.env, platform2 = process.platform, home = os5.homedir()) {
39070
39642
  const configured = env2.npm_config_cache?.trim();
39071
- const cache = configured ? configured : platform2 === "win32" ? path29.join(env2.LOCALAPPDATA?.trim() || path29.join(home, "AppData", "Local"), "npm-cache") : path29.join(home, ".npm");
39072
- return path29.join(cache, "_npx");
39643
+ const cache = configured ? configured : platform2 === "win32" ? path30.join(env2.LOCALAPPDATA?.trim() || path30.join(home, "AppData", "Local"), "npm-cache") : path30.join(home, ".npm");
39644
+ return path30.join(cache, "_npx");
39073
39645
  }
39074
39646
  function damagedNpxDownloads(npxDir) {
39075
39647
  let entries;
39076
39648
  try {
39077
- entries = fs31.readdirSync(npxDir, { withFileTypes: true });
39649
+ entries = fs32.readdirSync(npxDir, { withFileTypes: true });
39078
39650
  } catch {
39079
39651
  return [];
39080
39652
  }
39081
39653
  const damaged = [];
39082
39654
  const gone = (p) => {
39083
39655
  try {
39084
- fs31.statSync(p);
39656
+ fs32.statSync(p);
39085
39657
  return false;
39086
39658
  } catch (e) {
39087
39659
  return e.code === "ENOENT";
39088
39660
  }
39089
39661
  };
39090
39662
  for (const entry of entries) {
39091
- const dir = path29.join(npxDir, entry.name);
39092
- const pkg2 = path29.join(dir, PACKAGE_SUBDIR);
39663
+ const dir = path30.join(npxDir, entry.name);
39664
+ const pkg2 = path30.join(dir, PACKAGE_SUBDIR);
39093
39665
  try {
39094
- if (!fs31.statSync(dir).isDirectory() || !fs31.statSync(pkg2).isDirectory()) continue;
39666
+ if (!fs32.statSync(dir).isDirectory() || !fs32.statSync(pkg2).isDirectory()) continue;
39095
39667
  } catch {
39096
39668
  continue;
39097
39669
  }
39098
- if (gone(path29.join(dir, "package.json")) || gone(path29.join(pkg2, "package.json"))) damaged.push(dir);
39670
+ if (gone(path30.join(dir, "package.json")) || gone(path30.join(pkg2, "package.json"))) damaged.push(dir);
39099
39671
  }
39100
39672
  return damaged.sort();
39101
39673
  }
@@ -39104,14 +39676,14 @@ function damagedDownloadLine(dirs) {
39104
39676
  const one = dirs.length === 1;
39105
39677
  return `${one ? "One earlier download" : "Earlier downloads"} of GHL Command on this computer ${one ? "is" : "are"} damaged, which makes Claude say "Connection closed" instead of starting it. Delete ${one ? "this folder" : "these folders"}, then start Claude again: ${dirs.join(" and ")}`;
39106
39678
  }
39107
- var fs31, os5, path29, PACKAGE_SUBDIR;
39679
+ var fs32, os5, path30, PACKAGE_SUBDIR;
39108
39680
  var init_npx_cache_check = __esm({
39109
39681
  "src/npx-cache-check.ts"() {
39110
39682
  "use strict";
39111
- fs31 = __toESM(require("node:fs"));
39683
+ fs32 = __toESM(require("node:fs"));
39112
39684
  os5 = __toESM(require("node:os"));
39113
- path29 = __toESM(require("node:path"));
39114
- PACKAGE_SUBDIR = path29.join("node_modules", "@elitedcs", "ghl-mcp");
39685
+ path30 = __toESM(require("node:path"));
39686
+ PACKAGE_SUBDIR = path30.join("node_modules", "@elitedcs", "ghl-mcp");
39115
39687
  }
39116
39688
  });
39117
39689
 
@@ -39135,7 +39707,7 @@ function defaultIO() {
39135
39707
  npxCacheDir: defaultNpxCacheDir(),
39136
39708
  out: (l) => process.stdout.write(l + "\n"),
39137
39709
  err: (l) => process.stderr.write(l + "\n"),
39138
- rename: (from, to) => fs32.renameSync(from, to),
39710
+ rename: (from, to) => fs33.renameSync(from, to),
39139
39711
  sleep: (ms) => new Promise((r) => setTimeout(r, ms))
39140
39712
  };
39141
39713
  }
@@ -39145,32 +39717,32 @@ function candidateConfigPaths(opts) {
39145
39717
  const file2 = "claude_desktop_config.json";
39146
39718
  const candidates = [];
39147
39719
  if (platform2 === "darwin") {
39148
- candidates.push(path30.join(home, "Library", "Application Support", "Claude", file2));
39720
+ candidates.push(path31.join(home, "Library", "Application Support", "Claude", file2));
39149
39721
  } else if (platform2 === "win32") {
39150
- const appData = opts?.appData ?? process.env.APPDATA ?? path30.join(home, "AppData", "Roaming");
39151
- candidates.push(path30.join(appData, "Claude", file2));
39152
- const localAppData = opts?.localAppData ?? process.env.LOCALAPPDATA ?? path30.join(home, "AppData", "Local");
39153
- const packagesDir = path30.join(localAppData, "Packages");
39722
+ const appData = opts?.appData ?? process.env.APPDATA ?? path31.join(home, "AppData", "Roaming");
39723
+ candidates.push(path31.join(appData, "Claude", file2));
39724
+ const localAppData = opts?.localAppData ?? process.env.LOCALAPPDATA ?? path31.join(home, "AppData", "Local");
39725
+ const packagesDir = path31.join(localAppData, "Packages");
39154
39726
  try {
39155
- for (const entry of fs32.readdirSync(packagesDir)) {
39727
+ for (const entry of fs33.readdirSync(packagesDir)) {
39156
39728
  if (entry.startsWith("Claude_")) {
39157
- candidates.push(path30.join(packagesDir, entry, "LocalCache", "Roaming", "Claude", file2));
39729
+ candidates.push(path31.join(packagesDir, entry, "LocalCache", "Roaming", "Claude", file2));
39158
39730
  }
39159
39731
  }
39160
39732
  } catch {
39161
39733
  }
39162
39734
  } else {
39163
- candidates.push(path30.join(home, ".config", "Claude", file2));
39735
+ candidates.push(path31.join(home, ".config", "Claude", file2));
39164
39736
  }
39165
39737
  return candidates;
39166
39738
  }
39167
39739
  function resolveConfigPath(explicitPath) {
39168
39740
  if (explicitPath) {
39169
- const p = path30.resolve(explicitPath);
39170
- return { configPath: p, exists: fs32.existsSync(p) };
39741
+ const p = path31.resolve(explicitPath);
39742
+ return { configPath: p, exists: fs33.existsSync(p) };
39171
39743
  }
39172
39744
  const candidates = candidateConfigPaths();
39173
- const existing = candidates.filter((c) => fs32.existsSync(c));
39745
+ const existing = candidates.filter((c) => fs33.existsSync(c));
39174
39746
  if (existing.length > 1) {
39175
39747
  throw new InstallStop(
39176
39748
  EXIT_REFUSED,
@@ -39187,22 +39759,22 @@ function resolveConfigPath(explicitPath) {
39187
39759
  function resolveSymlinkPolicy(configPath, explicitPath, home = os6.homedir()) {
39188
39760
  let st;
39189
39761
  try {
39190
- st = fs32.lstatSync(configPath);
39762
+ st = fs33.lstatSync(configPath);
39191
39763
  } catch {
39192
39764
  return configPath;
39193
39765
  }
39194
39766
  if (!st.isSymbolicLink()) return configPath;
39195
39767
  let real;
39196
39768
  try {
39197
- real = fs32.realpathSync(configPath);
39769
+ real = fs33.realpathSync(configPath);
39198
39770
  } catch {
39199
39771
  throw new InstallStop(
39200
39772
  EXIT_REFUSED,
39201
39773
  `${configPath} is a link that points to a file that does not exist. Fix or remove the link, or run again with --path to a real file. Nothing was changed.`
39202
39774
  );
39203
39775
  }
39204
- const rel = path30.relative(path30.resolve(home), real);
39205
- const outsideHome = rel.startsWith("..") || path30.isAbsolute(rel);
39776
+ const rel = path31.relative(path31.resolve(home), real);
39777
+ const outsideHome = rel.startsWith("..") || path31.isAbsolute(rel);
39206
39778
  if (outsideHome && !explicitPath) {
39207
39779
  throw new InstallStop(
39208
39780
  EXIT_REFUSED,
@@ -39212,8 +39784,8 @@ function resolveSymlinkPolicy(configPath, explicitPath, home = os6.homedir()) {
39212
39784
  return real;
39213
39785
  }
39214
39786
  function readConfigBytes(configPath) {
39215
- const bytes = fs32.readFileSync(configPath);
39216
- const stat = fs32.statSync(configPath);
39787
+ const bytes = fs33.readFileSync(configPath);
39788
+ const stat = fs33.statSync(configPath);
39217
39789
  if (bytes.length >= 2 && (bytes[0] === 255 && bytes[1] === 254 || bytes[0] === 254 && bytes[1] === 255)) {
39218
39790
  throw new InstallStop(
39219
39791
  EXIT_REFUSED,
@@ -39287,23 +39859,23 @@ function backupStamp(now) {
39287
39859
  }
39288
39860
  function chooseBackupPath(configPath, now = /* @__PURE__ */ new Date()) {
39289
39861
  const base = `${configPath}.bak-${backupStamp(now)}`;
39290
- if (!fs32.existsSync(base)) return base;
39862
+ if (!fs33.existsSync(base)) return base;
39291
39863
  for (let i = 2; ; i++) {
39292
39864
  const candidate = `${base}-${i}`;
39293
- if (!fs32.existsSync(candidate)) return candidate;
39865
+ if (!fs33.existsSync(candidate)) return candidate;
39294
39866
  }
39295
39867
  }
39296
39868
  function writeVerifiedBackup(configPath, originalBytes) {
39297
39869
  const backupPath = chooseBackupPath(configPath);
39298
39870
  try {
39299
- fs32.writeFileSync(backupPath, originalBytes);
39300
- const readBack = fs32.readFileSync(backupPath);
39871
+ fs33.writeFileSync(backupPath, originalBytes);
39872
+ const readBack = fs33.readFileSync(backupPath);
39301
39873
  if (!readBack.equals(originalBytes)) {
39302
39874
  throw new Error("backup read-back did not match");
39303
39875
  }
39304
39876
  } catch (e) {
39305
39877
  try {
39306
- fs32.rmSync(backupPath, { force: true });
39878
+ fs33.rmSync(backupPath, { force: true });
39307
39879
  } catch {
39308
39880
  }
39309
39881
  throw new InstallStop(
@@ -39314,7 +39886,7 @@ function writeVerifiedBackup(configPath, originalBytes) {
39314
39886
  return backupPath;
39315
39887
  }
39316
39888
  function sha256(bytes) {
39317
- return (0, import_node_crypto11.createHash)("sha256").update(bytes).digest("hex");
39889
+ return (0, import_node_crypto12.createHash)("sha256").update(bytes).digest("hex");
39318
39890
  }
39319
39891
  function baselineOf(bytes, stat) {
39320
39892
  return { size: stat.size, mtimeMs: stat.mtimeMs, hash: sha256(bytes) };
@@ -39322,9 +39894,9 @@ function baselineOf(bytes, stat) {
39322
39894
  function assertNotStale(configPath, baseline) {
39323
39895
  let ok = false;
39324
39896
  try {
39325
- const st = fs32.statSync(configPath);
39897
+ const st = fs33.statSync(configPath);
39326
39898
  if (st.size === baseline.size && st.mtimeMs === baseline.mtimeMs) {
39327
- ok = sha256(fs32.readFileSync(configPath)) === baseline.hash;
39899
+ ok = sha256(fs33.readFileSync(configPath)) === baseline.hash;
39328
39900
  } else {
39329
39901
  ok = false;
39330
39902
  }
@@ -39339,14 +39911,14 @@ function assertNotStale(configPath, baseline) {
39339
39911
  }
39340
39912
  }
39341
39913
  async function atomicReplace(opts) {
39342
- const dir = path30.dirname(opts.configPath);
39343
- const tempPath = path30.join(dir, `.${path30.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto11.randomBytes)(4).toString("hex")}`);
39344
- const fd = fs32.openSync(tempPath, "w");
39914
+ const dir = path31.dirname(opts.configPath);
39915
+ const tempPath = path31.join(dir, `.${path31.basename(opts.configPath)}.tmp-${process.pid}-${(0, import_node_crypto12.randomBytes)(4).toString("hex")}`);
39916
+ const fd = fs33.openSync(tempPath, "w");
39345
39917
  try {
39346
- fs32.writeFileSync(fd, opts.newBytes);
39347
- fs32.fsyncSync(fd);
39918
+ fs33.writeFileSync(fd, opts.newBytes);
39919
+ fs33.fsyncSync(fd);
39348
39920
  } finally {
39349
- fs32.closeSync(fd);
39921
+ fs33.closeSync(fd);
39350
39922
  }
39351
39923
  try {
39352
39924
  for (let attempt = 0; ; attempt++) {
@@ -39371,17 +39943,17 @@ async function atomicReplace(opts) {
39371
39943
  }
39372
39944
  } catch (e) {
39373
39945
  try {
39374
- fs32.rmSync(tempPath, { force: true });
39946
+ fs33.rmSync(tempPath, { force: true });
39375
39947
  } catch {
39376
39948
  }
39377
39949
  throw e;
39378
39950
  }
39379
39951
  try {
39380
- const dirFd = fs32.openSync(dir, "r");
39952
+ const dirFd = fs33.openSync(dir, "r");
39381
39953
  try {
39382
- fs32.fsyncSync(dirFd);
39954
+ fs33.fsyncSync(dirFd);
39383
39955
  } finally {
39384
- fs32.closeSync(dirFd);
39956
+ fs33.closeSync(dirFd);
39385
39957
  }
39386
39958
  } catch {
39387
39959
  }
@@ -39439,7 +40011,7 @@ async function runInstall(argv, ioOverride) {
39439
40011
  try {
39440
40012
  const resolved2 = resolveConfigPath(flags.path);
39441
40013
  const configPath = resolveSymlinkPolicy(resolved2.configPath, flags.path !== void 0);
39442
- const exists = fs32.existsSync(configPath);
40014
+ const exists = fs33.existsSync(configPath);
39443
40015
  let read2 = null;
39444
40016
  let outcome;
39445
40017
  if (exists) {
@@ -39471,7 +40043,7 @@ async function runInstall(argv, ioOverride) {
39471
40043
  return EXIT_OK;
39472
40044
  }
39473
40045
  const backupPath = exists && read2 ? writeVerifiedBackup(configPath, read2.bytes) : null;
39474
- if (!exists) fs32.mkdirSync(path30.dirname(configPath), { recursive: true });
40046
+ if (!exists) fs33.mkdirSync(path31.dirname(configPath), { recursive: true });
39475
40047
  await atomicReplace({
39476
40048
  configPath,
39477
40049
  newBytes,
@@ -39490,14 +40062,14 @@ async function runInstall(argv, ioOverride) {
39490
40062
  return EXIT_ABORTED;
39491
40063
  }
39492
40064
  }
39493
- var fs32, os6, path30, import_node_crypto11, import_node_util, import_json5, SERVER_KEY, DESIRED_ENTRY, EXIT_OK, EXIT_USAGE, EXIT_REFUSED, EXIT_ABORTED, RENAME_RETRY_DELAYS_MS, LOCK_ERROR_CODES, BOM_UTF8, InstallStop;
40065
+ var fs33, os6, path31, import_node_crypto12, import_node_util, import_json5, SERVER_KEY, DESIRED_ENTRY, EXIT_OK, EXIT_USAGE, EXIT_REFUSED, EXIT_ABORTED, RENAME_RETRY_DELAYS_MS, LOCK_ERROR_CODES, BOM_UTF8, InstallStop;
39494
40066
  var init_config_installer = __esm({
39495
40067
  "src/config-installer.ts"() {
39496
40068
  "use strict";
39497
- fs32 = __toESM(require("node:fs"));
40069
+ fs33 = __toESM(require("node:fs"));
39498
40070
  os6 = __toESM(require("node:os"));
39499
- path30 = __toESM(require("node:path"));
39500
- import_node_crypto11 = require("node:crypto");
40071
+ path31 = __toESM(require("node:path"));
40072
+ import_node_crypto12 = require("node:crypto");
39501
40073
  import_node_util = require("node:util");
39502
40074
  import_json5 = __toESM(require("json5"));
39503
40075
  init_npx_cache_check();
@@ -39530,9 +40102,9 @@ function errLine(msg3) {
39530
40102
  function preflightWritable() {
39531
40103
  try {
39532
40104
  const dir = ensureAppDataDir();
39533
- const probe = path31.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
39534
- fs33.writeFileSync(probe, "ok");
39535
- fs33.unlinkSync(probe);
40105
+ const probe = path32.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
40106
+ fs34.writeFileSync(probe, "ok");
40107
+ fs34.unlinkSync(probe);
39536
40108
  return true;
39537
40109
  } catch (error) {
39538
40110
  errLine(`Config dir is not writable: ${error instanceof Error ? error.message : String(error)}`);
@@ -39801,13 +40373,13 @@ async function runCli(subcommand, argv) {
39801
40373
  return EXIT_USAGE2;
39802
40374
  }
39803
40375
  }
39804
- var import_node_util2, fs33, path31, import_crypto2, EXIT_OK2, EXIT_USAGE2, EXIT_VALIDATION, EXIT_FS, USAGE;
40376
+ var import_node_util2, fs34, path32, import_crypto2, EXIT_OK2, EXIT_USAGE2, EXIT_VALIDATION, EXIT_FS, USAGE;
39805
40377
  var init_cli = __esm({
39806
40378
  "src/cli.ts"() {
39807
40379
  "use strict";
39808
40380
  import_node_util2 = require("node:util");
39809
- fs33 = __toESM(require("fs"));
39810
- path31 = __toESM(require("path"));
40381
+ fs34 = __toESM(require("fs"));
40382
+ path32 = __toESM(require("path"));
39811
40383
  import_crypto2 = require("crypto");
39812
40384
  init_ghl_client();
39813
40385
  init_token_registry();
@@ -40353,11 +40925,11 @@ __export(intake_overlay_exports, {
40353
40925
  writeOverlay: () => writeOverlay
40354
40926
  });
40355
40927
  function overlayPath() {
40356
- return path32.join(appDataDir(), "intake-overlay.json");
40928
+ return path33.join(appDataDir(), "intake-overlay.json");
40357
40929
  }
40358
40930
  function readOverlay() {
40359
40931
  try {
40360
- const raw = JSON.parse(fs34.readFileSync(overlayPath(), "utf8"));
40932
+ const raw = JSON.parse(fs35.readFileSync(overlayPath(), "utf8"));
40361
40933
  if (raw && typeof raw === "object") return raw;
40362
40934
  } catch {
40363
40935
  }
@@ -40365,14 +40937,14 @@ function readOverlay() {
40365
40937
  }
40366
40938
  function writeOverlay(layer) {
40367
40939
  ensureAppDataDir();
40368
- fs34.writeFileSync(overlayPath(), JSON.stringify(layer, null, 2), { mode: 384 });
40940
+ fs35.writeFileSync(overlayPath(), JSON.stringify(layer, null, 2), { mode: 384 });
40369
40941
  }
40370
- var fs34, path32;
40942
+ var fs35, path33;
40371
40943
  var init_intake_overlay = __esm({
40372
40944
  "src/intake-overlay.ts"() {
40373
40945
  "use strict";
40374
- fs34 = __toESM(require("fs"));
40375
- path32 = __toESM(require("path"));
40946
+ fs35 = __toESM(require("fs"));
40947
+ path33 = __toESM(require("path"));
40376
40948
  init_credentials_store();
40377
40949
  }
40378
40950
  });
@@ -40848,49 +41420,49 @@ What we know about this client, newest call last. Advice for the agency, never s
40848
41420
  };
40849
41421
  }
40850
41422
  function inside(base, p) {
40851
- return p.startsWith(base.endsWith(path33.sep) ? base : base + path33.sep);
41423
+ return p.startsWith(base.endsWith(path34.sep) ? base : base + path34.sep);
40852
41424
  }
40853
41425
  function writeClientFileLocally(baseDir, file2) {
40854
41426
  try {
40855
- if (!path33.isAbsolute(baseDir)) return { written: false, error: `the client files folder must be a full path, not "${baseDir}".` };
40856
- const base = path33.resolve(baseDir);
41427
+ if (!path34.isAbsolute(baseDir)) return { written: false, error: `the client files folder must be a full path, not "${baseDir}".` };
41428
+ const base = path34.resolve(baseDir);
40857
41429
  let st;
40858
41430
  try {
40859
- st = fs35.statSync(base);
41431
+ st = fs36.statSync(base);
40860
41432
  } catch {
40861
41433
  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.` };
40862
41434
  }
40863
41435
  if (!st.isDirectory()) return { written: false, error: `${base} is not a folder.` };
40864
- const realBase = fs35.realpathSync(base);
40865
- const callsDir = path33.resolve(base, ...file2.call.folderParts);
40866
- const clientDir = path33.resolve(base, ...file2.understanding.folderParts);
41436
+ const realBase = fs36.realpathSync(base);
41437
+ const callsDir = path34.resolve(base, ...file2.call.folderParts);
41438
+ const clientDir = path34.resolve(base, ...file2.understanding.folderParts);
40867
41439
  if (!inside(base, callsDir) || !inside(base, clientDir)) return { written: false, error: "the client folder name would land outside the client files folder." };
40868
41440
  let cur = base;
40869
41441
  for (const part of file2.call.folderParts) {
40870
- cur = path33.join(cur, part);
41442
+ cur = path34.join(cur, part);
40871
41443
  let here;
40872
41444
  try {
40873
- here = fs35.lstatSync(cur);
41445
+ here = fs36.lstatSync(cur);
40874
41446
  } catch {
40875
41447
  }
40876
41448
  if (here && (here.isSymbolicLink() || !here.isDirectory())) {
40877
41449
  return { written: false, error: `${cur} is a link or a file, not a folder, so nothing was written.` };
40878
41450
  }
40879
- if (!here) fs35.mkdirSync(cur, { mode: 448 });
41451
+ if (!here) fs36.mkdirSync(cur, { mode: 448 });
40880
41452
  }
40881
- if (!inside(realBase, fs35.realpathSync(callsDir))) return { written: false, error: "the client folder resolves outside the client files folder, so nothing was written." };
41453
+ if (!inside(realBase, fs36.realpathSync(callsDir))) return { written: false, error: "the client folder resolves outside the client files folder, so nothing was written." };
40882
41454
  const stem = file2.call.name.replace(/\.md$/, "");
40883
41455
  const tStem = file2.transcript.name.replace(/\.md$/, "");
40884
41456
  let callFile = "", transcriptFile = "";
40885
41457
  for (let n = 1; n <= 50 && !callFile; n++) {
40886
41458
  const suffix = n === 1 ? "" : ` (${n})`;
40887
- const callPath = path33.join(callsDir, `${stem}${suffix}.md`);
41459
+ const callPath = path34.join(callsDir, `${stem}${suffix}.md`);
40888
41460
  const tName = `${tStem.slice(0, tStem.length - " transcript".length)}${suffix} transcript.md`;
40889
- const tPath = path33.join(callsDir, tName);
40890
- if (fs35.existsSync(callPath) || fs35.existsSync(tPath)) continue;
41461
+ const tPath = path34.join(callsDir, tName);
41462
+ if (fs36.existsSync(callPath) || fs36.existsSync(tPath)) continue;
40891
41463
  try {
40892
- fs35.writeFileSync(tPath, file2.transcript.content, { encoding: "utf8", flag: "wx", mode: 384 });
40893
- fs35.writeFileSync(callPath, file2.call.content.split(`Calls/${file2.transcript.name}`).join(`Calls/${tName}`), { encoding: "utf8", flag: "wx", mode: 384 });
41464
+ fs36.writeFileSync(tPath, file2.transcript.content, { encoding: "utf8", flag: "wx", mode: 384 });
41465
+ fs36.writeFileSync(callPath, file2.call.content.split(`Calls/${file2.transcript.name}`).join(`Calls/${tName}`), { encoding: "utf8", flag: "wx", mode: 384 });
40894
41466
  callFile = callPath;
40895
41467
  transcriptFile = tPath;
40896
41468
  } catch (e) {
@@ -40898,22 +41470,22 @@ function writeClientFileLocally(baseDir, file2) {
40898
41470
  }
40899
41471
  }
40900
41472
  if (!callFile) return { written: false, error: `fifty calls named "${stem}" already exist; nothing was overwritten.` };
40901
- fs35.chmodSync(callFile, 384);
40902
- fs35.chmodSync(transcriptFile, 384);
40903
- const understandingFile = path33.join(clientDir, file2.understanding.name);
41473
+ fs36.chmodSync(callFile, 384);
41474
+ fs36.chmodSync(transcriptFile, 384);
41475
+ const understandingFile = path34.join(clientDir, file2.understanding.name);
40904
41476
  let created = false;
40905
41477
  try {
40906
- fs35.writeFileSync(understandingFile, file2.understanding.headerIfNew + file2.understanding.append, { encoding: "utf8", flag: "wx", mode: 384 });
41478
+ fs36.writeFileSync(understandingFile, file2.understanding.headerIfNew + file2.understanding.append, { encoding: "utf8", flag: "wx", mode: 384 });
40907
41479
  created = true;
40908
41480
  } catch (e) {
40909
41481
  if (e.code !== "EEXIST") throw e;
40910
41482
  }
40911
41483
  if (!created) {
40912
- const st2 = fs35.lstatSync(understandingFile);
41484
+ const st2 = fs36.lstatSync(understandingFile);
40913
41485
  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}` };
40914
- fs35.appendFileSync(understandingFile, file2.understanding.append, { encoding: "utf8", mode: 384 });
41486
+ fs36.appendFileSync(understandingFile, file2.understanding.append, { encoding: "utf8", mode: 384 });
40915
41487
  }
40916
- fs35.chmodSync(understandingFile, 384);
41488
+ fs36.chmodSync(understandingFile, 384);
40917
41489
  return { written: true, callFile, transcriptFile, understandingFile };
40918
41490
  } catch (e) {
40919
41491
  return { written: false, error: `could not write the client file: ${e?.message ?? String(e)}` };
@@ -40923,28 +41495,28 @@ function writeConfirmationLocally(baseDir, file2) {
40923
41495
  const c = file2.confirmation;
40924
41496
  if (!c) return writeClientFileLocally(baseDir, file2);
40925
41497
  try {
40926
- if (!path33.isAbsolute(baseDir)) return { written: false, error: `the client files folder must be a full path, not "${baseDir}".` };
40927
- const base = path33.resolve(baseDir);
40928
- const callPath = path33.resolve(base, ...file2.call.folderParts, c.callName);
40929
- const understandingPath = path33.resolve(base, ...file2.understanding.folderParts, file2.understanding.name);
41498
+ if (!path34.isAbsolute(baseDir)) return { written: false, error: `the client files folder must be a full path, not "${baseDir}".` };
41499
+ const base = path34.resolve(baseDir);
41500
+ const callPath = path34.resolve(base, ...file2.call.folderParts, c.callName);
41501
+ const understandingPath = path34.resolve(base, ...file2.understanding.folderParts, file2.understanding.name);
40930
41502
  if (!inside(base, callPath) || !inside(base, understandingPath)) return { written: false, error: "the client folder name would land outside the client files folder." };
40931
41503
  let st;
40932
41504
  try {
40933
- st = fs35.lstatSync(callPath);
41505
+ st = fs36.lstatSync(callPath);
40934
41506
  } catch {
40935
41507
  }
40936
41508
  if (!st) return writeClientFileLocally(baseDir, { ...file2, confirmation: void 0 });
40937
41509
  if (st.isSymbolicLink() || !st.isFile() || st.nlink > 1) return { written: false, error: `${callPath} is a link, not a plain file, so nothing was added to it.` };
40938
- if (!inside(fs35.realpathSync(base), fs35.realpathSync(callPath))) return { written: false, error: "the call file resolves outside the client files folder, so nothing was added." };
40939
- fs35.appendFileSync(callPath, c.appendToCall, { encoding: "utf8", mode: 384 });
41510
+ if (!inside(fs36.realpathSync(base), fs36.realpathSync(callPath))) return { written: false, error: "the call file resolves outside the client files folder, so nothing was added." };
41511
+ fs36.appendFileSync(callPath, c.appendToCall, { encoding: "utf8", mode: 384 });
40940
41512
  let u;
40941
41513
  try {
40942
- u = fs35.lstatSync(understandingPath);
41514
+ u = fs36.lstatSync(understandingPath);
40943
41515
  } catch {
40944
41516
  }
40945
41517
  if (u && (u.isSymbolicLink() || !u.isFile() || u.nlink > 1)) return { written: false, error: `${understandingPath} is a link, not a plain file, so it was not appended to. The call file was updated: ${callPath}` };
40946
- if (u) fs35.appendFileSync(understandingPath, c.appendToUnderstanding, { encoding: "utf8", mode: 384 });
40947
- else fs35.writeFileSync(understandingPath, file2.understanding.headerIfNew + c.appendToUnderstanding, { encoding: "utf8", flag: "wx", mode: 384 });
41518
+ if (u) fs36.appendFileSync(understandingPath, c.appendToUnderstanding, { encoding: "utf8", mode: 384 });
41519
+ else fs36.writeFileSync(understandingPath, file2.understanding.headerIfNew + c.appendToUnderstanding, { encoding: "utf8", flag: "wx", mode: 384 });
40948
41520
  return { written: true, callFile: callPath, transcriptFile: "", understandingFile: understandingPath };
40949
41521
  } catch (e) {
40950
41522
  return { written: false, error: `could not add the confirmation to the client file: ${e?.message ?? String(e)}` };
@@ -41185,12 +41757,12 @@ ${body}
41185
41757
  localCopy
41186
41758
  };
41187
41759
  }
41188
- var fs35, path33, 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;
41760
+ var fs36, path34, 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;
41189
41761
  var init_client_calls = __esm({
41190
41762
  "src/command-os/client-calls.ts"() {
41191
41763
  "use strict";
41192
- fs35 = __toESM(require("node:fs"));
41193
- path33 = __toESM(require("node:path"));
41764
+ fs36 = __toESM(require("node:fs"));
41765
+ path34 = __toESM(require("node:path"));
41194
41766
  init_transcript();
41195
41767
  init_words();
41196
41768
  init_store3();
@@ -41596,11 +42168,11 @@ function registerAssessmentTools(server2, client) {
41596
42168
  warnings.push(`"${svc.name}" covers ${covers.length ? covers.map((c) => `"${c}"`).join(", ") : "nothing"}, and this assessment found ${covers.length ? "none of those" : "no finding for it"}. It is priced in the investment total but the report never says what it is for.`);
41597
42169
  }
41598
42170
  }
41599
- const dir = args.outDir ?? (0, import_node_path7.join)((0, import_node_os.homedir)(), "Documents", "assessments");
41600
- (0, import_node_fs7.mkdirSync)(dir, { recursive: true });
42171
+ const dir = args.outDir ?? (0, import_node_path8.join)((0, import_node_os.homedir)(), "Documents", "assessments");
42172
+ (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
41601
42173
  const slug3 = (state.biz || "assessment").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
41602
- const file2 = (0, import_node_path7.join)(dir, `${slug3}-${(0, import_node_crypto12.randomBytes)(4).toString("hex")}.html`);
41603
- (0, import_node_fs7.writeFileSync)(file2, html2, "utf8");
42174
+ const file2 = (0, import_node_path8.join)(dir, `${slug3}-${(0, import_node_crypto13.randomBytes)(4).toString("hex")}.html`);
42175
+ (0, import_node_fs8.writeFileSync)(file2, html2, "utf8");
41604
42176
  return {
41605
42177
  written: file2,
41606
42178
  verdict: r.verdict,
@@ -41622,7 +42194,7 @@ function registerAssessmentTools(server2, client) {
41622
42194
  }
41623
42195
  );
41624
42196
  }
41625
- var import_node_util3, import_zod61, import_node_crypto12, import_node_fs7, import_node_os, import_node_path7, QUESTION_UNITS, AREA_TITLE, ASK, AREA_OF, ASSUMPTION_LABEL, LOOKUP, StateSchema;
42197
+ var import_node_util3, import_zod61, import_node_crypto13, import_node_fs8, import_node_os, import_node_path8, QUESTION_UNITS, AREA_TITLE, ASK, AREA_OF, ASSUMPTION_LABEL, LOOKUP, StateSchema;
41626
42198
  var init_assessment = __esm({
41627
42199
  "src/tools/assessment.ts"() {
41628
42200
  "use strict";
@@ -41635,10 +42207,10 @@ var init_assessment = __esm({
41635
42207
  init_store3();
41636
42208
  init_engine();
41637
42209
  init_report();
41638
- import_node_crypto12 = require("node:crypto");
41639
- import_node_fs7 = require("node:fs");
42210
+ import_node_crypto13 = require("node:crypto");
42211
+ import_node_fs8 = require("node:fs");
41640
42212
  import_node_os = require("node:os");
41641
- import_node_path7 = require("node:path");
42213
+ import_node_path8 = require("node:path");
41642
42214
  QUESTION_UNITS = new Map(Q.map((q3) => [q3[0], q3[4]]));
41643
42215
  AREA_TITLE = new Map(AREAS.map((a) => [a[0], a[1]]));
41644
42216
  ASK = new Map(Q.map((q3) => [q3[0], q3[2]]));
@@ -41822,11 +42394,11 @@ function recoverStuckStages(state) {
41822
42394
  return { state: { ...state, clients }, recovered };
41823
42395
  }
41824
42396
  function cockpitStatePath() {
41825
- return path34.join(appDataDir(), "cockpit-state.json");
42397
+ return path35.join(appDataDir(), "cockpit-state.json");
41826
42398
  }
41827
42399
  function readCockpitState() {
41828
42400
  try {
41829
- const raw = JSON.parse(fs36.readFileSync(cockpitStatePath(), "utf8"));
42401
+ const raw = JSON.parse(fs37.readFileSync(cockpitStatePath(), "utf8"));
41830
42402
  if (raw && raw.v === 1 && raw.clients && typeof raw.clients === "object") return raw;
41831
42403
  } catch {
41832
42404
  }
@@ -41834,7 +42406,7 @@ function readCockpitState() {
41834
42406
  }
41835
42407
  function writeCockpitState(state) {
41836
42408
  ensureAppDataDir();
41837
- fs36.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
42409
+ fs37.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
41838
42410
  }
41839
42411
  function setStage(state, locationId2, stageIndex, status) {
41840
42412
  if (!Number.isInteger(stageIndex) || stageIndex < 0 || stageIndex >= STAGES2.length) throw new Error("bad stage index");
@@ -42165,7 +42737,7 @@ function notify(message) {
42165
42737
  `);
42166
42738
  try {
42167
42739
  ensureAppDataDir();
42168
- fs36.appendFileSync(path34.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
42740
+ fs37.appendFileSync(path35.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
42169
42741
  `);
42170
42742
  } catch {
42171
42743
  }
@@ -42235,20 +42807,20 @@ function withCockpitShell(page, screen, extraHead = "") {
42235
42807
  function clientDocsDir(locationId2, base = appDataDir()) {
42236
42808
  const safeLoc = String(locationId2).replace(/[^A-Za-z0-9_-]/g, "");
42237
42809
  if (!safeLoc) throw new Error("no client");
42238
- return path34.join(base, "clients", safeLoc, "documents");
42810
+ return path35.join(base, "clients", safeLoc, "documents");
42239
42811
  }
42240
42812
  function safeDocName(name) {
42241
- const cleaned = path34.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, " ").trim();
42242
- const ext = path34.extname(cleaned).toLowerCase();
42813
+ const cleaned = path35.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, " ").trim();
42814
+ const ext = path35.extname(cleaned).toLowerCase();
42243
42815
  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.");
42244
42816
  const stem = cleaned.slice(0, cleaned.length - ext.length).slice(0, 120 - ext.length).trim();
42245
42817
  return `${stem || "document"}${ext}`;
42246
42818
  }
42247
42819
  function listClientDocs(locationId2, base = appDataDir()) {
42248
42820
  const dir = clientDocsDir(locationId2, base);
42249
- if (!fs36.existsSync(dir)) return [];
42250
- return fs36.readdirSync(dir).filter((f) => DOC_EXTS.has(path34.extname(f).toLowerCase())).map((f) => {
42251
- const st = fs36.statSync(path34.join(dir, f));
42821
+ if (!fs37.existsSync(dir)) return [];
42822
+ return fs37.readdirSync(dir).filter((f) => DOC_EXTS.has(path35.extname(f).toLowerCase())).map((f) => {
42823
+ const st = fs37.statSync(path35.join(dir, f));
42252
42824
  return { name: f, bytes: st.size, savedAt: st.mtime.toISOString() };
42253
42825
  }).sort((a, b) => b.savedAt.localeCompare(a.savedAt));
42254
42826
  }
@@ -42266,7 +42838,7 @@ function readFailureText(e) {
42266
42838
  function logQuietly(message) {
42267
42839
  try {
42268
42840
  ensureAppDataDir();
42269
- fs36.appendFileSync(path34.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
42841
+ fs37.appendFileSync(path35.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
42270
42842
  `);
42271
42843
  } catch {
42272
42844
  }
@@ -42339,40 +42911,40 @@ function newClientFailure(error, surface = "command-os", account2) {
42339
42911
  }
42340
42912
  function insideDir(dir, p) {
42341
42913
  try {
42342
- const real = fs36.realpathSync(p);
42343
- const base = fs36.realpathSync(dir);
42344
- return real === base || real.startsWith(base + path34.sep);
42914
+ const real = fs37.realpathSync(p);
42915
+ const base = fs37.realpathSync(dir);
42916
+ return real === base || real.startsWith(base + path35.sep);
42345
42917
  } catch {
42346
42918
  return false;
42347
42919
  }
42348
42920
  }
42349
42921
  function uniqueDocName(dir, name) {
42350
- const ext = path34.extname(name), stem = name.slice(0, name.length - ext.length);
42922
+ const ext = path35.extname(name), stem = name.slice(0, name.length - ext.length);
42351
42923
  let candidate = name;
42352
- for (let n = 2; fs36.existsSync(path34.join(dir, candidate)); n++) candidate = `${stem} (${n})${ext}`;
42924
+ for (let n = 2; fs37.existsSync(path35.join(dir, candidate)); n++) candidate = `${stem} (${n})${ext}`;
42353
42925
  return candidate;
42354
42926
  }
42355
42927
  function writeDocAtomically(dir, name, bytes) {
42356
- fs36.mkdirSync(dir, { recursive: true, mode: 448 });
42928
+ fs37.mkdirSync(dir, { recursive: true, mode: 448 });
42357
42929
  const finalName = uniqueDocName(dir, name);
42358
- const tmp = path34.join(dir, `.${finalName}.${process.pid}.tmp`);
42359
- const fd = fs36.openSync(tmp, fs36.constants.O_WRONLY | fs36.constants.O_CREAT | fs36.constants.O_EXCL | (fs36.constants.O_NOFOLLOW ?? 0), 384);
42930
+ const tmp = path35.join(dir, `.${finalName}.${process.pid}.tmp`);
42931
+ const fd = fs37.openSync(tmp, fs37.constants.O_WRONLY | fs37.constants.O_CREAT | fs37.constants.O_EXCL | (fs37.constants.O_NOFOLLOW ?? 0), 384);
42360
42932
  try {
42361
- fs36.writeSync(fd, bytes);
42362
- fs36.fsyncSync(fd);
42933
+ fs37.writeSync(fd, bytes);
42934
+ fs37.fsyncSync(fd);
42363
42935
  } finally {
42364
- fs36.closeSync(fd);
42936
+ fs37.closeSync(fd);
42365
42937
  }
42366
- const dest = path34.join(dir, finalName);
42938
+ const dest = path35.join(dir, finalName);
42367
42939
  try {
42368
- if (fs36.lstatSync(dest).isSymbolicLink()) {
42369
- fs36.unlinkSync(tmp);
42940
+ if (fs37.lstatSync(dest).isSymbolicLink()) {
42941
+ fs37.unlinkSync(tmp);
42370
42942
  throw new Error("that name is taken by a link");
42371
42943
  }
42372
42944
  } catch (e) {
42373
42945
  if (e.code !== "ENOENT") throw e;
42374
42946
  }
42375
- fs36.renameSync(tmp, dest);
42947
+ fs37.renameSync(tmp, dest);
42376
42948
  return finalName;
42377
42949
  }
42378
42950
  function readableAccount(locationId2, roster = currentRosterRef(), state = readCockpitState()) {
@@ -44005,17 +44577,19 @@ async function runDashboard(argv) {
44005
44577
  const c = clientFor(locationId2);
44006
44578
  if (!c) return void 0;
44007
44579
  let stepLog;
44580
+ let ffAdmin;
44008
44581
  try {
44009
44582
  const b = workflowReadConnection(registry2, c);
44010
44583
  stepLog = liveStepLogReader(locationId2, () => b.buildHeaders());
44584
+ ffAdmin = builderAdmin(b);
44011
44585
  } catch {
44012
44586
  }
44013
44587
  try {
44014
- const run = await finishShopper(locationId2, runId, { client: c, stepLog });
44015
- if (run.status === "watching" && Date.now() >= run.closesAt) {
44588
+ const run = await finishShopper(locationId2, runId, { client: c, stepLog, ffAdmin });
44589
+ if (run.status === "watching") {
44016
44590
  const again = setTimeout(() => {
44017
44591
  void finishShopperQuietly(locationId2, runId);
44018
- }, 6e4);
44592
+ }, Math.max(6e4, run.closesAt - Date.now() + 15e3));
44019
44593
  again.unref?.();
44020
44594
  }
44021
44595
  return run;
@@ -44039,6 +44613,23 @@ async function runDashboard(argv) {
44039
44613
  } catch (e) {
44040
44614
  logQuietly(`secret shopper boot: ${(0, import_node_util4.inspect)(e)}`);
44041
44615
  }
44616
+ const ffSweepAll = async () => {
44617
+ for (const { locationId: locationId2 } of registry2.listLocations()) {
44618
+ if (openCopies(locationId2).length === 0) continue;
44619
+ const c = clientFor(locationId2);
44620
+ if (!c) continue;
44621
+ try {
44622
+ const problems = await sweepLeftovers(builderAdmin(workflowReadConnection(registry2, c)), locationId2);
44623
+ if (problems.length) logQuietly(`fast-forward sweep ${locationId2}: ${problems.join(" ")}`);
44624
+ } catch (e) {
44625
+ logQuietly(`fast-forward sweep ${locationId2}: ${(0, import_node_util4.inspect)(e)}`);
44626
+ }
44627
+ }
44628
+ };
44629
+ void ffSweepAll();
44630
+ setInterval(() => {
44631
+ void ffSweepAll();
44632
+ }, 30 * 6e4).unref?.();
44042
44633
  const shopperJson = (res, status, body) => {
44043
44634
  res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
44044
44635
  res.end(JSON.stringify(body));
@@ -44080,6 +44671,47 @@ async function runDashboard(argv) {
44080
44671
  });
44081
44672
  return;
44082
44673
  }
44674
+ if (req.method === "GET" && url === "/api/shopper/users") {
44675
+ const locationId2 = u.searchParams.get("client") ?? "";
44676
+ if (!readableAccount(locationId2)) {
44677
+ shopperJson(res, 200, { ok: false, error: "That account is not on the board." });
44678
+ return;
44679
+ }
44680
+ const c = clientFor(locationId2);
44681
+ if (!c) {
44682
+ shopperJson(res, 200, { ok: false, error: "No key on file for this account." });
44683
+ return;
44684
+ }
44685
+ void accountUsers(c, locationId2).then((users) => shopperJson(res, 200, { ok: true, users: users.sort((a, b) => a.name.localeCompare(b.name)) })).catch((e) => {
44686
+ logQuietly(`secret shopper users: ${(0, import_node_util4.inspect)(e)}`);
44687
+ shopperJson(res, 200, { ok: false, error: "This account's users could not be read." });
44688
+ });
44689
+ return;
44690
+ }
44691
+ if (req.method === "GET" && url === "/api/shopper/webhooks") {
44692
+ const locationId2 = u.searchParams.get("client") ?? "";
44693
+ if (!readableAccount(locationId2)) {
44694
+ shopperJson(res, 200, { ok: false, error: "That account is not on the board." });
44695
+ return;
44696
+ }
44697
+ const c = clientFor(locationId2);
44698
+ if (!c) {
44699
+ shopperJson(res, 200, { ok: false, error: "No key on file for this account. Register it in GHL Command first." });
44700
+ return;
44701
+ }
44702
+ const ids = (u.searchParams.get("ids") ?? "").split(",").map((s) => s.trim()).filter((x) => /^[A-Za-z0-9_-]{1,64}$/.test(x)).slice(0, 5);
44703
+ void (async () => {
44704
+ try {
44705
+ const b = workflowReadConnection(registry2, c);
44706
+ const sources = await Promise.all(ids.map(async (id) => toSource(id, await b.getWorkflow(id))));
44707
+ shopperJson(res, 200, { ok: true, destinations: webhookDestinations(sources) });
44708
+ } catch (e) {
44709
+ logQuietly(`secret shopper webhooks: ${(0, import_node_util4.inspect)(e)}`);
44710
+ shopperJson(res, 200, { ok: false, error: "The chosen workflows could not be read, so their outside services are unknown. Check this account's workflow builder connection." });
44711
+ }
44712
+ })();
44713
+ return;
44714
+ }
44083
44715
  if (req.method === "POST" && (url === "/api/shopper/start" || url === "/api/shopper/finish")) {
44084
44716
  readBody(req, res, 2e4, async (body) => {
44085
44717
  let input;
@@ -44116,6 +44748,24 @@ async function runDashboard(argv) {
44116
44748
  const workflowIds = Array.isArray(input.workflowIds) ? input.workflowIds.filter((x) => typeof x === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(x)).slice(0, 20) : [];
44117
44749
  const ownerEmail = typeof input.ownerEmail === "string" ? input.ownerEmail : "";
44118
44750
  const testPhone = typeof input.testPhone === "string" && input.testPhone.trim() ? input.testPhone.trim() : void 0;
44751
+ if (input.fastForward === true) {
44752
+ const rawHosts = Array.isArray(input.allowedHosts) ? input.allowedHosts.filter((h) => typeof h === "string") : typeof input.allowedHosts === "string" ? input.allowedHosts.split(/[\s,]+/) : [];
44753
+ const allowedHosts = rawHosts.map(normalizeHost).filter(Boolean);
44754
+ try {
44755
+ const b = workflowReadConnection(registry2, c);
44756
+ const others = registry2.listLocations().filter((l) => l.locationId !== locationId2).map((l) => l.name);
44757
+ const r = await startFastForward({ client: c, admin: builderAdmin(b), reader: b, locationId: locationId2, ownerEmail, alertUserId: typeof input.alertUserId === "string" && input.alertUserId ? input.alertUserId : void 0, workflowIds, allowedHosts, testPhone, keepLead: input.keepLead === true, otherBusinessNames: others });
44758
+ if (!r.ok) {
44759
+ shopperJson(res, 200, { ok: false, error: r.error });
44760
+ return;
44761
+ }
44762
+ scheduleShopperFinish(r.run);
44763
+ shopperJson(res, 200, { ok: true, runId: r.run.runId });
44764
+ } catch (e) {
44765
+ shopperJson(res, 200, { ok: false, error: e instanceof Error ? e.message.split("\n")[0] : "The fast-forward check did not start." });
44766
+ }
44767
+ return;
44768
+ }
44119
44769
  let expect;
44120
44770
  let planNote;
44121
44771
  let workflowNames;
@@ -44429,8 +45079,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
44429
45079
  if (!bytes.length) throw new Error("that file is empty");
44430
45080
  if (bytes.length > DOC_MAX_BYTES) throw new Error("that file is over 20 MB");
44431
45081
  const dir = clientDocsDir(args.locationId);
44432
- fs36.mkdirSync(dir, { recursive: true, mode: 448 });
44433
- if (!insideDir(path34.join(appDataDir(), "clients"), dir)) throw new Error("This client's documents folder is not where it should be, so nothing was saved.");
45082
+ fs37.mkdirSync(dir, { recursive: true, mode: 448 });
45083
+ if (!insideDir(path35.join(appDataDir(), "clients"), dir)) throw new Error("This client's documents folder is not where it should be, so nothing was saved.");
44434
45084
  const saved = writeDocAtomically(dir, name, bytes);
44435
45085
  res.end(JSON.stringify({ ok: true, name: saved, files: listClientDocs(args.locationId) }));
44436
45086
  } catch (e) {
@@ -44786,10 +45436,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
44786
45436
  if (args.file) {
44787
45437
  const name = safeDocName(args.file);
44788
45438
  const dir = clientDocsDir(args.locationId);
44789
- const p = path34.join(dir, name);
44790
- if (!fs36.existsSync(p)) throw new Error("that document is not saved for this client");
44791
- if (fs36.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error("that document is not saved for this client");
44792
- const text2 = fs36.readFileSync(p, "utf8");
45439
+ const p = path35.join(dir, name);
45440
+ if (!fs37.existsSync(p)) throw new Error("that document is not saved for this client");
45441
+ if (fs37.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error("that document is not saved for this client");
45442
+ const text2 = fs37.readFileSync(p, "utf8");
44793
45443
  if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
44794
45444
  fileSource = { kind: "text", text: text2 };
44795
45445
  }
@@ -44863,14 +45513,14 @@ ${opportunityNoteFailure?.detail ?? ""}`);
44863
45513
  }
44864
45514
  if (req.method === "GET" && /^\/brand\/[a-z0-9-]+\/screenshot\.png$/.test(url)) {
44865
45515
  const slug3 = url.split("/")[2];
44866
- const file2 = path34.join(brandDir(slug3), "screenshot.png");
44867
- if (!fs36.existsSync(file2)) {
45516
+ const file2 = path35.join(brandDir(slug3), "screenshot.png");
45517
+ if (!fs37.existsSync(file2)) {
44868
45518
  res.writeHead(404);
44869
45519
  res.end();
44870
45520
  return;
44871
45521
  }
44872
45522
  res.writeHead(200, { "Content-Type": "image/png", "Cache-Control": "no-store" });
44873
- res.end(fs36.readFileSync(file2));
45523
+ res.end(fs37.readFileSync(file2));
44874
45524
  return;
44875
45525
  }
44876
45526
  if (req.method === "GET" && /^\/brand\/[a-z0-9-]+$/.test(url)) {
@@ -44879,7 +45529,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
44879
45529
  const dna = houseLooks().find((d) => d.slug === slug3) ?? readBrand(slug3);
44880
45530
  const dir = brandExists(slug3) ? brandDir(slug3) : void 0;
44881
45531
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
44882
- res.end(brandCardHtml(dna, { screenshot: dir ? fs36.existsSync(path34.join(dir, "screenshot.png")) : false, dir, seatToken: mayServeSeat(req, port) ? seatToken : "" }));
45532
+ res.end(brandCardHtml(dna, { screenshot: dir ? fs37.existsSync(path35.join(dir, "screenshot.png")) : false, dir, seatToken: mayServeSeat(req, port) ? seatToken : "" }));
44883
45533
  } catch (e) {
44884
45534
  res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
44885
45535
  res.end(e.message);
@@ -45041,7 +45691,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
45041
45691
  }
45042
45692
  try {
45043
45693
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
45044
- res.end(fs36.readFileSync(localCopyPath(locationId2), "utf8"));
45694
+ res.end(fs37.readFileSync(localCopyPath(locationId2), "utf8"));
45045
45695
  } catch {
45046
45696
  res.end("The handoff is not prepared yet. Run Verify first.");
45047
45697
  }
@@ -45647,7 +46297,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
45647
46297
  process.on("SIGTERM", stop2);
45648
46298
  });
45649
46299
  }
45650
- var import_zod62, import_node_util4, fs36, path34, 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;
46300
+ var import_zod62, import_node_util4, fs37, path35, 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;
45651
46301
  var init_dashboard = __esm({
45652
46302
  "src/dashboard.ts"() {
45653
46303
  "use strict";
@@ -45656,6 +46306,8 @@ var init_dashboard = __esm({
45656
46306
  init_run();
45657
46307
  init_plan2();
45658
46308
  init_steps();
46309
+ init_fastforward();
46310
+ init_ff_start();
45659
46311
  init_workflow_read_connection();
45660
46312
  init_brief_page();
45661
46313
  init_brief_dashboard();
@@ -45680,8 +46332,8 @@ var init_dashboard = __esm({
45680
46332
  init_handoff_snapshot();
45681
46333
  init_member_progress();
45682
46334
  init_build_obligations();
45683
- fs36 = __toESM(require("fs"));
45684
- path34 = __toESM(require("path"));
46335
+ fs37 = __toESM(require("fs"));
46336
+ path35 = __toESM(require("path"));
45685
46337
  http = __toESM(require("http"));
45686
46338
  import_child_process4 = require("child_process");
45687
46339
  init_credentials_store();
@@ -47375,12 +48027,12 @@ function registerWorkflowClonerTools(server2, builderClient) {
47375
48027
  const triggers = source.triggers || [];
47376
48028
  for (const action of actions) {
47377
48029
  if (action.id) {
47378
- idMap.set(action.id, crypto7.randomUUID());
48030
+ idMap.set(action.id, crypto8.randomUUID());
47379
48031
  }
47380
48032
  }
47381
48033
  for (const trigger of triggers) {
47382
48034
  if (trigger.id) {
47383
- idMap.set(trigger.id, crypto7.randomUUID());
48035
+ idMap.set(trigger.id, crypto8.randomUUID());
47384
48036
  }
47385
48037
  }
47386
48038
  const remap = (id) => {
@@ -47418,15 +48070,15 @@ function registerWorkflowClonerTools(server2, builderClient) {
47418
48070
  throw new Error("Failed to create new workflow \u2014 no ID returned");
47419
48071
  }
47420
48072
  const WORKFLOW_REF_KEYS = /* @__PURE__ */ new Set(["workflow_id", "workflowId", "targetWorkflowId"]);
47421
- const remapSelf = (value, key) => {
48073
+ const remapSelf2 = (value, key) => {
47422
48074
  if (typeof value === "string") {
47423
48075
  const isRef = key !== void 0 && WORKFLOW_REF_KEYS.has(key);
47424
48076
  return isRef && value === sourceWorkflowId ? newId : value;
47425
48077
  }
47426
- if (Array.isArray(value)) return value.map((v) => remapSelf(v, key));
48078
+ if (Array.isArray(value)) return value.map((v) => remapSelf2(v, key));
47427
48079
  if (value && typeof value === "object") {
47428
48080
  const out = {};
47429
- for (const [k, v] of Object.entries(value)) out[k] = remapSelf(v, k);
48081
+ for (const [k, v] of Object.entries(value)) out[k] = remapSelf2(v, k);
47430
48082
  return out;
47431
48083
  }
47432
48084
  return value;
@@ -47434,8 +48086,8 @@ function registerWorkflowClonerTools(server2, builderClient) {
47434
48086
  await client.updateWorkflow(newId, {
47435
48087
  name: newName,
47436
48088
  status: "draft",
47437
- actions: remapSelf(clonedActions),
47438
- triggers: remapSelf(clonedTriggers)
48089
+ actions: remapSelf2(clonedActions),
48090
+ triggers: remapSelf2(clonedTriggers)
47439
48091
  });
47440
48092
  let verified;
47441
48093
  try {
@@ -47493,12 +48145,12 @@ function registerWorkflowClonerTools(server2, builderClient) {
47493
48145
  }
47494
48146
  );
47495
48147
  }
47496
- var import_zod66, crypto7;
48148
+ var import_zod66, crypto8;
47497
48149
  var init_workflow_cloner = __esm({
47498
48150
  "src/tools/workflow-cloner.ts"() {
47499
48151
  "use strict";
47500
48152
  import_zod66 = require("zod");
47501
- crypto7 = __toESM(require("crypto"));
48153
+ crypto8 = __toESM(require("crypto"));
47502
48154
  }
47503
48155
  });
47504
48156
 
@@ -47544,13 +48196,13 @@ function contactServiceNote(payload) {
47544
48196
  function registerSmartListTools(server2, builderClient) {
47545
48197
  if (!builderClient) return;
47546
48198
  const client = builderClient;
47547
- async function smartListRequest(method, path43, body) {
47548
- const url = `${SMARTLIST_BASE}${path43}`;
48199
+ async function smartListRequest(method, path44, body) {
48200
+ const url = `${SMARTLIST_BASE}${path44}`;
47549
48201
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
47550
48202
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
47551
48203
  if (!response.ok) {
47552
48204
  const text3 = await response.text();
47553
- throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path43}
48205
+ throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path44}
47554
48206
  ${text3}`);
47555
48207
  }
47556
48208
  const text2 = await response.text();
@@ -47795,11 +48447,11 @@ var init_smart_lists = __esm({
47795
48447
  function registerReputationTools(server2, builderClient) {
47796
48448
  if (!builderClient) return;
47797
48449
  const client = builderClient;
47798
- async function reputationRequest(method, path43) {
47799
- const response = await builderRequest(client, method, `${REPUTATION_BASE}${path43}`);
48450
+ async function reputationRequest(method, path44) {
48451
+ const response = await builderRequest(client, method, `${REPUTATION_BASE}${path44}`);
47800
48452
  if (!response.ok) {
47801
48453
  const text3 = await response.text();
47802
- throw new Error(`Reputation API Error ${response.status}: ${method} ${path43}
48454
+ throw new Error(`Reputation API Error ${response.status}: ${method} ${path44}
47803
48455
  ${text3}`);
47804
48456
  }
47805
48457
  const text2 = await response.text();
@@ -47937,11 +48589,11 @@ var init_email_campaigns = __esm({
47937
48589
  function registerMembershipTools(server2, builderClient) {
47938
48590
  if (!builderClient) return;
47939
48591
  const client = builderClient;
47940
- async function membershipRequest(path43, method = "GET", body) {
47941
- const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path43}`, body ? { body } : {});
48592
+ async function membershipRequest(path44, method = "GET", body) {
48593
+ const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path44}`, body ? { body } : {});
47942
48594
  if (!response.ok) {
47943
48595
  const text3 = await response.text();
47944
- throw new Error(`Membership API Error ${response.status}: ${method} ${path43}
48596
+ throw new Error(`Membership API Error ${response.status}: ${method} ${path44}
47945
48597
  ${text3}`);
47946
48598
  }
47947
48599
  const text2 = await response.text();
@@ -48134,21 +48786,21 @@ function isAlreadyExistsError(message) {
48134
48786
  }
48135
48787
  function registerTemplateDeployerTools(server2, client) {
48136
48788
  const builderClient = WorkflowBuilderClient.fromEnv();
48137
- const templatesDir = path35.resolve(__dirname, "..", "templates");
48789
+ const templatesDir = path36.resolve(__dirname, "..", "templates");
48138
48790
  function validateTemplatePath(templateFile) {
48139
- const resolved2 = path35.resolve(templateFile);
48140
- const realPath = fs37.existsSync(resolved2) ? fs37.realpathSync(resolved2) : resolved2;
48791
+ const resolved2 = path36.resolve(templateFile);
48792
+ const realPath = fs38.existsSync(resolved2) ? fs38.realpathSync(resolved2) : resolved2;
48141
48793
  const allowedDirs = [
48142
48794
  templatesDir,
48143
- path35.resolve(process.cwd(), "templates"),
48144
- path35.resolve(__dirname, "..", "..", "templates")
48145
- ].map((d) => fs37.existsSync(d) ? fs37.realpathSync(d) : d);
48146
- const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir + path35.sep));
48795
+ path36.resolve(process.cwd(), "templates"),
48796
+ path36.resolve(__dirname, "..", "..", "templates")
48797
+ ].map((d) => fs38.existsSync(d) ? fs38.realpathSync(d) : d);
48798
+ const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir + path36.sep));
48147
48799
  if (!isAllowed) {
48148
48800
  throw new Error(`Template file must be inside a templates/ directory. Got: ${realPath}`);
48149
48801
  }
48150
- if (path35.relative(allowedDirs[0], realPath).startsWith("..")) {
48151
- const otherMatch = allowedDirs.slice(1).some((d) => !path35.relative(d, realPath).startsWith(".."));
48802
+ if (path36.relative(allowedDirs[0], realPath).startsWith("..")) {
48803
+ const otherMatch = allowedDirs.slice(1).some((d) => !path36.relative(d, realPath).startsWith(".."));
48152
48804
  if (!otherMatch) {
48153
48805
  throw new Error(`Template path escapes allowed directory.`);
48154
48806
  }
@@ -48163,26 +48815,26 @@ function registerTemplateDeployerTools(server2, client) {
48163
48815
  try {
48164
48816
  const dirs = [
48165
48817
  templatesDir,
48166
- path35.resolve(process.cwd(), "templates"),
48167
- path35.resolve(__dirname, "..", "..", "templates")
48818
+ path36.resolve(process.cwd(), "templates"),
48819
+ path36.resolve(__dirname, "..", "..", "templates")
48168
48820
  ];
48169
48821
  const templates = [];
48170
48822
  const seen = /* @__PURE__ */ new Set();
48171
48823
  for (const dir of dirs) {
48172
- if (!fs37.existsSync(dir)) continue;
48173
- const files = fs37.readdirSync(dir).filter((f) => f.endsWith(".json"));
48824
+ if (!fs38.existsSync(dir)) continue;
48825
+ const files = fs38.readdirSync(dir).filter((f) => f.endsWith(".json"));
48174
48826
  for (const file2 of files) {
48175
- const full = path35.join(dir, file2);
48827
+ const full = path36.join(dir, file2);
48176
48828
  let real;
48177
48829
  try {
48178
- real = fs37.realpathSync(full);
48830
+ real = fs38.realpathSync(full);
48179
48831
  } catch {
48180
48832
  continue;
48181
48833
  }
48182
48834
  if (seen.has(real)) continue;
48183
48835
  seen.add(real);
48184
48836
  try {
48185
- const content = JSON.parse(fs37.readFileSync(full, "utf-8"));
48837
+ const content = JSON.parse(fs38.readFileSync(full, "utf-8"));
48186
48838
  if (typeof content.templateName !== "string" || !content.templateName) continue;
48187
48839
  const parsed2 = TemplateSchema.safeParse(content);
48188
48840
  templates.push({
@@ -48219,7 +48871,7 @@ function registerTemplateDeployerTools(server2, client) {
48219
48871
  async ({ templateFile }) => {
48220
48872
  try {
48221
48873
  const safePath = validateTemplatePath(templateFile);
48222
- const content = JSON.parse(fs37.readFileSync(safePath, "utf-8"));
48874
+ const content = JSON.parse(fs38.readFileSync(safePath, "utf-8"));
48223
48875
  return {
48224
48876
  content: [
48225
48877
  {
@@ -48259,7 +48911,7 @@ function registerTemplateDeployerTools(server2, client) {
48259
48911
  try {
48260
48912
  const locId = client.resolveLocationId(locationId2);
48261
48913
  const safePath = validateTemplatePath(templateFile);
48262
- const template = TemplateSchema.parse(JSON.parse(fs37.readFileSync(safePath, "utf-8")));
48914
+ const template = TemplateSchema.parse(JSON.parse(fs38.readFileSync(safePath, "utf-8")));
48263
48915
  const resolve13 = (text2) => {
48264
48916
  if (typeof text2 !== "string") return text2;
48265
48917
  let result = text2;
@@ -48631,15 +49283,15 @@ ${errors.join("\n")}` : dryRun ? "\nNo errors!" : "\nNo errors \u2014 every item
48631
49283
  }
48632
49284
  );
48633
49285
  }
48634
- var import_zod71, fs37, path35, TemplateSchema;
49286
+ var import_zod71, fs38, path36, TemplateSchema;
48635
49287
  var init_template_deployer = __esm({
48636
49288
  "src/tools/template-deployer.ts"() {
48637
49289
  "use strict";
48638
49290
  import_zod71 = require("zod");
48639
49291
  init_workflow_builder_client();
48640
49292
  init_tool_helpers();
48641
- fs37 = __toESM(require("fs"));
48642
- path35 = __toESM(require("path"));
49293
+ fs38 = __toESM(require("fs"));
49294
+ path36 = __toESM(require("path"));
48643
49295
  init_workflow_builder_client();
48644
49296
  TemplateSchema = import_zod71.z.object({
48645
49297
  templateName: import_zod71.z.string(),
@@ -48960,10 +49612,10 @@ async function confirm(run) {
48960
49612
  async function runProbes(client, loc) {
48961
49613
  return Promise.all(
48962
49614
  PROBES.map((probe) => confirm(async () => {
48963
- const path43 = probe.path.replace("{loc}", loc);
49615
+ const path44 = probe.path.replace("{loc}", loc);
48964
49616
  const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
48965
49617
  try {
48966
- const raw = await client.get(path43, { params, noRetry: true });
49618
+ const raw = await client.get(path44, { params, noRetry: true });
48967
49619
  const count2 = countRows(raw, probe.listKeys);
48968
49620
  return {
48969
49621
  key: probe.key,
@@ -49239,14 +49891,14 @@ var init_checkup = __esm({
49239
49891
 
49240
49892
  // src/tools/user-guide.ts
49241
49893
  function resolveGuidePath() {
49242
- const bundled = path36.resolve(__dirname, "..", "guide", "guide.html");
49243
- if (fs38.existsSync(bundled)) return bundled;
49244
- const devRoot = path36.resolve(__dirname, "..", "..");
49894
+ const bundled = path37.resolve(__dirname, "..", "guide", "guide.html");
49895
+ if (fs39.existsSync(bundled)) return bundled;
49896
+ const devRoot = path37.resolve(__dirname, "..", "..");
49245
49897
  try {
49246
- const pkg2 = JSON.parse(fs38.readFileSync(path36.join(devRoot, "package.json"), "utf8"));
49898
+ const pkg2 = JSON.parse(fs39.readFileSync(path37.join(devRoot, "package.json"), "utf8"));
49247
49899
  if (pkg2.name === "@elitedcs/ghl-mcp") {
49248
- const dev = path36.join(devRoot, "guide", "guide.html");
49249
- if (fs38.existsSync(dev)) return dev;
49900
+ const dev = path37.join(devRoot, "guide", "guide.html");
49901
+ if (fs39.existsSync(dev)) return dev;
49250
49902
  }
49251
49903
  } catch {
49252
49904
  }
@@ -49255,7 +49907,7 @@ function resolveGuidePath() {
49255
49907
  function guideContents(file2) {
49256
49908
  let html2 = "";
49257
49909
  try {
49258
- html2 = fs38.readFileSync(file2, "utf8");
49910
+ html2 = fs39.readFileSync(file2, "utf8");
49259
49911
  } catch {
49260
49912
  return "The guide is installed but could not be read.";
49261
49913
  }
@@ -49279,7 +49931,7 @@ function registerUserGuideTools(server2) {
49279
49931
  { 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.") },
49280
49932
  async ({ open }) => {
49281
49933
  const guidePath = resolveGuidePath();
49282
- if (!fs38.existsSync(guidePath)) {
49934
+ if (!fs39.existsSync(guidePath)) {
49283
49935
  return {
49284
49936
  content: [{
49285
49937
  type: "text",
@@ -49312,14 +49964,14 @@ ${guidePath}`;
49312
49964
  }
49313
49965
  );
49314
49966
  }
49315
- var import_child_process6, import_zod73, fs38, path36;
49967
+ var import_child_process6, import_zod73, fs39, path37;
49316
49968
  var init_user_guide = __esm({
49317
49969
  "src/tools/user-guide.ts"() {
49318
49970
  "use strict";
49319
49971
  import_child_process6 = require("child_process");
49320
49972
  import_zod73 = require("zod");
49321
- fs38 = __toESM(require("fs"));
49322
- path36 = __toESM(require("path"));
49973
+ fs39 = __toESM(require("fs"));
49974
+ path37 = __toESM(require("path"));
49323
49975
  }
49324
49976
  });
49325
49977
 
@@ -51530,9 +52182,9 @@ async function settle(page) {
51530
52182
  }
51531
52183
  async function readTarget(target) {
51532
52184
  if (isUrl(target)) return { html: null, url: target };
51533
- const abs = path37.resolve(target);
51534
- if (!fs39.existsSync(abs)) throw new Error(`No file at ${abs}.`);
51535
- return { html: fs39.readFileSync(abs, "utf8"), url: (0, import_node_url.pathToFileURL)(abs).toString() };
52185
+ const abs = path38.resolve(target);
52186
+ if (!fs40.existsSync(abs)) throw new Error(`No file at ${abs}.`);
52187
+ return { html: fs40.readFileSync(abs, "utf8"), url: (0, import_node_url.pathToFileURL)(abs).toString() };
51536
52188
  }
51537
52189
  function slidePixels(index, png, file2, sec, brand) {
51538
52190
  let img;
@@ -51580,7 +52232,7 @@ async function verifyDesign(opts) {
51580
52232
  const { html: html2, url } = await readTarget(opts.target);
51581
52233
  const placement = opts.placement ?? "html";
51582
52234
  const medium = opts.medium;
51583
- fs39.mkdirSync(opts.outDir, { recursive: true });
52235
+ fs40.mkdirSync(opts.outDir, { recursive: true });
51584
52236
  const files = [];
51585
52237
  const screenshots = [];
51586
52238
  const at = (/* @__PURE__ */ new Date()).toISOString();
@@ -51625,8 +52277,8 @@ async function verifyDesign(opts) {
51625
52277
  try {
51626
52278
  await loc.scrollIntoViewIfNeeded({ timeout: 5e3 });
51627
52279
  const png = await loc.screenshot({ type: "png", timeout: 15e3 });
51628
- fs39.writeFileSync(path37.join(opts.outDir, file2), png);
51629
- files.push(path37.join(opts.outDir, file2));
52280
+ fs40.writeFileSync(path38.join(opts.outDir, file2), png);
52281
+ files.push(path38.join(opts.outDir, file2));
51630
52282
  screenshots.push(file2);
51631
52283
  pixels.push(slidePixels(sec.index, png, file2, sec, opts.brand));
51632
52284
  } catch {
@@ -51658,8 +52310,8 @@ async function verifyDesign(opts) {
51658
52310
  const shot = "page-1440.png";
51659
52311
  try {
51660
52312
  const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
51661
- fs39.writeFileSync(path37.join(opts.outDir, shot), png);
51662
- files.push(path37.join(opts.outDir, shot));
52313
+ fs40.writeFileSync(path38.join(opts.outDir, shot), png);
52314
+ files.push(path38.join(opts.outDir, shot));
51663
52315
  screenshots.push(shot);
51664
52316
  } catch {
51665
52317
  }
@@ -51667,8 +52319,8 @@ async function verifyDesign(opts) {
51667
52319
  await page.setViewportSize({ width: 390, height: 844 });
51668
52320
  await page.evaluate(SETTLE_SCROLL_JS).catch(() => void 0);
51669
52321
  const png = await page.screenshot({ type: "png", fullPage: true, timeout: 2e4 });
51670
- fs39.writeFileSync(path37.join(opts.outDir, "page-390.png"), png);
51671
- files.push(path37.join(opts.outDir, "page-390.png"));
52322
+ fs40.writeFileSync(path38.join(opts.outDir, "page-390.png"), png);
52323
+ files.push(path38.join(opts.outDir, "page-390.png"));
51672
52324
  screenshots.push("page-390.png");
51673
52325
  } catch {
51674
52326
  }
@@ -51741,21 +52393,21 @@ async function verifyDesign(opts) {
51741
52393
  screenshots
51742
52394
  };
51743
52395
  const checked = VerificationSchema.parse(report3);
51744
- const jsonPath = path37.join(opts.outDir, "verification.json");
51745
- fs39.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
52396
+ const jsonPath = path38.join(opts.outDir, "verification.json");
52397
+ fs40.writeFileSync(jsonPath, JSON.stringify(checked, null, 2) + "\n");
51746
52398
  files.push(jsonPath);
51747
52399
  const reportHtml = renderVerificationHtml(checked, opts.branding);
51748
- const htmlPath = path37.join(opts.outDir, "verification.html");
51749
- fs39.writeFileSync(htmlPath, reportHtml);
52400
+ const htmlPath = path38.join(opts.outDir, "verification.html");
52401
+ fs40.writeFileSync(htmlPath, reportHtml);
51750
52402
  files.push(htmlPath);
51751
52403
  return { report: checked, files, reportHtml };
51752
52404
  }
51753
- var fs39, path37, import_node_url, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS;
52405
+ var fs40, path38, import_node_url, WEB_WIDTHS, SETTLE_SCROLL_JS, FOCUS_JS;
51754
52406
  var init_verify = __esm({
51755
52407
  "src/command-os/design/verify.ts"() {
51756
52408
  "use strict";
51757
- fs39 = __toESM(require("node:fs"));
51758
- path37 = __toESM(require("node:path"));
52409
+ fs40 = __toESM(require("node:fs"));
52410
+ path38 = __toESM(require("node:path"));
51759
52411
  import_node_url = require("node:url");
51760
52412
  init_lint();
51761
52413
  init_probe();
@@ -52018,7 +52670,7 @@ async function defaultVerify(file2, medium, brand, outDir, mode, headless = true
52018
52670
  outDir,
52019
52671
  mode,
52020
52672
  headless,
52021
- profileDir: mkdtempSync((0, import_node_path8.join)(tmpdir2(), "ds-run-profile-")),
52673
+ profileDir: mkdtempSync((0, import_node_path9.join)(tmpdir2(), "ds-run-profile-")),
52022
52674
  ...conversion ? { pageMode: "conversion", conversion: conversion.brief, gate: conversion.gate } : {}
52023
52675
  });
52024
52676
  if (r.report.renderedWith !== "browser") {
@@ -52036,8 +52688,8 @@ async function runDesign(opts) {
52036
52688
  if (!checkedBrief.success) throw new Error(designValidationMessage(checkedBrief.error));
52037
52689
  const brief = checkedBrief.data;
52038
52690
  const medium = brief.medium;
52039
- const outDir = (0, import_node_path8.resolve)(opts.outDir);
52040
- (0, import_node_fs8.mkdirSync)(outDir, { recursive: true });
52691
+ const outDir = (0, import_node_path9.resolve)(opts.outDir);
52692
+ (0, import_node_fs9.mkdirSync)(outDir, { recursive: true });
52041
52693
  const pack = composeBrief(dna, brief, { tools: false });
52042
52694
  const maxPasses = Math.max(1, Math.min(3, opts.maxPasses ?? 3));
52043
52695
  const generate = opts.generate ?? claudeGenerate(opts.model ?? DEFAULT_MODEL);
@@ -52055,9 +52707,9 @@ ${FIRST}`;
52055
52707
  result.costUsd += g.costUsd;
52056
52708
  result.model = result.model ?? g.model ?? null;
52057
52709
  const html2 = extractHtml(g.text);
52058
- const file2 = (0, import_node_path8.join)(outDir, `pass-${pass}.html`);
52710
+ const file2 = (0, import_node_path9.join)(outDir, `pass-${pass}.html`);
52059
52711
  if (!html2) {
52060
- (0, import_node_fs8.writeFileSync)((0, import_node_path8.join)(outDir, `pass-${pass}.txt`), g.text);
52712
+ (0, import_node_fs9.writeFileSync)((0, import_node_path9.join)(outDir, `pass-${pass}.txt`), g.text);
52061
52713
  const p2 = { pass, lint: false, lintProblems: ["the reply carried no HTML file"], verdict: "NO-HTML", failed: [], costUsd: g.costUsd, ms: g.ms };
52062
52714
  result.passes.push(p2);
52063
52715
  opts.onPass?.(p2);
@@ -52067,11 +52719,11 @@ ${FIRST}`;
52067
52719
  Your previous reply carried no HTML file. ${FIRST}`;
52068
52720
  continue;
52069
52721
  }
52070
- (0, import_node_fs8.writeFileSync)(file2, html2);
52722
+ (0, import_node_fs9.writeFileSync)(file2, html2);
52071
52723
  const lint = lintDesignContract(html2, medium);
52072
52724
  let verdict = "NOT-GRADED", failed = [], report3;
52073
52725
  if (lint.ok) {
52074
- const v = await verify(file2, medium, dna, (0, import_node_path8.join)(outDir, `verify-${pass}`), mode);
52726
+ const v = await verify(file2, medium, dna, (0, import_node_path9.join)(outDir, `verify-${pass}`), mode);
52075
52727
  verdict = v.verdict;
52076
52728
  failed = v.failed;
52077
52729
  report3 = v.report;
@@ -52096,18 +52748,18 @@ Your previous reply carried no HTML file. ${FIRST}`;
52096
52748
  result.report = last?.report;
52097
52749
  result.stoppedBecause = `not green after ${result.passes.length} pass(es); last: ${last ? last.lint ? last.verdict + (last.failed.length ? " " + last.failed.map((f) => f.split(" ")[0]).join(",") : "") : "contract " + last.lintProblems[0] : "nothing ran"}`;
52098
52750
  }
52099
- if (opts.gradeCopyFor && result.file && (0, import_node_fs8.existsSync)(result.file)) {
52100
- const g = await gradeCopy((0, import_node_fs8.readFileSync)(result.file, "utf8"), generate, { icp: opts.gradeCopyFor, cwd: outDir });
52751
+ if (opts.gradeCopyFor && result.file && (0, import_node_fs9.existsSync)(result.file)) {
52752
+ const g = await gradeCopy((0, import_node_fs9.readFileSync)(result.file, "utf8"), generate, { icp: opts.gradeCopyFor, cwd: outDir });
52101
52753
  result.copy = g.grade ? { verdict: g.grade.verdict, scores: g.grade.scores, firstBreakdown: g.grade.firstBreakdown, fix: g.grade.fix, summary: g.grade.summary, costUsd: g.costUsd } : { error: g.error ?? "the copy could not be graded" };
52102
52754
  result.costUsd += g.costUsd;
52103
52755
  }
52104
52756
  const finalName = medium === "deck" ? "deck.html" : medium === "site" ? "site.html" : "report.html";
52105
- if (result.file && (0, import_node_fs8.existsSync)(result.file)) {
52757
+ if (result.file && (0, import_node_fs9.existsSync)(result.file)) {
52106
52758
  const { copyFileSync: copyFileSync6 } = await import("node:fs");
52107
- copyFileSync6(result.file, (0, import_node_path8.join)(outDir, finalName));
52108
- if (result.ok) result.file = (0, import_node_path8.join)(outDir, finalName);
52759
+ copyFileSync6(result.file, (0, import_node_path9.join)(outDir, finalName));
52760
+ if (result.ok) result.file = (0, import_node_path9.join)(outDir, finalName);
52109
52761
  }
52110
- (0, import_node_fs8.writeFileSync)((0, import_node_path8.join)(outDir, "run.json"), JSON.stringify({ ...result, brief, model: result.model, costBasis: "total_cost_usd as claude -p reports it (list price); on a Max plan it is usage, not a bill" }, null, 2));
52762
+ (0, import_node_fs9.writeFileSync)((0, import_node_path9.join)(outDir, "run.json"), JSON.stringify({ ...result, brief, model: result.model, costBasis: "total_cost_usd as claude -p reports it (list price); on a Max plan it is usage, not a bill" }, null, 2));
52111
52763
  return result;
52112
52764
  }
52113
52765
  function summariseRun(r) {
@@ -52115,14 +52767,14 @@ function summariseRun(r) {
52115
52767
  const copy = !r.copy ? "" : "error" in r.copy ? ` \xB7 copy NOT GRADED (${r.copy.error})` : ` \xB7 copy ${r.copy.verdict}${r.copy.firstBreakdown ? ` (first breakdown: ${r.copy.firstBreakdown})` : ""}`;
52116
52768
  return `${r.medium} for ${r.brandSlug}: design ${r.ok ? `PASS on pass ${r.greenAt}` : "NOT GREEN"}${copy} \xB7 ${passes} \xB7 $${r.costUsd.toFixed(3)} \xB7 ${Math.round(r.ms / 1e3)}s \xB7 ${r.model ?? "model unrecorded"}${r.file ? ` \xB7 ${r.file}` : ""}${r.report ? ` \xB7 ${r.report}` : ""}`;
52117
52769
  }
52118
- var import_node_child_process5, import_node_fs8, import_node_path8, DEFAULT_MODEL, FIRST;
52770
+ var import_node_child_process5, import_node_fs9, import_node_path9, DEFAULT_MODEL, FIRST;
52119
52771
  var init_run2 = __esm({
52120
52772
  "src/command-os/design/run.ts"() {
52121
52773
  "use strict";
52122
52774
  import_node_child_process5 = require("node:child_process");
52123
52775
  init_claude_locate();
52124
- import_node_fs8 = require("node:fs");
52125
- import_node_path8 = require("node:path");
52776
+ import_node_fs9 = require("node:fs");
52777
+ import_node_path9 = require("node:path");
52126
52778
  init_brief();
52127
52779
  init_copy_grade();
52128
52780
  init_lint();
@@ -52174,7 +52826,7 @@ function registerDesignStudioTools(server2) {
52174
52826
  const { dna, logoSvg, logoPng } = deriveBrandDna(raw, { name, slug: slug3, rights: { declaration: a.rights, at: (/* @__PURE__ */ new Date()).toISOString() }, browser, firecrawl: fc });
52175
52827
  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.");
52176
52828
  const written = writeBrand(dna, { logoSvg, logoPng, screenshotPng: browser?.screenshotPng });
52177
- 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) => path38.basename(f)), notes };
52829
+ 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) => path39.basename(f)), notes };
52178
52830
  }
52179
52831
  );
52180
52832
  safeTool(
@@ -52341,9 +52993,9 @@ function registerDesignStudioTools(server2) {
52341
52993
  "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.",
52342
52994
  { file: import_zod76.z.string().describe("Path to the .html file."), medium: import_zod76.z.enum(MEDIA) },
52343
52995
  async ({ file: file2, medium }) => {
52344
- const abs = path38.resolve(file2);
52345
- if (!fs40.existsSync(abs)) throw new Error(`No file at ${abs}.`);
52346
- const r = lintDesignContract(fs40.readFileSync(abs, "utf8"), medium);
52996
+ const abs = path39.resolve(file2);
52997
+ if (!fs41.existsSync(abs)) throw new Error(`No file at ${abs}.`);
52998
+ const r = lintDesignContract(fs41.readFileSync(abs, "utf8"), medium);
52347
52999
  return { ok: r.ok, problems: r.problems, warnings: r.warnings, facts: r.facts };
52348
53000
  }
52349
53001
  );
@@ -52364,7 +53016,7 @@ function registerDesignStudioTools(server2) {
52364
53016
  async (a) => {
52365
53017
  const brand = a.brandSlug ? houseLooks().find((d) => d.slug === a.brandSlug) ?? readBrand(slugify(a.brandSlug)) : void 0;
52366
53018
  const isUrl2 = /^https?:\/\//i.test(a.target);
52367
- const outDir = a.outDir ?? (isUrl2 ? path38.resolve(`verify-${new URL(a.target).hostname.replace(/[^a-z0-9.-]/gi, "_")}`) : path38.join(path38.dirname(path38.resolve(a.target)), `verify-${path38.basename(a.target, ".html")}`));
53019
+ const outDir = a.outDir ?? (isUrl2 ? path39.resolve(`verify-${new URL(a.target).hostname.replace(/[^a-z0-9.-]/gi, "_")}`) : path39.join(path39.dirname(path39.resolve(a.target)), `verify-${path39.basename(a.target, ".html")}`));
52368
53020
  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() });
52369
53021
  const v = r.report;
52370
53022
  const failed = v.rules.filter((x) => x.state === "MEASURED-FAIL").map((x) => `${x.ruleId} ${x.name}: ${x.measured ?? ""}`);
@@ -52379,7 +53031,7 @@ function registerDesignStudioTools(server2) {
52379
53031
  judgments: v.rules.filter((x) => x.state === "ASSERTED").map((x) => `${x.ruleId} ${x.name}: ${x.detail ?? ""}`),
52380
53032
  notApplicable: v.rules.filter((x) => x.state === "N-A").map((x) => `${x.ruleId}: ${x.detail ?? ""}`),
52381
53033
  brandMatch: v.brandMatch,
52382
- report: path38.join(outDir, "verification.html"),
53034
+ report: path39.join(outDir, "verification.html"),
52383
53035
  files: r.files,
52384
53036
  ...a.placement === "ghl" && { builderOwned: builderOwnedRules().map((b) => `${b.rule.id} ${b.rule.name}: ${b.why}`) }
52385
53037
  };
@@ -52407,9 +53059,9 @@ function registerDesignStudioTools(server2) {
52407
53059
  if (!a.pageDataDownloadUrl && !a.file) throw new Error("Pass pageDataDownloadUrl (from get_page_full) or file.");
52408
53060
  let page;
52409
53061
  if (a.file) {
52410
- const abs = path38.resolve(a.file);
52411
- if (!fs40.existsSync(abs)) throw new Error(`No file at ${abs}.`);
52412
- page = JSON.parse(fs40.readFileSync(abs, "utf8"));
53062
+ const abs = path39.resolve(a.file);
53063
+ if (!fs41.existsSync(abs)) throw new Error(`No file at ${abs}.`);
53064
+ page = JSON.parse(fs41.readFileSync(abs, "utf8"));
52413
53065
  } else {
52414
53066
  const response = await fetch(a.pageDataDownloadUrl, { headers: { Accept: "application/json,*/*" } });
52415
53067
  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.`);
@@ -52450,9 +53102,9 @@ function registerDesignStudioTools(server2) {
52450
53102
  if (!a.file && !a.text) throw new Error("Pass file or text.");
52451
53103
  let html2 = a.text ?? "";
52452
53104
  if (a.file) {
52453
- const abs = path38.resolve(a.file);
52454
- if (!fs40.existsSync(abs)) throw new Error(`No file at ${abs}.`);
52455
- html2 = fs40.readFileSync(abs, "utf8");
53105
+ const abs = path39.resolve(a.file);
53106
+ if (!fs41.existsSync(abs)) throw new Error(`No file at ${abs}.`);
53107
+ html2 = fs41.readFileSync(abs, "utf8");
52456
53108
  }
52457
53109
  const r = await gradeCopy(html2, claudeGenerate(a.model), { icp: a.icp });
52458
53110
  if (!r.grade) {
@@ -52487,13 +53139,13 @@ function registerDesignStudioTools(server2) {
52487
53139
  }
52488
53140
  );
52489
53141
  }
52490
- var import_zod76, fs40, path38, summarise2;
53142
+ var import_zod76, fs41, path39, summarise2;
52491
53143
  var init_design_studio = __esm({
52492
53144
  "src/tools/design-studio.ts"() {
52493
53145
  "use strict";
52494
53146
  import_zod76 = require("zod");
52495
- fs40 = __toESM(require("node:fs"));
52496
- path38 = __toESM(require("node:path"));
53147
+ fs41 = __toESM(require("node:fs"));
53148
+ path39 = __toESM(require("node:path"));
52497
53149
  init_tool_helpers();
52498
53150
  init_extract();
52499
53151
  init_extract_browser();
@@ -53217,11 +53869,11 @@ var init_audit = __esm({
53217
53869
  // src/command-os/delivery-audit/store.ts
53218
53870
  function deliveryAuditPath() {
53219
53871
  const override = process.env[DELIVERY_AUDIT_ENV];
53220
- return override && override.trim() ? override : path39.join(appDataDir(), "delivery-audit.json");
53872
+ return override && override.trim() ? override : path40.join(appDataDir(), "delivery-audit.json");
53221
53873
  }
53222
53874
  function readDeliveryAudit() {
53223
53875
  try {
53224
- const parsed2 = JSON.parse(fs41.readFileSync(deliveryAuditPath(), "utf-8"));
53876
+ const parsed2 = JSON.parse(fs42.readFileSync(deliveryAuditPath(), "utf-8"));
53225
53877
  if (!parsed2 || typeof parsed2 !== "object" || !parsed2.answers || typeof parsed2.answers !== "object") return { ...EMPTY3 };
53226
53878
  return { v: 1, answers: parsed2.answers, updated_at: parsed2.updated_at };
53227
53879
  } catch {
@@ -53232,7 +53884,7 @@ function readForWrite2() {
53232
53884
  const target = deliveryAuditPath();
53233
53885
  let raw;
53234
53886
  try {
53235
- raw = fs41.readFileSync(target, "utf-8");
53887
+ raw = fs42.readFileSync(target, "utf-8");
53236
53888
  } catch (e) {
53237
53889
  if (e?.code === "ENOENT") return { ...EMPTY3 };
53238
53890
  throw new Error("Your saved audit could not be read, so nothing was changed. Nothing was lost.");
@@ -53244,7 +53896,7 @@ function readForWrite2() {
53244
53896
  } catch {
53245
53897
  const kept = `${target}.unreadable-${Date.now()}`;
53246
53898
  try {
53247
- fs41.copyFileSync(target, kept);
53899
+ fs42.copyFileSync(target, kept);
53248
53900
  } catch {
53249
53901
  }
53250
53902
  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.`);
@@ -53256,13 +53908,13 @@ function writeFile2(f) {
53256
53908
  const tmp = `${target}.${process.pid}.tmp`;
53257
53909
  let renamed = false;
53258
53910
  try {
53259
- fs41.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
53260
- fs41.renameSync(tmp, target);
53911
+ fs42.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
53912
+ fs42.renameSync(tmp, target);
53261
53913
  renamed = true;
53262
53914
  } finally {
53263
53915
  if (!renamed) {
53264
53916
  try {
53265
- fs41.unlinkSync(tmp);
53917
+ fs42.unlinkSync(tmp);
53266
53918
  } catch {
53267
53919
  }
53268
53920
  }
@@ -53283,12 +53935,12 @@ function resetDeliveryAudit(now = () => /* @__PURE__ */ new Date()) {
53283
53935
  writeFile2(next);
53284
53936
  return next;
53285
53937
  }
53286
- var fs41, path39, EMPTY3, DELIVERY_AUDIT_ENV;
53938
+ var fs42, path40, EMPTY3, DELIVERY_AUDIT_ENV;
53287
53939
  var init_store4 = __esm({
53288
53940
  "src/command-os/delivery-audit/store.ts"() {
53289
53941
  "use strict";
53290
- fs41 = __toESM(require("node:fs"));
53291
- path39 = __toESM(require("node:path"));
53942
+ fs42 = __toESM(require("node:fs"));
53943
+ path40 = __toESM(require("node:path"));
53292
53944
  init_credentials_store();
53293
53945
  EMPTY3 = { v: 1, answers: {} };
53294
53946
  DELIVERY_AUDIT_ENV = "GHL_MCP_DELIVERY_AUDIT";
@@ -53513,7 +54165,7 @@ function planFingerprint(goal, steps) {
53513
54165
  goal,
53514
54166
  steps: steps.map((s) => ({ id: s.id, tool: s.tool, targets: s.targets, creates: s.creates ?? [], args: s.args ?? {} }))
53515
54167
  });
53516
- return (0, import_node_crypto13.createHash)("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 16);
54168
+ return (0, import_node_crypto14.createHash)("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 16);
53517
54169
  }
53518
54170
  function checkPlan(goal, findings, steps, now = /* @__PURE__ */ new Date(), knownTools) {
53519
54171
  const refusals = [];
@@ -53730,11 +54382,11 @@ function abandon(a, why) {
53730
54382
  const stillApplied = position(a).undo;
53731
54383
  return { ok: true, automation: { ...a, phase: "abandoned", goal: a.goal, abandonedBecause: why }, stillApplied };
53732
54384
  }
53733
- var import_node_crypto13, isExisting, FINDINGS_STALE_AFTER_MINUTES;
54385
+ var import_node_crypto14, isExisting, FINDINGS_STALE_AFTER_MINUTES;
53734
54386
  var init_automation = __esm({
53735
54387
  "src/command-os/build/automation.ts"() {
53736
54388
  "use strict";
53737
- import_node_crypto13 = require("node:crypto");
54389
+ import_node_crypto14 = require("node:crypto");
53738
54390
  init_verify_findings();
53739
54391
  isExisting = (t) => "existing" in t;
53740
54392
  FINDINGS_STALE_AFTER_MINUTES = 60;
@@ -53744,7 +54396,7 @@ var init_automation = __esm({
53744
54396
  // src/command-os/build/store.ts
53745
54397
  function automationsPath() {
53746
54398
  const override = process.env[AUTOMATIONS_ENV];
53747
- return override && override.trim() ? override : path40.join(appDataDir(), "automations.json");
54399
+ return override && override.trim() ? override : path41.join(appDataDir(), "automations.json");
53748
54400
  }
53749
54401
  function parse4(raw) {
53750
54402
  const p = JSON.parse(raw);
@@ -53753,7 +54405,7 @@ function parse4(raw) {
53753
54405
  }
53754
54406
  function readAutomations() {
53755
54407
  try {
53756
- return parse4(fs42.readFileSync(automationsPath(), "utf-8")).builds;
54408
+ return parse4(fs43.readFileSync(automationsPath(), "utf-8")).builds;
53757
54409
  } catch {
53758
54410
  return { ...EMPTY4.builds };
53759
54411
  }
@@ -53762,7 +54414,7 @@ function readForWrite3() {
53762
54414
  const target = automationsPath();
53763
54415
  let raw;
53764
54416
  try {
53765
- raw = fs42.readFileSync(target, "utf-8");
54417
+ raw = fs43.readFileSync(target, "utf-8");
53766
54418
  } catch (e) {
53767
54419
  if (e?.code === "ENOENT") return { v: 1, builds: {} };
53768
54420
  throw new Error("Your builds file could not be read, so nothing was changed. Nothing was lost.");
@@ -53772,7 +54424,7 @@ function readForWrite3() {
53772
54424
  } catch {
53773
54425
  const kept = `${target}.unreadable-${Date.now()}`;
53774
54426
  try {
53775
- fs42.copyFileSync(target, kept);
54427
+ fs43.copyFileSync(target, kept);
53776
54428
  } catch {
53777
54429
  }
53778
54430
  throw new Error(
@@ -53786,13 +54438,13 @@ function writeFile3(f) {
53786
54438
  const tmp = `${target}.${process.pid}.tmp`;
53787
54439
  let renamed = false;
53788
54440
  try {
53789
- fs42.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
53790
- fs42.renameSync(tmp, target);
54441
+ fs43.writeFileSync(tmp, JSON.stringify(f, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
54442
+ fs43.renameSync(tmp, target);
53791
54443
  renamed = true;
53792
54444
  } finally {
53793
54445
  if (!renamed) {
53794
54446
  try {
53795
- fs42.unlinkSync(tmp);
54447
+ fs43.unlinkSync(tmp);
53796
54448
  } catch {
53797
54449
  }
53798
54450
  }
@@ -53818,12 +54470,12 @@ function newAutomationId(now = () => /* @__PURE__ */ new Date()) {
53818
54470
  }
53819
54471
  return id;
53820
54472
  }
53821
- var fs42, path40, AUTOMATIONS_ENV, EMPTY4;
54473
+ var fs43, path41, AUTOMATIONS_ENV, EMPTY4;
53822
54474
  var init_store5 = __esm({
53823
54475
  "src/command-os/build/store.ts"() {
53824
54476
  "use strict";
53825
- fs42 = __toESM(require("node:fs"));
53826
- path40 = __toESM(require("node:path"));
54477
+ fs43 = __toESM(require("node:fs"));
54478
+ path41 = __toESM(require("node:path"));
53827
54479
  init_credentials_store();
53828
54480
  AUTOMATIONS_ENV = "GHL_MCP_AUTOMATIONS";
53829
54481
  EMPTY4 = { v: 1, builds: {} };
@@ -53941,6 +54593,8 @@ var init_arg_ids = __esm({
53941
54593
  userId: "user",
53942
54594
  userIds: "user",
53943
54595
  assignedUserId: "user",
54596
+ alertUserId: "user",
54597
+ // run_secret_shopper fast-forward: who stands in for the staff
53944
54598
  sentBy: "user",
53945
54599
  // send_document's sender (release review F2)
53946
54600
  // Workflow action payloads (CLAUDE.md action schemas). These do not end in
@@ -55242,6 +55896,9 @@ function registerSecretShopperTools(server2, client, registry2) {
55242
55896
  watchMinutes: import_zod83.z.number().optional().describe("start: how long to watch, 1 to 1440 minutes. Leave it out to watch until the last planned message is due plus 2 minutes (10 when no plan is known)."),
55243
55897
  expect: import_zod83.z.array(expectation).optional().describe("start: messages the build plan says the lead should get, so a missing one is reported."),
55244
55898
  keepLead: import_zod83.z.boolean().optional().describe("Keep the test lead in the account after finishing, so the owner can look at it. Default false."),
55899
+ fastForward: import_zod83.z.boolean().optional().describe("start with workflowIds: test long workflows in minutes. Makes a temporary 'ZZ TEST FF' copy of each (every step kept, every time wait cut to 1 minute, no trigger), runs the test lead through the copies, then deletes them; the originals are only read. Staff alerts, tasks and lead assignments go to one chosen user (alertUserId) marked TEST. Straight-line workflows with time waits only for now (if/else, hand-offs to other workflows and other waits are refused with the reason). Tell the owner before starting."),
55900
+ alertUserId: import_zod83.z.string().optional().describe("fastForward: the GoHighLevel user (get_users) who receives the staff alerts, tasks and lead assignments instead of the staff. Defaults to the user whose email matches ownerEmail; if none matches, ask the owner who should get them."),
55901
+ allowWebhookHosts: import_zod83.z.array(import_zod83.z.string()).optional().describe("fastForward: website hosts (e.g. hooks.zapier.com) the owner allows the test lead to be sent to by webhook steps. Any other webhook host makes the check refuse and name it."),
55245
55902
  runId: import_zod83.z.string().optional().describe("finish/status: which run. Defaults to the latest run on the active account.")
55246
55903
  },
55247
55904
  async (args) => {
@@ -55258,7 +55915,14 @@ function registerSecretShopperTools(server2, client, registry2) {
55258
55915
  } catch {
55259
55916
  }
55260
55917
  }
55261
- const finished = await finishShopper(locationId2, run2.runId, { client, stepLog });
55918
+ const ffAdmin = run2.fastForward && registry2 ? (() => {
55919
+ try {
55920
+ return builderAdmin(workflowReadConnection(registry2, client));
55921
+ } catch {
55922
+ return void 0;
55923
+ }
55924
+ })() : void 0;
55925
+ const finished = await finishShopper(locationId2, run2.runId, { client, stepLog, ffAdmin });
55262
55926
  return { ok: true, ...report2(finished), ...finished.busy ? { note: "Another finish of this check is running (the cockpit, or an earlier request). Ask again in a minute for its result." } : {} };
55263
55927
  }
55264
55928
  const entries = [args.workflowIds?.length ? "w" : "", args.tag ? "t" : "", args.captureUrl ? "u" : ""].filter(Boolean);
@@ -55266,6 +55930,20 @@ function registerSecretShopperTools(server2, client, registry2) {
55266
55930
  const entry = args.workflowIds?.length ? { kind: "workflows", workflowIds: args.workflowIds } : args.tag ? { kind: "tag", tag: args.tag } : { kind: "url", url: args.captureUrl ?? "" };
55267
55931
  const ownerEmail = args.ownerEmail ?? readAgencyProfile().sender?.replyTo;
55268
55932
  if (!ownerEmail) return { ok: false, error: "Give ownerEmail (your own address), or set a reply-to address on Your agency in the cockpit. The test lead uses it so every message comes to you, never to a customer." };
55933
+ if (args.fastForward) {
55934
+ if (entry.kind !== "workflows") return { ok: false, error: "Fast-forward works on named workflows: give workflowIds." };
55935
+ if (!registry2) return { ok: false, error: "Fast-forward needs the workflow builder connection, which this install does not have." };
55936
+ let b;
55937
+ try {
55938
+ b = workflowReadConnection(registry2, client);
55939
+ } catch (e) {
55940
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
55941
+ }
55942
+ const others2 = registry2.listLocations().filter((l) => l.locationId !== locationId2).map((l) => l.name);
55943
+ const r = await startFastForward({ client, admin: builderAdmin(b), reader: b, locationId: locationId2, ownerEmail, alertUserId: args.alertUserId, workflowIds: entry.workflowIds, allowedHosts: args.allowWebhookHosts ?? [], testPhone: args.testPhone, keepLead: args.keepLead, otherBusinessNames: others2 });
55944
+ if (!r.ok) return { ok: false, error: r.error };
55945
+ return { ok: true, ...report2(r.run), fastForward: r.run.fastForward?.copies.map((c) => ({ workflow: c.sourceName, waitsInRealLife: c.waits })), next: `Say "finish the secret shopper" after ${new Date(r.run.closesAt).toLocaleTimeString()} to read what was sent. The temporary copies are removed when it finishes.` };
55946
+ }
55269
55947
  let expect = args.expect;
55270
55948
  let planNote = { source: expect?.length ? "given" : "none" };
55271
55949
  let workflowNames;
@@ -55300,6 +55978,8 @@ var init_secret_shopper = __esm({
55300
55978
  init_plan2();
55301
55979
  init_workflow_read_connection();
55302
55980
  init_steps();
55981
+ init_fastforward();
55982
+ init_ff_start();
55303
55983
  expectation = import_zod83.z.object({
55304
55984
  label: import_zod83.z.string().describe('What the owner calls the message, e.g. "Welcome email".'),
55305
55985
  match: import_zod83.z.string().describe("Words from its subject or text that identify it."),
@@ -57325,13 +58005,13 @@ ${text3}`);
57325
58005
  };
57326
58006
  }
57327
58007
  function makePipelineApi(builderClient) {
57328
- return async (method, path43, body) => {
57329
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path43}`;
58008
+ return async (method, path44, body) => {
58009
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path44}`;
57330
58010
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
57331
58011
  const response = await builderRequest(builderClient, method, url, sendsBody ? { body } : {});
57332
58012
  if (!response.ok) {
57333
58013
  const text3 = await response.text();
57334
- throw new Error(`Pipeline API ${response.status}: ${method} ${path43}
58014
+ throw new Error(`Pipeline API ${response.status}: ${method} ${path44}
57335
58015
  ${text3}`);
57336
58016
  }
57337
58017
  const text2 = await response.text();
@@ -57344,8 +58024,8 @@ ${text3}`);
57344
58024
  };
57345
58025
  }
57346
58026
  function makeFunnelApi(builderClient) {
57347
- return async (method, path43, body) => {
57348
- const url = `https://backend.leadconnectorhq.com/funnels${path43}`;
58027
+ return async (method, path44, body) => {
58028
+ const url = `https://backend.leadconnectorhq.com/funnels${path44}`;
57349
58029
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
57350
58030
  const response = await builderRequest(builderClient, method, url, {
57351
58031
  ...sendsBody ? { body } : {},
@@ -57358,7 +58038,7 @@ function makeFunnelApi(builderClient) {
57358
58038
  });
57359
58039
  if (!response.ok) {
57360
58040
  const text3 = await response.text();
57361
- throw new Error(`Funnel API ${response.status}: ${method} ${path43}
58041
+ throw new Error(`Funnel API ${response.status}: ${method} ${path44}
57362
58042
  ${text3.slice(0, 300)}`);
57363
58043
  }
57364
58044
  const text2 = await response.text();
@@ -58673,7 +59353,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env2 = process
58673
59353
  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.` }));
58674
59354
  }
58675
59355
  const reader = token2 ? new GHLClient({ apiKey: token2.apiKey, locationId: loc }) : client;
58676
- return refuseUnseenFindings2((path43, params) => reader.get(path43, params ? { params } : void 0), loc, findings);
59356
+ return refuseUnseenFindings2((path44, params) => reader.get(path44, params ? { params } : void 0), loc, findings);
58677
59357
  }, () => client.defaultLocationId);
58678
59358
  registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2);
58679
59359
  registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
@@ -58988,8 +59668,8 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
58988
59668
  io.out(USAGE2);
58989
59669
  return argv.length === 0 ? 2 : 0;
58990
59670
  }
58991
- const str6 = (k) => typeof a[k] === "string" ? a[k] : void 0;
58992
- const brand = str6("brand"), medium = str6("medium"), headline3 = str6("headline"), points = str6("points");
59671
+ const str8 = (k) => typeof a[k] === "string" ? a[k] : void 0;
59672
+ const brand = str8("brand"), medium = str8("medium"), headline3 = str8("headline"), points = str8("points");
58993
59673
  if (!brand || !medium || !headline3 || !points) {
58994
59674
  io.err("design-run: --brand, --medium, --headline and --points are required\n" + USAGE2);
58995
59675
  return 2;
@@ -59003,7 +59683,7 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
59003
59683
  return 2;
59004
59684
  }
59005
59685
  let conversion;
59006
- const convFile = str6("conversion");
59686
+ const convFile = str8("conversion");
59007
59687
  if (convFile) {
59008
59688
  try {
59009
59689
  conversion = JSON.parse(require("node:fs").readFileSync(convFile, "utf8"));
@@ -59016,7 +59696,7 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
59016
59696
  return 2;
59017
59697
  }
59018
59698
  }
59019
- const gate2 = str6("gate");
59699
+ const gate2 = str8("gate");
59020
59700
  if (gate2 && gate2 !== "draft" && gate2 !== "ship") {
59021
59701
  io.err("design-run: --gate must be draft or ship");
59022
59702
  return 2;
@@ -59026,8 +59706,8 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
59026
59706
  io.err("design-run: --passes must be 1, 2 or 3");
59027
59707
  return 2;
59028
59708
  }
59029
- const outDir = str6("out") ?? (0, import_node_path9.join)((0, import_node_os2.tmpdir)(), `design-run-${brand}-${medium}-${Date.now()}`);
59030
- const model = str6("model") ?? DEFAULT_MODEL;
59709
+ const outDir = str8("out") ?? (0, import_node_path10.join)((0, import_node_os2.tmpdir)(), `design-run-${brand}-${medium}-${Date.now()}`);
59710
+ const model = str8("model") ?? DEFAULT_MODEL;
59031
59711
  io.out(`design-run: ${medium} for ${brand} on your Claude (${model}), up to ${passes} pass(es) \u2192 ${outDir}`);
59032
59712
  let r;
59033
59713
  try {
@@ -59035,11 +59715,11 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
59035
59715
  brand,
59036
59716
  brief: {
59037
59717
  medium,
59038
- content: { headline: headline3, points: points.split("|").map((s) => s.trim()).filter(Boolean), audience: str6("audience"), job: str6("job"), ctaLabel: str6("cta"), proof: str6("proof") },
59718
+ content: { headline: headline3, points: points.split("|").map((s) => s.trim()).filter(Boolean), audience: str8("audience"), job: str8("job"), ctaLabel: str8("cta"), proof: str8("proof") },
59039
59719
  options: {
59040
- ...str6("mode") && { mode: str6("mode") },
59720
+ ...str8("mode") && { mode: str8("mode") },
59041
59721
  ...a.slides && { slides: Number(a.slides) },
59042
- theme: str6("theme") ?? "both",
59722
+ theme: str8("theme") ?? "both",
59043
59723
  ...conversion ? { pageMode: "conversion" } : {}
59044
59724
  },
59045
59725
  ...conversion ? { conversion } : {}
@@ -59051,7 +59731,7 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
59051
59731
  headless: !a.headed,
59052
59732
  onPass: (p) => io.out(` pass ${p.pass}: ${p.lint ? p.verdict === "PASS" ? "PASS" : `${p.verdict}${p.failed.length ? " \u2014 " + p.failed.join(" | ") : ""}` : `contract: ${p.lintProblems.join("; ")}`} ($${p.costUsd.toFixed(3)}, ${Math.round(p.ms / 1e3)}s)`),
59053
59733
  // Two verdicts, never one: craft is measured, copy is judged.
59054
- ...str6("grade-copy") ? { gradeCopyFor: str6("grade-copy") } : {}
59734
+ ...str8("grade-copy") ? { gradeCopyFor: str8("grade-copy") } : {}
59055
59735
  });
59056
59736
  } catch (error) {
59057
59737
  io.err(error instanceof import_zod89.z.ZodError ? designValidationMessage(error) : error instanceof Error ? error.message : "The design run could not finish. Check the design details before trying again.");
@@ -59066,14 +59746,14 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
59066
59746
  }
59067
59747
  return r.ok ? 0 : 1;
59068
59748
  }
59069
- var import_zod89, import_node_child_process6, import_node_path9, import_node_os2, USAGE2;
59749
+ var import_zod89, import_node_child_process6, import_node_path10, import_node_os2, USAGE2;
59070
59750
  var init_cli2 = __esm({
59071
59751
  "src/command-os/design/cli.ts"() {
59072
59752
  "use strict";
59073
59753
  import_zod89 = require("zod");
59074
59754
  init_brief_validation();
59075
59755
  import_node_child_process6 = require("node:child_process");
59076
- import_node_path9 = require("node:path");
59756
+ import_node_path10 = require("node:path");
59077
59757
  import_node_os2 = require("node:os");
59078
59758
  init_run2();
59079
59759
  init_looks();
@@ -59099,8 +59779,8 @@ __export(index_exports, {
59099
59779
  });
59100
59780
  module.exports = __toCommonJS(index_exports);
59101
59781
  var dotenv2 = __toESM(require("dotenv"));
59102
- var path42 = __toESM(require("path"));
59103
- var fs44 = __toESM(require("fs"));
59782
+ var path43 = __toESM(require("path"));
59783
+ var fs45 = __toESM(require("fs"));
59104
59784
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
59105
59785
 
59106
59786
  // src/version-compare.ts
@@ -59171,12 +59851,12 @@ init_setup_tool();
59171
59851
  var import_zod88 = require("zod");
59172
59852
 
59173
59853
  // src/skill-installer.ts
59174
- var import_node_crypto14 = require("node:crypto");
59175
- var fs43 = __toESM(require("node:fs"));
59176
- var path41 = __toESM(require("node:path"));
59854
+ var import_node_crypto15 = require("node:crypto");
59855
+ var fs44 = __toESM(require("node:fs"));
59856
+ var path42 = __toESM(require("node:path"));
59177
59857
  var os7 = __toESM(require("node:os"));
59178
59858
  function sha2562(buf) {
59179
- return (0, import_node_crypto14.createHash)("sha256").update(buf).digest("hex");
59859
+ return (0, import_node_crypto15.createHash)("sha256").update(buf).digest("hex");
59180
59860
  }
59181
59861
  function isOlderRelease(candidate, installed) {
59182
59862
  const parts = (v) => v.trim().split(".").map((n) => Number.parseInt(n, 10));
@@ -59188,9 +59868,9 @@ function isOlderRelease(candidate, installed) {
59188
59868
  return false;
59189
59869
  }
59190
59870
  function bundledSkillsDir(baseDir) {
59191
- const candidate = path41.resolve(baseDir, "..", "skills");
59871
+ const candidate = path42.resolve(baseDir, "..", "skills");
59192
59872
  try {
59193
- return fs43.statSync(candidate).isDirectory() ? candidate : null;
59873
+ return fs44.statSync(candidate).isDirectory() ? candidate : null;
59194
59874
  } catch {
59195
59875
  return null;
59196
59876
  }
@@ -59198,10 +59878,10 @@ function bundledSkillsDir(baseDir) {
59198
59878
  function listFiles(root) {
59199
59879
  const out = [];
59200
59880
  const walk = (dir) => {
59201
- for (const entry of fs43.readdirSync(dir, { withFileTypes: true })) {
59202
- const full = path41.join(dir, entry.name);
59881
+ for (const entry of fs44.readdirSync(dir, { withFileTypes: true })) {
59882
+ const full = path42.join(dir, entry.name);
59203
59883
  if (entry.isDirectory()) walk(full);
59204
- else if (entry.isFile()) out.push(path41.relative(root, full));
59884
+ else if (entry.isFile()) out.push(path42.relative(root, full));
59205
59885
  }
59206
59886
  };
59207
59887
  walk(root);
@@ -59209,7 +59889,7 @@ function listFiles(root) {
59209
59889
  }
59210
59890
  function readMarker(markerPath) {
59211
59891
  try {
59212
- const parsed2 = JSON.parse(fs43.readFileSync(markerPath, "utf8"));
59892
+ const parsed2 = JSON.parse(fs44.readFileSync(markerPath, "utf8"));
59213
59893
  return parsed2 && typeof parsed2 === "object" && parsed2.files ? parsed2 : null;
59214
59894
  } catch {
59215
59895
  return null;
@@ -59223,35 +59903,35 @@ function installBundledSkills(opts) {
59223
59903
  skippedOlderPackage: [],
59224
59904
  unchanged: [],
59225
59905
  errors: [],
59226
- targetDir: opts.targetDir ?? path41.join(os7.homedir(), ".claude", "skills"),
59906
+ targetDir: opts.targetDir ?? path42.join(os7.homedir(), ".claude", "skills"),
59227
59907
  bundledDir: null
59228
59908
  };
59229
59909
  const bundled = bundledSkillsDir(opts.baseDir);
59230
59910
  result.bundledDir = bundled;
59231
59911
  if (!bundled) return result;
59232
59912
  try {
59233
- fs43.mkdirSync(result.targetDir, { recursive: true });
59913
+ fs44.mkdirSync(result.targetDir, { recursive: true });
59234
59914
  } catch (e) {
59235
59915
  result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
59236
59916
  return result;
59237
59917
  }
59238
- const markerPath = path41.join(result.targetDir, ".ghl-command-skills.json");
59918
+ const markerPath = path42.join(result.targetDir, ".ghl-command-skills.json");
59239
59919
  const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
59240
59920
  const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
59241
59921
  for (const rel of listFiles(bundled)) {
59242
59922
  try {
59243
- const src = fs43.readFileSync(path41.join(bundled, rel));
59244
- const dest = path41.join(result.targetDir, rel);
59923
+ const src = fs44.readFileSync(path42.join(bundled, rel));
59924
+ const dest = path42.join(result.targetDir, rel);
59245
59925
  const srcHash = sha2562(src);
59246
59926
  let destBuf = null;
59247
59927
  try {
59248
- destBuf = fs43.readFileSync(dest);
59928
+ destBuf = fs44.readFileSync(dest);
59249
59929
  } catch {
59250
59930
  destBuf = null;
59251
59931
  }
59252
59932
  if (destBuf === null) {
59253
- fs43.mkdirSync(path41.dirname(dest), { recursive: true });
59254
- fs43.writeFileSync(dest, src);
59933
+ fs44.mkdirSync(path42.dirname(dest), { recursive: true });
59934
+ fs44.writeFileSync(dest, src);
59255
59935
  newMarker.files[rel] = srcHash;
59256
59936
  result.installed.push(rel);
59257
59937
  continue;
@@ -59268,7 +59948,7 @@ function installBundledSkills(opts) {
59268
59948
  newMarker.packageVersion = marker.packageVersion;
59269
59949
  result.skippedOlderPackage.push(rel);
59270
59950
  } else if (lastWritten && destHash === lastWritten) {
59271
- fs43.writeFileSync(dest, src);
59951
+ fs44.writeFileSync(dest, src);
59272
59952
  newMarker.files[rel] = srcHash;
59273
59953
  result.updated.push(rel);
59274
59954
  } else {
@@ -59279,7 +59959,7 @@ function installBundledSkills(opts) {
59279
59959
  }
59280
59960
  }
59281
59961
  try {
59282
- fs43.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
59962
+ fs44.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
59283
59963
  } catch (e) {
59284
59964
  result.errors.push(`marker write failed: ${String(e)}`);
59285
59965
  }
@@ -59435,7 +60115,7 @@ dotenv2.config();
59435
60115
  setPkgVersion(pkg.version);
59436
60116
  {
59437
60117
  const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
59438
- if (configDirOverride && !path42.isAbsolute(configDirOverride)) {
60118
+ if (configDirOverride && !path43.isAbsolute(configDirOverride)) {
59439
60119
  process.stderr.write(
59440
60120
  `[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.
59441
60121
  `
@@ -59448,20 +60128,20 @@ process.on("unhandledRejection", (reason3) => {
59448
60128
  `);
59449
60129
  });
59450
60130
  function hardenSecretFilePerms() {
59451
- const repoDir = path42.resolve(__dirname, "..");
60131
+ const repoDir = path43.resolve(__dirname, "..");
59452
60132
  const candidates = [
59453
- { file: path42.join(repoDir, "start-mcp.sh"), mode: 448 },
60133
+ { file: path43.join(repoDir, "start-mcp.sh"), mode: 448 },
59454
60134
  // Legacy registry location (pre-migration); new location lives in app-data.
59455
- { file: path42.join(repoDir, ".ghl-tokens.json"), mode: 384 },
60135
+ { file: path43.join(repoDir, ".ghl-tokens.json"), mode: 384 },
59456
60136
  { file: tokenRegistryPath(), mode: 384 }
59457
60137
  ];
59458
60138
  for (const { file: file2, mode } of candidates) {
59459
60139
  let current;
59460
60140
  try {
59461
- if (!fs44.existsSync(file2)) continue;
59462
- current = fs44.statSync(file2).mode & 511;
60141
+ if (!fs45.existsSync(file2)) continue;
60142
+ current = fs45.statSync(file2).mode & 511;
59463
60143
  if (current !== mode) {
59464
- fs44.chmodSync(file2, mode);
60144
+ fs45.chmodSync(file2, mode);
59465
60145
  }
59466
60146
  } catch (error) {
59467
60147
  const message = error instanceof Error ? error.message : String(error);