remote-for-xirp/docs/api.md at main · AboveColin/remote-for-xirp · GitHub
Skip to content

Latest commit

 

History

History
98 lines (84 loc) · 5.73 KB

api.md

File metadata and controls

98 lines (84 loc) · 5.73 KB

Configuration and HTTP API

Configuration

./xirp-remote                                   # foreground, open, no auth
XIRP_REMOTE_KEY=<32+ hex chars> ./xirp-remote    # foreground, key required
./xirp-remote install --key <32+ hex chars>      # as a service, key required
Variable Default Meaning
XIRP_REMOTE_KEY (unset) Access key. Unset means open, no authentication. A key shorter than 16 chars is refused rather than half-honoured.
XIRP_REMOTE_ADDR 0.0.0.0:8790 Listen address.

install bakes both into the LaunchAgent's EnvironmentVariables, so the plist is the record of how the service is configured.

Notifications

Turn them on per device in settings. Three parties have to agree — the browser grants permission, its push service issues a subscription, and this server keeps it and signs each message — so the setting reports which step failed rather than a generic "off".

The VAPID keypair is generated on first use and kept in ~/Library/Application Support/xirp-remote/push.json, mode 0600. It must persist: a new keypair silently invalidates every existing subscription. Subscriptions that keep failing are dropped after five attempts, and a 404 or 410 removes one immediately — that is the push service saying the browser is gone.

The watcher asks the daemon nothing while no device is subscribed, so this costs nothing until you switch it on. On iOS the app has to be installed to the home screen before the browser will issue a subscription at all.

HTTP endpoints

When a key is configured, endpoints take it as an X-Xirp-Key header or the cookie from POST /api/auth. In open mode no credential is needed.

Method Path Purpose
GET /healthz Liveness. No auth.
POST /api/auth {key} → sets cookie.
GET /api/sessions Projected session list.
GET /api/sessions/{id}?limit=N Session plus transcript.
POST /api/sessions/{id}/message {text} → drives the agent.
POST /api/sessions/{id}/stop Stop the session.
GET /api/permissions Live permission requests (see the caveat in protocol.md).
POST /api/permissions/{id} {behavior: allow|deny, message?}.
GET /api/search?q= Full-text search across sessions, deduplicated, capped at 40.
GET /api/meta Projects and installed agents, for the create form.
POST /api/sessions Create: {projectId, goal, name?, agent?, newBranch?, useTerminal?}.
GET /api/sessions/{id}/git Branch and staged/modified/untracked counts.
GET /api/sessions/{id}/log Last 10 commits in the session's worktree.
GET /api/sessions/{id}/urls URLs the daemon detected in the session's output.
POST /api/sessions/{id}/ack Acknowledge a completed or failed session.
GET /api/models?agent= Models for an agent, with context window and pricing.
GET /api/sessions/{id}/pane?lines=N The session's tmux pane, ANSI intact.
POST /api/sessions/{id}/keys?key= Send one allowlisted key (escape, tab, up, down, enter, ctrl-c, …).
GET /api/pair[?format=png] Pairing URL, or a QR PNG of it.
GET /api/diagnostics Daemon, tmux, database and module state.
GET /api/logs?level=&limit= Daemon log records at or above a pino level.
GET /api/sessions/{id}/changes Changed files: uncommitted and branch-vs-base, plus the PR.
GET /api/sessions/{id}/diff?path=&mode= One file's unified diff (worktree or branch).
POST /api/sessions/{id}/fork {newBranch?, forkIntoWorktree?, agent?}.
POST /api/sessions/{id}/swap {agent, reason?}.
GET /api/push/key VAPID public key, generated on first use.
POST /api/push/subscribe Store a browser subscription.
POST /api/push/unsubscribe Forget one, or all.
POST /api/push/test Send a notification now, to prove the chain.
GET /api/restorable Sessions left needing a restore-or-dismiss decision.
POST /api/restore {restore: [], dismiss: []}.
POST /api/sessions/{id}/rename {name} or {regenerate: true}.
GET /api/sessions/{id}/file?path= One file from the session's checkout.

Four traps in the daemon's API worth knowing

All four cost real debugging time and none is visible from the message catalogue:

  1. agents:list replies under the key harnesses, not agents. Reading the obvious key returns an empty list with no error, so the create form silently offered no agents.
  2. A model is not a top-level create field. The daemon derives a session's requestedModel from agent.options.model; passing model at the top level is silently ignored. Verified by creating a session on claude-haiku-4-5 and reading requestedModel back.
  3. useTerminal does not mean "terminal instead of agent". On session:create it selects the tmux transport, which is the default; setting it to false selects Claude-only SDK mode. Passing true hoping for a shell yields a full agent session — the pane came up running Claude Code. A standalone shell is a different message: project:create-terminal.
  4. Session search answers from three sources, each marking its own completion. metadata and messages return in ~4ms already flagged done: true, while jsonl streams a frame per matching transcript for seconds afterwards. Stopping at the first done ends the search before the source that finds anything has started — it returned 0 results in 4ms instead of 36 in 3s. CallStream instead waits until every source seen so far is done and the wire has been idle briefly, so a new upstream source cannot silently truncate results.