Terminal Automation
Section titled “Terminal Automation”The Terminal Automation API lets you drive TUI programs programmatically. Snapshot the rendered screen, search for regex matches, press keys, paste text, send mouse events, and block on conditions — all without driving an xterm.js frontend in a browser. Every operation is backed by a server-side libvterm parser that mirrors the session’s terminal state, so a snapshot returned by the API reflects exactly what a user would see in the terminal.
Snapshot and Search
Section titled “Snapshot and Search”GET /api/v1/terminal/snapshot
Section titled “GET /api/v1/terminal/snapshot”Returns a rendered snapshot of the terminal screen as seen by a user: the visible text grid (lines), cursor position, window title, fullscreen (alt-screen) state, reverse-video highlight spans, and a monotonic sequence counter. Optionally includes ANSI SGR colored lines. On the first call for a session, the parser is lazily initialized by replaying the session’s output buffer, so the snapshot reflects the full terminal history.
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”{ "terminal_id": 12, "lines": [ "$ npm install", "audited 1247 packages in 3s", "found 0 vulnerabilities", "$ █" ], "colored_lines": null, "cursor": { "row": 3, "col": 3 }, "title": "bash — node-app", "alt_screen": false, "highlights": [ { "row": 1, "start_col": 0, "end_col": 34 } ], "sequence": 4821}{ "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 and cURL
Section titled “SDK and cURL”curl -G "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/snapshot" \ --data-urlencode "terminal_id=12" \ --data-urlencode "include_colors=true"const snapshot = await client.terminal.terminalAutomation.getTerminalSnapshot({ terminal_id: '12', include_colors: true});GET /api/v1/terminal/find
Section titled “GET /api/v1/terminal/find”Searches 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.
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”{ "terminal_id": 12, "total": 3, "truncated": false, "deadline_exceeded": false, "hits": [ { "row": 4, "col": 0, "length": 11, "text": "Error: file" }, { "row": 17, "col": 12, "length": 11, "text": "Error: file" }, { "row": 22, "col": 4, "length": 11, "text": "Error: file" } ]}{ "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"}The deadline_exceeded flag is true when the scan hit the internal 500 ms wall-clock bound (ReDoS-shaped patterns); truncated is true when total >= limit.
SDK and cURL
Section titled “SDK and cURL”curl -G "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/find" \ --data-urlencode "terminal_id=12" \ --data-urlencode "pattern=Error:.*" \ --data-urlencode "scope=screen" \ --data-urlencode "limit=100"const results = await client.terminal.terminalAutomation.findInTerminal({ terminal_id: '12', pattern: 'Error:.*', scope: 'screen', limit: 100});Session State and Metrics
Section titled “Session State and Metrics”GET /api/v1/terminal/{terminal_id}/automation
Section titled “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 — for example, “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”{ "terminal_id": 12, "vterm_active": true, "dimensions": { "rows": 24, "cols": 80 }, "sequence": 4821, "time_since_change_ms": 142, "alt_screen": false, "title": "bash — node-app", "scrollback_lines": 312, "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 and cURL
Section titled “SDK and cURL”curl "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/12/automation"const state = await client.terminal.terminalAutomation.getSessionAutomationState('12');GET /api/v1/terminal/automation/metrics
Section titled “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”{ "active_sessions": 47, "memory_used_mb": 128.4, "memory_cap_mb": 512, "active_waiters": 12, "max_sessions": 256, "max_waiters_per_session": 16}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}SDK and cURL
Section titled “SDK and cURL”curl "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/automation/metrics"const metrics = await client.terminal.terminalAutomation.getAutomationMetrics();These endpoints inject input into a terminal session: keystrokes, pasted text, and cell-based mouse events. All three share the same per-session atomicity contract — input is validated before any of it is sent, so a single invalid key (or out-of-range coordinate) rejects the entire request with no partial writes.
GET /api/v1/terminal/keys
Section titled “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", "tab", "escape", "backspace", "space", "arrow_up", "arrow_down", "arrow_left", "arrow_right", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "ctrl+c", "ctrl+d", "ctrl+z", "ctrl+l", "insert", "delete", "home", "end", "page_up", "page_down", "esc", "cr", "lf", "bs", "del" ]}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}SDK and cURL
Section titled “SDK and cURL”curl "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/keys"const { keys } = await client.terminal.terminalAutomation.listSupportedKeys();POST /api/v1/terminal/press
Section titled “POST /api/v1/terminal/press”Sends 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.
| Field | 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 |
Response
Section titled “Response”{ "terminal_id": 12, "keys_pressed": 3, "bytes_written": 6}{ "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"}For status 405, the body uses session_readonly for read-only (PID-attach) sessions and method_not_allowed (with an Allow: POST header) for wrong methods.
SDK and cURL
Section titled “SDK and cURL”curl -X POST \ "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/press?terminal_id=12" \ -H "Content-Type: application/json" \ -d '{ "keys": ["ctrl+c", "arrow_up", "enter"] }'await client.terminal.terminalAutomation.pressTerminalKeys( { keys: ['ctrl+c', 'arrow_up', 'enter'] }, { terminal_id: '12' });POST /api/v1/terminal/paste
Section titled “POST /api/v1/terminal/paste”Pastes 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”| Field | 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 |
Response
Section titled “Response”{ "terminal_id": 12, "bytes_written": 41, "bracketed_active": true, "esc_neutralized": 0}{ "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"}bracketed_active reflects whether libvterm actually emitted the \e[200~...\e[201~ envelope (requires the running program to have enabled DECSET 2004). esc_neutralized is the count of input CSI-starter codepoints substituted with U+FFFD inside the envelope body — this covers both 7-bit ESC (U+001B) and 8-bit C1 CSI (U+009B), because libvterm can emit the envelope markers in either form depending on whether S8C1T is active. Neutralization prevents an embedded \e[201~ (or \u009b201~) in the paste from ending the frame early and letting the tail run as unwrapped keyboard input. When bracketed=false or the envelope is not in effect, esc_neutralized is always 0.
For status 405, the body uses session_readonly for read-only sessions and method_not_allowed (with an Allow: POST header) for wrong methods.
SDK and cURL
Section titled “SDK and cURL”curl -X POST \ "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/paste?terminal_id=12" \ -H "Content-Type: application/json" \ -d '{ "text": "git status\n", "bracketed": true }'await client.terminal.terminalAutomation.pasteTerminalText( { text: 'git status\n', bracketed: true }, { terminal_id: '12' });POST /api/v1/terminal/mouse
Section titled “POST /api/v1/terminal/mouse”Sends 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”The body must contain exactly one of event (single event) or events (array of 1-256 events).
| Field | Type | Required | Description |
|---|---|---|---|
event | object | No | A single TerminalMouseEvent |
events | array | No | Array of TerminalMouseEvent objects (minItems 1, maxItems 256) |
TerminalMouseEvent
Section titled “TerminalMouseEvent”| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Mouse event kind. One of move, down, up, click, scroll. click expands to down/up; scroll uses wheel buttons |
row | integer | Yes | Zero-based terminal row cell (≥ 0) |
col | integer | Yes | Zero-based terminal column cell (≥ 0) |
button | integer | No | Mouse button. Non-scroll events accept 1-3; scroll accepts 4-5 |
amount | integer | No | Scroll repeat count for scroll events (1-20) |
direction | string | No | Optional scroll direction (up or down); overrides the scroll button |
modifiers | array | No | Keyboard modifiers applied to the mouse event. Each entry is one of shift, alt, meta, ctrl, control (maxItems 8) |
Response
Section titled “Response”{ "terminal_id": 12, "events_processed": 2, "bytes_written": 18}{ "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"}For status 405, the body uses session_readonly for read-only (PID-attach) sessions and method_not_allowed (with an Allow: POST header) for wrong methods.
SDK and cURL
Section titled “SDK and cURL”# Single eventcurl -X POST \ "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/mouse?terminal_id=12" \ -H "Content-Type: application/json" \ -d '{ "event": { "type": "click", "row": 10, "col": 20, "button": 1, "modifiers": ["ctrl"] } }'
# Batch of eventscurl -X POST \ "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/mouse?terminal_id=12" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "type": "down", "row": 10, "col": 20, "button": 1 }, { "type": "up", "row": 10, "col": 20, "button": 1 } ] }'// Single eventawait client.terminal.terminalAutomation.sendTerminalMouseEvents( { event: { type: 'click', row: 10, col: 20, button: 1, modifiers: ['ctrl'] } }, { terminal_id: '12' });
// Batch of eventsawait client.terminal.terminalAutomation.sendTerminalMouseEvents( { events: [ { type: 'down', row: 10, col: 20, button: 1 }, { type: 'up', row: 10, col: 20, button: 1 } ] }, { terminal_id: '12' });POST /api/v1/terminal/wait
Section titled “POST /api/v1/terminal/wait”Blocks until a terminal condition is met, then returns 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”| Field | 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 |
Response
Section titled “Response”{ "terminal_id": 12, "status": "matched", "match": { "row": 8, "col": 4, "length": 7, "text": "ready >" }, "snapshot": { "lines": [ "Initializing...", "Loading config...", "Ready.", "$ ready >█" ], "cursor": { "row": 3, "col": 9 }, "title": "bash — node-app", "alt_screen": false, "sequence": 5012 }}{ "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"}The status field is one of:
matched— regex hit. Response includesmatchandsnapshot.stable— no damage fordebounce_ms. Response includessnapshot.timeout— hittimeout_ms. Response includessnapshot.exited— underlying process died mid-wait. Response includessnapshot.vterm_reinit— the VT parser was torn down and re-initialized mid-wait due to a memory-cap resize. Client should retry. Response includes neithermatchnorsnapshot.
SDK and cURL
Section titled “SDK and cURL”# Stable mode: wait for the screen to settle, then snapshotcurl -X POST \ "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/wait?terminal_id=12" \ -H "Content-Type: application/json" \ -d '{ "mode": "stable", "debounce_ms": 200, "timeout_ms": 10000 }'
# Regex mode: wait until "ready >" appears anywhere on the screencurl -X POST \ "https://myproject-mycontainer-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/wait?terminal_id=12" \ -H "Content-Type: application/json" \ -d '{ "mode": "regex", "pattern": "ready >", "timeout_ms": 15000, "search_scope": "screen" }'// Stable modeconst settled = await client.terminal.terminalAutomation.waitForTerminal( { mode: 'stable', debounce_ms: 200, timeout_ms: 10000 }, { terminal_id: '12' });
// Regex modeconst ready = await client.terminal.terminalAutomation.waitForTerminal( { mode: 'regex', pattern: 'ready >', timeout_ms: 15000, search_scope: 'screen' }, { terminal_id: '12' });