# hyperclay-local > Hyperclay Local is a cross-platform Electron desktop app that makes every HTML file in > a folder you choose save itself. It runs a small Express server on > http://localhost:4321, serves that folder, and accepts `POST /_/save` from the page > itself: the page serializes its own DOM and the server writes it back to the real file > on disk, keeping the previous version as a backup. It needs no account, no build step > and no internet connection. Optional two-way sync with a hyperclay.com account exists > and stays off until you turn it on. This file is the complete reference for agents. The human-readable versions live at https://hyperclaylocal.com and https://docs.hyperclay.com/docs/hyperclay-local-app. The source is at https://github.com/panphora/hyperclay-local, product name `HyperclayLocal`, npm package name `hyperclay-local-electron` (not published to npm), current version 1.22.4. Licensed under the First Million Stays Yours License 1.0, SPDX identifier `LicenseRef-First-Million-Stays-Yours-1.0`. Free to use for anything; a fee applies only if products built on it earn you more than US $1,000,000 in a calendar year. Each release carries a printed MIT Conversion Date in its own LICENSE file, after which that version is plain MIT. Plain answers: https://hyperclay.com/host-program. Contributions are accepted inbound MIT with a DCO sign-off. ## Install Download an installer for your platform. These are the 1.22.4 filenames; the URL pattern is stable across releases: ``` https://local.hyperclay.com/HyperclayLocal-1.22.4-arm64.dmg macOS, Apple Silicon https://local.hyperclay.com/HyperclayLocal-1.22.4.dmg macOS, Intel https://local.hyperclay.com/HyperclayLocal-Setup-1.22.4.exe Windows, x64, NSIS https://local.hyperclay.com/HyperclayLocal-1.22.4.AppImage Linux, x64, AppImage ``` To find the current version without scraping a page, read the release manifest: ```bash curl -s https://local.hyperclay.com/release-info.json ``` It returns `{ "version", "commit", "date", "files": [...], "sizes": { filename: bytes } }`. Each build is roughly 100 MB, because it is an Electron app. macOS builds are signed by Hyperspace Systems LLC and notarized. The Windows build is an interactive NSIS installer and lets you change the install directory; it is not code signed, so SmartScreen warns: click "More info" then "Run anyway". Post-install fixes for the two known platform complaints: ```bash # macOS, if Gatekeeper reports "App is damaged" xattr -cr "/Applications/HyperclayLocal.app" # Linux, the AppImage arrives without the executable bit chmod +x HyperclayLocal-*.AppImage ``` ## Running the server The app has no main window. It lives in the system tray, and on macOS it hides the dock icon entirely (`app.dock.hide()`). 1. Click the tray icon to open the popover panel (300x460 px). 2. Click "Choose Folder..." and pick a folder in the native directory picker. The dialog title is "Select folder containing your malleable HTML files". 3. **On a first run that is all you do: choosing the folder also starts the server**, so one click takes a fresh install to a served folder. After that the SERVER rocker, or "Start Server" in the tray menu, turns it on and off. 4. The popover then shows a `localhost:4321` link. Click it, or use "Open Browser" in the tray menu, to open the folder in your default browser. Nothing opens on its own. Click the mounted folder in the popover to open it in your file manager. Click "change" to pick a different one: the server stops and restarts on the new folder, and **sync pauses rather than silently following you**, so re-enable it deliberately if the new folder should sync too. The port is **4321**, hardcoded as `const PORT = 4321` in `src/main/server.js`, and the server binds to `localhost` only (`app.listen(PORT, 'localhost')`). There is no setting, flag or environment variable to change it, and only one folder can be served at a time. If another process holds 4321, the app shows an error dialog; free the port and start again. The selected folder and the server state are written to `settings.json`, so a folder that was being served when you quit is served again on the next launch (`settings.serverEnabled` plus `settings.serverFolder`). Quitting stops the sync engine first, then the server. ## The URL space With the server running and a folder selected: | URL | Serves | |---|---| | `http://localhost:4321/` | Directory listing of the folder root | | `http://localhost:4321/notes.html` | The file `notes.html` | | `http://localhost:4321/blog/` | Directory listing of `blog/` | | `http://localhost:4321/blog/post.html` | The file `blog/post.html` | | `http://localhost:4321/blog/post.html/any/route` | Also `blog/post.html`; everything after the extension is a client-side route | | `http://localhost:4321/logo.png` | Any static file, sent as-is | Documents are `.html` and `.htmlclay`. Every other file type is served as a static asset. `.svg` and `.svgz` are served with `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`, so an SVG can never execute inline with the authority of the page beside it. Paths are rejected before they touch the filesystem: no `\`, no `\0`, no absolute paths, no `.` or `..` segments, no segment longer than 255 characters, and any segment beginning with `.` returns 404. `sites-versions` is a reserved first segment and is never served. All system routes live under the `/_/` marker: `save`, `sync`, `live-sync`, `bus`, `data-loss`, `api`, `meta`, `upload`. Anything else under `/_/` returns 404, so `/_/foo.html` can never fall through and be served as a document. ## Making an HTML file save itself The shortest version, using HyperclayJS from a CDN: ```html Notes

