@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.
Files changed (84) hide show
  1. package/.github/workflows/ci.yml +90 -0
  2. package/.github/workflows/release.yml +177 -0
  3. package/Cargo.lock +2662 -0
  4. package/Cargo.toml +38 -0
  5. package/PROTOCOL.md +1351 -0
  6. package/README.md +386 -0
  7. package/agents/ceo/AGENTS.md +24 -0
  8. package/agents/ceo/HEARTBEAT.md +72 -0
  9. package/agents/ceo/SOUL.md +33 -0
  10. package/agents/ceo/TOOLS.md +3 -0
  11. package/agents/founding-engineer/AGENTS.md +44 -0
  12. package/crates/wrightty/Cargo.toml +43 -0
  13. package/crates/wrightty/src/client_cmds.rs +366 -0
  14. package/crates/wrightty/src/discover.rs +78 -0
  15. package/crates/wrightty/src/main.rs +100 -0
  16. package/crates/wrightty/src/server.rs +100 -0
  17. package/crates/wrightty/src/term.rs +338 -0
  18. package/crates/wrightty-bridge-ghostty/Cargo.toml +27 -0
  19. package/crates/wrightty-bridge-ghostty/src/ghostty.rs +422 -0
  20. package/crates/wrightty-bridge-ghostty/src/lib.rs +2 -0
  21. package/crates/wrightty-bridge-ghostty/src/main.rs +146 -0
  22. package/crates/wrightty-bridge-ghostty/src/rpc.rs +307 -0
  23. package/crates/wrightty-bridge-kitty/Cargo.toml +26 -0
  24. package/crates/wrightty-bridge-kitty/src/kitty.rs +269 -0
  25. package/crates/wrightty-bridge-kitty/src/lib.rs +2 -0
  26. package/crates/wrightty-bridge-kitty/src/main.rs +124 -0
  27. package/crates/wrightty-bridge-kitty/src/rpc.rs +304 -0
  28. package/crates/wrightty-bridge-tmux/Cargo.toml +26 -0
  29. package/crates/wrightty-bridge-tmux/src/lib.rs +2 -0
  30. package/crates/wrightty-bridge-tmux/src/main.rs +119 -0
  31. package/crates/wrightty-bridge-tmux/src/rpc.rs +291 -0
  32. package/crates/wrightty-bridge-tmux/src/tmux.rs +215 -0
  33. package/crates/wrightty-bridge-wezterm/Cargo.toml +26 -0
  34. package/crates/wrightty-bridge-wezterm/src/lib.rs +2 -0
  35. package/crates/wrightty-bridge-wezterm/src/main.rs +119 -0
  36. package/crates/wrightty-bridge-wezterm/src/rpc.rs +339 -0
  37. package/crates/wrightty-bridge-wezterm/src/wezterm.rs +190 -0
  38. package/crates/wrightty-bridge-zellij/Cargo.toml +27 -0
  39. package/crates/wrightty-bridge-zellij/src/lib.rs +2 -0
  40. package/crates/wrightty-bridge-zellij/src/main.rs +125 -0
  41. package/crates/wrightty-bridge-zellij/src/rpc.rs +328 -0
  42. package/crates/wrightty-bridge-zellij/src/zellij.rs +199 -0
  43. package/crates/wrightty-client/Cargo.toml +16 -0
  44. package/crates/wrightty-client/src/client.rs +254 -0
  45. package/crates/wrightty-client/src/lib.rs +2 -0
  46. package/crates/wrightty-core/Cargo.toml +21 -0
  47. package/crates/wrightty-core/src/input.rs +212 -0
  48. package/crates/wrightty-core/src/lib.rs +4 -0
  49. package/crates/wrightty-core/src/screen.rs +325 -0
  50. package/crates/wrightty-core/src/session.rs +249 -0
  51. package/crates/wrightty-core/src/session_manager.rs +77 -0
  52. package/crates/wrightty-protocol/Cargo.toml +13 -0
  53. package/crates/wrightty-protocol/src/error.rs +8 -0
  54. package/crates/wrightty-protocol/src/events.rs +138 -0
  55. package/crates/wrightty-protocol/src/lib.rs +4 -0
  56. package/crates/wrightty-protocol/src/methods.rs +321 -0
  57. package/crates/wrightty-protocol/src/types.rs +201 -0
  58. package/crates/wrightty-server/Cargo.toml +23 -0
  59. package/crates/wrightty-server/src/lib.rs +2 -0
  60. package/crates/wrightty-server/src/main.rs +65 -0
  61. package/crates/wrightty-server/src/rpc.rs +455 -0
  62. package/crates/wrightty-server/src/state.rs +39 -0
  63. package/examples/basic_command.py +53 -0
  64. package/examples/interactive_tui.py +86 -0
  65. package/examples/record_session.py +96 -0
  66. package/install.sh +81 -0
  67. package/package.json +24 -0
  68. package/sdks/node/package-lock.json +85 -0
  69. package/sdks/node/package.json +44 -0
  70. package/sdks/node/src/client.ts +94 -0
  71. package/sdks/node/src/index.ts +19 -0
  72. package/sdks/node/src/terminal.ts +258 -0
  73. package/sdks/node/src/types.ts +105 -0
  74. package/sdks/node/tsconfig.json +17 -0
  75. package/sdks/python/README.md +96 -0
  76. package/sdks/python/pyproject.toml +42 -0
  77. package/sdks/python/wrightty/__init__.py +6 -0
  78. package/sdks/python/wrightty/cli.py +210 -0
  79. package/sdks/python/wrightty/client.py +136 -0
  80. package/sdks/python/wrightty/mcp_server.py +434 -0
  81. package/sdks/python/wrightty/terminal.py +333 -0
  82. package/skills/wrightty/SKILL.md +261 -0
  83. package/src/lib.rs +1 -0
  84. package/tests/integration_test.rs +618 -0
