@moejay/wrightty 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +90 -0
- package/.github/workflows/release.yml +177 -0
- package/Cargo.lock +2662 -0
- package/Cargo.toml +38 -0
- package/PROTOCOL.md +1351 -0
- package/README.md +386 -0
- package/agents/ceo/AGENTS.md +24 -0
- package/agents/ceo/HEARTBEAT.md +72 -0
- package/agents/ceo/SOUL.md +33 -0
- package/agents/ceo/TOOLS.md +3 -0
- package/agents/founding-engineer/AGENTS.md +44 -0
- package/crates/wrightty/Cargo.toml +43 -0
- package/crates/wrightty/src/client_cmds.rs +366 -0
- package/crates/wrightty/src/discover.rs +78 -0
- package/crates/wrightty/src/main.rs +100 -0
- package/crates/wrightty/src/server.rs +100 -0
- package/crates/wrightty/src/term.rs +338 -0
- package/crates/wrightty-bridge-ghostty/Cargo.toml +27 -0
- package/crates/wrightty-bridge-ghostty/src/ghostty.rs +422 -0
- package/crates/wrightty-bridge-ghostty/src/lib.rs +2 -0
- package/crates/wrightty-bridge-ghostty/src/main.rs +146 -0
- package/crates/wrightty-bridge-ghostty/src/rpc.rs +307 -0
- package/crates/wrightty-bridge-kitty/Cargo.toml +26 -0
- package/crates/wrightty-bridge-kitty/src/kitty.rs +269 -0
- package/crates/wrightty-bridge-kitty/src/lib.rs +2 -0
- package/crates/wrightty-bridge-kitty/src/main.rs +124 -0
- package/crates/wrightty-bridge-kitty/src/rpc.rs +304 -0
- package/crates/wrightty-bridge-tmux/Cargo.toml +26 -0
- package/crates/wrightty-bridge-tmux/src/lib.rs +2 -0
- package/crates/wrightty-bridge-tmux/src/main.rs +119 -0
- package/crates/wrightty-bridge-tmux/src/rpc.rs +291 -0
- package/crates/wrightty-bridge-tmux/src/tmux.rs +215 -0
- package/crates/wrightty-bridge-wezterm/Cargo.toml +26 -0
- package/crates/wrightty-bridge-wezterm/src/lib.rs +2 -0
- package/crates/wrightty-bridge-wezterm/src/main.rs +119 -0
- package/crates/wrightty-bridge-wezterm/src/rpc.rs +339 -0
- package/crates/wrightty-bridge-wezterm/src/wezterm.rs +190 -0
- package/crates/wrightty-bridge-zellij/Cargo.toml +27 -0
- package/crates/wrightty-bridge-zellij/src/lib.rs +2 -0
- package/crates/wrightty-bridge-zellij/src/main.rs +125 -0
- package/crates/wrightty-bridge-zellij/src/rpc.rs +328 -0
- package/crates/wrightty-bridge-zellij/src/zellij.rs +199 -0
- package/crates/wrightty-client/Cargo.toml +16 -0
- package/crates/wrightty-client/src/client.rs +254 -0
- package/crates/wrightty-client/src/lib.rs +2 -0
- package/crates/wrightty-core/Cargo.toml +21 -0
- package/crates/wrightty-core/src/input.rs +212 -0
- package/crates/wrightty-core/src/lib.rs +4 -0
- package/crates/wrightty-core/src/screen.rs +325 -0
- package/crates/wrightty-core/src/session.rs +249 -0
- package/crates/wrightty-core/src/session_manager.rs +77 -0
- package/crates/wrightty-protocol/Cargo.toml +13 -0
- package/crates/wrightty-protocol/src/error.rs +8 -0
- package/crates/wrightty-protocol/src/events.rs +138 -0
- package/crates/wrightty-protocol/src/lib.rs +4 -0
- package/crates/wrightty-protocol/src/methods.rs +321 -0
- package/crates/wrightty-protocol/src/types.rs +201 -0
- package/crates/wrightty-server/Cargo.toml +23 -0
- package/crates/wrightty-server/src/lib.rs +2 -0
- package/crates/wrightty-server/src/main.rs +65 -0
- package/crates/wrightty-server/src/rpc.rs +455 -0
- package/crates/wrightty-server/src/state.rs +39 -0
- package/examples/basic_command.py +53 -0
- package/examples/interactive_tui.py +86 -0
- package/examples/record_session.py +96 -0
- package/install.sh +81 -0
- package/package.json +24 -0
- package/sdks/node/package-lock.json +85 -0
- package/sdks/node/package.json +44 -0
- package/sdks/node/src/client.ts +94 -0
- package/sdks/node/src/index.ts +19 -0
- package/sdks/node/src/terminal.ts +258 -0
- package/sdks/node/src/types.ts +105 -0
- package/sdks/node/tsconfig.json +17 -0
- package/sdks/python/README.md +96 -0
- package/sdks/python/pyproject.toml +42 -0
- package/sdks/python/wrightty/__init__.py +6 -0
- package/sdks/python/wrightty/cli.py +210 -0
- package/sdks/python/wrightty/client.py +136 -0
- package/sdks/python/wrightty/mcp_server.py +434 -0
- package/sdks/python/wrightty/terminal.py +333 -0
- package/skills/wrightty/SKILL.md +261 -0
- package/src/lib.rs +1 -0
- package/tests/integration_test.rs +618 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""High-level Terminal API for AI agents and automation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from wrightty.client import WrighttyClient, WrighttyError
|
|
10
|
+
|
|
11
|
+
PORT_RANGE_START = 9420
|
|
12
|
+
PORT_RANGE_END = 9520
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Terminal:
|
|
16
|
+
"""High-level terminal automation interface.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
# Connect to a running wrightty server (daemon or native emulator)
|
|
20
|
+
term = Terminal.connect()
|
|
21
|
+
output = term.run("ls -la")
|
|
22
|
+
print(output)
|
|
23
|
+
|
|
24
|
+
# Spawn a new session on a wrightty-server daemon
|
|
25
|
+
term = Terminal.spawn()
|
|
26
|
+
term.run("echo hello")
|
|
27
|
+
term.close()
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, client: WrighttyClient, session_id: str):
|
|
31
|
+
self._client = client
|
|
32
|
+
self._session_id = session_id
|
|
33
|
+
self._prompt_pattern = r"[$#>%]\s*$"
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def discover(host: str = "127.0.0.1") -> list[dict]:
|
|
37
|
+
"""Scan for running wrightty servers on ports 9420-9520.
|
|
38
|
+
|
|
39
|
+
Returns a list of dicts with keys: url, version, implementation, capabilities.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
servers = Terminal.discover()
|
|
43
|
+
for s in servers:
|
|
44
|
+
print(f"{s['url']} — {s['implementation']}")
|
|
45
|
+
# ws://127.0.0.1:9420 — wrightty (headless)
|
|
46
|
+
# ws://127.0.0.1:9421 — wrightty (wezterm bridge)
|
|
47
|
+
"""
|
|
48
|
+
found = []
|
|
49
|
+
for port in range(PORT_RANGE_START, PORT_RANGE_END + 1):
|
|
50
|
+
url = f"ws://{host}:{port}"
|
|
51
|
+
try:
|
|
52
|
+
client = WrighttyClient.connect(url)
|
|
53
|
+
info = client.request("Wrightty.getInfo")
|
|
54
|
+
client.close()
|
|
55
|
+
found.append({
|
|
56
|
+
"url": url,
|
|
57
|
+
"port": port,
|
|
58
|
+
"version": info.get("version", "unknown"),
|
|
59
|
+
"implementation": info.get("implementation", "unknown"),
|
|
60
|
+
"capabilities": info.get("capabilities", {}),
|
|
61
|
+
})
|
|
62
|
+
except (ConnectionError, ConnectionRefusedError, OSError, WrighttyError):
|
|
63
|
+
continue
|
|
64
|
+
return found
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def connect(
|
|
68
|
+
cls,
|
|
69
|
+
url: str | None = None,
|
|
70
|
+
session_id: str | None = None,
|
|
71
|
+
) -> Terminal:
|
|
72
|
+
"""Connect to a wrightty server.
|
|
73
|
+
|
|
74
|
+
If no URL is given, auto-discovers the first available server
|
|
75
|
+
by scanning ports 9420-9440.
|
|
76
|
+
"""
|
|
77
|
+
if url is None:
|
|
78
|
+
servers = cls.discover()
|
|
79
|
+
if not servers:
|
|
80
|
+
raise ConnectionError(
|
|
81
|
+
"No wrightty server found. Start one with:\n"
|
|
82
|
+
" wrightty term --headless\n"
|
|
83
|
+
" wrightty term --bridge-tmux\n"
|
|
84
|
+
" wrightty term --bridge-wezterm"
|
|
85
|
+
)
|
|
86
|
+
url = servers[0]["url"]
|
|
87
|
+
|
|
88
|
+
client = WrighttyClient.connect(url)
|
|
89
|
+
|
|
90
|
+
if session_id is None:
|
|
91
|
+
result = client.request("Session.list")
|
|
92
|
+
sessions = result.get("sessions", [])
|
|
93
|
+
if sessions:
|
|
94
|
+
session_id = sessions[0]["sessionId"]
|
|
95
|
+
else:
|
|
96
|
+
session_id = "0"
|
|
97
|
+
|
|
98
|
+
return cls(client, session_id)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def spawn(
|
|
102
|
+
cls,
|
|
103
|
+
shell: str | None = None,
|
|
104
|
+
cols: int = 120,
|
|
105
|
+
rows: int = 40,
|
|
106
|
+
cwd: str | None = None,
|
|
107
|
+
server_url: str = "ws://127.0.0.1:9420",
|
|
108
|
+
) -> Terminal:
|
|
109
|
+
"""Connect to a wrightty-server daemon and create a new session."""
|
|
110
|
+
client = WrighttyClient.connect(server_url)
|
|
111
|
+
|
|
112
|
+
params: dict[str, Any] = {"cols": cols, "rows": rows}
|
|
113
|
+
if shell:
|
|
114
|
+
params["shell"] = shell
|
|
115
|
+
if cwd:
|
|
116
|
+
params["cwd"] = cwd
|
|
117
|
+
|
|
118
|
+
result = client.request("Session.create", params)
|
|
119
|
+
session_id = result["sessionId"]
|
|
120
|
+
|
|
121
|
+
term = cls(client, session_id)
|
|
122
|
+
term.wait_for_prompt(timeout=5)
|
|
123
|
+
return term
|
|
124
|
+
|
|
125
|
+
def close(self):
|
|
126
|
+
"""Close the connection."""
|
|
127
|
+
self._client.close()
|
|
128
|
+
|
|
129
|
+
def __enter__(self):
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def __exit__(self, *args):
|
|
133
|
+
self.close()
|
|
134
|
+
|
|
135
|
+
# --- High-level API ---
|
|
136
|
+
|
|
137
|
+
def run(self, command: str, timeout: float = 30) -> str:
|
|
138
|
+
"""Run a command and return its output.
|
|
139
|
+
|
|
140
|
+
Sends the command, waits for the prompt to reappear, and returns
|
|
141
|
+
everything between the command echo and the next prompt.
|
|
142
|
+
"""
|
|
143
|
+
self.send_text(command + "\n")
|
|
144
|
+
|
|
145
|
+
# Wait for prompt to come back.
|
|
146
|
+
self.wait_for_prompt(timeout=timeout)
|
|
147
|
+
|
|
148
|
+
# Read screen and extract output.
|
|
149
|
+
screen = self.read_screen()
|
|
150
|
+
lines = screen.strip().split("\n")
|
|
151
|
+
|
|
152
|
+
output_lines = []
|
|
153
|
+
found_cmd = False
|
|
154
|
+
for line in lines:
|
|
155
|
+
if not found_cmd:
|
|
156
|
+
if command in line:
|
|
157
|
+
found_cmd = True
|
|
158
|
+
continue
|
|
159
|
+
if re.search(self._prompt_pattern, line):
|
|
160
|
+
break
|
|
161
|
+
output_lines.append(line)
|
|
162
|
+
|
|
163
|
+
return "\n".join(output_lines)
|
|
164
|
+
|
|
165
|
+
def send_text(self, text: str):
|
|
166
|
+
"""Send raw text to the terminal."""
|
|
167
|
+
self._client.request("Input.sendText", {"sessionId": self._session_id, "text": text})
|
|
168
|
+
|
|
169
|
+
def send_keys(self, *keys: str):
|
|
170
|
+
"""Send structured keystrokes.
|
|
171
|
+
|
|
172
|
+
Examples:
|
|
173
|
+
term.send_keys("Ctrl+c")
|
|
174
|
+
term.send_keys("ArrowUp", "Enter")
|
|
175
|
+
term.send_keys("Escape", ":", "w", "q", "Enter") # vim :wq
|
|
176
|
+
"""
|
|
177
|
+
self._client.request(
|
|
178
|
+
"Input.sendKeys", {"sessionId": self._session_id, "keys": list(keys)}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def read_screen(self) -> str:
|
|
182
|
+
"""Read the current visible screen as text."""
|
|
183
|
+
result = self._client.request("Screen.getText", {"sessionId": self._session_id})
|
|
184
|
+
return result["text"]
|
|
185
|
+
|
|
186
|
+
def screenshot(self, format: str = "svg") -> str | bytes:
|
|
187
|
+
"""Take a screenshot. Returns str for text/svg, bytes for png."""
|
|
188
|
+
result = self._client.request(
|
|
189
|
+
"Screen.screenshot", {"sessionId": self._session_id, "format": format}
|
|
190
|
+
)
|
|
191
|
+
data = result["data"]
|
|
192
|
+
if format == "png":
|
|
193
|
+
import base64
|
|
194
|
+
return base64.b64decode(data)
|
|
195
|
+
return data
|
|
196
|
+
|
|
197
|
+
def wait_for(self, pattern: str, timeout: float = 30, regex: bool = False) -> str:
|
|
198
|
+
"""Wait until a pattern appears on screen. Returns the screen text when found."""
|
|
199
|
+
deadline = time.monotonic() + timeout
|
|
200
|
+
while time.monotonic() < deadline:
|
|
201
|
+
screen = self.read_screen()
|
|
202
|
+
if regex:
|
|
203
|
+
if re.search(pattern, screen):
|
|
204
|
+
return screen
|
|
205
|
+
else:
|
|
206
|
+
if pattern in screen:
|
|
207
|
+
return screen
|
|
208
|
+
time.sleep(0.2)
|
|
209
|
+
raise TimeoutError(f"Pattern {pattern!r} not found within {timeout}s")
|
|
210
|
+
|
|
211
|
+
def wait_for_prompt(self, timeout: float = 10) -> str:
|
|
212
|
+
"""Wait for the shell prompt to appear."""
|
|
213
|
+
return self.wait_for(self._prompt_pattern, timeout=timeout, regex=True)
|
|
214
|
+
|
|
215
|
+
def set_prompt_pattern(self, pattern: str):
|
|
216
|
+
"""Override the regex used to detect the shell prompt."""
|
|
217
|
+
self._prompt_pattern = pattern
|
|
218
|
+
|
|
219
|
+
def get_size(self) -> tuple[int, int]:
|
|
220
|
+
"""Get terminal dimensions as (cols, rows)."""
|
|
221
|
+
result = self._client.request("Terminal.getSize", {"sessionId": self._session_id})
|
|
222
|
+
return result["cols"], result["rows"]
|
|
223
|
+
|
|
224
|
+
def resize(self, cols: int, rows: int):
|
|
225
|
+
"""Resize the terminal."""
|
|
226
|
+
self._client.request(
|
|
227
|
+
"Terminal.resize", {"sessionId": self._session_id, "cols": cols, "rows": rows}
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def get_info(self) -> dict:
|
|
231
|
+
"""Get server info and capabilities."""
|
|
232
|
+
return self._client.request("Wrightty.getInfo")
|
|
233
|
+
|
|
234
|
+
# --- Recording ---
|
|
235
|
+
|
|
236
|
+
def start_session_recording(self, include_input: bool = False) -> str:
|
|
237
|
+
"""Start recording raw PTY I/O (asciicast v2 format).
|
|
238
|
+
|
|
239
|
+
Returns a recording ID. Stop with stop_session_recording().
|
|
240
|
+
The recording can be played back with `asciinema play`.
|
|
241
|
+
"""
|
|
242
|
+
result = self._client.request(
|
|
243
|
+
"Recording.startSession",
|
|
244
|
+
{"sessionId": self._session_id, "includeInput": include_input},
|
|
245
|
+
)
|
|
246
|
+
return result["recordingId"]
|
|
247
|
+
|
|
248
|
+
def stop_session_recording(self, recording_id: str) -> dict:
|
|
249
|
+
"""Stop a session recording and return asciicast data.
|
|
250
|
+
|
|
251
|
+
Returns dict with keys: format, data, duration, events.
|
|
252
|
+
Save `data` to a .cast file for asciinema playback.
|
|
253
|
+
"""
|
|
254
|
+
return self._client.request("Recording.stopSession", {"recordingId": recording_id})
|
|
255
|
+
|
|
256
|
+
def start_action_recording(self, format: str = "python") -> str:
|
|
257
|
+
"""Start recording wrightty API calls as a replayable script.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
format: "python", "json", or "cli"
|
|
261
|
+
|
|
262
|
+
Returns a recording ID. Stop with stop_action_recording().
|
|
263
|
+
"""
|
|
264
|
+
result = self._client.request(
|
|
265
|
+
"Recording.startActions",
|
|
266
|
+
{"sessionId": self._session_id, "format": format},
|
|
267
|
+
)
|
|
268
|
+
return result["recordingId"]
|
|
269
|
+
|
|
270
|
+
def stop_action_recording(self, recording_id: str) -> dict:
|
|
271
|
+
"""Stop action recording and return the generated script.
|
|
272
|
+
|
|
273
|
+
Returns dict with keys: format, data, actions, duration.
|
|
274
|
+
"""
|
|
275
|
+
return self._client.request("Recording.stopActions", {"recordingId": recording_id})
|
|
276
|
+
|
|
277
|
+
def capture_screen(self, format: str = "svg") -> dict:
|
|
278
|
+
"""Capture a single screen frame.
|
|
279
|
+
|
|
280
|
+
Returns dict with keys: frameId, timestamp, format, data.
|
|
281
|
+
"""
|
|
282
|
+
return self._client.request(
|
|
283
|
+
"Recording.captureScreen",
|
|
284
|
+
{"sessionId": self._session_id, "format": format},
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def start_screen_recording(self, interval_ms: int = 1000, format: str = "svg") -> str:
|
|
288
|
+
"""Start periodic screen capture.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
interval_ms: Capture interval in milliseconds (default: 1000)
|
|
292
|
+
format: "svg", "text", or "png"
|
|
293
|
+
|
|
294
|
+
Returns a recording ID. Stop with stop_screen_recording().
|
|
295
|
+
"""
|
|
296
|
+
result = self._client.request(
|
|
297
|
+
"Recording.startScreenCapture",
|
|
298
|
+
{"sessionId": self._session_id, "intervalMs": interval_ms, "format": format},
|
|
299
|
+
)
|
|
300
|
+
return result["recordingId"]
|
|
301
|
+
|
|
302
|
+
def stop_screen_recording(self, recording_id: str) -> dict:
|
|
303
|
+
"""Stop screen capture and return all frames.
|
|
304
|
+
|
|
305
|
+
Returns dict with keys: frames, duration, frameCount, format.
|
|
306
|
+
"""
|
|
307
|
+
return self._client.request("Recording.stopScreenCapture", {"recordingId": recording_id})
|
|
308
|
+
|
|
309
|
+
def start_video(self, fps: int = 30, format: str = "mp4") -> str:
|
|
310
|
+
"""Start framebuffer video recording (native emulators only).
|
|
311
|
+
|
|
312
|
+
Captures the actual rendered pixels via glReadPixels and pipes to ffmpeg.
|
|
313
|
+
Requires ffmpeg installed on the system.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
fps: Frames per second (default: 30)
|
|
317
|
+
format: "mp4", "webm", or "gif"
|
|
318
|
+
|
|
319
|
+
Returns a recording ID. Stop with stop_video().
|
|
320
|
+
"""
|
|
321
|
+
result = self._client.request(
|
|
322
|
+
"Recording.startVideo",
|
|
323
|
+
{"sessionId": self._session_id, "fps": fps, "format": format},
|
|
324
|
+
)
|
|
325
|
+
return result["recordingId"]
|
|
326
|
+
|
|
327
|
+
def stop_video(self, recording_id: str) -> dict:
|
|
328
|
+
"""Stop video recording and return metadata.
|
|
329
|
+
|
|
330
|
+
Returns dict with keys: format, path, duration, frames, width, height, size.
|
|
331
|
+
The video file is at the returned `path`.
|
|
332
|
+
"""
|
|
333
|
+
return self._client.request("Recording.stopVideo", {"recordingId": recording_id})
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wrightty
|
|
3
|
+
description: Control terminal emulators programmatically via the wrightty protocol. Use when you need to run commands in a real terminal, interact with TUI applications, read terminal screen output, take screenshots, send keystrokes, or automate terminal workflows. Triggers on tasks involving terminal automation, running shell commands through a real PTY, controlling vim/htop/other TUI apps, or capturing terminal output.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: moejay
|
|
7
|
+
version: "0.1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# wrightty — Terminal Automation Protocol
|
|
11
|
+
|
|
12
|
+
Wrightty lets you control terminal emulators over WebSocket JSON-RPC. You can run commands, read the screen, send keystrokes, and take screenshots — just like a human would interact with a terminal, but programmatically.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- Running shell commands in a real terminal (not subprocess) where you need the full PTY environment
|
|
17
|
+
- Interacting with TUI applications (vim, htop, less, etc.)
|
|
18
|
+
- Reading terminal screen output with colors and formatting
|
|
19
|
+
- Taking terminal screenshots (SVG)
|
|
20
|
+
- Automating interactive terminal workflows
|
|
21
|
+
- Waiting for specific output to appear before proceeding
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
Wrightty needs a terminal backend. Pick one:
|
|
26
|
+
|
|
27
|
+
**Alacritty (native, zero overhead):**
|
|
28
|
+
```bash
|
|
29
|
+
git clone -b wrightty-support https://github.com/moejay/alacritty.git
|
|
30
|
+
cd alacritty && cargo build --features wrightty
|
|
31
|
+
./target/debug/alacritty --wrightty # listens on ws://127.0.0.1:9420
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**WezTerm (via bridge):**
|
|
35
|
+
```bash
|
|
36
|
+
# Start WezTerm normally, then:
|
|
37
|
+
cargo run -p wrightty-bridge-wezterm # listens on ws://127.0.0.1:9421
|
|
38
|
+
|
|
39
|
+
# For flatpak:
|
|
40
|
+
WEZTERM_CMD="flatpak run --command=wezterm org.wezfurlong.wezterm" cargo run -p wrightty-bridge-wezterm
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Headless daemon (no GUI):**
|
|
44
|
+
```bash
|
|
45
|
+
cargo run -p wrightty-server # listens on ws://127.0.0.1:9420
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Python SDK
|
|
49
|
+
|
|
50
|
+
The SDK has zero external dependencies.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from wrightty import Terminal
|
|
54
|
+
|
|
55
|
+
# Connect to a running terminal
|
|
56
|
+
term = Terminal.connect() # ws://127.0.0.1:9420
|
|
57
|
+
term = Terminal.connect("ws://127.0.0.1:9421") # WezTerm bridge
|
|
58
|
+
|
|
59
|
+
# Run a command and get output
|
|
60
|
+
output = term.run("cargo test", timeout=120)
|
|
61
|
+
|
|
62
|
+
# Read the current screen
|
|
63
|
+
screen = term.read_screen()
|
|
64
|
+
|
|
65
|
+
# Wait for text to appear
|
|
66
|
+
term.wait_for("tests passed", timeout=60)
|
|
67
|
+
term.wait_for(r"error\[\w+\]", regex=True)
|
|
68
|
+
|
|
69
|
+
# Send keystrokes (TUI apps)
|
|
70
|
+
term.send_keys("Escape", ":", "w", "q", "Enter") # vim: save and quit
|
|
71
|
+
term.send_keys("Ctrl+c") # interrupt
|
|
72
|
+
term.send_keys("ArrowUp", "Enter") # repeat last command
|
|
73
|
+
|
|
74
|
+
# Screenshots
|
|
75
|
+
svg = term.screenshot("svg")
|
|
76
|
+
|
|
77
|
+
# Terminal info
|
|
78
|
+
cols, rows = term.get_size()
|
|
79
|
+
|
|
80
|
+
term.close()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Install: `pip install /path/to/wrightty/sdks/python` or set `PYTHONPATH=/path/to/wrightty/sdks/python`.
|
|
84
|
+
|
|
85
|
+
## CLI
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
wrightty run "ls -la" # run command, print output
|
|
89
|
+
wrightty run "cargo test" --timeout 120 # with timeout
|
|
90
|
+
wrightty read # dump current screen
|
|
91
|
+
wrightty send-text "echo hello\n" # send raw text
|
|
92
|
+
wrightty send-keys Ctrl+c # send keystroke
|
|
93
|
+
wrightty send-keys Escape : w q Enter # vim: :wq
|
|
94
|
+
wrightty wait-for "BUILD SUCCESS" --timeout 60 # block until text appears
|
|
95
|
+
wrightty screenshot --format svg -o terminal.svg # take screenshot
|
|
96
|
+
wrightty info # server capabilities
|
|
97
|
+
wrightty size # terminal dimensions
|
|
98
|
+
wrightty --url ws://127.0.0.1:9421 run "ls" # connect to different server
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## MCP Server
|
|
102
|
+
|
|
103
|
+
Add to your MCP config (Claude, Cursor, etc.):
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"wrightty": {
|
|
109
|
+
"command": "python3",
|
|
110
|
+
"args": ["-m", "wrightty.mcp_server"],
|
|
111
|
+
"env": {
|
|
112
|
+
"PYTHONPATH": "/path/to/wrightty/sdks/python",
|
|
113
|
+
"WRIGHTTY_SOCKET": "ws://127.0.0.1:9420"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Available MCP tools:
|
|
121
|
+
|
|
122
|
+
| Tool | Description |
|
|
123
|
+
|------|-------------|
|
|
124
|
+
| `run_command` | Run a shell command, return output |
|
|
125
|
+
| `read_terminal` | Read current screen text |
|
|
126
|
+
| `send_keys` | Send keystrokes (`["Ctrl+c"]`, `["Escape", ":", "q", "Enter"]`) |
|
|
127
|
+
| `send_text` | Send raw text (use `\n` for newline) |
|
|
128
|
+
| `screenshot` | Terminal screenshot as SVG |
|
|
129
|
+
| `wait_for_text` | Block until pattern appears on screen |
|
|
130
|
+
| `terminal_info` | Terminal dimensions and capabilities |
|
|
131
|
+
|
|
132
|
+
## Protocol reference
|
|
133
|
+
|
|
134
|
+
WebSocket JSON-RPC 2.0 on `ws://127.0.0.1:9420`. Full spec: [PROTOCOL.md](https://github.com/moejay/wrightty/blob/main/PROTOCOL.md).
|
|
135
|
+
|
|
136
|
+
### Core methods
|
|
137
|
+
|
|
138
|
+
**Run a command:**
|
|
139
|
+
```json
|
|
140
|
+
{"jsonrpc":"2.0","id":1,"method":"Input.sendText","params":{"sessionId":"0","text":"ls -la\n"}}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Read the screen:**
|
|
144
|
+
```json
|
|
145
|
+
{"jsonrpc":"2.0","id":2,"method":"Screen.getText","params":{"sessionId":"0"}}
|
|
146
|
+
// → {"result":{"text":"$ ls -la\ntotal 140\ndrwxr-xr-x 8 user user 4096 ...\n$"}}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Send keystrokes:**
|
|
150
|
+
```json
|
|
151
|
+
{"jsonrpc":"2.0","id":3,"method":"Input.sendKeys","params":{"sessionId":"0","keys":["Ctrl+c"]}}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Take a screenshot:**
|
|
155
|
+
```json
|
|
156
|
+
{"jsonrpc":"2.0","id":4,"method":"Screen.screenshot","params":{"sessionId":"0","format":"svg"}}
|
|
157
|
+
// → {"result":{"format":"svg","data":"<svg ...>"}}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Get terminal size:**
|
|
161
|
+
```json
|
|
162
|
+
{"jsonrpc":"2.0","id":5,"method":"Terminal.getSize","params":{"sessionId":"0"}}
|
|
163
|
+
// → {"result":{"cols":120,"rows":40}}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Server capabilities:**
|
|
167
|
+
```json
|
|
168
|
+
{"jsonrpc":"2.0","id":6,"method":"Wrightty.getInfo","params":{}}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Key names for sendKeys
|
|
172
|
+
|
|
173
|
+
Single characters: `a`, `1`, `/`, `.`
|
|
174
|
+
Special keys: `Enter`, `Tab`, `Escape`, `Backspace`, `Delete`, `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight`, `Home`, `End`, `PageUp`, `PageDown`, `F1`-`F12`
|
|
175
|
+
Modifiers: `Ctrl+c`, `Alt+x`, `Shift+Tab`
|
|
176
|
+
|
|
177
|
+
### Session management (headless daemon only)
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
// Create session
|
|
181
|
+
{"jsonrpc":"2.0","id":1,"method":"Session.create","params":{"cols":80,"rows":24}}
|
|
182
|
+
// → {"result":{"sessionId":"abc-123"}}
|
|
183
|
+
|
|
184
|
+
// List sessions
|
|
185
|
+
{"jsonrpc":"2.0","id":2,"method":"Session.list","params":{}}
|
|
186
|
+
|
|
187
|
+
// Destroy session
|
|
188
|
+
{"jsonrpc":"2.0","id":3,"method":"Session.destroy","params":{"sessionId":"abc-123"}}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Native emulator mode uses `"sessionId":"0"` for the active terminal. Session create/destroy is not supported — the emulator manages its own sessions.
|
|
192
|
+
|
|
193
|
+
## Recording
|
|
194
|
+
|
|
195
|
+
Three types of recording:
|
|
196
|
+
|
|
197
|
+
### Session recording (asciicast)
|
|
198
|
+
Records raw PTY I/O with timestamps. Compatible with `asciinema play`.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
rec_id = term.start_session_recording(include_input=True)
|
|
202
|
+
# ... do stuff ...
|
|
203
|
+
result = term.stop_session_recording(rec_id)
|
|
204
|
+
with open("session.cast", "w") as f:
|
|
205
|
+
f.write(result["data"])
|
|
206
|
+
# Play back: asciinema play session.cast
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Action recording (script generation)
|
|
210
|
+
Records wrightty API calls as a replayable Python/JSON/CLI script. Like Playwright codegen.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
rec_id = term.start_action_recording(format="python")
|
|
214
|
+
# ... do stuff ...
|
|
215
|
+
result = term.stop_action_recording(rec_id)
|
|
216
|
+
print(result["data"]) # prints a Python script that replays the actions
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Screen capture
|
|
220
|
+
Capture individual frames or record periodic screenshots.
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
# Single frame
|
|
224
|
+
frame = term.capture_screen(format="svg")
|
|
225
|
+
|
|
226
|
+
# Periodic capture
|
|
227
|
+
rec_id = term.start_screen_recording(interval_ms=500, format="svg")
|
|
228
|
+
# ... do stuff ...
|
|
229
|
+
result = term.stop_screen_recording(rec_id)
|
|
230
|
+
# result["frames"] is a list of SVG frames with timestamps
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Video recording (native emulators only, requires ffmpeg)
|
|
234
|
+
Captures the actual rendered framebuffer — pixel perfect, including colors, fonts, sixel images, everything on screen.
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
rec_id = term.start_video(fps=30, format="mp4")
|
|
238
|
+
# ... do stuff ...
|
|
239
|
+
result = term.stop_video(rec_id)
|
|
240
|
+
print(result["path"]) # /tmp/wrightty-rec_001.mp4
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### CLI
|
|
244
|
+
```bash
|
|
245
|
+
wrightty record -o session.cast # Ctrl+C to stop
|
|
246
|
+
wrightty record-actions -o script.py # generates Python script
|
|
247
|
+
wrightty record-actions --format cli # generates shell commands
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### MCP tools
|
|
251
|
+
- `start_recording` — starts session + action recording
|
|
252
|
+
- `stop_recording` — stops and returns asciicast data + Python script
|
|
253
|
+
- `capture_screen_frame` — single SVG snapshot
|
|
254
|
+
|
|
255
|
+
## Tips
|
|
256
|
+
|
|
257
|
+
- Use `term.run()` for most commands — it sends the command and waits for the prompt to return
|
|
258
|
+
- Use `term.send_keys()` for interactive/TUI apps where you need specific keystrokes
|
|
259
|
+
- Use `term.wait_for()` before reading output from slow commands (builds, tests)
|
|
260
|
+
- The default prompt detection pattern is `[$#>%]\s*$` — override with `term.set_prompt_pattern()` if your prompt is different
|
|
261
|
+
- Screenshots are SVG by default — good for rendering in browsers and docs, and readable by vision models
|
package/src/lib.rs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Root package exists only for integration tests.
|