Agent Activity
Give your coding agents a glanceable presence on your phone. Agent Activity is a native iOS app that turns agent progress into Live Activities on the Lock Screen and Dynamic Island, plus updatable notifications — driven by a tiny CLI any script or agent can call.
Start a Live Activity when a long task begins, push updates as it moves, and end it when it's done. A build, a test suite, a deploy, a training run, a scrape — anything that takes longer than a glance gets its own progress card you can watch without unlocking your phone.
Native iOS
Live Activities on the Lock Screen and Dynamic Island, plus updatable notification banners — no third-party push service in the loop.
macOS menu-bar app
A companion menu-bar app surfaces what's running and keeps the pairing handy on your Mac.
LAN-first
On the same Wi-Fi, the CLI POSTs straight to a small HTTP server on your iPhone — fast, private, no cloud.
Off-network via APNs
Deploy one Cloudflare Worker and updates keep arriving over Apple Push even when the app is closed and you're off the LAN.
The command-line tool is agent-activity. It speaks one JSON envelope —
the AgentPacket — over your LAN
(http://<iphone-ip>:8473) or over HTTPS to the APNs Worker when you're away.
Quick start #
Four steps: install the app, install the CLI, pair the two, and send your first Live Activity.
1. Install the iOS app
Install Agent Activity on your iPhone and open it. The app runs a small
HTTP server on your local network. Open the Activities tab and tap the
"Listening on LAN" pill to reveal the device address, e.g.
192.168.1.42:8473.
2. Install the CLI
The CLI is a single Node.js script (Node 18+) with zero dependencies and no build step.
# clone, then link the binary onto your PATH as `agent-activity` cd agent-activity/cli chmod +x bin/agent-activity.js npm link # creates the `agent-activity` symlink # verify agent-activity --help
3. Pair the CLI with your phone
On the same Wi-Fi, point the CLI at the iPhone's IP and confirm it answers:
agent-activity config set --host 192.168.1.42 --port 8473 agent-activity config show # saved + effective config agent-activity health # confirm the phone answers agent-activity ping # round-trip a packet (no UI)
Config is saved to ~/.agent-activity/config.json (mode 0600).
Off the LAN? Point it at your deployed APNs Worker instead —
end-users do nothing.
4. Send your first Live Activity
live start prints the new activity's id on its own line —
capture it, then address every update and the final end by that id.
# start — the id is the LAST printed line ID=$(agent-activity live start \ --project build --name "Build" \ --title "Compiling" --status "Resolving deps" \ --style bar --symbol hammer.fill --tint "#0A84FF" | tail -1) # update it live agent-activity live update --id "$ID" --progress 0.6 --status "Running tests" # finish — flip to success, keep it visible 30s, then dismiss agent-activity live update --id "$ID" --progress 1.0 --phase success --tint "#34C759" agent-activity live end --id "$ID" --dismiss after:30
That's it. Watch the card animate on your Lock Screen. For a quick alert with no progress bar, use agent-activity notify instead — see the CLI reference.
Concepts #
A few ideas make everything else fall into place.
Project
A project is one agent run or workstream, e.g.
--project build-pipeline. It groups records, logs, and activities in the
app and defaults to "default". You can address every activity in a project
at once with a project-wide live update — see the
fan-out example.
Live Activity — each has its own id
A Live Activity is a persistent card on the Lock Screen and in the Dynamic Island that you start once, update many times, then end. It's the right tool for anything longer than a few seconds.
Every activity has its own id. live start returns/prints
that id — there is no implicit "current" activity. Capture the id and pass it to every
later update and end. Multiple activities run concurrently:
two agents (or two stages) each get their own card.
Updatable notification
A notification (agent-activity notify) is an updatable
banner. Reuse the same --id to rewrite it in place instead
of stacking new banners — best for short milestones and final results. Omit
--id on a fresh notify and one is generated and printed so you
can update it later.
The foreground rule for starting
iOS only lets a Live Activity be started while the app is in the foreground (or freshly launched). Once it's running, it can be updated and ended from the background or off-network. In practice: open the Agent Activity app, kick off your task, then let it move to the background — updates keep flowing.
APNs for closed-app delivery
On the LAN, the app's keep-alive serves updates while backgrounded — but if the app is force-quit, LAN delivery stops. For updates that arrive even when the app is closed and you're off-network, deploy the APNs Cloudflare Worker. Apple Push then delivers start / update / end events straight to the system.
| Primitive | Command | Lifecycle | Best for |
|---|---|---|---|
| Live Activity | live start/update/end | start once → update many → end | builds, tests, deploys, training |
| Notification | notify / update / dismiss | present → update in place → dismiss | milestones, final results, alerts |
| Ping | ping | one-shot | connectivity check, no UI |
CLI reference #
Run agent-activity <command> --help for any command's exact flags.
Add --json to any command for machine-readable output; every command exits
non-zero on failure. Set NO_COLOR=1 to drop ANSI colors.
Configuration & precedence
Every setting can come from a flag, an env var, the config file, or a default — flags → env → file → defaults:
| Setting | Flag | Env var | Default |
|---|---|---|---|
| host | --host | AGENT_ACTIVITY_HOST | — |
| port | --port | AGENT_ACTIVITY_PORT | 8473 |
| relay / worker | --relay | AGENT_ACTIVITY_RELAY | — |
| token | --token | AGENT_ACTIVITY_TOKEN | — |
| via | --via | AGENT_ACTIVITY_VIA | auto |
--via auto (default) prefers the LAN and falls back to the
relay/Worker when the phone is unreachable. Force one with --via lan or
--via relay. Global flags work before or after the command.
Command summary
agent-activity config set --host <ip> [--port 8473] [--relay <url>] [--token <t>] [--via auto|lan|relay] agent-activity config show agent-activity health # GET /health on the LAN server agent-activity ping # connectivity check (no UI) agent-activity discover # mDNS scan for servers on the LAN agent-activity notify --project <id> [--name <n>] --title <t> [...] agent-activity notify update --id <id> --project <id> [--progress] [--body] [--status] [--image] agent-activity notify dismiss --id <id> --project <id> agent-activity live start --project <id> [--name <n>] --title <t> [...] # prints the activity id agent-activity live update --id <id> [--project <id>] [...] agent-activity live end --id <id> [--project <id>] [--dismiss immediate|default|after:<sec>] agent-activity list # ongoing activities (aliases: ps, activities) agent-activity list --local # ids THIS machine started, offline agent-activity logs # compact list of active Live Activities
config set / config show
Point the CLI at your phone once. config show prints the saved file plus the effective config and its source.
# LAN — the normal path agent-activity config set --host 192.168.1.42 --port 8473 # Off-network — point at your APNs Worker (or relay) with a pairing token agent-activity config set --relay https://agent-activity-apns.you.workers.dev --token YOUR_TOKEN agent-activity config show
health & ping
health does a GET /health on the LAN host and prints device info (LAN-only). ping round-trips a packet and honors --via, so it works over LAN or the Worker. Neither produces UI on the phone.
notify — updatable banner
--title is required for a fresh notify; update and dismiss require --id. --image accepts an https://… URL, a data:image/png;base64,… data URL, or raw base64.
# present a notification with a progress bar agent-activity notify \ --project deploy --name "Deploy" \ --title "Deploying to prod" --body "Uploading build…" \ --progress 0.15 --id deploy-42 \ --symbol "arrow.up.circle.fill" --tint "#0A84FF" --sound # update it in place (reuse the id) agent-activity notify update --id deploy-42 --project deploy \ --progress 0.8 --body "Running migrations…" # remove it agent-activity notify dismiss --id deploy-42 --project deploy
live start / update / end
live start requires --title and prints the activity id on its own line (and as id in --json). update and end require --id (unless you fan out by project). --dismiss controls how the card disappears on end: immediate, default, or after:<seconds>.
ID=$(agent-activity live start \ --project build --name "Build pipeline" \ --title "Compiling" --status "Resolving deps" \ --style bar --symbol "hammer.fill" --tint "#34C759" \ --total 8 --completed 0 | tail -1) agent-activity live update --id "$ID" --project build \ --status "Compiling module 4/8" --completed 4 --progress 0.5 agent-activity live end --id "$ID" --project build --dismiss after:5
Checklists with --tasks
For the tasks style, pass a comma-separated string — each item is name or name:done (also :true/:1/:yes). It is not a JSON array.
agent-activity live update --id "$ID" --project build \ --tasks "Lint:done,Typecheck:done,Build,Test,Deploy"
list (ps, activities) & logs
list fetches GET /v1/activities from the device and prints an
aligned table (ID PROJECT STATUS PROGRESS PHASE UPDATED). PROGRESS
shows 0–100%, or — when indeterminate. ps and
activities are aliases; logs is a lighter view of the same data.
agent-activity list # Ongoing Live Activities (3): # ID PROJECT STATUS PROGRESS PHASE UPDATED # act-aaa build Compiling 50% running 2m ago # act-bbb build Linking — running 31s ago # act-ccc deploy Uploading 20% running just now agent-activity list --json | jq -r '.[].id' # just the ongoing ids
Offline id cache — list --local
Every live start records { id, project, title, startedAt } to
~/.agent-activity/activities.json; every live end removes that
id. list --local reads that cache so an agent can re-discover the ids it
started even while offline. The cache is CLI-only state — never sent to
the device — and self-healing (a missing or corrupt file is treated as empty).
Update many activities at once
Repeat --id to hit several explicit activities, or omit --id and pass --project to update every ongoing activity in that project.
# target specific activities agent-activity live update --id act-aaa --id act-bbb \ --status "Paused for review" --phase waiting # update EVERY ongoing activity in the project (one result line per id) agent-activity live update --project build \ --phase success --status "Build succeeded" --tint "#34C759" # ✓ act-aaa # ✓ act-bbb
discover (optional, mDNS)
If your network allows multicast DNS, scan for the app instead of typing the IP. Best-effort — VPNs and guest Wi-Fi often block it, in which case it exits 0 with guidance and you fall back to config set --host.
agent-activity discover # Found 1 service(s): # Agent Activity 192.168.1.42:8473 # agent-activity config set --host 192.168.1.42 --port 8473
Visual fields #
These flags control how a notification or Live Activity looks and reads at a glance.
| Flag | Values | What it does |
|---|---|---|
--style | bar · ring · tasks · pulse | Activity layout. tasks = checklist; pulse = indeterminate "working" animation. |
--phase | running · waiting · success · failure | Drives color & iconography. Set success/failure on the final update so it reads instantly. |
--symbol | SF Symbol name | e.g. hammer.fill, checkmark.seal.fill, xmark.octagon.fill. |
--emoji | one emoji | Lightweight glyph, no asset needed. |
--tint | hex color | #34C759 green, #FF453A red, #0A84FF blue. Leading # optional. |
--tasks | "Lint:done,Build,Test" | Comma-separated string; each item name or name:done. For the tasks style. Not a JSON array. |
--image | URL · data URL · raw base64 | Phone downloads/decodes and attaches as the notification image / activity thumbnail. |
--progress | 0.0–1.0 | Fraction. Negative or omitted = indeterminate (no determinate bar). |
--completed / --total | integers | Unit counts, e.g. --completed 3 --total 8 ("3 of 8"). |
Lock Screen always renders a progress BAR, regardless of
--style. The ring is the Dynamic Island circular indicator
only. Pick the style for how it reads in the Dynamic Island; the Lock Screen falls
back to a bar.
How --tasks expands on the wire:
"tasks": [ { "id": "lint", "name": "Lint", "done": true }, { "id": "build", "name": "Build", "done": false }, { "id": "test", "name": "Test", "done": false } ]
Packet schema #
Every transport — LAN HTTP body, Worker request, CLI payload — speaks the same JSON
envelope: the AgentPacket. Decoding is permissive: unknown
fields are ignored and nearly everything is optional, so clients of different versions
interoperate.
The envelope
{
"kind": "notify" | "activity" | "ping", // required
"action": "...", // per-kind; sensible default per kind
"project": "build-pipeline", // groups records; defaults to "default"
"projectName": "Build Pipeline",
"id": "stable-id", // reuse to update/dismiss / address an activity
"title": "Compiling",
"body": "Building module 3 of 8",
"status": "Running tests", // current task label / subtitle
"progress": 0.37, // 0.0..1.0; negative/omitted = indeterminate
"completed": 3,
"total": 8,
"symbol": "hammer.fill", // SF Symbol name
"emoji": "🛠️",
"tint": "#34C759", // hex
"style": "bar" | "ring" | "tasks" | "pulse",
"phase": "running" | "waiting" | "success" | "failure",
"tasks": [ { "id":"t1", "name":"Lint", "done":true } ],
"image": "https://… | data:image/png;base64,… | <raw base64>",
"sound": true, // notifications: play default sound
"dismiss": "immediate" | "default" | "after:300" // activity end policy
}
Actions by kind
| kind | action | Behavior |
|---|---|---|
notify | present (default) | Show a banner. Matching id → updated in place (no spam). |
update | Update the notification with id (progress / body / image). | |
dismiss | Remove the delivered notification with id. | |
activity | start (default) | Begin a Live Activity. Returns the id (echoed or generated). |
update | Push new status / progress / tasks / image to id. | |
end | End the activity with id, honoring dismiss. | |
ping | — | Connectivity check. Server replies { "ok": true } with no UI. |
Response
{ "ok": true, "id": "the-id-used", "message": "optional" }
Endpoints — LAN HTTP server & relay
| Method | Path | Body | Purpose |
|---|---|---|---|
| GET | /health | — | Liveness; returns device info. |
| POST | /v1/packet | envelope | Universal — any kind. |
| POST | /v1/notify | envelope | Convenience — forces notify. |
| POST | /v1/activity | envelope | Convenience — forces activity. |
| GET | /v1/activities | — | List active Live Activities. |
The relay/Worker adds wss://…/device?token=… (phone connects out) and POST https://…/v1/packet?token=… (agent pushes in).
Off-network (APNs) #
A tiny, serverless backend that pushes Agent Activity Live Activities and notifications over the public internet through Apple Push Notification service (APNs). With it deployed, your agent's Live Activity keeps updating even when the phone is off your local network and the app is backgrounded or killed — no LAN HTTP server, no always-on relay process to babysit.
It is a single Cloudflare Worker (plain JavaScript, no build step) plus a Workers KV namespace for token storage. The routing key is the iOS app's pairing token: the app registers its push tokens under that key, and the agent/CLI targets the same key when it sends packets.
End users do nothing. If someone hands you a deployed Worker URL, paste it into the iOS app (Settings → push backend) and you're done. Everything below is the owner's one-time setup.
Owner — one-time setup
You need an Apple Developer account, Node.js, and a few minutes.
a. Create an APNs Auth Key (.p8)
In the Apple Developer portal → Certificates, Identifiers & Profiles → Keys,
create a new key with Apple Push Notifications service (APNs) enabled and
download the .p8 (you can only download it once). Note the 10-char
Key ID and your 10-char Team ID.
b. Install Wrangler & create the KV namespace
npm i -g wrangler wrangler login wrangler kv namespace create TOKENS # copy the printed `id` into wrangler.toml, # replacing REPLACE_WITH_YOUR_KV_NAMESPACE_ID
c. Set the three secrets
Stored encrypted by Cloudflare — they never live in the repo.
wrangler secret put APNS_KEY # paste FULL .p8 contents, then Ctrl-D wrangler secret put APNS_KEY_ID # the 10-char Key ID wrangler secret put APNS_TEAM_ID # your Apple Team ID
For APNS_KEY, paste the entire PEM including the -----BEGIN PRIVATE KEY----- / -----END PRIVATE KEY----- lines. The Worker strips the envelope itself.
d. Deploy & bake the URL
npm install # optional — only for `wrangler dev` npm run deploy # == wrangler deploy # wrangler prints your Worker URL, e.g. # https://agent-activity-apns.<subdomain>.workers.dev # smoke-test it curl https://agent-activity-apns.<subdomain>.workers.dev/health # → {"ok":true}
Then bake that URL in: set AppShared.pushBackendURL in the app (a Settings field wires it up), and target the same URL + pairing token from the CLI — it POSTs packets to /v1/packet?token=<pairing>.
Sandbox vs. production
wrangler.toml ships with APNS_HOST = "api.sandbox.push.apple.com".
A development build (Xcode / dev-signed) gets sandbox tokens
— keep the sandbox host. A TestFlight / App Store build gets
production tokens — change APNS_HOST to
api.push.apple.com and deploy again. Mixing them yields a
BadDeviceToken error from APNs.
Worker API reference
All POST endpoints take the pairing token as ?token=<pairing>.
| Method | Path | Body | Purpose |
|---|---|---|---|
| GET | /health | — | Liveness → {ok:true}. |
| POST | /v1/register | {pushToStartToken?, deviceToken?, deviceName?} | Merge-store device tokens (dev:<pairing>). |
| POST | /v1/activity/<id>/token | {pushToken, projectID?, projectName?} | Store a Live Activity's update token. |
| POST | /v1/packet | an AgentPacket | Translate to an APNs push. |
/v1/packet by kind: activity/start push-to-starts via
pushToStartToken; activity/update merges the last
content-state with deltas and pushes to the activity's pushToken;
activity/end pushes an end event and deletes the record;
notify sends an APNs alert with
apns-collapse-id = packet.id so repeated ids replace in place.
Responses are {ok:true, id} or {ok:false, error} (404 when no
token is registered, 502 when APNs rejects the push).
The Claude skill #
Agent Activity ships as a Claude skill, so an agent reaches for it automatically when you say things like "ping me when the build finishes," "show progress on my iPhone," or "track this run as a Live Activity." The skill is the operating manual: it teaches the agent the mental model and the exact, real commands — it never invents flags.
How an agent uses it
The skill follows a simple loop:
- At task start —
live starta Live Activity and capture its id. - On each milestone —
live update --id "$ID"with fresh progress + status. Push when the % moves meaningfully or a step changes; don't flood. - To see everything running —
list(device) orlist --local(offline). - To finish — flip to a terminal
--phase success|failure, thenlive endwith a--dismisspolicy. - Quick alerts — a plain
notifywith a symbol and--sound.
#!/usr/bin/env bash set -euo pipefail PROJECT="ci" # 1) Start with the tasks style + a comma-separated checklist. ID=$(agent-activity live start \ --project "$PROJECT" --name "CI Build" \ --title "Build & Test" --status "Starting…" \ --style tasks --symbol "gearshape.2.fill" --tint "#5E5CE6" \ --completed 0 --total 4 \ --tasks "Install deps,Compile,Test,Archive" | tail -1) # 2) Step through the work. agent-activity live update --id "$ID" --status "Compiling" \ --completed 2 --total 4 --progress 0.5 \ --tasks "Install deps:done,Compile:done,Test,Archive" # 3) Flip to success, then end (keep it up 30s). agent-activity live update --id "$ID" --status "Build succeeded" \ --completed 4 --total 4 --progress 1.0 --phase success --tint "#34C759" agent-activity live end --id "$ID" --dismiss after:30
Skill guidelines: always end Live Activities (a stale "running" card is worse than none); reuse one id per logical task; set --phase success|failure on the final state; keep --status / --body short and glanceable. If the CLI isn't on PATH, the skill falls back to bundled curl wrapper scripts that take the same flags.