cua-computer 0.1.0__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.
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: cua-computer
3
+ Version: 0.1.0
4
+ Summary: Computer-Use Interface (CUI) framework powering Cua
5
+ Author-Email: TryCua <gh@trycua.com>
6
+ Requires-Python: <3.13,>=3.10
7
+ Requires-Dist: pylume>=0.1.8
8
+ Requires-Dist: pillow>=10.0.0
9
+ Requires-Dist: websocket-client>=1.8.0
10
+ Requires-Dist: websockets>=12.0
11
+ Requires-Dist: aiohttp>=3.9.0
12
+
@@ -0,0 +1,66 @@
1
+ <div align="center">
2
+ <h1>
3
+ <div class="image-wrapper" style="display: inline-block;">
4
+ <picture>
5
+ <source media="(prefers-color-scheme: dark)" alt="logo" height="150" srcset="../../img/logo_white.png" style="display: block; margin: auto;">
6
+ <source media="(prefers-color-scheme: light)" alt="logo" height="150" srcset="../../img/logo_black.png" style="display: block; margin: auto;">
7
+ <img alt="Shows my svg">
8
+ </picture>
9
+ </div>
10
+
11
+ [![Python](https://img.shields.io/badge/Python-333333?logo=python&logoColor=white&labelColor=333333)](#)
12
+ [![macOS](https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=F0F0F0)](#)
13
+ [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.com/invite/mVnXXpdE85)
14
+ [![PyPI](https://img.shields.io/pypi/v/cua-computer?color=333333)](https://pypi.org/project/cua-computer/)
15
+ </h1>
16
+ </div>
17
+
18
+ **Computer** is a Computer-Use Interface (CUI) framework powering Cua for interacting with local macOS and Linux sandboxes, PyAutoGUI-compatible, and pluggable with any AI agent systems (Cua, Langchain, CrewAI, AutoGen). Computer relies on [Lume](https://github.com/trycua/lume) for creating and managing sandbox environments.
19
+
20
+ ### Get started with Computer
21
+
22
+ <div align="center">
23
+ <img src="../../img/computer.png"/>
24
+ </div>
25
+
26
+ ```python
27
+ from computer import Computer
28
+
29
+ computer = Computer(os="macos", display="1024x768", memory="8GB", cpu="4")
30
+ try:
31
+ await computer.run()
32
+
33
+ screenshot = await computer.interface.screenshot()
34
+ with open("screenshot.png", "wb") as f:
35
+ f.write(screenshot)
36
+
37
+ await computer.interface.move_cursor(100, 100)
38
+ await computer.interface.left_click()
39
+ await computer.interface.right_click(300, 300)
40
+ await computer.interface.double_click(400, 400)
41
+
42
+ await computer.interface.type("Hello, World!")
43
+ await computer.interface.press_key("enter")
44
+
45
+ await computer.interface.set_clipboard("Test clipboard")
46
+ content = await computer.interface.copy_to_clipboard()
47
+ print(f"Clipboard content: {content}")
48
+ finally:
49
+ await computer.stop()
50
+ ```
51
+
52
+ ## Install
53
+
54
+ To install the Computer-Use Interface (CUI):
55
+
56
+ ```bash
57
+ pip install cua-computer
58
+ ```
59
+
60
+ The `cua-computer` PyPi package pulls automatically the latest package version of Lume through [pylume](https://github.com/trycua/pylume).
61
+
62
+ ## Run
63
+
64
+ Refer to this notebook for a step-by-step guide on how to use the Computer-Use Interface (CUI):
65
+
66
+ - [Computer-Use Interface (CUI)](../../notebooks/computer_nb.ipynb)
@@ -0,0 +1,7 @@
1
+ """CUA Computer Interface for cross-platform computer control."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .computer import Computer
6
+
7
+ __all__ = ["Computer"]
@@ -0,0 +1,486 @@
1
+ from typing import Optional, List, Literal, Dict, Any, Union
2
+ from pylume import PyLume, VMRunOpts, SharedDirectory
3
+ import asyncio
4
+ from .models import Computer as ComputerConfig, Image, Display
5
+ from .interface.factory import InterfaceFactory
6
+ from pylume import ImageRef, VMUpdateOpts, PyLume
7
+ import time
8
+ from PIL import Image
9
+ import io
10
+ from .utils import bytes_to_image
11
+ import re
12
+ from .logger import Logger, LogLevel
13
+ import json
14
+ from pylume.models import VMRunOpts, VMUpdateOpts, ImageRef, SharedDirectory, VMStatus
15
+ import logging
16
+
17
+ OSType = Literal["macos", "linux"]
18
+
19
+
20
+ class Computer:
21
+ """Main computer interface."""
22
+
23
+ def __init__(
24
+ self,
25
+ display: Union[Display, Dict[str, int], str] = "1024x768",
26
+ memory: str = "8GB",
27
+ cpu: str = "4",
28
+ os: OSType = "macos",
29
+ name: str = "",
30
+ image: str = "macos-sequoia-cua:latest",
31
+ shared_directories: Optional[List[str]] = None,
32
+ use_host_computer_server: bool = False,
33
+ verbosity: Union[int, LogLevel] = logging.INFO,
34
+ ):
35
+ """Initialize a new Computer instance.
36
+
37
+ Args:
38
+ display: The display configuration. Can be:
39
+ - A Display object
40
+ - A dict with 'width' and 'height'
41
+ - A string in format "WIDTHxHEIGHT" (e.g. "1920x1080")
42
+ Defaults to "1024x768"
43
+ memory: The VM memory allocation. Defaults to "8GB"
44
+ cpu: The VM CPU allocation. Defaults to "4"
45
+ os: The operating system type ('macos' or 'linux')
46
+ name: The VM name
47
+ image: The VM image name
48
+ shared_directories: Optional list of directory paths to share with the VM
49
+ use_host_computer_server: If True, target localhost instead of starting a VM
50
+ verbosity: Logging level (standard Python logging levels: logging.DEBUG, logging.INFO, etc.)
51
+ LogLevel enum values are still accepted for backward compatibility
52
+ """
53
+ self.logger = Logger("cua.computer", verbosity)
54
+ self.logger.info("Initializing Computer...")
55
+
56
+ # Store original parameters
57
+ self.image = image
58
+
59
+ # Set initialization flag
60
+ self._initialized = False
61
+ self._running = False
62
+
63
+ # Configure root logger
64
+ self.verbosity = verbosity
65
+ self.logger = Logger("cua", verbosity)
66
+
67
+ # Configure component loggers with proper hierarchy
68
+ self.vm_logger = Logger("cua.vm", verbosity)
69
+ self.interface_logger = Logger("cua.interface", verbosity)
70
+
71
+ if not use_host_computer_server:
72
+ if ":" not in image or len(image.split(":")) != 2:
73
+ raise ValueError("Image must be in the format <image_name>:<tag>")
74
+
75
+ if not name:
76
+ # Normalize the name to be used for the VM
77
+ name = image.replace(":", "_")
78
+
79
+ # Convert display parameter to Display object
80
+ if isinstance(display, str):
81
+ # Parse string format "WIDTHxHEIGHT"
82
+ match = re.match(r"(\d+)x(\d+)", display)
83
+ if not match:
84
+ raise ValueError(
85
+ "Display string must be in format 'WIDTHxHEIGHT' (e.g. '1024x768')"
86
+ )
87
+ width, height = map(int, match.groups())
88
+ display_config = Display(width=width, height=height)
89
+ elif isinstance(display, dict):
90
+ display_config = Display(**display)
91
+ else:
92
+ display_config = display
93
+
94
+ self.config = ComputerConfig(
95
+ image=image.split(":")[0],
96
+ tag=image.split(":")[1],
97
+ name=name,
98
+ display=display_config,
99
+ memory=memory,
100
+ cpu=cpu,
101
+ )
102
+ # Initialize PyLume but don't start the server yet - we'll do that in run()
103
+ self.config.pylume = PyLume(
104
+ debug=(self.verbosity == LogLevel.DEBUG),
105
+ port=3000,
106
+ use_existing_server=False,
107
+ server_start_timeout=120, # Increase timeout to 2 minutes
108
+ )
109
+
110
+ self._interface = None
111
+ self.os = os
112
+ self.shared_paths = []
113
+ if shared_directories:
114
+ for path in shared_directories:
115
+ abs_path = os.path.abspath(os.path.expanduser(path)) # type: ignore[attr-defined]
116
+ if not os.path.exists(abs_path): # type: ignore[attr-defined]
117
+ raise ValueError(f"Shared directory does not exist: {path}")
118
+ self.shared_paths.append(abs_path)
119
+ self._pylume_context = None
120
+ self.use_host_computer_server = use_host_computer_server
121
+
122
+ async def __aenter__(self):
123
+ """Enter async context manager."""
124
+ await self.run()
125
+ return self
126
+
127
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
128
+ """Exit async context manager."""
129
+ # await self.stop()
130
+ pass
131
+
132
+ async def run(self) -> None:
133
+ """Initialize the VM and computer interface."""
134
+ # If already initialized, just log and return
135
+ if hasattr(self, "_initialized") and self._initialized:
136
+ self.logger.info("Computer already initialized, skipping initialization")
137
+ return
138
+
139
+ self.logger.info("Starting computer...")
140
+
141
+ try:
142
+ # If using host computer server
143
+ if self.use_host_computer_server:
144
+ self.logger.info("Using host computer server")
145
+ # Set ip_address for host computer server mode
146
+ ip_address = "localhost"
147
+ # Create the interface
148
+ self._interface = InterfaceFactory.create_interface_for_os(
149
+ os=self.os, ip_address=ip_address # type: ignore[arg-type]
150
+ )
151
+
152
+ self.logger.info("Waiting for host computer server to be ready...")
153
+ await self._interface.wait_for_ready()
154
+ self.logger.info("Host computer server ready")
155
+ else:
156
+ # Start or connect to VM
157
+ self.logger.info(f"Starting VM: {self.image}")
158
+ if not self._pylume_context:
159
+ try:
160
+ self.logger.verbose("Initializing PyLume context...")
161
+ self._pylume_context = await self.config.pylume.__aenter__() # type: ignore[attr-defined]
162
+ self.logger.verbose("PyLume context initialized successfully")
163
+ except Exception as e:
164
+ self.logger.error(f"Failed to initialize PyLume context: {e}")
165
+ raise RuntimeError(f"Failed to initialize PyLume: {e}")
166
+
167
+ # Try to get the VM, if it doesn't exist, create it and pull the image
168
+ try:
169
+ vm = await self.config.pylume.get_vm(self.config.name) # type: ignore[attr-defined]
170
+ self.logger.verbose(f"Found existing VM: {self.config.name}")
171
+ except Exception as e:
172
+ self.logger.verbose(f"VM not found, pulling image: {e}")
173
+ image_ref = ImageRef(
174
+ image=self.config.image,
175
+ tag=self.config.tag,
176
+ registry="ghcr.io",
177
+ organization="trycua",
178
+ )
179
+ self.logger.info(f"Pulling image {self.config.image}:{self.config.tag}...")
180
+ try:
181
+ await self.config.pylume.pull_image(image_ref, name=self.config.name) # type: ignore[attr-defined]
182
+ except Exception as pull_error:
183
+ self.logger.error(f"Failed to pull image: {pull_error}")
184
+ raise RuntimeError(f"Failed to pull VM image: {pull_error}")
185
+
186
+ # Convert paths to SharedDirectory objects
187
+ shared_directories = []
188
+ for path in self.shared_paths:
189
+ self.logger.verbose(f"Adding shared directory: {path}")
190
+ shared_directories.append(
191
+ SharedDirectory(host_path=path) # type: ignore[arg-type]
192
+ )
193
+
194
+ # Run with shared directories
195
+ self.logger.info(f"Starting VM {self.config.name}...")
196
+ run_opts = VMRunOpts(
197
+ no_display=False, # type: ignore[arg-type]
198
+ shared_directories=shared_directories, # type: ignore[arg-type]
199
+ )
200
+
201
+ # Log the run options for debugging
202
+ self.logger.info(f"VM run options: {vars(run_opts)}")
203
+
204
+ # Log the equivalent curl command for debugging
205
+ payload = json.dumps({"noDisplay": False, "sharedDirectories": []})
206
+ curl_cmd = f"curl -X POST 'http://localhost:3000/lume/vms/{self.config.name}/run' -H 'Content-Type: application/json' -d '{payload}'"
207
+ print(f"\nEquivalent curl command:\n{curl_cmd}\n")
208
+
209
+ try:
210
+ response = await self.config.pylume.run_vm(self.config.name, run_opts) # type: ignore[attr-defined]
211
+ self.logger.info(f"VM run response: {response if response else 'None'}")
212
+ except Exception as run_error:
213
+ self.logger.error(f"Failed to run VM: {run_error}")
214
+ raise RuntimeError(f"Failed to start VM: {run_error}")
215
+
216
+ # Wait for VM to be ready with required properties
217
+ self.logger.info("Waiting for VM to be ready...")
218
+ try:
219
+ vm = await self.wait_vm_ready()
220
+ if not vm or not vm.ip_address: # type: ignore[attr-defined]
221
+ raise RuntimeError(f"VM {self.config.name} failed to get IP address")
222
+ ip_address = vm.ip_address # type: ignore[attr-defined]
223
+ self.logger.info(f"VM is ready with IP: {ip_address}")
224
+ except Exception as wait_error:
225
+ self.logger.error(f"Error waiting for VM: {wait_error}")
226
+ raise RuntimeError(f"VM failed to become ready: {wait_error}")
227
+ except Exception as e:
228
+ self.logger.error(f"Failed to initialize computer: {e}")
229
+ raise RuntimeError(f"Failed to initialize computer: {e}")
230
+
231
+ # Initialize the interface using the factory with the specified OS
232
+ self.logger.info(f"Initializing interface for {self.os} at {ip_address}")
233
+ self._interface = InterfaceFactory.create_interface_for_os(
234
+ os=self.os, ip_address=ip_address # type: ignore[arg-type]
235
+ )
236
+
237
+ # Wait for the WebSocket interface to be ready
238
+ self.logger.info("Connecting to WebSocket interface...")
239
+
240
+ try:
241
+ # Use a single timeout for the entire connection process
242
+ await self._interface.wait_for_ready(timeout=60)
243
+ self.logger.info("WebSocket interface connected successfully")
244
+ except TimeoutError as e:
245
+ self.logger.error("Failed to connect to WebSocket interface")
246
+ raise TimeoutError(
247
+ f"Could not connect to WebSocket interface at {ip_address}:8000/ws: {str(e)}"
248
+ )
249
+
250
+ # Create an event to keep the VM running in background if needed
251
+ if not self.use_host_computer_server:
252
+ self._stop_event = asyncio.Event()
253
+ self._keep_alive_task = asyncio.create_task(self._stop_event.wait())
254
+
255
+ self.logger.info("Computer is ready")
256
+
257
+ # Set the initialization flag and clear the initializing flag
258
+ self._initialized = True
259
+ self.logger.info("Computer successfully initialized")
260
+ return
261
+
262
+ async def stop(self) -> None:
263
+ """Stop computer control."""
264
+ if self._running:
265
+ self._running = False
266
+ self.logger.info("Stopping Computer...")
267
+
268
+ try:
269
+ if hasattr(self, "_stop_event"):
270
+ self._stop_event.set()
271
+ if hasattr(self, "_keep_alive_task"):
272
+ await self._keep_alive_task
273
+
274
+ if self._interface: # Only try to close interface if it exists
275
+ self.logger.verbose("Closing interface...")
276
+ # For host computer server, just use normal close to keep the server running
277
+ if self.use_host_computer_server:
278
+ self._interface.close()
279
+ else:
280
+ # For VM mode, force close the connection
281
+ if hasattr(self._interface, "force_close"):
282
+ self._interface.force_close()
283
+ else:
284
+ self._interface.close()
285
+
286
+ if not self.use_host_computer_server and self._pylume_context:
287
+ try:
288
+ self.logger.info(f"Stopping VM {self.config.name}...")
289
+ await self.config.pylume.stop_vm(self.config.name) # type: ignore[attr-defined]
290
+ except Exception as e:
291
+ self.logger.verbose(f"Error stopping VM: {e}") # VM might already be stopped
292
+ self.logger.verbose("Closing PyLume context...")
293
+ await self.config.pylume.__aexit__(None, None, None) # type: ignore[attr-defined]
294
+ self._pylume_context = None
295
+ self.logger.info("Computer stopped")
296
+ except Exception as e:
297
+ self.logger.debug(
298
+ f"Error during cleanup: {e}"
299
+ ) # Log as debug since this might be expected
300
+
301
+ # @property
302
+ async def get_ip(self) -> str:
303
+ """Get the IP address of the VM or localhost if using host computer server."""
304
+ if self.use_host_computer_server:
305
+ return "127.0.0.1"
306
+ ip = await self.config.get_ip()
307
+ return ip or "unknown" # Return "unknown" if ip is None
308
+
309
+ async def wait_vm_ready(self) -> Optional[Union[Dict[str, Any], "VMStatus"]]:
310
+ """Wait for VM to be ready with an IP address.
311
+
312
+ Returns:
313
+ VM status information or None if using host computer server.
314
+ """
315
+ if self.use_host_computer_server:
316
+ return None
317
+
318
+ timeout = 600 # 10 minutes timeout (increased from 4 minutes)
319
+ interval = 2.0 # 2 seconds between checks (increased to reduce API load)
320
+ start_time = time.time()
321
+ last_status = None
322
+ attempts = 0
323
+
324
+ self.logger.info(f"Waiting for VM {self.config.name} to be ready (timeout: {timeout}s)...")
325
+
326
+ while time.time() - start_time < timeout:
327
+ attempts += 1
328
+ elapsed = time.time() - start_time
329
+
330
+ try:
331
+ # Keep polling for VM info
332
+ vm = await self.config.pylume.get_vm(self.config.name) # type: ignore[attr-defined]
333
+
334
+ # Log full VM properties for debugging (every 30 attempts)
335
+ if attempts % 30 == 0:
336
+ self.logger.info(
337
+ f"VM properties at attempt {attempts}: {vars(vm) if vm else 'None'}"
338
+ )
339
+
340
+ # Get current status for logging
341
+ current_status = getattr(vm, "status", None) if vm else None
342
+ if current_status != last_status:
343
+ self.logger.info(
344
+ f"VM status changed to: {current_status} (after {elapsed:.1f}s)"
345
+ )
346
+ last_status = current_status
347
+
348
+ # Check for IP address - ensure it's not None or empty
349
+ ip = getattr(vm, "ip_address", None) if vm else None
350
+ if ip and ip.strip(): # Check for non-empty string
351
+ self.logger.info(
352
+ f"VM {self.config.name} got IP address: {ip} (after {elapsed:.1f}s)"
353
+ )
354
+ return vm
355
+
356
+ if attempts % 10 == 0: # Log every 10 attempts to avoid flooding
357
+ self.logger.info(
358
+ f"Still waiting for VM IP address... (elapsed: {elapsed:.1f}s)"
359
+ )
360
+ else:
361
+ self.logger.debug(
362
+ f"Waiting for VM IP address... Current IP: {ip}, Status: {current_status}"
363
+ )
364
+
365
+ except Exception as e:
366
+ self.logger.warning(f"Error checking VM status (attempt {attempts}): {str(e)}")
367
+ # If we've been trying for a while and still getting errors, log more details
368
+ if elapsed > 60: # After 1 minute of errors, log more details
369
+ self.logger.error(f"Persistent error getting VM status: {str(e)}")
370
+ self.logger.info("Trying to get VM list for debugging...")
371
+ try:
372
+ vms = await self.config.pylume.list_vms() # type: ignore[attr-defined]
373
+ self.logger.info(
374
+ f"Available VMs: {[vm.name for vm in vms if hasattr(vm, 'name')]}"
375
+ )
376
+ except Exception as list_error:
377
+ self.logger.error(f"Failed to list VMs: {str(list_error)}")
378
+
379
+ await asyncio.sleep(interval)
380
+
381
+ # If we get here, we've timed out
382
+ elapsed = time.time() - start_time
383
+ self.logger.error(f"VM {self.config.name} not ready after {elapsed:.1f} seconds")
384
+
385
+ # Try to get final VM status for debugging
386
+ try:
387
+ vm = await self.config.pylume.get_vm(self.config.name) # type: ignore[attr-defined]
388
+ status = getattr(vm, "status", "unknown") if vm else "unknown"
389
+ ip = getattr(vm, "ip_address", None) if vm else None
390
+ self.logger.error(f"Final VM status: {status}, IP: {ip}")
391
+ except Exception as e:
392
+ self.logger.error(f"Failed to get final VM status: {str(e)}")
393
+
394
+ raise TimeoutError(
395
+ f"VM {self.config.name} not ready after {elapsed:.1f} seconds - IP address not assigned"
396
+ )
397
+
398
+ async def update(self, cpu: Optional[int] = None, memory: Optional[str] = None):
399
+ """Update VM settings."""
400
+ self.logger.info(
401
+ f"Updating VM settings: CPU={cpu or self.config.cpu}, Memory={memory or self.config.memory}"
402
+ )
403
+ update_opts = VMUpdateOpts(
404
+ cpu=cpu or int(self.config.cpu), memory=memory or self.config.memory
405
+ )
406
+ await self.config.pylume.update_vm(self.config.image, update_opts) # type: ignore[attr-defined]
407
+
408
+ def get_screenshot_size(self, screenshot: bytes) -> Dict[str, int]:
409
+ """Get the dimensions of a screenshot.
410
+
411
+ Args:
412
+ screenshot: The screenshot bytes
413
+
414
+ Returns:
415
+ Dict[str, int]: Dictionary containing 'width' and 'height' of the image
416
+ """
417
+ image = Image.open(io.BytesIO(screenshot))
418
+ width, height = image.size
419
+ return {"width": width, "height": height}
420
+
421
+ @property
422
+ def interface(self):
423
+ """Get the computer interface for interacting with the VM.
424
+
425
+ Returns:
426
+ BaseComputerInterface: The interface for controlling the VM
427
+
428
+ Raises:
429
+ RuntimeError: If the interface is not initialized (run() not called)
430
+ """
431
+ if not hasattr(self, "_interface") or self._interface is None:
432
+ error_msg = "Computer interface not initialized. Call run() first."
433
+ self.logger.error(error_msg)
434
+ self.logger.error(
435
+ "Make sure to call await computer.run() before using any interface methods."
436
+ )
437
+ raise RuntimeError(error_msg)
438
+ return self._interface
439
+
440
+ def __getattr__(self, name: str):
441
+ """Delegate all other method calls to the interface.
442
+
443
+ This is kept for backward compatibility, prefer using computer.interface directly.
444
+ """
445
+ # First check if the interface is available
446
+ interface = self.interface # This will raise the proper exception if not initialized
447
+ return getattr(interface, name)
448
+
449
+ async def to_screen_coordinates(self, x: float, y: float) -> tuple[float, float]:
450
+ """Convert normalized coordinates to screen coordinates.
451
+
452
+ This is a convenience method that delegates to the interface.
453
+
454
+ Args:
455
+ x: X coordinate between 0 and 1
456
+ y: Y coordinate between 0 and 1
457
+
458
+ Returns:
459
+ tuple[float, float]: Screen coordinates (x, y)
460
+ """
461
+ return await self.interface.to_screen_coordinates(x, y)
462
+
463
+ async def to_screenshot_coordinates(self, x: float, y: float) -> tuple[float, float]:
464
+ """Convert screen coordinates to screenshot coordinates.
465
+
466
+ Args:
467
+ x: X coordinate in screen space
468
+ y: Y coordinate in screen space
469
+
470
+ Returns:
471
+ tuple[float, float]: (x, y) coordinates in screenshot space
472
+ """
473
+ screen_size = await self.interface.get_screen_size()
474
+ screenshot = await self.interface.screenshot()
475
+ screenshot_img = bytes_to_image(screenshot)
476
+ screenshot_width, screenshot_height = screenshot_img.size
477
+
478
+ # Calculate scaling factors
479
+ width_scale = screenshot_width / screen_size["width"]
480
+ height_scale = screenshot_height / screen_size["height"]
481
+
482
+ # Convert coordinates
483
+ screenshot_x = x * width_scale
484
+ screenshot_y = y * height_scale
485
+
486
+ return screenshot_x, screenshot_y
@@ -0,0 +1,13 @@
1
+ """
2
+ Interface package for Computer SDK.
3
+ """
4
+
5
+ from .factory import InterfaceFactory
6
+ from .base import BaseComputerInterface
7
+ from .macos import MacOSComputerInterface
8
+
9
+ __all__ = [
10
+ "InterfaceFactory",
11
+ "BaseComputerInterface",
12
+ "MacOSComputerInterface",
13
+ ]