@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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/index.js +1348 -668
- package/package.json +1 -1
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
650
|
-
const url = new 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,
|
|
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,
|
|
663
|
+
return retryAccountRead(() => this.request(method, path44, options, attempt), `${method} ${path44}`);
|
|
664
664
|
}
|
|
665
665
|
if (attempt === 0) {
|
|
666
|
-
assertWriteAllowed(method,
|
|
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(
|
|
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} ${
|
|
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} ${
|
|
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,
|
|
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} ${
|
|
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,
|
|
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} ${
|
|
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(
|
|
742
|
-
return this.request("GET",
|
|
741
|
+
async get(path44, options) {
|
|
742
|
+
return this.request("GET", path44, options);
|
|
743
743
|
}
|
|
744
|
-
async post(
|
|
745
|
-
return this.request("POST",
|
|
744
|
+
async post(path44, options) {
|
|
745
|
+
return this.request("POST", path44, options);
|
|
746
746
|
}
|
|
747
|
-
async put(
|
|
748
|
-
return this.request("PUT",
|
|
747
|
+
async put(path44, options) {
|
|
748
|
+
return this.request("PUT", path44, options);
|
|
749
749
|
}
|
|
750
|
-
async patch(
|
|
751
|
-
return this.request("PATCH",
|
|
750
|
+
async patch(path44, options) {
|
|
751
|
+
return this.request("PATCH", path44, options);
|
|
752
752
|
}
|
|
753
|
-
async delete(
|
|
754
|
-
return this.request("DELETE",
|
|
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(
|
|
1048
|
+
updateFirebaseRefreshToken(newToken2) {
|
|
1049
1049
|
if (this.data.firebase) {
|
|
1050
|
-
this.data.firebase.refreshToken =
|
|
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,
|
|
1105
|
+
updateCompanyFirebaseRefreshToken(companyId, newToken2) {
|
|
1106
1106
|
const cfg = this.data.firebaseByCompany?.[_TokenRegistry.normalizeCompanyId(companyId)];
|
|
1107
1107
|
if (cfg) {
|
|
1108
|
-
cfg.refreshToken =
|
|
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(
|
|
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,
|
|
2346
|
+
this.registry?.updateCompanyFirebaseRefreshToken(company, newToken2);
|
|
2347
2347
|
if (company === this.homeCompanyId) {
|
|
2348
|
-
this.homeRefreshToken =
|
|
2348
|
+
this.homeRefreshToken = newToken2;
|
|
2349
2349
|
}
|
|
2350
2350
|
return;
|
|
2351
2351
|
}
|
|
2352
2352
|
if (company === void 0 || company === this.homeCompanyId) {
|
|
2353
|
-
this.homeRefreshToken =
|
|
2353
|
+
this.homeRefreshToken = newToken2;
|
|
2354
2354
|
}
|
|
2355
|
-
this.persistRefreshToken(
|
|
2356
|
-
this.registry?.updateFirebaseRefreshToken(
|
|
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(
|
|
2369
|
+
persistRefreshToken(newToken2) {
|
|
2370
2370
|
try {
|
|
2371
2371
|
const creds = readCredentials();
|
|
2372
2372
|
if (creds) {
|
|
2373
|
-
writeCredentials({ ...creds, ghl_firebase_refresh_token:
|
|
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=${
|
|
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
|
|
3026
|
-
processEphemeralId =
|
|
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(
|
|
3473
|
-
return
|
|
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,
|
|
7112
|
-
const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${
|
|
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${
|
|
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(
|
|
8567
|
-
const p = demoJournalPath(
|
|
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(
|
|
8570
|
+
fs7.writeFileSync(tmp, JSON.stringify(journal2, null, 2));
|
|
8571
8571
|
fs7.renameSync(tmp, p);
|
|
8572
8572
|
return p;
|
|
8573
8573
|
}
|
|
8574
|
-
function activeRun(
|
|
8575
|
-
return
|
|
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(
|
|
8578
|
-
if (runId) return
|
|
8579
|
-
const open =
|
|
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] ?? [...
|
|
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
|
|
8740
|
-
if (
|
|
8741
|
-
throw new Error(`This sub-account already has a demo bot recorded: ${
|
|
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:
|
|
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
|
-
|
|
8798
|
-
save(
|
|
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
|
|
8836
|
-
const botId2 = 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(
|
|
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
|
-
|
|
8842
|
-
save(
|
|
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
|
|
8961
|
-
const run = findRun(
|
|
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(
|
|
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
|
|
9039
|
-
const run = findRun(
|
|
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(
|
|
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(
|
|
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
|
|
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 = () => !
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
9295
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
9315
|
-
if (!
|
|
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
|
|
10926
|
-
const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${
|
|
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 ${
|
|
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(
|
|
10989
|
-
return client.request("GET",
|
|
10988
|
+
async function internalGet(path44) {
|
|
10989
|
+
return client.request("GET", path44);
|
|
10990
10990
|
}
|
|
10991
|
-
async function internalPost(
|
|
10992
|
-
return client.request("POST",
|
|
10991
|
+
async function internalPost(path44, body) {
|
|
10992
|
+
return client.request("POST", path44, body);
|
|
10993
10993
|
}
|
|
10994
|
-
async function internalPut(
|
|
10995
|
-
return client.request("PUT",
|
|
10994
|
+
async function internalPut(path44, body) {
|
|
10995
|
+
return client.request("PUT", path44, body);
|
|
10996
10996
|
}
|
|
10997
|
-
async function internalDelete(
|
|
10998
|
-
return client.request("DELETE",
|
|
10997
|
+
async function internalDelete(path44) {
|
|
10998
|
+
return client.request("DELETE", path44);
|
|
10999
10999
|
}
|
|
11000
|
-
async function funnelRequest(method,
|
|
11001
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
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} ${
|
|
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,
|
|
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) =>
|
|
11608
|
-
assertWriteAllowed("POST", SVC +
|
|
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 +
|
|
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 ${
|
|
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,
|
|
11973
|
-
if (Array.isArray(node)) return void node.forEach((x, i) => walk(x, `${
|
|
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, `${
|
|
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:
|
|
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 = (
|
|
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(
|
|
16657
|
+
function brandedPath(path44, name, taken) {
|
|
16658
16658
|
const normalize2 = (s) => s.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
16659
|
-
const base = normalize2(
|
|
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 [
|
|
16688
|
-
suffix = from.slice(
|
|
16689
|
-
matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(
|
|
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,
|
|
16773
|
-
const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${
|
|
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} ${
|
|
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,
|
|
17652
|
-
const url = `https://backend.leadconnectorhq.com/forms${
|
|
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} ${
|
|
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,
|
|
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
|
|
17834
|
-
if (formId)
|
|
17835
|
-
if (skip)
|
|
17836
|
-
const result = await formRequest("GET",
|
|
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,
|
|
18245
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
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} ${
|
|
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
|
|
19238
|
+
const crypto9 = require("node:crypto");
|
|
19239
19239
|
const raw = `${os8.hostname()}:${os8.userInfo().username}:${os8.platform()}:${os8.arch()}`;
|
|
19240
|
-
return
|
|
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
|
|
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 ||
|
|
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 =
|
|
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(
|
|
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}${
|
|
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 === (
|
|
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 (
|
|
21214
|
-
const url = new URL(`https://api.stripe.com${
|
|
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,
|
|
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(
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
30147
|
-
|
|
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(
|
|
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 =
|
|
30621
|
+
const dir = import_node_path3.default.join(shopperDir(base), locationId2);
|
|
30159
30622
|
let names = [];
|
|
30160
30623
|
try {
|
|
30161
|
-
names =
|
|
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(
|
|
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 = (
|
|
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 =
|
|
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 =
|
|
30675
|
+
const convId = str5(asObj4(c).id);
|
|
30213
30676
|
if (!convId) continue;
|
|
30214
|
-
const raw =
|
|
30215
|
-
const list2 =
|
|
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 =
|
|
30218
|
-
if (
|
|
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(
|
|
30684
|
+
const at = Date.parse(str5(m.dateAdded) ?? "");
|
|
30222
30685
|
if (Number.isNaN(at) || at < since - 5e3) continue;
|
|
30223
|
-
const 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:
|
|
30230
|
-
body: htmlToText(
|
|
30231
|
-
raw:
|
|
30232
|
-
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
|
|
30241
|
-
const co =
|
|
30242
|
-
if (
|
|
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 =
|
|
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 =
|
|
30264
|
-
const businessName =
|
|
30265
|
-
const websiteHost = siteHost(
|
|
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 =
|
|
30330
|
-
run.contactId =
|
|
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 =
|
|
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 =
|
|
30368
|
-
const added = Date.parse(
|
|
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
|
-
|
|
30392
|
-
const token2 =
|
|
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 =
|
|
30860
|
+
const fd = import_node_fs3.default.openSync(lock, "wx", 384);
|
|
30397
30861
|
try {
|
|
30398
|
-
|
|
30862
|
+
import_node_fs3.default.writeSync(fd, mine);
|
|
30399
30863
|
} finally {
|
|
30400
|
-
|
|
30864
|
+
import_node_fs3.default.closeSync(fd);
|
|
30401
30865
|
}
|
|
30402
30866
|
return () => {
|
|
30403
30867
|
try {
|
|
30404
|
-
if (
|
|
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 =
|
|
30414
|
-
age = Date.now() -
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
30498
|
-
|
|
30499
|
-
|
|
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
|
-
|
|
30507
|
-
|
|
30508
|
-
|
|
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 =
|
|
30520
|
-
if (
|
|
30521
|
-
const s =
|
|
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 = (
|
|
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 =
|
|
30543
|
-
const 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 =
|
|
31038
|
+
const type = str6(node.type);
|
|
30548
31039
|
if (type !== "sms" && type !== "email") continue;
|
|
30549
31040
|
let minutes = 0;
|
|
30550
|
-
let conditional = Boolean(
|
|
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
|
-
|
|
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) ||
|
|
31049
|
+
if (isBranch(p) || str6(p.parent) && isBranch(nodes.get(str6(p.parent) ?? "") ?? {})) {
|
|
30558
31050
|
conditional = true;
|
|
30559
31051
|
break;
|
|
30560
31052
|
}
|
|
30561
|
-
if (
|
|
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 =
|
|
31062
|
+
cur = str6(p.parentKey);
|
|
30570
31063
|
}
|
|
30571
31064
|
if (conditional) {
|
|
30572
31065
|
leftOut.conditional++;
|
|
30573
31066
|
continue;
|
|
30574
31067
|
}
|
|
30575
|
-
const a =
|
|
30576
|
-
const key = type === "email" ? wordingKey(
|
|
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 =
|
|
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 =
|
|
30595
|
-
const name =
|
|
31087
|
+
const wf = asObj5(await reader.getWorkflow(id));
|
|
31088
|
+
const name = str6(wf.name) ?? id;
|
|
30596
31089
|
names[id] = name;
|
|
30597
|
-
if (
|
|
30598
|
-
workflows.push({ name, 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
|
|
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
|
-
|
|
30611
|
-
|
|
30612
|
-
|
|
30613
|
-
isBranch = (n) => /^branch-/.test(
|
|
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,
|
|
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
|
|
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
|
-
|
|
32549
|
-
|
|
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
|
|
33527
|
+
return path23.join(base, "client-status.json");
|
|
32956
33528
|
}
|
|
32957
33529
|
function readStatusStore(base) {
|
|
32958
33530
|
try {
|
|
32959
|
-
const raw = JSON.parse(
|
|
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
|
-
|
|
32968
|
-
|
|
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: ${
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
33066
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
33266
|
-
|
|
33267
|
-
|
|
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
|
|
34097
|
+
return path24.join(base, "handoffs");
|
|
33526
34098
|
}
|
|
33527
34099
|
function handoffSnapshotPath(locationId2, base) {
|
|
33528
|
-
return
|
|
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
|
-
|
|
33533
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
33559
|
-
const file2 =
|
|
33560
|
-
|
|
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(
|
|
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 ?
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
33704
|
-
|
|
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,
|
|
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,
|
|
33731
|
-
(0,
|
|
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
|
|
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
|
-
|
|
33739
|
-
|
|
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,
|
|
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
|
|
34355
|
+
return path25.join(appDataDir(), "build-obligations", id + ".json");
|
|
33784
34356
|
}
|
|
33785
34357
|
function readBuildObligations(id) {
|
|
33786
34358
|
try {
|
|
33787
|
-
const data = JSON.parse(
|
|
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
|
-
|
|
33799
|
-
|
|
33800
|
-
|
|
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
|
-
|
|
33826
|
-
|
|
33827
|
-
|
|
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
|
|
34401
|
+
var fs27, path25;
|
|
33830
34402
|
var init_build_obligations = __esm({
|
|
33831
34403
|
"src/build-obligations.ts"() {
|
|
33832
34404
|
"use strict";
|
|
33833
|
-
|
|
33834
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
33965
|
-
const 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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
34008
|
-
(0,
|
|
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,
|
|
34029
|
-
const outDir = (0,
|
|
34030
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
34073
|
-
if (!(0,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
34101
|
-
|
|
34102
|
-
|
|
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
|
|
34702
|
+
return path26.join(appDataDir(), "command-os-run-cost.jsonl");
|
|
34131
34703
|
}
|
|
34132
34704
|
function recordRunCost(rec, file2 = runCostPath()) {
|
|
34133
|
-
|
|
34134
|
-
|
|
34705
|
+
fs28.mkdirSync(path26.dirname(file2), { recursive: true });
|
|
34706
|
+
fs28.appendFileSync(file2, `${JSON.stringify(rec)}
|
|
34135
34707
|
`, { mode: 384 });
|
|
34136
34708
|
try {
|
|
34137
|
-
|
|
34709
|
+
fs28.chmodSync(file2, 384);
|
|
34138
34710
|
} catch {
|
|
34139
34711
|
}
|
|
34140
34712
|
}
|
|
34141
34713
|
function readRunCosts(file2 = runCostPath()) {
|
|
34142
34714
|
try {
|
|
34143
|
-
return
|
|
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
|
|
34746
|
+
var fs28, path26;
|
|
34175
34747
|
var init_run_cost = __esm({
|
|
34176
34748
|
"src/run-cost.ts"() {
|
|
34177
34749
|
"use strict";
|
|
34178
|
-
|
|
34179
|
-
|
|
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,
|
|
35553
|
+
function setPath(target, path44, value) {
|
|
34982
35554
|
if (value === void 0) return;
|
|
34983
|
-
const parts =
|
|
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
|
-
|
|
36354
|
+
path27.join(__dirname, "..", "skills", "blueprint", "presets"),
|
|
35783
36355
|
// dist/ → package root
|
|
35784
|
-
|
|
36356
|
+
path27.join(__dirname, "..", "..", "skills", "blueprint", "presets"),
|
|
35785
36357
|
// src/intake-to-build in dev
|
|
35786
|
-
|
|
36358
|
+
path27.join(process.cwd(), "skills", "blueprint", "presets")
|
|
35787
36359
|
];
|
|
35788
|
-
return candidates.find((c) =>
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
35840
|
-
|
|
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
|
|
37801
|
+
return path28.join(base, "plans");
|
|
37230
37802
|
}
|
|
37231
37803
|
function savePlanRecord(rec, base) {
|
|
37232
37804
|
const dir = plansDir(base);
|
|
37233
|
-
|
|
37234
|
-
const file2 =
|
|
37235
|
-
|
|
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(
|
|
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
|
|
37845
|
+
var fs30, path28;
|
|
37274
37846
|
var init_plan_store = __esm({
|
|
37275
37847
|
"src/plan-store.ts"() {
|
|
37276
37848
|
"use strict";
|
|
37277
|
-
|
|
37278
|
-
|
|
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
|
|
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
|
-
|
|
39521
|
+
fs31.accessSync(systemApps, fs31.constants.W_OK);
|
|
38950
39522
|
base = "";
|
|
38951
39523
|
} catch {
|
|
38952
39524
|
}
|
|
38953
|
-
const appDir = base ?
|
|
38954
|
-
const macOSDir =
|
|
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:
|
|
38984
|
-
{ path:
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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
|
-
|
|
39038
|
-
|
|
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
|
|
39630
|
+
var fs31, os4, path29;
|
|
39059
39631
|
var init_launcher = __esm({
|
|
39060
39632
|
"src/launcher.ts"() {
|
|
39061
39633
|
"use strict";
|
|
39062
|
-
|
|
39634
|
+
fs31 = __toESM(require("fs"));
|
|
39063
39635
|
os4 = __toESM(require("os"));
|
|
39064
|
-
|
|
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" ?
|
|
39072
|
-
return
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
39092
|
-
const pkg2 =
|
|
39663
|
+
const dir = path30.join(npxDir, entry.name);
|
|
39664
|
+
const pkg2 = path30.join(dir, PACKAGE_SUBDIR);
|
|
39093
39665
|
try {
|
|
39094
|
-
if (!
|
|
39666
|
+
if (!fs32.statSync(dir).isDirectory() || !fs32.statSync(pkg2).isDirectory()) continue;
|
|
39095
39667
|
} catch {
|
|
39096
39668
|
continue;
|
|
39097
39669
|
}
|
|
39098
|
-
if (gone(
|
|
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
|
|
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
|
-
|
|
39683
|
+
fs32 = __toESM(require("node:fs"));
|
|
39112
39684
|
os5 = __toESM(require("node:os"));
|
|
39113
|
-
|
|
39114
|
-
PACKAGE_SUBDIR =
|
|
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) =>
|
|
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(
|
|
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 ??
|
|
39151
|
-
candidates.push(
|
|
39152
|
-
const localAppData = opts?.localAppData ?? process.env.LOCALAPPDATA ??
|
|
39153
|
-
const packagesDir =
|
|
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
|
|
39727
|
+
for (const entry of fs33.readdirSync(packagesDir)) {
|
|
39156
39728
|
if (entry.startsWith("Claude_")) {
|
|
39157
|
-
candidates.push(
|
|
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(
|
|
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 =
|
|
39170
|
-
return { configPath: p, exists:
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
39205
|
-
const outsideHome = rel.startsWith("..") ||
|
|
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 =
|
|
39216
|
-
const stat =
|
|
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 (!
|
|
39862
|
+
if (!fs33.existsSync(base)) return base;
|
|
39291
39863
|
for (let i = 2; ; i++) {
|
|
39292
39864
|
const candidate = `${base}-${i}`;
|
|
39293
|
-
if (!
|
|
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
|
-
|
|
39300
|
-
const readBack =
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
39897
|
+
const st = fs33.statSync(configPath);
|
|
39326
39898
|
if (st.size === baseline.size && st.mtimeMs === baseline.mtimeMs) {
|
|
39327
|
-
ok = sha256(
|
|
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 =
|
|
39343
|
-
const tempPath =
|
|
39344
|
-
const fd =
|
|
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
|
-
|
|
39347
|
-
|
|
39918
|
+
fs33.writeFileSync(fd, opts.newBytes);
|
|
39919
|
+
fs33.fsyncSync(fd);
|
|
39348
39920
|
} finally {
|
|
39349
|
-
|
|
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
|
-
|
|
39946
|
+
fs33.rmSync(tempPath, { force: true });
|
|
39375
39947
|
} catch {
|
|
39376
39948
|
}
|
|
39377
39949
|
throw e;
|
|
39378
39950
|
}
|
|
39379
39951
|
try {
|
|
39380
|
-
const dirFd =
|
|
39952
|
+
const dirFd = fs33.openSync(dir, "r");
|
|
39381
39953
|
try {
|
|
39382
|
-
|
|
39954
|
+
fs33.fsyncSync(dirFd);
|
|
39383
39955
|
} finally {
|
|
39384
|
-
|
|
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 =
|
|
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)
|
|
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
|
|
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
|
-
|
|
40069
|
+
fs33 = __toESM(require("node:fs"));
|
|
39498
40070
|
os6 = __toESM(require("node:os"));
|
|
39499
|
-
|
|
39500
|
-
|
|
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 =
|
|
39534
|
-
|
|
39535
|
-
|
|
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,
|
|
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
|
-
|
|
39810
|
-
|
|
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
|
|
40928
|
+
return path33.join(appDataDir(), "intake-overlay.json");
|
|
40357
40929
|
}
|
|
40358
40930
|
function readOverlay() {
|
|
40359
40931
|
try {
|
|
40360
|
-
const raw = JSON.parse(
|
|
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
|
-
|
|
40940
|
+
fs35.writeFileSync(overlayPath(), JSON.stringify(layer, null, 2), { mode: 384 });
|
|
40369
40941
|
}
|
|
40370
|
-
var
|
|
40942
|
+
var fs35, path33;
|
|
40371
40943
|
var init_intake_overlay = __esm({
|
|
40372
40944
|
"src/intake-overlay.ts"() {
|
|
40373
40945
|
"use strict";
|
|
40374
|
-
|
|
40375
|
-
|
|
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(
|
|
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 (!
|
|
40856
|
-
const base =
|
|
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 =
|
|
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 =
|
|
40865
|
-
const callsDir =
|
|
40866
|
-
const clientDir =
|
|
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 =
|
|
41442
|
+
cur = path34.join(cur, part);
|
|
40871
41443
|
let here;
|
|
40872
41444
|
try {
|
|
40873
|
-
here =
|
|
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)
|
|
41451
|
+
if (!here) fs36.mkdirSync(cur, { mode: 448 });
|
|
40880
41452
|
}
|
|
40881
|
-
if (!inside(realBase,
|
|
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 =
|
|
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 =
|
|
40890
|
-
if (
|
|
41461
|
+
const tPath = path34.join(callsDir, tName);
|
|
41462
|
+
if (fs36.existsSync(callPath) || fs36.existsSync(tPath)) continue;
|
|
40891
41463
|
try {
|
|
40892
|
-
|
|
40893
|
-
|
|
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
|
-
|
|
40902
|
-
|
|
40903
|
-
const understandingFile =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
41486
|
+
fs36.appendFileSync(understandingFile, file2.understanding.append, { encoding: "utf8", mode: 384 });
|
|
40915
41487
|
}
|
|
40916
|
-
|
|
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 (!
|
|
40927
|
-
const base =
|
|
40928
|
-
const callPath =
|
|
40929
|
-
const understandingPath =
|
|
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 =
|
|
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(
|
|
40939
|
-
|
|
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 =
|
|
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)
|
|
40947
|
-
else
|
|
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
|
|
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
|
-
|
|
41193
|
-
|
|
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,
|
|
41600
|
-
(0,
|
|
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,
|
|
41603
|
-
(0,
|
|
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,
|
|
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
|
-
|
|
41639
|
-
|
|
42210
|
+
import_node_crypto13 = require("node:crypto");
|
|
42211
|
+
import_node_fs8 = require("node:fs");
|
|
41640
42212
|
import_node_os = require("node:os");
|
|
41641
|
-
|
|
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
|
|
42397
|
+
return path35.join(appDataDir(), "cockpit-state.json");
|
|
41826
42398
|
}
|
|
41827
42399
|
function readCockpitState() {
|
|
41828
42400
|
try {
|
|
41829
|
-
const raw = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
42810
|
+
return path35.join(base, "clients", safeLoc, "documents");
|
|
42239
42811
|
}
|
|
42240
42812
|
function safeDocName(name) {
|
|
42241
|
-
const cleaned =
|
|
42242
|
-
const ext =
|
|
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 (!
|
|
42250
|
-
return
|
|
42251
|
-
const st =
|
|
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
|
-
|
|
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 =
|
|
42343
|
-
const base =
|
|
42344
|
-
return real === base || real.startsWith(base +
|
|
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 =
|
|
42922
|
+
const ext = path35.extname(name), stem = name.slice(0, name.length - ext.length);
|
|
42351
42923
|
let candidate = name;
|
|
42352
|
-
for (let n = 2;
|
|
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
|
-
|
|
42928
|
+
fs37.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
42357
42929
|
const finalName = uniqueDocName(dir, name);
|
|
42358
|
-
const tmp =
|
|
42359
|
-
const fd =
|
|
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
|
-
|
|
42362
|
-
|
|
42933
|
+
fs37.writeSync(fd, bytes);
|
|
42934
|
+
fs37.fsyncSync(fd);
|
|
42363
42935
|
} finally {
|
|
42364
|
-
|
|
42936
|
+
fs37.closeSync(fd);
|
|
42365
42937
|
}
|
|
42366
|
-
const dest =
|
|
42938
|
+
const dest = path35.join(dir, finalName);
|
|
42367
42939
|
try {
|
|
42368
|
-
if (
|
|
42369
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
44433
|
-
if (!insideDir(
|
|
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 =
|
|
44790
|
-
if (!
|
|
44791
|
-
if (
|
|
44792
|
-
const text2 =
|
|
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 =
|
|
44867
|
-
if (!
|
|
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(
|
|
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 ?
|
|
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(
|
|
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,
|
|
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
|
-
|
|
45684
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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) =>
|
|
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] =
|
|
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:
|
|
47438
|
-
triggers:
|
|
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,
|
|
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
|
-
|
|
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,
|
|
47548
|
-
const url = `${SMARTLIST_BASE}${
|
|
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} ${
|
|
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,
|
|
47799
|
-
const response = await builderRequest(client, method, `${REPUTATION_BASE}${
|
|
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} ${
|
|
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(
|
|
47941
|
-
const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${
|
|
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} ${
|
|
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 =
|
|
48789
|
+
const templatesDir = path36.resolve(__dirname, "..", "templates");
|
|
48138
48790
|
function validateTemplatePath(templateFile) {
|
|
48139
|
-
const resolved2 =
|
|
48140
|
-
const realPath =
|
|
48791
|
+
const resolved2 = path36.resolve(templateFile);
|
|
48792
|
+
const realPath = fs38.existsSync(resolved2) ? fs38.realpathSync(resolved2) : resolved2;
|
|
48141
48793
|
const allowedDirs = [
|
|
48142
48794
|
templatesDir,
|
|
48143
|
-
|
|
48144
|
-
|
|
48145
|
-
].map((d) =>
|
|
48146
|
-
const isAllowed = allowedDirs.some((dir) => realPath.startsWith(dir +
|
|
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 (
|
|
48151
|
-
const otherMatch = allowedDirs.slice(1).some((d) => !
|
|
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
|
-
|
|
48167
|
-
|
|
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 (!
|
|
48173
|
-
const files =
|
|
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 =
|
|
48827
|
+
const full = path36.join(dir, file2);
|
|
48176
48828
|
let real;
|
|
48177
48829
|
try {
|
|
48178
|
-
real =
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
48642
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
49243
|
-
if (
|
|
49244
|
-
const devRoot =
|
|
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(
|
|
49898
|
+
const pkg2 = JSON.parse(fs39.readFileSync(path37.join(devRoot, "package.json"), "utf8"));
|
|
49247
49899
|
if (pkg2.name === "@elitedcs/ghl-mcp") {
|
|
49248
|
-
const dev =
|
|
49249
|
-
if (
|
|
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 =
|
|
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 (!
|
|
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,
|
|
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
|
-
|
|
49322
|
-
|
|
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 =
|
|
51534
|
-
if (!
|
|
51535
|
-
return { html:
|
|
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
|
-
|
|
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
|
-
|
|
51629
|
-
files.push(
|
|
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
|
-
|
|
51662
|
-
files.push(
|
|
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
|
-
|
|
51671
|
-
files.push(
|
|
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 =
|
|
51745
|
-
|
|
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 =
|
|
51749
|
-
|
|
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
|
|
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
|
-
|
|
51758
|
-
|
|
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,
|
|
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,
|
|
52040
|
-
(0,
|
|
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,
|
|
52710
|
+
const file2 = (0, import_node_path9.join)(outDir, `pass-${pass}.html`);
|
|
52059
52711
|
if (!html2) {
|
|
52060
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
52100
|
-
const g = await gradeCopy((0,
|
|
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,
|
|
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,
|
|
52108
|
-
if (result.ok) result.file = (0,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
52125
|
-
|
|
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) =>
|
|
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 =
|
|
52345
|
-
if (!
|
|
52346
|
-
const r = lintDesignContract(
|
|
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 ?
|
|
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:
|
|
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 =
|
|
52411
|
-
if (!
|
|
52412
|
-
page = JSON.parse(
|
|
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 =
|
|
52454
|
-
if (!
|
|
52455
|
-
html2 =
|
|
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,
|
|
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
|
-
|
|
52496
|
-
|
|
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 :
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
53260
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
53291
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 :
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
53790
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
53826
|
-
|
|
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
|
|
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,
|
|
57329
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
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} ${
|
|
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,
|
|
57348
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
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} ${
|
|
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((
|
|
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
|
|
58992
|
-
const brand =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
59030
|
-
const 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:
|
|
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
|
-
...
|
|
59720
|
+
...str8("mode") && { mode: str8("mode") },
|
|
59041
59721
|
...a.slides && { slides: Number(a.slides) },
|
|
59042
|
-
theme:
|
|
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
|
-
...
|
|
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,
|
|
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
|
-
|
|
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
|
|
59103
|
-
var
|
|
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
|
|
59175
|
-
var
|
|
59176
|
-
var
|
|
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,
|
|
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 =
|
|
59871
|
+
const candidate = path42.resolve(baseDir, "..", "skills");
|
|
59192
59872
|
try {
|
|
59193
|
-
return
|
|
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
|
|
59202
|
-
const full =
|
|
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(
|
|
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(
|
|
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 ??
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
59244
|
-
const dest =
|
|
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 =
|
|
59928
|
+
destBuf = fs44.readFileSync(dest);
|
|
59249
59929
|
} catch {
|
|
59250
59930
|
destBuf = null;
|
|
59251
59931
|
}
|
|
59252
59932
|
if (destBuf === null) {
|
|
59253
|
-
|
|
59254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 && !
|
|
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 =
|
|
60131
|
+
const repoDir = path43.resolve(__dirname, "..");
|
|
59452
60132
|
const candidates = [
|
|
59453
|
-
{ file:
|
|
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:
|
|
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 (!
|
|
59462
|
-
current =
|
|
60141
|
+
if (!fs45.existsSync(file2)) continue;
|
|
60142
|
+
current = fs45.statSync(file2).mode & 511;
|
|
59463
60143
|
if (current !== mode) {
|
|
59464
|
-
|
|
60144
|
+
fs45.chmodSync(file2, mode);
|
|
59465
60145
|
}
|
|
59466
60146
|
} catch (error) {
|
|
59467
60147
|
const message = error instanceof Error ? error.message : String(error);
|