API

Drive pm7Code without the GUI

Most of the time you work with pm7Code in its macOS workspace. But the same agent engine runs behind a small, long-lived remote motor that speaks a WebSocket API — full control over sessions and the event stream. For one-shot prompts in scripts, cron, and CI, use the pm7code CLI instead.

Architecture

One motor, two ways in

The remote motor is a long-lived process that owns sessions. It is deliberately decoupled from any single connection, so a dropped socket never cancels a running turn — you can disconnect and reconnect without losing work. You reach it either through the CLI or straight over its WebSocket API.

1

The motor runs

A WebSocket server owns sessions and streams a per-session event log. Locally, the CLI starts it for you on demand.

2

You connect

The pm7code CLI for one-shot prompts, or your own WebSocket client for full session control.

3

A turn runs

init a session, send a chat turn, read the streamed events, and collect the final answer.

The CLI and the API are the same engine. Anything the CLI does, it does by talking to the motor — so the API is simply the lower-level way to do more than one-shot prompts. The pm7code command itself — options, environment variables, and exit codes — is documented in the main documentation.

API

The WebSocket API

When one-shot prompts are not enough — you want to keep a session open, read the live event stream, answer approvals, or reconnect and replay — connect to the motor directly. It speaks JSON text frames over a single WebSocket. Every message is a small tagged object; the tag field t tells you what it is.

// 1. Open a WebSocket to ws://127.0.0.1:8787, then send the hello FIRST.
//    Loopback may send device: null; a remote motor requires a paired device.
{ "t": "hello", "protocolVersion": 2, "capabilities": [], "appVersion": "2.x",
  "device": null }
// non-loopback: "device": { "id": "<device-id>", "key": "<device-key>" }
// server -> { "t": "helloAck", "protocolVersion": 2, "capabilities": [...] }

// 2. Start a session. persist:true mints a durable, resumable pm7SessionId.
{ "t": "req", "id": "1", "op": "init",
  "params": { "cwd": "/path/to/repo", "agent": "claude-sdk", "persist": true } }
// server -> { "t": "res", "id": "1", "ok": true,
//             "result": { "sessionId": "...", "pm7SessionId": "..." } }

// 3. Run one turn. Events stream while it runs; the res arrives at turn end.
{ "t": "req", "id": "2", "op": "chat",
  "params": { "sessionId": "...", "text": "your prompt" } }
// server -> { "t": "evt", "sessionId": "...", "seq": 7, "evt": "status", "data": {...} }
// server -> { "t": "res", "id": "2", "ok": true,
//             "result": { "finalText": "...", "messageCount": 12, "interrupted": false } }

// 4. Close the session when the turn is done.
{ "t": "req", "id": "3", "op": "close", "params": { "sessionId": "..." } }

Request / response

You send { t:"req", id, op, params }; the motor replies with { t:"res", id, ok, result } or { t:"res", id, ok:false, error } using the same id.

Server-pushed events

Alongside responses, the motor pushes { t:"evt", sessionId, seq, evt, data } frames with a monotonic seq, so you can track and replay exactly what you have already seen.

API

Handshake and authentication

Because the motor is long-lived and can outlive an app update, every connection starts with a version handshake so a new client never talks silently to an old motor. The hello must be the first frame you send. The motor answers with a helloAck; on any mismatch it sends a fatalError and closes the socket — fail-fast, with a machine-readable code.

Auth model

Loopback connections (127.0.0.0/8, ::1) are trusted and auth-free, so the local GUI and CLI just work. Any non-loopback connection must carry a paired device credential in the hello's device field. Pairing happens once with a 6-digit single-use code started on the server; the motor stores only a hash of each device key and compares in constant time. Devices can be revoked individually on the server.

fatalError codeMeaning
INCOMPATIBLEThe client and motor protocolVersion differ. Restart the side that is out of date.
HANDSHAKE_REQUIREDA non-hello frame arrived first. The hello must be the very first frame on every connection.
AUTH_REQUIREDA non-loopback connection sent no device credential. Pair the device and send { device: { id, key } } in the hello.
AUTH_INVALIDThe supplied device key is unknown or was revoked. Pair the device again.
PAIRING_INVALIDThe pairing claim was rejected — wrong, expired, or already-used code.

