CLI

The preen CLI

preen — a terminal authoring tool that publishes widgets and live-reloads them on the phone as you edit.

preen is a human-facing command-line tool for authoring and publishing widgets from your terminal. It talks to the same localhost admin surface the MCP uses (with a write token). It covers widget publishing, feeds, datastore operations, versioning, and registry sharing; the MCP also exposes the authoring guide and action registration. new and watch are CLI authoring helpers. Its headline feature is watch: save an .html file and the phone reloads live.

Run either the Preen Mac app or the standalone headless hub. Both serve the admin API; the daemon also supports terminal pairing, device and consent management, account sign-in, and a macOS user service.

Install

The preen binary ships inside the Mac app. To put it on your $PATH, open the Preen menu and turn on the CLI toggle (it’s also offered during first-run setup). The app installs preen for you and tells you if the install directory isn’t already on your $PATH. Confirm with preen list.

Auth & environment

The CLI resolves its target hub and token from the environment, with per-command overrides:

VariableDefaultMeaning
PREEN_URLhttp://127.0.0.1:8444Admin base URL (localhost only)
PREEN_TOKENread from diskWrite token
PREEN_REGISTRY_TOKEN—Publisher token, for preen registry publish only
PREEN_REGISTRY_URLhttps://platform.preendisplay.comRegistry base (local dev)

When PREEN_TOKEN is unset, the CLI discovers a running daemon through its private endpoint/token files, or falls back to the Mac app’s ~/Library/Application Support/Preen/write-token. Set PREEN_STATE_DIR or pass --state-dir PATH for a custom daemon. Explicit URLs for another endpoint do not automatically receive the local token.

Commands

preen new      <name> [--out FILE.html] [--force]   scaffold a starter widget
preen list                                          list widgets (* = active)
preen status                                         is a phone paired + connected?
preen publish  <file.html> [--name N] [--icon SPEC] [--refresh MS]
preen update   <id|name> <file.html> [--icon SPEC] [--refresh MS] [--note TEXT]
preen icon     <id|name> <SPEC>                      set/replace icon (no HTML resend)
preen versions <id|name>                             list code history (* = current)
preen set-version <id|name> <version>                set which version is live
preen watch    <file.html> [--name N] [--icon SPEC] [--refresh MS]
preen data     <id|name> <json | @file.json | ->    push live data
preen active   <id|name>                            make a widget the active one
preen delete   <id|name>
preen endpoint <id|name>                            print external push URL + token
preen db put    <id|name> <collection> <json | @file | ->   append a datastore record
preen db get    <id|name> <collection> [--limit N] [--since TS] [--order asc|desc]
preen db delete <id|name> <collection> [--record ID]        delete one record / clear
preen registry export  <id|name> --contract TEXT [--description TEXT]
                                 [--sample <json|@file|->] [--out FILE]
preen registry publish <id|name> --contract TEXT [--description TEXT]
                                 [--sample <json|@file|->] [--changelog TEXT]
                                 [--reapply] [--registry-id UUID]

preen db reaches a widget’s datastore — durable, per-widget SQLite, unlike preen data (a single last-value feed). db put appends a record (add --record ID to replace one); db get prints matching records as JSON; db delete removes one record, or the whole collection when --record is omitted.

Most commands take an id or a (unique, case-insensitive) name — so preen active Temperature works as well as preen active wgt_abc123. An ambiguous name is rejected; pass the id.

preen status is the read-only health check: it reports whether an iPhone is paired and currently connected — the CLI counterpart of the MCP’s pair_status.

preen status        # → "Paired: Desk (iPhone, connected)" or "No device paired."

Mac Notches

preen surface status lists connected displays and their paired Notches. Pair with preen surface pair DISPLAY_ID, then target its returned ID explicitly:

preen surface show SURFACE_ID "Battery"
preen surface event SURFACE_ID '{"title":"Build finished"}'
preen surface configure SURFACE_ID '{"hoverEnabled":true}'
preen surface rules SURFACE_ID @rules.json
preen surface hide SURFACE_ID
preen surface remove SURFACE_ID

One Notch belongs to one display. Hiding keeps its pairing; removing frees the screen. See Mac Notches for layouts, hooks, and settings.

The dev loop: watch

watch is the reason to reach for the CLI over the MCP. It publishes the file once (updating an existing widget of the same name, or publishing a new one), then re-publishes on every save:

