@elitedcs/ghl-mcp 3.56.0 → 3.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,103 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.57.0 — Clone Site: copy a live page for a client, with the guardrails built in
4
+
5
+ A second bundled skill. Point Claude at any live URL and get a working,
6
+ rebranded copy for a client — same layout, images, video and CSS, with the
7
+ new business's details in place of the original's. It installs itself like
8
+ Blueprint; there is nothing to download and no new tools to learn.
9
+
10
+ Cloning normally fails because the model *regenerates* the page instead of
11
+ copying it, and you get a worse version of what you pointed at. This skill
12
+ runs real scripts: they download the bytes, swap the strings, and verify the
13
+ result. Nothing is retyped from memory.
14
+
15
+ - **The rights question is step 0 and cannot be skipped.** Before anything is
16
+ copied, the skill asks who owns the page — you, your client (authorized),
17
+ written permission, or none — and records the answer in the run report and
18
+ in the final report header. It does not verify ownership; the declaration is
19
+ yours to make. The mirror script refuses to run without it.
20
+ - **"None" gets a real lane, not a refusal.** It extracts the page's actual
21
+ design system — palette, CSS variables, font stack and type scale, spacing
22
+ rhythm, radii, shadows, breakpoints, and the section skeleton as shape — so
23
+ the rebuild starts from the real design instead of a from-memory redraw. No
24
+ bytes, images, video or copy are copied in that lane.
25
+ - **Substitution that doesn't leak the old brand.** Derives the bare brand word,
26
+ every phone format, the address as a unit (not just the city), and domain and
27
+ email variants; runs across JS bundles as well as HTML and CSS, because
28
+ compiled sites keep contact details only in the bundle. Asset filenames are
29
+ protected from rewriting. Dry-run counts first, a zero-check after, and a
30
+ NEAR MISS warning when one of your facts is *almost* right.
31
+ - **Catches the leak a string search cannot see.** A brand mark is usually
32
+ two-tone — `GHL <span style="color:#D4AF37;">Command</span>` — which reads as
33
+ the old brand on screen while that string appears nowhere in the file. Every
34
+ literal check calls it clean. Clone Site renders the page to text while
35
+ keeping a map back to source offsets, so it reports exactly which occurrences
36
+ are split by markup, shows the markup, and fails the zero-check until they are
37
+ hand-edited. Found on a real page, in the header and the footer, after a
38
+ file-level rebrand had already "passed."
39
+ - **Asset capture from all four places** — HTML, CSS `url()`, CDN URLs in JS
40
+ bundles, and root-relative paths inside JS bundles (the one that silently
41
+ breaks images). Oversized files are reported for object storage, never
42
+ hot-linked back to the original owner's CDN.
43
+ - **A mandatory pre-launch REVIEW REQUIRED report** on every run: form endpoints,
44
+ webhooks, payment links and publishable keys, booking embeds, analytics and ad
45
+ pixels (MUST REPOINT); names, addresses, testimonials and likenesses; and
46
+ inherited claims with regulated health/efficacy language flagged separately.
47
+ Nothing is auto-deleted — findings go to a human.
48
+ - **Verification by content-type, not status code**, because static hosts answer
49
+ 200 with the page shell for assets that don't exist. It retries once before
50
+ calling anything broken (a CDN rollout can answer a single request with HTML),
51
+ and separates references the *source* page was already serving badly from
52
+ breakage the clone caused.
53
+ - Reports are written outside the deployable folder, so a rights declaration and
54
+ a liability list can never be published with the site.
55
+
56
+ Everything below was found by running the tool against real production sites
57
+ rather than test fixtures, and every one of them shipped a fix:
58
+
59
+ - **Whole-site cloning.** A one-page mirror inherits the original's entire
60
+ navigation, so every menu item 404s the moment a client clicks it.
61
+ `--crawl --depth N --max-pages N` follows the site's own navigation, writes
62
+ each page at its own path, and rewrites links between your pages as
63
+ root-relative so the nav works locally and after deploy — while canonical and
64
+ og:url keep their host so the rebrand can point them at the client's domain.
65
+ The skill asks how deep to go rather than assuming. Real numbers: a 36-page
66
+ contractor site came to 2,104 files and 935 MB at depth 2.
67
+ - **Assets that only exist at runtime.** Page-builders publish an asset base
68
+ path and concatenate chunk filenames onto it in JavaScript, so those files
69
+ appear in no attribute, stylesheet or string literal. Six missing Elementor
70
+ chunks meant no section background was painted at all: the hero rendered blank
71
+ and its white headline was invisible on white, while every file-level check
72
+ reported success. New `repair.mjs` plus a mandatory render-and-repair loop
73
+ fetches whatever the running page asks for and cannot find.
74
+ - **Media hidden in escaped JSON.** Background slideshows and galleries store
75
+ image URLs inside entity-encoded, backslash-escaped JSON attributes. Those are
76
+ now extracted and localised, escaping preserved.
77
+ - **Brand marks split across tags.** `GHL <span style="color:#D4AF37">Command</span>`
78
+ reads as the old brand on screen while that string exists nowhere in the file.
79
+ Detected exactly via a rendered-text-to-source offset map; the zero-check now
80
+ fails until they are hand-edited instead of reporting a false clean.
81
+ - **HTML-entity brand forms.** A brand containing `&` is stored as `&amp;`. On
82
+ one page the plain token matched twice and the encoded form 56 times.
83
+ - **Brand baked into image pixels.** Logos and graphics carrying the old phone
84
+ number or web address are flagged — substitution cannot edit artwork, and the
85
+ logo is the most visible way a clone gives itself away.
86
+ - **New audit rules from real pages:** contractor and professional licence
87
+ numbers (the highest-severity item on any trades or medical clone), consumer
88
+ financing links, review widgets that load the original's reviews at runtime,
89
+ and internal links with no page in the clone.
90
+ - **Bare-word derivation is now cautioned**, because a brand beginning with a
91
+ place name ("Arizona AC & Heating") would otherwise rewrite ordinary sentences.
92
+ - **Style lane returns the brand's palette, not the framework's.** Plugin CSS
93
+ repeats neutral greys hundreds of times while the real brand colours appear
94
+ twice; vendor stylesheets are now separated, accents are attributed to their
95
+ source file, declared colour tokens are surfaced first, and CSS variables are
96
+ resolved so the type scale comes back as real pixel values.
97
+ - **The style lane's deliverable is a page, not a document.** Its rebuild is now
98
+ five explicit steps, including a media step and a leak check against the
99
+ original page.
100
+
3
101
  ## 3.56.0 — Security off-switches: any tool can be disabled; sub-account create/delete is opt-in
