@moejay/wrightty 0.0.0 → 0.1.1

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 (94) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.js +144 -0
  3. package/dist/client.d.ts +14 -0
  4. package/dist/client.js +83 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.js +8 -0
  7. package/dist/terminal.d.ts +48 -0
  8. package/dist/terminal.js +210 -0
  9. package/dist/types.d.ts +90 -0
  10. package/dist/types.js +3 -0
  11. package/package.json +38 -15
  12. package/.github/workflows/ci.yml +0 -90
  13. package/.github/workflows/release.yml +0 -177
  14. package/Cargo.lock +0 -2662
  15. package/Cargo.toml +0 -38
  16. package/PROTOCOL.md +0 -1351
  17. package/README.md +0 -386
  18. package/agents/ceo/AGENTS.md +0 -24
  19. package/agents/ceo/HEARTBEAT.md +0 -72
  20. package/agents/ceo/SOUL.md +0 -33
  21. package/agents/ceo/TOOLS.md +0 -3
  22. package/agents/founding-engineer/AGENTS.md +0 -44
  23. package/crates/wrightty/Cargo.toml +0 -43
  24. package/crates/wrightty/src/client_cmds.rs +0 -366
  25. package/crates/wrightty/src/discover.rs +0 -78
  26. package/crates/wrightty/src/main.rs +0 -100
  27. package/crates/wrightty/src/server.rs +0 -100
  28. package/crates/wrightty/src/term.rs +0 -338
  29. package/crates/wrightty-bridge-ghostty/Cargo.toml +0 -27
  30. package/crates/wrightty-bridge-ghostty/src/ghostty.rs +0 -422
  31. package/crates/wrightty-bridge-ghostty/src/lib.rs +0 -2
  32. package/crates/wrightty-bridge-ghostty/src/main.rs +0 -146
  33. package/crates/wrightty-bridge-ghostty/src/rpc.rs +0 -307
  34. package/crates/wrightty-bridge-kitty/Cargo.toml +0 -26
  35. package/crates/wrightty-bridge-kitty/src/kitty.rs +0 -269
  36. package/crates/wrightty-bridge-kitty/src/lib.rs +0 -2
  37. package/crates/wrightty-bridge-kitty/src/main.rs +0 -124
  38. package/crates/wrightty-bridge-kitty/src/rpc.rs +0 -304
  39. package/crates/wrightty-bridge-tmux/Cargo.toml +0 -26
  40. package/crates/wrightty-bridge-tmux/src/lib.rs +0 -2
  41. package/crates/wrightty-bridge-tmux/src/main.rs +0 -119
  42. package/crates/wrightty-bridge-tmux/src/rpc.rs +0 -291
  43. package/crates/wrightty-bridge-tmux/src/tmux.rs +0 -215
  44. package/crates/wrightty-bridge-wezterm/Cargo.toml +0 -26
  45. package/crates/wrightty-bridge-wezterm/src/lib.rs +0 -2
  46. package/crates/wrightty-bridge-wezterm/src/main.rs +0 -119
  47. package/crates/wrightty-bridge-wezterm/src/rpc.rs +0 -339
  48. package/crates/wrightty-bridge-wezterm/src/wezterm.rs +0 -190
  49. package/crates/wrightty-bridge-zellij/Cargo.toml +0 -27
  50. package/crates/wrightty-bridge-zellij/src/lib.rs +0 -2
  51. package/crates/wrightty-bridge-zellij/src/main.rs +0 -125
  52. package/crates/wrightty-bridge-zellij/src/rpc.rs +0 -328
  53. package/crates/wrightty-bridge-zellij/src/zellij.rs +0 -199
  54. package/crates/wrightty-client/Cargo.toml +0 -16
  55. package/crates/wrightty-client/src/client.rs +0 -254
  56. package/crates/wrightty-client/src/lib.rs +0 -2
  57. package/crates/wrightty-core/Cargo.toml +0 -21
  58. package/crates/wrightty-core/src/input.rs +0 -212
  59. package/crates/wrightty-core/src/lib.rs +0 -4
  60. package/crates/wrightty-core/src/screen.rs +0 -325
  61. package/crates/wrightty-core/src/session.rs +0 -249
  62. package/crates/wrightty-core/src/session_manager.rs +0 -77
  63. package/crates/wrightty-protocol/Cargo.toml +0 -13
  64. package/crates/wrightty-protocol/src/error.rs +0 -8
  65. package/crates/wrightty-protocol/src/events.rs +0 -138
  66. package/crates/wrightty-protocol/src/lib.rs +0 -4
  67. package/crates/wrightty-protocol/src/methods.rs +0 -321
  68. package/crates/wrightty-protocol/src/types.rs +0 -201
  69. package/crates/wrightty-server/Cargo.toml +0 -23
  70. package/crates/wrightty-server/src/lib.rs +0 -2
  71. package/crates/wrightty-server/src/main.rs +0 -65
  72. package/crates/wrightty-server/src/rpc.rs +0 -455
  73. package/crates/wrightty-server/src/state.rs +0 -39
  74. package/examples/basic_command.py +0 -53
  75. package/examples/interactive_tui.py +0 -86
  76. package/examples/record_session.py +0 -96
  77. package/install.sh +0 -81
  78. package/sdks/node/package-lock.json +0 -85
  79. package/sdks/node/package.json +0 -44
  80. package/sdks/node/src/client.ts +0 -94
  81. package/sdks/node/src/index.ts +0 -19
  82. package/sdks/node/src/terminal.ts +0 -258
  83. package/sdks/node/src/types.ts +0 -105
  84. package/sdks/node/tsconfig.json +0 -17
  85. package/sdks/python/README.md +0 -96
  86. package/sdks/python/pyproject.toml +0 -42
  87. package/sdks/python/wrightty/__init__.py +0 -6
  88. package/sdks/python/wrightty/cli.py +0 -210
  89. package/sdks/python/wrightty/client.py +0 -136
  90. package/sdks/python/wrightty/mcp_server.py +0 -434
  91. package/sdks/python/wrightty/terminal.py +0 -333
  92. package/skills/wrightty/SKILL.md +0 -261
  93. package/src/lib.rs +0 -1
  94. package/tests/integration_test.rs +0 -618