@@ -0,0 +1,258 @@
1
+ /** High-level Terminal API for AI agents and automation. */
2
+
3
+ import { WrighttyClient } from "./client";
4
+ import type {
5
+ ConnectOptions,
6
+ DiscoveredServer,
7
+ ScreenshotFormat,
8
+ ScreenshotResult,
9
+ SessionInfo,
10
+ SpawnOptions,
11
+ WaitForTextResult,
12
+ SessionRecordingData,
13
+ ActionRecordingData,
14
+ } from "./types";
15
+
16
+ const PORT_RANGE_START = 9420;
17
+ const PORT_RANGE_END = 9520;
18
+
19
+ export class Terminal {
20
+ private client: WrighttyClient;
21
+ private sessionId: string;
22
+ private promptPattern = /[$#>%]\s*$/;
23
+
24
+ private constructor(client: WrighttyClient, sessionId: string) {
25
+ this.client = client;
26
+ this.sessionId = sessionId;
27
+ }
28
+
29
+ /** Scan for running wrightty servers on ports 9420-9520. */
30
+ static async discover(host = "127.0.0.1"): Promise<DiscoveredServer[]> {
31
+ const found: DiscoveredServer[] = [];
32
+
33
+ const checks = [];
34
+ for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
35
+ const url = `ws://${host}:${port}`;
36
+ checks.push(
37
+ WrighttyClient.connect(url, 200)
38
+ .then(async (client) => {
39
+ try {
40
+ const info = await client.request("Wrightty.getInfo");
41
+ found.push({
42
+ url,
43
+ port,
44
+ version: info.version ?? "unknown",
45
+ implementation: info.implementation ?? "unknown",
46
+ capabilities: info.capabilities ?? {},
47
+ });
48
+ } finally {
49
+ client.close();
50
+ }
51
+ })
52
+ .catch(() => {
53
+ /* port not listening */
54
+ }),
55
+ );
56
+ }
57
+
58
+ await Promise.all(checks);
59
+ return found.sort((a, b) => a.port - b.port);
60
+ }
61
+
62
+ /** Connect to a wrightty server. Auto-discovers if no URL given. */
63
+ static async connect(options: ConnectOptions = {}): Promise<Terminal> {
64
+ let url = options.url;
65
+
66
+ if (!url) {
67
+ const servers = await Terminal.discover();
68
+ if (servers.length === 0) {
69
+ throw new Error(
70
+ "No wrightty server found. Start one with:\n" +
71
+ " wrightty term --headless\n" +
72
+ " wrightty term --bridge-tmux\n" +
73
+ " wrightty term --bridge-wezterm",
74
+ );
75
+ }
76
+ url = servers[0].url;
77
+ }
78
+
79
+ const client = await WrighttyClient.connect(url, options.timeout ?? 5000);
80
+
81
+ let sessionId = options.sessionId;
82
+ if (!sessionId) {
83
+ const result = await client.request("Session.list");
84
+ const sessions: SessionInfo[] = result.sessions ?? [];
85
+ sessionId = sessions.length > 0 ? sessions[0].sessionId : "0";
86
+ }
87
+
88
+ return new Terminal(client, sessionId);
89
+ }
90
+
91
+ /** Connect to a headless server and create a new session. */
92
+ static async spawn(options: SpawnOptions = {}): Promise<Terminal> {
93
+ const url = options.serverUrl ?? "ws://127.0.0.1:9420";
94
+ const client = await WrighttyClient.connect(url);
95
+
96
+ const result = await client.request("Session.create", {
97
+ cols: options.cols ?? 120,
98
+ rows: options.rows ?? 40,
99
+ shell: options.shell,
100
+ cwd: options.cwd,
101
+ });
102
+
103
+ const term = new Terminal(client, result.sessionId);
104
+ await term.waitForPrompt(5000);
105
+ return term;
106
+ }
107
+
108
+ /** Close the connection. */
109
+ close(): void {
110
+ this.client.close();
111
+ }
112
+
113
+ // --- High-level API ---
114
+
115
+ /** Run a command and return its output. */
116
+ async run(command: string, timeoutMs = 30000): Promise<string> {
117
+ await this.sendText(command + "\n");
118
+ await this.waitForPrompt(timeoutMs);
119
+
120
+ const screen = await this.readScreen();
121
+ const lines = screen.trim().split("\n");
122
+
123
+ const outputLines: string[] = [];
124
+ let foundCmd = false;
125
+ for (const line of lines) {
126
+ if (!foundCmd) {
127
+ if (line.includes(command)) foundCmd = true;
128
+ continue;
129
+ }
130
+ if (this.promptPattern.test(line)) break;
131
+ outputLines.push(line);
132
+ }
133
+
134
+ return outputLines.join("\n");
135
+ }
136
+
137
+ /** Send raw text to the terminal. */
138
+ async sendText(text: string): Promise<void> {
139
+ await this.client.request("Input.sendText", {
140
+ sessionId: this.sessionId,
141
+ text,
142
+ });
143
+ }
144
+
145
+ /** Send structured keystrokes. */
146
+ async sendKeys(...keys: string[]): Promise<void> {
147
+ await this.client.request("Input.sendKeys", {
148
+ sessionId: this.sessionId,
149
+ keys,
150
+ });
151
+ }
152
+
153
+ /** Read the current visible screen as text. */
154
+ async readScreen(): Promise<string> {
155
+ const result = await this.client.request("Screen.getText", {
156
+ sessionId: this.sessionId,
157
+ });
158
+ return result.text;
159
+ }
160
+
161
+ /** Take a screenshot. */
162
+ async screenshot(format: ScreenshotFormat = "svg"): Promise<ScreenshotResult> {
163
+ return this.client.request("Screen.screenshot", {
164
+ sessionId: this.sessionId,
165
+ format,
166
+ });
167
+ }
168
+
169
+ /** Wait until a pattern appears on screen. */
170
+ async waitFor(pattern: string | RegExp, timeoutMs = 30000): Promise<string> {
171
+ const isRegex = pattern instanceof RegExp;
172
+ const patternStr = isRegex ? pattern.source : pattern;
173
+
174
+ const result: WaitForTextResult = await this.client.request("Screen.waitForText", {
175
+ sessionId: this.sessionId,
176
+ pattern: patternStr,
177
+ isRegex,
178
+ timeout: timeoutMs,
179
+ interval: 50,
180
+ });
181
+
182
+ if (!result.found) {
183
+ throw new Error(`Pattern ${patternStr} not found within ${timeoutMs}ms`);
184
+ }
185
+
186
+ return this.readScreen();
187
+ }
188
+
189
+ /** Wait for the shell prompt to appear. */
190
+ async waitForPrompt(timeoutMs = 10000): Promise<string> {
191
+ return this.waitFor(this.promptPattern, timeoutMs);
192
+ }
193
+
194
+ /** Override the regex used to detect the shell prompt. */
195
+ setPromptPattern(pattern: RegExp): void {
196
+ this.promptPattern = pattern;
197
+ }
198
+
199
+ /** Get terminal dimensions as [cols, rows]. */
200
+ async getSize(): Promise<[number, number]> {
201
+ const result = await this.client.request("Terminal.getSize", {
202
+ sessionId: this.sessionId,
203
+ });
204
+ return [result.cols, result.rows];
205
+ }
206
+
207
+ /** Resize the terminal. */
208
+ async resize(cols: number, rows: number): Promise<void> {
209
+ await this.client.request("Terminal.resize", {
210
+ sessionId: this.sessionId,
211
+ cols,
212
+ rows,
213
+ });
214
+ }
215
+
216
+ /** Get server info and capabilities. */
217
+ async getInfo(): Promise<Record<string, any>> {
218
+ return this.client.request("Wrightty.getInfo");
219
+ }
220
+
221
+ // --- Recording ---
222
+
223
+ /** Start recording raw PTY I/O (asciicast v2 format). */
224
+ async startSessionRecording(includeInput = false): Promise<string> {
225
+ const result = await this.client.request("Recording.startSession", {
226
+ sessionId: this.sessionId,
227
+ includeInput,
228
+ });
229
+ return result.recordingId;
230
+ }
231
+
232
+ /** Stop a session recording and return asciicast data. */
233
+ async stopSessionRecording(recordingId: string): Promise<SessionRecordingData> {
234
+ return this.client.request("Recording.stopSession", { recordingId });
235
+ }
236
+
237
+ /** Start recording wrightty API calls as a replayable script. */
238
+ async startActionRecording(format: "python" | "json" | "cli" = "python"): Promise<string> {
239
+ const result = await this.client.request("Recording.startActions", {
240
+ sessionId: this.sessionId,
241
+ format,
242
+ });
243
+ return result.recordingId;
244
+ }
245
+
246
+ /** Stop action recording and return the generated script. */
247
+ async stopActionRecording(recordingId: string): Promise<ActionRecordingData> {
248
+ return this.client.request("Recording.stopActions", { recordingId });
249
+ }
250
+
251
+ /** Capture a single screen frame. */
252
+ async captureScreen(format: ScreenshotFormat = "svg"): Promise<Record<string, any>> {
253
+ return this.client.request("Recording.captureScreen", {
254
+ sessionId: this.sessionId,
255
+ format,
256
+ });
257
+ }
258
+ }
@@ -0,0 +1,105 @@
1
+ /** Wrightty protocol types — mirrors wrightty-protocol Rust crate. */
2
+
3
+ export interface ServerInfo {
4
+ version: string;
5
+ implementation: string;
6
+ capabilities: Capabilities;
7
+ }
8
+
9
+ export interface Capabilities {
10
+ screenshot: ScreenshotFormat[];
11
+ maxSessions: number;
12
+ supportsResize: boolean;
13
+ supportsScrollback: boolean;
14
+ supportsMouse: boolean;
15
+ supportsSessionCreate: boolean;
16
+ supportsColorPalette: boolean;
17
+ supportsRawOutput: boolean;
18
+ supportsShellIntegration: boolean;
19
+ events: string[];
20
+ }
21
+
22
+ export type ScreenshotFormat = "text" | "svg" | "png" | "json";
23
+
24
+ export interface SessionInfo {
25
+ sessionId: string;
26
+ title: string;
27
+ cwd?: string;
28
+ cols: number;
29
+ rows: number;
30
+ pid?: number;
31
+ running: boolean;
32
+ alternateScreen: boolean;
33
+ }
34
+
35
+ export type KeyInput = string | KeyEvent;
36
+
37
+ export interface KeyEvent {
38
+ key: string;
39
+ char?: string;
40
+ n?: number;
41
+ modifiers: string[];
42
+ }
43
+
44
+ export interface TextMatch {
45
+ text: string;
46
+ row: number;
47
+ col: number;
48
+ length: number;
49
+ }
50
+
51
+ export interface WaitForTextResult {
52
+ found: boolean;
53
+ matches: TextMatch[];
54
+ elapsed: number;
55
+ }
56
+
57
+ export interface ScreenshotResult {
58
+ format: ScreenshotFormat;
59
+ data: string;
60
+ width?: number;
61
+ height?: number;
62
+ }
63
+
64
+ export interface RecordingResult {
65
+ recordingId: string;
66
+ }
67
+
68
+ export interface SessionRecordingData {
69
+ format: string;
70
+ data: string;
71
+ duration: number;
72
+ events: number;
73
+ }
74
+
75
+ export interface ActionRecordingData {
76
+ format: string;
77
+ data: string;
78
+ actions: number;
79
+ duration: number;
80
+ }
81
+
82
+ export interface DiscoveredServer {
83
+ url: string;
84
+ port: number;
85
+ version: string;
86
+ implementation: string;
87
+ capabilities: Capabilities;
88
+ }
89
+
90
+ export interface ConnectOptions {
91
+ /** Server URL (default: auto-discover) */
92
+ url?: string;
93
+ /** Session ID (default: first available) */
94
+ sessionId?: string;
95
+ /** Connection timeout in ms (default: 5000) */
96
+ timeout?: number;
97
+ }
98
+
99
+ export interface SpawnOptions {
100
+ shell?: string;
101
+ cols?: number;
102
+ rows?: number;
103
+ cwd?: string;
104
+ serverUrl?: string;
105
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "declaration": true,
7
+ "strict": true,
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
@@ -0,0 +1,96 @@
1
+ # wrightty (Python SDK)
2
+
3
+ Python SDK for the [Wrightty](https://github.com/moejay/wrightty) terminal automation protocol.
4
+
5
+ Control any terminal emulator programmatically — send keystrokes, read the screen, take screenshots, and run commands over WebSocket JSON-RPC.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install wrightty
11
+ # with MCP server:
12
+ pip install wrightty[mcp]
13
+ ```
14
+
15
+ You also need a wrightty server running. Install it via:
16
+
17
+ ```bash
18
+ curl -fsSL https://raw.githubusercontent.com/moejay/wrightty/main/install.sh | sh
19
+ # or
20
+ cargo install wrightty
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```python
26
+ from wrightty import Terminal
27
+
28
+ # Connect to a running wrightty server (auto-discovers)
29
+ term = Terminal.connect()
30
+
31
+ # Run a command and get its output
32
+ output = term.run("cargo test")
33
+ print(output)
34
+
35
+ # Read the screen
36
+ screen = term.read_screen()
37
+
38
+ # Send keystrokes
39
+ term.send_keys("Ctrl+c")
40
+ term.send_keys("Escape", ":", "w", "q", "Enter")
41
+
42
+ # Wait for text to appear
43
+ term.wait_for("BUILD SUCCESS", timeout=60)
44
+
45
+ # Screenshots
46
+ svg = term.screenshot("svg")
47
+
48
+ # Recording
49
+ rec_id = term.start_session_recording()
50
+ term.run("make build")
51
+ result = term.stop_session_recording(rec_id)
52
+ open("build.cast", "w").write(result["data"]) # asciinema play
53
+
54
+ term.close()
55
+ ```
56
+
57
+ ## Starting a server
58
+
59
+ First start a wrightty terminal server:
60
+
61
+ ```bash
62
+ wrightty term --headless # virtual PTY, no GUI
63
+ wrightty term --bridge-tmux # attach to tmux
64
+ wrightty term --bridge-wezterm # attach to WezTerm
65
+ wrightty term --bridge-kitty # attach to Kitty
66
+ wrightty term --bridge-zellij # attach to Zellij
67
+ wrightty term --bridge-ghostty # attach to Ghostty
68
+ ```
69
+
70
+ Then connect from Python as shown above.
71
+
72
+ ## MCP server (for Claude, Cursor, etc.)
73
+
74
+ ```bash
75
+ pip install wrightty[mcp]
76
+ ```
77
+
78
+ Add to your MCP config:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "wrightty": {
84
+ "command": "python3",
85
+ "args": ["-m", "wrightty.mcp_server"],
86
+ "env": { "WRIGHTTY_SOCKET": "ws://127.0.0.1:9420" }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ See the [main repository](https://github.com/moejay/wrightty) for full docs.
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wrightty"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the Wrightty terminal automation protocol"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Moe Jay"}]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Terminals",
22
+ "Topic :: Software Development :: Testing",
23
+ "Intended Audience :: Developers",
24
+ ]
25
+ dependencies = [
26
+ "websockets>=12.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ cli = ["click>=8.0"]
31
+ mcp = ["mcp>=1.0"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/moejay/wrightty"
35
+ Repository = "https://github.com/moejay/wrightty"
36
+ Issues = "https://github.com/moejay/wrightty/issues"
37
+
38
+ [project.scripts]
39
+ wrightty-py = "wrightty.cli:main"
40
+
41
+ [project.entry-points."mcp.servers"]
42
+ wrightty = "wrightty.mcp_server:serve"
@@ -0,0 +1,6 @@
1
+ from wrightty.terminal import Terminal
2
+
3
+ discover = Terminal.discover
4
+
5
+ __all__ = ["Terminal", "discover"]
6
+ __version__ = "0.1.0"