6.8 KiB
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:
HERDR_SOCKET_PATH$XDG_RUNTIME_DIR/herdr.sock$XDG_CONFIG_HOME/herdr/herdr.sock$HOME/.config/herdr/herdr.sock/tmp/herdr.sock
request shape
all requests use this envelope:
{
"id": "req_1",
"method": "ping",
"params": {}
}
success responses:
{
"id": "req_1",
"result": {
"type": "pong",
"version": "0.1.2"
}
}
error responses:
{
"id": "req_1",
"error": {
"code": "pane_not_found",
"message": "pane 1-99 not found"
}
}
ids
workspace ids look like:
12
pane ids look like:
1-11-22-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.
core request methods
currently useful methods include:
basic
ping
workspace
workspace.listworkspace.getworkspace.createworkspace.focusworkspace.renameworkspace.close
pane
pane.listpane.getpane.readpane.send_textpane.send_keyspane.splitpane.close
waits / events
pane.wait_for_outputevents.subscribe
example: create a workspace
{
"id": "req_create",
"method": "workspace.create",
"params": {
"cwd": "/home/can/Projects/herdr",
"focus": true
}
}
example response:
{
"id": "req_create",
"result": {
"type": "workspace_info",
"workspace": {
"workspace_id": "1",
"number": 1,
"label": "herdr",
"focused": true,
"pane_count": 1,
"agent_state": "unknown"
}
}
}
example: read pane output
{
"id": "req_read",
"method": "pane.read",
"params": {
"pane_id": "1-1",
"source": "recent",
"lines": 80
}
}
source can be:
visiblerecent
example: send text and press enter
low-level input is intentionally explicit:
{
"id": "req_send_text",
"method": "pane.send_text",
"params": {
"pane_id": "1-1",
"text": "bun run dev"
}
}
then:
{
"id": "req_send_keys",
"method": "pane.send_keys",
"params": {
"pane_id": "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
{
"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
}
}
regex matching is also supported:
{
"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
{
"id": "sub_1",
"result": {
"type": "subscription_started"
}
}
supported subscriptions
lifecycle / base events
workspace.createdworkspace.closedworkspace.focusedpane.createdpane.closedpane.focusedpane.exitedpane.agent_detectedpane.agent_state_changed
parameterized event
pane.output_matched
example: subscribe to lifecycle events
{
"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:
{
"event": "workspace_created",
"data": {
"workspace": {
"workspace_id": "1",
"number": 1,
"label": "herdr",
"focused": true,
"pane_count": 1,
"agent_state": "unknown"
}
}
}
example: subscribe to output matches and agent state changes
{
"id": "sub_1",
"method": "events.subscribe",
"params": {
"subscriptions": [
{
"type": "pane.output_matched",
"pane_id": "1-1",
"source": "recent",
"lines": 200,
"match": { "type": "substring", "value": "ready" }
},
{
"type": "pane.agent_state_changed",
"pane_id": "1-1",
"state": "idle"
}
]
}
}
example pushed pane.output_matched event:
{
"event": "pane.output_matched",
"data": {
"pane_id": "1-1",
"matched_line": "server ready",
"read": {
"pane_id": "1-1",
"workspace_id": "1",
"source": "recent",
"text": "...server ready...",
"revision": 0,
"truncated": false
}
}
}
example pushed pane.agent_state_changed event:
{
"event": "pane.agent_state_changed",
"data": {
"pane_id": "1-1",
"workspace_id": "1",
"state": "idle",
"agent": "pi"
}
}
behavior notes
pane.output_matchedemits 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
herdr ...commands = ergonomic wrapper for humans and coding agents
current wrapper commands include:
herdr workspace listherdr 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).