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
(role-3 write token) and reuses PreenKit’s client, so the two can’t drift — the
MCP is what Claude Code drives; preen is what you drive by hand. It now mirrors
the full MCP admin surface, with new and watch added as
CLI-only local-dev helpers. Its headline feature is watch: save an .html file
and the phone reloads live.
Like the MCP, it needs the Preen Mac app running — the app serves the admin API and writes the token the CLI reads.
Install
tools/install-cli.sh # builds release, symlinks `preen` onto /usr/local/bin
tools/install-cli.sh ~/bin # …or into a directory you choose
It symlinks .build/release/preen, so a later swift build -c release --product preen is picked up in place. Confirm with preen list.
Auth & environment
The CLI resolves its target hub and token from the environment, with per-command overrides:
| Variable | Default | Meaning |
|---|---|---|
PREEN_URL | http://127.0.0.1:8444 | Admin base URL (localhost only) |
PREEN_TOKEN | read from disk | Role-3 write token |
When PREEN_TOKEN is unset, the token is read from ~/Library/Application Support/Preen/write-token, where the Mac app stores it. Pass --url or --token
to override either for one invocation. Every command authenticates with the
write token, but you rarely pass it: read-only commands like list work with no
configuration because the token is read from disk automatically. If that file is
missing (the Mac app has never run), commands fail with a clear message.
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]
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 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 with iPhone (connected: true)." or "No phone paired yet."
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 the phone reflects it within a poll interval — no reload, no
manual publish step. Ctrl-C to stop.
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, forward-only
code history. preen versions lists it newest-first (* marks the current
version); preen set-version re-applies an earlier snapshot as a new current
version — history never rewinds. 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 # re-applies v2 as a new current version
If that widget is the active one, the phone reloads it.
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.
| Spec | Icon |
|---|---|
emoji:📊 | An emoji glyph |
sf:chart.bar.fill | An SF Symbol |
png:/path/to.png | A custom PNG (base64-encoded for you) |
preen icon Battery sf:battery.100 # swap the glyph, leave the HTML alone