@elitedcs/ghl-mcp 3.84.1 → 3.84.3
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 +67 -0
- package/README.md +1 -1
- package/dist/index.js +957 -650
- package/package.json +1 -1
- package/skills/blueprint/SKILL.md +7 -0
- package/skills/blueprint/references/agency-os-detection.md +3 -3
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 };
|
|
@@ -3053,7 +3077,7 @@ function commandOsUpgradeMessage(toolName) {
|
|
|
3053
3077
|
"Everything else works exactly as it does now: audits, builds, fixes, Blueprint and multi-location are all part of what you have."
|
|
3054
3078
|
].join("\n");
|
|
3055
3079
|
}
|
|
3056
|
-
var COMMAND_OS_ONLY_TOOLS, COMMAND_OS_ONLY_MODULES,
|
|
3080
|
+
var COMMAND_OS_ONLY_TOOLS, COMMAND_OS_ONLY_MODULES, COMMAND_OS_DESC_SUFFIX;
|
|
3057
3081
|
var init_product_manifest = __esm({
|
|
3058
3082
|
"src/product-manifest.ts"() {
|
|
3059
3083
|
"use strict";
|
|
@@ -3084,489 +3108,6 @@ var init_product_manifest = __esm({
|
|
|
3084
3108
|
// ruling, same lane; none of its 12 tools has ever shipped.
|
|
3085
3109
|
"voice-ai"
|
|
3086
3110
|
]);
|
|
3087
|
-
COMMAND_OS_CATALOGUE = [
|
|
3088
|
-
{
|
|
3089
|
-
name: "connect_google_calendar",
|
|
3090
|
-
does: "Connects your own Google Calendar with read-only access. The first run opens Google sign-in and saves nothing. The second run saves the connection only after reading your calendars and tells you which ones it found. You can check the connection using only the last four characters or disconnect and revoke access.",
|
|
3091
|
-
why: "Command OS connects your own systems to help you run your business. The $97 GHL Command product manages client accounts in GoHighLevel and does not connect your personal Google Calendar.",
|
|
3092
|
-
layer: "Layer 2 \u2014 Data"
|
|
3093
|
-
},
|
|
3094
|
-
{
|
|
3095
|
-
name: "read_my_calendar",
|
|
3096
|
-
does: "Reads today or the week ahead from your own Google Calendar. Each entry shows its time in your timezone and the other people attending. All-day entries say all day. An empty window says so. An unknown calendar name returns the calendars you can read.",
|
|
3097
|
-
why: "Command OS helps you plan your own day across your business using your personal calendar. The $97 GHL Command product reads appointments inside a client's GoHighLevel account.",
|
|
3098
|
-
layer: "Layer 2 \u2014 Data"
|
|
3099
|
-
},
|
|
3100
|
-
{
|
|
3101
|
-
name: "extract_brand",
|
|
3102
|
-
does: "Reads a website and writes down its brand: the six colours, the two typefaces, the corner radius, the logo and the tone of the headlines, each with where it came from and a confidence score. The client's own site by default; a rendered pass in the automation browser when the page is built by scripts.",
|
|
3103
|
-
why: "It reads a website the agency does not own and stores the brand on the agency's machine for every deck, page and report that follows. Nothing in the $97 product reads a brand or keeps one.",
|
|
3104
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3105
|
-
},
|
|
3106
|
-
{
|
|
3107
|
-
name: "import_reference_look",
|
|
3108
|
-
does: "Fetches one of the reference looks (Apple, Notion, Stripe and about seventy more) onto this machine as a starting palette: colours, type and shape only, never a logo, never the reference brand's name in the output. The client's own logo and accent then override it.",
|
|
3109
|
-
why: "A starting look for a client with no website yet is agency design work, not GoHighLevel account work; the $97 product has nowhere to keep one.",
|
|
3110
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3111
|
-
},
|
|
3112
|
-
{
|
|
3113
|
-
name: "list_brands",
|
|
3114
|
-
does: "Lists every brand on this machine \u2014 extracted from client sites, imported reference looks and the house looks \u2014 with scheme, accent, faces and confidence, plus the reference looks that can be imported.",
|
|
3115
|
-
why: "The brand shelf is the agency's, across every client. Nothing in the $97 product has one.",
|
|
3116
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3117
|
-
},
|
|
3118
|
-
{
|
|
3119
|
-
name: "get_brand",
|
|
3120
|
-
does: "Reads one brand in full: colours with their OKLCH twins and contrast-corrected text shades, faces with what actually renders when a face is licensed, logo, voice, provenance for every value.",
|
|
3121
|
-
why: "It is the record every design job reads from; it lives with the agency, not in any client's GoHighLevel.",
|
|
3122
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3123
|
-
},
|
|
3124
|
-
{
|
|
3125
|
-
name: "set_brand",
|
|
3126
|
-
does: "Changes one or more values on a stored brand \u2014 a new accent, a font swap, a corrected name \u2014 and records who changed it and when in the brand's provenance. Never invents a value; a field left out is left alone.",
|
|
3127
|
-
why: "Editing the agency's own brand shelf is agency work; the $97 product keeps no brand to edit.",
|
|
3128
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3129
|
-
},
|
|
3130
|
-
{
|
|
3131
|
-
name: "compose_design_brief",
|
|
3132
|
-
does: "Turns a brand plus a short brief (headline, points, audience, the one job) into the pack your own Claude writes a deck, a landing page or a report from: the brand tokens ready to paste, the rules it will be graded on with their thresholds, the output contract, and the sameness tells as banned. Calls no model itself.",
|
|
3133
|
-
why: "The brief is the whole point of Design Studio: brand times rules, written once so every design job starts from the same place. Nothing in the $97 product composes one.",
|
|
3134
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3135
|
-
},
|
|
3136
|
-
{
|
|
3137
|
-
name: "lint_design_contract",
|
|
3138
|
-
does: "Checks a written HTML file honours the output contract before it is graded: the section and role markers are present and consistent, no outside scripts, one h1 on a page, every image described. Returns the fix list in plain words; a file that fails is not graded on design rules.",
|
|
3139
|
-
why: "It is the gate between your Claude's draft and the verifier; it exists only because the verifier does.",
|
|
3140
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3141
|
-
},
|
|
3142
|
-
{
|
|
3143
|
-
name: "verify_design",
|
|
3144
|
-
does: "Opens a finished deck or page in the automation browser and measures it against the design rules: type sizes, contrast, line length, spacing, safe zones, whitespace and accent share from real pixels, landmarks, forms, the meta layer, and the sameness tells. Every rule comes back on its own line as measured, a proxy or a judgment call, with the value and the evidence; nothing is hidden. Writes a report you can open.",
|
|
3145
|
-
why: "This is the piece the free skill it was modelled on never had: proof instead of a checklist the model ticks itself. Nothing in the $97 product renders or measures a design.",
|
|
3146
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3147
|
-
},
|
|
3148
|
-
{
|
|
3149
|
-
name: "grade_copy",
|
|
3150
|
-
does: "Grades whether a page would actually persuade the reader it is for, scoring the four jobs copy must do in reading order \u2014 stop the right prospect, make them feel understood, earn belief, make the next step obvious \u2014 and naming the FIRST place they fall off, with a rewrite of that one thing.",
|
|
3151
|
-
why: "Every design rule in the module measures craft and none of them asks whether anyone would care; a page can pass all of them and be unsellable. This is the other half, and it runs on the operator's own Claude subscription.",
|
|
3152
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3153
|
-
},
|
|
3154
|
-
{
|
|
3155
|
-
name: "check_page_theme",
|
|
3156
|
-
does: "Reads a GoHighLevel page back after Page Studio saved it and measures whether the brand actually landed: each of the six slots must be present in the saved data and the donor template's own colour or face must be gone. Confirms the page's public preview address by fetching it, so the design rules can be graded on the real draft.",
|
|
3157
|
-
why: "A snapshot image cannot show a half-finished swap, and a 200 from a save is not evidence. This is the read-back that turns \u201Cthe draft wears the brand\u201D from a claim into a measurement.",
|
|
3158
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3159
|
-
},
|
|
3160
|
-
{
|
|
3161
|
-
name: "brand_to_theme",
|
|
3162
|
-
does: "Hands Page Studio the six-slot theme a stored brand maps to \u2014 accent, soft tint, surface, text, heading face, body face \u2014 with the warnings about what the donor template cannot carry (a dark scheme, a licensed face). Page Studio's own tools then write the draft.",
|
|
3163
|
-
why: "It joins the agency's brand shelf to the page writer the $97 product already has; the shelf is the Command OS half.",
|
|
3164
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3165
|
-
},
|
|
3166
|
-
{
|
|
3167
|
-
name: "get_agency_profile",
|
|
3168
|
-
does: "Reads your agency's own details \u2014 the brand clients see, the offers you sell and the price below which you do not take the work, who your messages come from, your delivery SOPs, and your staff.",
|
|
3169
|
-
why: "The agency's own facts are the thing Command OS adds. GHL Command works on a client's account; this describes the business doing the work.",
|
|
3170
|
-
layer: "Layer 1 \u2014 Context"
|
|
3171
|
-
},
|
|
3172
|
-
{
|
|
3173
|
-
name: "set_agency_profile",
|
|
3174
|
-
does: "Saves or updates those details. Send only what is changing; anything you leave out stays as it was, and passing nothing for a field clears just that field.",
|
|
3175
|
-
why: "Same store as the read. Writing the agency's own facts is not part of managing a client's GoHighLevel account.",
|
|
3176
|
-
layer: "Layer 1 \u2014 Context"
|
|
3177
|
-
},
|
|
3178
|
-
{
|
|
3179
|
-
name: "get_income_sources",
|
|
3180
|
-
does: "Reads every source of income you have recorded \u2014 clients whose GoHighLevel accounts you run, software subscribers, event fees \u2014 the offer they are on, what they pay, monthly or one-time, when it started and renews, who runs the account \u2014 plus your recurring revenue stated as a floor, with the number of income sources that have no price recorded.",
|
|
3181
|
-
why: "A roster of who pays you and for what is agency data, not GoHighLevel data. Nothing in the $97 product has ever had it.",
|
|
3182
|
-
layer: "Layer 1 \u2014 Context"
|
|
3183
|
-
},
|
|
3184
|
-
{
|
|
3185
|
-
name: "set_income_source",
|
|
3186
|
-
does: "Records or updates ONE source of income \u2014 a client whose GoHighLevel account you run, a software subscriber, an event fee. Safe to call repeatedly \u2014 it never overwrites a field you did not mention.",
|
|
3187
|
-
why: "As above; it is the write half of the same agency-side record.",
|
|
3188
|
-
layer: "Layer 1 \u2014 Context"
|
|
3189
|
-
},
|
|
3190
|
-
{
|
|
3191
|
-
name: "client_this_week",
|
|
3192
|
-
does: "Answers \u201Cwhat should I be doing for this client this week?\u201D from what you already have: the agreement, where their build has stopped, what is waiting on you rather than on Command OS, a renewal that is close, whether the price is under your own floor, and the SOP that applies. Anything it cannot know, it says plainly instead of guessing.",
|
|
3193
|
-
why: "It only works because the agency profile and the client roster exist, and both are Command OS. It is the win the PRD names for this layer.",
|
|
3194
|
-
layer: "Layer 1 \u2014 Context"
|
|
3195
|
-
},
|
|
3196
|
-
{
|
|
3197
|
-
name: "build_audit_report",
|
|
3198
|
-
does: "Turns an audit you have already run into a document you can put in front of a client: what is broken in plain English, what it is costing, ranked so the quick wins come first. Every number carries the assumption behind it, and no figure exists without your own close rate and client value.",
|
|
3199
|
-
why: "Finding the problem is GHL Command and stays free-tier. Packaging it as a deliverable an agency charges for, priced from the agency's own numbers, is the agency product.",
|
|
3200
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3201
|
-
},
|
|
3202
|
-
{
|
|
3203
|
-
name: "daily_brief",
|
|
3204
|
-
does: "Answers \u201Cwhat needs me today?\u201D across every client at once: today\u2019s appointments first, then what is waiting on you, what Command OS is running, renewals inside thirty days, clients paying under your own floor, and new leads. Any account it could not read is named with the reason, so \u201Cnothing needs you\u201D only ever covers the accounts it actually got into. It ends by naming each client\u2019s email addresses and website and asking your own Gmail, Google Calendar and Slack, through the connectors your assistant already has, which client mail is waiting on you and what your own day holds.",
|
|
3205
|
-
why: "GHL Command answers questions about the ONE sub-account you are already looking at. Nothing in the $97 product reads every client together, and nothing in it knows your price floor or your renewal dates to have an opinion about what matters this morning.",
|
|
3206
|
-
layer: "Layer 3 \u2014 Intelligence"
|
|
3207
|
-
},
|
|
3208
|
-
{
|
|
3209
|
-
name: "build_site_audit_report",
|
|
3210
|
-
does: "Turns a read-through of a PROSPECT\u2019s website into the assessment document you put in front of them: what contradicts itself, what a customer cannot find, each one carrying the quote and the page it came from, ranked. It never invents money, and it returns what the crawl refused so you can see what was thrown out rather than wonder what was missed.",
|
|
3211
|
-
why: "This is the paid deliverable, and it is about a business whose GoHighLevel account you do not have \u2014 there is nothing in the $97 product that reads a stranger\u2019s website or knows the rule that your own price floor must never price their findings.",
|
|
3212
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3213
|
-
},
|
|
3214
|
-
{
|
|
3215
|
-
name: "save_assessment",
|
|
3216
|
-
does: "Saves an assessment into your own GoHighLevel: the prospect as a contact, the whole answer sheet as a note you can read in plain English, and the deal as an opportunity on your Assessments pipeline. If the record is too long for one note it is split and reassembled on load, never truncated.",
|
|
3217
|
-
why: "It is your business's filing cabinet, not ours \u2014 we hold nothing. That is also what lets a remote employee run the assessment on Tuesday and the owner read it on Wednesday, using the GoHighLevel logins and permissions they already have.",
|
|
3218
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3219
|
-
},
|
|
3220
|
-
{
|
|
3221
|
-
name: "load_assessment",
|
|
3222
|
-
does: "Loads a saved assessment back off a contact so somebody else can pick it up, returning the most recent COMPLETE answer sheet. If a note was edited or a piece is missing it returns nothing at all rather than a partial record.",
|
|
3223
|
-
why: "A half-loaded assessment overwriting a real one is the worst thing this could do to an agency, so refusing is the feature. Nothing in the $97 product stores an interview or knows what a complete one looks like.",
|
|
3224
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3225
|
-
},
|
|
3226
|
-
{
|
|
3227
|
-
name: "attach_assessment",
|
|
3228
|
-
does: "Joins a saved assessment to a client, so the numbers from the sales conversation follow them into the intake, the Review and their Blueprint. It fills only the answers the assessment actually holds, never puts a free-text answer into a fixed-choice question, and prices a feature in \u201CAvailable next\u201D only where the assessment priced it. A snapshot, taken when you attach; nothing is written to the client's account.",
|
|
3229
|
-
why: "It is the join between the agency's own sales conversation and the account it then builds, and both halves are Command OS. The $97 product has no assessment, no Review and no Blueprint to carry a number into.",
|
|
3230
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3231
|
-
},
|
|
3232
|
-
{
|
|
3233
|
-
name: "list_assessments",
|
|
3234
|
-
does: "Lists the assessments on your Assessments pipeline: who each is for, what stage it is at, and what the deal is worth.",
|
|
3235
|
-
why: "This is the agency owner's board and the list a remote employee picks their next job from. It reads across your own prospects, which is the agency's business rather than any one client's account.",
|
|
3236
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3237
|
-
},
|
|
3238
|
-
{
|
|
3239
|
-
name: "read_assessment_transcript",
|
|
3240
|
-
does: "Turns a recording of the meeting into assessment answers. Every answer has to carry the owner's own words, and it checks those words actually appear in the transcript \u2014 anything citing a sentence that is not there is thrown out. Anything worked out rather than heard, like two or three calls a week becoming eleven a month, is held back for you to confirm.",
|
|
3241
|
-
why: "It is the guard between a recording and a document you hand a prospect. Nothing in the $97 product reads a conversation, and nothing in it has a reason to refuse a number that sounds right.",
|
|
3242
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3243
|
-
},
|
|
3244
|
-
{
|
|
3245
|
-
name: "write_assessment_report",
|
|
3246
|
-
does: "Writes the finished assessment as a complete page on your own machine, in your branding, ready to hand to the prospect. It loads nothing from anywhere, so it opens on any host in any browser; it is marked not-to-be-indexed and given a random filename, because it carries another business's revenue and customer numbers.",
|
|
3247
|
-
why: "We never publish it. It is your prospect's data on your letterhead, so where it goes is yours to decide \u2014 and nothing in the $97 product produces a client-facing document at all.",
|
|
3248
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3249
|
-
},
|
|
3250
|
-
{
|
|
3251
|
-
name: "audit_my_delivery",
|
|
3252
|
-
does: "Audits the operator\u2019s OWN agency rather than a client: how many hours a month each recurring job costs them, ranked worst first, with the ones Command OS can already take off them named and the ones nothing handles said plainly. A job nobody can put minutes against is named as needing measuring and never estimated, and the answers save between sittings.",
|
|
3253
|
-
why: "It is a diagnosis of the agency\u2019s own week. The $97 product manages one GoHighLevel account and has no concept of an agency\u2019s delivery, no list of what the agency itself does, and nothing to weigh it against \u2014 and the \u201Chandled by\u201D column only means anything to somebody who has the modules it names.",
|
|
3254
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3255
|
-
},
|
|
3256
|
-
{
|
|
3257
|
-
name: "plan_automation",
|
|
3258
|
-
does: "Plans one of the agency\u2019s OWN automations out of the tools they already have. It refuses a plan that targets an id nobody looked at, a plan whose steps cannot run in any order, a step naming a tool that does not exist, and a step with no reason on it \u2014 all before the operator is asked to approve anything. It writes nothing itself.",
|
|
3259
|
-
why: "The $97 product runs tools one at a time against the account in front of it. It has no concept of a multi-step build belonging to an AGENCY, no record of what was explored, and nothing that could refuse a plan on the operator\u2019s behalf.",
|
|
3260
|
-
layer: "Layer 5 \u2014 Build"
|
|
3261
|
-
},
|
|
3262
|
-
{
|
|
3263
|
-
name: "approve_automation",
|
|
3264
|
-
does: "Records that the operator has read this exact plan and approved it. The approval carries the plan\u2019s fingerprint, so changing a single argument afterwards voids it \u2014 the plan they read and the plan that runs have to be the same document. Nothing may be recorded as applied before this.",
|
|
3265
|
-
why: "This is the agency\u2019s own approval gate over its own build, not an action inside one client\u2019s account. Nothing in the $97 product has a plan to approve.",
|
|
3266
|
-
layer: "Layer 5 \u2014 Build"
|
|
3267
|
-
},
|
|
3268
|
-
{
|
|
3269
|
-
name: "record_automation_step",
|
|
3270
|
-
does: "Records what happened when a step ran: applied, with how to undo it, or failed, with what the account actually said. A step cannot be recorded as done without its undo, and cannot be recorded before the thing it depends on has run.",
|
|
3271
|
-
why: "It maintains the agency\u2019s own rollback record across a build that runs over days. The $97 product has no build that spans days and nothing that would hold the undo list.",
|
|
3272
|
-
layer: "Layer 5 \u2014 Build"
|
|
3273
|
-
},
|
|
3274
|
-
{
|
|
3275
|
-
name: "abandon_automation",
|
|
3276
|
-
does: "Stops one of the agency\u2019s own builds for good. It does not undo anything and does not pretend to \u2014 it hands back every step still applied to the account, so abandoning a plan and cleaning up after it stay two separate acts. A reason is required.",
|
|
3277
|
-
why: "It closes out the agency\u2019s own build record. The $97 product has no build to abandon and nothing that would know what was left behind.",
|
|
3278
|
-
layer: "Layer 5 \u2014 Build"
|
|
3279
|
-
},
|
|
3280
|
-
{
|
|
3281
|
-
name: "automation_status",
|
|
3282
|
-
does: "Where the agency\u2019s own builds actually are. A build that stopped is reported as STALLED, naming the step and what came back, never as a percentage \u2014 and it hands back the undo list newest first.",
|
|
3283
|
-
why: "It reports across the agency\u2019s builds rather than inside one client\u2019s account, and the $97 product has nothing whose progress it could report.",
|
|
3284
|
-
layer: "Layer 5 \u2014 Build"
|
|
3285
|
-
},
|
|
3286
|
-
{
|
|
3287
|
-
name: "connect_system",
|
|
3288
|
-
does: "Stores a connector's credentials on YOUR machine so Command OS can read that system: Stripe for who pays you what and when, Slack for delivering the brief and reading what is waiting. Written beside your GoHighLevel credentials, readable only by you, never sent anywhere and never returned by any tool.",
|
|
3289
|
-
why: "The $97 product manages one GoHighLevel account. Reading your Stripe or your Slack is about the agency's own business, not a client's account.",
|
|
3290
|
-
layer: "Layer 2 \u2014 Data"
|
|
3291
|
-
},
|
|
3292
|
-
{
|
|
3293
|
-
name: "post_to_slack",
|
|
3294
|
-
does: "Posts a message to one of your Slack channels, so the morning brief or a build that stopped lands where you already look instead of waiting to be asked for. Public channels need no invite; a private one needs the bot invited first, and the reply says so.",
|
|
3295
|
-
why: "It delivers the agency-wide brief into the agency's own Slack. The $97 product has no brief to deliver and no idea of a system outside the one GoHighLevel account it is pointed at.",
|
|
3296
|
-
layer: "Layer 3 \u2014 Intelligence"
|
|
3297
|
-
},
|
|
3298
|
-
{
|
|
3299
|
-
name: "read_slack",
|
|
3300
|
-
does: "Shows what is waiting unanswered in Slack: messages from people in the channels the bot has been invited to that nobody replied to, newest first, naming which channels it read and which it could not.",
|
|
3301
|
-
why: "It reads the agency's own team channels to feed the morning brief. Nothing in the $97 product reads Slack or has a brief to feed.",
|
|
3302
|
-
layer: "Layer 2 \u2014 Data"
|
|
3303
|
-
},
|
|
3304
|
-
{
|
|
3305
|
-
name: "list_connected_systems",
|
|
3306
|
-
does: "Which systems Command OS can read on this machine, and when each was connected. Never shows a key.",
|
|
3307
|
-
why: "It reports on the agency's own stack rather than a client's account, and the $97 product has no concept of a system beyond the one GoHighLevel account it is pointed at.",
|
|
3308
|
-
layer: "Layer 2 \u2014 Data"
|
|
3309
|
-
},
|
|
3310
|
-
{
|
|
3311
|
-
name: "disconnect_system",
|
|
3312
|
-
does: "Removes a connector's credentials from this machine. Anything already written into your income records stays.",
|
|
3313
|
-
why: "It removes access to the agency's own billing system, which is a thing the $97 product never had access to in the first place.",
|
|
3314
|
-
layer: "Layer 2 \u2014 Data"
|
|
3315
|
-
},
|
|
3316
|
-
{
|
|
3317
|
-
name: "sync_stripe",
|
|
3318
|
-
does: "Reads your Stripe and fills in what your records do not know: what each person or company pays you, when they renew, and who owes you money. Handles clients whose GoHighLevel accounts you run and people who simply pay you, like software subscribers and event fees. Shows you every change before making any, and reports a customer it cannot match to one of your clients rather than guessing.",
|
|
3319
|
-
why: "This is the record your daily brief already reads and finds empty. Nothing in the $97 product reads your billing, and nothing in it knows which of your clients a payment belongs to.",
|
|
3320
|
-
layer: "Layer 2 \u2014 Data"
|
|
3321
|
-
},
|
|
3322
|
-
{
|
|
3323
|
-
name: "list_knowledge_bases",
|
|
3324
|
-
does: "Lists the knowledge bases on the sub-account you are pointed at, with the count against GoHighLevel's limit of fifteen per sub-account, so you know there is room before you make one.",
|
|
3325
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3326
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3327
|
-
},
|
|
3328
|
-
{
|
|
3329
|
-
name: "get_knowledge_base",
|
|
3330
|
-
does: "Reads one knowledge base by id: its name, description and dates.",
|
|
3331
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3332
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3333
|
-
},
|
|
3334
|
-
{
|
|
3335
|
-
name: "create_knowledge_base",
|
|
3336
|
-
does: "Creates an empty knowledge base with a name and a description of what it will hold. Nothing a client sees changes until a bot reads from it.",
|
|
3337
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3338
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3339
|
-
},
|
|
3340
|
-
{
|
|
3341
|
-
name: "update_knowledge_base",
|
|
3342
|
-
does: "Renames or re-describes an existing knowledge base. What it has learned is untouched.",
|
|
3343
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3344
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3345
|
-
},
|
|
3346
|
-
{
|
|
3347
|
-
name: "delete_knowledge_base",
|
|
3348
|
-
does: "Deletes a knowledge base and everything in it. GoHighLevel has no undo, so the literal word DELETE is required, and any bot still listing the id keeps listing it and simply knows less.",
|
|
3349
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3350
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3351
|
-
},
|
|
3352
|
-
{
|
|
3353
|
-
name: "crawl_kb_website",
|
|
3354
|
-
does: "Reads a website for a knowledge base. Exact mode trains that one page at once. Path and Domain discover the pages under a path or the whole site and train nothing until you name them. It waits for discovery and returns the page list, saying plainly when a large site may still be listing.",
|
|
3355
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3356
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3357
|
-
},
|
|
3358
|
-
{
|
|
3359
|
-
name: "train_kb_urls",
|
|
3360
|
-
does: "Trains the pages you named from a discovery and waits until GoHighLevel reports them learned, so the next step never runs against a page that is still pending.",
|
|
3361
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3362
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3363
|
-
},
|
|
3364
|
-
{
|
|
3365
|
-
name: "get_kb_crawl_status",
|
|
3366
|
-
does: "Reports where a crawl stands: pages discovered, trained and pending, and whether it has settled or is still working.",
|
|
3367
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3368
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3369
|
-
},
|
|
3370
|
-
{
|
|
3371
|
-
name: "list_kb_urls",
|
|
3372
|
-
does: "Lists every trained page in a knowledge base, each with a link to the exact text the bot extracted from it. That link is the answer to what does it know, read before anything is promised.",
|
|
3373
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3374
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3375
|
-
},
|
|
3376
|
-
{
|
|
3377
|
-
name: "delete_kb_url",
|
|
3378
|
-
does: "Removes trained pages from a knowledge base so the bot stops answering from them. The literal word DELETE is required.",
|
|
3379
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3380
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3381
|
-
},
|
|
3382
|
-
{
|
|
3383
|
-
name: "list_kb_faqs",
|
|
3384
|
-
does: "Lists the written question-and-answer pairs in a knowledge base.",
|
|
3385
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3386
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3387
|
-
},
|
|
3388
|
-
{
|
|
3389
|
-
name: "create_kb_faq",
|
|
3390
|
-
does: "Adds one question and its answer to a knowledge base, word for word as the bot should give it.",
|
|
3391
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3392
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3393
|
-
},
|
|
3394
|
-
{
|
|
3395
|
-
name: "update_kb_faq",
|
|
3396
|
-
does: "Replaces one written answer in a knowledge base with new wording; every other answer and every trained page stays as it was.",
|
|
3397
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3398
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3399
|
-
},
|
|
3400
|
-
{
|
|
3401
|
-
name: "delete_kb_faq",
|
|
3402
|
-
does: "Removes one written answer from a knowledge base. The literal word DELETE is required.",
|
|
3403
|
-
why: "A knowledge base is what an AI employee is allowed to know. Nothing in the $97 product reads, makes or trains one; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3404
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3405
|
-
},
|
|
3406
|
-
{
|
|
3407
|
-
name: "list_conversation_agents",
|
|
3408
|
-
does: "Lists the Conversation AI agents on the sub-account: name, mode, channels and which bot is primary on each channel.",
|
|
3409
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3410
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3411
|
-
},
|
|
3412
|
-
{
|
|
3413
|
-
name: "get_conversation_agent",
|
|
3414
|
-
does: "Reads one agent in full: personality, goal, instructions, knowledge bases, channels, mode, wait and sleep settings.",
|
|
3415
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3416
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3417
|
-
},
|
|
3418
|
-
{
|
|
3419
|
-
name: "create_conversation_agent",
|
|
3420
|
-
does: "Builds an agent from your words: its name, who it is, what every conversation is for, the rules, the channels it may work on and the knowledge bases it may read. It is created in suggestive mode, drafting for a human, or switched off; auto-pilot is never offered at creation.",
|
|
3421
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3422
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3423
|
-
},
|
|
3424
|
-
{
|
|
3425
|
-
name: "update_conversation_agent",
|
|
3426
|
-
does: "Changes an agent in place. It reads the agent first and sends the whole prompt back with your change, because GoHighLevel requires personality, goal and instructions on every update. Auto-pilot is switched on here; nothing checks that anyone watched the bot first, so that discipline is yours.",
|
|
3427
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3428
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3429
|
-
},
|
|
3430
|
-
{
|
|
3431
|
-
name: "delete_conversation_agent",
|
|
3432
|
-
does: "Deletes an agent and its actions. Irreversible; the literal word DELETE is required. If it was primary on a channel, that channel has no bot afterwards.",
|
|
3433
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3434
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3435
|
-
},
|
|
3436
|
-
{
|
|
3437
|
-
name: "list_conversation_actions",
|
|
3438
|
-
does: "Lists an agent's actions: what it may do beyond replying, such as booking, handing to a person, firing a workflow, following up, or stopping.",
|
|
3439
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3440
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3441
|
-
},
|
|
3442
|
-
{
|
|
3443
|
-
name: "create_conversation_action",
|
|
3444
|
-
does: "Adds one action to an agent: book into a calendar, hand to a named person, fire a workflow, update a contact field, follow up later, transfer to another bot, or stop for good. It refuses the shapes GoHighLevel's server fails on, such as a handover with nobody to hand to, before anything is written.",
|
|
3445
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3446
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3447
|
-
},
|
|
3448
|
-
{
|
|
3449
|
-
name: "update_conversation_action",
|
|
3450
|
-
does: "Changes one of an agent's actions in place, with the same refusals as creating one.",
|
|
3451
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3452
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3453
|
-
},
|
|
3454
|
-
{
|
|
3455
|
-
name: "delete_conversation_action",
|
|
3456
|
-
does: "Removes an action from an agent. The literal word DELETE is required. A goodbye that stops the bot cannot be removed; GoHighLevel refuses, and it leaves with the agent.",
|
|
3457
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3458
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3459
|
-
},
|
|
3460
|
-
{
|
|
3461
|
-
name: "update_conversation_followup",
|
|
3462
|
-
does: "Sets how an agent's follow-ups behave: whether a follow-up may move to another channel, whether it respects working hours and which hours, and whether the contact's or the business's timezone applies. It applies to follow-up actions the bot already has.",
|
|
3463
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3464
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3465
|
-
},
|
|
3466
|
-
{
|
|
3467
|
-
name: "get_agent_generations",
|
|
3468
|
-
does: "Shows why the bot said what it said, for one message: the exact prompt it ran, the intent it read, its reply, which written answers and website chunks it used, the actions it took and the history it saw. This is the proof that it answered correctly.",
|
|
3469
|
-
why: "The bot itself. Nothing in the $97 product builds, changes or reads a Conversation AI agent; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02, so it never moves the $97 price.",
|
|
3470
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3471
|
-
},
|
|
3472
|
-
{
|
|
3473
|
-
name: "install_ai_employee_demo",
|
|
3474
|
-
does: "Sets up the agency's demo sub-account once: creates the demo contact fields the demo bot's prompt reads, creates the demo bot or adopts one you already have, records which number prospects will text, and tells you the one click left, making that bot primary on SMS in GoHighLevel's screen. It asks for the words DEMO ACCOUNT in writing, your statement that this is not a client's account, refuses a key that can list sub-accounts (an agency-level key) and refuses to guess when that check cannot be made, so the designation is made with the account's own key, and records a fingerprint of that key; that designation and that key are what every other demo tool checks: a sub-account that was never installed is refused before anything is read or written, even when it is the active one.",
|
|
3475
|
-
why: "The sales demo of the AI employee, run on the agency's own demo sub-account. Nothing in the $97 product prepares a demo for a prospect or puts a bot back afterwards; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02.",
|
|
3476
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3477
|
-
},
|
|
3478
|
-
{
|
|
3479
|
-
name: "prepare_ai_employee_demo",
|
|
3480
|
-
does: "Prepares a demo for one prospect from their website, their business name and the phone they will text from. It crawls their site into a knowledge base made for this run, writes the demo fields onto their contact and reads them back, saves the demo bot's before-state, points the bot at the knowledge base, and returns the number to text. Say arm it and the bot answers on its own; otherwise the bot's mode is left exactly as it was, and the tool reports what that is: silent, drafting, or already on auto-pilot and answering, with a warning in that last case.",
|
|
3481
|
-
why: "The sales demo of the AI employee, run on the agency's own demo sub-account. Nothing in the $97 product prepares a demo for a prospect or puts a bot back afterwards; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02.",
|
|
3482
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3483
|
-
},
|
|
3484
|
-
{
|
|
3485
|
-
name: "show_ai_employee_demo",
|
|
3486
|
-
does: "Shows the demo conversation as it happens, after proving the key in hand reads the demo sub-account and is the very key the account was designated with (any other key, another sub-account's or an agency-level one swapped in for a moment, is refused by comparison): the most recent messages of this run (ten unless you ask for more, up to forty), the prospect's texts and the bot's replies, oldest first, bound to the one contact the demo was prepared for; anything dated before the run began or after it ended is left out, with a fifteen-second allowance for clock differences, and it says how many were left out on each side, so end the demo after the last reply has arrived; a message with no date is listed separately as unplaced, never inside the run.",
|
|
3487
|
-
why: "The sales demo of the AI employee, run on the agency's own demo sub-account. Nothing in the $97 product prepares a demo for a prospect or puts a bot back afterwards; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02.",
|
|
3488
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3489
|
-
},
|
|
3490
|
-
{
|
|
3491
|
-
name: "end_ai_employee_demo",
|
|
3492
|
-
does: "Ends a demo: puts the demo bot back to its saved before-state, reads it back independently and compares every field that changes behaviour, deletes the knowledge base made for the run, and closes the run. The prospect's contact and the transcript stay. The literal word END is required.",
|
|
3493
|
-
why: "The sales demo of the AI employee, run on the agency's own demo sub-account. Nothing in the $97 product prepares a demo for a prospect or puts a bot back afterwards; the AI employee is Command OS work from the first step, on Jerry's ruling of 2026-09-02.",
|
|
3494
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3495
|
-
},
|
|
3496
|
-
{
|
|
3497
|
-
name: "check_voice_ai_readiness",
|
|
3498
|
-
does: "Answers one question before any phone agent is built: can a call land on this sub-account? It reads the numbers it owns, which agent answers each, the number pools and the agents, and when nothing can ring it hands back the exact step to take in GoHighLevel.",
|
|
3499
|
-
why: "A phone agent with no number is a prompt nobody can call, and GoHighLevel will let you build one all day. Nothing in the $97 product reads a Voice AI agent; the AI employee is Command OS work on Jerry's ruling of 2026-09-02.",
|
|
3500
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3501
|
-
},
|
|
3502
|
-
{
|
|
3503
|
-
name: "list_voice_agents",
|
|
3504
|
-
does: "Lists the phone agents on a sub-account: name, the business each speaks for, the number each answers on, its language, its call cap and how many things it may do mid-call.",
|
|
3505
|
-
why: "The survey before a build: which agent already answers which number. The $97 product has no view of Voice AI at all.",
|
|
3506
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3507
|
-
},
|
|
3508
|
-
{
|
|
3509
|
-
name: "get_voice_agent",
|
|
3510
|
-
does: "Reads one phone agent in full: its greeting, its prompt, its voice and language, how quickly it speaks, the call cap, the number it answers on, what happens after a call, its working hours and every action with its settings.",
|
|
3511
|
-
why: "The stored truth of a phone agent, including the generic prompt GoHighLevel writes when nobody gave one. Command OS only, with the rest of the AI employee.",
|
|
3512
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3513
|
-
},
|
|
3514
|
-
{
|
|
3515
|
-
name: "create_voice_agent",
|
|
3516
|
-
does: "Builds a phone agent from your words: its name, the business, the greeting it speaks, its prompt, voice, language, patience, call cap, working hours and what runs when a call ends. It is born without a number, so it cannot take a call until you bind one, and it is kept out of the fallback rota while you are still building it.",
|
|
3517
|
-
why: "The phone half of the AI employee. Every default here is chosen so a half-built agent cannot pick up a real call; the $97 product builds no agent of any kind.",
|
|
3518
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3519
|
-
},
|
|
3520
|
-
{
|
|
3521
|
-
name: "update_voice_agent",
|
|
3522
|
-
does: "Changes any part of a phone agent, including binding it to a number this sub-account owns that no other agent answers on, or unbinding it. Sends only what changes, reads the agent back and reports the fields that actually differ.",
|
|
3523
|
-
why: "Binding a number is the moment an agent goes live to callers, and GoHighLevel will silently let a second agent take a number away from the first. This refuses that. Command OS only.",
|
|
3524
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3525
|
-
},
|
|
3526
|
-
{
|
|
3527
|
-
name: "delete_voice_agent",
|
|
3528
|
-
does: "Removes a phone agent and everything it could do, after saying which number it was answering. Irreversible; the literal DELETE is required.",
|
|
3529
|
-
why: "Disposal of a Command OS asset; the $97 product never created one.",
|
|
3530
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3531
|
-
},
|
|
3532
|
-
{
|
|
3533
|
-
name: "get_voice_action",
|
|
3534
|
-
does: "Reads one thing a phone agent may do mid-call, with its settings: the transfer number, the text it sends, the field it fills, the calendar it books, the workflow it starts, the knowledge base it asks or the API it calls.",
|
|
3535
|
-
why: "Part of the phone agent's build surface, which exists only in Command OS.",
|
|
3536
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3537
|
-
},
|
|
3538
|
-
{
|
|
3539
|
-
name: "create_voice_action",
|
|
3540
|
-
does: "Gives a phone agent something it may do mid-call: transfer to a person, text the caller, fill a contact field from what was said, book on a calendar, start a workflow, answer from a knowledge base, or call an outside API. Checks the name and the shape first, refuses a draft workflow and a second extraction into a field the agent already fills, and refuses the two settings that crash GoHighLevel's server; an unknown calendar, knowledge base or field id is refused by GoHighLevel itself, which names it.",
|
|
3541
|
-
why: "Wiring an agent to a calendar, a workflow or a person is the part that books, fires or hands off for real. GoHighLevel refuses an unknown id but accepts a draft workflow that will never run, and saves a crashed custom action anyway; this catches both. Command OS only.",
|
|
3542
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3543
|
-
},
|
|
3544
|
-
{
|
|
3545
|
-
name: "update_voice_action",
|
|
3546
|
-
does: "Replaces what one mid-call action does, with the same checks as creating one. GoHighLevel takes the whole action on update, so the complete settings travel with every change.",
|
|
3547
|
-
why: "Re-pointing what an agent does on a live line; Command OS only.",
|
|
3548
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3549
|
-
},
|
|
3550
|
-
{
|
|
3551
|
-
name: "delete_voice_action",
|
|
3552
|
-
does: "Removes one thing a phone agent may do. The agent keeps answering without it. The literal DELETE is required.",
|
|
3553
|
-
why: "Part of the phone agent's build surface; Command OS only.",
|
|
3554
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3555
|
-
},
|
|
3556
|
-
{
|
|
3557
|
-
name: "get_voice_call_logs",
|
|
3558
|
-
does: "Lists the calls an agent took, each with its transcript, summary, length, the caller's number, the actions it ran and the data it pulled out. Filter by agent, caller, live or test calls, a date window or action type.",
|
|
3559
|
-
why: "The proof. Not that the agent exists, but the words it said to a real caller, read back. Nothing in the $97 product reads a call log.",
|
|
3560
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3561
|
-
},
|
|
3562
|
-
{
|
|
3563
|
-
name: "get_voice_call_log",
|
|
3564
|
-
does: "One call in full: the transcript line by line, the summary, the length, who called, each action with the moment it fired, and the data extracted.",
|
|
3565
|
-
why: "What you show a client instead of saying it works. Command OS only.",
|
|
3566
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3567
|
-
}
|
|
3568
|
-
];
|
|
3569
|
-
CATALOGUED_TOOLS = new Set(COMMAND_OS_CATALOGUE.map((t) => t.name));
|
|
3570
3111
|
COMMAND_OS_DESC_SUFFIX = " [NOT INCLUDED IN THIS LICENCE \u2014 calling this returns a notice, it will NOT perform the action.]";
|
|
3571
3112
|
}
|
|
3572
3113
|
});
|
|
@@ -5197,8 +4738,40 @@ function registerCalendarTools(server2, client) {
|
|
|
5197
4738
|
`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
4739
|
);
|
|
5199
4740
|
}
|
|
4741
|
+
if (existing && existing.calendarType === "event") existing = { ...existing, teamMembers: void 0 };
|
|
4742
|
+
if (existing && args.teamMembers === void 0 && Array.isArray(existing.teamMembers)) {
|
|
4743
|
+
const stripped = existing.teamMembers.map((m) => {
|
|
4744
|
+
const r = m ?? {};
|
|
4745
|
+
const configs = Array.isArray(r.locationConfigurations) ? r.locationConfigurations.map((c) => {
|
|
4746
|
+
const x = c ?? {};
|
|
4747
|
+
return { kind: x.kind, position: x.position, ...typeof x.location === "string" ? { location: x.location } : {} };
|
|
4748
|
+
}) : r.locationConfigurations;
|
|
4749
|
+
return { userId: r.userId, priority: r.priority, isPrimary: r.isPrimary, selected: r.selected, locationConfigurations: configs };
|
|
4750
|
+
});
|
|
4751
|
+
const parsed2 = import_zod12.z.array(CalendarTeamMemberSchema).safeParse(stripped);
|
|
4752
|
+
if (!parsed2.success) {
|
|
4753
|
+
const issue = parsed2.error.issues[0];
|
|
4754
|
+
throw new Error(
|
|
4755
|
+
`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.`
|
|
4756
|
+
);
|
|
4757
|
+
}
|
|
4758
|
+
existing = { ...existing, teamMembers: parsed2.data };
|
|
4759
|
+
}
|
|
5200
4760
|
const body = buildUpdateCalendarBody(mergeCalendarUpdate(existing, args));
|
|
5201
|
-
|
|
4761
|
+
const updated = await client.put(`/calendars/${args.calendarId}`, { body });
|
|
4762
|
+
let storedTeamMembers;
|
|
4763
|
+
try {
|
|
4764
|
+
const after = await client.get(`/calendars/${args.calendarId}`);
|
|
4765
|
+
const cal = after?.calendar ?? after;
|
|
4766
|
+
storedTeamMembers = Array.isArray(cal?.teamMembers) ? cal.teamMembers.length : 0;
|
|
4767
|
+
} catch (e) {
|
|
4768
|
+
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.`;
|
|
4769
|
+
}
|
|
4770
|
+
return {
|
|
4771
|
+
updated,
|
|
4772
|
+
stored_team_members: storedTeamMembers,
|
|
4773
|
+
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."
|
|
4774
|
+
};
|
|
5202
4775
|
}
|
|
5203
4776
|
);
|
|
5204
4777
|
safeTool(
|
|
@@ -5401,7 +4974,13 @@ var init_calendars = __esm({
|
|
|
5401
4974
|
"appointmentPerDay",
|
|
5402
4975
|
"allowBookingAfter",
|
|
5403
4976
|
"allowBookingAfterUnit",
|
|
5404
|
-
"availabilityType"
|
|
4977
|
+
"availabilityType",
|
|
4978
|
+
"teamMembers",
|
|
4979
|
+
"description",
|
|
4980
|
+
"slug",
|
|
4981
|
+
"widgetSlug",
|
|
4982
|
+
"eventTitle",
|
|
4983
|
+
"eventColor"
|
|
5405
4984
|
];
|
|
5406
4985
|
}
|
|
5407
4986
|
});
|
|
@@ -5454,11 +5033,11 @@ function registerLocationTools(server2, client) {
|
|
|
5454
5033
|
if (email !== void 0) body.email = email;
|
|
5455
5034
|
if (settings !== void 0) body.settings = settings;
|
|
5456
5035
|
const BRAND_FIELDS = ["name", "address", "city", "state", "postalCode", "country", "phone", "website", "email"];
|
|
5457
|
-
const
|
|
5036
|
+
const isRec2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5458
5037
|
let current = null;
|
|
5459
5038
|
try {
|
|
5460
5039
|
const raw = await client.get(`/locations/${locationId2}`);
|
|
5461
|
-
current =
|
|
5040
|
+
current = isRec2(raw) && isRec2(raw.location) ? raw.location : isRec2(raw) ? raw : null;
|
|
5462
5041
|
} catch {
|
|
5463
5042
|
current = null;
|
|
5464
5043
|
}
|
|
@@ -5484,13 +5063,13 @@ Nothing was changed. Pass overwriteBusinessInfo: true if you intend to change th
|
|
|
5484
5063
|
const result = await client.put(`/locations/${locationId2}`, { body });
|
|
5485
5064
|
if (!current) {
|
|
5486
5065
|
return {
|
|
5487
|
-
...
|
|
5066
|
+
...isRec2(result) ? result : { result },
|
|
5488
5067
|
business_record_note: `Could not read ${locationId2} before writing, so the A2P brand check did not run. If this account has a registered brand, verify its details in GHL.`
|
|
5489
5068
|
};
|
|
5490
5069
|
}
|
|
5491
5070
|
if (changes.length > 0) {
|
|
5492
5071
|
return {
|
|
5493
|
-
...
|
|
5072
|
+
...isRec2(result) ? result : { result },
|
|
5494
5073
|
business_record_change: {
|
|
5495
5074
|
changed: changes,
|
|
5496
5075
|
brandId: brandId || "(no registered A2P brand on this sub-account)",
|
|
@@ -8255,6 +7834,16 @@ function normalizeCrawlStatus(raw) {
|
|
|
8255
7834
|
urls
|
|
8256
7835
|
};
|
|
8257
7836
|
}
|
|
7837
|
+
function trainedOfRequested(s, urlIds) {
|
|
7838
|
+
const bucketOf = new Map(s.urls.map((u) => [u.id, u.bucket]));
|
|
7839
|
+
const isTrained = (id) => TRAINED_BUCKET.test(bucketOf.get(id) ?? "");
|
|
7840
|
+
const pending = urlIds.filter((id) => !isTrained(id));
|
|
7841
|
+
return {
|
|
7842
|
+
trained: urlIds.filter(isTrained),
|
|
7843
|
+
pending,
|
|
7844
|
+
buckets: [...new Set(pending.map((id) => bucketOf.get(id) ?? "not listed"))]
|
|
7845
|
+
};
|
|
7846
|
+
}
|
|
8258
7847
|
async function readCrawlStatus(client, locationId2, knowledgeBaseId, operationId) {
|
|
8259
7848
|
return normalizeCrawlStatus(await client.get("/knowledge-bases/crawler/status", { version: AI_API_VERSION, params: { locationId: locationId2, operationId, knowledgeBaseId } }));
|
|
8260
7849
|
}
|
|
@@ -8306,7 +7895,7 @@ async function crawlAndTrain(client, opts) {
|
|
|
8306
7895
|
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
7896
|
if (d.pollError) notes.push(`a status poll failed (${d.pollError})`);
|
|
8308
7897
|
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;
|
|
7898
|
+
let trained = 0, trainedPages = 0, trainStatus = "not started", trainSettled = false;
|
|
8310
7899
|
const chosen = mode === "Exact" ? [] : pickPages(d.s.urls, opts.maxPages ?? 8);
|
|
8311
7900
|
if (mode === "Exact") {
|
|
8312
7901
|
notes.push("Exact mode: the single page trains itself.");
|
|
@@ -8314,17 +7903,23 @@ async function crawlAndTrain(client, opts) {
|
|
|
8314
7903
|
notes.push("no pages were listed to train");
|
|
8315
7904
|
} else {
|
|
8316
7905
|
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
|
|
7906
|
+
const chosenIds = chosen.map((c) => c.id);
|
|
7907
|
+
const t = await settleCrawl(client, sleep7, opts.locationId, opts.knowledgeBaseId, operationId, opts.waitTrain ?? 120, (x) => trainSettledOn(x, chosenIds), { operationId, status: "Training" });
|
|
8318
7908
|
trained = t.s.trainedUrlsCount;
|
|
8319
7909
|
trainStatus = t.s.status;
|
|
8320
7910
|
trainSettled = t.settled;
|
|
7911
|
+
trainedPages = trainedOfRequested(t.s, chosenIds).trained.length;
|
|
8321
7912
|
if (t.pollError) notes.push(`a training poll failed (${t.pollError})`);
|
|
8322
|
-
if (
|
|
7913
|
+
else if (wasStopped(t.s.status)) notes.push(`GoHighLevel stopped this operation ("${t.s.status}"), so nothing here confirms these pages trained`);
|
|
7914
|
+
else if (!t.settled) {
|
|
7915
|
+
const { pending, buckets } = trainedOfRequested(t.s, chosenIds);
|
|
7916
|
+
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(", ")})`);
|
|
7917
|
+
}
|
|
8323
7918
|
}
|
|
8324
7919
|
const listed = unwrapEnvelope(await client.get("/knowledge-bases/crawler", { ...v, params: { knowledgeBaseId: opts.knowledgeBaseId, locationId: opts.locationId, page: 1, pageLength: 50 } }));
|
|
8325
7920
|
const rows2 = isRecord4(listed) && Array.isArray(listed.urls) ? listed.urls : [];
|
|
8326
7921
|
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 };
|
|
7922
|
+
return { operationId, discovered: d.s.urls.length, discoverStatus: d.s.status, discoverSettled: d.settled, trained, trainedPages, trainStatus, trainSettled, pages, notes };
|
|
8328
7923
|
}
|
|
8329
7924
|
function registerKnowledgeBaseTools(server2, client, deps = {}) {
|
|
8330
7925
|
const sleep7 = deps.sleep ?? realSleep;
|
|
@@ -8431,13 +8026,19 @@ function registerKnowledgeBaseTools(server2, client, deps = {}) {
|
|
|
8431
8026
|
const startStatus = typeof rec.status === "string" ? rec.status : "Processing";
|
|
8432
8027
|
const { s, polls, settled, pollError } = await settle2(loc, knowledgeBaseId, operationId, wait, (x) => isSettled(x.status), { operationId, status: startStatus, mode: option, url });
|
|
8433
8028
|
const listed = s.urls.length;
|
|
8029
|
+
const outcome = crawlOutcome(s.status, settled);
|
|
8434
8030
|
return {
|
|
8435
8031
|
operationId,
|
|
8436
8032
|
...s,
|
|
8437
8033
|
polls,
|
|
8438
8034
|
settled,
|
|
8035
|
+
outcome,
|
|
8439
8036
|
...pollError ? { pollError } : {},
|
|
8440
|
-
next:
|
|
8037
|
+
next: (
|
|
8038
|
+
// The refusal goes first, Exact included: a page that "trains itself"
|
|
8039
|
+
// trains nothing when GoHighLevel stopped the operation.
|
|
8040
|
+
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}".`
|
|
8041
|
+
)
|
|
8441
8042
|
};
|
|
8442
8043
|
}
|
|
8443
8044
|
);
|
|
@@ -8457,16 +8058,24 @@ function registerKnowledgeBaseTools(server2, client, deps = {}) {
|
|
|
8457
8058
|
const queued = await client.post("/knowledge-bases/crawler/train", { ...v, body: { locationId: loc, urlIds, knowledgeBaseId, operationId }, noRetry: true });
|
|
8458
8059
|
const wait = waitSeconds ?? 60;
|
|
8459
8060
|
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) =>
|
|
8061
|
+
const { s, polls, settled, pollError } = await settle2(loc, knowledgeBaseId, operationId, wait, (x) => trainSettledOn(x, urlIds), { operationId, status: "Training" });
|
|
8062
|
+
const { trained, pending, buckets } = trainedOfRequested(s, urlIds);
|
|
8063
|
+
const outcome = crawlOutcome(s.status, settled);
|
|
8461
8064
|
return {
|
|
8462
8065
|
queued,
|
|
8463
8066
|
status: s.status,
|
|
8067
|
+
outcome,
|
|
8068
|
+
trained,
|
|
8069
|
+
pending,
|
|
8070
|
+
// The operation's own counter, kept because it is what GoHighLevel
|
|
8071
|
+
// reports — it counts every page trained against this operationId,
|
|
8072
|
+
// including earlier train calls, so it is not a result for THIS one.
|
|
8464
8073
|
trainedUrlsCount: s.trainedUrlsCount,
|
|
8465
8074
|
requested: urlIds.length,
|
|
8466
8075
|
polls,
|
|
8467
8076
|
settled,
|
|
8468
8077
|
...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
|
|
8078
|
+
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
8079
|
};
|
|
8471
8080
|
}
|
|
8472
8081
|
);
|
|
@@ -8573,7 +8182,7 @@ function registerKnowledgeBaseTools(server2, client, deps = {}) {
|
|
|
8573
8182
|
}
|
|
8574
8183
|
);
|
|
8575
8184
|
}
|
|
8576
|
-
var import_zod34, AI_API_VERSION, KB_CAP_PER_LOCATION, realSleep, FINISHED_STATUS, isSettled, PAGE_PRIORITY, HOME_PATH, SKIP;
|
|
8185
|
+
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
8186
|
var init_knowledge_base = __esm({
|
|
8578
8187
|
"src/tools/knowledge-base.ts"() {
|
|
8579
8188
|
"use strict";
|
|
@@ -8584,6 +8193,11 @@ var init_knowledge_base = __esm({
|
|
|
8584
8193
|
realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8585
8194
|
FINISHED_STATUS = /^(successful|success|failed|failure|existing|restricted|cancelled|canceled|aborted|completed?)$/i;
|
|
8586
8195
|
isSettled = (status) => FINISHED_STATUS.test(status.trim());
|
|
8196
|
+
STOPPED_STATUS = /^(failed|failure|restricted|cancelled|canceled|aborted)$/i;
|
|
8197
|
+
wasStopped = (status) => STOPPED_STATUS.test(status.trim());
|
|
8198
|
+
crawlOutcome = (status, settled) => !settled ? "running" : wasStopped(status) ? "stopped" : "finished";
|
|
8199
|
+
TRAINED_BUCKET = /^trained$/i;
|
|
8200
|
+
trainSettledOn = (s, urlIds) => wasStopped(s.status) || trainedOfRequested(s, urlIds).pending.length === 0;
|
|
8587
8201
|
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
8202
|
HOME_PATH = /^\/(index(\.html?)?|home(\.html?)?)?\/?$/i;
|
|
8589
8203
|
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 +8874,17 @@ function registerAiEmployeeDemoTools(server2, shared, deps = {}) {
|
|
|
9260
8874
|
return {
|
|
9261
8875
|
runId: run.runId,
|
|
9262
8876
|
bot: { id: botId, name: upd.after.name, mode: upd.after.mode, knowledgeBaseIds: upd.after.knowledgeBaseIds, businessName: upd.after.businessName },
|
|
9263
|
-
|
|
8877
|
+
// trainedPages = pages GoHighLevel holds in a Trained bucket; the
|
|
8878
|
+
// operation counter climbs across every train call and is named for
|
|
8879
|
+
// what it is (delta review 3).
|
|
8880
|
+
knowledgeBase: { id: kbId, discovered: crawl.discovered, trainedPages: crawl.trainedPages ?? 0, operationTrainedCount: crawl.trained, learned, notes: crawl.notes },
|
|
9264
8881
|
prospect: { contactId, phone: prospectPhone, fieldsVerified: Object.keys(values).length },
|
|
9265
8882
|
numberToText,
|
|
9266
8883
|
primaryOnSms: primary,
|
|
9267
8884
|
armed: run.armed,
|
|
9268
8885
|
readiness: [
|
|
9269
|
-
|
|
8886
|
+
// Per-page buckets, not the operation counter (sweep review 7).
|
|
8887
|
+
(crawl.trainedPages ?? 0) > 0 ? `the bot has learned ${crawl.trainedPages} page(s)` : "the bot has learned NO pages yet",
|
|
9270
8888
|
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
8889
|
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
8890
|
numberToText ? `text ${numberToText}` : "this sub-account has no phone number",
|
|
@@ -9803,13 +9421,16 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9803
9421
|
for (const n of held) if (!holders.has(n)) holders.set(n, { agentId: String(a.id), agentName: String(a.agentName), via: `pool ${pool}` });
|
|
9804
9422
|
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
9423
|
});
|
|
9806
|
-
const
|
|
9424
|
+
const poolRouting = poolBound.filter((p) => Array.isArray(p.poolNumbers) && p.poolNumbers.length > 0);
|
|
9425
|
+
const poolUnproven = poolBound.filter((p) => typeof p.poolNumbers === "string");
|
|
9426
|
+
const unresolvedPool = poolUnproven.length > 0;
|
|
9807
9427
|
const numberRows = numbers.map((n) => ({ number: n, answeredBy: holders.get(n) ?? null }));
|
|
9808
9428
|
const free = unresolvedPool ? [] : numberRows.filter((r) => !r.answeredBy && !heldUnproven.has(r.number)).map((r) => r.number);
|
|
9809
9429
|
const rota = agents.filter(inBackupRota).map((a) => ({ agentId: String(a.id), agentName: String(a.agentName) }));
|
|
9810
|
-
const ready = numberRows.some((r) => r.answeredBy) ||
|
|
9430
|
+
const ready = numberRows.some((r) => r.answeredBy) || poolRouting.length > 0;
|
|
9811
9431
|
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
|
|
9432
|
+
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.` : "";
|
|
9433
|
+
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
9434
|
return {
|
|
9814
9435
|
locationId: loc,
|
|
9815
9436
|
verdict,
|
|
@@ -9939,6 +9560,7 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9939
9560
|
if (numberPoolId !== void 0) change.numberPoolId = numberPoolId;
|
|
9940
9561
|
if (Object.keys(change).length === 0 && inboundNumber === void 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
|
|
9941
9562
|
const before = await readVoiceAgent(client, loc, agentId);
|
|
9563
|
+
const bindNotes = [];
|
|
9942
9564
|
if (typeof inboundNumber === "string") {
|
|
9943
9565
|
const current = Array.isArray(before.inboundNumbers) ? before.inboundNumbers.filter((n) => typeof n === "string" && n.length > 0) : [];
|
|
9944
9566
|
change.inboundNumbers = [.../* @__PURE__ */ new Set([...current, inboundNumber])];
|
|
@@ -9965,8 +9587,26 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9965
9587
|
const { numbers, pools, poolObjects } = await readNumbers(client, loc);
|
|
9966
9588
|
if (typeof inboundNumber === "string") {
|
|
9967
9589
|
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
|
|
9590
|
+
const agents = await listAllVoiceAgents(client, loc);
|
|
9591
|
+
const clash = bindingClash(inboundNumber, agents, agentId, poolObjects);
|
|
9969
9592
|
if (clash) throw new Error(clash);
|
|
9593
|
+
const dormant = unprovenNumbers(before).filter((n) => n !== inboundNumber);
|
|
9594
|
+
const kept = [];
|
|
9595
|
+
for (const n of change.inboundNumbers) {
|
|
9596
|
+
if (n === inboundNumber) {
|
|
9597
|
+
kept.push(n);
|
|
9598
|
+
continue;
|
|
9599
|
+
}
|
|
9600
|
+
const holder = agents.find((a) => a.id !== agentId && boundNumbers(a).includes(n));
|
|
9601
|
+
if (holder && dormant.includes(n)) {
|
|
9602
|
+
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.`);
|
|
9603
|
+
continue;
|
|
9604
|
+
}
|
|
9605
|
+
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.`);
|
|
9606
|
+
kept.push(n);
|
|
9607
|
+
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.`);
|
|
9608
|
+
}
|
|
9609
|
+
change.inboundNumbers = kept;
|
|
9970
9610
|
}
|
|
9971
9611
|
if (typeof numberPoolId === "string") {
|
|
9972
9612
|
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 +9631,7 @@ function registerVoiceAiTools(server2, shared) {
|
|
|
9991
9631
|
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
9632
|
const after = rb.agent;
|
|
9993
9633
|
const changed = diffVoiceAgentSnapshots(before, after);
|
|
9994
|
-
const notes = [];
|
|
9634
|
+
const notes = [...bindNotes];
|
|
9995
9635
|
const differs = agentWriteDifferences(change, after);
|
|
9996
9636
|
for (const k of differs) notes.push(`${k}: sent ${JSON.stringify(change[k])}, reads back ${JSON.stringify(after[k])}.`);
|
|
9997
9637
|
let asserted2 = differs.length > 0;
|
|
@@ -11619,8 +11259,10 @@ ${await response.text()}`);
|
|
|
11619
11259
|
async ({ pageId, content, funnelId, isPublished }) => {
|
|
11620
11260
|
try {
|
|
11621
11261
|
let meta = {};
|
|
11262
|
+
let preReadOk = false;
|
|
11622
11263
|
try {
|
|
11623
11264
|
meta = await funnelRequest("GET", `/page/${pageId}?locationId=${client.locationId}`);
|
|
11265
|
+
preReadOk = true;
|
|
11624
11266
|
} catch (metaErr) {
|
|
11625
11267
|
if (!funnelId) throw metaErr;
|
|
11626
11268
|
}
|
|
@@ -11656,14 +11298,24 @@ ${await response.text()}`);
|
|
|
11656
11298
|
}
|
|
11657
11299
|
const afterVersion = typeof after.pageVersion === "number" ? after.pageVersion : null;
|
|
11658
11300
|
const becameVisible = afterUrl !== "" && afterUrl !== beforeUrl;
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11301
|
+
if (!preReadOk) {
|
|
11302
|
+
const versionMismatch = afterVersion !== null && afterVersion !== pageVersion;
|
|
11303
|
+
save_check = {
|
|
11304
|
+
visible: null,
|
|
11305
|
+
pageVersion_sent: pageVersion,
|
|
11306
|
+
pageVersion_now: afterVersion,
|
|
11307
|
+
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.` : "")
|
|
11308
|
+
};
|
|
11309
|
+
} else {
|
|
11310
|
+
save_check = {
|
|
11311
|
+
visible: becameVisible,
|
|
11312
|
+
pageVersion_sent: pageVersion,
|
|
11313
|
+
pageVersion_now: afterVersion,
|
|
11314
|
+
...becameVisible ? {} : {
|
|
11315
|
+
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."
|
|
11316
|
+
}
|
|
11317
|
+
};
|
|
11318
|
+
}
|
|
11667
11319
|
} catch (verifyErr) {
|
|
11668
11320
|
save_check = {
|
|
11669
11321
|
visible: null,
|
|
@@ -12468,6 +12120,10 @@ function pageFaces(envelope) {
|
|
|
12468
12120
|
function derivePalette(envelope) {
|
|
12469
12121
|
return derivePaletteFromUsage(usageOrHexes(envelope), pageFaces(envelope));
|
|
12470
12122
|
}
|
|
12123
|
+
function paintsAs(literal, from) {
|
|
12124
|
+
if (/^#[0-9a-f]{8}$/i.test(literal)) return { hit: literal.slice(0, 7).toLowerCase() === from, keep: literal.slice(7) };
|
|
12125
|
+
return { hit: normaliseColour(literal)?.toLowerCase() === from, keep: "" };
|
|
12126
|
+
}
|
|
12471
12127
|
function sectionPairs(envelope) {
|
|
12472
12128
|
const groups = /* @__PURE__ */ new Map();
|
|
12473
12129
|
for (const p of renderedPalette(envelope).paints) {
|
|
@@ -12517,28 +12173,47 @@ function rethemeEnvelope(envelope, brand, derived = derivePalette(envelope)) {
|
|
|
12517
12173
|
return out;
|
|
12518
12174
|
};
|
|
12519
12175
|
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
|
-
|
|
12176
|
+
const guardedAtStart = plan.filter((x) => x.guarded).length;
|
|
12177
|
+
for (let pass = 0; pass < guardedAtStart; pass++) {
|
|
12178
|
+
let removed = false;
|
|
12179
|
+
for (const hit of losses()) {
|
|
12180
|
+
for (const step of hit.involved.filter((x) => x.guarded)) {
|
|
12181
|
+
const without = (hex) => hex === step.from ? hex : target(hex);
|
|
12182
|
+
if (contrastHex(without(hit.t), without(hit.bg)) < 4.5) continue;
|
|
12183
|
+
skipped.push(`${step.role} ${step.from}: left as it was, because ${sayLoss(hit)}`);
|
|
12184
|
+
plan.splice(plan.indexOf(step), 1);
|
|
12185
|
+
removed = true;
|
|
12186
|
+
break;
|
|
12187
|
+
}
|
|
12188
|
+
if (removed) break;
|
|
12526
12189
|
}
|
|
12190
|
+
if (!removed) break;
|
|
12527
12191
|
}
|
|
12528
12192
|
const seen = /* @__PURE__ */ new Set();
|
|
12529
12193
|
for (const l of losses()) {
|
|
12530
|
-
const
|
|
12194
|
+
const brandRoles = l.involved.filter((x) => !x.guarded).map((x) => `the ${x.role}`);
|
|
12195
|
+
const guardedRoles = l.involved.filter((x) => x.guarded).map((x) => `the ${x.role}`);
|
|
12196
|
+
const because = [
|
|
12197
|
+
brandRoles.length ? `${brandRoles.join(" and ")} ${brandRoles.length > 1 ? "are" : "is"} the brand's own colour` : "",
|
|
12198
|
+
guardedRoles.length ? `leaving ${guardedRoles.join(" and ")} as it was would not make it readable` : ""
|
|
12199
|
+
].filter(Boolean).join(", and ");
|
|
12200
|
+
const line3 = `${sayLoss(l)} Kept, because ${because}: check this section before a client sees it.`;
|
|
12531
12201
|
if (!seen.has(`${target(l.t)} ${target(l.bg)}`)) warnings.push(line3);
|
|
12532
12202
|
seen.add(`${target(l.t)} ${target(l.bg)}`);
|
|
12533
12203
|
}
|
|
12534
12204
|
let text2 = JSON.stringify(envelope);
|
|
12535
12205
|
const colourTokens = [];
|
|
12536
12206
|
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
12207
|
const token2 = `\0COLOUR${i}\0`;
|
|
12541
|
-
|
|
12208
|
+
let occurrences = 0;
|
|
12209
|
+
const swapped = text2.replace(COLOUR_LITERAL_RE, (lit) => {
|
|
12210
|
+
const { hit, keep } = paintsAs(lit, step.from);
|
|
12211
|
+
if (!hit) return lit;
|
|
12212
|
+
occurrences++;
|
|
12213
|
+
return token2 + keep;
|
|
12214
|
+
});
|
|
12215
|
+
if (occurrences === 0) return;
|
|
12216
|
+
text2 = swapped;
|
|
12542
12217
|
colourTokens.push({ token: token2, to: step.to });
|
|
12543
12218
|
swaps.push({ role: step.role, from: step.from, to: step.to, occurrences });
|
|
12544
12219
|
});
|
|
@@ -12581,7 +12256,7 @@ function derivePaletteAcross(envelopes) {
|
|
|
12581
12256
|
for (const env2 of envelopes) for (const f of pageFaces(env2)) if (!fonts.includes(f)) fonts.push(f);
|
|
12582
12257
|
return derivePaletteFromUsage(pooled2, fonts.slice(0, 2));
|
|
12583
12258
|
}
|
|
12584
|
-
var ICON_FONTS, BRAND_CHROMA, TONE_CHROMA, escapeRe;
|
|
12259
|
+
var ICON_FONTS, BRAND_CHROMA, TONE_CHROMA, escapeRe, COLOUR_LITERAL_RE;
|
|
12585
12260
|
var init_retheme = __esm({
|
|
12586
12261
|
"src/command-os/design/retheme.ts"() {
|
|
12587
12262
|
"use strict";
|
|
@@ -12593,6 +12268,135 @@ var init_retheme = __esm({
|
|
|
12593
12268
|
BRAND_CHROMA = 0.06;
|
|
12594
12269
|
TONE_CHROMA = 0.02;
|
|
12595
12270
|
escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12271
|
+
COLOUR_LITERAL_RE = /#[0-9a-f]{3,8}(?![0-9a-f])|rgba?\([^()]*\)|hsla?\([^()]*\)/gi;
|
|
12272
|
+
}
|
|
12273
|
+
});
|
|
12274
|
+
|
|
12275
|
+
// src/command-os/design/global-sections.ts
|
|
12276
|
+
function sameColour(a, b) {
|
|
12277
|
+
if (a === void 0 || b === void 0) return false;
|
|
12278
|
+
const na = normaliseColour(a) ?? a.trim();
|
|
12279
|
+
const nb = normaliseColour(b) ?? b.trim();
|
|
12280
|
+
return na.toLowerCase() === nb.toLowerCase();
|
|
12281
|
+
}
|
|
12282
|
+
function nextGlobalSectionVersion(current) {
|
|
12283
|
+
const v = typeof current === "number" && Number.isFinite(current) ? current : 0;
|
|
12284
|
+
return !v || v >= MAX_VERSION ? INITIAL_VERSION : v + 1;
|
|
12285
|
+
}
|
|
12286
|
+
function siteColourNames(sources) {
|
|
12287
|
+
const names = /* @__PURE__ */ new Set();
|
|
12288
|
+
const take = (colours) => {
|
|
12289
|
+
if (!Array.isArray(colours)) return;
|
|
12290
|
+
for (const c of colours) {
|
|
12291
|
+
const label = isRec(c) && typeof c.label === "string" ? c.label.trim().toLowerCase() : "";
|
|
12292
|
+
if (/^[a-z0-9_-]+$/.test(label)) names.add(`--${label}`);
|
|
12293
|
+
}
|
|
12294
|
+
};
|
|
12295
|
+
for (const source of sources) {
|
|
12296
|
+
if (isRec(source) && isRec(source.general) && isRec(source.general.general)) take(source.general.general.colors);
|
|
12297
|
+
for (const section2 of sectionsOf(source)) if (isRec(section2) && isRec(section2.general)) take(section2.general.colors);
|
|
12298
|
+
}
|
|
12299
|
+
return names;
|
|
12300
|
+
}
|
|
12301
|
+
function colourDefs(source, names) {
|
|
12302
|
+
const out = /* @__PURE__ */ new Map();
|
|
12303
|
+
const scan = (styles) => {
|
|
12304
|
+
if (typeof styles !== "string") return;
|
|
12305
|
+
for (const block of styles.matchAll(ROOT_BLOCK)) {
|
|
12306
|
+
for (const decl of block[1].matchAll(DECLARATION)) {
|
|
12307
|
+
if (names ? names.has(decl[1]) : decl[1].startsWith("--color-")) out.set(decl[1], decl[2].trim());
|
|
12308
|
+
}
|
|
12309
|
+
}
|
|
12310
|
+
};
|
|
12311
|
+
if (isRec(source) && !Array.isArray(source)) scan(source.pageStyles);
|
|
12312
|
+
for (const section2 of sectionsOf(source)) {
|
|
12313
|
+
if (!isRec(section2)) continue;
|
|
12314
|
+
scan(isRec(section2.general) ? section2.general.sectionStyles : void 0);
|
|
12315
|
+
}
|
|
12316
|
+
return out;
|
|
12317
|
+
}
|
|
12318
|
+
function repaintGlobalSections(sections, brand, palette) {
|
|
12319
|
+
const painted = rethemeEnvelope({ sections: structuredClone(sections) }, brand, palette);
|
|
12320
|
+
return { sections: painted.envelope.sections, swaps: painted.swaps, warnings: painted.warnings };
|
|
12321
|
+
}
|
|
12322
|
+
var INITIAL_VERSION, MAX_VERSION, ROOT_BLOCK, DECLARATION, isRec, sectionsOf;
|
|
12323
|
+
var init_global_sections = __esm({
|
|
12324
|
+
"src/command-os/design/global-sections.ts"() {
|
|
12325
|
+
"use strict";
|
|
12326
|
+
init_retheme();
|
|
12327
|
+
init_rendered_palette();
|
|
12328
|
+
INITIAL_VERSION = 1;
|
|
12329
|
+
MAX_VERSION = 4;
|
|
12330
|
+
ROOT_BLOCK = /:root\s*\{([^}]*)\}/g;
|
|
12331
|
+
DECLARATION = /(--[a-zA-Z0-9_-]+)\s*:\s*([^;}]+)/g;
|
|
12332
|
+
isRec = (v) => Boolean(v) && typeof v === "object";
|
|
12333
|
+
sectionsOf = (source) => Array.isArray(source) ? source : isRec(source) && Array.isArray(source.sections) ? source.sections : [];
|
|
12334
|
+
}
|
|
12335
|
+
});
|
|
12336
|
+
|
|
12337
|
+
// src/command-os/design/served-palette.ts
|
|
12338
|
+
function rootBlocks(html2) {
|
|
12339
|
+
return [...html2.matchAll(ROOT_BLOCK2)].map((m) => m[0]);
|
|
12340
|
+
}
|
|
12341
|
+
function effectiveRootVars(html2) {
|
|
12342
|
+
const out = /* @__PURE__ */ new Map();
|
|
12343
|
+
for (const block of html2.matchAll(ROOT_BLOCK2)) {
|
|
12344
|
+
for (const decl of block[1].matchAll(DECLARATION2)) out.set(decl[1], decl[2].trim());
|
|
12345
|
+
}
|
|
12346
|
+
return out;
|
|
12347
|
+
}
|
|
12348
|
+
function varReferences(html2) {
|
|
12349
|
+
const out = /* @__PURE__ */ new Map();
|
|
12350
|
+
for (const m of html2.matchAll(REFERENCE)) out.set(m[1], (out.get(m[1]) ?? 0) + 1);
|
|
12351
|
+
return out;
|
|
12352
|
+
}
|
|
12353
|
+
function servedVerdict(input) {
|
|
12354
|
+
const blocks = rootBlocks(input.html);
|
|
12355
|
+
const effective = effectiveRootVars(input.html);
|
|
12356
|
+
const refs = varReferences(input.html);
|
|
12357
|
+
const stillTemplate = [];
|
|
12358
|
+
const unexplained = [];
|
|
12359
|
+
const undeclared = [];
|
|
12360
|
+
const asBrand = [];
|
|
12361
|
+
for (const [name, templateHex] of input.templateVars) {
|
|
12362
|
+
const references = refs.get(name) ?? 0;
|
|
12363
|
+
if (references === 0) continue;
|
|
12364
|
+
const value = effective.get(name) ?? null;
|
|
12365
|
+
const row = { name, effective: value, template: templateHex, references };
|
|
12366
|
+
const expected = input.expectedVars.get(name);
|
|
12367
|
+
if (value === null) undeclared.push(row);
|
|
12368
|
+
else if (sameColour(value, expected)) asBrand.push(row);
|
|
12369
|
+
else if (sameColour(value, templateHex)) stillTemplate.push(row);
|
|
12370
|
+
else unexplained.push(row);
|
|
12371
|
+
}
|
|
12372
|
+
const judged = stillTemplate.length + unexplained.length + undeclared.length + asBrand.length;
|
|
12373
|
+
const verdict = blocks.length === 0 ? "unreadable" : stillTemplate.length ? "template" : unexplained.length || undeclared.length ? "mixed" : judged === 0 ? "none" : "brand";
|
|
12374
|
+
const places = (n) => `at least ${n} place${n === 1 ? "" : "s"}`;
|
|
12375
|
+
const lines = [];
|
|
12376
|
+
if (stillTemplate.length) {
|
|
12377
|
+
lines.push(
|
|
12378
|
+
"The served page still paints the template's colours: " + stillTemplate.map((s) => `${s.template.toUpperCase()} in ${places(s.references)}`).join(", ") + (input.sharedRepainted === true ? "." : ". A shared section's own colour definition comes after the page's, so it wins for the whole page.")
|
|
12379
|
+
);
|
|
12380
|
+
}
|
|
12381
|
+
if (unexplained.length) {
|
|
12382
|
+
lines.push("One of the design's site colours resolves to neither the template's nor the saved colour: " + unexplained.map((s) => `${s.effective ?? "(undefined)"} where ${s.template.toUpperCase()} was expected to become the brand's`).join(", ") + ".");
|
|
12383
|
+
}
|
|
12384
|
+
if (undeclared.length) {
|
|
12385
|
+
lines.push("Painted through a site colour nothing on the served page defines: " + undeclared.map((s) => `${places(s.references)} through ${s.name}`).join(", ") + ".");
|
|
12386
|
+
}
|
|
12387
|
+
if (verdict === "unreadable") lines.push("The served page carried no colour definitions at all, so it is not a rendered page and what a visitor will see could not be checked.");
|
|
12388
|
+
if (verdict === "none") lines.push("The served page paints through no site colour the template defined, so there was nothing to judge here; the checks on the stored page stand on their own.");
|
|
12389
|
+
if (verdict === "brand") lines.push(`Every site colour judged on the served page resolves to what this install saved: ${asBrand.length} colour${asBrand.length === 1 ? "" : "s"}, ${places(asBrand.reduce((n, s) => n + s.references, 0))}.`);
|
|
12390
|
+
return { verdict, stillTemplate, unexplained, undeclared, brand: asBrand, summary: lines.join(" ") };
|
|
12391
|
+
}
|
|
12392
|
+
var ROOT_BLOCK2, DECLARATION2, REFERENCE;
|
|
12393
|
+
var init_served_palette = __esm({
|
|
12394
|
+
"src/command-os/design/served-palette.ts"() {
|
|
12395
|
+
"use strict";
|
|
12396
|
+
init_global_sections();
|
|
12397
|
+
ROOT_BLOCK2 = /:root\s*\{([^}]*)\}/g;
|
|
12398
|
+
DECLARATION2 = /(--[a-zA-Z0-9_-]+)\s*:\s*([^;]+)/g;
|
|
12399
|
+
REFERENCE = /var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g;
|
|
12596
12400
|
}
|
|
12597
12401
|
});
|
|
12598
12402
|
|
|
@@ -16804,6 +16608,94 @@ function removalHoldBack(opts) {
|
|
|
16804
16608
|
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`;
|
|
16805
16609
|
return null;
|
|
16806
16610
|
}
|
|
16611
|
+
function brandedPath(path40, name, taken) {
|
|
16612
|
+
const normalize = (s) => s.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
16613
|
+
const base = normalize(path40 ?? "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
|
|
16614
|
+
const used = new Set([...taken].map(normalize));
|
|
16615
|
+
let candidate = `${base}-branded`;
|
|
16616
|
+
for (let n = 2; used.has(candidate); n++) candidate = `${base}-branded-${n}`;
|
|
16617
|
+
taken.add(`/${candidate}`);
|
|
16618
|
+
return `/${candidate}`;
|
|
16619
|
+
}
|
|
16620
|
+
function rewriteTemplateLinks(envelope, destinations) {
|
|
16621
|
+
const links = [];
|
|
16622
|
+
const unmapped = [];
|
|
16623
|
+
const normalize = (s) => s.replace(/^\/+|\/+$/g, "");
|
|
16624
|
+
function rewrite(from, kind) {
|
|
16625
|
+
if (!from) return from;
|
|
16626
|
+
let matches = [];
|
|
16627
|
+
let representation = "path";
|
|
16628
|
+
let suffix = "";
|
|
16629
|
+
let previewPrefix = "";
|
|
16630
|
+
if (kind === "id") {
|
|
16631
|
+
matches = destinations.filter((d2) => d2.stepId === from);
|
|
16632
|
+
representation = "step";
|
|
16633
|
+
} else {
|
|
16634
|
+
const preview = from.match(/^(https:\/\/(?:app\.leadconnectorhq\.com\/v2|sites\.leadconnectorhq\.com)\/preview\/)([^/?#]+)(.*)$/);
|
|
16635
|
+
if (preview) {
|
|
16636
|
+
previewPrefix = preview[1];
|
|
16637
|
+
suffix = preview[3];
|
|
16638
|
+
representation = "page";
|
|
16639
|
+
matches = destinations.filter((d2) => d2.pageId === preview[2]);
|
|
16640
|
+
} else if (!/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(from) && !from.startsWith("#")) {
|
|
16641
|
+
const [path40] = from.split(/[?#]/);
|
|
16642
|
+
suffix = from.slice(path40.length);
|
|
16643
|
+
matches = destinations.filter((d2) => d2.path && normalize(d2.path) === normalize(path40));
|
|
16644
|
+
}
|
|
16645
|
+
}
|
|
16646
|
+
if (matches.length !== 1) {
|
|
16647
|
+
unmapped.push({ from, reason: matches.length > 1 ? "ambiguous original path" : "external or unknown destination" });
|
|
16648
|
+
return from;
|
|
16649
|
+
}
|
|
16650
|
+
const d = matches[0];
|
|
16651
|
+
if (!d.newStepId || !d.newPageId || !d.newPath) {
|
|
16652
|
+
unmapped.push({ from, reason: "the target step was not rebuilt" });
|
|
16653
|
+
return from;
|
|
16654
|
+
}
|
|
16655
|
+
const to = representation === "step" ? d.newStepId : representation === "page" ? `${previewPrefix}${d.newPageId}${suffix}` : `${from.startsWith("/") ? d.newPath : normalize(d.newPath)}${suffix}`;
|
|
16656
|
+
links.push({ from, to });
|
|
16657
|
+
return to;
|
|
16658
|
+
}
|
|
16659
|
+
function walk(value) {
|
|
16660
|
+
if (!value || typeof value !== "object") return;
|
|
16661
|
+
const extra = value.extra;
|
|
16662
|
+
if (extra) {
|
|
16663
|
+
if (typeof extra.stepPath?.value === "string") extra.stepPath.value = rewrite(extra.stepPath.value, "id");
|
|
16664
|
+
if (typeof extra.visitWebsite?.value?.url === "string") extra.visitWebsite.value.url = rewrite(extra.visitWebsite.value.url, "url");
|
|
16665
|
+
if (typeof extra.imageProperties?.value?.linkUrl === "string") extra.imageProperties.value.linkUrl = rewrite(extra.imageProperties.value.linkUrl, "url");
|
|
16666
|
+
const menu = (items) => {
|
|
16667
|
+
for (const item of items) {
|
|
16668
|
+
if (typeof item.goToId === "string") item.goToId = rewrite(item.goToId, "id");
|
|
16669
|
+
if (typeof item.url === "string") item.url = rewrite(item.url, "url");
|
|
16670
|
+
if (Array.isArray(item.childs)) menu(item.childs);
|
|
16671
|
+
}
|
|
16672
|
+
};
|
|
16673
|
+
if (Array.isArray(extra.menuItems?.value)) menu(extra.menuItems.value);
|
|
16674
|
+
for (const social of Array.isArray(extra.socials?.value) ? extra.socials.value : []) {
|
|
16675
|
+
if (typeof social.link === "string") social.link = rewrite(social.link, "url");
|
|
16676
|
+
}
|
|
16677
|
+
if (typeof extra.text?.value === "string") {
|
|
16678
|
+
extra.text.value = extra.text.value.replace(
|
|
16679
|
+
/(\bhref\s*=\s*)(["'])(.*?)\2/gi,
|
|
16680
|
+
(_, prefix, quote, href) => `${prefix}${quote}${rewrite(href, "url")}${quote}`
|
|
16681
|
+
);
|
|
16682
|
+
}
|
|
16683
|
+
}
|
|
16684
|
+
for (const child of Object.values(value)) walk(child);
|
|
16685
|
+
}
|
|
16686
|
+
walk(envelope);
|
|
16687
|
+
return { links, unmapped };
|
|
16688
|
+
}
|
|
16689
|
+
function templateForms(envelope) {
|
|
16690
|
+
const forms = [];
|
|
16691
|
+
function walk(value) {
|
|
16692
|
+
if (!value || typeof value !== "object") return;
|
|
16693
|
+
if (value.meta === "form") forms.push({ id: value.id, selector: value.extra?.formId });
|
|
16694
|
+
for (const child of Object.values(value)) walk(child);
|
|
16695
|
+
}
|
|
16696
|
+
walk(envelope);
|
|
16697
|
+
return forms;
|
|
16698
|
+
}
|
|
16807
16699
|
function registerPageStudioTools(server2, builderClient) {
|
|
16808
16700
|
if (!builderClient) return;
|
|
16809
16701
|
const client = builderClient;
|
|
@@ -16866,6 +16758,21 @@ ${await response.text()}`);
|
|
|
16866
16758
|
}
|
|
16867
16759
|
});
|
|
16868
16760
|
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: POST /builder/autosave/${pageId}
|
|
16761
|
+
${await response.text()}`);
|
|
16762
|
+
const text2 = await response.text();
|
|
16763
|
+
return text2 ? JSON.parse(text2) : {};
|
|
16764
|
+
}
|
|
16765
|
+
async function sharedSectionsUpload(funnelId, body) {
|
|
16766
|
+
const url = `https://backend.leadconnectorhq.com/funnels/builder/global-sections/${funnelId}`;
|
|
16767
|
+
const response = await builderRequest(client, "POST", url, {
|
|
16768
|
+
body,
|
|
16769
|
+
headers: {
|
|
16770
|
+
Origin: "https://page-builder.leadconnectorhq.com",
|
|
16771
|
+
Referer: "https://page-builder.leadconnectorhq.com/",
|
|
16772
|
+
"x-source-app": "page-builder"
|
|
16773
|
+
}
|
|
16774
|
+
});
|
|
16775
|
+
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: POST /builder/global-sections/${funnelId}
|
|
16869
16776
|
${await response.text()}`);
|
|
16870
16777
|
const text2 = await response.text();
|
|
16871
16778
|
return text2 ? JSON.parse(text2) : {};
|
|
@@ -16988,11 +16895,15 @@ ${await response.text()}`);
|
|
|
16988
16895
|
{
|
|
16989
16896
|
templateId: import_zod43.z.string().describe("From find_ghl_template."),
|
|
16990
16897
|
brandSlug: import_zod43.z.string().optional().describe("A stored brand to repaint the template in. Omit to install it in the designer's own colours."),
|
|
16898
|
+
formId: import_zod43.z.string().min(1).optional().describe("An existing form in this sub-account. Substitutes into the design's existing form widgets on fresh pages; does not create a widget."),
|
|
16991
16899
|
confirm: import_zod43.z.literal("INSTALL").describe("Type INSTALL. This writes into the live sub-account you are switched to."),
|
|
16992
|
-
|
|
16900
|
+
removeOriginals: import_zod43.z.boolean().optional().describe("Only true requests deletion of the original steps, after every replacement is verified. Safety guards can still veto removal."),
|
|
16901
|
+
keepOriginals: import_zod43.z.boolean().optional().describe("Compatibility option. Originals are kept by default, even when this is false or absent. Only removeOriginals:true requests deletion; it takes precedence.")
|
|
16993
16902
|
},
|
|
16994
|
-
async ({ templateId, brandSlug,
|
|
16903
|
+
async ({ templateId, brandSlug, formId, removeOriginals }) => {
|
|
16995
16904
|
const brand = brandSlug ? houseLooks().find((x) => x.slug === brandSlug) ?? readBrand(slugify(brandSlug)) : null;
|
|
16905
|
+
if (brandSlug && !brand) return { ok: false, error: "Choose a stored brand before installing a branded template. Nothing was installed." };
|
|
16906
|
+
if (formId && !brand) return { ok: false, error: "Choose a stored brand when binding a form on a branded template. Nothing was installed." };
|
|
16996
16907
|
const inventory = await funnelSummary();
|
|
16997
16908
|
const before = inventory.map;
|
|
16998
16909
|
const named = installedFunnelId(await installTemplate(templateApi(), templateId));
|
|
@@ -17035,138 +16946,354 @@ ${await response.text()}`);
|
|
|
17035
16946
|
return {
|
|
17036
16947
|
ok: true,
|
|
17037
16948
|
installed: true,
|
|
16949
|
+
originalsRemoved: 0,
|
|
16950
|
+
contrast: [],
|
|
16951
|
+
kept: "Nothing was deleted. No replacement pages were requested.",
|
|
17038
16952
|
funnel: { id: funnel._id, name: funnel.name, pages: (funnel.steps ?? []).length },
|
|
17039
16953
|
note: "Installed in the designer's own colours. Pass brandSlug to have it repainted in a client's brand on the way in.",
|
|
17040
16954
|
next: `Grade a page before the client sees it: get_funnels for its page ids, then verify_design on https://sites.leadconnectorhq.com/preview/<pageId> with medium "site" and placement "ghl".`,
|
|
17041
16955
|
undo: `delete_funnel with funnelId "${funnel._id}" and confirm "DELETE" removes it and its pages, and nothing else.`
|
|
17042
16956
|
};
|
|
17043
16957
|
}
|
|
17044
|
-
const originals = (funnel.steps ?? []).map((st) => ({
|
|
16958
|
+
const originals = (funnel.steps ?? []).map((st) => ({
|
|
16959
|
+
stepId: st.id,
|
|
16960
|
+
name: st.name,
|
|
16961
|
+
path: st.url,
|
|
16962
|
+
pageId: st.pages?.[0],
|
|
16963
|
+
pageCount: st.pages?.length ?? 0
|
|
16964
|
+
}));
|
|
17045
16965
|
const multiPage = originals.filter((x) => x.pageCount > 1);
|
|
17046
|
-
const done = [];
|
|
17047
16966
|
const problems = [];
|
|
16967
|
+
const warnings = [];
|
|
17048
16968
|
const loaded = [];
|
|
17049
16969
|
for (const step of originals) {
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
|
|
17054
|
-
|
|
16970
|
+
try {
|
|
16971
|
+
if (!step.pageId) throw new Error("this step has no page to rebuild");
|
|
16972
|
+
const meta = await funnelRequest("GET", `/page/${step.pageId}?locationId=${client.locationId}`);
|
|
16973
|
+
if (typeof meta.pageDataDownloadUrl !== "string" || !meta.pageDataDownloadUrl) throw new Error("GoHighLevel has no saved content for this page yet");
|
|
16974
|
+
const response = await fetch(meta.pageDataDownloadUrl);
|
|
16975
|
+
if (!response.ok) throw new Error(`page content read failed (${response.status})`);
|
|
16976
|
+
loaded.push({ ...step, envelope: await response.json() });
|
|
16977
|
+
} catch (e) {
|
|
16978
|
+
throw new Error(`${step.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
17055
16979
|
}
|
|
17056
|
-
loaded.push({ stepId: step.stepId, name: step.name, envelope: await (await fetch(dataUrl)).json() });
|
|
17057
16980
|
}
|
|
17058
|
-
const
|
|
16981
|
+
const publicRead = async (url) => {
|
|
16982
|
+
const controller = new AbortController();
|
|
16983
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
16984
|
+
try {
|
|
16985
|
+
return await fetch(url, { signal: controller.signal, headers: { "Cache-Control": "no-cache" } });
|
|
16986
|
+
} finally {
|
|
16987
|
+
clearTimeout(timer);
|
|
16988
|
+
}
|
|
16989
|
+
};
|
|
16990
|
+
let sharedOriginal = [];
|
|
16991
|
+
let sharedReadError = null;
|
|
16992
|
+
if (typeof funnel.globalSectionsUrl === "string" && funnel.globalSectionsUrl) {
|
|
16993
|
+
try {
|
|
16994
|
+
const res = await publicRead(funnel.globalSectionsUrl);
|
|
16995
|
+
if (!res.ok) throw new Error(`shared sections read failed (${res.status})`);
|
|
16996
|
+
const gs = await res.json();
|
|
16997
|
+
if (!Array.isArray(gs)) throw new Error("the shared sections file is not a list of sections; the shared sections were left alone");
|
|
16998
|
+
sharedOriginal = gs;
|
|
16999
|
+
} catch (e) {
|
|
17000
|
+
sharedReadError = e instanceof Error ? e.message : String(e);
|
|
17001
|
+
}
|
|
17002
|
+
}
|
|
17003
|
+
const idOf2 = (x) => x && typeof x === "object" ? x.id : void 0;
|
|
17004
|
+
const pageSectionIds = new Set(loaded.flatMap((l) => Array.isArray(l.envelope.sections) ? l.envelope.sections.map(idOf2) : []));
|
|
17005
|
+
const sharedNotInPages = sharedOriginal.filter((sec) => !pageSectionIds.has(idOf2(sec)));
|
|
17006
|
+
const siteColours = siteColourNames([...loaded.map((l) => l.envelope), ...sharedOriginal.length ? [{ sections: sharedOriginal }] : []]);
|
|
17007
|
+
const palette = derivePaletteAcross([...loaded.map((l) => l.envelope), ...sharedNotInPages.length ? [{ sections: sharedNotInPages }] : []]);
|
|
17008
|
+
const destinations = originals.map((s) => ({ stepId: s.stepId, pageId: s.pageId, path: s.path }));
|
|
17009
|
+
for (const st of funnel.steps ?? []) for (const pageId of (st.pages ?? []).slice(1)) {
|
|
17010
|
+
destinations.push({ stepId: `${st.id}:variant:${pageId}`, pageId });
|
|
17011
|
+
}
|
|
17012
|
+
const taken = new Set((funnel.steps ?? []).map((st) => st.url ?? ""));
|
|
17013
|
+
const createdSteps = [];
|
|
17059
17014
|
for (const step of loaded) {
|
|
17060
17015
|
try {
|
|
17061
|
-
const
|
|
17062
|
-
const
|
|
17063
|
-
const created = await funnelRequest("POST",
|
|
17016
|
+
const newStepId = crypto4.randomUUID();
|
|
17017
|
+
const newPath = brandedPath(step.path, step.name, taken);
|
|
17018
|
+
const created = await funnelRequest("POST", "/funnel/create-step", {
|
|
17064
17019
|
funnelId: funnel._id,
|
|
17065
|
-
step: { id:
|
|
17020
|
+
step: { id: newStepId, name: step.name, url: newPath, pages: [], control_traffic: 100, split: false, type: "optin_funnel_page" }
|
|
17066
17021
|
});
|
|
17067
|
-
|
|
17068
|
-
|
|
17069
|
-
|
|
17070
|
-
|
|
17022
|
+
if (!created.page?._id) throw new Error("GoHighLevel did not return a page for the new step");
|
|
17023
|
+
const replacement = { newStepId, newPageId: created.page._id, newPath };
|
|
17024
|
+
Object.assign(destinations.find((d) => d.stepId === step.stepId), replacement);
|
|
17025
|
+
createdSteps.push({ ...step, ...replacement });
|
|
17026
|
+
} catch (e) {
|
|
17027
|
+
problems.push(`${step.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
17028
|
+
}
|
|
17029
|
+
}
|
|
17030
|
+
const pages = [];
|
|
17031
|
+
const storedByPage = /* @__PURE__ */ new Map();
|
|
17032
|
+
for (const step of createdSteps) {
|
|
17033
|
+
const page = {
|
|
17034
|
+
page: step.name,
|
|
17035
|
+
newStepId: step.newStepId,
|
|
17036
|
+
newPageId: step.newPageId,
|
|
17037
|
+
url: step.newPath,
|
|
17038
|
+
oldStepId: step.stepId,
|
|
17039
|
+
links: [],
|
|
17040
|
+
unmapped: [],
|
|
17041
|
+
readBackOk: false,
|
|
17042
|
+
preview: ghlPreviewUrl(step.newPageId),
|
|
17043
|
+
...formId ? { formBound: false } : {}
|
|
17044
|
+
};
|
|
17045
|
+
pages.push(page);
|
|
17046
|
+
try {
|
|
17047
|
+
const painted = brand ? rethemeEnvelope(step.envelope, brand, palette) : null;
|
|
17048
|
+
const envelope = painted?.envelope ?? structuredClone(step.envelope);
|
|
17049
|
+
warnings.push(...painted?.warnings ?? []);
|
|
17050
|
+
page.skipped = painted?.skipped ?? [];
|
|
17051
|
+
Object.assign(page, rewriteTemplateLinks(envelope, destinations));
|
|
17052
|
+
const forms = templateForms(envelope);
|
|
17053
|
+
if (formId) {
|
|
17054
|
+
if (!forms.length) {
|
|
17055
|
+
page.formBound = "none";
|
|
17056
|
+
page.formNote = "This design carries no form. Choose a design with one, or add the form in the editor.";
|
|
17057
|
+
}
|
|
17058
|
+
for (const form of forms) {
|
|
17059
|
+
if (typeof form.selector?.value !== "string") throw new Error(`form ${form.id} has no captured formId selector; binding was not attempted`);
|
|
17060
|
+
form.selector.value = formId;
|
|
17061
|
+
if (typeof form.selector.text === "string") form.selector.text = formId;
|
|
17062
|
+
}
|
|
17071
17063
|
}
|
|
17072
|
-
|
|
17064
|
+
envelope.id = step.newPageId;
|
|
17065
|
+
for (const section2 of Array.isArray(envelope.sections) ? envelope.sections : []) {
|
|
17066
|
+
section2.pageId = step.newPageId;
|
|
17067
|
+
section2.funnelId = funnel._id;
|
|
17068
|
+
section2.locationId = client.locationId;
|
|
17069
|
+
}
|
|
17070
|
+
await autosaveRetrying(step.newPageId, {
|
|
17073
17071
|
funnelId: funnel._id,
|
|
17074
17072
|
pageVersion: 1,
|
|
17075
|
-
pageData: {
|
|
17076
|
-
...painted.envelope,
|
|
17077
|
-
pageVersion: 1,
|
|
17078
|
-
pageType: "draft",
|
|
17079
|
-
manualSave: true,
|
|
17080
|
-
integrations: computeIntegrations(painted.envelope)
|
|
17081
|
-
}
|
|
17073
|
+
pageData: { ...envelope, pageVersion: 1, pageType: "draft", manualSave: true, integrations: computeIntegrations(envelope) }
|
|
17082
17074
|
});
|
|
17083
17075
|
let after = {};
|
|
17084
17076
|
for (let i = 0; i < 6; i++) {
|
|
17085
|
-
after = await funnelRequest("GET", `/page/${newPageId}?locationId=${client.locationId}`);
|
|
17077
|
+
after = await funnelRequest("GET", `/page/${step.newPageId}?locationId=${client.locationId}`);
|
|
17086
17078
|
if (typeof after.pageDataDownloadUrl === "string" && after.pageDataDownloadUrl) break;
|
|
17087
17079
|
await new Promise((r) => setTimeout(r, 1200));
|
|
17088
17080
|
}
|
|
17089
|
-
|
|
17090
|
-
|
|
17091
|
-
|
|
17092
|
-
|
|
17081
|
+
if (typeof after.pageDataDownloadUrl !== "string" || !after.pageDataDownloadUrl) throw new Error("the fresh page saved but GoHighLevel is not serving it");
|
|
17082
|
+
const response = await fetch(after.pageDataDownloadUrl);
|
|
17083
|
+
if (!response.ok) throw new Error(`saved content read failed (${response.status})`);
|
|
17084
|
+
const saved = await response.json();
|
|
17085
|
+
storedByPage.set(step.newPageId, saved);
|
|
17086
|
+
const savedText = JSON.stringify(saved).toLowerCase();
|
|
17087
|
+
if (formId && forms.length) {
|
|
17088
|
+
const readForms2 = templateForms(saved);
|
|
17089
|
+
page.formBound = readForms2.length === forms.length && forms.every((f) => readForms2.some((r) => r.id === f.id && r.selector?.value === formId));
|
|
17090
|
+
if (!page.formBound) throw new Error("the requested form id was not present in every saved form widget");
|
|
17093
17091
|
}
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
if (stillThere) {
|
|
17098
|
-
problems.push(`${step.name}: the template's own ${palette.accent} is still in the saved page \u2014 a partial swap, so the original was kept`);
|
|
17099
|
-
continue;
|
|
17092
|
+
if (brand && palette.accent) {
|
|
17093
|
+
if (palette.accent.toLowerCase() !== brand.colors.accent.hex.toLowerCase() && savedText.includes(palette.accent.toLowerCase())) throw new Error(`the template's own ${palette.accent} is still in the saved page`);
|
|
17094
|
+
if (!savedText.includes(brand.colors.accent.hex.toLowerCase())) throw new Error(`the brand's ${brand.colors.accent.hex} is not in the saved page`);
|
|
17100
17095
|
}
|
|
17101
|
-
const
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
|
|
17096
|
+
const expectedNavigation = rewriteTemplateLinks(structuredClone(envelope), []);
|
|
17097
|
+
const savedNavigation = rewriteTemplateLinks(structuredClone(saved), []);
|
|
17098
|
+
if (JSON.stringify(expectedNavigation) !== JSON.stringify(savedNavigation)) throw new Error("the saved navigation does not match the replacement destinations");
|
|
17099
|
+
const repaint = repaintReport(step.envelope, saved);
|
|
17100
|
+
page.repaint = {
|
|
17101
|
+
verdict: repaint.verdict,
|
|
17102
|
+
summary: repaint.summary,
|
|
17103
|
+
backgrounds: repaint.backgrounds,
|
|
17104
|
+
neutralBackgrounds: repaint.neutralBackgrounds,
|
|
17105
|
+
text: repaint.text,
|
|
17106
|
+
changes: repaint.changes.filter((c) => c.kind === "background" || c.kind === "text"),
|
|
17107
|
+
fonts: repaint.fonts,
|
|
17108
|
+
contrast: repaint.contrast
|
|
17109
|
+
};
|
|
17110
|
+
const pageWarnings = (painted?.warnings ?? []).filter((w) => !palette.undecided.includes(w));
|
|
17111
|
+
if (pageWarnings.length) page.warnings = pageWarnings;
|
|
17112
|
+
page.readBackOk = true;
|
|
17113
|
+
page.readBack = "Saved navigation and the requested form binding match the saved page.";
|
|
17114
|
+
} catch (e) {
|
|
17115
|
+
problems.push(`${step.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
17116
|
+
}
|
|
17117
|
+
}
|
|
17118
|
+
for (const page of pages) {
|
|
17119
|
+
page.links = page.links.filter((link) => {
|
|
17120
|
+
const target = createdSteps.find((s) => link.to === s.newStepId || link.to.includes(`/preview/${s.newPageId}`) || link.to.split(/[?#]/)[0].replace(/^\//, "") === s.newPath.slice(1));
|
|
17121
|
+
if (target && !pages.find((p) => p.newPageId === target.newPageId)?.readBackOk) {
|
|
17122
|
+
page.unmapped.push({ from: link.from, reason: `replacement ${target.name} did not read back; saved link points to ${link.to}` });
|
|
17123
|
+
return false;
|
|
17105
17124
|
}
|
|
17106
|
-
|
|
17107
|
-
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
17125
|
+
return true;
|
|
17126
|
+
});
|
|
17127
|
+
}
|
|
17128
|
+
let sharedStored = [];
|
|
17129
|
+
const sharedSections = { found: sharedReadError ? "unknown" : sharedOriginal.length, repainted: false };
|
|
17130
|
+
if (sharedReadError) problems.push(`shared sections: ${sharedReadError}`);
|
|
17131
|
+
if (sharedOriginal.length && !namedFunnelId) {
|
|
17132
|
+
sharedSections.note = "Shared sections were left alone: this funnel was identified by comparing the account before and after, not by GoHighLevel naming it, and repainting shared sections on the wrong funnel would change every page of it.";
|
|
17133
|
+
} else if (sharedOriginal.length) {
|
|
17134
|
+
const previousVersion = funnel.globalSectionVersion;
|
|
17135
|
+
const previousUrl = funnel.globalSectionsUrl;
|
|
17136
|
+
try {
|
|
17137
|
+
const gs = sharedOriginal;
|
|
17138
|
+
const painted = repaintGlobalSections(gs, brand, palette);
|
|
17139
|
+
warnings.push(...painted.warnings);
|
|
17140
|
+
const fresh = (await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=${FUNNEL_PAGE}`)).funnels?.find((f) => f._id === funnel._id);
|
|
17141
|
+
const version = nextGlobalSectionVersion(fresh?.globalSectionVersion ?? funnel.globalSectionVersion);
|
|
17142
|
+
const uploaded = await sharedSectionsUpload(funnel._id, { version, sectionData: painted.sections });
|
|
17143
|
+
const wanted = colourDefs(painted.sections, siteColours);
|
|
17144
|
+
const swappedAway = painted.swaps.map((sw) => sw.from).filter((from) => /^#[0-9a-f]{6}$/i.test(from));
|
|
17145
|
+
const compare = async (url, what) => {
|
|
17146
|
+
const back = await publicRead(url);
|
|
17147
|
+
if (!back.ok) throw new Error(`${what} read-back failed (${back.status})`);
|
|
17148
|
+
const stored = await back.json();
|
|
17149
|
+
const got = colourDefs(Array.isArray(stored) ? stored : [], siteColours);
|
|
17150
|
+
const notAsSaved = [...wanted].filter(([name, value]) => !sameColour(got.get(name), value)).map(([name]) => name);
|
|
17151
|
+
if (notAsSaved.length) throw new Error(`${what} read back with ${notAsSaved.length} site colour(s) not as saved: ${notAsSaved.join(", ")}`);
|
|
17152
|
+
const storedText = JSON.stringify(stored).toLowerCase();
|
|
17153
|
+
const survivors = swappedAway.filter((from) => storedText.includes(from.toLowerCase()));
|
|
17154
|
+
if (survivors.length) throw new Error(`${what} read back still carrying the template's ${survivors.join(", ")}, which the repaint replaced`);
|
|
17155
|
+
return Array.isArray(stored) ? stored : [];
|
|
17156
|
+
};
|
|
17157
|
+
if (typeof uploaded.globalSectionsDownloadUrl === "string" && uploaded.globalSectionsDownloadUrl) sharedStored = await compare(uploaded.globalSectionsDownloadUrl, "the shared sections the write stored");
|
|
17158
|
+
let row;
|
|
17159
|
+
for (let i = 0; i < 5; i++) {
|
|
17160
|
+
const listed = await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=${FUNNEL_PAGE}`);
|
|
17161
|
+
row = listed.funnels?.find((f) => f._id === funnel._id);
|
|
17162
|
+
if (row && row.globalSectionVersion === version && typeof row.globalSectionsUrl === "string" && row.globalSectionsUrl) break;
|
|
17163
|
+
row = void 0;
|
|
17164
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
17165
|
+
}
|
|
17166
|
+
if (!row || typeof row.globalSectionsUrl !== "string") {
|
|
17167
|
+
throw new Error(`the write was accepted (version ${version}) but the funnel record has not caught up; pages render the shared sections the record points at, so what a visitor sees is unknown`);
|
|
17168
|
+
}
|
|
17169
|
+
sharedStored = await compare(row.globalSectionsUrl, "the shared sections the funnel record points at");
|
|
17170
|
+
Object.assign(sharedSections, {
|
|
17171
|
+
repainted: true,
|
|
17172
|
+
version,
|
|
17173
|
+
previousVersion: previousVersion ?? null,
|
|
17174
|
+
previousUrl: previousUrl ?? null,
|
|
17175
|
+
readBack: `${gs.length} shared section(s) repainted at the funnel level and read back at version ${version}.`,
|
|
17176
|
+
note: "Shared sections are funnel-wide, and their colour definitions win for every page of the funnel, so the original pages now render in the brand's colours too, not only their header and footer.",
|
|
17177
|
+
undo: `Re-upload the previous shared sections${typeof previousUrl === "string" ? ` (still stored at ${previousUrl})` : ""} as version ${nextGlobalSectionVersion(version)}, or delete the funnel.`
|
|
17125
17178
|
});
|
|
17126
17179
|
} catch (e) {
|
|
17127
|
-
problems.push(
|
|
17180
|
+
problems.push(`shared sections: ${e instanceof Error ? e.message : String(e)}`);
|
|
17181
|
+
}
|
|
17182
|
+
}
|
|
17183
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
17184
|
+
const templateVars = /* @__PURE__ */ new Map();
|
|
17185
|
+
for (const l of loaded) for (const [name, hex] of colourDefs(l.envelope, siteColours)) templateVars.set(name, hex);
|
|
17186
|
+
for (const [name, hex] of colourDefs(sharedOriginal, siteColours)) templateVars.set(name, hex);
|
|
17187
|
+
const readServed = async (url) => {
|
|
17188
|
+
let lastError = "";
|
|
17189
|
+
for (let i = 0; i < 4; i++) {
|
|
17190
|
+
try {
|
|
17191
|
+
const res = await publicRead(`${url}${url.includes("?") ? "&" : "?"}v=${Date.now()}`);
|
|
17192
|
+
if (res.ok) return await res.text();
|
|
17193
|
+
lastError = `HTTP ${res.status}`;
|
|
17194
|
+
} catch (e) {
|
|
17195
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
17196
|
+
}
|
|
17197
|
+
if (i < 3) await new Promise((r) => setTimeout(r, 1500));
|
|
17198
|
+
}
|
|
17199
|
+
throw new Error(`the served page could not be read (${lastError})`);
|
|
17200
|
+
};
|
|
17201
|
+
for (const page of pages) {
|
|
17202
|
+
if (!page.readBackOk) continue;
|
|
17203
|
+
try {
|
|
17204
|
+
const expectedVars = /* @__PURE__ */ new Map();
|
|
17205
|
+
for (const [name, hex] of colourDefs(storedByPage.get(page.newPageId), siteColours)) expectedVars.set(name, hex);
|
|
17206
|
+
for (const [name, hex] of colourDefs(sharedStored, siteColours)) expectedVars.set(name, hex);
|
|
17207
|
+
const sharedRepainted = sharedSections.repainted === true;
|
|
17208
|
+
let verdict = servedVerdict({ html: await readServed(page.preview), templateVars, expectedVars, sharedRepainted });
|
|
17209
|
+
if (verdict.verdict !== "brand" && verdict.verdict !== "none") {
|
|
17210
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
17211
|
+
verdict = servedVerdict({ html: await readServed(page.preview), templateVars, expectedVars, sharedRepainted });
|
|
17212
|
+
}
|
|
17213
|
+
page.served = { verdict: verdict.verdict, summary: verdict.summary, brand: verdict.brand, stillTemplate: verdict.stillTemplate, unexplained: verdict.unexplained, undeclared: verdict.undeclared };
|
|
17214
|
+
page.paintedOk = verdict.verdict === "brand" || verdict.verdict === "none";
|
|
17215
|
+
if (!page.paintedOk) {
|
|
17216
|
+
const said = verdict.verdict === "template" ? "served page still paints the template" : verdict.verdict === "mixed" ? "served page paints colours this install did not save" : "served page could not be judged";
|
|
17217
|
+
if (page.repaint) page.repaint = { ...page.repaint, storedVerdict: page.repaint.verdict, verdict: said };
|
|
17218
|
+
problems.push(`${page.page}: ${verdict.summary}`);
|
|
17219
|
+
}
|
|
17220
|
+
} catch (e) {
|
|
17221
|
+
page.paintedOk = false;
|
|
17222
|
+
problems.push(`${page.page}: ${e instanceof Error ? e.message : String(e)}`);
|
|
17128
17223
|
}
|
|
17129
17224
|
}
|
|
17225
|
+
const rebuilt = problems.length === 0 && pages.length === originals.length && pages.length > 0 && pages.every((p) => p.readBackOk && p.paintedOk === true);
|
|
17226
|
+
const unjudged = pages.filter((p) => p.served?.verdict === "none").map((p) => p.page);
|
|
17227
|
+
const holdBack = removalHoldBack({ truncated: inventory.truncated, multiPageStepNames: multiPage.map((m) => m.name), identifiedFromResponse: Boolean(namedFunnelId) && !inventory.truncated }) ?? (unjudged.length ? `the served page had no site colour to judge on ${unjudged.join(", ")}, so the repaint was not proven there; the originals are kept for you to compare` : null);
|
|
17130
17228
|
let removed = 0;
|
|
17131
|
-
const
|
|
17132
|
-
const
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
if (complete && removeOriginals && holdBack === null) {
|
|
17136
|
-
for (const d of done) {
|
|
17229
|
+
const accepted = [];
|
|
17230
|
+
const removalFailures = [];
|
|
17231
|
+
if (removeOriginals === true && rebuilt && holdBack === null) {
|
|
17232
|
+
for (const step of originals) {
|
|
17137
17233
|
try {
|
|
17138
|
-
await funnelRequest("DELETE", `/funnel/step/${funnel._id}/${
|
|
17139
|
-
|
|
17234
|
+
await funnelRequest("DELETE", `/funnel/step/${funnel._id}/${step.stepId}`);
|
|
17235
|
+
accepted.push(step);
|
|
17140
17236
|
} catch (e) {
|
|
17141
|
-
|
|
17237
|
+
removalFailures.push(`${step.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
17142
17238
|
}
|
|
17143
17239
|
}
|
|
17144
17240
|
}
|
|
17241
|
+
const confirmed = /* @__PURE__ */ new Set();
|
|
17242
|
+
for (let attempt = 0; accepted.length && attempt < 5; attempt++) {
|
|
17243
|
+
if (attempt) await new Promise((resolve12) => setTimeout(resolve12, attempt * 1e3));
|
|
17244
|
+
try {
|
|
17245
|
+
const listed = await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=${FUNNEL_PAGE}`);
|
|
17246
|
+
const current = listed.funnels?.find((item) => item._id === funnel._id);
|
|
17247
|
+
if (current && Array.isArray(current.steps)) {
|
|
17248
|
+
for (const step of accepted) if (!current.steps.some((item) => item.id === step.stepId)) confirmed.add(step.stepId);
|
|
17249
|
+
}
|
|
17250
|
+
if (confirmed.size === accepted.length) break;
|
|
17251
|
+
} catch {
|
|
17252
|
+
}
|
|
17253
|
+
}
|
|
17254
|
+
removed = confirmed.size;
|
|
17255
|
+
const unconfirmed = accepted.filter((step) => !confirmed.has(step.stepId));
|
|
17256
|
+
problems.push(...unconfirmed.map((step) => `${step.name}: removal accepted, not yet confirmed gone. Read the funnel again before taking further action.`));
|
|
17257
|
+
problems.push(...removalFailures.map((line3) => `Could not remove original step ${line3}`));
|
|
17258
|
+
const complete = rebuilt && problems.length === 0;
|
|
17145
17259
|
return {
|
|
17146
17260
|
ok: complete,
|
|
17147
17261
|
funnel: { id: funnel._id, name: funnel.name },
|
|
17148
|
-
brand: { slug: brand.slug, accent: brand.colors.accent.hex, display: brand.pageStudioTheme.headingFont, body: brand.pageStudioTheme.bodyFont },
|
|
17262
|
+
...brand && { brand: { slug: brand.slug, accent: brand.colors.accent.hex, display: brand.pageStudioTheme.headingFont, body: brand.pageStudioTheme.bodyFont } },
|
|
17149
17263
|
templatePalette: { accent: palette.accent, surface: palette.surface, text: palette.text, secondary: palette.secondary, faces: palette.fonts, undecided: palette.undecided },
|
|
17150
|
-
pages
|
|
17264
|
+
pages,
|
|
17265
|
+
sharedSections,
|
|
17151
17266
|
...(() => {
|
|
17152
|
-
const
|
|
17153
|
-
const
|
|
17267
|
+
const servedWith = (v) => pages.filter((d) => d.served?.verdict === v).map((d) => d.page);
|
|
17268
|
+
const servedTemplate = servedWith("template");
|
|
17269
|
+
const servedMixed = servedWith("mixed");
|
|
17270
|
+
const servedUnreadable = servedWith("unreadable");
|
|
17271
|
+
const gateNote = typeof sharedSections.note === "string" && sharedSections.repainted === false && sharedSections.found !== 0 ? " The shared sections were left alone (see sharedSections), which is why the served pages still show the template's colours." : "";
|
|
17272
|
+
const withRepaint = pages.filter((d) => d.repaint);
|
|
17273
|
+
const faint = withRepaint.filter((d) => (d.repaint.storedVerdict ?? d.repaint.verdict) !== "repainted").map((d) => d.page);
|
|
17274
|
+
const hard = withRepaint.filter((d) => (d.repaint.contrast ?? []).length > 0);
|
|
17154
17275
|
const lines = [
|
|
17276
|
+
...servedTemplate.length ? [`On ${servedTemplate.join(", ")} the SERVED page still paints the template's colours, whatever the stored page says; each page's served summary names them. Nothing was removed.${gateNote}`] : [],
|
|
17277
|
+
...servedMixed.length ? [`On ${servedMixed.join(", ")} the SERVED page paints colours this install did not save; each page's served summary names them. Nothing was removed.`] : [],
|
|
17278
|
+
...servedUnreadable.length ? [`On ${servedUnreadable.join(", ")} the served page could not be judged (GoHighLevel served no rendered page), so nothing was removed.`] : [],
|
|
17279
|
+
...servedWith("none").length ? [`On ${servedWith("none").join(", ")} the served page paints through no site colour the template defined, so the served check had nothing to judge; the stored-page checks stand, and the originals were kept for you to compare.`] : [],
|
|
17155
17280
|
...faint.length ? [`On ${faint.join(", ")} the visible palette barely changed. Each page's repaint says what still paints in the template's own colours.`] : [],
|
|
17156
|
-
...hard.length ? [`On ${hard.map((d) =>
|
|
17281
|
+
...hard.length ? [`On ${hard.map((d) => d.page).join(", ")} some type is now harder to read than WCAG AA allows; each page's repaint.contrast names the colours.`] : []
|
|
17157
17282
|
];
|
|
17158
|
-
const contrast = hard.flatMap((d) => d.repaint.contrast.map((c) => `${
|
|
17283
|
+
const contrast = hard.flatMap((d) => d.repaint.contrast.map((c) => `${d.page}: ${c.text} type on ${c.background} is ${c.after}:1 (was ${c.before}:1).`));
|
|
17159
17284
|
return {
|
|
17160
17285
|
...lines.length ? { repaintNote: `${lines.join(" ")} Look at the preview before calling it branded.` } : {},
|
|
17161
17286
|
...contrast.length ? { contrast } : {}
|
|
17162
17287
|
};
|
|
17163
17288
|
})(),
|
|
17164
|
-
originalsRemoved:
|
|
17165
|
-
...
|
|
17289
|
+
originalsRemoved: removed,
|
|
17290
|
+
...unconfirmed.length && { originalsUnconfirmed: unconfirmed.map((step) => step.stepId) },
|
|
17291
|
+
...removeOriginals === true && { removal: rebuilt && holdBack === null ? `${removed} original steps removed, ${unconfirmed.length} accepted but not yet confirmed gone, ${removalFailures.length} could not be removed${removalFailures.length ? `: ${removalFailures.join("; ")}` : ""}` : `0 original steps removed; removal was not attempted: ${holdBack ?? "not every replacement was verified"}` },
|
|
17292
|
+
...accepted.length === 0 && removed === 0 && { kept: `Nothing was deleted. ${removeOriginals !== true ? "Original removal was not requested." : holdBack ?? (!rebuilt ? "Not every replacement was verified." : "No originals were removed.")}` },
|
|
17293
|
+
...problems.length > 0 && { problems },
|
|
17166
17294
|
...namedButNotNew && { identification: "GoHighLevel named a funnel that this account ALREADY had before the install. That name was discarded and the account was compared instead, so the funnel above was identified the cautious way and originals are only removed if you asked for it explicitly. Worth reporting: it means the install response no longer means what it did." },
|
|
17167
|
-
|
|
17168
|
-
|
|
17169
|
-
next: done.length ? `Grade one before the client sees it: verify_design on ${String(done[0].preview)} with medium "site" and placement "ghl". GoHighLevel's own templates are designed but frequently fail contrast and tap-target rules.` : "Nothing was branded.",
|
|
17295
|
+
warnings: [.../* @__PURE__ */ new Set([...warnings, ...brand ? rethemeEnvelope({}, brand, palette).warnings : []])],
|
|
17296
|
+
next: pages.some((page) => page.readBackOk) ? `Grade one before the client sees it: verify_design on ${pages.find((page) => page.readBackOk)?.preview ?? ""} with medium "site" and placement "ghl". GoHighLevel's own templates are designed but frequently fail contrast and tap-target rules.` : "Nothing was branded.",
|
|
17170
17297
|
undo: `delete_funnel with funnelId "${funnel._id}" and confirm "DELETE" removes the whole thing.`
|
|
17171
17298
|
};
|
|
17172
17299
|
}
|
|
@@ -17229,21 +17356,24 @@ ${await response.text()}`);
|
|
|
17229
17356
|
integrations: computeIntegrations({ sections })
|
|
17230
17357
|
};
|
|
17231
17358
|
const saved = await autosaveRequest(pre.pageId, { funnelId, pageVersion: savePageVersion, pageData });
|
|
17232
|
-
const savedDataUrl = typeof saved.pageDataUrl === "string" ? saved.pageDataUrl : null;
|
|
17359
|
+
const savedDataUrl = typeof saved.pageDataUrl === "string" && saved.pageDataUrl.length > 0 ? saved.pageDataUrl : null;
|
|
17233
17360
|
let verified = false;
|
|
17361
|
+
let servedUrl = "";
|
|
17234
17362
|
for (let attempt = 1; attempt <= 8 && !verified; attempt++) {
|
|
17235
17363
|
await new Promise((r) => setTimeout(r, 4e3));
|
|
17236
17364
|
try {
|
|
17237
17365
|
const now = await funnelRequest("GET", `/page/${pre.pageId}?locationId=${client.locationId}`);
|
|
17238
|
-
|
|
17239
|
-
if (savedDataUrl !== null &&
|
|
17366
|
+
servedUrl = typeof now.pageDataDownloadUrl === "string" ? now.pageDataDownloadUrl : "";
|
|
17367
|
+
if (savedDataUrl !== null && servedUrl !== "" && servedUrl !== pre.priorDataUrl) verified = true;
|
|
17240
17368
|
} catch {
|
|
17241
17369
|
}
|
|
17242
17370
|
}
|
|
17243
17371
|
return {
|
|
17244
17372
|
saved,
|
|
17245
17373
|
verified,
|
|
17246
|
-
...verified ? {} : {
|
|
17374
|
+
...verified ? {} : {
|
|
17375
|
+
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."
|
|
17376
|
+
}
|
|
17247
17377
|
};
|
|
17248
17378
|
}
|
|
17249
17379
|
async function savePageGuarded(input) {
|
|
@@ -17377,6 +17507,8 @@ var init_page_studio = __esm({
|
|
|
17377
17507
|
init_workflow_builder_client();
|
|
17378
17508
|
init_ghl_templates();
|
|
17379
17509
|
init_retheme();
|
|
17510
|
+
init_served_palette();
|
|
17511
|
+
init_global_sections();
|
|
17380
17512
|
init_ghl_page();
|
|
17381
17513
|
init_looks();
|
|
17382
17514
|
init_store();
|
|
@@ -20541,6 +20673,10 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20541
20673
|
merge_field_warnings_total: 0,
|
|
20542
20674
|
workflows_with_shape_warnings: 0,
|
|
20543
20675
|
shape_warnings_total: 0,
|
|
20676
|
+
workflows_with_save_blocker_warnings: 0,
|
|
20677
|
+
save_blocker_warnings_total: 0,
|
|
20678
|
+
workflows_with_integration_warnings: 0,
|
|
20679
|
+
integration_warnings_total: 0,
|
|
20544
20680
|
workflows_unscannable: 0,
|
|
20545
20681
|
workflows_zero_references: 0,
|
|
20546
20682
|
// Nothing was scanned, so no reference category was loaded or needed.
|
|
@@ -20551,6 +20687,8 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20551
20687
|
workflows_with_issues: [],
|
|
20552
20688
|
merge_field_warnings: [],
|
|
20553
20689
|
shape_warnings: [],
|
|
20690
|
+
save_blocker_warnings: [],
|
|
20691
|
+
integration_warnings: [],
|
|
20554
20692
|
unscannable: [],
|
|
20555
20693
|
notes: catalog2.complete ? [] : [incompleteCatalogNote(catalog2.stopReason, 0, catalog2.reportedTotal)]
|
|
20556
20694
|
});
|
|
@@ -20587,6 +20725,12 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20587
20725
|
const warningsTotal = withWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
20588
20726
|
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
20727
|
const shapeWarningsTotal = withShapeWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
20728
|
+
const SAVE_BLOCKER_CATEGORIES = /* @__PURE__ */ new Set(["legacy_step_id", "legacy_wait_unit", "missing_required_attribute"]);
|
|
20729
|
+
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));
|
|
20730
|
+
const withSaveBlockerWarnings = bucket((f) => f.severity === "warning" && SAVE_BLOCKER_CATEGORIES.has(f.category));
|
|
20731
|
+
const saveBlockerWarningsTotal = withSaveBlockerWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
20732
|
+
const withIntegrationWarnings = bucket((f) => f.severity === "warning" && f.category === "missing_integration");
|
|
20733
|
+
const integrationWarningsTotal = withIntegrationWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
20590
20734
|
const unverifiedCats = ALL_CATEGORIES.filter((c) => lookups.status[c] !== "loaded");
|
|
20591
20735
|
const unverifiedRefs = results.reduce((n, r) => n + r.findings.filter((f) => f.severity === "unverified").length, 0);
|
|
20592
20736
|
return jsonResponse({
|
|
@@ -20606,14 +20750,24 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20606
20750
|
merge_field_warnings_total: warningsTotal,
|
|
20607
20751
|
workflows_with_shape_warnings: withShapeWarnings.length,
|
|
20608
20752
|
shape_warnings_total: shapeWarningsTotal,
|
|
20753
|
+
workflows_with_save_blocker_warnings: withSaveBlockerWarnings.length,
|
|
20754
|
+
save_blocker_warnings_total: saveBlockerWarningsTotal,
|
|
20755
|
+
workflows_with_integration_warnings: withIntegrationWarnings.length,
|
|
20756
|
+
integration_warnings_total: integrationWarningsTotal,
|
|
20609
20757
|
workflows_unscannable: unscannable.length,
|
|
20610
20758
|
workflows_zero_references: zeroRefCount,
|
|
20611
20759
|
unverified: { categories_unloaded: unverifiedCats, references_unverified: unverifiedRefs },
|
|
20760
|
+
// Unchanged on purpose: `status` has meant "are there ERRORS" since it
|
|
20761
|
+
// was written, and these four categories are warnings by ruling — the
|
|
20762
|
+
// workflows genuinely still run. What was broken is that they were
|
|
20763
|
+
// invisible, not that they were ranked too low.
|
|
20612
20764
|
status: errorsTotal > 0 ? "issues_found" : "ok"
|
|
20613
20765
|
},
|
|
20614
20766
|
workflows_with_issues: withErrors,
|
|
20615
20767
|
merge_field_warnings: withWarnings,
|
|
20616
20768
|
shape_warnings: withShapeWarnings,
|
|
20769
|
+
save_blocker_warnings: withSaveBlockerWarnings,
|
|
20770
|
+
integration_warnings: withIntegrationWarnings,
|
|
20617
20771
|
unscannable,
|
|
20618
20772
|
notes: [
|
|
20619
20773
|
// NEVER advise re-running for "the next batch": GHL's list endpoint ignores
|
|
@@ -20624,6 +20778,8 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
20624
20778
|
...unverifiedCats.length ? [`Could not fully load: ${unverifiedCats.join(", ")} \u2014 references to those are 'unverified', not 'broken'. Re-run.`] : [],
|
|
20625
20779
|
...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
20780
|
...shapeWarningsTotal ? [`${shapeWarningsTotal} Create/Update Opportunity step(s) have only pipeline + stage \u2014 they cannot create a card for a new contact (see shape_warnings).`] : [],
|
|
20781
|
+
...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.`] : [],
|
|
20782
|
+
...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
20783
|
"workflow_goal, goto, and unrecognized condition types are not deeply checked in this version."
|
|
20628
20784
|
]
|
|
20629
20785
|
});
|
|
@@ -27524,6 +27680,7 @@ Use these authoritative normalized intake answers when provided: ${JSON.stringif
|
|
|
27524
27680
|
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
27681
|
9. ${MISSING_STEP_RULE}
|
|
27526
27682
|
10. ${NO_INVENTED_CAUSE_RULE}
|
|
27683
|
+
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
27684
|
Finish with EXACTLY one final line:
|
|
27528
27685
|
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
27686
|
},
|
|
@@ -31568,7 +31725,7 @@ var require_package = __commonJS({
|
|
|
31568
31725
|
"package.json"(exports2, module2) {
|
|
31569
31726
|
module2.exports = {
|
|
31570
31727
|
name: "@elitedcs/ghl-mcp",
|
|
31571
|
-
version: "3.84.
|
|
31728
|
+
version: "3.84.3",
|
|
31572
31729
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
31573
31730
|
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
31731
|
main: "dist/index.js",
|
|
@@ -35255,8 +35412,8 @@ var init_capability_runs = __esm({
|
|
|
35255
35412
|
staff: ["get_users", "get_user", "get_calendars"],
|
|
35256
35413
|
phone: ["list_phone_numbers", "list_number_pools", "get_location"],
|
|
35257
35414
|
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
|
|
35415
|
+
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"],
|
|
35416
|
+
// contact segments live on the contacts service (3.84.2); no bulk tagging: that is a mass write the operator does by hand
|
|
35260
35417
|
templates: ["list_email_templates", "create_email_template", "update_email_template", "list_message_templates", "create_sms_template", "get_custom_values"],
|
|
35261
35418
|
broadcasts: ["list_email_templates", "create_email_template", "update_email_template", "list_smart_lists", "get_email_campaigns"],
|
|
35262
35419
|
// drafts the email as a template; sending or scheduling is the operator's click
|
|
@@ -39987,12 +40144,12 @@ function routeFirebaseForCompany(builderClient, registry2, companyId) {
|
|
|
39987
40144
|
companyId
|
|
39988
40145
|
});
|
|
39989
40146
|
return `
|
|
39990
|
-
Firebase:
|
|
40147
|
+
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
40148
|
}
|
|
39992
40149
|
builderClient.resetToHomeFirebase();
|
|
39993
40150
|
const homeCompanyId = builderClient.getCurrentCompanyId();
|
|
39994
40151
|
if (homeCompanyId === companyId) {
|
|
39995
|
-
return "\nFirebase: home company \u2014 workflow builder
|
|
40152
|
+
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
40153
|
}
|
|
39997
40154
|
const registeredCompanies = registry2?.listCompanyFirebases?.() ?? [];
|
|
39998
40155
|
if (registeredCompanies.length > 0) {
|
|
@@ -40223,7 +40380,17 @@ Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and
|
|
|
40223
40380
|
isError: true
|
|
40224
40381
|
};
|
|
40225
40382
|
}
|
|
40226
|
-
registry2.removeLocation(locationId2);
|
|
40383
|
+
const removed = registry2.removeLocation(locationId2);
|
|
40384
|
+
const saveError = registry2.getLastSaveError();
|
|
40385
|
+
if (!removed || saveError) {
|
|
40386
|
+
return {
|
|
40387
|
+
content: [{
|
|
40388
|
+
type: "text",
|
|
40389
|
+
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.`
|
|
40390
|
+
}],
|
|
40391
|
+
isError: true
|
|
40392
|
+
};
|
|
40393
|
+
}
|
|
40227
40394
|
return {
|
|
40228
40395
|
content: [{ type: "text", text: `Removed: ${token2.name} (${locationId2}) from token registry.` }]
|
|
40229
40396
|
};
|
|
@@ -40513,11 +40680,13 @@ ${lines.join("\n")}
|
|
|
40513
40680
|
try {
|
|
40514
40681
|
const agencyClient = new GHLClient({ apiKey: agencyKey });
|
|
40515
40682
|
await agencyClient.delete(`/locations/${location_id}`, { params: { deleteTwilioAccount: delete_twilio ?? false } });
|
|
40683
|
+
const hadKey = Boolean(registry2?.getToken(location_id));
|
|
40516
40684
|
const wasRegistered = registry2?.removeLocation(location_id) ?? false;
|
|
40685
|
+
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
40686
|
return {
|
|
40518
40687
|
content: [{
|
|
40519
40688
|
type: "text",
|
|
40520
|
-
text: `Sub-account ${location_id} permanently deleted.${
|
|
40689
|
+
text: `Sub-account ${location_id} permanently deleted.${keyLine}`
|
|
40521
40690
|
}]
|
|
40522
40691
|
};
|
|
40523
40692
|
} catch (error) {
|
|
@@ -41781,6 +41950,28 @@ function buildContactSmartListBody(args) {
|
|
|
41781
41950
|
locationId: args.locationId
|
|
41782
41951
|
};
|
|
41783
41952
|
}
|
|
41953
|
+
function findSmartListRecord(payload) {
|
|
41954
|
+
const looksLikeRecord = (v) => {
|
|
41955
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
41956
|
+
const o = v;
|
|
41957
|
+
return "objectKey" in o || "filters" in o || "id" in o || "_id" in o || "name" in o;
|
|
41958
|
+
};
|
|
41959
|
+
if (looksLikeRecord(payload)) return payload;
|
|
41960
|
+
if (payload && typeof payload === "object") {
|
|
41961
|
+
for (const key of ["list", "smartList", "smartlist", "data", "record"]) {
|
|
41962
|
+
const nested = payload[key];
|
|
41963
|
+
if (looksLikeRecord(nested)) return nested;
|
|
41964
|
+
}
|
|
41965
|
+
}
|
|
41966
|
+
return void 0;
|
|
41967
|
+
}
|
|
41968
|
+
function contactServiceNote(payload) {
|
|
41969
|
+
const rec = findSmartListRecord(payload);
|
|
41970
|
+
if (!rec) return void 0;
|
|
41971
|
+
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;
|
|
41972
|
+
if (!reason2) return void 0;
|
|
41973
|
+
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.`;
|
|
41974
|
+
}
|
|
41784
41975
|
function registerSmartListTools(server2, builderClient) {
|
|
41785
41976
|
if (!builderClient) return;
|
|
41786
41977
|
const client = builderClient;
|
|
@@ -41909,7 +42100,8 @@ ${await res.text()}`);
|
|
|
41909
42100
|
try {
|
|
41910
42101
|
const loc = locationId2 ?? client.locationId;
|
|
41911
42102
|
const result = await smartListRequest("GET", `/${loc}/${listId}`);
|
|
41912
|
-
|
|
42103
|
+
const note = contactServiceNote(result);
|
|
42104
|
+
return note ? jsonResponse({ smart_list: result, note }) : jsonResponse(result);
|
|
41913
42105
|
} catch (error) {
|
|
41914
42106
|
return errorResponse(error);
|
|
41915
42107
|
}
|
|
@@ -41917,10 +42109,10 @@ ${await res.text()}`);
|
|
|
41917
42109
|
);
|
|
41918
42110
|
server2.tool(
|
|
41919
42111
|
"create_smart_list",
|
|
41920
|
-
|
|
42112
|
+
'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
42113
|
{
|
|
41922
42114
|
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. '
|
|
42115
|
+
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
42116
|
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
42117
|
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
42118
|
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 +42121,11 @@ ${await res.text()}`);
|
|
|
41929
42121
|
},
|
|
41930
42122
|
async ({ name, objectKey, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
|
|
41931
42123
|
try {
|
|
42124
|
+
if (objectKey === "contacts") {
|
|
42125
|
+
throw new Error(
|
|
42126
|
+
"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."
|
|
42127
|
+
);
|
|
42128
|
+
}
|
|
41932
42129
|
const loc = locationId2 ?? client.locationId;
|
|
41933
42130
|
const body = { name, objectKey };
|
|
41934
42131
|
if (filters !== void 0) body.filters = filters;
|
|
@@ -41966,8 +42163,13 @@ ${await res.text()}`);
|
|
|
41966
42163
|
if (Object.keys(body).length === 0) {
|
|
41967
42164
|
throw new Error("Pass at least one of: name, filters, columns, pipelineIds, defaultInPipelines. Nothing to update.");
|
|
41968
42165
|
}
|
|
42166
|
+
let note;
|
|
42167
|
+
try {
|
|
42168
|
+
note = contactServiceNote(await smartListRequest("GET", `/${loc}/${listId}`));
|
|
42169
|
+
} catch {
|
|
42170
|
+
}
|
|
41969
42171
|
const result = await smartListRequest("PUT", `/${loc}/${listId}`, body);
|
|
41970
|
-
return jsonResponse(result);
|
|
42172
|
+
return note ? jsonResponse({ updated: result, note }) : jsonResponse(result);
|
|
41971
42173
|
} catch (error) {
|
|
41972
42174
|
return errorResponse(error);
|
|
41973
42175
|
}
|
|
@@ -41984,8 +42186,13 @@ ${await res.text()}`);
|
|
|
41984
42186
|
async ({ listId, locationId: locationId2 }) => {
|
|
41985
42187
|
try {
|
|
41986
42188
|
const loc = locationId2 ?? client.locationId;
|
|
42189
|
+
let note;
|
|
42190
|
+
try {
|
|
42191
|
+
note = contactServiceNote(await smartListRequest("GET", `/${loc}/${listId}`));
|
|
42192
|
+
} catch {
|
|
42193
|
+
}
|
|
41987
42194
|
const result = await smartListRequest("DELETE", `/${loc}/${listId}`);
|
|
41988
|
-
return jsonResponse(result);
|
|
42195
|
+
return note ? jsonResponse({ deleted: result, note }) : jsonResponse(result);
|
|
41989
42196
|
} catch (error) {
|
|
41990
42197
|
return errorResponse(error);
|
|
41991
42198
|
}
|
|
@@ -42551,11 +42758,11 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
42551
42758
|
if (template.location) {
|
|
42552
42759
|
const locationData = resolveObj(template.location);
|
|
42553
42760
|
const BRAND_FIELDS = /* @__PURE__ */ new Set(["name", "address", "city", "state", "postalCode", "country", "phone", "website", "email"]);
|
|
42554
|
-
const
|
|
42761
|
+
const isRec2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
42555
42762
|
let currentLoc = null;
|
|
42556
42763
|
try {
|
|
42557
42764
|
const raw = await client.get(`/locations/${locId}`);
|
|
42558
|
-
currentLoc =
|
|
42765
|
+
currentLoc = isRec2(raw) && isRec2(raw.location) ? raw.location : isRec2(raw) ? raw : null;
|
|
42559
42766
|
} catch {
|
|
42560
42767
|
currentLoc = null;
|
|
42561
42768
|
}
|
|
@@ -43377,6 +43584,9 @@ function registerCheckupTools(server2, installedVersion, client, builderClient,
|
|
|
43377
43584
|
format: import_zod64.z.enum(["report", "json"]).optional().describe("'report' (default) is the plain-English checkup. 'json' returns the same data for scripts and support tickets.")
|
|
43378
43585
|
},
|
|
43379
43586
|
async ({ locationId: locationId2, format }) => {
|
|
43587
|
+
if (locationId2 && locationId2 !== client.defaultLocationId) {
|
|
43588
|
+
return { content: [{ type: "text", text: "Switch to that sub-account first, then run its checkup. No account was read." }], isError: true };
|
|
43589
|
+
}
|
|
43380
43590
|
let loc;
|
|
43381
43591
|
try {
|
|
43382
43592
|
loc = client.resolveLocationId(locationId2);
|
|
@@ -48965,7 +49175,10 @@ var init_verify_findings = __esm({
|
|
|
48965
49175
|
tag: { path: (loc) => `/locations/${loc}/tags`, ids: (raw) => idsOf(raw, "tags"), paged: true },
|
|
48966
49176
|
calendar: { path: () => "/calendars/", params: byLocation, ids: (raw) => idsOf(raw, "calendars") },
|
|
48967
49177
|
form: { path: () => "/forms/", params: byLocation, ids: (raw) => idsOf(raw, "forms"), paged: true },
|
|
48968
|
-
user: { path: () => "/users/", params: byLocation, ids: (raw) => idsOf(raw, "users") }
|
|
49178
|
+
user: { path: () => "/users/", params: byLocation, ids: (raw) => idsOf(raw, "users") },
|
|
49179
|
+
// The list get_surveys reads; it takes limit/skip, so it is read the way
|
|
49180
|
+
// forms are, page by page and never assumed complete (3.84.2).
|
|
49181
|
+
survey: { path: () => "/surveys/", params: byLocation, ids: (raw) => idsOf(raw, "surveys"), paged: true }
|
|
48969
49182
|
};
|
|
48970
49183
|
CHECKABLE_KIND_NAMES = Object.keys(CHECKABLE_KINDS).join(", ");
|
|
48971
49184
|
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 +49545,13 @@ function scanArgs(args, maxNodes = MAX_ARG_NODES) {
|
|
|
49332
49545
|
continue;
|
|
49333
49546
|
}
|
|
49334
49547
|
if (!isRecord10(v)) continue;
|
|
49335
|
-
const
|
|
49548
|
+
const isNode = typeof v.type === "string" && v.type && (isRecord10(v.attributes) || Array.isArray(v.conditions));
|
|
49549
|
+
const nodeType = isNode ? v.type : parentType;
|
|
49336
49550
|
const refs = [];
|
|
49337
49551
|
if (typeof v.type === "string" && isRecord10(v.attributes)) extractFromAction(v, refs);
|
|
49338
49552
|
if (Array.isArray(v.conditions)) extractFromTrigger(v, refs);
|
|
49339
49553
|
for (const r of refs) {
|
|
49340
|
-
if (r.byKey) continue;
|
|
49554
|
+
if (r.byKey || !r.id.trim()) continue;
|
|
49341
49555
|
const kind = AUDIT_KIND[r.kind];
|
|
49342
49556
|
if (kind) out.ids.push({ kind, id: r.id, key: r.where });
|
|
49343
49557
|
else out.uncheckable.push({ kind: r.kind, id: r.id, key: r.where });
|
|
@@ -49410,6 +49624,8 @@ var init_arg_ids = __esm({
|
|
|
49410
49624
|
formId: "form",
|
|
49411
49625
|
formIds: "form",
|
|
49412
49626
|
// a form_submission goal's extras (build_goal_event)
|
|
49627
|
+
surveyId: "survey",
|
|
49628
|
+
// get_surveys reads /surveys/, so a survey is read back like a form (3.84.2)
|
|
49413
49629
|
userId: "user",
|
|
49414
49630
|
userIds: "user",
|
|
49415
49631
|
assignedUserId: "user",
|
|
@@ -49424,6 +49640,12 @@ var init_arg_ids = __esm({
|
|
|
49424
49640
|
...ID_ARG_KINDS,
|
|
49425
49641
|
"form.id": "form",
|
|
49426
49642
|
"calendar.id": "calendar",
|
|
49643
|
+
// The payment and task triggers name their form, calendar and assignee under
|
|
49644
|
+
// their own prefixes (src/trigger-schemas.ts); unread until 3.84.2.
|
|
49645
|
+
"payment.form.id": "form",
|
|
49646
|
+
"payment.calendar.id": "calendar",
|
|
49647
|
+
"task.assignedTo": "user",
|
|
49648
|
+
"survey.id": "survey",
|
|
49427
49649
|
"opportunity.pipelineId": "pipeline",
|
|
49428
49650
|
"opportunity.pipelineStageId": "pipeline_stage",
|
|
49429
49651
|
"opportunity.assignedTo": "user",
|
|
@@ -49438,7 +49660,7 @@ var init_arg_ids = __esm({
|
|
|
49438
49660
|
workflow: "workflow",
|
|
49439
49661
|
form: "form",
|
|
49440
49662
|
calendar: "calendar",
|
|
49441
|
-
survey:
|
|
49663
|
+
survey: "survey"
|
|
49442
49664
|
});
|
|
49443
49665
|
NOT_CHECKABLE_ID_ARGS = Object.freeze({
|
|
49444
49666
|
// The account itself, not an object inside it. One account per plan is
|
|
@@ -49499,7 +49721,6 @@ var init_arg_ids = __esm({
|
|
|
49499
49721
|
linkId: "trigger links have no read-back list here",
|
|
49500
49722
|
blogId: "blogs have no read-back list here",
|
|
49501
49723
|
categoryId: "categories have no read-back list here",
|
|
49502
|
-
surveyId: "surveys have no read-back list here",
|
|
49503
49724
|
submissionId: "form submissions have no read-back list here",
|
|
49504
49725
|
mediaId: "media files have no read-back list here",
|
|
49505
49726
|
photoAssetId: "media files have no read-back list here",
|
|
@@ -49556,6 +49777,10 @@ var init_arg_ids = __esm({
|
|
|
49556
49777
|
// src/tools/agency-build.ts
|
|
49557
49778
|
function accountMismatch(a, active) {
|
|
49558
49779
|
if (!a.locationId) {
|
|
49780
|
+
const applied = a.applied.filter((s) => s.state === "applied").length;
|
|
49781
|
+
if (applied > 0) {
|
|
49782
|
+
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.`;
|
|
49783
|
+
}
|
|
49559
49784
|
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
49785
|
}
|
|
49561
49786
|
if (active === a.locationId) return null;
|
|
@@ -50235,7 +50460,7 @@ function dailyBrief(input) {
|
|
|
50235
50460
|
);
|
|
50236
50461
|
}
|
|
50237
50462
|
}
|
|
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;
|
|
50463
|
+
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
50464
|
let headline3;
|
|
50240
50465
|
if (today2.length === 1) {
|
|
50241
50466
|
headline3 = `You have one call today \u2014 ${today2[0].who ?? today2[0].client} at ${today2[0].at}.`;
|
|
@@ -50266,6 +50491,8 @@ function dailyBrief(input) {
|
|
|
50266
50491
|
headline3 = `Nothing needs you in what I could read \u2014 but ${unread[0].client} could not be read this morning.`;
|
|
50267
50492
|
} else if (unread.length > 1) {
|
|
50268
50493
|
headline3 = `Nothing needs you in what I could read \u2014 but ${unread.length} accounts could not be read this morning.`;
|
|
50494
|
+
} else if (yourDayNote) {
|
|
50495
|
+
headline3 = "Nothing needs you in what I could read \u2014 but your own calendar could not be read this morning.";
|
|
50269
50496
|
} else if (floorUncheckable.length) {
|
|
50270
50497
|
const c = floorUncheckable[0].client;
|
|
50271
50498
|
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 +51143,45 @@ function translateWorkflowAudit(audit, inputs = {}) {
|
|
|
50916
51143
|
evidence: [{ tool: "audit_workflows", detail: `${s.shape_warnings_total ?? shapeCount} steps` }]
|
|
50917
51144
|
});
|
|
50918
51145
|
}
|
|
51146
|
+
const blockerCount = s.workflows_with_save_blocker_warnings ?? 0;
|
|
51147
|
+
if (blockerCount > 0) {
|
|
51148
|
+
const rows2 = audit.save_blocker_warnings ?? [];
|
|
51149
|
+
const names = rows2.map((r) => r.workflowName ?? "").filter(Boolean);
|
|
51150
|
+
const cats = new Set(rows2.flatMap((r) => (r.warnings ?? []).map((w) => w.category ?? "")).filter(Boolean));
|
|
51151
|
+
const editBlocked = cats.has("legacy_step_id") || cats.has("legacy_wait_unit");
|
|
51152
|
+
const alertsNobody = cats.has("missing_required_attribute");
|
|
51153
|
+
const unknownKind = !editBlocked && !alertsNobody;
|
|
51154
|
+
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.`;
|
|
51155
|
+
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.`;
|
|
51156
|
+
out.push({
|
|
51157
|
+
id: "steps-that-block-saving",
|
|
51158
|
+
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`,
|
|
51159
|
+
what: `${nameList(names, blockerCount)} ${blockerCount === 1 ? "has" : "have"} at least one step of this kind.` + (editBlocked || unknownKind ? editSentence : "") + (alertsNobody || unknownKind ? alertSentence : ""),
|
|
51160
|
+
severity: "high",
|
|
51161
|
+
impact: alertsNobody && !editBlocked ? "leads" : "efficiency",
|
|
51162
|
+
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."] },
|
|
51163
|
+
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.",
|
|
51164
|
+
fix: "guided",
|
|
51165
|
+
minutes: Math.max(10, blockerCount * 8),
|
|
51166
|
+
evidence: [{ tool: "audit_workflows", detail: `${s.save_blocker_warnings_total ?? blockerCount} steps across ${blockerCount} workflows` }]
|
|
51167
|
+
});
|
|
51168
|
+
}
|
|
51169
|
+
const integrationCount = s.workflows_with_integration_warnings ?? 0;
|
|
51170
|
+
if (integrationCount > 0) {
|
|
51171
|
+
const names = (audit.integration_warnings ?? []).map((r) => r.workflowName ?? "").filter(Boolean);
|
|
51172
|
+
out.push({
|
|
51173
|
+
id: "steps-without-their-account",
|
|
51174
|
+
title: `${integrationCount} automation${integrationCount === 1 ? " has a step" : "s have steps"} waiting on a social account that is not connected`,
|
|
51175
|
+
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.`,
|
|
51176
|
+
severity: "high",
|
|
51177
|
+
impact: "leads",
|
|
51178
|
+
money: { confidence: "unknown", assumptions: ["Not costed: it depends how many people reach that step, which we cannot read."] },
|
|
51179
|
+
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.)",
|
|
51180
|
+
fix: "manual",
|
|
51181
|
+
minutes: Math.max(10, integrationCount * 5),
|
|
51182
|
+
evidence: [{ tool: "audit_workflows", detail: `${s.integration_warnings_total ?? integrationCount} steps` }]
|
|
51183
|
+
});
|
|
51184
|
+
}
|
|
50919
51185
|
return out;
|
|
50920
51186
|
}
|
|
50921
51187
|
function notCheckedFrom(audit) {
|
|
@@ -51655,6 +51921,15 @@ function publishNote(r) {
|
|
|
51655
51921
|
if (r.publishFailed.length) {
|
|
51656
51922
|
const lead = r.published.length ? `Auto-published ${r.published.length} workflow(s) live, but ` : "";
|
|
51657
51923
|
const detail = r.publishFailed.map((p) => `${p.name} (${p.error})`).join("; ");
|
|
51924
|
+
const inert = r.publishFailed.filter((p) => p.kind === "inert"), unverified = r.publishFailed.filter((p) => p.kind === "not_verified");
|
|
51925
|
+
const drafts = r.publishFailed.filter((p) => p.kind !== "inert" && p.kind !== "not_verified");
|
|
51926
|
+
if (!drafts.length) {
|
|
51927
|
+
const parts = [
|
|
51928
|
+
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.` : "",
|
|
51929
|
+
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.` : ""
|
|
51930
|
+
].filter(Boolean).join(" ");
|
|
51931
|
+
return `${lead}${parts}${r.published.length ? " Any gated workflow stays DRAFT until its handoff is met." : ""}`;
|
|
51932
|
+
}
|
|
51658
51933
|
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
51934
|
}
|
|
51660
51935
|
if (r.published.length) {
|
|
@@ -51668,6 +51943,8 @@ function publishNote(r) {
|
|
|
51668
51943
|
function publishSummaryClause(r) {
|
|
51669
51944
|
if (r.publishFailed.length) {
|
|
51670
51945
|
const also = r.published.length ? ` (${r.published.length} did publish)` : "";
|
|
51946
|
+
const drafts = r.publishFailed.filter((p) => p.kind !== "inert" && p.kind !== "not_verified").length;
|
|
51947
|
+
if (!drafts) return ` ${r.publishFailed.length} workflow(s) published but NOT confirmed running${also} (inactive trigger or unconfirmed publish), see publishNote.`;
|
|
51671
51948
|
return ` ${r.publishFailed.length} workflow(s) BUILT BUT NOT PUBLISHED${also}. They are DRAFT and not running, see publishNote.`;
|
|
51672
51949
|
}
|
|
51673
51950
|
if (r.published.length) return ` Published ${r.published.length} workflow(s) live.`;
|
|
@@ -52864,8 +53141,14 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
52864
53141
|
}
|
|
52865
53142
|
if (opts.publishWorkflows && !opts.gatedWorkflowRefs?.has(wf.ref)) {
|
|
52866
53143
|
try {
|
|
52867
|
-
await deps.publishWorkflow(wfId);
|
|
52868
|
-
|
|
53144
|
+
const verdict = publishVerdict(await deps.publishWorkflow(wfId));
|
|
53145
|
+
if (!verdict.live) {
|
|
53146
|
+
publishFailed.push({ ref: wf.ref, name: wf.name, error: verdict.reason, kind: verdict.kind });
|
|
53147
|
+
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`}.` });
|
|
53148
|
+
} else {
|
|
53149
|
+
published.push(wf.ref);
|
|
53150
|
+
if (verdict.note) manual.push({ ref: wf.ref, type: "workflow-trigger", name: wf.name, reason: `[${wf.name}] published, but ${verdict.note}` });
|
|
53151
|
+
}
|
|
52869
53152
|
} catch (e) {
|
|
52870
53153
|
publishFailed.push({ ref: wf.ref, name: wf.name, error: msg2(e) });
|
|
52871
53154
|
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 +53268,21 @@ async function createWithReadback(create, read2, name, beforeIds, poll, wait) {
|
|
|
52985
53268
|
function msg2(e) {
|
|
52986
53269
|
return accountErrorText(e);
|
|
52987
53270
|
}
|
|
53271
|
+
function publishVerdict(outcome) {
|
|
53272
|
+
if (typeof outcome !== "object" || outcome === null) return { live: true };
|
|
53273
|
+
const check = outcome.publish_check;
|
|
53274
|
+
if (typeof check !== "object" || check === null) return { live: true };
|
|
53275
|
+
const c = check;
|
|
53276
|
+
const warning = typeof c.warning === "string" && c.warning ? c.warning : "";
|
|
53277
|
+
if (warning.startsWith("NOT VERIFIED")) return { live: false, kind: "not_verified", reason: warning };
|
|
53278
|
+
if (warning.startsWith("TRIGGER NOT ACTIVE")) {
|
|
53279
|
+
const active = typeof c.triggers_active === "number" ? c.triggers_active : 0;
|
|
53280
|
+
return active > 0 ? { live: true, note: warning } : { live: false, kind: "inert", reason: warning };
|
|
53281
|
+
}
|
|
53282
|
+
if (c.published === true) return { live: true };
|
|
53283
|
+
const status = typeof c.status_returned === "string" ? c.status_returned : "(GHL returned no status)";
|
|
53284
|
+
return { live: false, kind: "not_live", reason: warning || `NOT LIVE. The workflow read back as ${status} rather than "published", so it is not running.` };
|
|
53285
|
+
}
|
|
52988
53286
|
var norm5, sleep5, PREVIEW_SNIPPET_LENGTH, JournalHookError;
|
|
52989
53287
|
var init_execute = __esm({
|
|
52990
53288
|
"src/intake-to-build/execute.ts"() {
|
|
@@ -53549,9 +53847,12 @@ function makeExecuteDeps(client, builderClient, locationId2, registry2) {
|
|
|
53549
53847
|
deleteWorkflow: async (workflowId) => {
|
|
53550
53848
|
await builderClient.deleteWorkflow(workflowId);
|
|
53551
53849
|
},
|
|
53552
|
-
|
|
53553
|
-
|
|
53554
|
-
|
|
53850
|
+
// The result is RETURNED, not awaited-and-dropped. It carries the client's
|
|
53851
|
+
// own `publish_check` — the status read back from the SAVED workflow after
|
|
53852
|
+
// the write, with a NOT LIVE / NOT VERIFIED sentence when the publish did
|
|
53853
|
+
// not land. Discarding it is what let a workflow left in Draft be counted
|
|
53854
|
+
// into "Published N workflow(s) live"; the executor now reads it.
|
|
53855
|
+
publishWorkflow: (workflowId) => builderClient.publishWorkflow(workflowId)
|
|
53555
53856
|
};
|
|
53556
53857
|
}
|
|
53557
53858
|
function makeRevertDeps(client, builderClient, locationId2) {
|
|
@@ -53759,7 +54060,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
53759
54060
|
if (saved2?.wholePreset) {
|
|
53760
54061
|
scopeSource = "cockpit";
|
|
53761
54062
|
} else if (!saved2 && Array.isArray(board.assessmentPurchases?.[activeLocation])) {
|
|
53762
|
-
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error:
|
|
54063
|
+
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error: notConfirmedAsk(activeLocation) });
|
|
53763
54064
|
} else if (saved2 && Array.isArray(saved2.features)) {
|
|
53764
54065
|
const features = saved2.features.filter(isFeatureId);
|
|
53765
54066
|
if (!features.length) {
|
|
@@ -53817,11 +54118,11 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
53817
54118
|
if (!gate2.ok) return jsonResponse({ ok: false, phase: "plan", error: gate2.error, savedPlan: saved ? { planId: saved.planId, savedAt: saved.savedAt } : void 0 });
|
|
53818
54119
|
const plan = gate2.plan;
|
|
53819
54120
|
const planSource = gate2.source;
|
|
53820
|
-
if (
|
|
54121
|
+
if (!fromPreset) {
|
|
53821
54122
|
const board = readBoardState();
|
|
53822
54123
|
const current = board.scope?.[activeLocation];
|
|
53823
54124
|
if (!current && Array.isArray(board.assessmentPurchases?.[activeLocation])) {
|
|
53824
|
-
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error:
|
|
54125
|
+
return jsonResponse({ ok: false, phase: "scope", scopeSource: "cockpit", error: notConfirmedAsk(activeLocation) });
|
|
53825
54126
|
}
|
|
53826
54127
|
if (current && !current.wholePreset && Array.isArray(current.features) && !current.features.some(isFeatureId)) {
|
|
53827
54128
|
return jsonResponse({
|
|
@@ -53831,7 +54132,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
53831
54132
|
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
54133
|
});
|
|
53833
54134
|
}
|
|
53834
|
-
if (!savedPlanMatchesScope(saved, current)) {
|
|
54135
|
+
if (saved && planSource !== "argument-replaces-saved" && !savedPlanMatchesScope(saved, current)) {
|
|
53835
54136
|
const planned = saved?.scope ? [...saved.scope.built, ...(saved.scope.omitted ?? []).map((f) => f.id)].join(", ") || "no features" : "everything in the preset";
|
|
53836
54137
|
const now = current?.wholePreset ? "everything in the preset (confirmed)" : current ? current.features.filter(isFeatureId).join(", ") : "everything in the preset (nothing is saved)";
|
|
53837
54138
|
return jsonResponse({
|
|
@@ -54523,7 +54824,7 @@ function intakeFormMatches(expected, actual) {
|
|
|
54523
54824
|
const keys = ["id", "tag", "type", "hiddenFieldQueryKey", "required", "label", "options", "picklistOptions"];
|
|
54524
54825
|
return expected.every((field, i) => keys.every((key) => field[key] === void 0 || JSON.stringify(field[key]) === JSON.stringify(actual[i]?.[key])));
|
|
54525
54826
|
}
|
|
54526
|
-
var import_zod81, customFieldItemSchema, sleep6, NOT_CONFIRMED_SCOPE;
|
|
54827
|
+
var import_zod81, customFieldItemSchema, sleep6, NOT_CONFIRMED_SCOPE, WHOLE_PRESET_NOT_KEPT, notConfirmedAsk;
|
|
54527
54828
|
var init_intake_to_build = __esm({
|
|
54528
54829
|
"src/tools/intake-to-build.ts"() {
|
|
54529
54830
|
"use strict";
|
|
@@ -54570,6 +54871,12 @@ var init_intake_to_build = __esm({
|
|
|
54570
54871
|
}).passthrough();
|
|
54571
54872
|
sleep6 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
54572
54873
|
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.";
|
|
54874
|
+
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.";
|
|
54875
|
+
notConfirmedAsk = (locationId2) => {
|
|
54876
|
+
const sp = readSavedPlan(locationId2);
|
|
54877
|
+
const fromPreset = typeof sp?.plan?.preset === "string";
|
|
54878
|
+
return sp && !sp.scope && fromPreset ? `${NOT_CONFIRMED_SCOPE} ${WHOLE_PRESET_NOT_KEPT}` : NOT_CONFIRMED_SCOPE;
|
|
54879
|
+
};
|
|
54573
54880
|
}
|
|
54574
54881
|
});
|
|
54575
54882
|
|