@moejay/wrightty 0.0.0 → 0.1.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 (92) hide show
  1. package/dist/client.d.ts +14 -0
  2. package/dist/client.js +83 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +8 -0
  5. package/dist/terminal.d.ts +48 -0
  6. package/dist/terminal.js +210 -0
  7. package/dist/types.d.ts +90 -0
  8. package/dist/types.js +3 -0
  9. package/package.json +35 -15
  10. package/.github/workflows/ci.yml +0 -90
  11. package/.github/workflows/release.yml +0 -177
  12. package/Cargo.lock +0 -2662
  13. package/Cargo.toml +0 -38
  14. package/PROTOCOL.md +0 -1351
  15. package/README.md +0 -386
  16. package/agents/ceo/AGENTS.md +0 -24
  17. package/agents/ceo/HEARTBEAT.md +0 -72
  18. package/agents/ceo/SOUL.md +0 -33
  19. package/agents/ceo/TOOLS.md +0 -3
  20. package/agents/founding-engineer/AGENTS.md +0 -44
  21. package/crates/wrightty/Cargo.toml +0 -43
  22. package/crates/wrightty/src/client_cmds.rs +0 -366
  23. package/crates/wrightty/src/discover.rs +0 -78
  24. package/crates/wrightty/src/main.rs +0 -100
  25. package/crates/wrightty/src/server.rs +0 -100
  26. package/crates/wrightty/src/term.rs +0 -338
  27. package/crates/wrightty-bridge-ghostty/Cargo.toml +0 -27
  28. package/crates/wrightty-bridge-ghostty/src/ghostty.rs +0 -422
  29. package/crates/wrightty-bridge-ghostty/src/lib.rs +0 -2
  30. package/crates/wrightty-bridge-ghostty/src/main.rs +0 -146
  31. package/crates/wrightty-bridge-ghostty/src/rpc.rs +0 -307
  32. package/crates/wrightty-bridge-kitty/Cargo.toml +0 -26
  33. package/crates/wrightty-bridge-kitty/src/kitty.rs +0 -269
  34. package/crates/wrightty-bridge-kitty/src/lib.rs +0 -2
  35. package/crates/wrightty-bridge-kitty/src/main.rs +0 -124
  36. package/crates/wrightty-bridge-kitty/src/rpc.rs +0 -304
  37. package/crates/wrightty-bridge-tmux/Cargo.toml +0 -26
  38. package/crates/wrightty-bridge-tmux/src/lib.rs +0 -2
  39. package/crates/wrightty-bridge-tmux/src/main.rs +0 -119
  40. package/crates/wrightty-bridge-tmux/src/rpc.rs +0 -291
  41. package/crates/wrightty-bridge-tmux/src/tmux.rs +0 -215
  42. package/crates/wrightty-bridge-wezterm/Cargo.toml +0 -26
  43. package/crates/wrightty-bridge-wezterm/src/lib.rs +0 -2
  44. package/crates/wrightty-bridge-wezterm/src/main.rs +0 -119
  45. package/crates/wrightty-bridge-wezterm/src/rpc.rs +0 -339
  46. package/crates/wrightty-bridge-wezterm/src/wezterm.rs +0 -190
  47. package/crates/wrightty-bridge-zellij/Cargo.toml +0 -27
  48. package/crates/wrightty-bridge-zellij/src/lib.rs +0 -2
  49. package/crates/wrightty-bridge-zellij/src/main.rs +0 -125
  50. package/crates/wrightty-bridge-zellij/src/rpc.rs +0 -328
  51. package/crates/wrightty-bridge-zellij/src/zellij.rs +0 -199
  52. package/crates/wrightty-client/Cargo.toml +0 -16
  53. package/crates/wrightty-client/src/client.rs +0 -254
  54. package/crates/wrightty-client/src/lib.rs +0 -2
  55. package/crates/wrightty-core/Cargo.toml +0 -21
  56. package/crates/wrightty-core/src/input.rs +0 -212
  57. package/crates/wrightty-core/src/lib.rs +0 -4
  58. package/crates/wrightty-core/src/screen.rs +0 -325
  59. package/crates/wrightty-core/src/session.rs +0 -249
  60. package/crates/wrightty-core/src/session_manager.rs +0 -77
  61. package/crates/wrightty-protocol/Cargo.toml +0 -13
  62. package/crates/wrightty-protocol/src/error.rs +0 -8
  63. package/crates/wrightty-protocol/src/events.rs +0 -138
  64. package/crates/wrightty-protocol/src/lib.rs +0 -4
  65. package/crates/wrightty-protocol/src/methods.rs +0 -321
  66. package/crates/wrightty-protocol/src/types.rs +0 -201
  67. package/crates/wrightty-server/Cargo.toml +0 -23
  68. package/crates/wrightty-server/src/lib.rs +0 -2
  69. package/crates/wrightty-server/src/main.rs +0 -65
  70. package/crates/wrightty-server/src/rpc.rs +0 -455
  71. package/crates/wrightty-server/src/state.rs +0 -39
  72. package/examples/basic_command.py +0 -53
  73. package/examples/interactive_tui.py +0 -86
  74. package/examples/record_session.py +0 -96
  75. package/install.sh +0 -81
  76. package/sdks/node/package-lock.json +0 -85
  77. package/sdks/node/package.json +0 -44
  78. package/sdks/node/src/client.ts +0 -94
  79. package/sdks/node/src/index.ts +0 -19
  80. package/sdks/node/src/terminal.ts +0 -258
  81. package/sdks/node/src/types.ts +0 -105
  82. package/sdks/node/tsconfig.json +0 -17
  83. package/sdks/python/README.md +0 -96
  84. package/sdks/python/pyproject.toml +0 -42
  85. package/sdks/python/wrightty/__init__.py +0 -6
  86. package/sdks/python/wrightty/cli.py +0 -210
  87. package/sdks/python/wrightty/client.py +0 -136
  88. package/sdks/python/wrightty/mcp_server.py +0 -434
  89. package/sdks/python/wrightty/terminal.py +0 -333
  90. package/skills/wrightty/SKILL.md +0 -261
  91. package/src/lib.rs +0 -1
  92. package/tests/integration_test.rs +0 -618
