Terminal: Automation
Section titled “Terminal: Automation”Drive TUI applications programmatically: snapshot the rendered screen, search with regex, send named key presses, paste bracketed text, inject cell-based mouse events, and block until a screen condition is met. All endpoints read from and write to a server-side libvterm parser that mirrors the browser’s xterm.js state, so what automation sees matches what a connected user sees. Use these endpoints to script CLI tools, drive installers headlessly, or build agent-style workflows that observe and react to terminal output.
Inspection
Section titled “Inspection”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 response includes the visible text grid (lines array), 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.
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 |
curl -G \ https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/snapshot \ -H "Authorization: Bearer <token>" \ --data-urlencode "terminal_id=1" \ --data-urlencode "include_colors=true"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalAutomation.getTerminalSnapshot({ terminal_id: 1, include_colors: true });{ "terminal_id": 1, "seq": 142, "cols": 80, "rows": 24, "cursor": { "row": 5, "col": 12 }, "title": "vim test.txt", "alt_screen": false, "lines": [ " 1 #!/usr/bin/env bash", " 2 set -euo pipefail", " 3 echo \"hello, world\"", " 4 ", "~", "~", "~", ":w" ], "highlights": [ { "row": 0, "col_start": 0, "col_end": 2 } ]}{ "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"}GET /api/v1/terminal/find
Section titled “GET /api/v1/terminal/find”Search the rendered terminal screen (or scrollback) for a PCRE2 regular expression. Returns cell-coordinate hits with matched text. Supports case-insensitive matching, result limits, and scope selection. Pattern length is capped at 1024 bytes; the scan honors a 500 ms wall-clock bound 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 |
curl -G \ https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/find \ -H "Authorization: Bearer <token>" \ --data-urlencode "terminal_id=1" \ --data-urlencode "pattern=error|warning" \ --data-urlencode "scope=all" \ --data-urlencode "case_insensitive=true"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalAutomation.findInTerminal({ terminal_id: 1, pattern: 'error|warning', scope: 'all', case_insensitive: true });{ "terminal_id": 1, "pattern": "error|warning", "scope": "all", "total": 2, "truncated": false, "deadline_exceeded": false, "hits": [ { "row": 12, "col": 4, "match": "error", "context": "segmentation error at 0x4f" }, { "row": 47, "col": 8, "match": "warning", "context": "warning: deprecated API" } ]}{ "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"}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 the last screen change, alt-screen flag, title, scrollback length, and active waiter count. Useful for debugging automation workflows — for example, answering “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 |
curl -X GET \ https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/1/automation \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalAutomation.getSessionAutomationState(1);{ "terminal_id": 1, "vterm_active": true, "cols": 80, "rows": 24, "seq": 142, "ms_since_change": 45, "alt_screen": false, "title": "bash — bash", "scrollback_len": 320, "active_waiters": 0}{ "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"}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 and cap in MB, total active 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.
curl -X GET \ https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/automation/metrics \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalAutomation.getAutomationMetrics();{ "active_sessions": 12, "memory_used_mb": 18.4, "memory_cap_mb": 256, "active_waiters": 3, "max_waiters_per_session": 16, "max_body_size_bytes": 8388608}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}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.
curl -X GET \ https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/keys \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalAutomation.listSupportedKeys();{ "keys": [ "enter", "tab", "escape", "backspace", "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+f", "ctrl+g", "ctrl+h", "ctrl+i", "ctrl+j", "ctrl+k", "ctrl+l", "ctrl+m", "ctrl+n", "ctrl+o", "ctrl+p", "ctrl+q", "ctrl+r", "ctrl+s", "ctrl+t", "ctrl+u", "ctrl+v", "ctrl+w", "ctrl+x", "ctrl+y", "ctrl+z", "return", "cr", "esc", "bs", "del", "ins", "arrow_up", "arrow_down", "arrow_left", "arrow_right", "pageup", "pagedown" ]}{ "statusCode": 405, "error": "Method Not Allowed", "message": "method_not_allowed"}POST /api/v1/terminal/press
Section titled “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), producing 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”Provide exactly one of keys (array) or key (single string). The fields are mutually exclusive.
| Field | Type | Required | Description |
|---|---|---|---|
keys | array of strings | No | Array of key names to press in sequence (for example ["ctrl+c", "arrow_up", "enter"]). Mutually exclusive with key. Maximum 256 entries per request. |
key | string | No | Single key name for one-shot press (for example "enter"). Mutually exclusive with keys. |
curl -X POST \ https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/press?terminal_id=1 \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "keys": ["ctrl+c", "arrow_up", "enter"] }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalAutomation.pressTerminalKeys({ keys: ['ctrl+c', 'arrow_up', 'enter'] }, { terminal_id: 1 });{ "terminal_id": 1, "sent": 3}{ "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"}POST /api/v1/terminal/paste
Section titled “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 (for example 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 |
curl -X POST \ https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/paste?terminal_id=1 \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "text": "git commit -m \"fix: handle empty input edge case\"\n", "bracketed": true }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalAutomation.pasteTerminalText({ text: 'git commit -m "fix: handle empty input edge case"\n', bracketed: true }, { terminal_id: 1 });{ "terminal_id": 1, "bytes_sent": 51, "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"}POST /api/v1/terminal/mouse
Section titled “POST /api/v1/terminal/mouse”Send deterministic mouse events to a terminal session using libvterm’s mouse API. 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”Provide exactly one of event (single object) or events (array of 1 to 256 objects). The fields are mutually exclusive.
| Field | Type | Required | Description |
|---|---|---|---|
event | TerminalMouseEvent | No | A single mouse event to send. Mutually exclusive with events. |
events | array of TerminalMouseEvent | No | A batch of mouse events (1 to 256 items). Mutually exclusive with event. |
TerminalMouseEvent fields
Section titled “TerminalMouseEvent fields”| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Mouse event kind: move, down, up, click, or scroll. click expands to a down/up pair; scroll uses wheel buttons. |
row | integer >= 0 | Yes | Zero-based terminal row cell. |
col | integer >= 0 | Yes | Zero-based terminal column cell. |
button | integer 1-5 | No | Mouse button. Non-scroll events accept 1-3; scroll accepts 4-5. |
amount | integer 1-20 | No | Scroll repeat count for scroll events. |
direction | string | No | Optional scroll direction (up or down); overrides the scroll button. Scroll only. |
modifiers | array of strings | No | Keyboard modifiers applied to the mouse event. Each entry is one of shift, alt, meta, ctrl, or control. Maximum 8 entries. |
curl -X POST \ https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/mouse?terminal_id=1 \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "type": "move", "row": 10, "col": 5 }, { "type": "down", "row": 10, "col": 5, "button": 1 }, { "type": "up", "row": 10, "col": 5, "button": 1 } ] }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalAutomation.sendTerminalMouseEvents( { events: [ { type: 'move', row: 10, col: 5 }, { type: 'down', row: 10, col: 5, button: 1 }, { type: 'up', row: 10, col: 5, button: 1 } ] }, { terminal_id: 1 });{ "terminal_id": 1, "sent": 3, "filtered": 0, "protocol_active": true}{ "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"}Synchronization
Section titled “Synchronization”POST /api/v1/terminal/wait
Section titled “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 fordebounce_ms(default)regex— a PCRE2 pattern matches on the screeneither— first condition wins
The response includes a full snapshot for the matched, stable, timeout, and exited 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 due to a memory-cap resize and no coherent snapshot can be captured (clients 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 the response snapshot. Default: false |
include_highlights | boolean | No | Include highlights in the response snapshot. Default: true |
curl -X POST \ https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/wait?terminal_id=1 \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "mode": "regex", "pattern": "\\$ ", "timeout_ms": 5000, "search_scope": "screen" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalAutomation.waitForTerminal( { mode: 'regex', pattern: '\\$ ', timeout_ms: 5000, search_scope: 'screen' }, { terminal_id: 1 });{ "terminal_id": 1, "status": "matched", "elapsed_ms": 412, "match": { "row": 7, "col": 0, "text": "$ " }, "snapshot": { "seq": 156, "cols": 80, "rows": 24, "cursor": { "row": 7, "col": 2 }, "title": "bash — bash", "alt_screen": false, "lines": [ "user@host:~/projects/demo$ " ], "highlights": [] }}{ "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"}