Notes

Start typing here. Everything saves to disk automatically.

``` That gets autosave, a save keyboard shortcut, a warning before leaving with unsaved work, a toast on save, and edit mode. For a file that must work with no network, vendor the script next to the HTML instead of loading it from the CDN. No library is required. Any page that posts a prepared document to `/_/save` with a `Document-URL` header saves. This is a complete, working file: ```html Notes

Notes

Start typing here. Everything saves to disk automatically.

``` ## POST /_/save Writes a whole document to disk. The bare path `POST /save` is the older spelling and still works. ``` POST /_/save Content-Type: text/html; charset=utf-8 Document-URL: http://localhost:4321/blog/post.html Save-Trigger: user ... ``` - The body is the entire serialized document, as text. It must contain a top-level `` element; anything else returns 422 `{"msg":"Not a complete HTML document with a top-level element.","msgType":"error","code":"invalid-document"}`. - **`Document-URL` is the canonical header.** The pre-spec `Page-URL` spelling is still accepted when `Document-URL` is absent, and `Document-URL` wins when both are present. - The target file is the pathname of `Document-URL`, percent-decoded exactly once. A URL pointing at `/` resolves to `index.html`. - A JSON content type is refused with 415 `{"msg":"/_/save takes the document as text, not JSON.","msgType":"error","code":"unsupported-type"}`. - `Save-Trigger: user` marks a human gesture, `auto` marks autosave or a script, and any unknown value is treated as `auto`. The pre-spec header `X-Hyperclay-User-Driven: 1` is read only when `Save-Trigger` is absent. The value feeds the data-clobber guard. - Success is 200 `{"msg":"Saved","msgType":"success","etag":"…"}`. - Maximum body size is 20971520 bytes (20 MB). - Saving to a path that does not exist yet creates the file, including parent directories. Every save runs inside a per-file queue. It reads the previous bytes, writes the first backup of an existing file when its history is empty, and rewrites any Tailwind link to a folder-scoped URL. It reformats only when the root carries literal `formathtml="true"`; otherwise the remaining document bytes are preserved. It then backs up the new content, writes the file via a temp file plus rename, broadcasts the stored HTML to view-mode tabs on the live-sync `saved` lane, refreshes the `/_/api` data sidecar, runs the data-clobber guard, and compiles Tailwind CSS if the page links it. Sidecar, guard and Tailwind failures are logged and never fail the save. ## GET /_/meta Discovery. Returns what this host can do, and, when a `Document-URL` (or `Page-URL`) header names a file that exists, what that document allows: ``` GET /_/meta Document-URL: http://localhost:4321/index.html ``` ```json { "spec": 1, "extensions": ["conditional", "format", "sync", "upload"], "document": { "etag": "…", "writable": true, "maxBytes": 20971520, "upload": { "allowed": true, "maxBytes": 26214400 } } } ``` With no header, or naming a file that does not exist, the `document` key is omitted rather than answered differently. ## POST /_/upload Stores a file beside a document instead of embedding it as a data URL. Multipart form, one part named `file`. ```bash curl -X POST http://localhost:4321/_/upload \ -H 'Document-URL: http://localhost:4321/index.html' \ -F 'file=@note.txt' ``` ```json {"msg":"Uploaded","msgType":"success", "uploads":[{"name":"note-2cf24d.txt","url":"assets-index/note-2cf24d.txt","bytes":5}]} ``` - The file lands in `assets-/` beside the document. `blog/app.html` uploads into `blog/assets-app/`. - The stored name is `-`. Identical bytes converge on one file; different bytes never collide, and the hash prefix lengthens if they would. - `url` is percent-encoded per segment and is relative to the document, so it can be written straight into `src` or `href`. - Maximum 26214400 bytes (25 MB). Over that returns 413 with `"code":"too-large"`. - Refused by extension: `.html`, `.htm`, `.xhtml`, `.htmlclay`, `.js`, `.mjs`, `.cjs`, `.xml`, `.xht`, `.xsl`, `.xslt`, returning 415 `{"msg":"That kind of file cannot be uploaded.","msgType":"error","code":"unsupported-type"}`. SVG is deliberately allowed, because the static lane serves it inert. - The document named by `Document-URL` must already exist; otherwise 404 with `"code":"not-found"`. - `code` values: `bad-request`, `forbidden`, `not-found`, `conflict`, `too-large`, `unsupported-type`. ## Live sync between local browser tabs Two tabs open on the same file stay in step over Server-Sent Events. This is browser-to-browser only. There is no file watcher broadcasting local disk edits, so if you change a file in a text editor, reload the browser. Subscribe: ``` GET /_/sync?document-url=http://localhost:4321/index.html GET /_/sync?document-url=http://localhost:4321/index.html&lane=saved ``` Publish: ``` POST /_/sync Content-Type: application/json Document-URL: http://localhost:4321/index.html {"snapshot": "...", "sender": "tab-1"} ``` - Two lanes. The default `live` lane carries pre-strip snapshots between edit-mode tabs. The `saved` lane carries only post-strip on-disk HTML, broadcast by the server after a write, and is what view-mode tabs subscribe to. - `sender` is echoed to peers so a tab can ignore its own broadcast. - A keep-alive comment is written every 30 seconds; the stream opens with `: connected`. - The legacy addresses `GET /_/live-sync/stream` and `POST /_/live-sync/save` are the same handlers and stay forever, because HyperclayJS hardcodes them. `html` is the older spelling of `snapshot`, and `page-url` the older spelling of `document-url`; both are still read. - Body limit on the POST is 10 MB. ## The per-site data API Any document can publish structured JSON extracted from its own DOM. Put a rules tag in the page: ```html ``` Then read it: ```bash curl http://localhost:4321/_/api/index.html # {"title":"Notes","items":["a","b"]} curl http://localhost:4321/_/api # same as /_/api/index.html curl http://localhost:4321/_/api/blog/post.html ``` `data-rules-name` is a space-separated token list, so `data-rules-name="api cms collection"` matches the `api` token. `data-rules-version` must be `"1"`; any other value returns 400 `{"error":"Unsupported rules version",...}`. A page with no matching tag returns 400 `{"error":"No api rules tag",...}`. Results are cached in a hidden sidecar at `.hyperclay/api/.json` inside the served folder, rewritten on every save and regenerated on a request when its mtime is older than the source file. A regenerated response carries `X-Served-By: app-generated`; a cache hit does not. For one-off extraction with no tag in the page, pass relaxed-JSON rules on the query string of the document itself: ```bash curl 'http://localhost:4321/index.html?data={title:"h1",xs:".item[]"}' # {"title":"Notes","xs":["a","b"]} ``` `?data=` intercepts a GET before the file is served. Without it, the same URL returns the raw HTML. Both endpoints are the local port of hyperclay.com's `serveSiteApi` and `extractSiteData`, and are built on the `hyper-html-api` engine. ## Tailwind CSS A page that links a `/tailwindcss/.css` stylesheet gets that file compiled on save from the classes in the document, using Tailwind v4 with `@tailwindcss/typography` and `@tailwindcss/forms` (forms in `class` strategy). Both the relative form and a fully qualified `https://any.host/tailwindcss/.css` are recognized, and the domain prefix is preserved when the link is rewritten. ```html ``` On save the link is rescoped to the document's own folder, so a nested `blog/post.html` rewrites to `/tailwindcss/blog/post.css` and compiles to `tailwindcss/blog/post.css` on disk. Two documents with the same basename in different folders therefore cannot collide. A request for a stylesheet that is not on disk compiles it on the spot. Compilation failures are logged and never fail the save. ## Version history Every save writes a copy into `sites-versions/` inside the served folder, before and after the write on the very first save of an existing file: ``` sites-versions/index/2026-08-27-22-09-56-277-0400.html sites-versions/blog/post/2026-08-27-22-11-12-709-0400.html ``` The name is local wall time to the millisecond plus the signed UTC offset in force at that moment, so two versions written during a daylight-saving fallback still rank correctly. Same-millisecond bursts get a `-001` through `-999` suffix. Retention keeps the union of two rules: everything newer than 60 days, and the newest 20 per site. A pruning pass runs once at server startup and at most once an hour after that. The tray's "Backups" item opens this folder. `sites-versions` is a reserved path: it is never served over HTTP and never synced. ## The data-clobber guard The app watches for saves that destroy data, keyed off the same `api` rules tag the data API uses, and stores per-file state at `.hyperclay/guard/.json` with a whole-file recovery copy beside it as `.recover.html`. ```bash curl 'http://localhost:4321/_/data-loss?file=index.html' # {"event":null} curl -X POST http://localhost:4321/_/data-loss \ -H 'Content-Type: application/json' \ -d '{"file":"index.html","choice":"dismiss"}' ``` `choice` must be `dismiss`, `revert` or `restore`; anything else returns 400. `revert` and `restore` write through the same path a save does: backup, format, atomic write, broadcast to view-mode tabs, sidecar refresh, Tailwind recompile. Dismissing an incident also posts a control message to hyperclay.com when sync is running, so the same incident clears on your other devices. ## Cloud sync with hyperclay.com Off by default. Enabling it is the only time a file leaves the machine. The app makes no other network requests except an update check against `https://cdn.jsdelivr.net/gh/panphora/hyperclay-local@main/package.json`. **Setup.** Get an API key from https://hyperclay.com/dashboard, open the popover, enter your hyperclay.com username and the key. Keys start with `hcsk_`; the prefix is the only format check. The key is validated with `GET https://hyperclay.com/_/sync/status` carrying an `X-API-Key` header, then stored in `settings.json` encrypted with Electron's `safeStorage` and base64 encoded. If `safeStorage.isEncryptionAvailable()` is false on the platform, the key is written in plaintext. **The remote host** is `https://hyperclay.com`, or `https://localhyperclay.com` when the app runs with `NODE_ENV=development` or `--dev`. It is not user-configurable. **Endpoints the engine calls**, all with `X-API-Key`: ``` GET https://hyperclay.com/_/sync/status GET https://hyperclay.com/_/sync/nodes POST https://hyperclay.com/_/sync/nodes GET https://hyperclay.com/_/sync/nodes/{nodeId}/content PUT https://hyperclay.com/_/sync/nodes/{nodeId}/content PATCH https://hyperclay.com/_/sync/nodes/{nodeId}/rename PATCH https://hyperclay.com/_/sync/nodes/{nodeId}/move DELETE https://hyperclay.com/_/sync/nodes/{nodeId}[?cascade=true] POST https://hyperclay.com/_/sync/control GET https://hyperclay.com/_/sync/stream (Server-Sent Events) ``` **Which direction moves what.** Local to remote is driven by a chokidar watcher on the synced folder. `add`, `change`, `unlink`, `addDir` and `unlinkDir` are debounced 500 ms into a queue and pushed as content writes, folder creations, renames, moves and deletes. A local delete waits 3000 ms before it is committed to the server, so a matching `add` can be recognized as a rename or a move rather than a delete followed by a create. Failed pushes retry at 5 s, 15 s and 60 s, three attempts, and only for network-shaped errors: an authentication failure, a name conflict or an invalid name is never retried. Remote to local is driven by the SSE stream. Frames are typed in their JSON body, not by SSE event name: `live-sync`, `node-saved`, `node-renamed`, `node-moved`, `node-deleted` and `control`. The connection reconnects after 5000 ms with no backoff. A watchdog ticks every 60 seconds and, after 5 minutes of silence, runs a reconciling check that can move files in either direction. Echoes are suppressed two ways. A `live-sync` frame whose sender matches this machine's device id (a `crypto.randomUUID()` persisted as `settings.deviceId`) is dropped. Node mutations are recorded in an in-flight outbox keyed `:` with a 30 second TTL, so the server's broadcast of your own change is ignored. **Reconciliation on start** runs three passes, folders then sites then uploads. For a file present on both sides, in this order: 1. A local mtime more than 60 seconds in the future is treated as intentional: local wins and nothing is uploaded. 2. Local is newer than the server by more than 10 seconds: local wins and uploads. 3. Truncated SHA-256 checksums match: nothing happens. 4. Otherwise the server wins and the file downloads. Ties inside the 10 second buffer go to the server. Clock skew is corrected with an offset measured against `/_/sync/status`. A path change on the server always wins over a local move or rename. The first ever sync never deletes anything on either side. A file deleted on the server while you were offline is moved to `.trash/` inside the synced folder rather than removed, unless the local copy is newer, in which case it is kept and simply dropped from the map. **Never synced**, regardless of what else is true: ``` **/node_modules/** **/sites-versions/** **/tailwindcss/** **/.* **/.*/** **/.trash/** **/.DS_Store **/Thumbs.db ``` Which means `.hyperclay/api/`, `.hyperclay/guard/` and every other dotfile stay local by construction. **Names that will not sync.** This is the most common surprise, because the local server is far more permissive than the sync engine: - A document must match `^[a-z0-9_-]+\.(html|htmlclay)$`. `My Notes.html`, `Post.HTML` and `café.html` all serve perfectly on localhost and never reach hyperclay.com. - No leading or trailing hyphen, no `--`, no Windows reserved basename (`CON`, `PRN`, `AUX`, `NUL`, `COM1`, `LPT1` and so on). - A folder must match `^[a-z0-9_-]+$`. - Maximum folder depth is 5. - Non-document files sync as uploads: name at most 255 bytes of UTF-8, no leading or trailing dot, and none of the control characters or the characters `/ \ < > : " | ? *` and their full-width equivalents. - An upload larger than 10 MB is refused with "File exceeds 10MB limit". Documents have no sync size limit; the 20 MB cap on `/_/save` still applies locally. A name that fails these rules is not silently skipped. It is rejected when the change is queued, raises a `sync-error` marked not retryable, and shows in the popover's notices with the reason, for example `Invalid site name: "My Notes.html". Must be lowercase letters, numbers, hyphens, underscores, ending with .html or .htmlclay`. **Sync state on disk.** No database. Three JSON files per synced folder, in a directory named for the first 12 hex characters of the SHA-256 of the folder's absolute path: ``` /sync-meta/<12 hex>/node-map.json /sync-meta/<12 hex>/sync-state.json /sync-meta/<12 hex>/tombstones.json ``` Tombstones expire after 7 days. All three are written temp file plus rename. ## The local message bus An in-process publish and subscribe bus, provided by `@panphora/hyper-wire`, that lets a page talk to a handler process running on the same machine. It stores nothing and executes nothing itself. ``` GET /_/bus/subscribe?channel=ai-edit (Server-Sent Events) POST /_/bus/send {"channel","type","v","payload","sender"} ``` `POST /_/bus/send` answers `{"delivered": }`. A missing or invalid channel name returns 400. The body limit is 10 MB. Both lanes require the `/_/` prefix, so a real user folder named `bus` is still served as a folder, and both refuse a non-loopback `Host` header. The app ships one built-in bus handler, `ai-edit`, served on the channel `ai-edit` with at most 2 concurrent requests. It takes an element's HTML and a comment and streams back a revised element. The leading `@word` of the comment picks the agent: `@claude` (the default), `@fable`, `@codex`, `@agy`, or any engine you define under `settings.aiEdit.engines` as a command. `@page` is a context token, not an engine, and an unknown leading `@word` is an error rather than a silent fallback. An agent command comes only from settings; a bus payload can never name one. Toggle the whole plugin from the tray ("Disable AI Editing") or with `settings.aiEdit.enabled`. ## Files the app writes Inside the folder you selected: ``` sites-versions//.html version history, never served, never synced tailwindcss/.css compiled stylesheets assets-/ files stored by POST /_/upload .hyperclay/api/.json data API sidecar .hyperclay/guard/.json data-clobber guard state .hyperclay/guard/.recover.html whole-file recovery copy .trash/ files deleted on the server while offline ``` Outside it, under Electron's `userData` directory, which is Electron's `appData` directory plus the app name `Hyperclay Local`: `~/Library/Application Support/Hyperclay Local` on macOS, `%APPDATA%\Hyperclay Local` on Windows, and `$XDG_CONFIG_HOME/Hyperclay Local` (defaulting to `~/.config/Hyperclay Local`) on Linux: ``` settings.json selectedFolder, serverEnabled, serverFolder, syncEnabled, syncFolder, syncUsername, serverUrl, apiKey (encrypted), hasApiKey, deviceId, autoStartEnabled, aiEdit sync-meta/<12 hex>/ node-map.json, sync-state.json, tombstones.json ``` Logs go to `app.getPath('logs')`, which is `~/Library/Logs/Hyperclay Local` on macOS and a `logs` directory inside `userData` on Windows and Linux. One file per day, kept 30 days: ``` sync/2026-08-27.log [timestamp] [LEVEL] [context] message | {json} errors/2026-08-27.log ``` Both are one click away in the tray under "View Sync Logs" and "View Error Logs". Paths in the sync log have the sync folder prefix stripped. A development build appends `-dev` to the userData directory, so `~/Library/Application Support/Hyperclay Local-dev` never collides with the installed app's state. ## The tray menu Right-click the tray icon, or use the popover's Options button: - Three read-only status lines: `Server: On|Off`, `Sync: On|Off`, `AI Editing: On|Off` - Start Server / Stop Server - Enable Sync / Disable Sync, greyed out until an API key and a sync folder both exist - Enable / Disable AI Editing - Open Folder, Backups (opens `sites-versions/`), Open Browser - View Sync Logs, View Error Logs - Select Folder, Enter API Key for Sync, Autostart on Login (popover Options menu only) - About Hyperclay Local, Quit ## Offline behavior Everything except cloud sync works with no network at all: serving, saving, version backups, uploads, the data API, the data-clobber guard, the message bus, live sync between local tabs, and Tailwind compilation, which runs from bundled dependencies and never fetches anything. Two things do reach the internet on their own. The update check hits jsDelivr on launch and, if a newer version exists, shows a notice in the popover; it never installs anything. Sync, when you have turned it on, connects to hyperclay.com. Both fail quietly when offline. A page that loads HyperclayJS or ClayJS from a CDN is the one thing that will break offline, because the script tag itself is a network request. Vendor the library beside the HTML file for a genuinely offline document. The directory listing page links one webfont from `https://hyperclay.com/public/fonts/BerkeleyMonoVariable-Regular.woff2`. It has a full fallback stack and `font-display: swap`, so offline the listing renders in a system monospace font and nothing else changes. The tray popover uses a copy of the same font bundled in the app. Sync resumes by itself: if `syncEnabled`, `hasApiKey` and `syncFolder` are all set, the app restarts the engine on the next launch, and reconciles whatever changed on both sides while it was down. ## Requests the server will reject These rules apply to the whole origin, not per route, and are the usual reason a request that looks correct returns 403. - The `Host` header must name loopback: `localhost`, any `127.x.x.x`, or `::1` in any spelling. Anything else returns 403 `Invalid Host header`. This is what stops DNS rebinding, which a loopback bind alone does not. - On `POST`, `PUT`, `PATCH` and `DELETE`: `Sec-Fetch-Site: cross-site` is refused; `Origin: null` is refused; any other `Origin` must be loopback on any port. A request with no `Origin` at all passes, which is why `curl`, shell scripts and the sync engine work. Refusals return 403 `{"msg":"Cross-origin requests are not allowed.","msgType":"error"}`. - Every request sets `isAdminOfCurrentResource=true` and `isLoggedIn=true` cookies. The local server is single user by design: whoever can reach port 4321 owns every file in the folder. There is no authentication on any local route. - Symlinks pointing out of the served folder are consented at open time only. A link created while the server is running is refused on both reads and writes. ## Limits and non-goals - One folder and one port. No multiple simultaneous servers, no configurable port. - No HTTPS. The server is plain HTTP on loopback. - No auto-update. The app detects a new version and tells you; you download and install it. - No file watcher pushing local disk edits to the browser. Edit a file in a text editor and the open tab will not know until you reload. - No user accounts, no permissions, no multi-tenant behavior. That is what hyperclay.com is for. - Memory use of 100 to 200 MB is normal, because it is an Electron app. ## Development ```bash git clone https://github.com/panphora/hyperclay-local.git cd hyperclay-local npm install npm run dev # Tailwind watch + webpack watch + electronmon, hot reload npm test # jest npm run test:node # node --test, for the pure-ESM engine paths npm run build # distributable for the current platform ``` Node 20 or newer. Building signed installers is documented in `BUILD.md`. The marketing page in `website/` is a static, no-build directory; `npm test -- tests/website` checks it. Two debugging affordances exist only when `!app.isPackaged`: ```bash # Chrome DevTools Protocol on port 9229, for driving the popover's React UI agent-browser connect 9229 # Pin the popover open across reloads, so it does not vanish on blur curl -X POST http://localhost:4321/__dev/popover/show curl -X POST http://localhost:4321/__dev/popover/hide ``` Neither route is registered in a production build. ## Related projects - HyperclayJS, the client library that handles saving, edit mode and autosave for you: https://github.com/panphora/hyperclayjs - ClayJS, the newer client library: https://clayjs.com - hyperclay.com, the hosted platform this app syncs with: https://hyperclay.com ## Links - Site: https://hyperclaylocal.com - Docs: https://docs.hyperclay.com/docs/hyperclay-local-app - Repo: https://github.com/panphora/hyperclay-local - Downloads: https://local.hyperclay.com/release-info.json - License questions: license@hyperclay.com, https://hyperclay.com/host-program