Cheldrop Build Plan — v3, 12 August 2026
Supersedes v2 (2 August). Two changes: (1) status — Phases 0–3 are built, committed, and verified against the code (see per-phase DONE markers); (2) a new Phase 3.5 inserted: decide and, if confirmed, execute the free-tier backend switch from Pages Direct Upload to Workers static assets, before the folder architecture is built on top of the publish path. Background: Cloudflare develops new features for Workers only and de-emphasizes Pages; no sunset date exists and existing sites are safe, but Cheldrop's acquisition funnel (free tier) currently rides the path Cloudflare steers people away from, while the paid R2+Worker path already sits right. Pre-launch is the cheapest moment to switch.
v2 preamble follows; per-phase status markers are inline.
Supersedes v1 (1 August). The change: site folders moved from "a feature" to the product's architecture, live sync ships before beta, and the app becomes a background agent. Core reframe, in Chris's words: each subfolder of ~/Documents/Cheldrops/ is a site, with its own config, settings, and theme carried in files inside the folder — and the app runs all the time, watching.
This is the feature-by-feature plan from verified Mac build to launch. Work through it phase by phase with Claude Code. Each feature is a mini-brief: why, what, where, what not to break, done-when.
How to use this plan with Claude Code
Start each feature in a fresh Claude Code session in ~/Developer/Cheldrop/app. Reference docs/cheldrop-functional-spec.md and the relevant section of this plan; have Claude Code read the spec's "What must not break" section before writing code. After each feature: test manually, run npm run build:mac + npm run verify:mac when main-process code changed, commit. One feature per session.
The architecture shift (read once, applies everywhere)
A site is a folder. The managed root is ~/Documents/Cheldrops/ (decided 2 August). One subfolder per site, named by site ID (preview.myclient.com/). The folder carries its own configuration in cheldrop.yaml and optional theme in cheldrop.css, both living in the folder root, both excluded from publish by exact name.
cheldrop.yaml is the source of truth for site configuration. lowdb demotes to a registry: which folders Cheldrop knows about, plus runtime data and secrets that must never live in a folder (folders end up in iCloud/Dropbox sync — treat their contents as semi-public). Split:
- In
cheldrop.yaml: site ID, backend, live on/off, password on/off, TOC/toolbar settings, theme reference, publish-ignore globs.schema_version: 1from day one. - In electron-store/lowdb only: Cloudflare token (safeStorage-encrypted),
adminSecret, password hashes (until the Worker holds them),projectNamecache, timestamps.
Draft schema (finalize in Phase 4.1):
# cheldrop.yaml — managed by Cheldrop; editable by humans and Claude
schema_version: 1
site: preview.myclient.com
backend: r2 # workers | r2
live: true # auto-publish on change
password_protected: true # the hash lives in the Worker, never in this file
toc: index # index | sidebar (docs site) | none
show_toc: true # written in step with `toc:` so older versions still read it
show_toolbar: true
toolbar: { date: true, raw_file: true, download: true, copy_link: false }
theme: cheldrop.css # optional; injected into generated pages
theme_user_html: false # opt-in: also inject theme into user-supplied HTML
header: Acme Docs # optional site chrome (title/logo/link)
footer: © 2026 Acme AS # optional (text/links); both absent by default
ignore: ["*.tmp"] # optional extra publish excludes
Conflict rules: UI edits write the yaml. External edits (human or Claude) are picked up by the watcher and applied; a config change can trigger a republish. Invalid yaml → macOS notification + keep last-good config, never crash, never publish a broken guess. A folder with a valid cheldrop.yaml can be adopted by any Cheldrop install (point the app at it, or drop it into the Cheldrops root) — settings travel with the folder; only secrets need re-establishing. This is site portability and disaster recovery in one move.
cheldrop.css is a rendering input, not a passthrough file. It gets injected (link tag) into everything Cheldrop generates — markdown wrappers, TOC, gallery, auth page. Injection into the user's own HTML files is opt-in via theme_user_html, because silently rewriting user HTML is how you break sites. This file is also the seed of the brandable-output story: a Claude SKILL that edits cheldrop.css + cheldrop.yaml is how "Claude reconfigures your site" works with zero API surface.
Spec collision to fix explicitly: the current renderer passes hidden files through to published output. cheldrop.yaml and cheldrop.css must be excluded by exact name at publish, and the ignore globs honored, in renderer.js — mode detection AND output.
Phase 0 — Housekeeping + smoke test (no code, ~1 hour + ongoing) — MOSTLY DONE 2026-08-12
- Copy
mcp-plan.mdand this plan from Dropbox…/Prosjekter/Cheldrop/docs/into~/Developer/Cheldrop/app/docs/, fix the stale~/Documents/Prosjekter/…paths in mcp-plan.md's embedded prompt, commit. - Delete the retired Dropbox
app/folder and~/Developer/Cheldrop/_to_delete/. - Install the verified DMG into
/Applications. - Smoke test, not beta: give the current build to 2–3 friendly users now — purpose is onboarding only (token creation, permission warnings, first publish on unfamiliar Cloudflare accounts). That surface doesn't change with the folder work, and strangers' accounts break things yours can't. The real beta moves to Phase 5, after the flagship exists — beta feedback on the pre-folder product would validate the wrong product.
- Lemon Squeezy paperwork (tax form, store approval) runs in parallel throughout — it's the long pole.
Status 12 Aug: docs copied into repo ✓, _to_delete/ removed ✓, DMG installed in /Applications ✓. Still open: the 2–3 person onboarding smoke test, and Lemon Squeezy paperwork (tax form + store approval — still the long pole, still blocking payouts).
Phase 1 — Small fixes (2–3 days) — DONE 2026-08-02
All four shipped: 1.1 password toggle (fc73e8e), 1.2 settings autosave (84b2756), 1.3 safeStorage token encryption with idempotent plaintext migration + fallback warning (e5a91a2, 1443caa), 1.4 token onboarding deep-link with pre-filled permissions (c487395 + follow-ups). The deep-link's Pages permission was removed in Phase 3.5 (b42aa63); the floor is now Zone:Read, DNS:Read, Workers:Edit, R2:Edit.
1.1 Password toggle bug
For a password-protected site re-selected in Publish: restore the toggle to on, leave the password field empty and required, never store the password (decision 1 Aug). Where: Publish.jsx, the same restore block as showToc/showToolbar (~lines 76–78). Don't break the 66ec00a per-site restore. Done when publishing unprotected-by-accident is impossible. (Note: Phase 3 changes password handling for R2; this fix still applies to the interim and to the publish-time UX.)
1.2 Settings autosave
Autosave on token validate, license activate/deactivate, defaultBackend/defaultSession change; "Settings updated" toast; remove the Save button. Where: Settings.jsx via existing save-settings IPC. Don't fire a save mid-way through the validate → zones → permissions chain.
1.3 Encrypt the API token with safeStorage
Encrypt cfToken via Electron safeStorage in the get-settings/save-settings handlers; idempotent migration from plaintext on first run; fallback with warning if encryption unavailable. Renderer sees no change. Matters more in v2: a background agent holds this token 24/7.
1.4 Token onboarding deep-link
Quick Start button opening Cloudflare's dashboard token page pre-filled (name + exact permission set) via the undocumented URL format (parameter encoding per cfdata.lol's generator); guided manual flow remains as visible fallback; cf-check-permissions verifies after paste. Floor permissions: Pages edit, Workers edit, R2 edit, DNS edit (the Pages CNAME upsert is mandatory), zone-scoped. (Superseded by Phase 3.5: Workers edit, R2 edit, DNS read, zone read.)
Phase 2 — Auto-update (1–2 days + testing) — DONE 2026-08-02
Shipped (4e37008); full update loop verified end to end with the 1.0.1 release (0ea1e64). main/updater.js exists; electron-updater in dependencies.
electron-updater + GitHub Releases. Check on launch (non-blocking) + menu item; quiet download; install on quit. Add the mac zip target (electron-updater requires it) to the existing sign/notarize pipeline; latest-mac.yml in the release. If the repo stays private, publish releases from a public releases-only repo or generic HTTPS feed — the updater needs unauthenticated access.
Elevated in v2: this app becomes a permanently-running background agent on other people's machines (Phase 4.3). An always-on process with no update channel is not shippable. Test the full loop with a dummy 1.0.1 before trusting it — auto-update is the one feature you can't fix remotely if it ships broken.
Don't break: validateStoredLicense() non-blocking at launch; updater must never block window creation.
Phase 3 — R2: real password protection + purge fix (3–4 days) — DONE 2026-08-03
Shipped (2d6a040). Verified in code: main/r2-worker.js implements PBKDF2-HMAC-SHA256 password hash + HMAC-signed session cookie (server-side auth, no request reaches an object without a valid session); main/cloudflare.js purgeR2Bucket() purges via the R2 API instead of the live domain (DNS race fixed) in both re-publish and delete paths. Remaining from "done when": the curl test matrix belongs to the Phase 9 test pass.
Phase 3.5 — Free-tier backend: Pages → Workers static assets — DONE 2026-08-13
Decision confirmed by Chris and shipped the same day (b42aa63). Cloudflare builds new features for Workers only; Pages is de-emphasized with no sunset date. The free tier — the acquisition funnel — was the last thing publishing via the Pages Direct Upload API; the paid path (R2+Worker) already sat on Workers. Done before Phase 4 deliberately: folders and live sync build straight on the publish path, so this was the last cheap moment.
What landed:
main/cloudflare.js: the Pages flow is gone.uploadWorkerAssetsregisters a manifest ofSHA-256(base64+ext)content hashes at/workers/scripts/:name/assets-upload-session, uploads only the buckets Cloudflare asks for, and redeems the completion token indeployAssetsWorker. An empty bucket list is the unchanged-files case — a re-publish of an untouched folder uploads nothing.- Assets-only Worker (no
main_module): every request is served from the asset store, where requests are free and unlimited, rather than spending the 100k/day Worker request budget.html_handling: 'auto-trailing-slash'preserves the URL shapes Pages produced, so published links keep working. - Custom domain via
addWorkerDomain— the R2 path's existing code, reused as planned. - The permission floor did shrink, as hoped: Zone:Read, DNS:Read, Workers Scripts:Edit, R2:Edit. Pages:Edit is gone and DNS:Edit fell away entirely — a Worker custom domain makes Cloudflare create the record itself (verified: a proxied
AAAAto100::). DNS:Read is kept only for the "this hostname is already taken" warning, which matters more now: Cloudflare refuses a custom domain on a hostname that already has a CNAME. Deep-link (1.4) andcf-check-permissionsboth updated. - Backend id is now
workers.pagessurvives only on site entries written before this change and normalises on the next publish — which also retires that site's Pages project and hand-made CNAME first, so "just re-publish" actually works. Deleting a legacy entry still takes the Pages delete path. - Untouched, as required: the
chel-{subdomain}-{domain-with-dashes}derivation, the free-tier gates (1 site, branding footer, no password), and the whole R2 path.
Verified: end to end on a throwaway site — TOC index at /, markdown wrapper, raw .md served as text/markdown, branding footer present; re-publish took the unchanged-files path; delete removed Worker, domain and DNS record. npm run build:mac + npm run verify:mac all green.
3.1 Server-side auth in the R2 Worker — now a hard prerequisite for live vaults
Why (v2 reason): live sync publishes unattended. The never-store-the-password decision means nobody is present to re-enter it at sync time — so on the client-side-guard model, a protected live site is impossible. Server-side auth dissolves the contradiction: the Worker keeps a salted hash (PBKDF2 via Web Crypto) and HMAC session secret as Worker secrets; content republishes never touch auth. Plus the original reason: current client-side protection leaves every file publicly fetchable by direct URL, which is not a thing you can sell as "password protection."
What: Worker checks every request for a valid HMAC-signed HttpOnly session cookie; otherwise serves the login page; login POST verifies against the hash and sets the cookie with the configured session length. Password set/changed in the app (or at protect-time) provisions the Worker secret — an allowed exception to the re-publish-skips-Worker-deploy rule; keep the skip for all other republishes. Stop injecting the client-side guard on R2 sites. /__cheldrop_admin__/purge stays exempt (adminSecret header).
DECISION — resolved 2 August: password protection is R2-only. UI copy: "Password protection requires the R2 backend." Remove the client-side guard path entirely (Pages sites are simply public); selecting password protection switches/requires backend r2, which is Business Class — the license gate does the upsell work.
Done when: curl of any file URL on a protected R2 site returns login/401, never content; sessions expire per config; purge/delete flows work; a Live protected site republishes with no password prompt.
3.2 R2 purge via API
Replace the live-domain purge with R2 API list + delete (paginated, batched) in the re-publish path. Fixes the DNS-propagation race — which live sync would otherwise hit constantly on fresh sites. Keep the Worker purge endpoint for delete-site (or migrate it too).
Phase 4 — The flagship: folder architecture, live sync, background agent (6–9 days)
Build in this order — each step is testable alone.
4.1 Site folders + cheldrop.yaml (the data-model inversion) — DONE 2026-08-13
What landed:
main/sitefolder.js— new module owning the whole surface: the~/Documents/Cheldrops/root (created lazily),cheldrop.yamlread/write viajs-yaml, config ⇄ app-settings mapping, theme reading,ignoreglobs, folder creation, materialization and adoption.- The merge rule: settings the caller supplies win; settings it leaves undefined come from the file; unknown keys round-trip untouched. That is what makes hand-editing work and what will let an unattended publish (4.2) carry no UI state at all.
- Invalid yaml fails the publish with the parse error rather than falling back to defaults — a bad guess would silently change what a client sees. A
schema_versionhigher than the app knows is refused the same way. - Renderer filters reserved names and
ignoreglobs once on entry, so mode detection, wrappers, TOC andfileMapnever see them. The theme publishes ascheldrop-theme.css(the source name is never published) and is linked into every generated page, last, so it wins. User HTML stays byte-for-byte unlesstheme_user_html: true. - UI: folder mode now uses a native picker — "Create site folder in Cheldrops", "Choose a folder…", "Adopt a site folder…". Selecting a folder site re-reads its yaml from disk; toggling TOC/toolbar/protection writes back, debounced. A files-mode publish offers one-click "Save as site folder". Sites gained a folder badge, path, Reveal, Relink for a moved folder, an adopt banner for unclaimed folders in the root, and keep-or-trash on delete (keep is the default; trashing uses
shell.trashItem). - License gates moved into
ipc.js— free tier refusesbackend: r2and refuses a first publish of a second site. Necessary because a backend can now arrive from a hand-edited yaml, and adoption creates entries without publishing; both walked past the UI-only gate.
Deliberate call, worth knowing: the plan says the theme is injected as a link tag but not what URL it points at. Publishing it under the source name cheldrop.css would contradict "excluded by exact name", so it publishes as theme.css (Chris's call, 13 Aug) and is linked absolutely (/theme.css), which also works at any subdirectory depth. A user file already named theme.css pushes the generated one to theme-1.css.
Fixed in the same session (found while wiring the render options, fixed on Chris's go-ahead): licenseStatus was never passed into renderFiles, and seven wrapper/TOC call sites dropped isBusinessClass — so Business Class sites carried the free-tier "Published with Cheldrop" footer on every page.
Still open from this section: live is read, preserved and cached on the site entry, but nothing watches yet — that is 4.2 by design.
What: Create ~/Documents/Cheldrops/ lazily. New folder-mode sites are created here by default; a drag-drop (Files mode) publish offers one-click "Save as site folder" that materializes the published set into Cheldrops/<site-id>/, writes a cheldrop.yaml from current settings, and converts the site to folder mode. On every publish of a folder site, read cheldrop.yaml as the config source (lowdb entry becomes cache + secrets). UI settings changes write back to the yaml. "Adopt folder" flow: point Cheldrop at any folder containing a valid cheldrop.yaml (or drop it into Cheldrops root) → site appears in the app; prompt to (re)provision anything requiring secrets. Add "Reveal in Finder" per site; missing-folder → clear relink dialog. Deleting a site asks keep-or-trash the folder (default keep).
Renderer changes: exclude cheldrop.yaml/cheldrop.css by exact name from mode detection AND output; honor ignore globs; inject cheldrop.css into generated pages (and into user HTML only when theme_user_html: true).
Where: main/ipc.js (folder create/materialize/adopt, yaml read/write — add js-yaml), main/renderer.js (exclusions + theme injection), Publish.jsx/Sites.jsx (UI). Site entry keeps linkedFolder/publishMode — no schema break.
Don't break: existing sites with linkedFolder elsewhere keep working, no forced migration; read-linked-folder recursion; publishedAt preservation; the resource-naming derivation.
Done when: a site folder moved to a second machine and adopted publishes identically (minus secrets re-link); editing cheldrop.yaml by hand changes app-visible settings; yaml/css never appear on the published site.
4.2 Live sync — DONE 2026-08-13
What landed:
main/livesync.js— chokidar watchers, one per live site. Debounce 5 s after the last change, hard rate cap of one publish per 60 s per site, and changes arriving mid-publish are queued so the latest state publishes once rather than each save publishing in turn.- The publish path is the publish path. The watcher calls the same
performPublish()the Publish button calls — extracted out of thepublishIPC handler for exactly this — and passes no UI state at all: every setting is undefined, so the 4.1 merge rule takes all of them fromcheldrop.yaml. Rendering, TOC, theme, branding and auth are therefore identical by construction, not by duplication. - A protected site republishes unattended, as 3.1 promised: no password is passed, the Worker keeps the auth record it already holds, and a content-only publish never re-deploys the Worker.
cheldrop.yamlis watched too, and applied immediately — not on the next allowed publish. Turninglive: falsein the file stops the watcher within seconds, invalid yaml surfaces as an error status and publishes nothing, and changedignoreglobs take effect before the next run. This is what makes the Claude SKILL work end to end.- Manual publish pauses every watcher for its duration and then resets that site's rate cap, so the two paths never race each other's full replacement.
- Failures back off 30 s → 1 m → 2 m → 5 m → 10 m and retry themselves; a success resets the ladder. Network loss recovers on its own.
- Cloud-sync noise is ignored: dotfiles (which covers the iCloud
.name.icloudplaceholder — the real file arrives as an ordinary add when it materializes),~$…, and.tmp/.temp/.part/.partial/.crdownload/.download/.swpsuffixes, plus the site's ownignoreglobs. - An empty folder publishes nothing. Publish is full replacement and an empty folder is nearly always a move in progress; wiping a client's live site unattended is not Cheldrop's call to make. The status says so instead.
- UI: a Live toggle and status line per site in Sites, and in Publish's folder section. States are Live / Syncing / error-with-reason, pushed from the main process over a new
live-statusevent.
Deliberate calls, worth knowing: (1) Live requires the site to have been published once — the watcher republishes, it does not provision, and an adopted folder has no Cloudflare resources yet. The toggle says so. (Amended 16 August, plan C2: the toggle is no longer disabled before that first publish — Live is a setting of the site, so it is set with the others at creation and written to the yaml; it simply starts working when the site publishes.) (2) .tmp-style files are ignored as triggers but are still published if they sit in the folder, because a manual publish would publish them too; the folder's ignore globs are the way to exclude them from both. (3) Watchers run while the app runs — closing the window still ends them until the menu bar agent lands in 4.3.
Verified: headless Electron smoke test over a real folder — debounce holds, the rate cap holds, two saves inside the cap collapse into one publish carrying the latest state, cheldrop.yaml never enters the publish set, invalid yaml errors without publishing, and live: false stops the watcher. npm run build:mac + npm run verify:mac green.
Still open: the several-day soak with sleep/wake and real network drops, and the "window closed, agent keeps syncing" half of the done-when — that is 4.3.
What: Per-site live: true (folder sites only). Default on, superseding the "default off" this brief was written with — decided 15 August 2026, shipped with polish plan C1 + C2 on 16 August. A new site folder is created with live: true, so the normal case is folder + Live with no configuration; a folder whose path is inside a cloud-synced tree (iCloud, Dropbox, Google Drive, OneDrive) is created with live: false, because that is where the two-machines-publishing-over-each-other risk lives. The rule is one function, sitefolder.defaultLiveFor(), and it decides the default only — it never overrides what a cheldrop.yaml already says. Live still starts working only after the site's first publish: the watcher republishes, it does not provision. Main process watches live folders (chokidar): debounce ~5 s after last change, hard rate cap ~1 publish/60 s/site, queue latest state. Reuse the existing publish path end-to-end so rendering, TOC, auth, theme all apply identically. Watcher also watches cheldrop.yaml itself — config edits apply live (this is what makes the Claude SKILL work end-to-end). Pause watcher during any manual publish. Status in Sites list (Live / syncing / error).
Watch out: publish is full-replacement — debounce and rate cap are mandatory, and check current Cloudflare limits (Workers static assets: 25 MB/file, 20,000 files). Unchanged files are not re-uploaded (Phase 3.5), which makes frequent small edits cheap, but each publish still deploys a new Worker version. Folders inside iCloud/Dropbox: ignore dotfiles/.tmp/partial-download patterns; treat dataless iCloud placeholders as changed-when-materialized; document "enable Live on one machine per site" (two machines + shared folder + Live on both = publish ping-pong).
DECISION — resolved 2 August: Live is available on the free tier's single site. The gates stay: site count (1 free), R2/vaults (Business Class), branding footer (free). Live is the wow that sells the upgrade.
Done when: save a file in any editor → change is live on the domain in ~10 s with the app window closed (see 4.3); network loss mid-sync recovers with backoff; toggling Live off stops the watcher instantly.
4.3 Background agent (menu bar) — DONE 2026-08-14
What landed:
main/tray.js— a menu bar presence that exists from launch: glyph (idle / syncing / error), per-site rows reading✓ site.id — last publish 4 minutes ago(or the error, or what it is publishing right now), pause-all, Open Cheldrop, Launch at login, and a Quit that stops the watchers first. Clicking a site row opens it.- The tray listens to the watchers directly through a new
livesync.onStatus(), not to the IPC event windows use — it is the only surface that reports on live sync when there is no renderer at all. - Failure notifications are per-episode. One macOS notification the first time a site's publish fails; the retry ladder underneath it is silent; a successful publish closes the episode so the next failure speaks again. Success never notifies.
- Pause-all stops publishing without stopping watching: changes accumulate and publish once on resume. Deliberately not persisted — a pause that survived a restart is a site that quietly stopped syncing.
- Window lifecycle: closing the window leaves the agent running (already the macOS default here, now the point of it); the Dock icon is an opt-in hide (
Menu bar only); a single-instance lock focuses the running copy instead of starting a second set of watchers on the same folders. - Launch at login is offered once, by dialog, the first time any site goes Live, and lives permanently in Settings → Background agent alongside the Dock-icon and pause toggles. The OS is the source of truth for it; only "have we asked" is stored. A login-item launch comes up with no window — tray, watchers and license check, nothing else — which is the done-when's first line.
- Tray icons are generated template images (
build/tray/*Template.png, 16 + 32 px, black + alpha) shipped as an extra resource: a hexagon, with a centre dot while syncing and an exclamation on error. Phase 8 redraws them as part of the reskin.
Verified: headless Electron test — 15 checks covering icon loading, tray creation with no window, menu building against the real registry, the worst-state glyph rule, one-notification-per-episode with silent recovery and a fresh episode after it, pause/resume, the ask-once login prompt, and the relative-time strings. npm run build:mac + npm run verify:mac green, tray resources present in the bundle.
Still open: the login/logout and multi-day soak runs belong to the Phase 9 test pass, and the Windows differences (tray behaviour, login items, window-all-closed) stay a Phase 9 item — on Windows the app still quits with its last window.
Why: "An app that runs all the time, watching folders" is a different app posture. Live sync while the window happens to be open is a demo; live sync as an ambient guarantee is the product.
What: Menu bar (tray) presence: status glyph (idle / syncing / error), menu with per-site status + last publish time, pause-all, open dashboard, quit. Closing the main window keeps the agent running (standard Mac behavior; Dock icon optional setting). "Launch at login" toggle (app.setLoginItemSettings), prompted once when the user enables their first Live site. macOS notifications for unattended failures — an agent that fails silently is worse than no agent; batch/throttle them (one per site per error episode, not one per retry). Real quit from the tray menu stops all watchers cleanly.
Where: main/index.js (window lifecycle, tray, login item), new tray module; notifications via Electron Notification.
Don't break: theme init and license validation on window re-open; single-instance lock (second launch focuses the running instance instead of spawning a twin — add if absent).
Done when: log in → agent is in the menu bar with no window; edit a file in a Live folder → published, notification-silent on success; unplug network → one clear failure notification, auto-recovery on reconnect; quit actually quits.
Phase 5 — Beta (the real one)
Now the beta tests the actual product: Cheldrops folder, Live sites, background agent, R2 vaults. 5–10 real freelancers, free tier + a comped Business Class each (you want R2/vault feedback more than $49). Structured ask: set up one real client-facing site, run it Live for a week, report every moment of confusion. Auto-update is in place, so shipping them fixes is cheap. Triage: ship-blockers fixed now, everything else to backlog.
Phase 6 — Cloudflare sync — DONE 2026-08-14 (built out of order, before Phase 5)
What landed:
main/reconcile.js— the comparison, and nothing but the comparison: pure functions, no network, no Electron, so the rules are testable directly.ipc.jsdoes the fetching and hands it three sets of names.- Three read-only listers in
cloudflare.js(listWorkerScripts,listR2Buckets,listPagesProjects). Nothing in this phase writes to Cloudflare, ever. - "I could not look" is never "it is not there." Each lister reports
forbiddenseparately fromok, and a resource kind the token cannot read produces no finding at all. A token without R2 permission must not turn a healthy R2 site into a scary red badge — that is how someone deletes a working site. - Site states:
ok,partial(half-deleted — e.g. the Worker is gone but the bucket remains),missing,unpublished(adopted or imported, nothing expected yet),unknown(unanswerable). Onlymissingandpartialare surfaced, with a plain sentence and two actions: Re-publish… (opens the site in Publish) and Forget this site (drops the local entry only — no Cloudflare calls, no touching the folder). - Orphans —
chel--named resources no entry claims — are offered for import as registry-only entries (importedAtset,publishedAtnull, so the free-tier gate and the Live gate keep their meaning). A Worker and its bucket group into one offer. Names that do not resolve to a domain in the account are listed but not importable, because Cheldrop cannot say what URL they serve. - Reverse-deriving the site ID from a resource name is ambiguous on its own (
chel-my-project-example-comsplits several ways) — resolved against the account's actual zone list, longest domain first, and only accepted if it round-trips throughresourceNameFor. - One definition of the join key.
resourceNameFor()now lives incloudflare.jsand is used by the publish path, the adopt path and reconciliation; the formula is unchanged and the two hand-rolled copies are gone. - UI: a "Check Cloudflare" button on Sites with the last check time and the sentence "Cheldrop only reads — nothing is deleted there", broken-site notes inline on the row, an import banner for orphans, and warnings when the token cannot answer. Runs automatically on page load, cached five minutes in the main process; the cache clears on publish, delete, adopt, import and forget.
Verified: 25 pure-logic checks (name→site-ID splitting including dashed domains and longest-match zones, ok/partial/missing/unpublished/unknown, orphan grouping and importability, legacy Pages sites) plus a live read-only run against a real Cloudflare account: 5 Workers listed, R2 and Pages correctly reported as unreadable rather than empty, 2 orphaned chel- Workers found — which is exactly the drift this phase exists to surface. npm run build:mac + npm run verify:mac green.
Out of scope, deliberately: nothing here deletes a remote resource. Cleaning up an orphan is import-then-delete through the normal delete flow, where the confirmation and the consequences are already spelled out.
Original brief follows.
Phase 6 — Cloudflare sync (1–2 days, scope reduced in v2)
Folder adoption (4.1) now covers local recovery and portability, so this shrinks to remote reconciliation: list chel--named Workers (and any leftover Pages projects from before Phase 3.5), flag local sites whose remote resources vanished (broken badge + repair/remove), list remote chel- resources with no local site (offer import as registry-only entries). Read-only against Cloudflare; never deletes remote resources. Join key is the naming derivation — do not vary it. Matters before MCP so list_sites tells the truth.
Phase 7 — MCP server + the Cheldrop SKILL — DONE 2026-08-14
What landed:
main/mcp-server.js— Streamable HTTP JSON-RPC on127.0.0.1:3847/mcp, started with the app, authenticated by a shared secret (x-cheldrop-mcp-secret, orAuthorization: Bearer) generated on first use. Bound to loopback only,Originchecked before auth so a web page cannot rebind its way in, 1 MB body cap, and a busy port is reported in Settings instead of thrown at startup.- Seven tools, exactly the ones this plan lists:
get_settings(withapp_versionand the Cheldrops root),list_sites(folder-aware:folder_path,live,live_status),create_site,adopt_folder,publish_now,set_password,delete_site.publish_files/publish_folderfrom mcp-plan.md were deliberately not built: with Live folders, writing files is the publish interface. - Nothing is reimplemented. Every tool calls the same function the UI calls.
ipc.js's handler bodies were lifted into named functions (manualPublish,adoptFolderAt,deleteSiteById,setSiteLive) and theipcMain.handlewrappers now call those — so an MCP publish and a button press cannot drift apart, and live sync's pause/rate-cap interplay applies to both. create_siteis the whole "set up a vault for client X" move: folder in~/Documents/Cheldrops/, startercheldrop.yaml, a placeholderindex.md(an empty folder would publish a site that serves nothing), provision + first publish, then Live on — in that order, because Live requires a published site. A failed publish keeps the folder and says so.- Secrets never cross the boundary. No API token, no
adminSecret, no password in any response.set_passwordtakes a password, derives the Worker's auth record, and reports only that it applied — plus the sentence that it cannot be recovered, only replaced. - The guards are the product here.
delete_sitedescribes the damage on the first call and only deletes on a second withconfirmed: true; publishing an empty folder or one with invalid yaml is refused rather than wiping a live site; free-tier limits (one site, no R2, no password protection) answer with the plan'supgradenudge; a domain outside the account is refused with the list of domains that would work. - Settings → MCP server: on/off, running state, the secret (masked, reveal, copy), a copy-ready
claude_desktop_config.jsonsnippet, and New secret, which revokes every configured client immediately. - The SKILL (
docs/cheldrop-skill/SKILL.md) teaches the split first — folder for content and config, tools for what needs the token — then the folder convention, the yaml schema key by key, the css injection model, the rules (never a secret in the yaml, never editsite:, invalid yaml publishes nothing, respectignore), the seven tools, and the four working patterns. Docs page Working with Claude ships in-app with it.
Verified: 81 offline checks against the real modules with Electron stubbed — tool surface and schemas, JSON-RPC (initialize/version negotiation/notifications/unknown method and tool/malformed request), the origin guard, every token-needing tool before setup, domain and subdomain validation, the free-tier gates and their nudges, files-mode and moved-folder refusals, the empty-folder and invalid-yaml stops, password refusals on free/Workers/files-mode with the password never echoed, the delete confirmation gate leaving both registry and folder untouched, adoption, and the HTTP surface (no/wrong/short secret, Bearer, foreign origin, notification 202, parse error, GET 405, unknown path 404, revocation, enable/disable). npm run build:mac + npm run verify:mac green.
Still open: the clean-Claude-Desktop end-to-end (create a real vault, curl it without a cookie for the 401) is a live run against a real domain — it belongs to the Phase 9 test pass, alongside the landing-page "Works with Claude" section, which is the one thing mcp-plan.md defers until after the feature works.
Original brief follows.
Phase 7 — MCP server + the Cheldrop SKILL (4–5 days)
Two integration surfaces now, and they split cleanly:
Files are the content/config interface. With Live folders, Claude updates a site by writing files into it and reconfigures it by editing cheldrop.yaml/cheldrop.css — no API needed. Ship a Cheldrop SKILL (SKILL.md, distributed from cheldrop.com/docs and bundled where useful) that teaches Claude: the Cheldrops folder convention, the yaml schema, the css injection model, and the rules (never write secrets to yaml, respect ignore, one Live machine per site). This is a docs artifact, not app code — cheap to build, huge leverage.
The MCP is the provisioning/action interface — the things files can't do because they need the token: get_settings, list_sites (now folder-aware: path, live status), create_site (provision subdomain/backend + create the folder + write starter yaml — the end-to-end "set up a vault for client X" move), adopt_folder, publish_now (force immediate publish, bypass debounce), delete_site (double-confirm, per mcp-plan.md), set_password (provisions the Worker hash per 3.1; never stored, never echoed). Build on docs/mcp-plan.md's transport/auth/guardrails (localhost:3847, shared secret, confirm-overwrite, upgrade nudges) — the tool list above supersedes its original tool list where they differ. get_settings gains app_version.
Done when: from Claude Desktop: "set up a password-protected vault at vault.client.com for the files I'm about to give you" → MCP creates folder + provisions site + sets password → Claude writes files into the folder → Live publishes them → link works, curl without cookie gets 401. Docs page + landing section ship with the feature (per mcp-plan.md's "when this ships" rules).
Phase 8 — Reskin (2–3 days, floats — start the design exploration now)
Claude Design brief: keep the layout; replace the espresso/terracotta palette ("too brown") with something easier to like on first contact but still with personality; dark + light; WCAG AA body text. Implementation is a CSS-custom-property token swap plus component polish. Propagate in one pass: app tokens → landing → DMG background → icon accent → brand skill doc — a half-reskinned brand reads as abandoned. New in v2: the tray icon needs designing (template image, dark/light menu bar, idle/syncing/error states) — partly done 2026-08-14, out of order, at Chris's request: the hexagon was replaced by the brand mark (disc + four-point flare, knocked out) with the state language unchanged, generated by build/tray/make-tray-icons.py from the app icon's geometry. It is committed source, so the reskin re-runs it with new proportions rather than redrawing by hand. The palette swap itself landed 14 August as Stillwater+ (218509a) across app tokens, the published output (wrapper, TOC, 404, gallery), the app icon and the tray glyph; default cheldrop.css starter themes are part of the design language now — their mechanism shipped 16 August with polish plan C3: three themes live as files in templates/themes/ (Pond, Ember, Ultraviolet, plus "Cheldrop default" = no file), picked in the publish form and copied into the folder as cheldrop.css. Adding or replacing a theme is editing a file in that folder; this phase revisits the CSS, not the plumbing. Can land any time after Phase 1; before Phase 5 beta is ideal so screenshots show the final face.
Propagation status (17 August). The install window is done: build/brand/dmg-background.svg is the source, bash build/make-dmg-background.sh renders build/dmg-background.png + @2x (electron-builder pairs them into a retina tiff with tiffutil at package time), and package.json's dmg block pins the layout the artwork is drawn for — app at (140,180), Applications at (400,180), 128px icons in a 540×380 window. The drop lands on the app icon and the wave carries it right; the ripple is --accent on --bg, the app's own tokens. It carries no text on purpose: Finder draws both icon labels anyway, and text would bind the file to a font that is not installed on a clean machine — the woff2 in node_modules cannot be rendered by rsvg-convert, so a lettered version could not be regenerated without a new build dependency. Still open in the propagation pass: the cheldrop.com landing page and the brand skill doc. Both live outside this repo; until they follow, the brand is half-reskinned in exactly the way the paragraph above warns about.
Phase 9 — Pre-release tests, launch, Windows
- Test pass: the original three (R2 re-publish via API purge, R2 per-file links, permission warnings) plus: auto-update loop; Worker auth curl-tests on every file type; live sync soak (a Live site with real edits over several days, sleep/wake, network drops); background agent across login/logout; yaml hand-editing including invalid yaml; adoption on a second Mac; MCP + SKILL end-to-end from clean Claude Desktop.
- Launch when: LS checkout live + tax cleared, auto-update proven, R2 auth shipped, folders/Live/agent stable through beta, MCP listed. Launch story leads with the folder: "a folder on your Mac that is your website — on your domain, in your Cloudflare account, operable by Claude." Surfaces: cheldrop.com, MCP directories, Product Hunt, your network. Launch execution gets its own plan.
- Windows after Mac launch. Note v2 raises Windows effort: tray/background/notification behavior and login items differ meaningfully on Windows. Still a distribution multiplier, not a validation multiplier.
- Post-launch backlog: starter theme gallery for
cheldrop.css, brandable TOC/toolbar building on the theme system, "did the client view it" (Worker-side counter in the user's own account — keeps the we-host-nothing promise), menu-bar-only mode refinements, expiring vault links, versioning/rollback (folder history), interactive Markdown checkboxes, site locking with countdown.
What changed from v1, and why (for the record)
Site folders went from feature to architecture: cheldrop.yaml in-folder becomes the config source of truth (lowdb → registry + secrets), which buys portability, disaster recovery, hand-editability, and a file-based Claude interface for free. Live sync + background agent moved ahead of beta, because beta on the pre-folder product would validate the wrong product; a 2–3 person onboarding smoke test stays in Phase 0 to keep stranger-account learning flowing. R2 server-side auth was promoted from "honesty" to "prerequisite": unattended republish + never-store-the-password is only coherent when the Worker owns auth. Cloudflare sync shrank (folders now handle local recovery). The MCP split into MCP-for-provisioning + SKILL-for-files. Cost: build time goes from ~3–4 weeks to ~5–6; realistic launch slides from September toward October 2026. Chosen deliberately: depth over date — "a folder that is your website" is a launch story, "a nicer Cloudflare GUI" is not.