cua-computer 0.2.9__tar.gz → 0.2.11__tar.gz
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.
- {cua_computer-0.2.9 → cua_computer-0.2.11}/PKG-INFO +1 -1
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/computer.py +36 -13
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/interface/base.py +67 -1
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/interface/factory.py +6 -3
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/interface/linux.py +65 -6
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/interface/macos.py +64 -5
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/interface/models.py +3 -0
- cua_computer-0.2.11/computer/interface/windows.py +687 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/base.py +1 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/cloud/provider.py +3 -3
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/factory.py +22 -0
- cua_computer-0.2.11/computer/providers/winsandbox/__init__.py +11 -0
- cua_computer-0.2.11/computer/providers/winsandbox/provider.py +468 -0
- cua_computer-0.2.11/computer/providers/winsandbox/setup_script.ps1 +124 -0
- cua_computer-0.2.11/computer/ui/__main__.py +15 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/ui/gradio/app.py +70 -10
- {cua_computer-0.2.9 → cua_computer-0.2.11}/pyproject.toml +3 -3
- {cua_computer-0.2.9 → cua_computer-0.2.11}/README.md +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/__init__.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/diorama_computer.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/helpers.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/interface/__init__.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/logger.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/models.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/__init__.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/cloud/__init__.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/lume/__init__.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/lume/provider.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/lume_api.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/lumier/__init__.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/providers/lumier/provider.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/telemetry.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/ui/__init__.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/ui/gradio/__init__.py +0 -0
- {cua_computer-0.2.9 → cua_computer-0.2.11}/computer/utils.py +0 -0
@@ -106,7 +106,15 @@ class Computer:
|
|
106
106
|
# The default is currently to use non-ephemeral storage
|
107
107
|
if storage and ephemeral and storage != "ephemeral":
|
108
108
|
raise ValueError("Storage path and ephemeral flag cannot be used together")
|
109
|
-
|
109
|
+
|
110
|
+
# Windows Sandbox always uses ephemeral storage
|
111
|
+
if self.provider_type == VMProviderType.WINSANDBOX:
|
112
|
+
if not ephemeral and storage != None and storage != "ephemeral":
|
113
|
+
self.logger.warning("Windows Sandbox storage is always ephemeral. Setting ephemeral=True.")
|
114
|
+
self.ephemeral = True
|
115
|
+
self.storage = "ephemeral"
|
116
|
+
else:
|
117
|
+
self.storage = "ephemeral" if ephemeral else storage
|
110
118
|
|
111
119
|
# For Lumier provider, store the first shared directory path to use
|
112
120
|
# for VM file sharing
|
@@ -181,24 +189,25 @@ class Computer:
|
|
181
189
|
self.logger.debug("Telemetry disabled - skipping initialization tracking")
|
182
190
|
|
183
191
|
async def __aenter__(self):
|
184
|
-
"""
|
192
|
+
"""Start the computer."""
|
193
|
+
await self.run()
|
185
194
|
return self
|
186
195
|
|
187
196
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
188
|
-
"""
|
189
|
-
|
197
|
+
"""Stop the computer."""
|
198
|
+
await self.disconnect()
|
190
199
|
|
191
200
|
def __enter__(self):
|
192
|
-
"""
|
193
|
-
# Run the event loop to call the async
|
201
|
+
"""Start the computer."""
|
202
|
+
# Run the event loop to call the async enter method
|
194
203
|
loop = asyncio.get_event_loop()
|
195
|
-
loop.run_until_complete(self.
|
204
|
+
loop.run_until_complete(self.__aenter__())
|
196
205
|
return self
|
197
206
|
|
198
207
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
199
|
-
"""
|
200
|
-
|
201
|
-
|
208
|
+
"""Stop the computer."""
|
209
|
+
loop = asyncio.get_event_loop()
|
210
|
+
loop.run_until_complete(self.__aexit__(exc_type, exc_val, exc_tb))
|
202
211
|
|
203
212
|
async def run(self) -> Optional[str]:
|
204
213
|
"""Initialize the VM and computer interface."""
|
@@ -284,6 +293,15 @@ class Computer:
|
|
284
293
|
api_key=self.api_key,
|
285
294
|
verbose=verbose,
|
286
295
|
)
|
296
|
+
elif self.provider_type == VMProviderType.WINSANDBOX:
|
297
|
+
self.config.vm_provider = VMProviderFactory.create_provider(
|
298
|
+
self.provider_type,
|
299
|
+
port=port,
|
300
|
+
host=host,
|
301
|
+
storage=storage,
|
302
|
+
verbose=verbose,
|
303
|
+
ephemeral=ephemeral,
|
304
|
+
)
|
287
305
|
else:
|
288
306
|
raise ValueError(f"Unsupported provider type: {self.provider_type}")
|
289
307
|
self._provider_context = await self.config.vm_provider.__aenter__()
|
@@ -382,7 +400,6 @@ class Computer:
|
|
382
400
|
# Wait for VM to be ready with a valid IP address
|
383
401
|
self.logger.info("Waiting for VM to be ready with a valid IP address...")
|
384
402
|
try:
|
385
|
-
# Increased values for Lumier provider which needs more time for initial setup
|
386
403
|
if self.provider_type == VMProviderType.LUMIER:
|
387
404
|
max_retries = 60 # Increased for Lumier VM startup which takes longer
|
388
405
|
retry_delay = 3 # 3 seconds between retries for Lumier
|
@@ -473,9 +490,14 @@ class Computer:
|
|
473
490
|
duration_ms = (time.time() - start_time) * 1000
|
474
491
|
self.logger.debug(f"Computer initialization took {duration_ms:.2f}ms")
|
475
492
|
return
|
493
|
+
|
494
|
+
async def disconnect(self) -> None:
|
495
|
+
"""Disconnect from the computer's WebSocket interface."""
|
496
|
+
if self._interface:
|
497
|
+
self._interface.close()
|
476
498
|
|
477
499
|
async def stop(self) -> None:
|
478
|
-
"""
|
500
|
+
"""Disconnect from the computer's WebSocket interface and stop the computer."""
|
479
501
|
start_time = time.time()
|
480
502
|
|
481
503
|
try:
|
@@ -496,6 +518,7 @@ class Computer:
|
|
496
518
|
await self.config.vm_provider.__aexit__(None, None, None)
|
497
519
|
self._provider_context = None
|
498
520
|
|
521
|
+
await self.disconnect()
|
499
522
|
self.logger.info("Computer stopped")
|
500
523
|
except Exception as e:
|
501
524
|
self.logger.debug(f"Error during cleanup: {e}") # Log as debug since this might be expected
|
@@ -506,7 +529,7 @@ class Computer:
|
|
506
529
|
return
|
507
530
|
|
508
531
|
# @property
|
509
|
-
async def get_ip(self, max_retries: int = 15, retry_delay: int =
|
532
|
+
async def get_ip(self, max_retries: int = 15, retry_delay: int = 3) -> str:
|
510
533
|
"""Get the IP address of the VM or localhost if using host computer server.
|
511
534
|
|
512
535
|
This method delegates to the provider's get_ip method, which waits indefinitely
|
@@ -3,6 +3,7 @@
|
|
3
3
|
from abc import ABC, abstractmethod
|
4
4
|
from typing import Optional, Dict, Any, Tuple, List
|
5
5
|
from ..logger import Logger, LogLevel
|
6
|
+
from .models import MouseButton
|
6
7
|
|
7
8
|
|
8
9
|
class BaseComputerInterface(ABC):
|
@@ -51,6 +52,16 @@ class BaseComputerInterface(ABC):
|
|
51
52
|
self.close()
|
52
53
|
|
53
54
|
# Mouse Actions
|
55
|
+
@abstractmethod
|
56
|
+
async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: "MouseButton" = "left") -> None:
|
57
|
+
"""Press and hold a mouse button."""
|
58
|
+
pass
|
59
|
+
|
60
|
+
@abstractmethod
|
61
|
+
async def mouse_up(self, x: Optional[int] = None, y: Optional[int] = None, button: "MouseButton" = "left") -> None:
|
62
|
+
"""Release a mouse button."""
|
63
|
+
pass
|
64
|
+
|
54
65
|
@abstractmethod
|
55
66
|
async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> None:
|
56
67
|
"""Perform a left click."""
|
@@ -95,6 +106,16 @@ class BaseComputerInterface(ABC):
|
|
95
106
|
pass
|
96
107
|
|
97
108
|
# Keyboard Actions
|
109
|
+
@abstractmethod
|
110
|
+
async def key_down(self, key: str) -> None:
|
111
|
+
"""Press and hold a key."""
|
112
|
+
pass
|
113
|
+
|
114
|
+
@abstractmethod
|
115
|
+
async def key_up(self, key: str) -> None:
|
116
|
+
"""Release a key."""
|
117
|
+
pass
|
118
|
+
|
98
119
|
@abstractmethod
|
99
120
|
async def type_text(self, text: str) -> None:
|
100
121
|
"""Type the specified text."""
|
@@ -111,6 +132,11 @@ class BaseComputerInterface(ABC):
|
|
111
132
|
pass
|
112
133
|
|
113
134
|
# Scrolling Actions
|
135
|
+
@abstractmethod
|
136
|
+
async def scroll(self, x: int, y: int) -> None:
|
137
|
+
"""Scroll the mouse wheel."""
|
138
|
+
pass
|
139
|
+
|
114
140
|
@abstractmethod
|
115
141
|
async def scroll_down(self, clicks: int = 1) -> None:
|
116
142
|
"""Scroll down."""
|
@@ -166,7 +192,47 @@ class BaseComputerInterface(ABC):
|
|
166
192
|
async def directory_exists(self, path: str) -> bool:
|
167
193
|
"""Check if directory exists."""
|
168
194
|
pass
|
169
|
-
|
195
|
+
|
196
|
+
@abstractmethod
|
197
|
+
async def list_dir(self, path: str) -> List[str]:
|
198
|
+
"""List directory contents."""
|
199
|
+
pass
|
200
|
+
|
201
|
+
@abstractmethod
|
202
|
+
async def read_text(self, path: str) -> str:
|
203
|
+
"""Read file text contents."""
|
204
|
+
pass
|
205
|
+
|
206
|
+
@abstractmethod
|
207
|
+
async def write_text(self, path: str, content: str) -> None:
|
208
|
+
"""Write file text contents."""
|
209
|
+
pass
|
210
|
+
|
211
|
+
@abstractmethod
|
212
|
+
async def read_bytes(self, path: str) -> bytes:
|
213
|
+
"""Read file binary contents."""
|
214
|
+
pass
|
215
|
+
|
216
|
+
@abstractmethod
|
217
|
+
async def write_bytes(self, path: str, content: bytes) -> None:
|
218
|
+
"""Write file binary contents."""
|
219
|
+
pass
|
220
|
+
|
221
|
+
@abstractmethod
|
222
|
+
async def delete_file(self, path: str) -> None:
|
223
|
+
"""Delete file."""
|
224
|
+
pass
|
225
|
+
|
226
|
+
@abstractmethod
|
227
|
+
async def create_dir(self, path: str) -> None:
|
228
|
+
"""Create directory."""
|
229
|
+
pass
|
230
|
+
|
231
|
+
@abstractmethod
|
232
|
+
async def delete_dir(self, path: str) -> None:
|
233
|
+
"""Delete directory."""
|
234
|
+
pass
|
235
|
+
|
170
236
|
@abstractmethod
|
171
237
|
async def run_command(self, command: str) -> Tuple[str, str]:
|
172
238
|
"""Run shell command."""
|
@@ -8,7 +8,7 @@ class InterfaceFactory:
|
|
8
8
|
|
9
9
|
@staticmethod
|
10
10
|
def create_interface_for_os(
|
11
|
-
os: Literal['macos', 'linux'],
|
11
|
+
os: Literal['macos', 'linux', 'windows'],
|
12
12
|
ip_address: str,
|
13
13
|
api_key: Optional[str] = None,
|
14
14
|
vm_name: Optional[str] = None
|
@@ -16,7 +16,7 @@ class InterfaceFactory:
|
|
16
16
|
"""Create an interface for the specified OS.
|
17
17
|
|
18
18
|
Args:
|
19
|
-
os: Operating system type ('macos' or '
|
19
|
+
os: Operating system type ('macos', 'linux', or 'windows')
|
20
20
|
ip_address: IP address of the computer to control
|
21
21
|
api_key: Optional API key for cloud authentication
|
22
22
|
vm_name: Optional VM name for cloud authentication
|
@@ -30,10 +30,13 @@ class InterfaceFactory:
|
|
30
30
|
# Import implementations here to avoid circular imports
|
31
31
|
from .macos import MacOSComputerInterface
|
32
32
|
from .linux import LinuxComputerInterface
|
33
|
+
from .windows import WindowsComputerInterface
|
33
34
|
|
34
35
|
if os == 'macos':
|
35
36
|
return MacOSComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
|
36
37
|
elif os == 'linux':
|
37
38
|
return LinuxComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
|
39
|
+
elif os == 'windows':
|
40
|
+
return WindowsComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
|
38
41
|
else:
|
39
|
-
raise ValueError(f"Unsupported OS type: {os}")
|
42
|
+
raise ValueError(f"Unsupported OS type: {os}")
|
@@ -8,8 +8,8 @@ import websockets
|
|
8
8
|
|
9
9
|
from ..logger import Logger, LogLevel
|
10
10
|
from .base import BaseComputerInterface
|
11
|
-
from ..utils import decode_base64_image, bytes_to_image, draw_box, resize_image
|
12
|
-
from .models import Key, KeyType
|
11
|
+
from ..utils import decode_base64_image, encode_base64_image, bytes_to_image, draw_box, resize_image
|
12
|
+
from .models import Key, KeyType, MouseButton
|
13
13
|
|
14
14
|
|
15
15
|
class LinuxComputerInterface(BaseComputerInterface):
|
@@ -22,7 +22,7 @@ class LinuxComputerInterface(BaseComputerInterface):
|
|
22
22
|
self._closed = False
|
23
23
|
self._last_ping = 0
|
24
24
|
self._ping_interval = 5 # Send ping every 5 seconds
|
25
|
-
self._ping_timeout =
|
25
|
+
self._ping_timeout = 120 # Wait 120 seconds for pong response
|
26
26
|
self._reconnect_delay = 1 # Start with 1 second delay
|
27
27
|
self._max_reconnect_delay = 30 # Maximum delay between reconnection attempts
|
28
28
|
self._log_connection_attempts = True # Flag to control connection attempt logging
|
@@ -87,7 +87,7 @@ class LinuxComputerInterface(BaseComputerInterface):
|
|
87
87
|
close_timeout=5,
|
88
88
|
compression=None, # Disable compression to reduce overhead
|
89
89
|
),
|
90
|
-
timeout=
|
90
|
+
timeout=120,
|
91
91
|
)
|
92
92
|
self.logger.info("WebSocket connection established")
|
93
93
|
|
@@ -349,6 +349,12 @@ class LinuxComputerInterface(BaseComputerInterface):
|
|
349
349
|
self._ws = None
|
350
350
|
|
351
351
|
# Mouse Actions
|
352
|
+
async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> None:
|
353
|
+
await self._send_command("mouse_down", {"x": x, "y": y, "button": button})
|
354
|
+
|
355
|
+
async def mouse_up(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> None:
|
356
|
+
await self._send_command("mouse_up", {"x": x, "y": y, "button": button})
|
357
|
+
|
352
358
|
async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> None:
|
353
359
|
await self._send_command("left_click", {"x": x, "y": y})
|
354
360
|
|
@@ -361,17 +367,23 @@ class LinuxComputerInterface(BaseComputerInterface):
|
|
361
367
|
async def move_cursor(self, x: int, y: int) -> None:
|
362
368
|
await self._send_command("move_cursor", {"x": x, "y": y})
|
363
369
|
|
364
|
-
async def drag_to(self, x: int, y: int, button:
|
370
|
+
async def drag_to(self, x: int, y: int, button: "MouseButton" = "left", duration: float = 0.5) -> None:
|
365
371
|
await self._send_command(
|
366
372
|
"drag_to", {"x": x, "y": y, "button": button, "duration": duration}
|
367
373
|
)
|
368
374
|
|
369
|
-
async def drag(self, path: List[Tuple[int, int]], button:
|
375
|
+
async def drag(self, path: List[Tuple[int, int]], button: "MouseButton" = "left", duration: float = 0.5) -> None:
|
370
376
|
await self._send_command(
|
371
377
|
"drag", {"path": path, "button": button, "duration": duration}
|
372
378
|
)
|
373
379
|
|
374
380
|
# Keyboard Actions
|
381
|
+
async def key_down(self, key: "KeyType") -> None:
|
382
|
+
await self._send_command("key_down", {"key": key})
|
383
|
+
|
384
|
+
async def key_up(self, key: "KeyType") -> None:
|
385
|
+
await self._send_command("key_up", {"key": key})
|
386
|
+
|
375
387
|
async def type_text(self, text: str) -> None:
|
376
388
|
# Temporary fix for https://github.com/trycua/cua/issues/165
|
377
389
|
# Check if text contains Unicode characters
|
@@ -464,6 +476,9 @@ class LinuxComputerInterface(BaseComputerInterface):
|
|
464
476
|
await self._send_command("hotkey", {"keys": actual_keys})
|
465
477
|
|
466
478
|
# Scrolling Actions
|
479
|
+
async def scroll(self, x: int, y: int) -> None:
|
480
|
+
await self._send_command("scroll", {"x": x, "y": y})
|
481
|
+
|
467
482
|
async def scroll_down(self, clicks: int = 1) -> None:
|
468
483
|
await self._send_command("scroll_down", {"clicks": clicks})
|
469
484
|
|
@@ -557,6 +572,50 @@ class LinuxComputerInterface(BaseComputerInterface):
|
|
557
572
|
result = await self._send_command("directory_exists", {"path": path})
|
558
573
|
return result.get("exists", False)
|
559
574
|
|
575
|
+
async def list_dir(self, path: str) -> list[str]:
|
576
|
+
result = await self._send_command("list_dir", {"path": path})
|
577
|
+
if not result.get("success", False):
|
578
|
+
raise RuntimeError(result.get("error", "Failed to list directory"))
|
579
|
+
return result.get("files", [])
|
580
|
+
|
581
|
+
async def read_text(self, path: str) -> str:
|
582
|
+
result = await self._send_command("read_text", {"path": path})
|
583
|
+
if not result.get("success", False):
|
584
|
+
raise RuntimeError(result.get("error", "Failed to read file"))
|
585
|
+
return result.get("content", "")
|
586
|
+
|
587
|
+
async def write_text(self, path: str, content: str) -> None:
|
588
|
+
result = await self._send_command("write_text", {"path": path, "content": content})
|
589
|
+
if not result.get("success", False):
|
590
|
+
raise RuntimeError(result.get("error", "Failed to write file"))
|
591
|
+
|
592
|
+
async def read_bytes(self, path: str) -> bytes:
|
593
|
+
result = await self._send_command("read_bytes", {"path": path})
|
594
|
+
if not result.get("success", False):
|
595
|
+
raise RuntimeError(result.get("error", "Failed to read file"))
|
596
|
+
content_b64 = result.get("content_b64", "")
|
597
|
+
return decode_base64_image(content_b64)
|
598
|
+
|
599
|
+
async def write_bytes(self, path: str, content: bytes) -> None:
|
600
|
+
result = await self._send_command("write_bytes", {"path": path, "content_b64": encode_base64_image(content)})
|
601
|
+
if not result.get("success", False):
|
602
|
+
raise RuntimeError(result.get("error", "Failed to write file"))
|
603
|
+
|
604
|
+
async def delete_file(self, path: str) -> None:
|
605
|
+
result = await self._send_command("delete_file", {"path": path})
|
606
|
+
if not result.get("success", False):
|
607
|
+
raise RuntimeError(result.get("error", "Failed to delete file"))
|
608
|
+
|
609
|
+
async def create_dir(self, path: str) -> None:
|
610
|
+
result = await self._send_command("create_dir", {"path": path})
|
611
|
+
if not result.get("success", False):
|
612
|
+
raise RuntimeError(result.get("error", "Failed to create directory"))
|
613
|
+
|
614
|
+
async def delete_dir(self, path: str) -> None:
|
615
|
+
result = await self._send_command("delete_dir", {"path": path})
|
616
|
+
if not result.get("success", False):
|
617
|
+
raise RuntimeError(result.get("error", "Failed to delete directory"))
|
618
|
+
|
560
619
|
async def run_command(self, command: str) -> Tuple[str, str]:
|
561
620
|
result = await self._send_command("run_command", {"command": command})
|
562
621
|
if not result.get("success", False):
|
@@ -8,8 +8,8 @@ import websockets
|
|
8
8
|
|
9
9
|
from ..logger import Logger, LogLevel
|
10
10
|
from .base import BaseComputerInterface
|
11
|
-
from ..utils import decode_base64_image, bytes_to_image, draw_box, resize_image
|
12
|
-
from .models import Key, KeyType
|
11
|
+
from ..utils import decode_base64_image, encode_base64_image, bytes_to_image, draw_box, resize_image
|
12
|
+
from .models import Key, KeyType, MouseButton
|
13
13
|
|
14
14
|
|
15
15
|
class MacOSComputerInterface(BaseComputerInterface):
|
@@ -22,7 +22,7 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
22
22
|
self._closed = False
|
23
23
|
self._last_ping = 0
|
24
24
|
self._ping_interval = 5 # Send ping every 5 seconds
|
25
|
-
self._ping_timeout =
|
25
|
+
self._ping_timeout = 120 # Wait 120 seconds for pong response
|
26
26
|
self._reconnect_delay = 1 # Start with 1 second delay
|
27
27
|
self._max_reconnect_delay = 30 # Maximum delay between reconnection attempts
|
28
28
|
self._log_connection_attempts = True # Flag to control connection attempt logging
|
@@ -86,7 +86,7 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
86
86
|
close_timeout=5,
|
87
87
|
compression=None, # Disable compression to reduce overhead
|
88
88
|
),
|
89
|
-
timeout=
|
89
|
+
timeout=120,
|
90
90
|
)
|
91
91
|
self.logger.info("WebSocket connection established")
|
92
92
|
|
@@ -231,7 +231,7 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
231
231
|
|
232
232
|
message = {"command": command, "params": params or {}}
|
233
233
|
await self._ws.send(json.dumps(message))
|
234
|
-
response = await asyncio.wait_for(self._ws.recv(), timeout=
|
234
|
+
response = await asyncio.wait_for(self._ws.recv(), timeout=120)
|
235
235
|
self.logger.debug(f"Completed command: {command}")
|
236
236
|
return json.loads(response)
|
237
237
|
except Exception as e:
|
@@ -356,6 +356,12 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
356
356
|
return await self._send_command("diorama_cmd", {"action": action, "arguments": arguments or {}})
|
357
357
|
|
358
358
|
# Mouse Actions
|
359
|
+
async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: "MouseButton" = "left") -> None:
|
360
|
+
await self._send_command("mouse_down", {"x": x, "y": y, "button": button})
|
361
|
+
|
362
|
+
async def mouse_up(self, x: Optional[int] = None, y: Optional[int] = None, button: "MouseButton" = "left") -> None:
|
363
|
+
await self._send_command("mouse_up", {"x": x, "y": y, "button": button})
|
364
|
+
|
359
365
|
async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> None:
|
360
366
|
await self._send_command("left_click", {"x": x, "y": y})
|
361
367
|
|
@@ -379,6 +385,12 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
379
385
|
)
|
380
386
|
|
381
387
|
# Keyboard Actions
|
388
|
+
async def key_down(self, key: "KeyType") -> None:
|
389
|
+
await self._send_command("key_down", {"key": key})
|
390
|
+
|
391
|
+
async def key_up(self, key: "KeyType") -> None:
|
392
|
+
await self._send_command("key_up", {"key": key})
|
393
|
+
|
382
394
|
async def type_text(self, text: str) -> None:
|
383
395
|
# Temporary fix for https://github.com/trycua/cua/issues/165
|
384
396
|
# Check if text contains Unicode characters
|
@@ -471,6 +483,9 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
471
483
|
await self._send_command("hotkey", {"keys": actual_keys})
|
472
484
|
|
473
485
|
# Scrolling Actions
|
486
|
+
async def scroll(self, x: int, y: int) -> None:
|
487
|
+
await self._send_command("scroll", {"x": x, "y": y})
|
488
|
+
|
474
489
|
async def scroll_down(self, clicks: int = 1) -> None:
|
475
490
|
await self._send_command("scroll_down", {"clicks": clicks})
|
476
491
|
|
@@ -564,6 +579,50 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
564
579
|
result = await self._send_command("directory_exists", {"path": path})
|
565
580
|
return result.get("exists", False)
|
566
581
|
|
582
|
+
async def list_dir(self, path: str) -> list[str]:
|
583
|
+
result = await self._send_command("list_dir", {"path": path})
|
584
|
+
if not result.get("success", False):
|
585
|
+
raise RuntimeError(result.get("error", "Failed to list directory"))
|
586
|
+
return result.get("files", [])
|
587
|
+
|
588
|
+
async def read_text(self, path: str) -> str:
|
589
|
+
result = await self._send_command("read_text", {"path": path})
|
590
|
+
if not result.get("success", False):
|
591
|
+
raise RuntimeError(result.get("error", "Failed to read file"))
|
592
|
+
return result.get("content", "")
|
593
|
+
|
594
|
+
async def write_text(self, path: str, content: str) -> None:
|
595
|
+
result = await self._send_command("write_text", {"path": path, "content": content})
|
596
|
+
if not result.get("success", False):
|
597
|
+
raise RuntimeError(result.get("error", "Failed to write file"))
|
598
|
+
|
599
|
+
async def read_bytes(self, path: str) -> bytes:
|
600
|
+
result = await self._send_command("read_bytes", {"path": path})
|
601
|
+
if not result.get("success", False):
|
602
|
+
raise RuntimeError(result.get("error", "Failed to read file"))
|
603
|
+
content_b64 = result.get("content_b64", "")
|
604
|
+
return decode_base64_image(content_b64)
|
605
|
+
|
606
|
+
async def write_bytes(self, path: str, content: bytes) -> None:
|
607
|
+
result = await self._send_command("write_bytes", {"path": path, "content_b64": encode_base64_image(content)})
|
608
|
+
if not result.get("success", False):
|
609
|
+
raise RuntimeError(result.get("error", "Failed to write file"))
|
610
|
+
|
611
|
+
async def delete_file(self, path: str) -> None:
|
612
|
+
result = await self._send_command("delete_file", {"path": path})
|
613
|
+
if not result.get("success", False):
|
614
|
+
raise RuntimeError(result.get("error", "Failed to delete file"))
|
615
|
+
|
616
|
+
async def create_dir(self, path: str) -> None:
|
617
|
+
result = await self._send_command("create_dir", {"path": path})
|
618
|
+
if not result.get("success", False):
|
619
|
+
raise RuntimeError(result.get("error", "Failed to create directory"))
|
620
|
+
|
621
|
+
async def delete_dir(self, path: str) -> None:
|
622
|
+
result = await self._send_command("delete_dir", {"path": path})
|
623
|
+
if not result.get("success", False):
|
624
|
+
raise RuntimeError(result.get("error", "Failed to delete directory"))
|
625
|
+
|
567
626
|
async def run_command(self, command: str) -> Tuple[str, str]:
|
568
627
|
result = await self._send_command("run_command", {"command": command})
|
569
628
|
if not result.get("success", False):
|
@@ -106,6 +106,9 @@ class Key(Enum):
|
|
106
106
|
# Combined key type
|
107
107
|
KeyType = Union[Key, NavigationKey, SpecialKey, ModifierKey, FunctionKey, str]
|
108
108
|
|
109
|
+
# Key type for mouse actions
|
110
|
+
MouseButton = Literal['left', 'right', 'middle']
|
111
|
+
|
109
112
|
class AccessibilityWindow(TypedDict):
|
110
113
|
"""Information about a window in the accessibility tree."""
|
111
114
|
app_name: str
|