@elitedcs/ghl-mcp 3.84.2 → 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 +21 -0
- package/README.md +1 -1
- package/dist/index.js +546 -573
- 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
|
@@ -3077,7 +3077,7 @@ function commandOsUpgradeMessage(toolName) {
|
|
|
3077
3077
|
"Everything else works exactly as it does now: audits, builds, fixes, Blueprint and multi-location are all part of what you have."
|
|
3078
3078
|
].join("\n");
|
|
3079
3079
|
}
|
|
3080
|
-
var COMMAND_OS_ONLY_TOOLS, COMMAND_OS_ONLY_MODULES,
|
|
3080
|
+
var COMMAND_OS_ONLY_TOOLS, COMMAND_OS_ONLY_MODULES, COMMAND_OS_DESC_SUFFIX;
|
|
3081
3081
|
var init_product_manifest = __esm({
|
|
3082
3082
|
"src/product-manifest.ts"() {
|
|
3083
3083
|
"use strict";
|
|
@@ -3108,489 +3108,6 @@ var init_product_manifest = __esm({
|
|
|
3108
3108
|
// ruling, same lane; none of its 12 tools has ever shipped.
|
|
3109
3109
|
"voice-ai"
|
|
3110
3110
|
]);
|
|
3111
|
-
COMMAND_OS_CATALOGUE = [
|
|
3112
|
-
{
|
|
3113
|
-
name: "connect_google_calendar",
|
|
3114
|
-
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.",
|
|
3115
|
-
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.",
|
|
3116
|
-
layer: "Layer 2 \u2014 Data"
|
|
3117
|
-
},
|
|
3118
|
-
{
|
|
3119
|
-
name: "read_my_calendar",
|
|
3120
|
-
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.",
|
|
3121
|
-
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.",
|
|
3122
|
-
layer: "Layer 2 \u2014 Data"
|
|
3123
|
-
},
|
|
3124
|
-
{
|
|
3125
|
-
name: "extract_brand",
|
|
3126
|
-
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.",
|
|
3127
|
-
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.",
|
|
3128
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3129
|
-
},
|
|
3130
|
-
{
|
|
3131
|
-
name: "import_reference_look",
|
|
3132
|
-
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.",
|
|
3133
|
-
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.",
|
|
3134
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3135
|
-
},
|
|
3136
|
-
{
|
|
3137
|
-
name: "list_brands",
|
|
3138
|
-
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.",
|
|
3139
|
-
why: "The brand shelf is the agency's, across every client. Nothing in the $97 product has one.",
|
|
3140
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3141
|
-
},
|
|
3142
|
-
{
|
|
3143
|
-
name: "get_brand",
|
|
3144
|
-
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.",
|
|
3145
|
-
why: "It is the record every design job reads from; it lives with the agency, not in any client's GoHighLevel.",
|
|
3146
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3147
|
-
},
|
|
3148
|
-
{
|
|
3149
|
-
name: "set_brand",
|
|
3150
|
-
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.",
|
|
3151
|
-
why: "Editing the agency's own brand shelf is agency work; the $97 product keeps no brand to edit.",
|
|
3152
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3153
|
-
},
|
|
3154
|
-
{
|
|
3155
|
-
name: "compose_design_brief",
|
|
3156
|
-
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.",
|
|
3157
|
-
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.",
|
|
3158
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3159
|
-
},
|
|
3160
|
-
{
|
|
3161
|
-
name: "lint_design_contract",
|
|
3162
|
-
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.",
|
|
3163
|
-
why: "It is the gate between your Claude's draft and the verifier; it exists only because the verifier does.",
|
|
3164
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3165
|
-
},
|
|
3166
|
-
{
|
|
3167
|
-
name: "verify_design",
|
|
3168
|
-
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.",
|
|
3169
|
-
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.",
|
|
3170
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3171
|
-
},
|
|
3172
|
-
{
|
|
3173
|
-
name: "grade_copy",
|
|
3174
|
-
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.",
|
|
3175
|
-
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.",
|
|
3176
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3177
|
-
},
|
|
3178
|
-
{
|
|
3179
|
-
name: "check_page_theme",
|
|
3180
|
-
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.",
|
|
3181
|
-
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.",
|
|
3182
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3183
|
-
},
|
|
3184
|
-
{
|
|
3185
|
-
name: "brand_to_theme",
|
|
3186
|
-
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.",
|
|
3187
|
-
why: "It joins the agency's brand shelf to the page writer the $97 product already has; the shelf is the Command OS half.",
|
|
3188
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3189
|
-
},
|
|
3190
|
-
{
|
|
3191
|
-
name: "get_agency_profile",
|
|
3192
|
-
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.",
|
|
3193
|
-
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.",
|
|
3194
|
-
layer: "Layer 1 \u2014 Context"
|
|
3195
|
-
},
|
|
3196
|
-
{
|
|
3197
|
-
name: "set_agency_profile",
|
|
3198
|
-
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.",
|
|
3199
|
-
why: "Same store as the read. Writing the agency's own facts is not part of managing a client's GoHighLevel account.",
|
|
3200
|
-
layer: "Layer 1 \u2014 Context"
|
|
3201
|
-
},
|
|
3202
|
-
{
|
|
3203
|
-
name: "get_income_sources",
|
|
3204
|
-
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.",
|
|
3205
|
-
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.",
|
|
3206
|
-
layer: "Layer 1 \u2014 Context"
|
|
3207
|
-
},
|
|
3208
|
-
{
|
|
3209
|
-
name: "set_income_source",
|
|
3210
|
-
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.",
|
|
3211
|
-
why: "As above; it is the write half of the same agency-side record.",
|
|
3212
|
-
layer: "Layer 1 \u2014 Context"
|
|
3213
|
-
},
|
|
3214
|
-
{
|
|
3215
|
-
name: "client_this_week",
|
|
3216
|
-
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.",
|
|
3217
|
-
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.",
|
|
3218
|
-
layer: "Layer 1 \u2014 Context"
|
|
3219
|
-
},
|
|
3220
|
-
{
|
|
3221
|
-
name: "build_audit_report",
|
|
3222
|
-
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.",
|
|
3223
|
-
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.",
|
|
3224
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3225
|
-
},
|
|
3226
|
-
{
|
|
3227
|
-
name: "daily_brief",
|
|
3228
|
-
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.",
|
|
3229
|
-
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.",
|
|
3230
|
-
layer: "Layer 3 \u2014 Intelligence"
|
|
3231
|
-
},
|
|
3232
|
-
{
|
|
3233
|
-
name: "build_site_audit_report",
|
|
3234
|
-
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.",
|
|
3235
|
-
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.",
|
|
3236
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3237
|
-
},
|
|
3238
|
-
{
|
|
3239
|
-
name: "save_assessment",
|
|
3240
|
-
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.",
|
|
3241
|
-
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.",
|
|
3242
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3243
|
-
},
|
|
3244
|
-
{
|
|
3245
|
-
name: "load_assessment",
|
|
3246
|
-
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.",
|
|
3247
|
-
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.",
|
|
3248
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3249
|
-
},
|
|
3250
|
-
{
|
|
3251
|
-
name: "attach_assessment",
|
|
3252
|
-
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.",
|
|
3253
|
-
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.",
|
|
3254
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3255
|
-
},
|
|
3256
|
-
{
|
|
3257
|
-
name: "list_assessments",
|
|
3258
|
-
does: "Lists the assessments on your Assessments pipeline: who each is for, what stage it is at, and what the deal is worth.",
|
|
3259
|
-
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.",
|
|
3260
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3261
|
-
},
|
|
3262
|
-
{
|
|
3263
|
-
name: "read_assessment_transcript",
|
|
3264
|
-
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.",
|
|
3265
|
-
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.",
|
|
3266
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3267
|
-
},
|
|
3268
|
-
{
|
|
3269
|
-
name: "write_assessment_report",
|
|
3270
|
-
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.",
|
|
3271
|
-
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.",
|
|
3272
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3273
|
-
},
|
|
3274
|
-
{
|
|
3275
|
-
name: "audit_my_delivery",
|
|
3276
|
-
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.",
|
|
3277
|
-
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.",
|
|
3278
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3279
|
-
},
|
|
3280
|
-
{
|
|
3281
|
-
name: "plan_automation",
|
|
3282
|
-
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.",
|
|
3283
|
-
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.",
|
|
3284
|
-
layer: "Layer 5 \u2014 Build"
|
|
3285
|
-
},
|
|
3286
|
-
{
|
|
3287
|
-
name: "approve_automation",
|
|
3288
|
-
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.",
|
|
3289
|
-
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.",
|
|
3290
|
-
layer: "Layer 5 \u2014 Build"
|
|
3291
|
-
},
|
|
3292
|
-
{
|
|
3293
|
-
name: "record_automation_step",
|
|
3294
|
-
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.",
|
|
3295
|
-
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.",
|
|
3296
|
-
layer: "Layer 5 \u2014 Build"
|
|
3297
|
-
},
|
|
3298
|
-
{
|
|
3299
|
-
name: "abandon_automation",
|
|
3300
|
-
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.",
|
|
3301
|
-
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.",
|
|
3302
|
-
layer: "Layer 5 \u2014 Build"
|
|
3303
|
-
},
|
|
3304
|
-
{
|
|
3305
|
-
name: "automation_status",
|
|
3306
|
-
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.",
|
|
3307
|
-
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.",
|
|
3308
|
-
layer: "Layer 5 \u2014 Build"
|
|
3309
|
-
},
|
|
3310
|
-
{
|
|
3311
|
-
name: "connect_system",
|
|
3312
|
-
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.",
|
|
3313
|
-
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.",
|
|
3314
|
-
layer: "Layer 2 \u2014 Data"
|
|
3315
|
-
},
|
|
3316
|
-
{
|
|
3317
|
-
name: "post_to_slack",
|
|
3318
|
-
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.",
|
|
3319
|
-
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.",
|
|
3320
|
-
layer: "Layer 3 \u2014 Intelligence"
|
|
3321
|
-
},
|
|
3322
|
-
{
|
|
3323
|
-
name: "read_slack",
|
|
3324
|
-
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.",
|
|
3325
|
-
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.",
|
|
3326
|
-
layer: "Layer 2 \u2014 Data"
|
|
3327
|
-
},
|
|
3328
|
-
{
|
|
3329
|
-
name: "list_connected_systems",
|
|
3330
|
-
does: "Which systems Command OS can read on this machine, and when each was connected. Never shows a key.",
|
|
3331
|
-
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.",
|
|
3332
|
-
layer: "Layer 2 \u2014 Data"
|
|
3333
|
-
},
|
|
3334
|
-
{
|
|
3335
|
-
name: "disconnect_system",
|
|
3336
|
-
does: "Removes a connector's credentials from this machine. Anything already written into your income records stays.",
|
|
3337
|
-
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.",
|
|
3338
|
-
layer: "Layer 2 \u2014 Data"
|
|
3339
|
-
},
|
|
3340
|
-
{
|
|
3341
|
-
name: "sync_stripe",
|
|
3342
|
-
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.",
|
|
3343
|
-
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.",
|
|
3344
|
-
layer: "Layer 2 \u2014 Data"
|
|
3345
|
-
},
|
|
3346
|
-
{
|
|
3347
|
-
name: "list_knowledge_bases",
|
|
3348
|
-
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.",
|
|
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: "get_knowledge_base",
|
|
3354
|
-
does: "Reads one knowledge base by id: its name, description and dates.",
|
|
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: "create_knowledge_base",
|
|
3360
|
-
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.",
|
|
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: "update_knowledge_base",
|
|
3366
|
-
does: "Renames or re-describes an existing knowledge base. What it has learned is untouched.",
|
|
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: "delete_knowledge_base",
|
|
3372
|
-
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.",
|
|
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: "crawl_kb_website",
|
|
3378
|
-
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.",
|
|
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: "train_kb_urls",
|
|
3384
|
-
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.",
|
|
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: "get_kb_crawl_status",
|
|
3390
|
-
does: "Reports where a crawl stands: pages discovered, trained and pending, and whether it has settled or is still working.",
|
|
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: "list_kb_urls",
|
|
3396
|
-
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.",
|
|
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_url",
|
|
3402
|
-
does: "Removes trained pages from a knowledge base so the bot stops answering from them. 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_kb_faqs",
|
|
3408
|
-
does: "Lists the written question-and-answer pairs in a knowledge base.",
|
|
3409
|
-
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.",
|
|
3410
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3411
|
-
},
|
|
3412
|
-
{
|
|
3413
|
-
name: "create_kb_faq",
|
|
3414
|
-
does: "Adds one question and its answer to a knowledge base, word for word as the bot should give it.",
|
|
3415
|
-
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.",
|
|
3416
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3417
|
-
},
|
|
3418
|
-
{
|
|
3419
|
-
name: "update_kb_faq",
|
|
3420
|
-
does: "Replaces one written answer in a knowledge base with new wording; every other answer and every trained page stays as it was.",
|
|
3421
|
-
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.",
|
|
3422
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3423
|
-
},
|
|
3424
|
-
{
|
|
3425
|
-
name: "delete_kb_faq",
|
|
3426
|
-
does: "Removes one written answer from a knowledge base. The literal word DELETE is required.",
|
|
3427
|
-
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.",
|
|
3428
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3429
|
-
},
|
|
3430
|
-
{
|
|
3431
|
-
name: "list_conversation_agents",
|
|
3432
|
-
does: "Lists the Conversation AI agents on the sub-account: name, mode, channels and which bot is primary on each channel.",
|
|
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: "get_conversation_agent",
|
|
3438
|
-
does: "Reads one agent in full: personality, goal, instructions, knowledge bases, channels, mode, wait and sleep settings.",
|
|
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_agent",
|
|
3444
|
-
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.",
|
|
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_agent",
|
|
3450
|
-
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.",
|
|
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_agent",
|
|
3456
|
-
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.",
|
|
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: "list_conversation_actions",
|
|
3462
|
-
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.",
|
|
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: "create_conversation_action",
|
|
3468
|
-
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.",
|
|
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: "update_conversation_action",
|
|
3474
|
-
does: "Changes one of an agent's actions in place, with the same refusals as creating one.",
|
|
3475
|
-
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.",
|
|
3476
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3477
|
-
},
|
|
3478
|
-
{
|
|
3479
|
-
name: "delete_conversation_action",
|
|
3480
|
-
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.",
|
|
3481
|
-
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.",
|
|
3482
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3483
|
-
},
|
|
3484
|
-
{
|
|
3485
|
-
name: "update_conversation_followup",
|
|
3486
|
-
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.",
|
|
3487
|
-
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.",
|
|
3488
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3489
|
-
},
|
|
3490
|
-
{
|
|
3491
|
-
name: "get_agent_generations",
|
|
3492
|
-
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.",
|
|
3493
|
-
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.",
|
|
3494
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3495
|
-
},
|
|
3496
|
-
{
|
|
3497
|
-
name: "install_ai_employee_demo",
|
|
3498
|
-
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.",
|
|
3499
|
-
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.",
|
|
3500
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3501
|
-
},
|
|
3502
|
-
{
|
|
3503
|
-
name: "prepare_ai_employee_demo",
|
|
3504
|
-
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.",
|
|
3505
|
-
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.",
|
|
3506
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3507
|
-
},
|
|
3508
|
-
{
|
|
3509
|
-
name: "show_ai_employee_demo",
|
|
3510
|
-
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.",
|
|
3511
|
-
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.",
|
|
3512
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3513
|
-
},
|
|
3514
|
-
{
|
|
3515
|
-
name: "end_ai_employee_demo",
|
|
3516
|
-
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.",
|
|
3517
|
-
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.",
|
|
3518
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3519
|
-
},
|
|
3520
|
-
{
|
|
3521
|
-
name: "check_voice_ai_readiness",
|
|
3522
|
-
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.",
|
|
3523
|
-
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.",
|
|
3524
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3525
|
-
},
|
|
3526
|
-
{
|
|
3527
|
-
name: "list_voice_agents",
|
|
3528
|
-
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.",
|
|
3529
|
-
why: "The survey before a build: which agent already answers which number. The $97 product has no view of Voice AI at all.",
|
|
3530
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3531
|
-
},
|
|
3532
|
-
{
|
|
3533
|
-
name: "get_voice_agent",
|
|
3534
|
-
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.",
|
|
3535
|
-
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.",
|
|
3536
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3537
|
-
},
|
|
3538
|
-
{
|
|
3539
|
-
name: "create_voice_agent",
|
|
3540
|
-
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.",
|
|
3541
|
-
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.",
|
|
3542
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3543
|
-
},
|
|
3544
|
-
{
|
|
3545
|
-
name: "update_voice_agent",
|
|
3546
|
-
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.",
|
|
3547
|
-
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.",
|
|
3548
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3549
|
-
},
|
|
3550
|
-
{
|
|
3551
|
-
name: "delete_voice_agent",
|
|
3552
|
-
does: "Removes a phone agent and everything it could do, after saying which number it was answering. Irreversible; the literal DELETE is required.",
|
|
3553
|
-
why: "Disposal of a Command OS asset; the $97 product never created one.",
|
|
3554
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3555
|
-
},
|
|
3556
|
-
{
|
|
3557
|
-
name: "get_voice_action",
|
|
3558
|
-
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.",
|
|
3559
|
-
why: "Part of the phone agent's build surface, which exists only in Command OS.",
|
|
3560
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3561
|
-
},
|
|
3562
|
-
{
|
|
3563
|
-
name: "create_voice_action",
|
|
3564
|
-
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.",
|
|
3565
|
-
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.",
|
|
3566
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3567
|
-
},
|
|
3568
|
-
{
|
|
3569
|
-
name: "update_voice_action",
|
|
3570
|
-
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.",
|
|
3571
|
-
why: "Re-pointing what an agent does on a live line; Command OS only.",
|
|
3572
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3573
|
-
},
|
|
3574
|
-
{
|
|
3575
|
-
name: "delete_voice_action",
|
|
3576
|
-
does: "Removes one thing a phone agent may do. The agent keeps answering without it. The literal DELETE is required.",
|
|
3577
|
-
why: "Part of the phone agent's build surface; Command OS only.",
|
|
3578
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3579
|
-
},
|
|
3580
|
-
{
|
|
3581
|
-
name: "get_voice_call_logs",
|
|
3582
|
-
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.",
|
|
3583
|
-
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.",
|
|
3584
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3585
|
-
},
|
|
3586
|
-
{
|
|
3587
|
-
name: "get_voice_call_log",
|
|
3588
|
-
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.",
|
|
3589
|
-
why: "What you show a client instead of saying it works. Command OS only.",
|
|
3590
|
-
layer: "Layer 4 \u2014 Delivery"
|
|
3591
|
-
}
|
|
3592
|
-
];
|
|
3593
|
-
CATALOGUED_TOOLS = new Set(COMMAND_OS_CATALOGUE.map((t) => t.name));
|
|
3594
3111
|
COMMAND_OS_DESC_SUFFIX = " [NOT INCLUDED IN THIS LICENCE \u2014 calling this returns a notice, it will NOT perform the action.]";
|
|
3595
3112
|
}
|
|
3596
3113
|
});
|
|
@@ -5516,11 +5033,11 @@ function registerLocationTools(server2, client) {
|
|
|
5516
5033
|
if (email !== void 0) body.email = email;
|
|
5517
5034
|
if (settings !== void 0) body.settings = settings;
|
|
5518
5035
|
const BRAND_FIELDS = ["name", "address", "city", "state", "postalCode", "country", "phone", "website", "email"];
|
|
5519
|
-
const
|
|
5036
|
+
const isRec2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5520
5037
|
let current = null;
|
|
5521
5038
|
try {
|
|
5522
5039
|
const raw = await client.get(`/locations/${locationId2}`);
|
|
5523
|
-
current =
|
|
5040
|
+
current = isRec2(raw) && isRec2(raw.location) ? raw.location : isRec2(raw) ? raw : null;
|
|
5524
5041
|
} catch {
|
|
5525
5042
|
current = null;
|
|
5526
5043
|
}
|
|
@@ -5546,13 +5063,13 @@ Nothing was changed. Pass overwriteBusinessInfo: true if you intend to change th
|
|
|
5546
5063
|
const result = await client.put(`/locations/${locationId2}`, { body });
|
|
5547
5064
|
if (!current) {
|
|
5548
5065
|
return {
|
|
5549
|
-
...
|
|
5066
|
+
...isRec2(result) ? result : { result },
|
|
5550
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.`
|
|
5551
5068
|
};
|
|
5552
5069
|
}
|
|
5553
5070
|
if (changes.length > 0) {
|
|
5554
5071
|
return {
|
|
5555
|
-
...
|
|
5072
|
+
...isRec2(result) ? result : { result },
|
|
5556
5073
|
business_record_change: {
|
|
5557
5074
|
changed: changes,
|
|
5558
5075
|
brandId: brandId || "(no registered A2P brand on this sub-account)",
|
|
@@ -12755,6 +12272,134 @@ var init_retheme = __esm({
|
|
|
12755
12272
|
}
|
|
12756
12273
|
});
|
|
12757
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;
|
|
12400
|
+
}
|
|
12401
|
+
});
|
|
12402
|
+
|
|
12758
12403
|
// src/command-os/design/reference-looks.json
|
|
12759
12404
|
var reference_looks_default;
|
|
12760
12405
|
var init_reference_looks = __esm({
|
|
@@ -16963,6 +16608,94 @@ function removalHoldBack(opts) {
|
|
|
16963
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`;
|
|
16964
16609
|
return null;
|
|
16965
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
|
+
}
|
|
16966
16699
|
function registerPageStudioTools(server2, builderClient) {
|
|
16967
16700
|
if (!builderClient) return;
|
|
16968
16701
|
const client = builderClient;
|
|
@@ -17025,6 +16758,21 @@ ${await response.text()}`);
|
|
|
17025
16758
|
}
|
|
17026
16759
|
});
|
|
17027
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}
|
|
17028
16776
|
${await response.text()}`);
|
|
17029
16777
|
const text2 = await response.text();
|
|
17030
16778
|
return text2 ? JSON.parse(text2) : {};
|
|
@@ -17147,11 +16895,15 @@ ${await response.text()}`);
|
|
|
17147
16895
|
{
|
|
17148
16896
|
templateId: import_zod43.z.string().describe("From find_ghl_template."),
|
|
17149
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."),
|
|
17150
16899
|
confirm: import_zod43.z.literal("INSTALL").describe("Type INSTALL. This writes into the live sub-account you are switched to."),
|
|
17151
|
-
|
|
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.")
|
|
17152
16902
|
},
|
|
17153
|
-
async ({ templateId, brandSlug,
|
|
16903
|
+
async ({ templateId, brandSlug, formId, removeOriginals }) => {
|
|
17154
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." };
|
|
17155
16907
|
const inventory = await funnelSummary();
|
|
17156
16908
|
const before = inventory.map;
|
|
17157
16909
|
const named = installedFunnelId(await installTemplate(templateApi(), templateId));
|
|
@@ -17194,138 +16946,354 @@ ${await response.text()}`);
|
|
|
17194
16946
|
return {
|
|
17195
16947
|
ok: true,
|
|
17196
16948
|
installed: true,
|
|
16949
|
+
originalsRemoved: 0,
|
|
16950
|
+
contrast: [],
|
|
16951
|
+
kept: "Nothing was deleted. No replacement pages were requested.",
|
|
17197
16952
|
funnel: { id: funnel._id, name: funnel.name, pages: (funnel.steps ?? []).length },
|
|
17198
16953
|
note: "Installed in the designer's own colours. Pass brandSlug to have it repainted in a client's brand on the way in.",
|
|
17199
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".`,
|
|
17200
16955
|
undo: `delete_funnel with funnelId "${funnel._id}" and confirm "DELETE" removes it and its pages, and nothing else.`
|
|
17201
16956
|
};
|
|
17202
16957
|
}
|
|
17203
|
-
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
|
+
}));
|
|
17204
16965
|
const multiPage = originals.filter((x) => x.pageCount > 1);
|
|
17205
|
-
const done = [];
|
|
17206
16966
|
const problems = [];
|
|
16967
|
+
const warnings = [];
|
|
17207
16968
|
const loaded = [];
|
|
17208
16969
|
for (const step of originals) {
|
|
17209
|
-
|
|
17210
|
-
|
|
17211
|
-
|
|
17212
|
-
|
|
17213
|
-
|
|
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)}`);
|
|
16979
|
+
}
|
|
16980
|
+
}
|
|
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);
|
|
17214
17001
|
}
|
|
17215
|
-
loaded.push({ stepId: step.stepId, name: step.name, envelope: await (await fetch(dataUrl)).json() });
|
|
17216
17002
|
}
|
|
17217
|
-
const
|
|
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 = [];
|
|
17218
17014
|
for (const step of loaded) {
|
|
17219
17015
|
try {
|
|
17220
|
-
const
|
|
17221
|
-
const
|
|
17222
|
-
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", {
|
|
17223
17019
|
funnelId: funnel._id,
|
|
17224
|
-
step: { id:
|
|
17020
|
+
step: { id: newStepId, name: step.name, url: newPath, pages: [], control_traffic: 100, split: false, type: "optin_funnel_page" }
|
|
17225
17021
|
});
|
|
17226
|
-
|
|
17227
|
-
|
|
17228
|
-
|
|
17229
|
-
|
|
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
|
+
}
|
|
17063
|
+
}
|
|
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;
|
|
17230
17069
|
}
|
|
17231
|
-
await autosaveRetrying(newPageId, {
|
|
17070
|
+
await autosaveRetrying(step.newPageId, {
|
|
17232
17071
|
funnelId: funnel._id,
|
|
17233
17072
|
pageVersion: 1,
|
|
17234
|
-
pageData: {
|
|
17235
|
-
...painted.envelope,
|
|
17236
|
-
pageVersion: 1,
|
|
17237
|
-
pageType: "draft",
|
|
17238
|
-
manualSave: true,
|
|
17239
|
-
integrations: computeIntegrations(painted.envelope)
|
|
17240
|
-
}
|
|
17073
|
+
pageData: { ...envelope, pageVersion: 1, pageType: "draft", manualSave: true, integrations: computeIntegrations(envelope) }
|
|
17241
17074
|
});
|
|
17242
17075
|
let after = {};
|
|
17243
17076
|
for (let i = 0; i < 6; i++) {
|
|
17244
|
-
after = await funnelRequest("GET", `/page/${newPageId}?locationId=${client.locationId}`);
|
|
17077
|
+
after = await funnelRequest("GET", `/page/${step.newPageId}?locationId=${client.locationId}`);
|
|
17245
17078
|
if (typeof after.pageDataDownloadUrl === "string" && after.pageDataDownloadUrl) break;
|
|
17246
17079
|
await new Promise((r) => setTimeout(r, 1200));
|
|
17247
17080
|
}
|
|
17248
|
-
|
|
17249
|
-
|
|
17250
|
-
|
|
17251
|
-
|
|
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");
|
|
17252
17091
|
}
|
|
17253
|
-
|
|
17254
|
-
|
|
17255
|
-
|
|
17256
|
-
if (stillThere) {
|
|
17257
|
-
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`);
|
|
17258
|
-
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`);
|
|
17259
17095
|
}
|
|
17260
|
-
const
|
|
17261
|
-
|
|
17262
|
-
|
|
17263
|
-
|
|
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;
|
|
17264
17124
|
}
|
|
17265
|
-
|
|
17266
|
-
|
|
17267
|
-
|
|
17268
|
-
|
|
17269
|
-
|
|
17270
|
-
|
|
17271
|
-
|
|
17272
|
-
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
|
|
17277
|
-
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
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.`
|
|
17284
17178
|
});
|
|
17285
17179
|
} catch (e) {
|
|
17286
|
-
problems.push(
|
|
17180
|
+
problems.push(`shared sections: ${e instanceof Error ? e.message : String(e)}`);
|
|
17287
17181
|
}
|
|
17288
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)}`);
|
|
17223
|
+
}
|
|
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);
|
|
17289
17228
|
let removed = 0;
|
|
17290
|
-
const
|
|
17291
|
-
const
|
|
17292
|
-
|
|
17293
|
-
|
|
17294
|
-
if (complete && removeOriginals && holdBack === null) {
|
|
17295
|
-
for (const d of done) {
|
|
17229
|
+
const accepted = [];
|
|
17230
|
+
const removalFailures = [];
|
|
17231
|
+
if (removeOriginals === true && rebuilt && holdBack === null) {
|
|
17232
|
+
for (const step of originals) {
|
|
17296
17233
|
try {
|
|
17297
|
-
await funnelRequest("DELETE", `/funnel/step/${funnel._id}/${
|
|
17298
|
-
|
|
17234
|
+
await funnelRequest("DELETE", `/funnel/step/${funnel._id}/${step.stepId}`);
|
|
17235
|
+
accepted.push(step);
|
|
17299
17236
|
} catch (e) {
|
|
17300
|
-
|
|
17237
|
+
removalFailures.push(`${step.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
17238
|
+
}
|
|
17239
|
+
}
|
|
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);
|
|
17301
17249
|
}
|
|
17250
|
+
if (confirmed.size === accepted.length) break;
|
|
17251
|
+
} catch {
|
|
17302
17252
|
}
|
|
17303
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;
|
|
17304
17259
|
return {
|
|
17305
17260
|
ok: complete,
|
|
17306
17261
|
funnel: { id: funnel._id, name: funnel.name },
|
|
17307
|
-
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 } },
|
|
17308
17263
|
templatePalette: { accent: palette.accent, surface: palette.surface, text: palette.text, secondary: palette.secondary, faces: palette.fonts, undecided: palette.undecided },
|
|
17309
|
-
pages
|
|
17264
|
+
pages,
|
|
17265
|
+
sharedSections,
|
|
17310
17266
|
...(() => {
|
|
17311
|
-
const
|
|
17312
|
-
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);
|
|
17313
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.`] : [],
|
|
17314
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.`] : [],
|
|
17315
|
-
...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.`] : []
|
|
17316
17282
|
];
|
|
17317
|
-
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).`));
|
|
17318
17284
|
return {
|
|
17319
17285
|
...lines.length ? { repaintNote: `${lines.join(" ")} Look at the preview before calling it branded.` } : {},
|
|
17320
17286
|
...contrast.length ? { contrast } : {}
|
|
17321
17287
|
};
|
|
17322
17288
|
})(),
|
|
17323
|
-
originalsRemoved:
|
|
17324
|
-
...
|
|
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 },
|
|
17325
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." },
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
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.",
|
|
17329
17297
|
undo: `delete_funnel with funnelId "${funnel._id}" and confirm "DELETE" removes the whole thing.`
|
|
17330
17298
|
};
|
|
17331
17299
|
}
|
|
@@ -17539,6 +17507,8 @@ var init_page_studio = __esm({
|
|
|
17539
17507
|
init_workflow_builder_client();
|
|
17540
17508
|
init_ghl_templates();
|
|
17541
17509
|
init_retheme();
|
|
17510
|
+
init_served_palette();
|
|
17511
|
+
init_global_sections();
|
|
17542
17512
|
init_ghl_page();
|
|
17543
17513
|
init_looks();
|
|
17544
17514
|
init_store();
|
|
@@ -31755,7 +31725,7 @@ var require_package = __commonJS({
|
|
|
31755
31725
|
"package.json"(exports2, module2) {
|
|
31756
31726
|
module2.exports = {
|
|
31757
31727
|
name: "@elitedcs/ghl-mcp",
|
|
31758
|
-
version: "3.84.
|
|
31728
|
+
version: "3.84.3",
|
|
31759
31729
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
31760
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.",
|
|
31761
31731
|
main: "dist/index.js",
|
|
@@ -42788,11 +42758,11 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
42788
42758
|
if (template.location) {
|
|
42789
42759
|
const locationData = resolveObj(template.location);
|
|
42790
42760
|
const BRAND_FIELDS = /* @__PURE__ */ new Set(["name", "address", "city", "state", "postalCode", "country", "phone", "website", "email"]);
|
|
42791
|
-
const
|
|
42761
|
+
const isRec2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
42792
42762
|
let currentLoc = null;
|
|
42793
42763
|
try {
|
|
42794
42764
|
const raw = await client.get(`/locations/${locId}`);
|
|
42795
|
-
currentLoc =
|
|
42765
|
+
currentLoc = isRec2(raw) && isRec2(raw.location) ? raw.location : isRec2(raw) ? raw : null;
|
|
42796
42766
|
} catch {
|
|
42797
42767
|
currentLoc = null;
|
|
42798
42768
|
}
|
|
@@ -43614,6 +43584,9 @@ function registerCheckupTools(server2, installedVersion, client, builderClient,
|
|
|
43614
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.")
|
|
43615
43585
|
},
|
|
43616
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
|
+
}
|
|
43617
43590
|
let loc;
|
|
43618
43591
|
try {
|
|
43619
43592
|
loc = client.resolveLocationId(locationId2);
|