API

Control ops

After the handshake, you drive a session with a handful of ops. Each is a req frame with an op name and params; the motor answers on the same id.

initcwd, agent, model?, permissionMode?, persist?, continueLatest?, resumePm7SessionId?

Start a session. Selects the agent runtime and returns the ids you reuse on later ops.

Returns { sessionId, pm7SessionId, cacheSid, resumedFrom }

chatsessionId, text, idempotencyKey?

Run one full turn. Blocks until the turn ends; events stream while it runs. The same idempotencyKey never starts a second turn.

Returns { finalText, messageCount, interrupted }

interruptsessionId

Abort the running turn mid-flight. The turn ends with a turnComplete carrying interrupted:true.

Returns { ok }

respondToApprovalsessionId, requestId, decision, message?

Answer a permission request that arrived as a permission event (decision: accept or decline).

Returns { ok }

attachsessionId, lastSeenSeq

Reconnect to a session that outlived the socket. The motor sends a snapshot, replays every event after lastSeenSeq, then resumes live.

Returns snapshot + replay, then live

closesessionId

Close the session. Pending turns are not abandoned silently — close after a turn completes.

Returns { ok }

API

The event stream

While a turn runs, the motor pushes evt frames so you can render progress live. Each frame has the session id, a monotonic seq, an evt kind, and a data payload.

evtMeaning
cacheDeltaAn incremental update to the transcript/cache for the session — the readable run, streamed.
statusThe agent's current activity (thinking, running a tool, compacting). Carries a status label and optional tool label.
turnCompleteThe turn finished. data.interrupted is true when it was stopped by an interrupt.
permissionA permission request lifecycle. data.phase is requested or resolved; requested carries requestId and toolName.
On attach the motor first sends a snapshot frame (status, lifecycle, the seq range it holds), then replays every event after your lastSeenSeq, and only then resumes live — so a reconnecting client catches up exactly once with no gaps or duplicates.

API

Sessions, persistence, and resume

A session is owned by the motor, not by your connection. Pass persist: true on init and the motor mints a durable pm7SessionId that survives a restart. Later you can resume it — by id, or by asking for the latest session in a working directory.

Two ids, two jobs

sessionId is the live handle for ops on the open socket. pm7SessionId is the durable, resumable identity that outlives the process.

Resume server-side

Resume is resolved by the motor, which can see the on-disk sessions. The CLI maps --continue and --resume onto init params so a remote client never needs that disk itself.

Idempotent turns

Send chat with an idempotencyKey to make retries safe: a finished turn replays its result, a running one joins instead of starting a second turn.

Operations

Running the motor

For local use you rarely start the motor by hand — the CLI does it for you. You run it explicitly when you want a remote motor on another machine, reachable over your tailnet.

# Loopback only — local clients connect auth-free
node remote/server.cjs

# Reachable over your tailnet — remote clients must present a paired device key
M2_HOST=0.0.0.0 node remote/server.cjs

Loopback by default

With no M2_HOST it binds 127.0.0.1 and device-auth sleeps — only local clients can reach it, exactly like the GUI.

Open it deliberately

Set M2_HOST=0.0.0.0 to reach it over the tailnet. Now every non-loopback client must present a paired device key.

Pairing, not shared secrets

Start pairing on the server itself (loopback-only op) to get a 6-digit single-use code; the claiming device receives its own revocable key. Paired devices live in ~/.pm7-code/devices.json (0600), hashes only.

Keep device keys secret. A paired key is the only thing standing between your tailnet and a session that can run tools in your repositories. Lost or leaked? Revoke that one device on the server and pair again — other devices keep working.

Looking for how pm7Code behaves inside the workspace — agents, prompts, interactive questions, and the Git Service? See the main documentation.