@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,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()