4
102
 
5
103
  Shaped directly by subscriber feedback: some teams keep account-level
package/README.md CHANGED
@@ -12,7 +12,7 @@ Works with **both the Claude Desktop App and Claude Code terminal** — your cho
12
12
 
13
13
  **How this differs from HighLevel's official MCP.** HighLevel ships an official Anthropic MCP that reads and writes your CRM (contacts, conversations, calendars, opportunities, payments, social) over the public API — it's free and it's good; use it for that. What it *cannot* do, because the public API doesn't expose it: **build or edit workflows, build funnels/landing pages, or build forms.** That's exactly what GHL Command does — the programmatic Workflow Builder, funnel/page builder, form builder, deep workflow cloning, the silent-failure `audit_workflows`, and Blueprint (build a whole client account from one intake — ships as a guided skill that **installs itself automatically** with this package; nothing to download), all on GHL's internal APIs. **Official MCP = read/write your CRM. GHL Command = build and fix it.** They stack fine together.
14
14
 
15
- **Bundled skills (install automatically).** The package ships guided skills that teach Claude complete multi-step workflows on top of the raw tools. On every start the server installs/updates them into your `~/.claude/skills/` (never overwriting files you've edited — your version wins and is reported). Current skills: **Blueprint** — turn one client intake into a complete reviewed GHL account build. Verify or repair any time by asking Claude to `run install_skills`, then fully restart Claude so new skills load.
15
+ **Bundled skills (install automatically).** The package ships guided skills that teach Claude complete multi-step workflows on top of the raw tools. On every start the server installs/updates them into your `~/.claude/skills/` (never overwriting files you've edited — your version wins and is reported). Current skills: **Blueprint** — turn one client intake into a complete reviewed GHL account build; **Clone Site** — point at a live URL and get a rebranded, deploy-ready copy for a client, with a recorded rights declaration, a style-only lane when you have no rights, and a mandatory pre-launch report of every lead/booking/payment destination, borrowed testimonial and inherited claim still belonging to the original owner. Verify or repair any time by asking Claude to `run install_skills`, then fully restart Claude so new skills load.
16
16
 
17
17
  Built by [Elite DCs, LLC](https://elitedcs.com).
18
18
 
package/dist/index.js CHANGED
@@ -2102,7 +2102,7 @@ var require_package = __commonJS({
2102
2102
  "package.json"(exports2, module2) {
2103
2103
  module2.exports = {
2104
2104
  name: "@elitedcs/ghl-mcp",
2105
- version: "3.56.0",
2105
+ version: "3.57.0",
2106
2106
  mcpName: "io.github.drjerryrelth/ghl-command",
2107
2107
  description: "GoHighLevel MCP Server for Claude. 233 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.",
2108
2108
  main: "dist/index.js",
@@ -17956,7 +17956,7 @@ function summarizeInstall(r) {
17956
17956
  function registerSkillsTool(server2, packageVersion, baseDir) {
17957
17957
  server2.tool(
17958
17958
  "install_skills",
17959
- "Install (or repair) the guided skills bundled with GHL Command \u2014 currently the Blueprint skill (build a whole client account from one intake) \u2014 into your ~/.claude/skills/ so Claude can use them. Runs automatically on startup; call this to verify what is installed, or to re-install after deleting a skill. NEVER overwrites files you have edited (your version is kept and reported). Restart Claude after install for new skills to load.",
17959
+ "Install (or repair) the guided skills bundled with GHL Command \u2014 Blueprint (build a whole client account from one intake) and Clone Site (clone and rebrand a live web page for a client, with a rights declaration and a pre-launch liability report) \u2014 into your ~/.claude/skills/ so Claude can use them. Runs automatically on startup; call this to verify what is installed, or to re-install after deleting a skill. NEVER overwrites files you have edited (your version is kept and reported). Restart Claude after install for new skills to load.",
17960
17960
  {
17961
17961
  targetDir: import_zod57.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
17962
17962
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.56.0",
3
+ "version": "3.57.0",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
5
  "description": "GoHighLevel MCP Server for Claude. 233 tools — full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,42 @@
1
+ # Clone Site
2
+
3
+ Point Claude at a live web page and get a working copy, rebranded for a new client — same layout, same images and video, same CSS, with your client's name, phone, address, domain and colors in place of the original's.
4
+
5
+ The reason this works when "clone this page" normally doesn't: cloning is a **copy** job, and models paraphrase. This skill runs real scripts that download the bytes and swap the strings, so nothing gets re-typed from memory and quietly turned into a worse version of the page.
6
+
7
+ ## Install
8
+
9
+ Nothing to download. It installs itself with GHL Command into `~/.claude/skills/clone-site/`. Fully restart Claude after an update so it loads, then say **"clone this page for my client"** with a URL.
10
+
11
+ ## What it asks you first
12
+
13
+ One question, before it copies anything:
14
+
15
+ > Who owns this page, or what permission do you have to copy it?
16
+ > **I own it** · **My client owns it and authorized this** · **I have written permission** · **None of these**
17
+
18
+ Your answer is recorded in the run report. It isn't verified and it isn't argued with — but it does decide how the page gets built:
19
+
20
+ - **You have rights** → a true byte-for-byte clone, rebranded.
21
+ - **You don't** → it extracts the page's actual design system (exact palette, fonts, type scale, spacing, section structure) and rebuilds on that with your own copy and images. Not a from-memory redraw — that's what makes those rebuilds look cheap.
22
+
23
+ ## What you get back
24
+
25
+ - A deploy-ready site folder (and the deploy itself, if you want it on Cloudflare Pages, Vercel or Netlify).
26
+ - A substitution table showing exactly how many times each old detail was found and replaced, plus a warning when one of your facts is *almost* right — the usual reason the old brand survives.
27
+ - A live verification: every asset checked by content-type, not status code, because static hosts return "200 OK" for files that don't exist.
28
+ - A **REVIEW REQUIRED** report.
29
+
30
+ ## The REVIEW REQUIRED report
31
+
32
+ The part that protects you. Every clone run ends with it, listing:
33
+
34
+ - **Must repoint** — form endpoints, webhooks, payment links, booking calendars, analytics and ad pixels still pointing at the original owner. Leave one and your client's leads, bookings or money go to a stranger.
35
+ - **Other people's content** — names, street address, testimonials, before/after photos, staff headshots.
36
+ - **Inherited and regulated claims** — FDA and medical language, "clinically proven," star ratings, client counts, years in business, awards, pricing. All of it was true (or not) for a different business.
37
+
38
+ Nothing is deleted for you. It's flagged, and you decide.
39
+
40
+ ## Where the clone can live
41
+
42
+ Cloudflare Pages (default), Vercel, Netlify, a folder you host yourself — or **inside GoHighLevel**, in which case it hands off to Page Studio and builds the page in your sub-account.
@@ -0,0 +1,247 @@
1
+ ---
2
+ name: clone-site
3
+ description: Clone an existing live website or landing page from its URL and rebrand it for a new client — same layout, images, video and CSS, with new company name, contact details, colors and copy. Starts by asking what rights you have to the page and records your answer; with rights it makes a true byte-for-byte clone, without them it extracts the page's real design system and rebuilds on that. Every run ends with a REVIEW REQUIRED report of everything still belonging to the original owner — lead, booking and payment destinations, other people's names and testimonials, and inherited regulated claims. Triggers on clone this site, clone this page, copy this website, duplicate this funnel, rebuild this landing page for, make this site for my client, mirror this URL, clone URL, copy this landing page, steal this layout, make me one like this.
4
+ compatibility: Claude Code, Claude Cowork, Claude.ai
5
+ ---
6
+
7
+ # Clone Site
8
+
9
+ A clone is a **copy** operation, never a generation operation. You never retype a page from your understanding of it — the scripts in `scripts/` copy the bytes, you build the substitution map, and you verify the result. That distinction is the whole skill: models paraphrase, and a paraphrased clone is the thing that comes back "not even close."
10
+
11
+ One run = **rights → acquire → inventory → rebrand → host → verify → REVIEW REQUIRED report.**
12
+
13
+ **The scripts live at `~/.claude/skills/clone-site/scripts/`** (installed with GHL Command). Call them with that path — dependency-free Node, nothing to install. Work in a run directory outside the user's repo unless they name one.
14
+
15
+ ## Hard rules
16
+
17
+ 1. **Ask for rights first, record the answer, then proceed.** Step 0 below. You are not a rights verifier; the user's declaration is the user's responsibility, and recording it makes that explicit.
18
+ 2. **Coach, never block.** "No rights" routes to the style lane — never to a refusal, and never to a from-memory redraw.
19
+ 3. **Copy, never regenerate.** If a fetch fails, say so. Do not reconstruct a page you could not download.
20
+ 4. **Never silently fix ownership content.** Names, addresses, testimonials, likenesses and claims belonging to the original owner go into the report for a human decision. Flag and propose; never quietly delete or invent replacements.
21
+ 5. **No credentials or payment identifiers are ever carried into a live clone.** `pk_live_*`, payment links, form endpoints and webhook URLs are MUST REPOINT findings.
22
+ 6. **Verify by content-type and by rendering, never by status code.** Static hosts serve 200 + HTML for a missing asset.
23
+ 7. **Never deploy the `reports/` folder.** It contains the user's rights declaration and a list of the original owner's liabilities. Deploy `site/` only.
24
+
25
+ ## STEP 0 — Rights (before any bytes are copied)
26
+
27
+ Ask this first, in these words, before fetching anything:
28
+
29
+ > **Before I copy this page — who owns it, or what permission do you have to copy it?**
30
+ > 1. **I own it**
31
+ > 2. **My client owns it and authorized the migration**
32
+ > 3. **I have written permission from the owner**
33
+ > 4. **None of these** — I found the page and want something like it
34
+ >
35
+ > I'm not checking this, and I'm not going to argue with your answer — it goes in the run report so it's on the record, and it decides which of two ways I build this.
36
+
37
+ Map the answer to `own` / `client-authorized` / `written-permission` / `none` and carry it as `--rights`. `mirror.mjs` refuses to run without it, so this step cannot be skipped by accident.
38
+
39
+ Do not lecture. Do not repeat the rights topic later in the run — it is asked once, recorded once, and the audit at the end carries the practical consequences.
40
+
41
+ | Answer | Lane |
42
+ |---|---|
43
+ | own / client-authorized / written-permission | **Full clone** — byte-for-byte mirror + rebrand |
44
+ | none | **Style clone** — extract the real design system, rebuild with original copy and licensed media |
45
+
46
+ If the user has no rights but insists on a byte-for-byte copy, tell them once, plainly, what that exposes them to (copyright in the layout and copy, other people's testimonials and likenesses, trademark, and inherited advertising claims), and that you'll do the style lane instead — which in practice looks closer to the original than a redraw does. Read `references/rights-and-lanes.md` for the full wording and the edge cases.
47
+
48
+ ---
49
+
50
+ # LANE A — Full clone (rights declared)
51
+
52
+ ## STEP 1 — Scope and ask (one message, defaults offered)
53
+
54
+ 1. **Source URL — and how much of the site?** Ask this outright; never assume a default:
55
+ > *"Just this one page, or the whole site? If it's the whole site, how deep should I follow the navigation — one level (the main menu), two levels (menu plus the pages those link to), or everything I can find?"*
56
+
57
+ A one-page mirror inherits the original's entire menu, so every nav item 404s. For a real client migration the answer is almost always the whole site. Give them the trade-off in their terms: a 36-page contractor site at depth 2 came to **2,104 files and 935 MB**, and the audit then listed **1,598 images with the old brand baked into the artwork**. Run the crawl, then show them the discovered page list and the size before going further.
58
+ 2. **New client facts** — company name, phone, email, address, domain, owner/staff names.
59
+ 3. **Host** — default **Cloudflare Pages**; also Vercel, Netlify, GHL-native (→ Page Studio), or "just give me the folder."
60
+ 4. **Brand changes** — keep the original colors/fonts, or new hex/font values?
61
+ 5. **Media** — re-host everything (default and recommended) or keep hot-links (fragile, and it bills the original owner's bandwidth).
62
+
63
+ If they give a URL and nothing else, run step 2 first and show them the inventory — the substitution questions are much easier to answer once they can see what is actually in the page.
64
+
65
+ ## STEP 2 — Mirror (deterministic)
66
+
67
+ ```bash
68
+ # one page
69
+ node scripts/mirror.mjs --url "<URL>" --out <run-dir> --rights <declaration> --declared "<their words>"
70
+
71
+ # the whole site — follows the site's own navigation
72
+ node scripts/mirror.mjs --url "<URL>" --out <run-dir> --rights <declaration> \
73
+ --crawl --depth 2 --max-pages 40
74
+ ```
75
+
76
+ **Answer the single-page-or-whole-site question with `--crawl`, not with a caveat.** A one-page mirror keeps the original's full navigation, so every menu item 404s the moment the client clicks it — the first thing they will do. Crawling writes each page at its own path (`/contact/` → `contact/index.html`) and rewrites `<a>` links to your own pages as root-relative so the nav works locally and after deploy. `<link rel=canonical>` and `og:url` keep their host on purpose, so the rebrand can point them at the client's real domain.
77
+
78
+ Depth 2 with a 40-page cap covered a 36-page contractor site in one pass. Assets are shared across pages, so the marginal cost per page is small — but the total is not: that site came to 2,104 files and 935 MB.
79
+
80
+ Creates `<run-dir>/site/` (deployable) and `<run-dir>/reports/run-report.json`. It collects assets from **four** places — miss any one and images break:
81
+
82
+ 1. HTML `src` / `href` / `poster` / `srcset`
83
+ 2. `url(...)` inside every downloaded stylesheet
84
+ 3. Full CDN URLs baked into JS bundles
85
+ 4. **Root-relative paths inside JS bundles** (`"/logo.png"`) — the one everyone misses; it caused 6 broken images in the proving run
86
+
87
+ It also does two things that decide whether the clone renders at all:
88
+
89
+ - **URLs inside escaped JSON attributes** (`data-settings="{&quot;url&quot;:&quot;https:\/\/host\/hero.jpg&quot;}"`) are found and localised. Page-builder background slideshows and galleries live here and are invisible to a src/href scan.
90
+ - **Same-origin asset URLs are made root-relative**, including inside inline JS config. A page-builder publishes an asset base path and concatenates chunk filenames onto it at runtime; left absolute, the rebrand points every runtime-built URL at a domain that does not exist.
91
+
92
+ External **media** is re-hosted locally; third-party **scripts** (analytics, chat, payment SDKs) are deliberately left external so the audit can see and flag them. Files over 25 MB are recorded, not downloaded — route those to object storage (`references/hosting-verify-and-audit.md`).
93
+
94
+ Show the user the inventory before rebranding: file count, size, breakdown by type, anything excluded, anything oversized.
95
+
96
+ ## STEP 3 — Rebrand (substitution, not rewriting)
97
+
98
+ Write a `facts.json` (schema in `references/facts-and-substitution.md`), then **dry run first**:
99
+
100
+ ```bash
101
+ node scripts/substitute.mjs --dir <run-dir> --facts facts.json # counts only
102
+ node scripts/substitute.mjs --dir <run-dir> --facts facts.json --apply # after they approve
103
+ ```
104
+
105
+ The script derives the tokens that leak in practice — the bare brand word, every phone format, the address as a unit, domain/www/email variants — protects asset filenames from being rewritten, and after `--apply` runs a zero-check that no replaced token survives.
106
+
107
+ Two warnings decide whether the rebrand is actually finished:
108
+
109
+ - **NEAR MISS** — a token that never matches exactly but appears loosely (different case, spacing or punctuation). The user's fact is slightly wrong. Fix it and re-run the dry run. This is how the old brand survives a "complete" list.
110
+ - **SPLIT ACROSS TAGS** — the old brand is visible on screen but broken up by markup in the file, almost always a two-tone brand mark like `GHL <span style="color:#D4AF37;">Command</span>`. **No string replacement can reach these.** You must edit them by hand, keeping the styling and changing only the words. The script prints the exact markup. The zero-check counts them, so `--apply` reports FAILED until they are fixed — a literal-only check would have said "clean" while the live page still showed the original brand in the header and the footer.
111
+
112
+ Never edit page copy to "improve" it during a clone. That is a different request; offer it afterwards.
113
+
114
+ ## STEP 3b — Render and repair (never skip; a clone that does not render is not a clone)
115
+
116
+ Static scanning cannot see a URL that JavaScript builds at runtime. Webpack lazy chunks are the usual case: the filename exists in no attribute, stylesheet or literal string, so it is never mirrored.
117
+
118
+ Serve the clone locally and load it in a browser:
119
+
120
+ ```bash
121
+ python3 -m http.server 8899 --bind 127.0.0.1 # from <run-dir>/site
122
+ ```
123
+
124
+ In the page, list every same-origin resource that failed:
125
+
126
+ ```js
127
+ const bad=[];
128
+ for (const e of performance.getEntriesByType('resource')) {
129
+ if (!e.name.startsWith(location.origin)) continue;
130
+ const r = await fetch(e.name, {method:'HEAD'});
131
+ if (!r.ok) bad.push(e.name.replace(location.origin,''));
132
+ }
133
+ bad
134
+ ```
135
+
136
+ Then fetch those from the original and re-render:
137
+
138
+ ```bash
139
+ node scripts/repair.mjs --dir <run-dir> --origin "<original-site-url>" --urls failed.txt
140
+ ```
141
+
142
+ **Repeat until nothing fails** — one repair pass often reveals the next, because code that was missing then runs and asks for more.
143
+
144
+ Why this is mandatory: on a real page six missing Elementor chunks meant `section-frontend-handlers` never loaded, so no section background was painted. The hero rendered blank, white headline text sat on white, and the clone looked nothing like the original — while every file-level check reported clean.
145
+
146
+ ## STEP 4 — Pre-launch audit (mandatory, never skipped)
147
+
148
+ ```bash
149
+ node scripts/audit.mjs --dir <run-dir>
150
+ ```
151
+
152
+ Writes `reports/REVIEW-REQUIRED.md`: MUST REPOINT (leads/bookings/money), other people's content, inherited and regulated claims, plus everything excluded. **Show it to the user before deploying**, lead with the MUST REPOINT list and the regulated-claims flag, and never auto-delete a finding. `references/hosting-verify-and-audit.md` explains how to walk them through it.
153
+
154
+ ## STEP 5 — Host
155
+
156
+ Deploy **`<run-dir>/site`** — never the run directory itself.
157
+
158
+ ```bash
159
+ wrangler pages deploy <run-dir>/site --project-name=<slug> # Cloudflare Pages (default)
160
+ ```
161
+
162
+ Create/confirm the project first (Pages fails opaquely if it doesn't exist), and add a `_redirects` 404 rule so missing assets stop returning 200. Host table, per-file caps and the R2 commands for oversized media: `references/hosting-verify-and-audit.md`.
163
+
164
+ **GHL-native seam:** if the clone should live *inside* GHL, stop after step 3 and hand the content plus the asset manifest to Page Studio (`compose_page` / `compose_website`). Do not write a second GHL page writer here.
165
+
166
+ ## STEP 6 — Verify the deployed URL
167
+
168
+ ```bash
169
+ node scripts/verify.mjs --url "<deployed-url>" --dir <run-dir>
170
+ ```
171
+
172
+ Checks every asset by content-type (not status code), retries once before calling anything broken (a single transient answer during a CDN rollout is not a broken asset), separates references the **source page was already serving badly** from ones the clone caused, and confirms old brand tokens are at zero — in the file *and* in the rendered text — with the new ones present.
173
+
174
+ Then open the URL in a browser and check three things, because no fetch-based check can see them:
175
+
176
+ ```js
177
+ // Lazy images report naturalWidth 0 until they load — force them first, or you
178
+ // will report broken images that are perfectly fine (live-caught 2026-07-30).
179
+ document.querySelectorAll('img[loading="lazy"]').forEach(i => { i.loading = 'eager'; });
180
+ document.querySelectorAll('img[data-src]').forEach(i => { if (!i.src) i.src = i.dataset.src; });
181
+ window.scrollTo(0, document.body.scrollHeight);
182
+ await new Promise(r => setTimeout(r, 4000));
183
+ window.scrollTo(0, 0);
184
+
185
+ [...document.querySelectorAll('img')].filter(i => i.complete && i.naturalWidth === 0) // must be empty
186
+ [...document.querySelectorAll('video')].filter(v => v.error) // must be empty
187
+ document.body.innerText.match(/<old brand>/g) // must be null
188
+ ```
189
+
190
+ That last one is not redundant. Rendered text is where a tag-split brand mark shows up, and it is the only check that sees text injected at runtime by third-party widgets. Report the numbers, not an impression.
191
+
192
+ Finish with: the live URL, the verification table, and the REVIEW REQUIRED report.
193
+
194
+ ---
195
+
196
+ # LANE B — Style clone (no rights, or "I just want the look")
197
+
198
+ The failure this lane exists to prevent: refusing the copy, then redrawing the page from memory and producing something drastically worse. Do not do that. Extract the actual design system:
199
+
200
+ ```bash
201
+ node scripts/extract-design.mjs --url "<URL>" --out <dir>
202
+ ```
203
+
204
+ Produces `DESIGN-SYSTEM.md` + `design-tokens.json`: palette ranked by frequency, the author's own CSS variables, font families and the real type scale, spacing rhythm, radii, shadows, breakpoints, container widths, and the section skeleton as **shape** (role, heading word counts, media and CTA counts) — deliberately never the words.
205
+
206
+ **Extraction is step 1 of 5. The deliverable is a working page, never a markdown file.** Do not stop here — a design-system document is not something the user can look at next to the original.
207
+
208
+ ### B1 — Read the extracted artefacts, not your instincts
209
+
210
+ Open `design-tokens.json`. Use `declaredPalette` first when it exists (the site names its own colours — no guessing), then `brandAccents` with their source files. Take the type scale, spacing, radii, container width and grid **verbatim**.
211
+
212
+ ### B2 — Lay the page out from `structure.sections`
213
+
214
+ The skeleton gives you section order, role, heading levels **with word counts**, and image/button/list counts. Build that. If it says the hero is `h1:3w · h5:17w · h2:5w · h6:25w` centred, build exactly that shape — matching the rhythm is what makes it read like the same designer's work. Do not substitute your own preferred layout; that is the from-memory redraw this lane exists to prevent.
215
+
216
+ If `spaShell` is true the structure is client-rendered and absent from the source; open the page in a browser and record the section order by eye first.
217
+
218
+ ### B3 — Write original copy
219
+
220
+ Every headline, paragraph and CTA fresh. Match length and function, never wording. Filling a "17-word subhead" slot by pasting the original's 17-word subhead is copying — it will be caught in B5.
221
+
222
+ ### B4 — Supply real media (this step is not optional)
223
+
224
+ A style-lane page with no imagery reads as a downgrade no matter how good the tokens are. Three legitimate sources: the client's own photographs, licensed stock, or **generated originals**. Never the source site's files. Label any slot you cannot fill yet, and say plainly in the handover that placeholders must be replaced before launch.
225
+
226
+ ### B5 — Leak check, then audit
227
+
228
+ ```bash
229
+ # compare against the ORIGINAL page, never against a rebranded clone
230
+ ```
231
+
232
+ Fetch the original's rendered text and confirm **zero shared 5-, 6- and 8-word sequences** with your rebuild, and zero shared image filenames. Comparing against a rebranded clone is meaningless — the new brand name shows up as "shared" and hides real leaks.
233
+
234
+ Then run `audit.mjs` on the finished page. A fresh build can still inherit a regulated claim you typed from memory.
235
+
236
+ ---
237
+
238
+ ## Anti-patterns
239
+
240
+ - Rewriting page copy "to make it better" mid-clone.
241
+ - Trusting HTTP 200 as proof an asset exists.
242
+ - Scanning only the HTML for assets.
243
+ - Replacing a city without the street address.
244
+ - Auto-deleting testimonials — it destroys the layout and hides the liability instead of surfacing it.
245
+ - Deploying with the original owner's CDN still serving the media.
246
+ - Shipping `reports/` to the live host.
247
+ - Repeating the rights warning after step 0. Ask once, record, move on.
@@ -0,0 +1,96 @@
1
+ # facts.json and how substitution works
2
+
3
+ ## The file
4
+
5
+ ```json
6
+ {
7
+ "old": {
8
+ "company": "Genesis Red Light",
9
+ "aliases": ["Genesis Red Light Therapy", "GRL"],
10
+ "domain": "genesisredlight.com",
11
+ "emails": ["info@genesisredlight.com"],
12
+ "phones": ["+15123992074"],
13
+ "people": ["Serenity Harris"],
14
+ "address": { "street": "103 N Bell Blvd", "suite": "Ste A2", "city": "Cedar Park", "state": "TX", "zip": "78613" }
15
+ },
16
+ "new": {
17
+ "company": "Northgate Wellness",
18
+ "domain": "northgatewellness.com",
19
+ "emails": ["hello@northgatewellness.com"],
20
+ "phones": ["(512) 555-0147"],
21
+ "people": ["Dana Whitfield"],
22
+ "address": { "street": "2200 Palm Valley Blvd", "suite": "Ste 300", "city": "Round Rock", "state": "TX", "zip": "78665" }
23
+ },
24
+ "colors": { "#c9a227": "#1f6feb" },
25
+ "fonts": { "Playfair Display": "Fraunces" },
26
+ "extra": [{ "label": "tagline", "old": "Light. Restored.", "new": "Recover Faster." }]
27
+ }
28
+ ```
29
+
30
+ `old` and `new` are positional: `old.people[0]` is replaced by `new.people[0]`, `old.phones[1]` by `new.phones[1]`. Singular keys (`phone`, `email`, `owner`) work too.
31
+
32
+ Leave a `new` value out and the token is still **counted and reported** — it just isn't replaced, and it lands in the audit under "present with no replacement value." That is deliberate: an unanswered fact should be visible, not silently kept.
33
+
34
+ ## What gets derived automatically
35
+
36
+ | Input | Derived |
37
+ |---|---|
38
+ | `company: "Genesis Red Light"` | the full phrase **and the bare word `Genesis`** — the bare token survived 41 times in the proving run after the full phrase was replaced |
39
+ | `phones: ["+15123992074"]` | `+15123992074`, `1512...`, `5123992074`, `(512) 399-2074`, `(512)399-2074`, `512-399-2074`, `512.399.2074`, `512 399 2074` — all of which appear in real pages, including inside `tel:` links |
40
+ | `address` | the full unit (street + suite + city + state + zip), street with suite, bare street, "city, state", city alone, zip. Ordered longest-first so the **unit** is replaced before the city — replacing only the city produced "new city, old street" in the proving run |
41
+ | `domain` | `www.domain`, bare `domain` |
42
+ | `colors` | the hex in original, upper and lower case |
43
+
44
+ Tokens are applied longest-first, so `Genesis Red Light` is consumed before bare `Genesis`.
45
+
46
+ Generic bare words are not derived (`The`, `Med`, `Spa`, `Wellness`, `Clinic`, `Center`, `Health`, `Group`, `LLC`, …) — replacing `Spa` across a page does far more harm than good.
47
+
48
+ ## Protected strings
49
+
50
+ Every asset path from `run-report.json` — full path, relative path and basename — is masked before substitution and restored after. Without this, replacing `genesis` → `northgate` rewrites `/genesis-logo.png` in the HTML while the file on disk is still `genesis-logo.png`, and the image 404s. Renaming asset files is not worth the risk; keep the source filenames, including hashes.
51
+
52
+ ## Where substitution runs
53
+
54
+ HTML, CSS **and JS bundles**. Minified JS is plain text and safe for literal replacement, and on compiled SPAs the contact details exist *only* in the bundle — HTML-only substitution leaves the original owner's phone number live on the page.
55
+
56
+ Only files under `<run-dir>/site/` are touched. The tool's own reports live in `<run-dir>/reports/` and are never scanned or rewritten.
57
+
58
+ ## Brand marks split across tags
59
+
60
+ The one failure literal substitution cannot fix by itself, and the reason the zero-check looks at rendered text as well as file text.
61
+
62
+ A styled brand mark is normally two elements:
63
+
64
+ ```html
65
+ <p>GHL <span style="color:#D4AF37;">Command</span></p>
66
+ ```
67
+
68
+ A visitor reads "GHL Command". The file contains no such string, so every literal search — including the zero-check — reports clean while the header and footer still carry the original brand. Live-caught on a real proof run 2026-07-30, on a page whose file-level substitution had already passed.
69
+
70
+ `substitute.mjs` detects these exactly: it renders the HTML to text while keeping a map back to source offsets, finds the token in the rendered text, and checks whether that span is contiguous in the file. Counting occurrences before and after tag-stripping does **not** work — text inside attributes disappears when tags are stripped, which cancels out the split gain and hides the problem.
71
+
72
+ Fix them by hand, preserving the styling:
73
+
74
+ ```html
75
+ <p>Beacon <span style="color:#D4AF37;">Ops</span></p>
76
+ ```
77
+
78
+ Then re-run `--apply`; the zero-check reads `in the file OR on screen` when it is genuinely clean.
79
+
80
+ ## The two warnings that matter
81
+
82
+ **NEAR MISS** — a token that matched zero times exactly, but does appear with different case, spacing or punctuation. This is the single most common cause of a leaked brand: `Elite DCs` in your facts, `Elite DCS LLC` on the page. Fix the fact and re-run the dry run.
83
+
84
+ **ZERO-CHECK** (after `--apply`) — re-scans every file for every replaced token, as a literal string *and* in the rendered text. It must read `no replaced token survives in the clone, in the file OR on screen`. If it doesn't, stop; do not deploy.
85
+
86
+ Re-running `substitute.mjs` over already-substituted files finds zero hits by design. The report preserves the earlier run's record rather than erasing it, because `verify.mjs` checks the live page against exactly that list.
87
+
88
+ ## Order of operations
89
+
90
+ 1. Dry run. Show the user the hit table.
91
+ 2. Fix near-misses and missing `new` values.
92
+ 3. Dry run again if anything changed.
93
+ 4. `--apply`, confirm the zero-check.
94
+ 5. Audit. Then deploy.
95
+
96
+ Never apply before the user has seen the counts.
@@ -0,0 +1,101 @@
1
+ # Hosting, verification, and the REVIEW REQUIRED report
2
+
3
+ ## Run directory layout
4
+
5
+ ```
6
+ <run-dir>/
7
+ site/ ← the deployable folder. Deploy THIS path, never its parent.
8
+ index.html
9
+ assets/ css/js exactly as the source named them — do not rename
10
+ media/ re-hosted external media, grouped by source host
11
+ reports/ ← run-report.json, substitution-report.json, REVIEW-REQUIRED.md
12
+ ```
13
+
14
+ `reports/` must never reach a public host: it contains the user's rights declaration and an itemized list of the original owner's liabilities. Deploying the run directory instead of `site/` publishes both.
15
+
16
+ New client media goes in `site/img/brand/` with descriptive kebab-case names (`owner-headshot.jpg`), never `IMG_4821.jpg`. Re-hosted media keeps its source filename, hashes included.
17
+
18
+ ## Host profiles
19
+
20
+ | Host | Deploy | Per-file cap | Oversized media | 404 behavior |
21
+ |---|---|---|---|---|
22
+ | **Cloudflare Pages** (default) | `wrangler pages deploy <run-dir>/site --project-name=<slug>` | **25 MB** | R2 or Stream | serves 200 + HTML unless `_redirects` has `/* /404.html 404` — **add it** |
23
+ | **Vercel** | `vercel deploy --prod` (from `site/`) | ~100 MB, plan-dependent | Vercel Blob | configure in `vercel.json` |
24
+ | **Netlify** | `netlify deploy --prod --dir=<run-dir>/site` | ~100 MB | Netlify LFS / external | `_redirects`, same syntax as Cloudflare |
25
+ | **GHL-native** | hand off to Page Studio (`compose_page` / `compose_website`) | GHL media library | GHL media library | GHL-managed |
26
+ | **Folder only** | zip `site/` and hand it over | n/a | advise per their host | n/a |
27
+
28
+ Create or confirm the project **before** deploying — Cloudflare Pages fails opaquely when the project doesn't exist.
29
+
30
+ ## Oversized media
31
+
32
+ `mirror.mjs` records anything over the cap in `run-report.json` under `oversized` and does **not** download it. Route those files explicitly:
33
+
34
+ ```bash
35
+ wrangler r2 bucket create <client-slug>-media
36
+ wrangler r2 object put <client-slug>-media/<file> --file ./<file>
37
+ # then bind media.<clientdomain> to the bucket and rewrite the refs to that host
38
+ ```
39
+
40
+ Never leave oversized media hot-linked to the original owner's CDN in a delivered clone. It breaks the day they delete the file, and until then the original owner pays for the bandwidth.
41
+
42
+ ## Verification
43
+
44
+ ```bash
45
+ node scripts/verify.mjs --url "<deployed-url>" --dir <run-dir>
46
+ ```
47
+
48
+ 1. **Content-type, not status code.** Static hosts return 200 with the page shell for a missing asset. An asset is present only if the served content-type matches its extension. (Proof: requesting `/reports/REVIEW-REQUIRED.md` on a correctly-deployed clone returns `200 text/html` — the file is not there at all.)
49
+ 2. **One retry before calling anything broken.** A CDN rollout or a burst rate-limit can answer one request with HTML. Verified live: an unretried pass reported 9 broken assets, a clean re-run reported 2, and the truth was 0. A flaky verifier is worse than a strict one.
50
+ 3. **Inherited breakage is separated out.** A reference the *source* page was already serving badly is reported but does not fail the run — redeploying cannot fix a file the original site does not have either.
51
+ 4. **Old tokens at zero, new tokens above zero**, checked against `substitution-report.json`, with asset paths masked so a filename never counts as a brand hit — and checked in the **rendered** text too, which is where a tag-split brand mark hides.
52
+ 5. Exit code 0 = pass, 5 = fail. A failing verify means fix and redeploy — do not show the user the URL first.
53
+
54
+ Then the part no script can do: open the deployed URL in a browser and confirm every `<img>` reports `naturalWidth > 0`, no `<video>` has an error, and `document.body.innerText` contains **zero** occurrences of the old brand. Report counts. "Looks right" is not a verification.
55
+
56
+ **Force lazy images to load before you measure.** A page with `loading="lazy"` images reports `naturalWidth === 0` for everything below the fold, and you will report a pile of broken images that are perfectly fine. Set `loading = 'eager'`, fill any `data-src`, scroll to the bottom, wait, and only count images where `img.complete` is true. Live-caught 2026-07-30 on a WordPress/Elementor page with 43 lazy images: the "broken" list was entirely lazy-loading, and every file served 200 with the right content-type.
57
+
58
+ The rendered-text check earns its place twice over: it catches tag-split brand marks, and it is the only way to see text a third-party widget injects at runtime. A cloned page keeps the original owner's chat widget live until you repoint it — it loads their business identity from their account, and nothing in the files can be edited to change that.
59
+
60
+ ## Walking the user through REVIEW REQUIRED
61
+
62
+ Lead with consequences, in this order:
63
+
64
+ 1. **MUST REPOINT first.** Every item here routes something real — a lead, a booking, a payment, an ad conversion — to the original owner. Read out the form endpoints, payment links, booking embeds and pixel IDs by name and ask what each should become. A cloned page that still posts to someone else's CRM is the worst failure mode this skill has.
65
+ 2. **The regulated-claims flag next.** FDA/medical/efficacy language, "clinically proven," "board certified." These were substantiated (or not) by a different business, and the exposure follows whoever publishes them. Ask directly whether the new business can stand behind each one.
66
+ 3. **Other people's content.** Names, street addresses, testimonials, before/after photos, staff headshots. Never delete these silently — deleting a testimonial block breaks the layout and hides the problem. Propose replacements and let the user decide.
67
+ 4. **Ratings, counts, tenure, awards, pricing.** Numbers that describe the original business. Each needs a true number for the new one or it comes out.
68
+ 5. **Excluded and unresolved.** Oversized files, assets the origin didn't actually serve, third-party scripts left external, and any token with no replacement value.
69
+
70
+ The report is deliberately over-inclusive — a quoted line of marketing copy shows up alongside a real customer testimonial. Say so. Over-flagging costs a minute of reading; under-flagging ships someone else's customer's face on a page selling a health service.
71
+
72
+ Close with the report's own line: *everything in it describes the original business, not the client, and each item needs replacing or removing before the page takes real traffic.*
73
+
74
+ ## Runtime-loaded assets (the render→repair loop)
75
+
76
+ A file-level check cannot see a URL that JavaScript constructs. The pattern that bites hardest:
77
+
78
+ 1. A page-builder publishes an asset base path in inline config.
79
+ 2. At runtime it concatenates chunk filenames onto that base.
80
+ 3. Those filenames appear in no attribute, stylesheet or string literal, so the mirror never fetches them.
81
+ 4. The rebrand rewrites the base path's host, so every runtime URL resolves against a domain that does not exist.
82
+
83
+ The result is a page that passes every static check and renders wrong. Live-caught 2026-07-30: six Elementor handler chunks failed, `section-frontend-handlers` among them, so no section background was painted — the hero was blank and its white headline invisible on white.
84
+
85
+ `mirror.mjs` now prevents step 4 by making same-origin asset URLs root-relative. Step 3 still needs the loop:
86
+
87
+ ```
88
+ render the clone → collect failed same-origin requests → repair.mjs → render again
89
+ ```
90
+
91
+ Repeat until nothing fails. One pass commonly reveals the next, because code restored in pass one runs in pass two and asks for its own dependencies.
92
+
93
+ ## What "done" means
94
+
95
+ A clone run is done when all of these are true, and not before:
96
+
97
+ - the render→repair loop has run to zero failed requests
98
+ - the deployed URL is live and `verify.mjs` exits 0
99
+ - a browser check confirms zero broken images and zero video errors
100
+ - `REVIEW-REQUIRED.md` has been shown to the user and the MUST REPOINT items have been answered
101
+ - `reports/` is not on the live host