aboutsummaryrefslogtreecommitdiff
path: root/SOCKET_API.md
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-30 15:33:12 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-30 15:33:12 +0300
commitfff21d64905cc50424aec698c88965aefd2df724 (patch)
treefa72dfdc937a522e94aad9de61dc34a1b25eb5e3 /SOCKET_API.md
parent55684493890b4f67c73ea07b5af5911ec1b4d80e (diff)
docs: refresh readme and socket api positioning
Diffstat (limited to 'SOCKET_API.md')
-rw-r--r--SOCKET_API.md564
1 files changed, 447 insertions, 117 deletions
diff --git a/SOCKET_API.md b/SOCKET_API.md
index bccfd3c6..333da511 100644
--- a/SOCKET_API.md
+++ b/SOCKET_API.md
@@ -1,18 +1,29 @@
# 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.
+herdr exposes a local unix socket api for scripts, tools, and coding agents that want to control a running herdr instance or subscribe to events.
-this is the low-level integration surface.
-a CLI wrapper on top of it is planned, but the socket API is the foundation.
+if you are teaching an agent that is already running inside herdr, start with [`SKILL.md`](./SKILL.md). use this document when you want the direct protocol, or when you want the cli wrapper reference for the commands that sit on top of it.
+
+## choose your integration layer
+
+there are three practical ways to integrate with herdr:
+
+- **agent skill** — [`SKILL.md`](./SKILL.md). best when an agent inside herdr just needs to learn the workflow quickly.
+- **cli wrappers** — `herdr workspace ...`, `herdr pane ...`, `herdr wait ...`. best for shell scripts and simple orchestration.
+- **raw socket api** — best when you want direct request/response control or long-lived event subscriptions.
+
+these layers are intentionally stacked on top of the same control surface.
+
+important difference: `pane.run` and `wait agent-state` are **cli conveniences**, not raw socket methods.
## 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
+- encoding: newline-delimited json
+- request/response: send one json request per line, read one json response per line
+- subscriptions: send `events.subscribe`, receive an ack, then keep the same connection open and continue reading pushed events
-socket path resolution:
+socket path resolution order:
1. `HERDR_SOCKET_PATH`
2. `$XDG_RUNTIME_DIR/herdr.sock`
@@ -20,9 +31,9 @@ socket path resolution:
4. `$HOME/.config/herdr/herdr.sock`
5. `/tmp/herdr.sock`
-## request shape
+## request and response envelopes
-all requests use this envelope:
+all socket requests use this envelope:
```json
{
@@ -32,7 +43,7 @@ all requests use this envelope:
}
```
-success responses:
+successful responses look like:
```json
{
@@ -44,7 +55,7 @@ success responses:
}
```
-error responses:
+errors look like:
```json
{
@@ -56,7 +67,7 @@ error responses:
}
```
-## ids
+## ids and numbering
workspace ids look like:
@@ -70,52 +81,128 @@ pane ids look like:
- `2-1`
that means:
+
- first number = current workspace number
- second number = current pane number within that workspace
-these are compact public ids for the current live session. if a workspace or pane is closed, numbering compacts.
+these are compact public ids for the current live session. they are **not durable database ids**. if a workspace or pane closes, higher numbers compact down.
-## core request methods
+## core objects
-currently useful methods include:
+`workspace_info` responses contain objects like:
-### basic
-- `ping`
+```json
+{
+ "workspace_id": "1",
+ "number": 1,
+ "label": "herdr",
+ "focused": true,
+ "pane_count": 1,
+ "agent_state": "unknown"
+}
+```
-### workspace
-- `workspace.list`
-- `workspace.get`
-- `workspace.create`
-- `workspace.focus`
-- `workspace.rename`
-- `workspace.close`
+`pane_info` responses contain objects like:
-### pane
-- `pane.list`
-- `pane.get`
-- `pane.read`
-- `pane.send_text`
-- `pane.send_keys`
-- `pane.split`
-- `pane.close`
+```json
+{
+ "pane_id": "1-1",
+ "workspace_id": "1",
+ "focused": true,
+ "cwd": "/home/can/Projects/herdr",
+ "agent": "pi",
+ "agent_state": "working",
+ "revision": 0
+}
+```
-### waits / events
-- `pane.wait_for_output`
-- `events.subscribe`
+`pane_read` responses contain objects like:
-## example: create a workspace
+```json
+{
+ "pane_id": "1-1",
+ "workspace_id": "1",
+ "source": "recent",
+ "text": "...",
+ "revision": 0,
+ "truncated": false
+}
+```
+
+agent states are:
+
+- `idle`
+- `working`
+- `blocked`
+- `unknown`
+
+## methods at a glance
+
+| method | purpose | success result type |
+|---|---|---|
+| `ping` | health check / version | `pong` |
+| `workspace.list` | list workspaces | `workspace_list` |
+| `workspace.get` | inspect one workspace | `workspace_info` |
+| `workspace.create` | create a workspace | `workspace_info` |
+| `workspace.focus` | focus a workspace | `workspace_info` |
+| `workspace.rename` | rename a workspace | `workspace_info` |
+| `workspace.close` | close a workspace | `ok` |
+| `pane.list` | list panes, optionally filtered by workspace | `pane_list` |
+| `pane.get` | inspect one pane | `pane_info` |
+| `pane.read` | read pane output | `pane_read` |
+| `pane.split` | split a pane and create a sibling pane | `pane_info` |
+| `pane.send_text` | send literal text without Enter | `ok` |
+| `pane.send_keys` | send keypresses like `Enter` | `ok` |
+| `pane.close` | close a pane | `ok` |
+| `pane.wait_for_output` | one-shot blocking wait for text | `output_matched` |
+| `events.subscribe` | start a long-lived subscription stream | `subscription_started` ack |
+
+## workspace methods
+
+### `workspace.list`
+
+request:
```json
{
- "id": "req_create",
- "method": "workspace.create",
- "params": {
- "cwd": "/home/can/Projects/herdr",
- "focus": true
- }
+ "id": "req_list",
+ "method": "workspace.list",
+ "params": {}
+}
+```
+
+returns `workspace_list` with zero or more workspace objects.
+
+### `workspace.get`
+
+params:
+
+```json
+{
+ "workspace_id": "1"
+}
+```
+
+returns `workspace_info` for one workspace.
+
+### `workspace.create`
+
+params:
+
+```json
+{
+ "cwd": "/home/can/Projects/herdr",
+ "focus": true
}
```
+notes:
+
+- `cwd` is optional
+- if `cwd` is omitted, herdr uses its current working directory and falls back to `/` if needed
+- `focus` is optional in raw socket requests and defaults to `false`
+- the cli wrapper is more ergonomic here: `herdr workspace create` focuses by default unless you pass `--no-focus`
+
example response:
```json
@@ -135,78 +222,252 @@ example response:
}
```
-## example: read pane output
+### `workspace.focus`
+
+params:
```json
{
- "id": "req_read",
- "method": "pane.read",
- "params": {
- "pane_id": "1-1",
- "source": "recent",
- "lines": 80
- }
+ "workspace_id": "1"
}
```
-`source` can be:
-- `visible`
-- `recent`
+returns the focused workspace as `workspace_info`.
-## example: send text and press enter
+### `workspace.rename`
-low-level input is intentionally explicit:
+params:
```json
{
- "id": "req_send_text",
- "method": "pane.send_text",
- "params": {
- "pane_id": "1-1",
- "text": "bun run dev"
+ "workspace_id": "1",
+ "label": "api"
+}
+```
+
+returns updated `workspace_info`.
+
+### `workspace.close`
+
+params:
+
+```json
+{
+ "workspace_id": "1"
+}
+```
+
+returns:
+
+```json
+{
+ "id": "req_close",
+ "result": {
+ "type": "ok"
}
}
```
-then:
+## pane methods
+
+### `pane.list`
+
+request with no filter:
```json
{
- "id": "req_send_keys",
- "method": "pane.send_keys",
+ "id": "req_panes",
+ "method": "pane.list",
+ "params": {}
+}
+```
+
+request filtered to one workspace:
+
+```json
+{
+ "id": "req_panes_ws",
+ "method": "pane.list",
"params": {
- "pane_id": "1-1",
- "keys": ["Enter"]
+ "workspace_id": "1"
}
}
```
-this is kept separate on purpose. sending text is not always the same thing as submitting it.
+returns `pane_list`.
-a future CLI wrapper will likely offer a more ergonomic `pane run` style command on top of this.
+### `pane.get`
-## example: one-shot wait for output
+params:
```json
{
- "id": "req_wait",
- "method": "pane.wait_for_output",
- "params": {
- "pane_id": "1-1",
- "source": "recent",
- "lines": 200,
- "match": { "type": "substring", "value": "ready" },
- "timeout_ms": 30000
+ "pane_id": "1-1"
+}
+```
+
+returns `pane_info`.
+
+### `pane.read`
+
+params:
+
+```json
+{
+ "pane_id": "1-1",
+ "source": "recent",
+ "lines": 80,
+ "strip_ansi": true
+}
+```
+
+notes:
+
+- `source` is required and must be `visible` or `recent`
+- `lines` is optional
+- current implementation defaults to `80` lines when `lines` is omitted and caps reads at `1000`
+- `strip_ansi` defaults to `true`
+
+`source` meanings:
+
+- `visible` — current viewport
+- `recent` — recent scrollback text
+
+example response:
+
+```json
+{
+ "id": "req_read",
+ "result": {
+ "type": "pane_read",
+ "read": {
+ "pane_id": "1-1",
+ "workspace_id": "1",
+ "source": "recent",
+ "text": "...",
+ "revision": 0,
+ "truncated": false
+ }
}
}
```
-regex matching is also supported:
+### `pane.split`
+
+params:
```json
{
- "type": "regex",
- "value": "server.*ready"
+ "target_pane_id": "1-1",
+ "direction": "right",
+ "focus": true
+}
+```
+
+notes:
+
+- `direction` must be `right` or `down`
+- `cwd` is optional
+- `focus` is optional in raw socket requests and defaults to `false`
+- the cli wrapper is more ergonomic here too: `herdr pane split ...` focuses by default unless you pass `--no-focus`
+
+returns `pane_info` for the new pane.
+
+### `pane.send_text`
+
+params:
+
+```json
+{
+ "pane_id": "1-1",
+ "text": "bun run dev"
+}
+```
+
+this sends literal text only. it does **not** press Enter.
+
+### `pane.send_keys`
+
+params:
+
+```json
+{
+ "pane_id": "1-1",
+ "keys": ["Enter"]
+}
+```
+
+use this after `pane.send_text` when you want to submit a command.
+
+### `pane.close`
+
+params:
+
+```json
+{
+ "pane_id": "1-2"
+}
+```
+
+returns `ok`.
+
+## waits
+
+### `pane.wait_for_output`
+
+this is the direct socket-side one-shot blocking wait.
+
+params:
+
+```json
+{
+ "pane_id": "1-1",
+ "source": "recent",
+ "lines": 200,
+ "match": { "type": "substring", "value": "ready" },
+ "timeout_ms": 30000,
+ "strip_ansi": true
+}
+```
+
+matcher forms:
+
+```json
+{ "type": "substring", "value": "ready" }
+```
+
+```json
+{ "type": "regex", "value": "server.*ready" }
+```
+
+notes:
+
+- `source` must be `visible` or `recent`
+- `lines` is optional
+- `timeout_ms` is optional
+- `strip_ansi` defaults to `true`
+- on success you get `output_matched`
+- on timeout you get an error response with code `timeout`
+
+example success response:
+
+```json
+{
+ "id": "req_wait",
+ "result": {
+ "type": "output_matched",
+ "pane_id": "1-1",
+ "revision": 0,
+ "matched_line": "server ready",
+ "read": {
+ "pane_id": "1-1",
+ "workspace_id": "1",
+ "source": "recent",
+ "text": "...server ready...",
+ "revision": 0,
+ "truncated": false
+ }
+ }
}
```
@@ -214,7 +475,7 @@ regex matching is also supported:
`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.
+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
@@ -227,9 +488,10 @@ you send a subscribe request once, get an ack on the same connection, and then k
}
```
-## supported subscriptions
+### supported subscriptions
+
+base lifecycle subscriptions:
-### lifecycle / base events
- `workspace.created`
- `workspace.closed`
- `workspace.focused`
@@ -238,12 +500,28 @@ you send a subscribe request once, get an ack on the same connection, and then k
- `pane.focused`
- `pane.exited`
- `pane.agent_detected`
-- `pane.agent_state_changed`
-### parameterized event
+parameterized subscriptions:
+
- `pane.output_matched`
+- `pane.agent_state_changed`
-## example: subscribe to lifecycle events
+### event naming rule
+
+this part matters because the pushed event names are **not all shaped the same**.
+
+- when you subscribe to a **base lifecycle event**, the pushed `event` value uses snake_case with underscores:
+ - subscribe with `workspace.created`
+ - receive `workspace_created`
+- when you subscribe to a **parameterized subscription**, the pushed `event` value keeps the dotted name:
+ - subscribe with `pane.output_matched`
+ - receive `pane.output_matched`
+
+examples below show both forms.
+
+### example: subscribe to lifecycle events
+
+request:
```json
{
@@ -281,7 +559,9 @@ example pushed event:
}
```
-## example: subscribe to output matches and agent state changes
+### example: subscribe to output matches and agent state changes
+
+request:
```json
{
@@ -306,6 +586,11 @@ example pushed event:
}
```
+notes:
+
+- `pane.output_matched` supports `source`, optional `lines`, matcher config, and optional `strip_ansi`
+- `pane.agent_state_changed` accepts an optional `state` filter; if omitted, any state transition for that pane can match
+
example pushed `pane.output_matched` event:
```json
@@ -340,37 +625,82 @@ example pushed `pane.agent_state_changed` event:
}
```
-## behavior notes
+## cli wrappers
+
+these commands talk to the same local socket surface and are usually the easiest starting point for shell scripts and coding agents.
+
+### command groups
+
+workspace commands:
+
+```text
+herdr workspace list
+herdr workspace create [--cwd PATH] [--no-focus]
+herdr workspace get <workspace_id>
+herdr workspace focus <workspace_id>
+herdr workspace rename <workspace_id> <label>
+herdr workspace close <workspace_id>
+```
+
+pane commands:
+
+```text
+herdr pane list [--workspace <workspace_id>]
+herdr pane get <pane_id>
+herdr pane read <pane_id> [--source visible|recent] [--lines N] [--raw]
+herdr pane split <pane_id> --direction right|down [--cwd PATH] [--no-focus]
+herdr pane close <pane_id>
+herdr pane send-text <pane_id> <text>
+herdr pane send-keys <pane_id> <key> [key ...]
+herdr pane run <pane_id> <command>
+```
+
+wait commands:
+
+```text
+herdr wait output <pane_id> --match <text> [--source visible|recent] [--lines N] [--timeout MS] [--regex] [--raw]
+herdr wait agent-state <pane_id> --state <idle|working|blocked|unknown> [--timeout MS]
+```
+
+### cli 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.
+- `workspace create` focuses by default; pass `--no-focus` to keep focus where it is
+- `pane split` focuses the new pane by default; pass `--no-focus` to keep focus on the original pane
+- `pane read` prints **text**, not json
+- `pane send-text`, `pane send-keys`, and `pane run` print nothing on success
+- list/get/create/split/wait commands print json on success
+- `pane run` is a convenience wrapper for `pane send-text` + `pane send-keys Enter`
+- `wait agent-state` is a cli convenience built on top of event subscriptions
+- `--raw` disables ansi stripping for `pane read` and `wait output`
+
+### cli examples
+
+create a workspace, split a pane, run a server, and wait for readiness:
+
+```bash
+herdr workspace create --cwd /path/to/project
+herdr pane split 1-1 --direction right --no-focus
+herdr pane run 1-2 "npm run dev"
+herdr wait output 1-2 --match "ready" --timeout 30000
+```
+
+wait for another agent to finish:
+
+```bash
+herdr wait agent-state 1-1 --state idle --timeout 60000
+```
+
+inspect another pane's output:
+
+```bash
+herdr pane read 1-1 --source recent --lines 80
+```
+
+## behavior notes and gotchas
+
+- `pane.send_text` sends literal text only. if you want to execute a command, follow it with `pane.send_keys` and `Enter`, or use cli `pane run`.
+- `pane.read` and `pane.wait_for_output` strip ansi by default.
+- `pane.output_matched` subscriptions fire on transitions into a matching state; they do not repeatedly spam the same still-visible match on every poll.
- closing the socket connection ends the subscription.
-- there is no separate transport for events.
+- there is no separate event transport.
- 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
-- `herdr ...` commands = ergonomic wrapper for humans and coding agents
-
-current wrapper commands include:
-- `herdr workspace list`
-- `herdr workspace create ...`
-- `herdr workspace get ...`
-- `herdr workspace focus ...`
-- `herdr workspace rename ...`
-- `herdr workspace close ...`
-- `herdr pane list ...`
-- `herdr pane get ...`
-- `herdr pane read ...`
-- `herdr pane split ...`
-- `herdr pane close ...`
-- `herdr pane send-text ...`
-- `herdr pane send-keys ...`
-- `herdr pane run ...`
-- `herdr wait output ...`
-- `herdr wait agent-state ...`
-
-those commands sit on top of this socket surface rather than replacing it.
-
-for convenience, the CLI accepts pane ids in either raw socket form (`1-1`) or short human form (`1-1`).