@elitedcs/ghl-mcp 3.84.1 → 3.84.2
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 +46 -0
- package/README.md +1 -1
- package/dist/index.js +412 -78
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 3.84.2 — done means read back, not echoed
|
|
6
|
+
|
|
7
|
+
Every tool that reports a thing as done, verified, ready or published was checked against a second,
|
|
8
|
+
independent read of what GoHighLevel actually holds. Thirteen places were claiming an outcome from
|
|
9
|
+
the write's own response, a counter, or the wrong field. Each now derives the claim from a read taken
|
|
10
|
+
after the write, or says plainly what could not be confirmed.
|
|
11
|
+
|
|
12
|
+
**A build report no longer says "Published N workflows live" for a workflow GoHighLevel kept as a
|
|
13
|
+
draft.** The publish step already read the workflow back and said NOT LIVE when it was not; the
|
|
14
|
+
build threw that answer away and counted it anyway. It is counted now, and a workflow that published
|
|
15
|
+
with every trigger still inactive is named as such rather than called running. The publish tool's
|
|
16
|
+
step and trigger counts come from the read after the write, with the number of active triggers
|
|
17
|
+
beside them.
|
|
18
|
+
|
|
19
|
+
**The workflow audit no longer says "ok" while dropping four kinds of warning.** Steps GoHighLevel
|
|
20
|
+
will not accept on save, waits in the unit it no longer takes, an alert set to notify a specific
|
|
21
|
+
person with nobody chosen, and steps needing a social account that is not connected were computed
|
|
22
|
+
for every workflow and then left out of the roll-up. They are in it, and in the client-facing report,
|
|
23
|
+
in plain words.
|
|
24
|
+
|
|
25
|
+
**"Trained." means the pages are trained.** Training a knowledge base settled on a status shared
|
|
26
|
+
with discovery, so it said Trained the moment discovery finished, for a failed run, and for pages
|
|
27
|
+
queued behind an earlier batch. It now settles only when every requested page is in the trained
|
|
28
|
+
bucket, and a crawl GoHighLevel stopped says so instead of "Discovery finished".
|
|
29
|
+
|
|
30
|
+
**The morning brief no longer calls a morning quiet when your own calendar could not be read.**
|
|
31
|
+
A connected Google Calendar that failed to open produced the same "Nothing needs you this morning"
|
|
32
|
+
as an empty one. The headline now says the calendar could not be read.
|
|
33
|
+
|
|
34
|
+
**Readiness, calendars, smart lists, keys and pages tell the truth.** A phone-agent readiness check
|
|
35
|
+
no longer says READY on a number pool whose numbers it cannot read. Renaming a calendar carries the
|
|
36
|
+
assigned staff back in the shape GoHighLevel validates and reports the stored team-member count from
|
|
37
|
+
a read after the update. A contact smart list is refused on the opportunity service instead of being
|
|
38
|
+
created where the Contacts screen never shows it. "Removed" for a sub-account key is said only when
|
|
39
|
+
the key really left this computer's file. A page save whose baseline could not be read is reported
|
|
40
|
+
as unverified rather than visible, and a page build is verified on the pointer GoHighLevel serves
|
|
41
|
+
the page from. Switching accounts says the workflow-builder credentials were found, not tested.
|
|
42
|
+
|
|
43
|
+
**Also in this release:** the eleven small findings deferred from the 3.84.1 reviews. Among them:
|
|
44
|
+
a survey trigger is checked against the account instead of refused as a dead end; three more trigger
|
|
45
|
+
slots are read back; a plan handed back by hand is held to the same confirmed selection as a saved
|
|
46
|
+
one; a half-applied older build is told to keep its undo and plan only the rest; a number an agent
|
|
47
|
+
held only in the old non-routing shape is named when a bind starts routing it; and the brand repaint
|
|
48
|
+
leaves a tint or tone as it was only when that makes the text readable, saying so honestly, and
|
|
49
|
+
repaints every spelling of a colour, not only the six-digit hex.
|
|
50
|
+
|
|
5
51
|
## 3.84.1 — the phone agent answers, the repaint tells the truth, and the build waits to be told
|
|
6
52
|
|
|
7
53
|
**A phone agent you connect now actually answers.** Connecting a number to a voice agent wrote a
|
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -948,20 +948,33 @@ var init_token_registry = __esm({
|
|
|
948
948
|
const token2 = this.data.tokens[locationId2];
|
|
949
949
|
if (!token2) return false;
|
|
950
950
|
if (token2.companyId === companyId) return true;
|
|
951
|
+
const previous = token2.companyId;
|
|
951
952
|
token2.companyId = companyId;
|
|
952
953
|
this.save();
|
|
954
|
+
if (this.lastSaveError) {
|
|
955
|
+
if (previous === void 0) delete token2.companyId;
|
|
956
|
+
else token2.companyId = previous;
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
953
959
|
return true;
|
|
954
960
|
}
|
|
955
961
|
/**
|
|
956
|
-
* Remove a location from the registry
|
|
962
|
+
* Remove a location from the registry. Returns true only when the removal
|
|
963
|
+
* reached the key file on disk: save() swallows a write failure into
|
|
964
|
+
* lastSaveError, and a key still sitting in .ghl-tokens.json comes back in
|
|
965
|
+
* full at the next start. A caller that reports "Removed" must not be told
|
|
966
|
+
* that by a delete from an in-memory map alone.
|
|
957
967
|
*/
|
|
958
968
|
removeLocation(locationId2) {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
969
|
+
const existing = this.data.tokens[locationId2];
|
|
970
|
+
if (!existing) return false;
|
|
971
|
+
delete this.data.tokens[locationId2];
|
|
972
|
+
this.save();
|
|
973
|
+
if (this.lastSaveError) {
|
|
974
|
+
this.data.tokens[locationId2] = existing;
|
|
975
|
+
return false;
|
|
963
976
|
}
|
|
964
|
-
return
|
|
977
|
+
return true;
|
|
965
978
|
}
|
|
966
979
|
/**
|
|
967
980
|
* List all registered locations
|
|
@@ -2610,19 +2623,27 @@ ${errorBody}`
|
|
|
2610
2623
|
const result = await this.updateWorkflow(workflowId, { status: "published", triggers });
|
|
2611
2624
|
let returned = null;
|
|
2612
2625
|
let readBackFailed = null;
|
|
2626
|
+
let savedSteps = null;
|
|
2627
|
+
let savedTriggers = null;
|
|
2613
2628
|
try {
|
|
2614
2629
|
const after = await this.getWorkflow(workflowId);
|
|
2615
2630
|
returned = typeof after.status === "string" ? after.status : null;
|
|
2631
|
+
savedSteps = (after.workflowData?.templates ?? []).length;
|
|
2632
|
+
savedTriggers = after.triggers ?? [];
|
|
2616
2633
|
} catch (e) {
|
|
2617
2634
|
readBackFailed = e instanceof Error ? e.message : String(e);
|
|
2618
2635
|
returned = isRecord2(result) && typeof result.status === "string" ? result.status : null;
|
|
2619
2636
|
}
|
|
2637
|
+
const activeTriggers = savedTriggers ? savedTriggers.filter((t) => t.active === true).length : null;
|
|
2638
|
+
const inactiveTriggers = savedTriggers ? savedTriggers.filter((t) => t.active === false).length : 0;
|
|
2620
2639
|
const publish_check = {
|
|
2621
2640
|
published: returned === "published",
|
|
2622
2641
|
status_returned: returned ?? "(GHL returned no status)",
|
|
2623
2642
|
status_source: readBackFailed ? "the publish response (re-read failed)" : "the saved workflow, read back",
|
|
2624
|
-
|
|
2625
|
-
|
|
2643
|
+
counts_source: savedTriggers ? "the saved workflow, read back" : "the read taken BEFORE the write (the re-read failed, so these counts do not prove what is saved)",
|
|
2644
|
+
steps: savedSteps ?? steps.length,
|
|
2645
|
+
triggers: savedTriggers ? savedTriggers.length : triggers.length,
|
|
2646
|
+
triggers_active: activeTriggers
|
|
2626
2647
|
};
|
|
2627
2648
|
if (readBackFailed) {
|
|
2628
2649
|
publish_check.warning = `NOT VERIFIED. The publish was accepted but reading the workflow back failed (${readBackFailed}), so this status is only what the publish call echoed, not the saved state. Confirm with get_workflow_full.`;
|
|
@@ -2630,7 +2651,10 @@ ${errorBody}`
|
|
|
2630
2651
|
if (returned !== "published") {
|
|
2631
2652
|
publish_check.warning = `NOT LIVE. GHL returned status ${returned === null ? "nothing" : `"${returned}"`} rather than "published", so do not report this workflow as running.`;
|
|
2632
2653
|
}
|
|
2633
|
-
if (
|
|
2654
|
+
if (returned === "published" && inactiveTriggers > 0) {
|
|
2655
|
+
publish_check.warning = `TRIGGER NOT ACTIVE. The workflow saved as "published", but ${inactiveTriggers} of ${savedTriggers.length} trigger(s) still read active:false, so ${inactiveTriggers === savedTriggers.length ? "nothing enrols contacts into it on its own" : "those triggers enrol nobody"}. Check the trigger in GHL before reporting this workflow as running.`;
|
|
2656
|
+
}
|
|
2657
|
+
if ((savedTriggers ? savedTriggers.length : triggers.length) === 0) {
|
|
2634
2658
|
publish_check.note = "This workflow has NO TRIGGER, so nothing starts it on its own. That is only correct if another workflow adds contacts to it; otherwise its trigger did not save.";
|
|
2635
2659
|
}
|
|
2636
2660
|
return isRecord2(result) ? { ...result, publish_check } : { result, publish_check };
|
|
@@ -5197,8 +5221,40 @@ function registerCalendarTools(server2, client) {
|
|
|
5197
5221
|
`update_calendar: could not read calendar ${args.calendarId} before updating it (${e instanceof Error ? e.message : String(e)}). Refusing to write: GHL's calendar update does not merge, so a partial write here would silently reset this calendar's slot length, buffer and availability hours.`
|
|
5198
5222
|
);
|
|
5199
5223
|
}
|
|
5224
|
+
if (existing && existing.calendarType === "event") existing = { ...existing, teamMembers: void 0 };
|
|
5225
|
+
if (existing && args.teamMembers === void 0 && Array.isArray(existing.teamMembers)) {
|
|
5226
|
+
const stripped = existing.teamMembers.map((m) => {
|
|
5227
|
+
const r = m ?? {};
|
|
5228
|
+
const configs = Array.isArray(r.locationConfigurations) ? r.locationConfigurations.map((c) => {
|
|
5229
|
+
const x = c ?? {};
|
|
5230
|
+
return { kind: x.kind, position: x.position, ...typeof x.location === "string" ? { location: x.location } : {} };
|
|
5231
|
+
}) : r.locationConfigurations;
|
|
5232
|
+
return { userId: r.userId, priority: r.priority, isPrimary: r.isPrimary, selected: r.selected, locationConfigurations: configs };
|
|
5233
|
+
});
|
|
5234
|
+
const parsed2 = import_zod12.z.array(CalendarTeamMemberSchema).safeParse(stripped);
|
|
5235
|
+
if (!parsed2.success) {
|
|
5236
|
+
const issue = parsed2.error.issues[0];
|
|
5237
|
+
throw new Error(
|
|
5238
|
+
`update_calendar: this calendar's stored team members cannot be carried over in the shape GoHighLevel's update validates (${issue ? `${issue.path.join(".")}: ${issue.message}` : "unknown"}). Nothing was changed, because a partial write here would unassign the staff. Pass teamMembers explicitly (userId, priority 0/0.5/1, isPrimary, selected, locationConfigurations with at least one entry) and run the update again.`
|
|
5239
|
+
);
|
|
5240
|
+
}
|
|
5241
|
+
existing = { ...existing, teamMembers: parsed2.data };
|
|
5242
|
+
}
|
|
5200
5243
|
const body = buildUpdateCalendarBody(mergeCalendarUpdate(existing, args));
|
|
5201
|
-
|
|
5244
|
+
const updated = await client.put(`/calendars/${args.calendarId}`, { body });
|
|
5245
|
+
let storedTeamMembers;
|
|
5246
|
+
try {
|
|
5247
|
+
const after = await client.get(`/calendars/${args.calendarId}`);
|
|
5248
|
+
const cal = after?.calendar ?? after;
|
|
5249
|
+
storedTeamMembers = Array.isArray(cal?.teamMembers) ? cal.teamMembers.length : 0;
|
|
5250
|
+
} catch (e) {
|
|
5251
|
+
storedTeamMembers = `NOT VERIFIED \u2014 the calendar could not be read back after the update (${e instanceof Error ? e.message : String(e)}), so its team-member assignment is unconfirmed. Run get_calendar to check.`;
|
|
5252
|
+
}
|
|
5253
|
+
return {
|
|
5254
|
+
updated,
|
|
5255
|
+
stored_team_members: storedTeamMembers,
|
|
5256
|
+
stored_team_members_source: "GET /calendars/{id} taken AFTER the update \u2014 a read of what GoHighLevel now holds, not the update's own response."
|
|
5257
|
+
};
|
|
5202
5258
|
}
|
|
5203
5259
|
);
|
|
5204
5260
|
safeTool(
|
|
@@ -5401,7 +5457,13 @@ var init_calendars = __esm({
|
|
|
5401
5457
|
"appointmentPerDay",
|
|
5402
5458
|
"allowBookingAfter",
|
|
5403
5459
|
"allowBookingAfterUnit",
|
|
5404
|
-
"availabilityType"
|
|
5460
|
+
"availabilityType",
|
|
5461
|
+
"teamMembers",
|
|
5462
|
+
"description",
|
|
5463
|
+
"slug",
|
|
5464
|
+
"widgetSlug",
|
|
5465
|
+
"eventTitle",
|
|
5466
|
+
"eventColor"
|
|
5405
5467
|
];
|
|
5406
5468
|
}
|
|
5407
5469
|
});
|
|
@@ -8255,6 +8317,16 @@ function normalizeCrawlStatus(raw) {
|
|
|
8255
8317
|
urls
|
|
8256
8318
|
};
|
|
8257
8319
|
}
|
|
8320
|
+
function trainedOfRequested(s, urlIds) {
|
|
8321
|
+
const bucketOf = new Map(s.urls.map((u) => [u.id, u.bucket]));
|
|
8322
|
+
const isTrained = (id) => TRAINED_BUCKET.test(bucketOf.get(id) ?? "");
|
|
8323
|
+
const pending = urlIds.filter((id) => !isTrained(id));
|
|
8324
|
+
return {
|
|
8325
|
+
trained: urlIds.filter(isTrained),
|
|
8326
|
+
pending,
|
|
8327
|
+
buckets: [...new Set(pending.map((id) => bucketOf.get(id) ?? "not listed"))]
|
|
8328
|
+
};
|
|
8329
|
+
}
|
|
8258
8330
|
async function readCrawlStatus(client, locationId2, knowledgeBaseId, operationId) {
|
|
8259
8331
|
return normalizeCrawlStatus(await client.get("/knowledge-bases/crawler/status", { version: AI_API_VERSION, params: { locationId: locationId2, operationId, knowledgeBaseId } }));
|
|
8260
8332
|
}
|
|
@@ -8306,7 +8378,7 @@ async function crawlAndTrain(client, opts) {
|
|
|
8306
8378
|
const d = await settleCrawl(client, sleep7, opts.locationId, opts.knowledgeBaseId, operationId, opts.waitDiscover ?? 90, (x) => isSettled(x.status), { operationId, status: typeof rec.status === "string" ? rec.status : "Processing", mode, url: opts.url });
|
|
8307
8379
|
if (d.pollError) notes.push(`a status poll failed (${d.pollError})`);
|
|
8308
8380
|
if (!d.settled && d.s.urls.length > 0) notes.push(`${d.s.urls.length} page(s) listed while the operation still read "${d.s.status}" \u2014 the list may be incomplete; the pages below are what was trained`);
|
|
8309
|
-
let trained = 0, trainStatus = "not started", trainSettled = false;
|
|
8381
|
+
let trained = 0, trainedPages = 0, trainStatus = "not started", trainSettled = false;
|
|
8310
8382
|
const chosen = mode === "Exact" ? [] : pickPages(d.s.urls, opts.maxPages ?? 8);
|
|
8311
8383
|
if (mode === "Exact") {
|
|
8312
8384
|
notes.push("Exact mode: the single page trains itself.");
|
|
@@ -8314,17 +8386,23 @@ async function crawlAndTrain(client, opts) {
|
|
|
8314
8386
|
notes.push("no pages were listed to train");
|
|
8315
8387
|
} else {
|
|
8316
8388
|
await client.post("/knowledge-bases/crawler/train", { ...v, body: { locationId: opts.locationId, urlIds: chosen.map((c) => c.id), knowledgeBaseId: opts.knowledgeBaseId, operationId }, noRetry: true });
|
|
8317
|
-
const
|
|
8389
|
+
const chosenIds = chosen.map((c) => c.id);
|
|
8390
|
+
const t = await settleCrawl(client, sleep7, opts.locationId, opts.knowledgeBaseId, operationId, opts.waitTrain ?? 120, (x) => trainSettledOn(x, chosenIds), { operationId, status: "Training" });
|
|
8318
8391
|
trained = t.s.trainedUrlsCount;
|
|
8319
8392
|
trainStatus = t.s.status;
|
|
8320
8393
|
trainSettled = t.settled;
|
|
8394
|
+
trainedPages = trainedOfRequested(t.s, chosenIds).trained.length;
|
|
8321
8395
|
if (t.pollError) notes.push(`a training poll failed (${t.pollError})`);
|
|
8322
|
-
if (
|
|
8396
|
+
else if (wasStopped(t.s.status)) notes.push(`GoHighLevel stopped this operation ("${t.s.status}"), so nothing here confirms these pages trained`);
|
|
8397
|
+
else if (!t.settled) {
|
|
8398
|
+
const { pending, buckets } = trainedOfRequested(t.s, chosenIds);
|
|
8399
|
+
notes.push(`${pending.length} of ${chosenIds.length} page(s) are still not in a "Trained" bucket after ${opts.waitTrain ?? 120}s (operation reads "${t.s.status}"; those pages read ${buckets.join(", ")})`);
|
|
8400
|
+
}
|
|
8323
8401
|
}
|
|
8324
8402
|
const listed = unwrapEnvelope(await client.get("/knowledge-bases/crawler", { ...v, params: { knowledgeBaseId: opts.knowledgeBaseId, locationId: opts.locationId, page: 1, pageLength: 50 } }));
|
|
8325
8403
|
const rows2 = isRecord4(listed) && Array.isArray(listed.urls) ? listed.urls : [];
|
|
8326
8404
|
const pages = rows2.filter(isRecord4).map((r) => ({ id: String(r.id ?? r._id ?? ""), url: String(r.url ?? ""), title: typeof r.title === "string" ? r.title : void 0, content: typeof r.content === "string" ? r.content : void 0 }));
|
|
8327
|
-
return { operationId, discovered: d.s.urls.length, discoverStatus: d.s.status, discoverSettled: d.settled, trained, trainStatus, trainSettled, pages, notes };
|
|
8405
|
+
return { operationId, discovered: d.s.urls.length, discoverStatus: d.s.status, discoverSettled: d.settled, trained, trainedPages, trainStatus, trainSettled, pages, notes };
|
|
8328
8406
|
}
|
|
8329
8407
|
function registerKnowledgeBaseTools(server2, client, deps = {}) {
|
|
8330
8408
|
const sleep7 = deps.sleep ?? realSleep;
|
|
@@ -8431,13 +8509,19 @@ function registerKnowledgeBaseTools(server2, client, deps = {}) {
|
|
|
8431
8509
|
const startStatus = typeof rec.status === "string" ? rec.status : "Processing";
|
|
8432
8510
|
const { s, polls, settled, pollError } = await settle2(loc, knowledgeBaseId, operationId, wait, (x) => isSettled(x.status), { operationId, status: startStatus, mode: option, url });
|
|
8433
8511
|
const listed = s.urls.length;
|
|
8512
|
+
const outcome = crawlOutcome(s.status, settled);
|
|
8434
8513
|
return {
|
|
8435
8514
|
operationId,
|
|
8436
8515
|
...s,
|
|
8437
8516
|
polls,
|
|
8438
8517
|
settled,
|
|
8518
|
+
outcome,
|
|
8439
8519
|
...pollError ? { pollError } : {},
|
|
8440
|
-
next:
|
|
8520
|
+
next: (
|
|
8521
|
+
// The refusal goes first, Exact included: a page that "trains itself"
|
|
8522
|
+
// trains nothing when GoHighLevel stopped the operation.
|
|
8523
|
+
outcome === "stopped" ? `GoHighLevel stopped this crawl (${s.status}) \u2014 it did not finish reading ${url}${listed ? `, and the ${listed} page(s) listed below are whatever it had before it stopped` : ", and nothing was listed"}. Nothing here says the site was read, so do not train from this run: fix what stopped it and crawl again.` : option === "Exact" ? "This page trains itself \u2014 do NOT call train_kb_urls on it (GoHighLevel answers 500). Check list_kb_urls in a moment." : pollError ? `A status poll failed (${pollError}); discovery may still be running \u2014 poll get_kb_crawl_status with operationId "${operationId}".` : settled ? `Discovery finished with ${listed} page(s). Pick the ones the agent should know and pass their ids to train_kb_urls with operationId "${operationId}".` : listed > 0 ? `${listed} page(s) listed so far, but the operation still reads "${s.status}" after ${wait}s, so the list may be incomplete. Train these now with train_kb_urls (operationId "${operationId}"), or poll get_kb_crawl_status for more first.` : `Discovery is still running after ${wait}s with nothing listed yet \u2014 poll get_kb_crawl_status with operationId "${operationId}".`
|
|
8524
|
+
)
|
|
8441
8525
|
};
|
|
8442
8526
|
}
|
|
8443
8527
|
);
|
|
@@ -8457,16 +8541,24 @@ function registerKnowledgeBaseTools(server2, client, deps = {}) {
|
|
|
8457
8541
|
const queued = await client.post("/knowledge-bases/crawler/train", { ...v, body: { locationId: loc, urlIds, knowledgeBaseId, operationId }, noRetry: true });
|
|
8458
8542
|
const wait = waitSeconds ?? 60;
|
|
8459
8543
|
if (wait === 0) return { queued, note: "Queued. Poll get_kb_crawl_status for trainedUrlsCount." };
|
|
8460
|
-
const { s, polls, settled, pollError } = await settle2(loc, knowledgeBaseId, operationId, wait, (x) =>
|
|
8544
|
+
const { s, polls, settled, pollError } = await settle2(loc, knowledgeBaseId, operationId, wait, (x) => trainSettledOn(x, urlIds), { operationId, status: "Training" });
|
|
8545
|
+
const { trained, pending, buckets } = trainedOfRequested(s, urlIds);
|
|
8546
|
+
const outcome = crawlOutcome(s.status, settled);
|
|
8461
8547
|
return {
|
|
8462
8548
|
queued,
|
|
8463
8549
|
status: s.status,
|
|
8550
|
+
outcome,
|
|
8551
|
+
trained,
|
|
8552
|
+
pending,
|
|
8553
|
+
// The operation's own counter, kept because it is what GoHighLevel
|
|
8554
|
+
// reports — it counts every page trained against this operationId,
|
|
8555
|
+
// including earlier train calls, so it is not a result for THIS one.
|
|
8464
8556
|
trainedUrlsCount: s.trainedUrlsCount,
|
|
8465
8557
|
requested: urlIds.length,
|
|
8466
8558
|
polls,
|
|
8467
8559
|
settled,
|
|
8468
8560
|
...pollError ? { pollError } : {},
|
|
8469
|
-
note: pollError ? `Training was queued, then a status poll failed (${pollError}) \u2014 poll get_kb_crawl_status with operationId "${operationId}".` : settled ? `Trained. list_kb_urls shows each page with a link to the text the agent learned.` : `Still training after ${wait}s \u2014
|
|
8561
|
+
note: pollError ? `Training was queued, then a status poll failed (${pollError}) \u2014 poll get_kb_crawl_status with operationId "${operationId}".` : outcome === "stopped" ? `GoHighLevel stopped this operation (${s.status}). ${trained.length} of ${urlIds.length} requested page(s) are in a "Trained" bucket; the other ${pending.length} are not, so the agent has not learned them.` : settled ? `Trained. list_kb_urls shows each page with a link to the text the agent learned.` : `Still training after ${wait}s \u2014 ${pending.length} of ${urlIds.length} requested page(s) are not in a "Trained" bucket yet (they read ${buckets.join(", ")}; the operation reads "${s.status}"). Poll get_kb_crawl_status with operationId "${operationId}".`
|
|
8470
8562
|
};
|
|
8471
8563
|
}
|
|
8472
8564
|
);
|
|
@@ -8573,7 +8665,7 @@ function registerKnowledgeBaseTools(server2, client, deps = {}) {
|
|
|
8573
8665
|
}
|
|
8574
8666
|
);
|
|
8575
8667
|
}
|
|
8576
|
-
var import_zod34, AI_API_VERSION, KB_CAP_PER_LOCATION, realSleep, FINISHED_STATUS, isSettled, PAGE_PRIORITY, HOME_PATH, SKIP;
|
|
8668
|
+
var import_zod34, AI_API_VERSION, KB_CAP_PER_LOCATION, realSleep, FINISHED_STATUS, isSettled, STOPPED_STATUS, wasStopped, crawlOutcome, TRAINED_BUCKET, trainSettledOn, PAGE_PRIORITY, HOME_PATH, SKIP;
|
|
8577
8669
|
var init_knowledge_base = __esm({
|
|
8578
8670
|
"src/tools/knowledge-base.ts"() {
|
|
8579
8671
|
"use strict";
|
|
@@ -8584,6 +8676,11 @@ var init_knowledge_base = __esm({
|
|
|
8584
8676
|
realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8585
8677
|
FINISHED_STATUS = /^(successful|success|failed|failure|existing|restricted|cancelled|canceled|aborted|completed?)$/i;
|
|
8586
8678
|
isSettled = (status) => FINISHED_STATUS.test(status.trim());
|
|
8679
|
+
STOPPED_STATUS = /^(failed|failure|restricted|cancelled|canceled|aborted)$/i;
|
|
8680
|
+
wasStopped = (status) => STOPPED_STATUS.test(status.trim());
|
|
8681
|
+
crawlOutcome = (status, settled) => !settled ? "running" : wasStopped(status) ? "stopped" : "finished";
|
|
8682
|
+
TRAINED_BUCKET = /^trained$/i;
|
|
8683
|
+
trainSettledOn = (s, urlIds) => wasStopped(s.status) || trainedOfRequested(s, urlIds).pending.length === 0;
|
|
8587
8684
|
PAGE_PRIORITY = /about|service|treatment|program|plan|product|menu|special|offer|pricing|price|cost|fee|insurance|contact|location|direction|faq|question|help|hour|book|schedule|appointment|new[- ]?patient|patient|client|team|staff|doctor|provider|review|testimonial/i;
|
|
8588
8685
|
HOME_PATH = /^\/(index(\.html?)?|home(\.html?)?)?\/?$/i;
|
|
8589
8686
|
SKIP = /privacy|terms|cookie|legal|login|sign[- ]?in|cart|checkout|account|unsubscribe|\.(pdf|jpe?g|png|gif|svg|xml|json|css|js)(\?|$)|\/blog\/|\/news\/|\/tag\/|\/category\/|\/author\/|\/page\/\d/i;
|
|
@@ -9260,13 +9357,17 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
|
|
|
9260
9357
|
return {
|
|
9261
9358
|
runId: run.runId,
|
|
9262
9359
|
bot: { id: botId, name: upd.after.name, mode: upd.after.mode, knowledgeBaseIds: upd.after.knowledgeBaseIds, businessName: upd.after.businessName },
|
|
9263
|
-
|
|
9360
|
+
// trainedPages = pages GoHighLevel holds in a Trained bucket; the
|
|
9361
|
+
// operation counter climbs across every train call and is named for
|
|
9362
|
+
// what it is (delta review 3).
|
|
9363
|
+
knowledgeBase: { id: kbId, discovered: crawl.discovered, trainedPages: crawl.trainedPages ?? 0, operationTrainedCount: crawl.trained, learned, notes: crawl.notes },
|
|
9264
9364
|
prospect: { contactId, phone: prospectPhone, fieldsVerified: Object.keys(values).length },
|
|
9265
9365
|
numberToText,
|
|
9266
9366
|
primaryOnSms: primary,
|
|
9267
9367
|
armed: run.armed,
|
|
9268
9368
|
readiness: [
|
|
9269
|
-
|
|
9369
|
+
// Per-page buckets, not the operation counter (sweep review 7).
|
|
9370
|
+
(crawl.trainedPages ?? 0) > 0 ? `the bot has learned ${crawl.trainedPages} page(s)` : "the bot has learned NO pages yet",
|
|
9270
9371
|
armedNow && wasAutoPilot ? "WARNING: the bot was ALREADY on auto-pilot before this demo and answers on its own; end_ai_employee_demo puts it back to auto-pilot, because that is its before-state" : armedNow ? "the bot is on auto-pilot (armed here)" : armAutoPilot ? `WARNING: arming was requested but the bot reads back as "${String(upd.after.mode)}" \u2014 it is NOT answering on its own` : "the bot is NOT armed \u2014 pass armAutoPilot: true to let it answer",
|
|
9271
9372
|
primary === true ? "the bot is primary on SMS" : primary === false ? "the bot is NOT primary on SMS \u2014 it will not answer texts until it is" : "could not read whether the bot is primary on SMS",
|
|
9272
9373
|
numberToText ? `text ${numberToText}` : "this sub-account has no phone number",
|
|
@@ -9803,13 +9904,16 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9803
9904
|
for (const n of held) if (!holders.has(n)) holders.set(n, { agentId: String(a.id), agentName: String(a.agentName), via: `pool ${pool}` });
|
|
9804
9905
|
return { agentId: String(a.id), agentName: String(a.agentName), poolId: pool, poolNumbers: held.length ? held : "unknown \u2014 this API does not list the pool's numbers" };
|
|
9805
9906
|
});
|
|
9806
|
-
const
|
|
9907
|
+
const poolRouting = poolBound.filter((p) => Array.isArray(p.poolNumbers) && p.poolNumbers.length > 0);
|
|
9908
|
+
const poolUnproven = poolBound.filter((p) => typeof p.poolNumbers === "string");
|
|
9909
|
+
const unresolvedPool = poolUnproven.length > 0;
|
|
9807
9910
|
const numberRows = numbers.map((n) => ({ number: n, answeredBy: holders.get(n) ?? null }));
|
|
9808
9911
|
const free = unresolvedPool ? [] : numberRows.filter((r) => !r.answeredBy && !heldUnproven.has(r.number)).map((r) => r.number);
|
|
9809
9912
|
const rota = agents.filter(inBackupRota).map((a) => ({ agentId: String(a.id), agentName: String(a.agentName) }));
|
|
9810
|
-
const ready = numberRows.some((r) => r.answeredBy) ||
|
|
9913
|
+
const ready = numberRows.some((r) => r.answeredBy) || poolRouting.length > 0;
|
|
9811
9914
|
const unprovenLine = unproven.length ? ` ${unproven.length} agent(s) carry a number binding NOT proven to route (${unproven.map((u) => `"${u.agentName}" on ${u.numbers.join(", ")}`).join("; ")}): see unprovenBindings for the fix.` : "";
|
|
9812
|
-
const
|
|
9915
|
+
const poolUnprovenLine = poolUnproven.length ? ` ${poolUnproven.length} agent(s) answer through a number pool this API does not list that pool's numbers for (${poolUnproven.map((p) => `"${p.agentName}" on pool ${p.poolId}`).join("; ")}), so nothing here proves a call lands: open the pool in GoHighLevel and check it holds a number.` : "";
|
|
9916
|
+
const verdict = numbers.length === 0 ? "NOT READY: no phone number on this sub-account." : ready ? `READY: ${numberRows.filter((r) => r.answeredBy).length} number(s) bound to a voice agent${poolRouting.length ? `, and ${poolRouting.length} agent(s) answering through a number pool` : ""}${rota.length ? `; ${rota.length} agent(s) also sit in the backup rota` : ""}.${unprovenLine}${poolUnprovenLine}` : unproven.length || poolUnproven.length ? `NOT PROVEN TO ROUTE: no number is bound in the shape GoHighLevel routes to an agent.${unprovenLine}${poolUnprovenLine}${rota.length ? ` ${rota.length} agent(s) sit in the backup rota (${rota.map((r) => r.agentName).join(", ")}) and may be handed unanswered calls.` : ""}` : `NUMBER PRESENT, NO AGENT BOUND BY API: ${numbers.length} number(s), none carried in inboundNumbers or as numberPoolId by any voice agent.${rota.length ? ` ${rota.length} agent(s) sit in the backup rota (${rota.map((r) => r.agentName).join(", ")}) and may be handed unanswered calls.` : ""} Calls may still reach an agent through the number's own routing, which this API cannot read.`;
|
|
9813
9917
|
return {
|
|
9814
9918
|
locationId: loc,
|
|
9815
9919
|
verdict,
|
|
@@ -9939,6 +10043,7 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9939
10043
|
if (numberPoolId !== void 0) change.numberPoolId = numberPoolId;
|
|
9940
10044
|
if (Object.keys(change).length === 0 && inboundNumber === void 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
|
|
9941
10045
|
const before = await readVoiceAgent(client, loc, agentId);
|
|
10046
|
+
const bindNotes = [];
|
|
9942
10047
|
if (typeof inboundNumber === "string") {
|
|
9943
10048
|
const current = Array.isArray(before.inboundNumbers) ? before.inboundNumbers.filter((n) => typeof n === "string" && n.length > 0) : [];
|
|
9944
10049
|
change.inboundNumbers = [.../* @__PURE__ */ new Set([...current, inboundNumber])];
|
|
@@ -9965,8 +10070,26 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9965
10070
|
const { numbers, pools, poolObjects } = await readNumbers(client, loc);
|
|
9966
10071
|
if (typeof inboundNumber === "string") {
|
|
9967
10072
|
if (!numbers.includes(inboundNumber)) throw new Error(numbers.length ? `${inboundNumber} is not a number on this sub-account. Its numbers: ${numbers.join(", ")}.` : NO_NUMBER_TASK);
|
|
9968
|
-
const
|
|
10073
|
+
const agents = await listAllVoiceAgents(client, loc);
|
|
10074
|
+
const clash = bindingClash(inboundNumber, agents, agentId, poolObjects);
|
|
9969
10075
|
if (clash) throw new Error(clash);
|
|
10076
|
+
const dormant = unprovenNumbers(before).filter((n) => n !== inboundNumber);
|
|
10077
|
+
const kept = [];
|
|
10078
|
+
for (const n of change.inboundNumbers) {
|
|
10079
|
+
if (n === inboundNumber) {
|
|
10080
|
+
kept.push(n);
|
|
10081
|
+
continue;
|
|
10082
|
+
}
|
|
10083
|
+
const holder = agents.find((a) => a.id !== agentId && boundNumbers(a).includes(n));
|
|
10084
|
+
if (holder && dormant.includes(n)) {
|
|
10085
|
+
bindNotes.push(`${n} was held by this agent in the shape not proven to route and now answers for agent "${String(holder.agentName)}" (${String(holder.id)}), so it was left off this agent rather than written onto two.`);
|
|
10086
|
+
continue;
|
|
10087
|
+
}
|
|
10088
|
+
if (holder) bindNotes.push(`${n} also answers for agent "${String(holder.agentName)}" (${String(holder.id)}) in GoHighLevel. It stays on this agent, which was routing it; GoHighLevel decides which of the two takes a call, so unbind it on one of them.`);
|
|
10089
|
+
kept.push(n);
|
|
10090
|
+
if (dormant.includes(n)) bindNotes.push(`${n} was held by this agent in the shape not proven to route (BUG-2026-09-15-02) and starts routing to it with this write.`);
|
|
10091
|
+
}
|
|
10092
|
+
change.inboundNumbers = kept;
|
|
9970
10093
|
}
|
|
9971
10094
|
if (typeof numberPoolId === "string") {
|
|
9972
10095
|
if (!pools.includes(numberPoolId)) throw new Error(pools.length ? `Number pool ${numberPoolId} is not on this sub-account. Its pools: ${pools.join(", ")}.` : "This sub-account has no number pools (list_number_pools).");
|
|
@@ -9991,7 +10114,7 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9991
10114
|
if (!rb.agent) return { sent: Object.keys(change), changed: [], before: {}, agent: void 0, answersOn: [], pool: null, inBackupRota: void 0, readBack: "ASSERTED", differs: void 0, notes: [READBACK_FAILED("update", agentId, rb.error ?? "unknown error")] };
|
|
9992
10115
|
const after = rb.agent;
|
|
9993
10116
|
const changed = diffVoiceAgentSnapshots(before, after);
|
|
9994
|
-
const notes = [];
|
|
10117
|
+
const notes = [...bindNotes];
|
|
9995
10118
|
const differs = agentWriteDifferences(change, after);
|
|
9996
10119
|
for (const k of differs) notes.push(`${k}: sent ${JSON.stringify(change[k])}, reads back ${JSON.stringify(after[k])}.`);
|
|
9997
10120
|
let asserted2 = differs.length > 0;
|
|
@@ -11619,8 +11742,10 @@ ${await response.text()}`);
|
|
|
11619
11742
|
async ({ pageId, content, funnelId, isPublished }) => {
|
|
11620
11743
|
try {
|
|
11621
11744
|
let meta = {};
|
|
11745
|
+
let preReadOk = false;
|
|
11622
11746
|
try {
|
|
11623
11747
|
meta = await funnelRequest("GET", `/page/${pageId}?locationId=${client.locationId}`);
|
|
11748
|
+
preReadOk = true;
|
|
11624
11749
|
} catch (metaErr) {
|
|
11625
11750
|
if (!funnelId) throw metaErr;
|
|
11626
11751
|
}
|
|
@@ -11656,14 +11781,24 @@ ${await response.text()}`);
|
|
|
11656
11781
|
}
|
|
11657
11782
|
const afterVersion = typeof after.pageVersion === "number" ? after.pageVersion : null;
|
|
11658
11783
|
const becameVisible = afterUrl !== "" && afterUrl !== beforeUrl;
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11784
|
+
if (!preReadOk) {
|
|
11785
|
+
const versionMismatch = afterVersion !== null && afterVersion !== pageVersion;
|
|
11786
|
+
save_check = {
|
|
11787
|
+
visible: null,
|
|
11788
|
+
pageVersion_sent: pageVersion,
|
|
11789
|
+
pageVersion_now: afterVersion,
|
|
11790
|
+
warning: "SAVE NOT VERIFIED. The page could not be read before the save, so whether the new version is the one visitors see is not verified \u2014 with no baseline pointer, a page that never moved off its old content is indistinguishable from one that took the save. Read it with get_page_full and compare before reporting it as updated." + (versionMismatch ? ` GoHighLevel now reports this page at pageVersion ${afterVersion} while the save was sent as pageVersion ${pageVersion}: the page already held content this save did not build on, which is exactly the case where GHL stores the write and keeps serving the previous version.` : "")
|
|
11791
|
+
};
|
|
11792
|
+
} else {
|
|
11793
|
+
save_check = {
|
|
11794
|
+
visible: becameVisible,
|
|
11795
|
+
pageVersion_sent: pageVersion,
|
|
11796
|
+
pageVersion_now: afterVersion,
|
|
11797
|
+
...becameVisible ? {} : {
|
|
11798
|
+
warning: "SAVED BUT NOT VISIBLE. GHL accepted the write and stored the content, but the page's live pointer did not move, so visitors still see the previous version. This is GHL's behaviour on a page that already has content, not a transport error. Rebuild on a fresh step with create_funnel_page and save onto that instead \u2014 do NOT report this page as updated."
|
|
11799
|
+
}
|
|
11800
|
+
};
|
|
11801
|
+
}
|
|
11667
11802
|
} catch (verifyErr) {
|
|
11668
11803
|
save_check = {
|
|
11669
11804
|
visible: null,
|
|
@@ -12468,6 +12603,10 @@ function pageFaces(envelope) {
|
|
|
12468
12603
|
function derivePalette(envelope) {
|
|
12469
12604
|
return derivePaletteFromUsage(usageOrHexes(envelope), pageFaces(envelope));
|
|
12470
12605
|
}
|
|
12606
|
+
function paintsAs(literal, from) {
|
|
12607
|
+
if (/^#[0-9a-f]{8}$/i.test(literal)) return { hit: literal.slice(0, 7).toLowerCase() === from, keep: literal.slice(7) };
|
|
12608
|
+
return { hit: normaliseColour(literal)?.toLowerCase() === from, keep: "" };
|
|
12609
|
+
}
|
|
12471
12610
|
function sectionPairs(envelope) {
|
|
12472
12611
|
const groups = /* @__PURE__ */ new Map();
|
|
12473
12612
|
for (const p of renderedPalette(envelope).paints) {
|
|
@@ -12517,28 +12656,47 @@ function rethemeEnvelope(envelope, brand, derived = derivePalette(envelope)) {
|
|
|
12517
12656
|
return out;
|
|
12518
12657
|
};
|
|
12519
12658
|
const sayLoss = (l) => `${l.where}: ${target(l.t).toUpperCase()} type on ${target(l.bg).toUpperCase()} would be ${l.after.toFixed(2)}:1 (it was ${l.before.toFixed(2)}:1); WCAG AA wants 4.5.`;
|
|
12520
|
-
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
for (const
|
|
12524
|
-
|
|
12525
|
-
|
|
12659
|
+
const guardedAtStart = plan.filter((x) => x.guarded).length;
|
|
12660
|
+
for (let pass = 0; pass < guardedAtStart; pass++) {
|
|
12661
|
+
let removed = false;
|
|
12662
|
+
for (const hit of losses()) {
|
|
12663
|
+
for (const step of hit.involved.filter((x) => x.guarded)) {
|
|
12664
|
+
const without = (hex) => hex === step.from ? hex : target(hex);
|
|
12665
|
+
if (contrastHex(without(hit.t), without(hit.bg)) < 4.5) continue;
|
|
12666
|
+
skipped.push(`${step.role} ${step.from}: left as it was, because ${sayLoss(hit)}`);
|
|
12667
|
+
plan.splice(plan.indexOf(step), 1);
|
|
12668
|
+
removed = true;
|
|
12669
|
+
break;
|
|
12670
|
+
}
|
|
12671
|
+
if (removed) break;
|
|
12526
12672
|
}
|
|
12673
|
+
if (!removed) break;
|
|
12527
12674
|
}
|
|
12528
12675
|
const seen = /* @__PURE__ */ new Set();
|
|
12529
12676
|
for (const l of losses()) {
|
|
12530
|
-
const
|
|
12677
|
+
const brandRoles = l.involved.filter((x) => !x.guarded).map((x) => `the ${x.role}`);
|
|
12678
|
+
const guardedRoles = l.involved.filter((x) => x.guarded).map((x) => `the ${x.role}`);
|
|
12679
|
+
const because = [
|
|
12680
|
+
brandRoles.length ? `${brandRoles.join(" and ")} ${brandRoles.length > 1 ? "are" : "is"} the brand's own colour` : "",
|
|
12681
|
+
guardedRoles.length ? `leaving ${guardedRoles.join(" and ")} as it was would not make it readable` : ""
|
|
12682
|
+
].filter(Boolean).join(", and ");
|
|
12683
|
+
const line3 = `${sayLoss(l)} Kept, because ${because}: check this section before a client sees it.`;
|
|
12531
12684
|
if (!seen.has(`${target(l.t)} ${target(l.bg)}`)) warnings.push(line3);
|
|
12532
12685
|
seen.add(`${target(l.t)} ${target(l.bg)}`);
|
|
12533
12686
|
}
|
|
12534
12687
|
let text2 = JSON.stringify(envelope);
|
|
12535
12688
|
const colourTokens = [];
|
|
12536
12689
|
plan.forEach((step, i) => {
|
|
12537
|
-
const re = new RegExp(escapeRe(step.from), "gi");
|
|
12538
|
-
const occurrences = (text2.match(re) ?? []).length;
|
|
12539
|
-
if (occurrences === 0) return;
|
|
12540
12690
|
const token2 = `\0COLOUR${i}\0`;
|
|
12541
|
-
|
|
12691
|
+
let occurrences = 0;
|
|
12692
|
+
const swapped = text2.replace(COLOUR_LITERAL_RE, (lit) => {
|
|
12693
|
+
const { hit, keep } = paintsAs(lit, step.from);
|
|
12694
|
+
if (!hit) return lit;
|
|
12695
|
+
occurrences++;
|
|
12696
|
+
return token2 + keep;
|
|
12697
|
+
});
|
|
12698
|
+
if (occurrences === 0) return;
|
|
12699
|
+
text2 = swapped;
|
|
12542
12700
|
colourTokens.push({ token: token2, to: step.to });
|
|
12543
12701
|
swaps.push({ role: step.role, from: step.from, to: step.to, occurrences });
|
|
12544
12702
|
});
|
|
@@ -12581,7 +12739,7 @@ function derivePaletteAcross(envelopes) {
|
|
|
12581
12739
|
for (const env2 of envelopes) for (const f of pageFaces(env2)) if (!fonts.includes(f)) fonts.push(f);
|
|
12582
12740
|
return derivePaletteFromUsage(pooled2, fonts.slice(0, 2));
|
|
12583
12741
|
}
|
|
12584
|
-
var ICON_FONTS, BRAND_CHROMA, TONE_CHROMA, escapeRe;
|
|
12742
|
+
var ICON_FONTS, BRAND_CHROMA, TONE_CHROMA, escapeRe, COLOUR_LITERAL_RE;
|
|
12585
12743
|
var init_retheme = __esm({
|
|
12586
12744
|
"src/command-os/design/retheme.ts"() {
|
|
12587
12745
|
"use strict";
|
|
@@ -12593,6 +12751,7 @@ var init_retheme = __esm({
|
|
|
12593
12751
|
BRAND_CHROMA = 0.06;
|
|
12594
12752
|
TONE_CHROMA = 0.02;
|
|
12595
12753
|
escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12754
|
+
COLOUR_LITERAL_RE = /#[0-9a-f]{3,8}(?![0-9a-f])|rgba?\([^()]*\)|hsla?\([^()]*\)/gi;
|
|
12596
12755
|
}
|
|
12597
12756
|
});
|
|
12598
12757
|
|
|
@@ -17229,21 +17388,24 @@ ${await response.text()}`);
|
|
|
17229
17388
|
integrations: computeIntegrations({ sections })
|
|
17230
17389
|
};
|
|
17231
17390
|
const saved = await autosaveRequest(pre.pageId, { funnelId, pageVersion: savePageVersion, pageData });
|
|
17232
|
-
const savedDataUrl = typeof saved.pageDataUrl === "string" ? saved.pageDataUrl : null;
|
|
17391
|
+
const savedDataUrl = typeof saved.pageDataUrl === "string" && saved.pageDataUrl.length > 0 ? saved.pageDataUrl : null;
|
|
17233
17392
|
let verified = false;
|
|
17393
|
+
let servedUrl = "";
|
|
17234
17394
|
for (let attempt = 1; attempt <= 8 && !verified; attempt++) {
|
|
17235
17395
|
await new Promise((r) => setTimeout(r, 4e3));
|
|
17236
17396
|
try {
|
|
17237
17397
|
const now = await funnelRequest("GET", `/page/${pre.pageId}?locationId=${client.locationId}`);
|
|
17238
|
-
|
|
17239
|
-
if (savedDataUrl !== null &&
|
|
17398
|
+
servedUrl = typeof now.pageDataDownloadUrl === "string" ? now.pageDataDownloadUrl : "";
|
|
17399
|
+
if (savedDataUrl !== null && servedUrl !== "" && servedUrl !== pre.priorDataUrl) verified = true;
|
|
17240
17400
|
} catch {
|
|
17241
17401
|
}
|
|
17242
17402
|
}
|
|
17243
17403
|
return {
|
|
17244
17404
|
saved,
|
|
17245
17405
|
verified,
|
|
17246
|
-
...verified ? {} : {
|
|
17406
|
+
...verified ? {} : {
|
|
17407
|
+
verifyNote: savedDataUrl === null ? "NOT VERIFIED: GoHighLevel's autosave answered without a pageDataUrl, so this save produced no pointer to confirm against" + (servedUrl === "" ? ", and the page still reports no stored content." : ", though the page does now report stored content.") + " Re-check get_page_full in a minute before publishing; if it never advances, the save did not take." : "The save was accepted but the page has not yet reported the new content (eventual consistency). Re-check get_page_full in a minute before publishing; if it never advances, the save did not take."
|
|
17408
|
+
}
|
|
17247
17409
|
};
|
|
17248
17410
|
}
|
|
17249
17411
|
async function savePageGuarded(input) {
|
|
@@ -20541,6 +20703,10 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20541
20703
|
merge_field_warnings_total: 0,
|
|
20542
20704
|
workflows_with_shape_warnings: 0,
|
|
20543
20705
|
shape_warnings_total: 0,
|
|
20706
|
+
workflows_with_save_blocker_warnings: 0,
|
|
20707
|
+
save_blocker_warnings_total: 0,
|
|
20708
|
+
workflows_with_integration_warnings: 0,
|
|
20709
|
+
integration_warnings_total: 0,
|
|
20544
20710
|
workflows_unscannable: 0,
|
|
20545
20711
|
workflows_zero_references: 0,
|
|
20546
20712
|
// Nothing was scanned, so no reference category was loaded or needed.
|
|
@@ -20551,6 +20717,8 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20551
20717
|
workflows_with_issues: [],
|
|
20552
20718
|
merge_field_warnings: [],
|
|
20553
20719
|
shape_warnings: [],
|
|
20720
|
+
save_blocker_warnings: [],
|
|
20721
|
+
integration_warnings: [],
|
|
20554
20722
|
unscannable: [],
|
|
20555
20723
|
notes: catalog2.complete ? [] : [incompleteCatalogNote(catalog2.stopReason, 0, catalog2.reportedTotal)]
|
|
20556
20724
|
});
|
|
@@ -20587,6 +20755,12 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20587
20755
|
const warningsTotal = withWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
20588
20756
|
const withShapeWarnings = results.filter((r) => r.findings.some((f) => f.severity === "warning" && f.category === "action_shape")).map((r) => ({ workflowId: r.id, workflowName: r.name, status: r.status, warnings: r.findings.filter((f) => f.severity === "warning" && f.category === "action_shape") })).sort((a, b) => (a.status === "published" ? -1 : 1) - (b.status === "published" ? -1 : 1));
|
|
20589
20757
|
const shapeWarningsTotal = withShapeWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
20758
|
+
const SAVE_BLOCKER_CATEGORIES = /* @__PURE__ */ new Set(["legacy_step_id", "legacy_wait_unit", "missing_required_attribute"]);
|
|
20759
|
+
const bucket = (match) => results.filter((r) => r.findings.some(match)).map((r) => ({ workflowId: r.id, workflowName: r.name, status: r.status, warnings: r.findings.filter(match) })).sort((a, b) => (a.status === "published" ? -1 : 1) - (b.status === "published" ? -1 : 1));
|
|
20760
|
+
const withSaveBlockerWarnings = bucket((f) => f.severity === "warning" && SAVE_BLOCKER_CATEGORIES.has(f.category));
|
|
20761
|
+
const saveBlockerWarningsTotal = withSaveBlockerWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
20762
|
+
const withIntegrationWarnings = bucket((f) => f.severity === "warning" && f.category === "missing_integration");
|
|
20763
|
+
const integrationWarningsTotal = withIntegrationWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
20590
20764
|
const unverifiedCats = ALL_CATEGORIES.filter((c) => lookups.status[c] !== "loaded");
|
|
20591
20765
|
const unverifiedRefs = results.reduce((n, r) => n + r.findings.filter((f) => f.severity === "unverified").length, 0);
|
|
20592
20766
|
return jsonResponse({
|
|
@@ -20606,14 +20780,24 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20606
20780
|
merge_field_warnings_total: warningsTotal,
|
|
20607
20781
|
workflows_with_shape_warnings: withShapeWarnings.length,
|
|
20608
20782
|
shape_warnings_total: shapeWarningsTotal,
|
|
20783
|
+
workflows_with_save_blocker_warnings: withSaveBlockerWarnings.length,
|
|
20784
|
+
save_blocker_warnings_total: saveBlockerWarningsTotal,
|
|
20785
|
+
workflows_with_integration_warnings: withIntegrationWarnings.length,
|
|
20786
|
+
integration_warnings_total: integrationWarningsTotal,
|
|
20609
20787
|
workflows_unscannable: unscannable.length,
|
|
20610
20788
|
workflows_zero_references: zeroRefCount,
|
|
20611
20789
|
unverified: { categories_unloaded: unverifiedCats, references_unverified: unverifiedRefs },
|
|
20790
|
+
// Unchanged on purpose: `status` has meant "are there ERRORS" since it
|
|
20791
|
+
// was written, and these four categories are warnings by ruling — the
|
|
20792
|
+
// workflows genuinely still run. What was broken is that they were
|
|
20793
|
+
// invisible, not that they were ranked too low.
|
|
20612
20794
|
status: errorsTotal > 0 ? "issues_found" : "ok"
|
|
20613
20795
|
},
|
|
20614
20796
|
workflows_with_issues: withErrors,
|
|
20615
20797
|
merge_field_warnings: withWarnings,
|
|
20616
20798
|
shape_warnings: withShapeWarnings,
|
|
20799
|
+
save_blocker_warnings: withSaveBlockerWarnings,
|
|
20800
|
+
integration_warnings: withIntegrationWarnings,
|
|
20617
20801
|
unscannable,
|
|
20618
20802
|
notes: [
|
|
20619
20803
|
// NEVER advise re-running for "the next batch": GHL's list endpoint ignores
|
|
@@ -20624,6 +20808,8 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20624
20808
|
...unverifiedCats.length ? [`Could not fully load: ${unverifiedCats.join(", ")} \u2014 references to those are 'unverified', not 'broken'. Re-run.`] : [],
|
|
20625
20809
|
...warningsTotal ? [`${warningsTotal} {{contact.X}} merge tag(s) reference a field that no longer exists \u2014 these render BLANK in the message but do NOT stop the workflow (warning, not a break). See merge_field_warnings.`] : [],
|
|
20626
20810
|
...shapeWarningsTotal ? [`${shapeWarningsTotal} Create/Update Opportunity step(s) have only pipeline + stage \u2014 they cannot create a card for a new contact (see shape_warnings).`] : [],
|
|
20811
|
+
...saveBlockerWarningsTotal ? [`${saveBlockerWarningsTotal} step(s) across ${withSaveBlockerWarnings.length} workflow(s) either stop the workflow being SAVED (a legacy step id, or the singular wait unit GoHighLevel no longer accepts \u2014 it still runs, it just cannot be edited) or save and run while doing nothing (a notification step with nobody selected). See save_blocker_warnings.`] : [],
|
|
20812
|
+
...integrationWarningsTotal ? [`${integrationWarningsTotal} step(s) across ${withIntegrationWarnings.length} workflow(s) need a social account this location has not connected \u2014 they save, publish and run, and do nothing when reached. See integration_warnings.`] : [],
|
|
20627
20813
|
"workflow_goal, goto, and unrecognized condition types are not deeply checked in this version."
|
|
20628
20814
|
]
|
|
20629
20815
|
});
|
|
@@ -27524,6 +27710,7 @@ Use these authoritative normalized intake answers when provided: ${JSON.stringif
|
|
|
27524
27710
|
8. ${STAFF_NOTIFICATION_RULE} In a hand-authored fallback plan, a notification's "to" is that user id, or "" when the account has no users.
|
|
27525
27711
|
9. ${MISSING_STEP_RULE}
|
|
27526
27712
|
10. ${NO_INVENTED_CAUSE_RULE}
|
|
27713
|
+
If apply_build_plan answers phase:"scope" (nothing is confirmed or selected for this client, or the selection changed), STOP: build nothing else and finish with RESULT: {"ok":false,"error":"<its error, word for word>"}.
|
|
27527
27714
|
Finish with EXACTLY one final line:
|
|
27528
27715
|
RESULT: {"ok":true,"summary":"<what was created: counts of pipelines/fields/tags/workflows, and whether the plan came from the preset or the fallback>","issues":["<anything skipped or needing a human, including every notification step waiting for a staff member>"]}` + loadPlanGuide(ctx?.industry)
|
|
27529
27716
|
},
|
|
@@ -31568,7 +31755,7 @@ var require_package = __commonJS({
|
|
|
31568
31755
|
"package.json"(exports2, module2) {
|
|
31569
31756
|
module2.exports = {
|
|
31570
31757
|
name: "@elitedcs/ghl-mcp",
|
|
31571
|
-
version: "3.84.
|
|
31758
|
+
version: "3.84.2",
|
|
31572
31759
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
31573
31760
|
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.",
|
|
31574
31761
|
main: "dist/index.js",
|
|
@@ -35255,8 +35442,8 @@ var init_capability_runs = __esm({
|
|
|
35255
35442
|
staff: ["get_users", "get_user", "get_calendars"],
|
|
35256
35443
|
phone: ["list_phone_numbers", "list_number_pools", "get_location"],
|
|
35257
35444
|
workflows: ["get_workflows", "get_workflow_full", "get_trigger_registry", "get_users", "get_calendars", "get_pipelines", "get_pipeline_full", "get_custom_fields", "get_custom_values", "get_location_tags", "list_email_templates", "list_message_templates", "validate_workflow", "create_workflow", "update_workflow_actions", "audit_workflows"],
|
|
35258
|
-
reactivation: ["get_contact_count", "search_contacts", "get_location_tags", "create_location_tag", "get_workflows", "get_workflow_full", "validate_workflow", "create_workflow", "update_workflow_actions", "create_smart_list", "list_smart_lists"],
|
|
35259
|
-
// no bulk tagging: that is a mass write the operator does by hand
|
|
35445
|
+
reactivation: ["get_contact_count", "search_contacts", "get_location_tags", "create_location_tag", "get_workflows", "get_workflow_full", "validate_workflow", "create_workflow", "update_workflow_actions", "create_smart_list", "list_smart_lists", "create_contact_smart_list", "get_contact_smart_list"],
|
|
35446
|
+
// contact segments live on the contacts service (3.84.2); no bulk tagging: that is a mass write the operator does by hand
|
|
35260
35447
|
templates: ["list_email_templates", "create_email_template", "update_email_template", "list_message_templates", "create_sms_template", "get_custom_values"],
|
|
35261
35448
|
broadcasts: ["list_email_templates", "create_email_template", "update_email_template", "list_smart_lists", "get_email_campaigns"],
|
|
35262
35449
|
// drafts the email as a template; sending or scheduling is the operator's click
|
|
@@ -39987,12 +40174,12 @@ function routeFirebaseForCompany(builderClient, registry2, companyId) {
|
|
|
39987
40174
|
companyId
|
|
39988
40175
|
});
|
|
39989
40176
|
return `
|
|
39990
|
-
Firebase:
|
|
40177
|
+
Firebase: found stored ${cfg.name || `company ${companyId}`} credentials and pointed the workflow builder at them \u2014 not tested this session. To confirm they still work, run register_company_firebase for this company with test_location_id, which makes a real workflow-builder call.`;
|
|
39991
40178
|
}
|
|
39992
40179
|
builderClient.resetToHomeFirebase();
|
|
39993
40180
|
const homeCompanyId = builderClient.getCurrentCompanyId();
|
|
39994
40181
|
if (homeCompanyId === companyId) {
|
|
39995
|
-
return "\nFirebase: home company \u2014 workflow builder
|
|
40182
|
+
return "\nFirebase: this location's company is your home company, so the home credentials apply \u2014 not tested this session. To confirm the workflow builder really works here, run register_company_firebase for this company with test_location_id, which makes a real workflow-builder call.";
|
|
39996
40183
|
}
|
|
39997
40184
|
const registeredCompanies = registry2?.listCompanyFirebases?.() ?? [];
|
|
39998
40185
|
if (registeredCompanies.length > 0) {
|
|
@@ -40223,7 +40410,17 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
|
|
|
40223
40410
|
isError: true
|
|
40224
40411
|
};
|
|
40225
40412
|
}
|
|
40226
|
-
registry2.removeLocation(locationId2);
|
|
40413
|
+
const removed = registry2.removeLocation(locationId2);
|
|
40414
|
+
const saveError = registry2.getLastSaveError();
|
|
40415
|
+
if (!removed || saveError) {
|
|
40416
|
+
return {
|
|
40417
|
+
content: [{
|
|
40418
|
+
type: "text",
|
|
40419
|
+
text: `${token2.name} (${locationId2}) could NOT be removed: this computer's key file could not be written${saveError ? ` (${saveError})` : ""}, so that sub-account's API key is still in it and will be back at the next start. Check storage permissions on the GHL Command app-data folder and run unregister_location again.`
|
|
40420
|
+
}],
|
|
40421
|
+
isError: true
|
|
40422
|
+
};
|
|
40423
|
+
}
|
|
40227
40424
|
return {
|
|
40228
40425
|
content: [{ type: "text", text: `Removed: ${token2.name} (${locationId2}) from token registry.` }]
|
|
40229
40426
|
};
|
|
@@ -40513,11 +40710,13 @@ ${lines.join("\n")}
|
|
|
40513
40710
|
try {
|
|
40514
40711
|
const agencyClient = new GHLClient({ apiKey: agencyKey });
|
|
40515
40712
|
await agencyClient.delete(`/locations/${location_id}`, { params: { deleteTwilioAccount: delete_twilio ?? false } });
|
|
40713
|
+
const hadKey = Boolean(registry2?.getToken(location_id));
|
|
40516
40714
|
const wasRegistered = registry2?.removeLocation(location_id) ?? false;
|
|
40715
|
+
const keyLine = wasRegistered ? " Removed from the token registry." : hadKey ? ` Its API key could NOT be removed from this computer's key file${registry2?.getLastSaveError() ? ` (${registry2.getLastSaveError()})` : ""}; the key is dead with the sub-account, but run unregister_location once the app-data folder is writable so it does not linger.` : "";
|
|
40517
40716
|
return {
|
|
40518
40717
|
content: [{
|
|
40519
40718
|
type: "text",
|
|
40520
|
-
text: `Sub-account ${location_id} permanently deleted.${
|
|
40719
|
+
text: `Sub-account ${location_id} permanently deleted.${keyLine}`
|
|
40521
40720
|
}]
|
|
40522
40721
|
};
|
|
40523
40722
|
} catch (error) {
|
|
@@ -41781,6 +41980,28 @@ function buildContactSmartListBody(args) {
|
|
|
41781
41980
|
locationId: args.locationId
|
|
41782
41981
|
};
|
|
41783
41982
|
}
|
|
41983
|
+
function findSmartListRecord(payload) {
|
|
41984
|
+
const looksLikeRecord = (v) => {
|
|
41985
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
41986
|
+
const o = v;
|
|
41987
|
+
return "objectKey" in o || "filters" in o || "id" in o || "_id" in o || "name" in o;
|
|
41988
|
+
};
|
|
41989
|
+
if (looksLikeRecord(payload)) return payload;
|
|
41990
|
+
if (payload && typeof payload === "object") {
|
|
41991
|
+
for (const key of ["list", "smartList", "smartlist", "data", "record"]) {
|
|
41992
|
+
const nested = payload[key];
|
|
41993
|
+
if (looksLikeRecord(nested)) return nested;
|
|
41994
|
+
}
|
|
41995
|
+
}
|
|
41996
|
+
return void 0;
|
|
41997
|
+
}
|
|
41998
|
+
function contactServiceNote(payload) {
|
|
41999
|
+
const rec = findSmartListRecord(payload);
|
|
42000
|
+
if (!rec) return void 0;
|
|
42001
|
+
const reason2 = rec.objectKey === "contacts" ? 'this record carries objectKey "contacts"' : rec.objectKey === "opportunity" ? void 0 : !("filters" in rec) ? "this record came back with no `filters` key at all" : void 0;
|
|
42002
|
+
if (!reason2) return void 0;
|
|
42003
|
+
return `This id was read from GoHighLevel's OPPORTUNITY smart-list service (/lists/dynamic) and ${reason2}. Contact smart lists do not live here and the Contacts screen never shows a record created here \u2014 create one with create_contact_smart_list, read it with get_contact_smart_list, and delete it with delete_contact_smart_list.`;
|
|
42004
|
+
}
|
|
41784
42005
|
function registerSmartListTools(server2, builderClient) {
|
|
41785
42006
|
if (!builderClient) return;
|
|
41786
42007
|
const client = builderClient;
|
|
@@ -41909,7 +42130,8 @@ ${await res.text()}`);
|
|
|
41909
42130
|
try {
|
|
41910
42131
|
const loc = locationId2 ?? client.locationId;
|
|
41911
42132
|
const result = await smartListRequest("GET", `/${loc}/${listId}`);
|
|
41912
|
-
|
|
42133
|
+
const note = contactServiceNote(result);
|
|
42134
|
+
return note ? jsonResponse({ smart_list: result, note }) : jsonResponse(result);
|
|
41913
42135
|
} catch (error) {
|
|
41914
42136
|
return errorResponse(error);
|
|
41915
42137
|
}
|
|
@@ -41917,10 +42139,10 @@ ${await res.text()}`);
|
|
|
41917
42139
|
);
|
|
41918
42140
|
server2.tool(
|
|
41919
42141
|
"create_smart_list",
|
|
41920
|
-
|
|
42142
|
+
'Create a new OPPORTUNITY smart list (dynamic filter list). objectKey "contacts" is refused here \u2014 this service cannot hold a contact segment; use create_contact_smart_list for that. Required: name + objectKey. Filters define the saved-search criteria \u2014 pass an empty array to create an empty list and add filters later via update_smart_list. The shape of filters/columns is opaque here; query an existing smart list with get_smart_list to see the format GHL expects.',
|
|
41921
42143
|
{
|
|
41922
42144
|
name: import_zod59.z.string().describe("Display name for the smart list."),
|
|
41923
|
-
objectKey: import_zod59.z.enum(OBJECT_KEYS).describe("Object type the list segments over. '
|
|
42145
|
+
objectKey: import_zod59.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'opportunity' \u2014 'contacts' is refused here and is created with create_contact_smart_list instead."),
|
|
41924
42146
|
filters: import_zod59.z.array(import_zod59.z.record(import_zod59.z.string(), import_zod59.z.unknown())).optional().describe("Array of filter objects. Each object has fields like {field, operator, value} \u2014 exact shape varies by filter type. See get_smart_list on an existing list to learn the format."),
|
|
41925
42147
|
columns: import_zod59.z.array(import_zod59.z.record(import_zod59.z.string(), import_zod59.z.unknown())).optional().describe("Array of column definitions for the smart list view in GHL UI. Each defines which contact/opportunity field shows as a column. Defaults to GHL's standard columns if omitted."),
|
|
41926
42148
|
pipelineIds: import_zod59.z.array(import_zod59.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs to restrict this smart list to. Empty array = all pipelines."),
|
|
@@ -41929,6 +42151,11 @@ ${await res.text()}`);
|
|
|
41929
42151
|
},
|
|
41930
42152
|
async ({ name, objectKey, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
|
|
41931
42153
|
try {
|
|
42154
|
+
if (objectKey === "contacts") {
|
|
42155
|
+
throw new Error(
|
|
42156
|
+
"create_smart_list writes to GoHighLevel's opportunity smart-list service, which cannot hold a contact segment \u2014 create a contact smart list with create_contact_smart_list instead."
|
|
42157
|
+
);
|
|
42158
|
+
}
|
|
41932
42159
|
const loc = locationId2 ?? client.locationId;
|
|
41933
42160
|
const body = { name, objectKey };
|
|
41934
42161
|
if (filters !== void 0) body.filters = filters;
|
|
@@ -41966,8 +42193,13 @@ ${await res.text()}`);
|
|
|
41966
42193
|
if (Object.keys(body).length === 0) {
|
|
41967
42194
|
throw new Error("Pass at least one of: name, filters, columns, pipelineIds, defaultInPipelines. Nothing to update.");
|
|
41968
42195
|
}
|
|
42196
|
+
let note;
|
|
42197
|
+
try {
|
|
42198
|
+
note = contactServiceNote(await smartListRequest("GET", `/${loc}/${listId}`));
|
|
42199
|
+
} catch {
|
|
42200
|
+
}
|
|
41969
42201
|
const result = await smartListRequest("PUT", `/${loc}/${listId}`, body);
|
|
41970
|
-
return jsonResponse(result);
|
|
42202
|
+
return note ? jsonResponse({ updated: result, note }) : jsonResponse(result);
|
|
41971
42203
|
} catch (error) {
|
|
41972
42204
|
return errorResponse(error);
|
|
41973
42205
|
}
|
|
@@ -41984,8 +42216,13 @@ ${await res.text()}`);
|
|
|
41984
42216
|
async ({ listId, locationId: locationId2 }) => {
|
|
41985
42217
|
try {
|
|
41986
42218
|
const loc = locationId2 ?? client.locationId;
|
|
42219
|
+
let note;
|
|
42220
|
+
try {
|
|
42221
|
+
note = contactServiceNote(await smartListRequest("GET", `/${loc}/${listId}`));
|
|
42222
|
+
} catch {
|
|
42223
|
+
}
|
|
41987
42224
|
const result = await smartListRequest("DELETE", `/${loc}/${listId}`);
|
|
41988
|
-
return jsonResponse(result);
|
|
42225
|
+
return note ? jsonResponse({ deleted: result, note }) : jsonResponse(result);
|
|
41989
42226
|
} catch (error) {
|
|
41990
42227
|
return errorResponse(error);
|
|
41991
42228
|
}
|
|
@@ -48965,7 +49202,10 @@ var init_verify_findings = __esm({
|
|
|
48965
49202
|
tag: { path: (loc) => `/locations/${loc}/tags`, ids: (raw) => idsOf(raw, "tags"), paged: true },
|
|
48966
49203
|
calendar: { path: () => "/calendars/", params: byLocation, ids: (raw) => idsOf(raw, "calendars") },
|
|
48967
49204
|
form: { path: () => "/forms/", params: byLocation, ids: (raw) => idsOf(raw, "forms"), paged: true },
|
|
48968
|
-
user: { path: () => "/users/", params: byLocation, ids: (raw) => idsOf(raw, "users") }
|
|
49205
|
+
user: { path: () => "/users/", params: byLocation, ids: (raw) => idsOf(raw, "users") },
|
|
49206
|
+
// The list get_surveys reads; it takes limit/skip, so it is read the way
|
|
49207
|
+
// forms are, page by page and never assumed complete (3.84.2).
|
|
49208
|
+
survey: { path: () => "/surveys/", params: byLocation, ids: (raw) => idsOf(raw, "surveys"), paged: true }
|
|
48969
49209
|
};
|
|
48970
49210
|
CHECKABLE_KIND_NAMES = Object.keys(CHECKABLE_KINDS).join(", ");
|
|
48971
49211
|
neverLookedAt = (id) => `"${id}" was never looked at. A step aimed at an unverified id silently kills every action after it, so this is refused rather than attempted.`;
|
|
@@ -49332,12 +49572,13 @@ function scanArgs(args, maxNodes = MAX_ARG_NODES) {
|
|
|
49332
49572
|
continue;
|
|
49333
49573
|
}
|
|
49334
49574
|
if (!isRecord10(v)) continue;
|
|
49335
|
-
const
|
|
49575
|
+
const isNode = typeof v.type === "string" && v.type && (isRecord10(v.attributes) || Array.isArray(v.conditions));
|
|
49576
|
+
const nodeType = isNode ? v.type : parentType;
|
|
49336
49577
|
const refs = [];
|
|
49337
49578
|
if (typeof v.type === "string" && isRecord10(v.attributes)) extractFromAction(v, refs);
|
|
49338
49579
|
if (Array.isArray(v.conditions)) extractFromTrigger(v, refs);
|
|
49339
49580
|
for (const r of refs) {
|
|
49340
|
-
if (r.byKey) continue;
|
|
49581
|
+
if (r.byKey || !r.id.trim()) continue;
|
|
49341
49582
|
const kind = AUDIT_KIND[r.kind];
|
|
49342
49583
|
if (kind) out.ids.push({ kind, id: r.id, key: r.where });
|
|
49343
49584
|
else out.uncheckable.push({ kind: r.kind, id: r.id, key: r.where });
|
|
@@ -49410,6 +49651,8 @@ var init_arg_ids = __esm({
|
|
|
49410
49651
|
formId: "form",
|
|
49411
49652
|
formIds: "form",
|
|
49412
49653
|
// a form_submission goal's extras (build_goal_event)
|
|
49654
|
+
surveyId: "survey",
|
|
49655
|
+
// get_surveys reads /surveys/, so a survey is read back like a form (3.84.2)
|
|
49413
49656
|
userId: "user",
|
|
49414
49657
|
userIds: "user",
|
|
49415
49658
|
assignedUserId: "user",
|
|
@@ -49424,6 +49667,12 @@ var init_arg_ids = __esm({
|
|
|
49424
49667
|
...ID_ARG_KINDS,
|
|
49425
49668
|
"form.id": "form",
|
|
49426
49669
|
"calendar.id": "calendar",
|
|
49670
|
+
// The payment and task triggers name their form, calendar and assignee under
|
|
49671
|
+
// their own prefixes (src/trigger-schemas.ts); unread until 3.84.2.
|
|
49672
|
+
"payment.form.id": "form",
|
|
49673
|
+
"payment.calendar.id": "calendar",
|
|
49674
|
+
"task.assignedTo": "user",
|
|
49675
|
+
"survey.id": "survey",
|
|
49427
49676
|
"opportunity.pipelineId": "pipeline",
|
|
49428
49677
|
"opportunity.pipelineStageId": "pipeline_stage",
|
|
49429
49678
|
"opportunity.assignedTo": "user",
|
|
@@ -49438,7 +49687,7 @@ var init_arg_ids = __esm({
|
|
|
49438
49687
|
workflow: "workflow",
|
|
49439
49688
|
form: "form",
|
|
49440
49689
|
calendar: "calendar",
|
|
49441
|
-
survey:
|
|
49690
|
+
survey: "survey"
|
|
49442
49691
|
});
|
|
49443
49692
|
NOT_CHECKABLE_ID_ARGS = Object.freeze({
|
|
49444
49693
|
// The account itself, not an object inside it. One account per plan is
|
|
@@ -49499,7 +49748,6 @@ var init_arg_ids = __esm({
|
|
|
49499
49748
|
linkId: "trigger links have no read-back list here",
|
|
49500
49749
|
blogId: "blogs have no read-back list here",
|
|
49501
49750
|
categoryId: "categories have no read-back list here",
|
|
49502
|
-
surveyId: "surveys have no read-back list here",
|
|
49503
49751
|
submissionId: "form submissions have no read-back list here",
|
|
49504
49752
|
mediaId: "media files have no read-back list here",
|
|
49505
49753
|
photoAssetId: "media files have no read-back list here",
|
|
@@ -49556,6 +49804,10 @@ var init_arg_ids = __esm({
|
|
|
49556
49804
|
// src/tools/agency-build.ts
|
|
49557
49805
|
function accountMismatch(a, active) {
|
|
49558
49806
|
if (!a.locationId) {
|
|
49807
|
+
const applied = a.applied.filter((s) => s.state === "applied").length;
|
|
49808
|
+
if (applied > 0) {
|
|
49809
|
+
return `This build was saved before builds recorded the sub-account their ids were checked against, and ${applied} of its ${a.steps.length} steps ${applied === 1 ? "is" : "are"} already applied, so nothing can say which account they belong to and re-planning it whole would run those steps twice. Keep the undo notes you have, run abandon_automation to list what is live, then plan only the remaining step${a.steps.length - applied === 1 ? "" : "s"} with plan_automation and approve that plan.`;
|
|
49810
|
+
}
|
|
49559
49811
|
return "This build was saved before builds recorded the sub-account their ids were checked against, so nothing can say which account they belong to: plan it again (plan_automation) and approve the new plan.";
|
|
49560
49812
|
}
|
|
49561
49813
|
if (active === a.locationId) return null;
|
|
@@ -50235,7 +50487,7 @@ function dailyBrief(input) {
|
|
|
50235
50487
|
);
|
|
50236
50488
|
}
|
|
50237
50489
|
}
|
|
50238
|
-
const quiet = !unread.length && !halfRead.length && !today2.length && !waitingOnYou.length && !runningNow.length && !neverStarted.length && !renewals.length && !underFloor.length && !newLeads.length && !payingButQuiet.length && !floorUncheckable.length && !renewalUnknown.length && !yourDay.length;
|
|
50490
|
+
const quiet = !unread.length && !halfRead.length && !today2.length && !waitingOnYou.length && !runningNow.length && !neverStarted.length && !renewals.length && !underFloor.length && !newLeads.length && !payingButQuiet.length && !floorUncheckable.length && !renewalUnknown.length && !yourDay.length && !yourDayNote;
|
|
50239
50491
|
let headline3;
|
|
50240
50492
|
if (today2.length === 1) {
|
|
50241
50493
|
headline3 = `You have one call today \u2014 ${today2[0].who ?? today2[0].client} at ${today2[0].at}.`;
|
|
@@ -50266,6 +50518,8 @@ function dailyBrief(input) {
|
|
|
50266
50518
|
headline3 = `Nothing needs you in what I could read \u2014 but ${unread[0].client} could not be read this morning.`;
|
|
50267
50519
|
} else if (unread.length > 1) {
|
|
50268
50520
|
headline3 = `Nothing needs you in what I could read \u2014 but ${unread.length} accounts could not be read this morning.`;
|
|
50521
|
+
} else if (yourDayNote) {
|
|
50522
|
+
headline3 = "Nothing needs you in what I could read \u2014 but your own calendar could not be read this morning.";
|
|
50269
50523
|
} else if (floorUncheckable.length) {
|
|
50270
50524
|
const c = floorUncheckable[0].client;
|
|
50271
50525
|
headline3 = floorUncheckable.length === 1 ? `Nothing needs you \u2014 but ${c}'s price cannot be checked against any floor, so nothing here can tell you whether they are under it.` : `Nothing needs you \u2014 but ${floorUncheckable.length} clients' prices cannot be checked against any floor, ${c} among them.`;
|
|
@@ -50916,6 +51170,45 @@ function translateWorkflowAudit(audit, inputs = {}) {
|
|
|
50916
51170
|
evidence: [{ tool: "audit_workflows", detail: `${s.shape_warnings_total ?? shapeCount} steps` }]
|
|
50917
51171
|
});
|
|
50918
51172
|
}
|
|
51173
|
+
const blockerCount = s.workflows_with_save_blocker_warnings ?? 0;
|
|
51174
|
+
if (blockerCount > 0) {
|
|
51175
|
+
const rows2 = audit.save_blocker_warnings ?? [];
|
|
51176
|
+
const names = rows2.map((r) => r.workflowName ?? "").filter(Boolean);
|
|
51177
|
+
const cats = new Set(rows2.flatMap((r) => (r.warnings ?? []).map((w) => w.category ?? "")).filter(Boolean));
|
|
51178
|
+
const editBlocked = cats.has("legacy_step_id") || cats.has("legacy_wait_unit");
|
|
51179
|
+
const alertsNobody = cats.has("missing_required_attribute");
|
|
51180
|
+
const unknownKind = !editBlocked && !alertsNobody;
|
|
51181
|
+
const editSentence = ` Some of their steps were built before GoHighLevel tightened what it accepts. The automation runs exactly as it always has; what breaks is CHANGING it \u2014 the save is refused, and the refusal names steps nobody touched, so it reads as unrelated to the edit that triggered it.`;
|
|
51182
|
+
const alertSentence = ` One or more alert steps are set to notify a specific person with nobody chosen. That step saves, publishes and runs, and simply never reaches anyone \u2014 so the automation looks healthy while the person waiting on the alert hears nothing.`;
|
|
51183
|
+
out.push({
|
|
51184
|
+
id: "steps-that-block-saving",
|
|
51185
|
+
title: alertsNobody && !editBlocked ? `${blockerCount} automation${blockerCount === 1 ? " sends" : "s send"} an alert that reaches nobody` : editBlocked && !alertsNobody ? `${blockerCount} automation${blockerCount === 1 ? " cannot" : "s cannot"} be changed without the save being refused` : `${blockerCount} automation${blockerCount === 1 ? " carries a step" : "s carry steps"} that block changes, or alert nobody`,
|
|
51186
|
+
what: `${nameList(names, blockerCount)} ${blockerCount === 1 ? "has" : "have"} at least one step of this kind.` + (editBlocked || unknownKind ? editSentence : "") + (alertsNobody || unknownKind ? alertSentence : ""),
|
|
51187
|
+
severity: "high",
|
|
51188
|
+
impact: alertsNobody && !editBlocked ? "leads" : "efficiency",
|
|
51189
|
+
money: { confidence: "unknown", assumptions: ["Not costed: what this costs depends on which automation and who was waiting on the alert, and we would rather say so than guess."] },
|
|
51190
|
+
remedy: alertsNobody && !editBlocked ? "Pick the person each alert should go to, or set it to notify everyone. Then re-run this check." : editBlocked && !alertsNobody ? "Rebuild those steps so GoHighLevel will accept a save again \u2014 nobody part-way through an automation can be moved while that is done. Then re-run this check." : "Rebuild those steps so GoHighLevel will accept a save again \u2014 nobody part-way through an automation can be moved while that is done \u2014 and pick a person for any alert with nobody selected. Then re-run this check.",
|
|
51191
|
+
fix: "guided",
|
|
51192
|
+
minutes: Math.max(10, blockerCount * 8),
|
|
51193
|
+
evidence: [{ tool: "audit_workflows", detail: `${s.save_blocker_warnings_total ?? blockerCount} steps across ${blockerCount} workflows` }]
|
|
51194
|
+
});
|
|
51195
|
+
}
|
|
51196
|
+
const integrationCount = s.workflows_with_integration_warnings ?? 0;
|
|
51197
|
+
if (integrationCount > 0) {
|
|
51198
|
+
const names = (audit.integration_warnings ?? []).map((r) => r.workflowName ?? "").filter(Boolean);
|
|
51199
|
+
out.push({
|
|
51200
|
+
id: "steps-without-their-account",
|
|
51201
|
+
title: `${integrationCount} automation${integrationCount === 1 ? " has a step" : "s have steps"} waiting on a social account that is not connected`,
|
|
51202
|
+
what: `${nameList(names, integrationCount)} include a step that messages or replies through a social account this location has not connected. The automation saves, publishes and runs; that step simply does nothing when it is reached, and nothing in the history says so.`,
|
|
51203
|
+
severity: "high",
|
|
51204
|
+
impact: "leads",
|
|
51205
|
+
money: { confidence: "unknown", assumptions: ["Not costed: it depends how many people reach that step, which we cannot read."] },
|
|
51206
|
+
remedy: "Connect the account in Settings, then re-run this check. (A connected account with no messaging permission fails the same way and this check cannot see it, so send one test message through the step as well.)",
|
|
51207
|
+
fix: "manual",
|
|
51208
|
+
minutes: Math.max(10, integrationCount * 5),
|
|
51209
|
+
evidence: [{ tool: "audit_workflows", detail: `${s.integration_warnings_total ?? integrationCount} steps` }]
|
|
51210
|
+
});
|
|
51211
|
+
}
|
|
50919
51212
|
return out;
|
|
50920
51213
|
}
|
|
50921
51214
|
function notCheckedFrom(audit) {
|
|
@@ -51655,6 +51948,15 @@ function publishNote(r) {
|
|
|
51655
51948
|
if (r.publishFailed.length) {
|
|
51656
51949
|
const lead = r.published.length ? `Auto-published ${r.published.length} workflow(s) live, but ` : "";
|
|
51657
51950
|
const detail = r.publishFailed.map((p) => `${p.name} (${p.error})`).join("; ");
|
|
51951
|
+
const inert = r.publishFailed.filter((p) => p.kind === "inert"), unverified = r.publishFailed.filter((p) => p.kind === "not_verified");
|
|
51952
|
+
const drafts = r.publishFailed.filter((p) => p.kind !== "inert" && p.kind !== "not_verified");
|
|
51953
|
+
if (!drafts.length) {
|
|
51954
|
+
const parts = [
|
|
51955
|
+
inert.length ? `${inert.length} workflow(s) PUBLISHED but with every trigger still inactive, so nothing enrols contacts into them: ${inert.map((p) => `${p.name} (${p.error})`).join("; ")}. Open each in GHL and check its trigger.` : "",
|
|
51956
|
+
unverified.length ? `${unverified.length} workflow(s) had the publish accepted but it could not be confirmed: ${unverified.map((p) => `${p.name} (${p.error})`).join("; ")}. Open each in GHL and check it shows Published before reporting it as running.` : ""
|
|
51957
|
+
].filter(Boolean).join(" ");
|
|
51958
|
+
return `${lead}${parts}${r.published.length ? " Any gated workflow stays DRAFT until its handoff is met." : ""}`;
|
|
51959
|
+
}
|
|
51658
51960
|
return `${lead}${r.publishFailed.length} workflow(s) were BUILT BUT COULD NOT BE PUBLISHED. They exist in GHL as DRAFT and are NOT running: ${detail}. You already opted in, so re-running with publishWorkflows will not help. If GHL rejected the workflow's contents, publishing by hand will hit the SAME error until the cause is fixed. Read the error above, fix it, then publish (see nextManualSteps).${r.published.length ? " Any gated workflow stays DRAFT until its handoff is met." : ""}`;
|
|
51659
51961
|
}
|
|
51660
51962
|
if (r.published.length) {
|
|
@@ -51668,6 +51970,8 @@ function publishNote(r) {
|
|
|
51668
51970
|
function publishSummaryClause(r) {
|
|
51669
51971
|
if (r.publishFailed.length) {
|
|
51670
51972
|
const also = r.published.length ? ` (${r.published.length} did publish)` : "";
|
|
51973
|
+
const drafts = r.publishFailed.filter((p) => p.kind !== "inert" && p.kind !== "not_verified").length;
|
|
51974
|
+
if (!drafts) return ` ${r.publishFailed.length} workflow(s) published but NOT confirmed running${also} (inactive trigger or unconfirmed publish), see publishNote.`;
|
|
51671
51975
|
return ` ${r.publishFailed.length} workflow(s) BUILT BUT NOT PUBLISHED${also}. They are DRAFT and not running, see publishNote.`;
|
|
51672
51976
|
}
|
|
51673
51977
|
if (r.published.length) return ` Published ${r.published.length} workflow(s) live.`;
|
|
@@ -52864,8 +53168,14 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
52864
53168
|
}
|
|
52865
53169
|
if (opts.publishWorkflows && !opts.gatedWorkflowRefs?.has(wf.ref)) {
|
|
52866
53170
|
try {
|
|
52867
|
-
await deps.publishWorkflow(wfId);
|
|
52868
|
-
|
|
53171
|
+
const verdict = publishVerdict(await deps.publishWorkflow(wfId));
|
|
53172
|
+
if (!verdict.live) {
|
|
53173
|
+
publishFailed.push({ ref: wf.ref, name: wf.name, error: verdict.reason, kind: verdict.kind });
|
|
53174
|
+
manual.push({ ref: wf.ref, type: "workflow-publish", name: wf.name, reason: `[${wf.name}] ${verdict.kind === "inert" ? `published, but every trigger on it still reads inactive, so nothing enrols contacts into it \u2014 open the workflow in GHL and check its trigger (${verdict.reason})` : verdict.kind === "not_verified" ? `the publish was accepted but could not be confirmed (${verdict.reason}) \u2014 open the workflow in GHL and check it shows Published` : `built successfully but not live (${verdict.reason}) \u2014 publish it manually in GHL`}.` });
|
|
53175
|
+
} else {
|
|
53176
|
+
published.push(wf.ref);
|
|
53177
|
+
if (verdict.note) manual.push({ ref: wf.ref, type: "workflow-trigger", name: wf.name, reason: `[${wf.name}] published, but ${verdict.note}` });
|
|
53178
|
+
}
|
|
52869
53179
|
} catch (e) {
|
|
52870
53180
|
publishFailed.push({ ref: wf.ref, name: wf.name, error: msg2(e) });
|
|
52871
53181
|
manual.push({ ref: wf.ref, type: "workflow-publish", name: wf.name, reason: `[${wf.name}] built successfully but auto-publish failed (${msg2(e)}) \u2014 publish it manually in GHL.` });
|
|
@@ -52985,6 +53295,21 @@ async function createWithReadback(create, read2, name, beforeIds, poll, wait) {
|
|
|
52985
53295
|
function msg2(e) {
|
|
52986
53296
|
return accountErrorText(e);
|
|
52987
53297
|
}
|
|
53298
|
+
function publishVerdict(outcome) {
|
|
53299
|
+
if (typeof outcome !== "object" || outcome === null) return { live: true };
|
|
53300
|
+
const check = outcome.publish_check;
|
|
53301
|
+
if (typeof check !== "object" || check === null) return { live: true };
|
|
53302
|
+
const c = check;
|
|
53303
|
+
const warning = typeof c.warning === "string" && c.warning ? c.warning : "";
|
|
53304
|
+
if (warning.startsWith("NOT VERIFIED")) return { live: false, kind: "not_verified", reason: warning };
|
|
53305
|
+
if (warning.startsWith("TRIGGER NOT ACTIVE")) {
|
|
53306
|
+
const active = typeof c.triggers_active === "number" ? c.triggers_active : 0;
|
|
53307
|
+
return active > 0 ? { live: true, note: warning } : { live: false, kind: "inert", reason: warning };
|
|
53308
|
+
}
|
|
53309
|
+
if (c.published === true) return { live: true };
|
|
53310
|
+
const status = typeof c.status_returned === "string" ? c.status_returned : "(GHL returned no status)";
|
|
53311
|
+
return { live: false, kind: "not_live", reason: warning || `NOT LIVE. The workflow read back as ${status} rather than "published", so it is not running.` };
|
|
53312
|
+
}
|
|
52988
53313
|
var norm5, sleep5, PREVIEW_SNIPPET_LENGTH, JournalHookError;
|
|
52989
53314
|
var init_execute = __esm({
|
|
52990
53315
|
"src/intake-to-build/execute.ts"() {
|
|
@@ -53549,9 +53874,12 @@ function makeExecuteDeps(client, builderClient, locationId2, registry2) {
|
|
|
53549
53874
|
deleteWorkflow: async (workflowId) => {
|
|
53550
53875
|
await builderClient.deleteWorkflow(workflowId);
|
|
53551
53876
|
},
|
|
53552
|
-
|
|
53553
|
-
|
|
53554
|
-
|
|
53877
|
+
// The result is RETURNED, not awaited-and-dropped. It carries the client's
|
|
53878
|
+
// own `publish_check` — the status read back from the SAVED workflow after
|
|
53879
|
+
// the write, with a NOT LIVE / NOT VERIFIED sentence when the publish did
|
|
53880
|
+
// not land. Discarding it is what let a workflow left in Draft be counted
|
|
53881
|
+
// into "Published N workflow(s) live"; the executor now reads it.
|
|
53882
|
+
publishWorkflow: (workflowId) => builderClient.publishWorkflow(workflowId)
|
|
53555
53883
|
};
|
|
53556
53884
|
}
|
|
53557
53885
|
function makeRevertDeps(client, builderClient, locationId2) {
|
|
@@ -53759,7 +54087,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
53759
54087
|
if (saved2?.wholePreset) {
|
|
53760
54088
|
scopeSource = "cockpit";
|
|
53761
54089
|
} else if (!saved2 && Array.isArray(board.assessmentPurchases?.[activeLocation])) {
|
|
53762
|
-
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error:
|
|
54090
|
+
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error: notConfirmedAsk(activeLocation) });
|
|
53763
54091
|
} else if (saved2 && Array.isArray(saved2.features)) {
|
|
53764
54092
|
const features = saved2.features.filter(isFeatureId);
|
|
53765
54093
|
if (!features.length) {
|
|
@@ -53817,11 +54145,11 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
53817
54145
|
if (!gate2.ok) return jsonResponse({ ok: false, phase: "plan", error: gate2.error, savedPlan: saved ? { planId: saved.planId, savedAt: saved.savedAt } : void 0 });
|
|
53818
54146
|
const plan = gate2.plan;
|
|
53819
54147
|
const planSource = gate2.source;
|
|
53820
|
-
if (
|
|
54148
|
+
if (!fromPreset) {
|
|
53821
54149
|
const board = readBoardState();
|
|
53822
54150
|
const current = board.scope?.[activeLocation];
|
|
53823
54151
|
if (!current && Array.isArray(board.assessmentPurchases?.[activeLocation])) {
|
|
53824
|
-
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error:
|
|
54152
|
+
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error: notConfirmedAsk(activeLocation) });
|
|
53825
54153
|
}
|
|
53826
54154
|
if (current && !current.wholePreset && Array.isArray(current.features) && !current.features.some(isFeatureId)) {
|
|
53827
54155
|
return jsonResponse({
|
|
@@ -53831,7 +54159,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
53831
54159
|
error: "No features are selected for this client, so nothing was built, not even from the saved plan. Open the client's Review screen, tick the features to build, confirm, and run the build again."
|
|
53832
54160
|
});
|
|
53833
54161
|
}
|
|
53834
|
-
if (!savedPlanMatchesScope(saved, current)) {
|
|
54162
|
+
if (saved && planSource !== "argument-replaces-saved" && !savedPlanMatchesScope(saved, current)) {
|
|
53835
54163
|
const planned = saved?.scope ? [...saved.scope.built, ...(saved.scope.omitted ?? []).map((f) => f.id)].join(", ") || "no features" : "everything in the preset";
|
|
53836
54164
|
const now = current?.wholePreset ? "everything in the preset (confirmed)" : current ? current.features.filter(isFeatureId).join(", ") : "everything in the preset (nothing is saved)";
|
|
53837
54165
|
return jsonResponse({
|
|
@@ -54523,7 +54851,7 @@ function intakeFormMatches(expected, actual) {
|
|
|
54523
54851
|
const keys = ["id", "tag", "type", "hiddenFieldQueryKey", "required", "label", "options", "picklistOptions"];
|
|
54524
54852
|
return expected.every((field, i) => keys.every((key) => field[key] === void 0 || JSON.stringify(field[key]) === JSON.stringify(actual[i]?.[key])));
|
|
54525
54853
|
}
|
|
54526
|
-
var import_zod81, customFieldItemSchema, sleep6, NOT_CONFIRMED_SCOPE;
|
|
54854
|
+
var import_zod81, customFieldItemSchema, sleep6, NOT_CONFIRMED_SCOPE, WHOLE_PRESET_NOT_KEPT, notConfirmedAsk;
|
|
54527
54855
|
var init_intake_to_build = __esm({
|
|
54528
54856
|
"src/tools/intake-to-build.ts"() {
|
|
54529
54857
|
"use strict";
|
|
@@ -54570,6 +54898,12 @@ var init_intake_to_build = __esm({
|
|
|
54570
54898
|
}).passthrough();
|
|
54571
54899
|
sleep6 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
54572
54900
|
NOT_CONFIRMED_SCOPE = "What this client bought is recorded, but nobody has confirmed what to build, so nothing was built. Open the client's Review screen: the features are ticked from what they bought. Change them if you need to, save to confirm, and run the build again.";
|
|
54901
|
+
WHOLE_PRESET_NOT_KEPT = "Note: an earlier build on this client used everything in the preset, and that choice was not kept, so the Review screen ticks only what they bought. Tick everything again if that is still what you want.";
|
|
54902
|
+
notConfirmedAsk = (locationId2) => {
|
|
54903
|
+
const sp = readSavedPlan(locationId2);
|
|
54904
|
+
const fromPreset = typeof sp?.plan?.preset === "string";
|
|
54905
|
+
return sp && !sp.scope && fromPreset ? `${NOT_CONFIRMED_SCOPE} ${WHOLE_PRESET_NOT_KEPT}` : NOT_CONFIRMED_SCOPE;
|
|
54906
|
+
};
|
|
54573
54907
|
}
|
|
54574
54908
|
});
|
|
54575
54909
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.84.
|
|
3
|
+
"version": "3.84.2",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
5
|
"description": "GoHighLevel MCP Server for Claude. 250 tools — full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
6
6
|
"main": "dist/index.js",
|