Terminal Automation
Section titled “Terminal Automation”Drive TUI apps programmatically — snapshot the rendered screen, search with regex, send named key presses, paste text, inject cell-based mouse events, and block until a condition resolves. All write operations are routed through a server-side VT parser (libvterm) that mirrors the browser’s xterm.js state, so byte sequences respect application-cursor mode (DECCKM), keypad mode (DECKPAM), bracketed-paste mode (DECSET 2004), and active mouse-reporting protocols. Input endpoints are validated all-or-nothing: a single invalid event or key rejects the entire request with no partial writes.
Observability
Section titled “Observability”Get rendered terminal snapshot
Section titled “Get rendered terminal snapshot”GET /api/v1/terminal/snapshot
Returns a rendered snapshot of the terminal screen as seen by a user. The response includes the visible text grid (lines), cursor position, window title, alt-screen state, reverse-video highlight spans, and a monotonic sequence counter. Optionally returns ANSI SGR colored lines. The snapshot is built from a server-side VT parser (libvterm) that mirrors the browser’s xterm.js state; on first call for a session the parser is lazily initialized by replaying the session’s output buffer.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
terminal_id | query | string | Yes | Terminal session ID (numeric 1-65535) |
include_colors | query | boolean | No | Include ANSI SGR colored_lines array alongside plain text lines. Default: false |
include_highlights | query | boolean | No | Include reverse-video highlight spans. Default: true |
scroll_offset | query | integer | No | Lines into scrollback (0 = live viewport). Default: 0 |
Response
Section titled “Response”{ "lines": [ "$ npm install", "", "added 247 packages in 12s", "", "user@host:~$ " ], "cursor": { "row": 4, "col": 11 }, "title": "user@host: ~", "alt_screen": false, "highlights": [ { "row": 2, "col_start": 0, "col_end": 23 } ], "sequence": 42, "rows": 24, "cols": 80, "colored_lines": null}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid parameters"}{ "statusCode": 404, "error": "Not Found", "message": "Session not found"}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}{ "statusCode": 503, "error": "Service Unavailable", "message": "VTerm memory cap exceeded"}SDK usage
Section titled “SDK usage”const snap = await client.terminal.terminalAutomation.getTerminalSnapshot({ terminal_id: "42", include_colors: true,});Search terminal screen with regex
Section titled “Search terminal screen with regex”GET /api/v1/terminal/find
Search the rendered terminal screen (or scrollback) for a PCRE2 regular expression pattern. Returns cell-coordinate hits with matched text. Supports case-insensitive matching, result limits, and scope selection (screen, scrollback, or all). Pattern length is capped at 1024 bytes. Match limits are enforced to prevent ReDoS — the scan honors an internal 500 ms wall-clock bound and reports deadline_exceeded if hit.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
terminal_id | query | string | Yes | Terminal session ID |
pattern | query | string | Yes | PCRE2 regex pattern to search for (max 1024 bytes) |
scope | query | string | No | Search scope: screen (default), scrollback, or all |
limit | query | integer | No | Maximum number of hits to return (default 100, max 1000) |
case_insensitive | query | boolean | No | Case-insensitive matching. Default: false |
scroll_offset | query | integer | No | Scrollback offset for screen scope (0 = live viewport). Default: 0 |
Response
Section titled “Response”{ "matches": [ { "row": 0, "col": 2, "text": "npm install" }, { "row": 2, "col": 6, "text": "added" } ], "total": 2, "truncated": false, "deadline_exceeded": false, "scope": "screen"}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid parameters or regex"}{ "statusCode": 404, "error": "Not Found", "message": "Session not found"}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}{ "statusCode": 503, "error": "Service Unavailable", "message": "VTerm memory cap exceeded"}SDK usage
Section titled “SDK usage”const hits = await client.terminal.terminalAutomation.findInTerminal({ terminal_id: "42", pattern: "error|warning", scope: "all", limit: 50,});Get per-session automation state
Section titled “Get per-session automation state”GET /api/v1/terminal/{terminal_id}/automation
Returns the VT parser state for a specific session: whether vterm is active, dimensions, update sequence counter, time since last screen change, alt-screen flag, title, scrollback length, and active waiter count. Useful for debugging automation workflows (“why did my wait timeout? did the screen actually change?”).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
terminal_id | path | string | Yes | Terminal session ID |
Response
Section titled “Response”{ "vterm_active": true, "rows": 24, "cols": 80, "sequence": 42, "ms_since_change": 1500, "alt_screen": false, "title": "user@host: ~", "scrollback_len": 1000, "active_waiters": 1}{ "statusCode": 400, "error": "Bad Request", "message": "Malformed terminal_id in the URL path (not numeric 1-65535)."}{ "statusCode": 404, "error": "Not Found", "message": "Session not found"}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}SDK usage
Section titled “SDK usage”const state = await client.terminal.terminalAutomation.getSessionAutomationState({ terminal_id: "42",});Get terminal automation metrics
Section titled “Get terminal automation metrics”GET /api/v1/terminal/automation/metrics
Returns global metrics for the server-side VT parser: active vterm session count, memory used/cap in MB, total active wait-waiters across all sessions, and configured limits. Use to monitor resource usage, tune --vterm-memory-cap-mb, and detect leaks.
This endpoint takes no parameters.
Response
Section titled “Response”{ "vterm_sessions": 3, "vterm_memory_used_mb": 12.4, "vterm_memory_cap_mb": 256, "active_waiters": 2, "limits": { "max_waiters_per_session": 16, "max_body_size": 8388608 }}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}SDK usage
Section titled “SDK usage”const metrics = await client.terminal.terminalAutomation.getAutomationMetrics();List supported key names
Section titled “List supported key names”GET /api/v1/terminal/keys
Returns the full list of key names accepted by /api/v1/terminal/press, including aliases and canonical forms. Useful for client-side validation and discoverability. Single printable characters (a-z, 0-9, punctuation) are also accepted but not listed individually.
This endpoint takes no parameters.
Response
Section titled “Response”{ "keys": [ "enter", "return", "tab", "escape", "esc", "backspace", "bs", "space", "up", "down", "left", "right", "home", "end", "page_up", "page_down", "insert", "delete", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "ctrl+a", "ctrl+b", "ctrl+c", "ctrl+d", "ctrl+e", "ctrl+k", "ctrl+l", "ctrl+r", "ctrl+u", "ctrl+w", "ctrl+z" ]}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}SDK usage
Section titled “SDK usage”const { keys } = await client.terminal.terminalAutomation.listSupportedKeys();Send named key presses
Section titled “Send named key presses”POST /api/v1/terminal/press
Send one or more named key presses to a terminal session. Keys are encoded through libvterm’s keyboard API which respects the terminal’s current application-cursor mode (DECCKM) and keypad mode (DECKPAM), ensuring correct byte sequences for programs like vim, htop, and tmux. Supports letters, ctrl+letter, arrow keys, function keys, enter, tab, escape, backspace, and more. All keys are validated before any are sent — a single unknown key rejects the entire request with no partial writes.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
terminal_id | query | string | Yes | Terminal session ID |
Request Body
Section titled “Request Body”Exactly one of key or keys must be supplied. Use key for a single one-shot press; use keys for a sequence (max 256 entries per request).
| Name | Type | Required | Description |
|---|---|---|---|
key | string | No | Single key name for one-shot press (e.g. enter). Mutually exclusive with keys |
keys | array | No | Array of key names to press in sequence (e.g. ["ctrl+c", "arrow_up", "enter"]). Mutually exclusive with key. Maximum 256 entries per request |
{ "keys": ["ctrl+c", "arrow_up", "enter"]}Response
Section titled “Response”{ "sent": 3, "keys": ["ctrl+c", "arrow_up", "enter"]}{ "statusCode": 400, "error": "Bad Request", "message": "Unknown key name or invalid request"}{ "statusCode": 404, "error": "Not Found", "message": "Session not found"}{ "statusCode": 405, "error": "Method Not Allowed", "message": "session_readonly"}{ "statusCode": 413, "error": "Payload Too Large", "message": "Request body exceeds --max-body-size cap (default 8 MB)."}{ "statusCode": 500, "error": "Internal Server Error", "message": "Write to the session's PTY or socket failed, OR the per-request 1 MiB drain cap was hit mid-sequence."}{ "statusCode": 503, "error": "Service Unavailable", "message": "VTerm memory cap exceeded"}SDK usage
Section titled “SDK usage”await client.terminal.terminalAutomation.pressTerminalKeys({ terminal_id: "42", data: { keys: ["ctrl+c", "arrow_up", "enter"] },});Paste text
Section titled “Paste text”POST /api/v1/terminal/paste
Paste text into a terminal session with optional bracketed paste mode. When bracketed=true (default), the text is wrapped in bracketed paste escape sequences if the running program has enabled DECSET 2004 (e.g., vim, zsh). This prevents auto-indent mangling and other paste artifacts. When bracketed=false, the text is sent as raw keystrokes. UTF-8 text including emoji and CJK is fully supported.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
terminal_id | query | string | Yes | Terminal session ID |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Text to paste (UTF-8) |
bracketed | boolean | No | Use bracketed paste mode if the program supports it. Default: true |
{ "text": "git status && git diff --stat", "bracketed": true}Response
Section titled “Response”{ "sent": true, "bracketed_active": true, "esc_neutralized": 0, "bytes": 28}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid request"}{ "statusCode": 404, "error": "Not Found", "message": "Session not found"}{ "statusCode": 405, "error": "Method Not Allowed", "message": "session_readonly"}{ "statusCode": 413, "error": "Payload Too Large", "message": "Request body exceeds --max-body-size cap (default 8 MB)."}{ "statusCode": 500, "error": "Internal Server Error", "message": "Write to the session's PTY or socket failed, OR the per-request 1 MiB paste drain cap was hit."}{ "statusCode": 503, "error": "Service Unavailable", "message": "VTerm memory cap exceeded"}SDK usage
Section titled “SDK usage”await client.terminal.terminalAutomation.pasteTerminalText({ terminal_id: "42", data: { text: "git status && git diff --stat", bracketed: true },});Send cell-based mouse events
Section titled “Send cell-based mouse events”POST /api/v1/terminal/mouse
Send deterministic mouse events to a terminal session using libvterm’s mouse API. Coordinates are zero-based terminal cells, not pixels. Mouse protocol output is emitted only when the target program has enabled terminal mouse reporting. Events are validated before any are sent, matching the all-or-nothing contract of /press.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
terminal_id | query | string | Yes | Terminal session ID |
Request Body
Section titled “Request Body”Supply exactly one of event (single event) or events (array of 1-256 events). Each element is a TerminalMouseEvent object.
| Name | Type | Required | Description |
|---|---|---|---|
event | object | No | Single TerminalMouseEvent. Mutually exclusive with events |
events | array | No | Array of TerminalMouseEvent objects. Mutually exclusive with event. Min 1, max 256 |
TerminalMouseEvent fields:
| Name | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Mouse event kind: move, down, up, click, or scroll. click expands to down/up; scroll uses wheel buttons |
row | integer | Yes | Zero-based terminal row cell (must be zero or positive) |
col | integer | Yes | Zero-based terminal column cell (must be zero or positive) |
button | integer | No | Mouse button (1-5). Non-scroll events accept 1-3; scroll accepts 4-5 |
amount | integer | No | Scroll repeat count for scroll events (1-20) |
direction | string | No | Scroll direction: up or down. Overrides the scroll button |
modifiers | array | No | Keyboard modifiers: shift, alt, meta, ctrl, or control. Max 8 items |
{ "events": [ { "type": "move", "row": 10, "col": 25 }, { "type": "down", "row": 10, "col": 25, "button": 1 }, { "type": "click", "row": 10, "col": 25, "button": 1, "modifiers": ["ctrl"] }, { "type": "up", "row": 10, "col": 25, "button": 1 } ]}Response
Section titled “Response”{ "sent": 4, "terminal_id": "42"}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid request"}{ "statusCode": 404, "error": "Not Found", "message": "Session not found"}{ "statusCode": 405, "error": "Method Not Allowed", "message": "session_readonly"}{ "statusCode": 413, "error": "Payload Too Large", "message": "Request body exceeds --max-body-size cap (default 8 MB)."}{ "statusCode": 500, "error": "Internal Server Error", "message": "Write to the session's PTY or socket failed, OR the per-request 1 MiB drain cap was hit."}{ "statusCode": 503, "error": "Service Unavailable", "message": "VTerm memory cap exceeded"}SDK usage
Section titled “SDK usage”// Single event formawait client.terminal.terminalAutomation.sendTerminalMouseEvents({ terminal_id: "42", data: { event: { type: "click", row: 10, col: 25, button: 1 } },});
// Multiple events formawait client.terminal.terminalAutomation.sendTerminalMouseEvents({ terminal_id: "42", data: { events: [ { type: "move", row: 10, col: 25 }, { type: "click", row: 10, col: 25, button: 1 }, ], },});Wait for terminal condition
Section titled “Wait for terminal condition”POST /api/v1/terminal/wait
Block until a terminal condition is met, then return an atomic snapshot of the screen at the moment of resolution. Supports three modes: stable (no screen updates for debounce_ms), regex (PCRE2 pattern matches on screen), or either (first condition wins). The response includes a full snapshot for the matched/stable/timeout/exited terminal statuses so clients avoid a TOCTOU race between wait and a follow-up /snapshot call; the vterm_reinit status is the lone exception — it fires when the VT parser was torn down mid-wait and no coherent snapshot can be captured (client should retry). Maximum 16 concurrent waiters per session.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
terminal_id | query | string | Yes | Terminal session ID |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
mode | string | No | Wait mode: stable, regex, or either. Default: stable |
debounce_ms | integer | No | Stable mode debounce in milliseconds (10-60000). Default: 100 |
pattern | string | No | PCRE2 regex pattern (required for regex/either modes, max 1024 bytes) |
timeout_ms | integer | No | Hard deadline in milliseconds (10-300000). Default: 5000 |
search_scope | string | No | Where to search: screen, scrollback, or all. Default: screen |
include_colors | boolean | No | Include colored_lines in response snapshot. Default: false |
include_highlights | boolean | No | Include highlights in response snapshot. Default: true |
{ "mode": "either", "pattern": "Password:|\\$ ", "timeout_ms": 30000, "search_scope": "screen"}Response
Section titled “Response”The status field is one of: matched (regex hit — includes match and snapshot), stable (no damage for debounce_ms — includes snapshot), timeout (hit timeout_ms — includes snapshot), exited (underlying process died mid-wait — includes snapshot), or vterm_reinit (the VT parser was torn down and re-initialized mid-wait due to a memory-cap resize — client should retry, no match or snapshot).
{ "status": "matched", "match": { "row": 0, "col": 6, "text": "Welcome" }, "snapshot": { "lines": [ "Welcome to hoody", "user@host:~$ " ], "cursor": { "row": 1, "col": 11 }, "title": "user@host: ~", "alt_screen": false, "highlights": [], "sequence": 17, "rows": 24, "cols": 80 }}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid parameters or regex"}{ "statusCode": 404, "error": "Not Found", "message": "Session not found"}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}{ "statusCode": 413, "error": "Payload Too Large", "message": "Request body exceeds --max-body-size cap (default 8 MB)."}{ "statusCode": 429, "error": "Too Many Requests", "message": "Too many concurrent waiters"}{ "statusCode": 500, "error": "Internal Server Error", "message": "Waiter could not be created (OOM)."}{ "statusCode": 503, "error": "Service Unavailable", "message": "VTerm memory cap exceeded"}SDK usage
Section titled “SDK usage”const result = await client.terminal.terminalAutomation.waitForTerminal({ terminal_id: "42", data: { mode: "either", pattern: "Password:|\\$ ", timeout_ms: 30000 },});
if (result.status === "matched") { console.log("matched at", result.match);} else if (result.status === "stable") { console.log("screen settled:", result.snapshot.lines);} else if (result.status === "vterm_reinit") { // retry the wait}