Serving data

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 (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:

VariableDefaultMeaning
PREEN_URLhttp://127.0.0.1:8444Admin base URL (localhost only)
PREEN_TOKENread from diskRole-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.

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