@@ -1,105 +0,0 @@
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
- }
@@ -1,17 +0,0 @@
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
- }
@@ -1,96 +0,0 @@
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
@@ -1,42 +0,0 @@
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"
@@ -1,6 +0,0 @@
1
- from wrightty.terminal import Terminal
2
-
3
- discover = Terminal.discover
4
-
5
- __all__ = ["Terminal", "discover"]
6
- __version__ = "0.1.0"
@@ -1,210 +0,0 @@
1
- """Wrightty CLI — control terminals from the command line."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import sys
7
-
8
- import click
9
-
10
- from wrightty.terminal import Terminal
11
-
12
-
13
- @click.group()
14
- @click.option("--url", default=None, help="Wrightty server URL (default: auto-discover)")
15
- @click.option("--session", default=None, help="Session ID (default: auto-detect)")
16
- @click.pass_context
17
- def main(ctx, url, session):
18
- """Wrightty — Playwright for terminals."""
19
- ctx.ensure_object(dict)
20
- ctx.obj["url"] = url
21
- ctx.obj["session"] = session
22
-
23
-
24
- def _connect(ctx) -> Terminal:
25
- return Terminal.connect(ctx.obj["url"], ctx.obj["session"])
26
-
27
-
28
- @main.command()
29
- @click.argument("command")
30
- @click.option("--timeout", default=30, type=float, help="Timeout in seconds")
31
- @click.pass_context
32
- def run(ctx, command, timeout):
33
- """Run a command and print its output."""
34
- term = _connect(ctx)
35
- try:
36
- output = term.run(command, timeout=timeout)
37
- click.echo(output)
38
- finally:
39
- term.close()
40
-
41
-
42
- @main.command("read")
43
- @click.pass_context
44
- def read_screen(ctx):
45
- """Read the current terminal screen."""
46
- term = _connect(ctx)
47
- try:
48
- click.echo(term.read_screen())
49
- finally:
50
- term.close()
51
-
52
-
53
- @main.command("send-text")
54
- @click.argument("text")
55
- @click.pass_context
56
- def send_text(ctx, text):
57
- """Send raw text to the terminal."""
58
- term = _connect(ctx)
59
- try:
60
- # Interpret \\n as actual newlines.
61
- text = text.replace("\\n", "\n")
62
- term.send_text(text)
63
- finally:
64
- term.close()
65
-
66
-
67
- @main.command("send-keys")
68
- @click.argument("keys", nargs=-1, required=True)
69
- @click.pass_context
70
- def send_keys(ctx, keys):
71
- """Send keystrokes to the terminal.
72
-
73
- Examples:
74
- wrightty send-keys Ctrl+c
75
- wrightty send-keys Escape : w q Enter
76
- """
77
- term = _connect(ctx)
78
- try:
79
- term.send_keys(*keys)
80
- finally:
81
- term.close()
82
-
83
-
84
- @main.command("wait-for")
85
- @click.argument("pattern")
86
- @click.option("--timeout", default=30, type=float, help="Timeout in seconds")
87
- @click.option("--regex", is_flag=True, help="Treat pattern as regex")
88
- @click.pass_context
89
- def wait_for(ctx, pattern, timeout, regex):
90
- """Wait until text appears on screen."""
91
- term = _connect(ctx)
92
- try:
93
- screen = term.wait_for(pattern, timeout=timeout, regex=regex)
94
- click.echo(screen)
95
- except TimeoutError as e:
96
- click.echo(str(e), err=True)
97
- sys.exit(1)
98
- finally:
99
- term.close()
100
-
101
-
102
- @main.command()
103
- @click.option("--format", "fmt", default="svg", type=click.Choice(["text", "svg", "png"]))
104
- @click.option("--output", "-o", default=None, help="Output file (default: stdout)")
105
- @click.pass_context
106
- def screenshot(ctx, fmt, output):
107
- """Take a terminal screenshot."""
108
- term = _connect(ctx)
109
- try:
110
- data = term.screenshot(fmt)
111
- if output:
112
- mode = "wb" if fmt == "png" else "w"
113
- with open(output, mode) as f:
114
- f.write(data)
115
- click.echo(f"Screenshot saved to {output}")
116
- else:
117
- if fmt == "png":
118
- sys.stdout.buffer.write(data)
119
- else:
120
- click.echo(data)
121
- finally:
122
- term.close()
123
-
124
-
125
- @main.command()
126
- @click.pass_context
127
- def info(ctx):
128
- """Show server info and capabilities."""
129
- term = _connect(ctx)
130
- try:
131
- info = term.get_info()
132
- click.echo(json.dumps(info, indent=2))
133
- finally:
134
- term.close()
135
-
136
-
137
- @main.command()
138
- @click.pass_context
139
- def size(ctx):
140
- """Get terminal dimensions."""
141
- term = _connect(ctx)
142
- try:
143
- cols, rows = term.get_size()
144
- click.echo(f"{cols}x{rows}")
145
- finally:
146
- term.close()
147
-
148
-
149
- @main.command()
150
- def discover():
151
- """Discover running wrightty servers on ports 9420-9520."""
152
- servers = Terminal.discover()
153
- if not servers:
154
- click.echo("No wrightty servers found.")
155
- return
156
- for s in servers:
157
- click.echo(f" {s['url']} {s['implementation']} v{s['version']}")
158
-
159
-
160
- @main.command()
161
- @click.option("--output", "-o", default=None, help="Output file (default: recording.cast)")
162
- @click.option("--include-input", is_flag=True, help="Also record input keystrokes")
163
- @click.pass_context
164
- def record(ctx, output, include_input):
165
- """Record a terminal session (asciicast format). Press Ctrl+C to stop."""
166
- output = output or "recording.cast"
167
- term = _connect(ctx)
168
- try:
169
- rec_id = term.start_session_recording(include_input=include_input)
170
- click.echo(f"Recording... (press Ctrl+C to stop, saving to {output})")
171
- try:
172
- import signal
173
- signal.pause()
174
- except KeyboardInterrupt:
175
- pass
176
- result = term.stop_session_recording(rec_id)
177
- with open(output, "w") as f:
178
- f.write(result["data"])
179
- click.echo(f"Saved {result['events']} events, {result['duration']:.1f}s to {output}")
180
- finally:
181
- term.close()
182
-
183
-
184
- @main.command("record-actions")
185
- @click.option("--output", "-o", default=None, help="Output file")
186
- @click.option("--format", "fmt", default="python", type=click.Choice(["python", "json", "cli"]))
187
- @click.pass_context
188
- def record_actions(ctx, output, fmt):
189
- """Record wrightty actions as a replayable script. Press Ctrl+C to stop."""
190
- ext = {"python": ".py", "json": ".json", "cli": ".sh"}[fmt]
191
- output = output or f"recording{ext}"
192
- term = _connect(ctx)
193
- try:
194
- rec_id = term.start_action_recording(format=fmt)
195
- click.echo(f"Recording actions... (press Ctrl+C to stop, saving to {output})")
196
- try:
197
- import signal
198
- signal.pause()
199
- except KeyboardInterrupt:
200
- pass
201
- result = term.stop_action_recording(rec_id)
202
- with open(output, "w") as f:
203
- f.write(result["data"] if isinstance(result["data"], str) else json.dumps(result["data"], indent=2))
204
- click.echo(f"Saved {result['actions']} actions, {result['duration']:.1f}s to {output}")
205
- finally:
206
- term.close()
207
-
208
-
209
- if __name__ == "__main__":
210
- main()
@@ -1,136 +0,0 @@
1
- """Low-level WebSocket JSON-RPC client for the Wrightty protocol.
2
-
3
- Uses a raw socket WebSocket implementation to avoid version issues
4
- with the `websockets` library. Zero external dependencies.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import base64
10
- import hashlib
11
- import json
12
- import os
13
- import socket
14
- import struct
15
- from typing import Any
16
- from urllib.parse import urlparse
17
-
18
-
19
- class WrighttyClient:
20
- """Raw JSON-RPC client over WebSocket. No async, no dependencies."""
21
-
22
- def __init__(self, sock: socket.socket):
23
- self._sock = sock
24
- self._next_id = 1
25
-
26
- @classmethod
27
- def connect(cls, url: str = "ws://127.0.0.1:9420") -> WrighttyClient:
28
- parsed = urlparse(url)
29
- host = parsed.hostname or "127.0.0.1"
30
- port = parsed.port or 9420
31
-
32
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
33
- sock.connect((host, port))
34
- sock.settimeout(30)
35
-
36
- # WebSocket handshake.
37
- key = base64.b64encode(os.urandom(16)).decode()
38
- request = (
39
- f"GET / HTTP/1.1\r\n"
40
- f"Host: {host}:{port}\r\n"
41
- f"Connection: Upgrade\r\n"
42
- f"Upgrade: websocket\r\n"
43
- f"Sec-WebSocket-Version: 13\r\n"
44
- f"Sec-WebSocket-Key: {key}\r\n"
45
- f"\r\n"
46
- )
47
- sock.sendall(request.encode())
48
-
49
- # Read response headers.
50
- response = b""
51
- while b"\r\n\r\n" not in response:
52
- response += sock.recv(4096)
53
-
54
- if b"101" not in response:
55
- raise ConnectionError(f"WebSocket handshake failed: {response.decode()}")
56
-
57
- return cls(sock)
58
-
59
- def close(self):
60
- try:
61
- self._sock.close()
62
- except Exception:
63
- pass
64
-
65
- def request(self, method: str, params: dict[str, Any] | None = None) -> Any:
66
- req_id = self._next_id
67
- self._next_id += 1
68
-
69
- msg = {"jsonrpc": "2.0", "id": req_id, "method": method, "params": params or {}}
70
- self._send_frame(json.dumps(msg))
71
- raw = self._recv_frame()
72
- resp = json.loads(raw)
73
-
74
- if "error" in resp:
75
- err = resp["error"]
76
- raise WrighttyError(err.get("code", -1), err.get("message", "Unknown error"))
77
-
78
- return resp.get("result")
79
-
80
- def _send_frame(self, msg: str):
81
- """Send a masked WebSocket text frame."""
82
- payload = msg.encode()
83
- mask = os.urandom(4)
84
- frame = bytearray([0x81]) # FIN + text opcode
85
-
86
- length = len(payload)
87
- if length < 126:
88
- frame.append(0x80 | length)
89
- elif length < 65536:
90
- frame.append(0x80 | 126)
91
- frame.extend(struct.pack(">H", length))
92
- else:
93
- frame.append(0x80 | 127)
94
- frame.extend(struct.pack(">Q", length))
95
-
96
- frame.extend(mask)
97
- for i, b in enumerate(payload):
98
- frame.append(b ^ mask[i % 4])
99
- self._sock.sendall(bytes(frame))
100
-
101
- def _recv_frame(self) -> str:
102
- """Receive a WebSocket text frame."""
103
- header = self._recv_exact(2)
104
- length = header[1] & 0x7F
105
-
106
- if length == 126:
107
- length = struct.unpack(">H", self._recv_exact(2))[0]
108
- elif length == 127:
109
- length = struct.unpack(">Q", self._recv_exact(8))[0]
110
-
111
- # Server frames are not masked.
112
- payload = self._recv_exact(length)
113
- return payload.decode()
114
-
115
- def _recv_exact(self, n: int) -> bytes:
116
- """Read exactly n bytes."""
117
- data = b""
118
- while len(data) < n:
119
- chunk = self._sock.recv(n - len(data))
120
- if not chunk:
121
- raise ConnectionError("Connection closed")
122
- data += chunk
123
- return data
124
-
125
- def __enter__(self):
126
- return self
127
-
128
- def __exit__(self, *args):
129
- self.close()
130
-
131
-
132
- class WrighttyError(Exception):
133
- def __init__(self, code: int, message: str):
134
- self.code = code
135
- self.message = message
136
- super().__init__(f"[{code}] {message}")