API & CLI

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 you can drive two ways: the pm7codecommand for one-shot prompts in scripts, cron, and CI, or the motor's WebSocket API directly when you want full control over sessions and the event stream.

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.

CLI

The pm7code command

pm7codeis a one-shot, scriptable entrypoint that ships with pm7Code. Give it a prompt and it runs a single turn, prints the agent's final answer to stdout, and exits — the same shape as claude -p, but through the pm7Code motor. Pass the prompt as an argument or pipe it on stdin.

# One-shot against the local motor (auto-started if needed)
pm7code "summarize what changed in this repo"

# Pipe the prompt via stdin, run it through Codex
echo "write a haiku about pty buffers" | pm7code --agent codex

When you point it at a loopback address and nothing is listening yet, the CLI starts a local motor for you and reuses it on the next call. Pointed at a remote address, it never spawns anything — it only connects.

# Machine-readable output for scripts/CI: NDJSON events + a final result line
pm7code --json --agent claude "list the open TODOs"

# Continue the most recent session in this folder, then resume an exact one
pm7code --continue "now add tests for it"
pm7code --resume <pm7SessionId> "follow up on that"

# Talk to a remote motor over your tailnet (paired device + opt-in full-auto)
pm7code --server ws://100.x.y.z:8787 \
        --device-id "$PM7CODE_DEVICE_ID" \
        --device-key "$PM7CODE_DEVICE_KEY" \
        --full-auto "deploy the site"

Permission stance

Locally it runs full-auto by default so scripts are not blocked. Remote is safer-by-default: it reads, but declines mutating actions unless you pass --full-auto.

Resumable by design

CLI sessions are always persisted. The --json output gives you a durable pm7SessionId you can later --resume, or use --continue for the latest session in a folder.

Three agents

The CLI exposes claude, codex, and pi. Use --model to override the model for the chosen agent.

CLI

Options

OptionWhat it does
--agent <claude|codex|pi>Agent runtime to use. Default: claude.
--model <name>Model override for the chosen agent.
--output <text|verbose|json>Output mode. Default: text.
--verboseAlias for --output verbose. Progress lines go to stderr.
--jsonAlias for --output json. NDJSON events plus a final result on stdout.
--full-auto / --no-full-autoForce the permission stance. Default: full-auto locally, safer-by-default remote.
--server <ws-url>Server URL. Default: ws://127.0.0.1:8787 (or env PM7CODE_SERVER).
--device-id <id>Device id for a remote motor, obtained by pairing (or env PM7CODE_DEVICE_ID).
--device-key <key>Device key for a remote motor, obtained by pairing (or env PM7CODE_DEVICE_KEY).
--cwd <path>Working directory for the session. Default: the current directory.
--continue, -cResume the latest session in this directory for this agent.
--resume <id>Resume a specific pm7 session id (the one returned by --json).
--timeout <sec>Maximum turn duration in seconds. 0 means no timeout. Default: 600.
-h, --helpPrint the built-in help and exit.

CLI

Environment variables and exit codes

The CLI reads a few environment variables so you can keep the server URL and device credential out of every command line.

VariableMeaning
PM7CODE_SERVERDefault server URL for the CLI. Falls back to ws://127.0.0.1:8787 when unset.
PM7CODE_DEVICE_IDDevice id for a remote motor, obtained by pairing. The motor stores paired devices in ~/.pm7-code/devices.json.
PM7CODE_DEVICE_KEYDevice key matching PM7CODE_DEVICE_ID. Sent in the hello; the motor only stores a hash of it.
M2_PORTPort the motor listens on, and the port in the CLI default URL. Default: 8787.
M2_HOSTBind address for the motor. Default 127.0.0.1 (loopback only). Set 0.0.0.0 to expose it over your tailnet.

Exit codes make the CLI safe to branch on in scripts and CI.

Exit codeMeaning
0Success.
1Agent error — the turn itself failed.
2Usage error — bad arguments or empty prompt.
3Auth error — missing/invalid device key, or a rejected pairing code.
4Network / connection error, or a protocol-version mismatch.
124Timeout — the turn exceeded --timeout and was interrupted.

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.