@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,96 @@
1
+ #!/usr/bin/env python3
2
+ """record_session.py -- Start a session recording, run commands, save the asciicast file.
3
+
4
+ Demonstrates Wrightty's recording capabilities:
5
+ - Session recording produces an asciicast v2 (.cast) file
6
+ - Compatible with `asciinema play` for playback and sharing
7
+
8
+ Usage:
9
+ python examples/record_session.py
10
+
11
+ Prerequisites:
12
+ - wrightty-server running on ws://127.0.0.1:9420
13
+ Start with: cargo run -p wrightty-server
14
+
15
+ - Python SDK installed (from repo root):
16
+ pip install -e sdks/python
17
+
18
+ - Optional: asciinema installed to play back the recording
19
+ pip install asciinema or brew install asciinema
20
+ """
21
+
22
+ import json
23
+ import os
24
+
25
+ from wrightty import Terminal
26
+
27
+ OUTPUT_PATH = "/tmp/wrightty_demo.cast"
28
+
29
+
30
+ def main():
31
+ print("Spawning terminal session...")
32
+
33
+ with Terminal.spawn(cols=100, rows=30) as term:
34
+ info = term.get_info()
35
+ print(f"Connected to {info['implementation']} v{info['version']}")
36
+ print()
37
+
38
+ # --- Start session recording ---
39
+ print("Starting session recording...")
40
+ recording_id = term.start_session_recording(include_input=False)
41
+ print(f"Recording ID: {recording_id}")
42
+ print()
43
+
44
+ # --- Run some commands while recording ---
45
+ print("Running commands (these will be captured in the recording)...")
46
+
47
+ output = term.run("echo 'Hello from Wrightty!'")
48
+ print(f" echo output: {output.strip()}")
49
+
50
+ output = term.run("date")
51
+ print(f" date output: {output.strip()}")
52
+
53
+ output = term.run("uname -s")
54
+ print(f" uname output: {output.strip()}")
55
+
56
+ output = term.run("echo 'Recording complete.'")
57
+ print(f" final echo: {output.strip()}")
58
+ print()
59
+
60
+ # --- Stop the recording ---
61
+ print("Stopping recording...")
62
+ result = term.stop_session_recording(recording_id)
63
+
64
+ asciicast_data = result["data"]
65
+ duration = result.get("duration", 0)
66
+ event_count = result.get("events", 0)
67
+
68
+ print(f"Recorded {event_count} events over {duration:.1f}s")
69
+ print()
70
+
71
+ # --- Save the asciicast file ---
72
+ with open(OUTPUT_PATH, "w") as f:
73
+ f.write(asciicast_data)
74
+
75
+ file_size = os.path.getsize(OUTPUT_PATH)
76
+ print(f"Saved asciicast to {OUTPUT_PATH} ({file_size} bytes)")
77
+ print()
78
+
79
+ # Show the asciicast header so the user can inspect it
80
+ lines = asciicast_data.splitlines()
81
+ if lines:
82
+ header = json.loads(lines[0])
83
+ print("Asciicast header:")
84
+ print(f" version: {header.get('version')}")
85
+ print(f" width: {header.get('width')}")
86
+ print(f" height: {header.get('height')}")
87
+ if "title" in header:
88
+ print(f" title: {header['title']}")
89
+ print()
90
+
91
+ print(f"Done. To play back the recording:")
92
+ print(f" asciinema play {OUTPUT_PATH}")
93
+
94
+
95
+ if __name__ == "__main__":
96
+ main()
package/install.sh ADDED
@@ -0,0 +1,81 @@
1
+ #!/bin/sh
2
+ # Install wrightty — terminal automation CLI
3
+ # Usage: curl -fsSL https://raw.githubusercontent.com/moejay/wrightty/main/install.sh | sh
4
+ set -e
5
+
6
+ REPO="moejay/wrightty"
7
+ INSTALL_DIR="${WRIGHTTY_INSTALL_DIR:-/usr/local/bin}"
8
+
9
+ # Detect OS and architecture
10
+ OS="$(uname -s)"
11
+ ARCH="$(uname -m)"
12
+
13
+ case "$OS" in
14
+ Linux) OS_NAME="linux" ;;
15
+ Darwin) OS_NAME="macos" ;;
16
+ *)
17
+ echo "error: Unsupported OS: $OS"
18
+ echo "Build from source: cargo install --git https://github.com/$REPO wrightty"
19
+ exit 1
20
+ ;;
21
+ esac
22
+
23
+ case "$ARCH" in
24
+ x86_64|amd64) ARCH_NAME="x86_64" ;;
25
+ aarch64|arm64) ARCH_NAME="aarch64" ;;
26
+ *)
27
+ echo "error: Unsupported architecture: $ARCH"
28
+ echo "Build from source: cargo install --git https://github.com/$REPO wrightty"
29
+ exit 1
30
+ ;;
31
+ esac
32
+
33
+ ASSET_NAME="wrightty-${OS_NAME}-${ARCH_NAME}"
34
+
35
+ # Get latest release URL
36
+ if [ -n "$WRIGHTTY_VERSION" ]; then
37
+ TAG="v${WRIGHTTY_VERSION}"
38
+ URL="https://github.com/$REPO/releases/download/$TAG/${ASSET_NAME}.tar.gz"
39
+ else
40
+ URL="https://github.com/$REPO/releases/latest/download/${ASSET_NAME}.tar.gz"
41
+ fi
42
+
43
+ echo "Downloading wrightty for ${OS_NAME}/${ARCH_NAME}..."
44
+ TMPDIR="$(mktemp -d)"
45
+ trap 'rm -rf "$TMPDIR"' EXIT
46
+
47
+ if command -v curl >/dev/null 2>&1; then
48
+ curl -fsSL "$URL" -o "$TMPDIR/wrightty.tar.gz"
49
+ elif command -v wget >/dev/null 2>&1; then
50
+ wget -q "$URL" -O "$TMPDIR/wrightty.tar.gz"
51
+ else
52
+ echo "error: curl or wget required"
53
+ exit 1
54
+ fi
55
+
56
+ tar xzf "$TMPDIR/wrightty.tar.gz" -C "$TMPDIR"
57
+
58
+ # Install
59
+ if [ -w "$INSTALL_DIR" ]; then
60
+ mv "$TMPDIR/wrightty" "$INSTALL_DIR/wrightty"
61
+ else
62
+ echo "Installing to $INSTALL_DIR (requires sudo)..."
63
+ sudo mv "$TMPDIR/wrightty" "$INSTALL_DIR/wrightty"
64
+ fi
65
+
66
+ chmod +x "$INSTALL_DIR/wrightty"
67
+
68
+ echo ""
69
+ echo "wrightty installed to $INSTALL_DIR/wrightty"
70
+ echo ""
71
+ echo "Get started:"
72
+ echo " wrightty term --headless # start a headless terminal server"
73
+ echo " wrightty run \"echo hello\" # run a command"
74
+ echo " wrightty discover # find running servers"
75
+ echo ""
76
+ echo "Or bridge to your terminal:"
77
+ echo " wrightty term --bridge-tmux # if you use tmux"
78
+ echo " wrightty term --bridge-wezterm # if you use WezTerm"
79
+ echo " wrightty term --bridge-kitty # if you use Kitty"
80
+ echo " wrightty term --bridge-ghostty # if you use Ghostty"
81
+ echo " wrightty term --bridge-zellij # if you use Zellij"
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@moejay/wrightty",
3
+ "version": "0.0.0",
4
+ "description": "[![CI](https://github.com/moejay/wrightty/actions/workflows/ci.yml/badge.svg)](https://github.com/moejay/wrightty/actions/workflows/ci.yml) [![Release](https://github.com/moejay/wrightty/actions/workflows/release.yml/badge.svg)](https://github.com/moejay/wrightty/actions/workflows/release.yml) [![crates.io](https://img.shields.io/crates/v/wrightty.svg)](https://crates.io/crates/wrightty) [![PyPI](https://img.shields.io/pypi/v/wrightty.svg)](https://pypi.org/project/wrightty/) [![npm](https://img.shields.io/npm/v/@moejay/wrightty.svg)](https://www.npmjs.com/package/@moejay/wrightty) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)",
5
+ "homepage": "https://github.com/moejay/wrightty#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/moejay/wrightty/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/moejay/wrightty.git"
12
+ },
13
+ "license": "ISC",
14
+ "author": "",
15
+ "type": "commonjs",
16
+ "main": "index.js",
17
+ "directories": {
18
+ "example": "examples",
19
+ "test": "tests"
20
+ },
21
+ "scripts": {
22
+ "test": "echo \"Error: no test specified\" && exit 1"
23
+ }
24
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "wrightty",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "wrightty",
9
+ "version": "0.1.0",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "ws": "^8.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "@types/ws": "^8.0.0",
16
+ "typescript": "^5.0.0"
17
+ },
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ }
21
+ },
22
+ "node_modules/@types/node": {
23
+ "version": "25.5.2",
24
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
25
+ "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
26
+ "dev": true,
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "undici-types": "~7.18.0"
30
+ }
31
+ },
32
+ "node_modules/@types/ws": {
33
+ "version": "8.18.1",
34
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
35
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
36
+ "dev": true,
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@types/node": "*"
40
+ }
41
+ },
42
+ "node_modules/typescript": {
43
+ "version": "5.9.3",
44
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
45
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
46
+ "dev": true,
47
+ "license": "Apache-2.0",
48
+ "bin": {
49
+ "tsc": "bin/tsc",
50
+ "tsserver": "bin/tsserver"
51
+ },
52
+ "engines": {
53
+ "node": ">=14.17"
54
+ }
55
+ },
56
+ "node_modules/undici-types": {
57
+ "version": "7.18.2",
58
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
59
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
60
+ "dev": true,
61
+ "license": "MIT"
62
+ },
63
+ "node_modules/ws": {
64
+ "version": "8.20.0",
65
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
66
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
67
+ "license": "MIT",
68
+ "engines": {
69
+ "node": ">=10.0.0"
70
+ },
71
+ "peerDependencies": {
72
+ "bufferutil": "^4.0.1",
73
+ "utf-8-validate": ">=5.0.2"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "bufferutil": {
77
+ "optional": true
78
+ },
79
+ "utf-8-validate": {
80
+ "optional": true
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@moejay/wrightty",
3
+ "version": "0.1.0",
4
+ "description": "Node.js SDK for Wrightty terminal automation protocol",
5
+ "author": "Moe Jay",
6
+ "homepage": "https://github.com/moejay/wrightty",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/moejay/wrightty.git",
10
+ "directory": "sdks/node"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/moejay/wrightty/issues"
14
+ },
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "keywords": [
25
+ "terminal",
26
+ "automation",
27
+ "tui",
28
+ "playwright",
29
+ "testing",
30
+ "ai",
31
+ "agent"
32
+ ],
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "ws": "^8.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/ws": "^8.0.0",
39
+ "typescript": "^5.0.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ }
44
+ }
@@ -0,0 +1,94 @@
1
+ /** Low-level WebSocket JSON-RPC 2.0 client for the Wrightty protocol. */
2
+
3
+ import WebSocket from "ws";
4
+
5
+ export class WrighttyError extends Error {
6
+ constructor(
7
+ public code: number,
8
+ message: string,
9
+ ) {
10
+ super(`[${code}] ${message}`);
11
+ this.name = "WrighttyError";
12
+ }
13
+ }
14
+
15
+ export class WrighttyClient {
16
+ private ws: WebSocket;
17
+ private nextId = 1;
18
+ private pending = new Map<
19
+ number,
20
+ {
21
+ resolve: (value: any) => void;
22
+ reject: (reason: any) => void;
23
+ }
24
+ >();
25
+
26
+ private constructor(ws: WebSocket) {
27
+ this.ws = ws;
28
+
29
+ ws.on("message", (data: WebSocket.Data) => {
30
+ try {
31
+ const msg = JSON.parse(data.toString());
32
+ const entry = this.pending.get(msg.id);
33
+ if (!entry) return;
34
+ this.pending.delete(msg.id);
35
+
36
+ if (msg.error) {
37
+ entry.reject(
38
+ new WrighttyError(msg.error.code ?? -1, msg.error.message ?? "Unknown error"),
39
+ );
40
+ } else {
41
+ entry.resolve(msg.result);
42
+ }
43
+ } catch {
44
+ // Ignore malformed messages
45
+ }
46
+ });
47
+
48
+ ws.on("close", () => {
49
+ for (const [id, entry] of this.pending) {
50
+ entry.reject(new Error("Connection closed"));
51
+ this.pending.delete(id);
52
+ }
53
+ });
54
+ }
55
+
56
+ static connect(url: string, timeoutMs = 5000): Promise<WrighttyClient> {
57
+ return new Promise((resolve, reject) => {
58
+ const ws = new WebSocket(url);
59
+ const timer = setTimeout(() => {
60
+ ws.close();
61
+ reject(new Error(`Connection timeout after ${timeoutMs}ms: ${url}`));
62
+ }, timeoutMs);
63
+
64
+ ws.on("open", () => {
65
+ clearTimeout(timer);
66
+ resolve(new WrighttyClient(ws));
67
+ });
68
+
69
+ ws.on("error", (err) => {
70
+ clearTimeout(timer);
71
+ reject(err);
72
+ });
73
+ });
74
+ }
75
+
76
+ async request(method: string, params: Record<string, any> = {}): Promise<any> {
77
+ const id = this.nextId++;
78
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
79
+
80
+ return new Promise((resolve, reject) => {
81
+ this.pending.set(id, { resolve, reject });
82
+ this.ws.send(msg, (err) => {
83
+ if (err) {
84
+ this.pending.delete(id);
85
+ reject(err);
86
+ }
87
+ });
88
+ });
89
+ }
90
+
91
+ close(): void {
92
+ this.ws.close();
93
+ }
94
+ }
@@ -0,0 +1,19 @@
1
+ export { Terminal } from "./terminal";
2
+ export { WrighttyClient, WrighttyError } from "./client";
3
+ export type {
4
+ ServerInfo,
5
+ Capabilities,
6
+ ScreenshotFormat,
7
+ SessionInfo,
8
+ KeyInput,
9
+ KeyEvent,
10
+ TextMatch,
11
+ WaitForTextResult,
12
+ ScreenshotResult,
13
+ RecordingResult,
14
+ SessionRecordingData,
15
+ ActionRecordingData,
16
+ DiscoveredServer,
17
+ ConnectOptions,
18
+ SpawnOptions,
19
+ } from "./types";