Agent Activity Agent Activity Docs
Introduction

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.

Get going

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.

terminal
# 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:

pair over LAN
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.

hello world
# 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.

Mental model

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.

PrimitiveCommandLifecycleBest for
Live Activitylive start/update/endstart once → update many → endbuilds, tests, deploys, training
Notificationnotify / update / dismisspresent → update in place → dismissmilestones, final results, alerts
Pingpingone-shotconnectivity check, no UI
Reference

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:

SettingFlagEnv varDefault
host--hostAGENT_ACTIVITY_HOST
port--portAGENT_ACTIVITY_PORT8473
relay / worker--relayAGENT_ACTIVITY_RELAY
token--tokenAGENT_ACTIVITY_TOKEN
via--viaAGENT_ACTIVITY_VIAauto

--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
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.

config
# 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.

notify
# 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>.

live activity
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.

tasks
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.

list
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.

fan-out
# 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.

discover
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
Customize the card

Visual fields #

These flags control how a notification or Live Activity looks and reads at a glance.

style bar style ring style tasks style pulse phase running phase waiting phase success phase failure
FlagValuesWhat it does
--stylebar · ring · tasks · pulseActivity layout. tasks = checklist; pulse = indeterminate "working" animation.
--phaserunning · waiting · success · failureDrives color & iconography. Set success/failure on the final update so it reads instantly.
--symbolSF Symbol namee.g. hammer.fill, checkmark.seal.fill, xmark.octagon.fill.
--emojione emojiLightweight glyph, no asset needed.
--tinthex 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.
--imageURL · data URL · raw base64Phone downloads/decodes and attaches as the notification image / activity thumbnail.
--progress0.01.0Fraction. Negative or omitted = indeterminate (no determinate bar).
--completed / --totalintegersUnit 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 "Lint:done,Build,Test"
"tasks": [
  { "id": "lint",  "name": "Lint",  "done": true },
  { "id": "build", "name": "Build", "done": false },
  { "id": "test",  "name": "Test",  "done": false }
]
Wire format

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

AgentPacket
{
  "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

kindactionBehavior
notifypresent (default)Show a banner. Matching id → updated in place (no spam).
updateUpdate the notification with id (progress / body / image).
dismissRemove the delivered notification with id.
activitystart (default)Begin a Live Activity. Returns the id (echoed or generated).
updatePush new status / progress / tasks / image to id.
endEnd the activity with id, honoring dismiss.
pingConnectivity check. Server replies { "ok": true } with no UI.

Response

response
{ "ok": true, "id": "the-id-used", "message": "optional" }

Endpoints — LAN HTTP server & relay

MethodPathBodyPurpose
GET/healthLiveness; returns device info.
POST/v1/packetenvelopeUniversal — any kind.
POST/v1/notifyenvelopeConvenience — forces notify.
POST/v1/activityenvelopeConvenience — forces activity.
GET/v1/activitiesList active Live Activities.

The relay/Worker adds wss://…/device?token=… (phone connects out) and POST https://…/v1/packet?token=… (agent pushes in).

Delivery anywhere

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

wrangler setup
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.

secrets
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

deploy
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>.

MethodPathBodyPurpose
GET/healthLiveness → {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/packetan AgentPacketTranslate 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).

For agents

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:

  1. At task startlive start a Live Activity and capture its id.
  2. On each milestonelive update --id "$ID" with fresh progress + status. Push when the % moves meaningfully or a step changes; don't flood.
  3. To see everything runninglist (device) or list --local (offline).
  4. To finish — flip to a terminal --phase success|failure, then live end with a --dismiss policy.
  5. Quick alerts — a plain notify with a symbol and --sound.
an agent reports build progress
#!/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.