@@ -1,434 +0,0 @@
1
- """Wrightty MCP Server — expose terminal control as tools for AI agents.
2
-
3
- Run with:
4
- python -m wrightty.mcp_server
5
- # or via MCP config in Claude/Cursor/etc.
6
-
7
- This exposes the following tools to AI agents:
8
- - run_command: Run a shell command and return output
9
- - read_terminal: Read the current terminal screen
10
- - send_keys: Send keystrokes (for TUI apps)
11
- - screenshot: Take a terminal screenshot (SVG for rendering)
12
- - wait_for_text: Wait until specific text appears
13
- - terminal_info: Get terminal info and dimensions
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- import asyncio
19
- import json
20
- import os
21
- import re
22
- import time
23
- from typing import Any
24
-
25
- from mcp.server import Server
26
- from mcp.server.stdio import stdio_server
27
- from mcp.types import TextContent, ImageContent, Tool
28
-
29
- from wrightty.client import WrighttyClient
30
-
31
-
32
- # Global client state.
33
- _client: WrighttyClient | None = None
34
- _session_id: str = "0"
35
- _prompt_pattern = r"[$#>%]\s*$"
36
-
37
-
38
- def get_client() -> WrighttyClient:
39
- global _client
40
- if _client is None:
41
- url = os.environ.get("WRIGHTTY_SOCKET", "ws://127.0.0.1:9420")
42
- _client = WrighttyClient.connect(url)
43
- return _client
44
-
45
-
46
- def read_screen() -> str:
47
- client = get_client()
48
- result = client.request("Screen.getText", {"sessionId": _session_id})
49
- return result["text"]
50
-
51
-
52
- async def wait_for_prompt(timeout: float = 10) -> str:
53
- deadline = time.monotonic() + timeout
54
- while time.monotonic() < deadline:
55
- screen = await asyncio.to_thread(read_screen)
56
- if re.search(_prompt_pattern, screen):
57
- return screen
58
- await asyncio.sleep(0.2)
59
- return await asyncio.to_thread(read_screen)
60
-
61
-
62
- app = Server("wrightty")
63
-
64
-
65
- @app.list_tools()
66
- async def list_tools() -> list[Tool]:
67
- return [
68
- Tool(
69
- name="run_command",
70
- description=(
71
- "Run a shell command in the terminal and return its output. "
72
- "The command is typed into a real terminal, executed, and the output "
73
- "is captured after the command completes (when the prompt returns). "
74
- "Use this for any shell operation: building, testing, file manipulation, git, etc."
75
- ),
76
- inputSchema={
77
- "type": "object",
78
- "properties": {
79
- "command": {
80
- "type": "string",
81
- "description": "The shell command to run",
82
- },
83
- "timeout": {
84
- "type": "number",
85
- "description": "Max seconds to wait for the command to finish (default: 30)",
86
- "default": 30,
87
- },
88
- },
89
- "required": ["command"],
90
- },
91
- ),
92
- Tool(
93
- name="read_terminal",
94
- description=(
95
- "Read the current visible content of the terminal screen. "
96
- "Returns the text currently displayed, including any running program's output. "
97
- "Useful for checking the state of long-running processes, TUI apps, or "
98
- "reading content that was printed before."
99
- ),
100
- inputSchema={
101
- "type": "object",
102
- "properties": {},
103
- },
104
- ),
105
- Tool(
106
- name="send_keys",
107
- description=(
108
- "Send keystrokes to the terminal. Use this for interactive programs like vim, "
109
- "htop, less, or any TUI application. Supports special keys and modifiers.\n\n"
110
- "Key names: Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, "
111
- "ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, F1-F12\n"
112
- "Modifiers: Ctrl+c, Alt+x, Shift+Tab\n"
113
- "Single characters: a, b, 1, /, etc."
114
- ),
115
- inputSchema={
116
- "type": "object",
117
- "properties": {
118
- "keys": {
119
- "type": "array",
120
- "items": {"type": "string"},
121
- "description": 'List of keys to send, e.g. ["Escape", ":", "w", "q", "Enter"]',
122
- },
123
- },
124
- "required": ["keys"],
125
- },
126
- ),
127
- Tool(
128
- name="send_text",
129
- description=(
130
- "Send raw text to the terminal without any key interpretation. "
131
- "Use \\n for newline. Useful for pasting content or sending multi-line input."
132
- ),
133
- inputSchema={
134
- "type": "object",
135
- "properties": {
136
- "text": {
137
- "type": "string",
138
- "description": "The text to send",
139
- },
140
- },
141
- "required": ["text"],
142
- },
143
- ),
144
- Tool(
145
- name="screenshot",
146
- description=(
147
- "Take a screenshot of the terminal. Returns an SVG image showing the terminal "
148
- "with colors, fonts, and styling. Useful for understanding visual layout of TUI apps."
149
- ),
150
- inputSchema={
151
- "type": "object",
152
- "properties": {
153
- "format": {
154
- "type": "string",
155
- "enum": ["text", "svg"],
156
- "default": "svg",
157
- "description": "Screenshot format: 'text' for plain text, 'svg' for styled image",
158
- },
159
- },
160
- },
161
- ),
162
- Tool(
163
- name="wait_for_text",
164
- description=(
165
- "Wait until specific text appears on the terminal screen. "
166
- "Blocks until the text is found or timeout is reached. "
167
- "Useful for waiting for compilation, test results, prompts, etc."
168
- ),
169
- inputSchema={
170
- "type": "object",
171
- "properties": {
172
- "pattern": {
173
- "type": "string",
174
- "description": "Text to wait for",
175
- },
176
- "timeout": {
177
- "type": "number",
178
- "description": "Max seconds to wait (default: 30)",
179
- "default": 30,
180
- },
181
- "regex": {
182
- "type": "boolean",
183
- "description": "Treat pattern as regex (default: false)",
184
- "default": False,
185
- },
186
- },
187
- "required": ["pattern"],
188
- },
189
- ),
190
- Tool(
191
- name="terminal_info",
192
- description="Get terminal information: dimensions, server version, capabilities.",
193
- inputSchema={
194
- "type": "object",
195
- "properties": {},
196
- },
197
- ),
198
- Tool(
199
- name="start_recording",
200
- description=(
201
- "Start recording the terminal session. Records raw PTY I/O in asciicast v2 format "
202
- "(compatible with asciinema). Also optionally records all wrightty actions as a "
203
- "replayable Python script. Returns recording IDs to pass to stop_recording."
204
- ),
205
- inputSchema={
206
- "type": "object",
207
- "properties": {
208
- "include_input": {
209
- "type": "boolean",
210
- "description": "Also record input keystrokes (default: false)",
211
- "default": False,
212
- },
213
- "record_actions": {
214
- "type": "boolean",
215
- "description": "Also record API actions as a Python script (default: true)",
216
- "default": True,
217
- },
218
- },
219
- },
220
- ),
221
- Tool(
222
- name="stop_recording",
223
- description=(
224
- "Stop recording and return the session recording (asciicast) and/or action script. "
225
- "The asciicast data can be saved to a .cast file and played with asciinema."
226
- ),
227
- inputSchema={
228
- "type": "object",
229
- "properties": {
230
- "session_recording_id": {
231
- "type": "string",
232
- "description": "Recording ID from start_recording (session)",
233
- },
234
- "action_recording_id": {
235
- "type": "string",
236
- "description": "Recording ID from start_recording (actions)",
237
- },
238
- },
239
- },
240
- ),
241
- Tool(
242
- name="capture_screen_frame",
243
- description=(
244
- "Capture a single screen frame as SVG. Use this to take snapshots at key moments "
245
- "during a session. Each call returns one frame with a timestamp."
246
- ),
247
- inputSchema={
248
- "type": "object",
249
- "properties": {
250
- "format": {
251
- "type": "string",
252
- "enum": ["svg", "text"],
253
- "default": "svg",
254
- },
255
- },
256
- },
257
- ),
258
- ]
259
-
260
-
261
- @app.call_tool()
262
- async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent | ImageContent]:
263
- client = get_client()
264
-
265
- if name == "run_command":
266
- command = arguments["command"]
267
- timeout = arguments.get("timeout", 30)
268
-
269
- # Send command.
270
- await asyncio.to_thread(
271
- client.request, "Input.sendText", {"sessionId": _session_id, "text": command + "\n"}
272
- )
273
-
274
- # Wait for prompt to return.
275
- deadline = time.monotonic() + timeout
276
- while time.monotonic() < deadline:
277
- screen = await asyncio.to_thread(read_screen)
278
- lines = screen.strip().split("\n")
279
- if lines and re.search(_prompt_pattern, lines[-1]) and command not in lines[-1]:
280
- break
281
- await asyncio.sleep(0.3)
282
-
283
- # Read final screen and extract output.
284
- screen = await asyncio.to_thread(read_screen)
285
- lines = screen.strip().split("\n")
286
-
287
- output_lines = []
288
- found_cmd = False
289
- for line in lines:
290
- if not found_cmd:
291
- if command in line:
292
- found_cmd = True
293
- continue
294
- if re.search(_prompt_pattern, line):
295
- break
296
- output_lines.append(line)
297
-
298
- output = "\n".join(output_lines) if output_lines else screen
299
- return [TextContent(type="text", text=output)]
300
-
301
- elif name == "read_terminal":
302
- screen = await asyncio.to_thread(read_screen)
303
- return [TextContent(type="text", text=screen)]
304
-
305
- elif name == "send_keys":
306
- keys = arguments["keys"]
307
- await asyncio.to_thread(
308
- client.request, "Input.sendKeys", {"sessionId": _session_id, "keys": keys}
309
- )
310
- await asyncio.sleep(0.3)
311
- screen = await asyncio.to_thread(read_screen)
312
- return [TextContent(type="text", text=screen)]
313
-
314
- elif name == "send_text":
315
- text = arguments["text"]
316
- await asyncio.to_thread(
317
- client.request, "Input.sendText", {"sessionId": _session_id, "text": text}
318
- )
319
- await asyncio.sleep(0.3)
320
- screen = await asyncio.to_thread(read_screen)
321
- return [TextContent(type="text", text=screen)]
322
-
323
- elif name == "screenshot":
324
- fmt = arguments.get("format", "svg")
325
- result = await asyncio.to_thread(
326
- client.request, "Screen.screenshot", {"sessionId": _session_id, "format": fmt}
327
- )
328
- return [TextContent(type="text", text=result["data"])]
329
-
330
- elif name == "wait_for_text":
331
- pattern = arguments["pattern"]
332
- timeout = arguments.get("timeout", 30)
333
- is_regex = arguments.get("regex", False)
334
-
335
- deadline = time.monotonic() + timeout
336
- while time.monotonic() < deadline:
337
- screen = await asyncio.to_thread(read_screen)
338
- if is_regex:
339
- if re.search(pattern, screen):
340
- return [TextContent(type="text", text=screen)]
341
- else:
342
- if pattern in screen:
343
- return [TextContent(type="text", text=screen)]
344
- await asyncio.sleep(0.3)
345
-
346
- return [TextContent(type="text", text=f"Timeout: '{pattern}' not found after {timeout}s")]
347
-
348
- elif name == "terminal_info":
349
- info = await asyncio.to_thread(client.request, "Wrightty.getInfo")
350
- size = await asyncio.to_thread(
351
- client.request, "Terminal.getSize", {"sessionId": _session_id}
352
- )
353
- info["size"] = size
354
- return [TextContent(type="text", text=json.dumps(info, indent=2))]
355
-
356
- elif name == "start_recording":
357
- results = {}
358
-
359
- include_input = arguments.get("include_input", False)
360
- result = await asyncio.to_thread(
361
- client.request,
362
- "Recording.startSession",
363
- {"sessionId": _session_id, "includeInput": include_input},
364
- )
365
- results["sessionRecordingId"] = result["recordingId"]
366
-
367
- if arguments.get("record_actions", True):
368
- result = await asyncio.to_thread(
369
- client.request,
370
- "Recording.startActions",
371
- {"sessionId": _session_id, "format": "python"},
372
- )
373
- results["actionRecordingId"] = result["recordingId"]
374
-
375
- return [TextContent(type="text", text=json.dumps(results, indent=2))]
376
-
377
- elif name == "stop_recording":
378
- results = {}
379
-
380
- session_id = arguments.get("session_recording_id")
381
- if session_id:
382
- result = await asyncio.to_thread(
383
- client.request, "Recording.stopSession", {"recordingId": session_id}
384
- )
385
- results["session"] = {
386
- "format": result.get("format"),
387
- "duration": result.get("duration"),
388
- "events": result.get("events"),
389
- "data_length": len(result.get("data", "")),
390
- }
391
- import tempfile
392
- cast_file = tempfile.NamedTemporaryFile(suffix=".cast", delete=False, mode="w")
393
- cast_file.write(result["data"])
394
- cast_file.close()
395
- results["session"]["file"] = cast_file.name
396
-
397
- action_id = arguments.get("action_recording_id")
398
- if action_id:
399
- result = await asyncio.to_thread(
400
- client.request, "Recording.stopActions", {"recordingId": action_id}
401
- )
402
- results["actions"] = {
403
- "format": result.get("format"),
404
- "actions": result.get("actions"),
405
- "duration": result.get("duration"),
406
- "script": result.get("data"),
407
- }
408
-
409
- return [TextContent(type="text", text=json.dumps(results, indent=2))]
410
-
411
- elif name == "capture_screen_frame":
412
- fmt = arguments.get("format", "svg")
413
- result = await asyncio.to_thread(
414
- client.request,
415
- "Recording.captureScreen",
416
- {"sessionId": _session_id, "format": fmt},
417
- )
418
- return [TextContent(type="text", text=result.get("data", ""))]
419
-
420
- else:
421
- return [TextContent(type="text", text=f"Unknown tool: {name}")]
422
-
423
-
424
- async def serve():
425
- async with stdio_server() as (read_stream, write_stream):
426
- await app.run(read_stream, write_stream, app.create_initialization_options())
427
-
428
-
429
- def main():
430
- asyncio.run(serve())
431
-
432
-
433
- if __name__ == "__main__":
434
- main()