diff options
Diffstat (limited to 'SOCKET_API.md')
| -rw-r--r-- | SOCKET_API.md | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/SOCKET_API.md b/SOCKET_API.md new file mode 100644 index 00000000..48c74741 --- /dev/null +++ b/SOCKET_API.md @@ -0,0 +1,360 @@ +# herdr socket api + +herdr exposes a local unix socket API for scripts, tools, and coding agents that want to control a running herdr instance or subscribe to pane/workspace events. + +this is the low-level integration surface. +a CLI wrapper on top of it is planned, but the socket API is the foundation. + +## transport + +- transport: unix domain socket +- encoding: newline-delimited JSON +- request/response: one JSON request per line, one JSON response per line +- subscriptions: send `events.subscribe`, receive an ack, then keep the same connection open for pushed events + +socket path resolution: + +1. `HERDR_SOCKET_PATH` +2. `$XDG_RUNTIME_DIR/herdr.sock` +3. `$XDG_CONFIG_HOME/herdr/herdr.sock` +4. `$HOME/.config/herdr/herdr.sock` +5. `/tmp/herdr.sock` + +## request shape + +all requests use this envelope: + +```json +{ + "id": "req_1", + "method": "ping", + "params": {} +} +``` + +success responses: + +```json +{ + "id": "req_1", + "result": { + "type": "pong", + "version": "0.1.2" + } +} +``` + +error responses: + +```json +{ + "id": "req_1", + "error": { + "code": "pane_not_found", + "message": "pane p_1_99 not found" + } +} +``` + +## ids + +workspace ids look like: + +- `w_1` +- `w_2` + +pane ids look like: + +- `p_1_1` +- `p_1_2` +- `p_2_1` + +that means: +- first number = workspace number +- second number = pane id inside that workspace + +## core request methods + +currently useful methods include: + +### basic +- `ping` + +### workspace +- `workspace.list` +- `workspace.get` +- `workspace.create` +- `workspace.focus` +- `workspace.rename` +- `workspace.close` + +### pane +- `pane.list` +- `pane.get` +- `pane.read` +- `pane.send_text` +- `pane.send_keys` +- `pane.split` +- `pane.close` + +### waits / events +- `pane.wait_for_output` +- `events.subscribe` + +## example: create a workspace + +```json +{ + "id": "req_create", + "method": "workspace.create", + "params": { + "cwd": "/home/can/Projects/herdr", + "focus": true + } +} +``` + +example response: + +```json +{ + "id": "req_create", + "result": { + "type": "workspace_info", + "workspace": { + "workspace_id": "w_1", + "number": 1, + "label": "herdr", + "focused": true, + "pane_count": 1, + "agent_state": "unknown" + } + } +} +``` + +## example: read pane output + +```json +{ + "id": "req_read", + "method": "pane.read", + "params": { + "pane_id": "p_1_1", + "source": "recent", + "lines": 80 + } +} +``` + +`source` can be: +- `visible` +- `recent` + +## example: send text and press enter + +low-level input is intentionally explicit: + +```json +{ + "id": "req_send_text", + "method": "pane.send_text", + "params": { + "pane_id": "p_1_1", + "text": "bun run dev" + } +} +``` + +then: + +```json +{ + "id": "req_send_keys", + "method": "pane.send_keys", + "params": { + "pane_id": "p_1_1", + "keys": ["Enter"] + } +} +``` + +this is kept separate on purpose. sending text is not always the same thing as submitting it. + +a future CLI wrapper will likely offer a more ergonomic `pane run` style command on top of this. + +## example: one-shot wait for output + +```json +{ + "id": "req_wait", + "method": "pane.wait_for_output", + "params": { + "pane_id": "p_1_1", + "source": "recent", + "lines": 200, + "match": { "type": "substring", "value": "ready" }, + "timeout_ms": 30000 + } +} +``` + +regex matching is also supported: + +```json +{ + "type": "regex", + "value": "server.*ready" +} +``` + +## subscriptions + +`events.subscribe` is the long-lived pubsub entrypoint. + +you send a subscribe request once, get an ack on the same connection, and then keep reading newline-delimited JSON events from that same socket. + +### subscription ack + +```json +{ + "id": "sub_1", + "result": { + "type": "subscription_started" + } +} +``` + +## supported subscriptions + +### lifecycle / base events +- `workspace.created` +- `workspace.closed` +- `workspace.focused` +- `pane.created` +- `pane.closed` +- `pane.focused` +- `pane.exited` +- `pane.agent_detected` +- `pane.agent_state_changed` + +### parameterized event +- `pane.output_matched` + +## example: subscribe to lifecycle events + +```json +{ + "id": "sub_life", + "method": "events.subscribe", + "params": { + "subscriptions": [ + { "type": "workspace.created" }, + { "type": "workspace.focused" }, + { "type": "pane.created" }, + { "type": "pane.focused" }, + { "type": "pane.agent_detected" }, + { "type": "pane.closed" }, + { "type": "workspace.closed" } + ] + } +} +``` + +example pushed event: + +```json +{ + "event": "workspace_created", + "data": { + "workspace": { + "workspace_id": "w_1", + "number": 1, + "label": "herdr", + "focused": true, + "pane_count": 1, + "agent_state": "unknown" + } + } +} +``` + +## example: subscribe to output matches and agent state changes + +```json +{ + "id": "sub_1", + "method": "events.subscribe", + "params": { + "subscriptions": [ + { + "type": "pane.output_matched", + "pane_id": "p_1_1", + "source": "recent", + "lines": 200, + "match": { "type": "substring", "value": "ready" } + }, + { + "type": "pane.agent_state_changed", + "pane_id": "p_1_1", + "state": "idle" + } + ] + } +} +``` + +example pushed `pane.output_matched` event: + +```json +{ + "event": "pane.output_matched", + "data": { + "pane_id": "p_1_1", + "matched_line": "server ready", + "read": { + "pane_id": "p_1_1", + "workspace_id": "w_1", + "source": "recent", + "text": "...server ready...", + "revision": 0, + "truncated": false + } + } +} +``` + +example pushed `pane.agent_state_changed` event: + +```json +{ + "event": "pane.agent_state_changed", + "data": { + "pane_id": "p_1_1", + "workspace_id": "w_1", + "state": "idle", + "agent": "pi" + } +} +``` + +## behavior notes + +- `pane.output_matched` emits when a subscription transitions into a matching state. it does not repeatedly spam the same visible match on every poll. +- closing the socket connection ends the subscription. +- there is no separate transport for events. +- the same herdr process can serve regular request/response calls and long-lived subscription connections at the same time. + +## intended layering + +recommended architecture: +- socket api = foundational integration protocol +- future `herdr ...` commands = ergonomic wrapper for humans and coding agents + +for agent workflows, the future CLI should likely expose blocking commands like: +- `herdr pane read ...` +- `herdr pane run ...` +- `herdr wait output ...` +- `herdr wait agent-state ...` + +but those should sit on top of this socket surface rather than replacing it. |