preen new battery --out battery.html       # scaffold a starter
preen watch battery.html --name "Battery"  # edit the file → phone reloads live

Save the file and watch updates the hub. The hub sends a WebSocket reload message so the connected phone refetches the HTML automatically. Data-only updates use the separate live feed and do not reload the page. Ctrl-C to stop.

--refresh stores manifest metadata; it does not control the current iOS streaming or backup polling interval. See Pushing data.

Widgets you publish with the CLI appear in the Mac app’s gallery under Browse widgets ▸ Custom, labeled as CLI-published — the same space MCP-published widgets land in. The Built-in tab is reserved for the trusted templates that ship with the app itself.

Pushing data

preen data accepts JSON three ways — inline, from a file, or from stdin:

preen data Battery '{"pct": 82, "available": true}'   # inline
preen data Battery @reading.json                       # from a file
some-sensor | preen data Battery -                     # from stdin

For producers that aren’t the CLI (cron, sensors, another machine), preen endpoint <id|name> prints the standalone push URL and token — the same pair get_data_endpoint returns. See Pushing data.

Versioning

Every publish, update, and watch save appends to a bounded code history; preen update --note "added sparkline" annotates the new cut. preen versions lists the history newest-first (* marks the version currently live, and cuts that are pristine copies of a shipped built-in template carry a [built-in vN] marker); preen set-version selects an earlier snapshot as live — nothing is appended and history stays intact, so you can flip back and forth freely. A snapshot is the whole widget cut (HTML, icon, actions), so reverting restores all of it. These mirror the MCP’s list_versions and set_widget_version.

preen versions    Battery     # 4* · 3 · 2 · 1   (number, timestamp, note)
preen set-version Battery 2   # v2 is now live; `versions` shows 4 · 3 · 2* · 1

If that widget is the active one, the phone reloads it.

Sharing a widget publicly

preen registry submits a widget to the public registry, so anyone can install it from a link. Submissions are reviewed before they go public — publish reports the share link and “pending review”, never “live”.

Mint a publisher token at platform.preendisplay.com/account/tokens (it is shown once — only its hash is stored) and set it as PREEN_REGISTRY_TOKEN. It is separate from PREEN_TOKEN: that one reaches your own Mac, this one reaches the internet. The CLI can read PREEN_TOKEN from the hub’s local token file; the publisher token must be supplied separately.

If the widget you’re publishing was itself installed from the registry, publish credits the widget it came from automatically — both share pages then show the lineage. --derived-from UUID,UUID adds parents for anything you borrowed without installing it. It’s attribution, not a dependency: nothing is fetched, and a parent going away doesn’t affect your widget.

export PREEN_REGISTRY_TOKEN=preen_pub_…

# Look at what would be submitted, without submitting it.
preen registry export "Bus times" --contract '{ "minutes": number, "route": string }' \
  --description "Next departures for the 38." --out bus-envelope.json

# Submit it. stdout is the share link, so it pipes.
preen registry publish "Bus times" --contract '{ "minutes": number, "route": string }' \
  --description "Next departures for the 38."

--contract is required: it is the plain-English shape of the JSON your widget renders, and without it an installer has your widget and no idea what to feed it. Everything else about the widget — name, HTML, icon, refresh interval — comes off the hub, so it describes what is actually running.

sampleData defaults to the widget’s last-pushed feed, which is usually right: a running widget already has real data of the right shape, and that is what the reviewer and the share page’s preview render. Override it with --sample (a JSON object, a @file, or - for stdin).

Publishing the same widget again submits a new version of the same registry entry — add --changelog "what changed", which shows on the share page. After a rejection, fix it and add --reapply: that keeps the version number and counts as another attempt.

Two limits worth knowing: registry installs receive no Mac actions (the envelope has no actions key, so interactive actions do not travel; local UI and datastore access still work), and only SF Symbol icons do — an emoji or PNG icon is left out, and the CLI says so when it happens.

Icons

--icon takes a kind:value spec. preen icon <id|name> <SPEC> sets or replaces a widget’s icon using the same grammar without re-sending its HTML (the CLI counterpart of the MCP’s set_widget_icon); the --icon flag on publish / update / watch carries it inline.

SpecIcon
emoji:📊An emoji glyph
sf:chart.bar.fillAn SF Symbol
png:/path/to.pngA custom PNG (base64-encoded for you)
preen icon Battery sf:battery.100        # swap the glyph, leave the HTML alone