vadgr-computer-use 0.1.0__py3-none-any.whl

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 (38) hide show
  1. computer_use/__init__.py +50 -0
  2. computer_use/__main__.py +134 -0
  3. computer_use/bridge/__init__.py +0 -0
  4. computer_use/bridge/actions.py +76 -0
  5. computer_use/bridge/capture.py +70 -0
  6. computer_use/bridge/client.py +187 -0
  7. computer_use/bridge/daemon.py +949 -0
  8. computer_use/bridge/deployer.py +300 -0
  9. computer_use/bridge/protocol.py +40 -0
  10. computer_use/bridge/supervisor.py +277 -0
  11. computer_use/core/__init__.py +0 -0
  12. computer_use/core/actions.py +94 -0
  13. computer_use/core/engine.py +220 -0
  14. computer_use/core/errors.py +29 -0
  15. computer_use/core/loop.py +183 -0
  16. computer_use/core/screenshot.py +43 -0
  17. computer_use/core/smooth_move.py +259 -0
  18. computer_use/core/types.py +92 -0
  19. computer_use/mcp_server.py +389 -0
  20. computer_use/platform/__init__.py +0 -0
  21. computer_use/platform/base.py +51 -0
  22. computer_use/platform/detect.py +74 -0
  23. computer_use/platform/linux.py +1352 -0
  24. computer_use/platform/macos.py +407 -0
  25. computer_use/platform/windows.py +407 -0
  26. computer_use/platform/wsl2.py +824 -0
  27. computer_use/providers/__init__.py +0 -0
  28. computer_use/providers/anthropic.py +262 -0
  29. computer_use/providers/base.py +57 -0
  30. computer_use/providers/openai.py +250 -0
  31. computer_use/providers/registry.py +69 -0
  32. vadgr_computer_use-0.1.0.dist-info/METADATA +156 -0
  33. vadgr_computer_use-0.1.0.dist-info/RECORD +38 -0
  34. vadgr_computer_use-0.1.0.dist-info/WHEEL +5 -0
  35. vadgr_computer_use-0.1.0.dist-info/entry_points.txt +2 -0
  36. vadgr_computer_use-0.1.0.dist-info/licenses/LICENSE +190 -0
  37. vadgr_computer_use-0.1.0.dist-info/licenses/NOTICE +11 -0
  38. vadgr_computer_use-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,50 @@
1
+ """Computer Use Engine -- gives LLM agents eyes and hands.
2
+
3
+ Library mode (agent calls engine):
4
+ from computer_use import ComputerUseEngine
5
+ engine = ComputerUseEngine()
6
+ screen = engine.screenshot()
7
+ engine.click(500, 300)
8
+ engine.type_text("hello")
9
+
10
+ Autonomous mode (engine calls LLM):
11
+ from computer_use import ComputerUseEngine
12
+ engine = ComputerUseEngine(provider="anthropic")
13
+ results = engine.run_task("Open Notepad and type hello")
14
+ """
15
+
16
+ from computer_use.core.engine import ComputerUseEngine
17
+ from computer_use.core.errors import (
18
+ ActionError,
19
+ ActionTimeoutError,
20
+ ComputerUseError,
21
+ ConfigError,
22
+ PlatformNotSupportedError,
23
+ ProviderError,
24
+ ScreenCaptureError,
25
+ )
26
+ from computer_use.core.types import (
27
+ Action,
28
+ ActionType,
29
+ Platform,
30
+ Region,
31
+ ScreenState,
32
+ StepResult,
33
+ )
34
+
35
+ __all__ = [
36
+ "ComputerUseEngine",
37
+ "Action",
38
+ "ActionType",
39
+ "Platform",
40
+ "Region",
41
+ "ScreenState",
42
+ "StepResult",
43
+ "ComputerUseError",
44
+ "ScreenCaptureError",
45
+ "ActionError",
46
+ "ActionTimeoutError",
47
+ "ProviderError",
48
+ "ConfigError",
49
+ "PlatformNotSupportedError",
50
+ ]
@@ -0,0 +1,134 @@
1
+ """CLI entry point for autonomous mode.
2
+
3
+ Usage:
4
+ python -m computer_use "Open Chrome and search for Vadgr"
5
+ python -m computer_use --provider anthropic "Open Notepad"
6
+ python -m computer_use --config custom.yaml "Click the Start menu"
7
+ python -m computer_use --info # Show platform info
8
+ """
9
+
10
+ import argparse
11
+ import logging
12
+ import sys
13
+
14
+
15
+ def main():
16
+ parser = argparse.ArgumentParser(
17
+ description="Computer Use Engine -- autonomous desktop control."
18
+ )
19
+ parser.add_argument(
20
+ "task",
21
+ nargs="?",
22
+ help="Natural-language task description.",
23
+ )
24
+ parser.add_argument(
25
+ "--provider",
26
+ default=None,
27
+ help="LLM provider (anthropic, openai).",
28
+ )
29
+ parser.add_argument(
30
+ "--config",
31
+ default=None,
32
+ help="Path to config.yaml.",
33
+ )
34
+ parser.add_argument(
35
+ "--max-steps",
36
+ type=int,
37
+ default=50,
38
+ help="Maximum steps before stopping.",
39
+ )
40
+ parser.add_argument(
41
+ "--no-verify",
42
+ action="store_true",
43
+ help="Skip action verification.",
44
+ )
45
+ parser.add_argument(
46
+ "--info",
47
+ action="store_true",
48
+ help="Show platform info and exit.",
49
+ )
50
+ parser.add_argument(
51
+ "--screenshot",
52
+ type=str,
53
+ default=None,
54
+ metavar="PATH",
55
+ help="Take a screenshot and save to PATH.",
56
+ )
57
+ parser.add_argument(
58
+ "--verbose", "-v",
59
+ action="store_true",
60
+ help="Enable debug logging.",
61
+ )
62
+ args = parser.parse_args()
63
+
64
+ logging.basicConfig(
65
+ level=logging.DEBUG if args.verbose else logging.INFO,
66
+ format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
67
+ )
68
+
69
+ from computer_use.core.engine import ComputerUseEngine
70
+
71
+ # Info mode
72
+ if args.info:
73
+ engine = ComputerUseEngine(config_path=args.config)
74
+ info = engine.get_platform_info()
75
+ print(f"Platform: {info['platform']}")
76
+ print(f"Backend available: {info['backend_available']}")
77
+ print(f"Accessibility API: {info['accessibility']['api_name']}")
78
+ print(f"Accessibility available: {info['accessibility']['available']}")
79
+ if info["accessibility"].get("notes"):
80
+ print(f"Notes: {info['accessibility']['notes']}")
81
+ screen_w, screen_h = engine.get_screen_size()
82
+ print(f"Screen size: {screen_w}x{screen_h}")
83
+ return
84
+
85
+ # Screenshot mode
86
+ if args.screenshot:
87
+ engine = ComputerUseEngine(config_path=args.config)
88
+ screen = engine.screenshot()
89
+ with open(args.screenshot, "wb") as f:
90
+ f.write(screen.image_bytes)
91
+ print(
92
+ f"Screenshot saved to {args.screenshot} "
93
+ f"({screen.width}x{screen.height})"
94
+ )
95
+ return
96
+
97
+ # Task mode (autonomous)
98
+ if not args.task:
99
+ parser.error("A task description is required (or use --info / --screenshot)")
100
+
101
+ provider = args.provider or "anthropic"
102
+ engine = ComputerUseEngine(
103
+ config_path=args.config,
104
+ provider=provider,
105
+ )
106
+
107
+ print(f"Platform: {engine.get_platform().value}")
108
+ print(f"Provider: {provider}")
109
+ print(f"Task: {args.task}")
110
+ print(f"Max steps: {args.max_steps}")
111
+ print("---")
112
+
113
+ results = engine.run_task(
114
+ task=args.task,
115
+ max_steps=args.max_steps,
116
+ verify=not args.no_verify,
117
+ )
118
+
119
+ print(f"\nCompleted {len(results)} steps.")
120
+ for i, r in enumerate(results, 1):
121
+ status = "OK" if r.success else "FAILED"
122
+ action = r.action_taken.action_type.value
123
+ reason = r.reasoning[:80] if r.reasoning else ""
124
+ print(f" Step {i}: [{status}] {action} -- {reason}")
125
+ if r.error:
126
+ print(f" Error: {r.error}")
127
+
128
+ success_count = sum(1 for r in results if r.success)
129
+ print(f"\n{success_count}/{len(results)} steps succeeded.")
130
+ sys.exit(0 if all(r.success for r in results) else 1)
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
File without changes
@@ -0,0 +1,76 @@
1
+ """Action execution via the bridge daemon."""
2
+
3
+ import logging
4
+
5
+ from computer_use.bridge.client import BridgeClient, BridgeError
6
+ from computer_use.core.actions import ActionExecutor
7
+ from computer_use.core.errors import ActionError
8
+
9
+ logger = logging.getLogger("computer_use.bridge.actions")
10
+
11
+
12
+ class BridgeActionExecutor(ActionExecutor):
13
+ """Implements ActionExecutor by delegating to the bridge daemon over TCP.
14
+
15
+ Accepts an optional *fallback* ActionExecutor (typically WSL2ActionExecutor).
16
+ When the bridge raises an error for text-input methods (type_text, key_press),
17
+ the fallback executor is tried before propagating the failure.
18
+ """
19
+
20
+ def __init__(self, client: BridgeClient, fallback: ActionExecutor | None = None):
21
+ self._client = client
22
+ self._fallback = fallback
23
+
24
+ def _act(self, method: str, params: dict) -> None:
25
+ try:
26
+ self._client.call(method, params, timeout=10.0)
27
+ except BridgeError as e:
28
+ raise ActionError(f"Bridge {method} failed: {e}") from e
29
+
30
+ def move_mouse(self, x: int, y: int) -> None:
31
+ self._act("move_mouse", {"x": x, "y": y})
32
+
33
+ def click(self, x: int, y: int, button: str = "left") -> None:
34
+ self._act("click", {"x": x, "y": y, "button": button})
35
+
36
+ def double_click(self, x: int, y: int) -> None:
37
+ self._act("double_click", {"x": x, "y": y})
38
+
39
+ def type_text(self, text: str) -> None:
40
+ try:
41
+ self._act("type_text", {"text": text})
42
+ except ActionError:
43
+ if self._fallback is not None:
44
+ logger.warning("Bridge type_text failed, using PowerShell fallback")
45
+ self._fallback.type_text(text)
46
+ else:
47
+ raise
48
+
49
+ def key_press(self, keys: list[str]) -> None:
50
+ try:
51
+ self._act("key_press", {"keys": keys})
52
+ except ActionError:
53
+ if self._fallback is not None:
54
+ logger.warning("Bridge key_press failed, using PowerShell fallback")
55
+ self._fallback.key_press(keys)
56
+ else:
57
+ raise
58
+
59
+ def scroll(self, x: int, y: int, amount: int) -> None:
60
+ self._act("scroll", {"x": x, "y": y, "amount": amount})
61
+
62
+ def drag(
63
+ self,
64
+ start_x: int,
65
+ start_y: int,
66
+ end_x: int,
67
+ end_y: int,
68
+ duration: float = 0.5,
69
+ ) -> None:
70
+ self._act("drag", {
71
+ "start_x": start_x,
72
+ "start_y": start_y,
73
+ "end_x": end_x,
74
+ "end_y": end_y,
75
+ "duration": duration,
76
+ })
@@ -0,0 +1,70 @@
1
+ """Screenshot capture via the bridge daemon."""
2
+
3
+ import base64
4
+
5
+ from computer_use.bridge.client import BridgeClient, BridgeError
6
+ from computer_use.core.errors import ScreenCaptureError
7
+ from computer_use.core.screenshot import ScreenCapture
8
+ from computer_use.core.types import Region, ScreenState
9
+
10
+
11
+ class BridgeScreenCapture(ScreenCapture):
12
+ """Implements ScreenCapture by delegating to the bridge daemon over TCP."""
13
+
14
+ def __init__(self, client: BridgeClient, quality: int = 85):
15
+ self._client = client
16
+ self._quality = quality
17
+
18
+ def capture_full(self) -> ScreenState:
19
+ try:
20
+ result = self._client.call(
21
+ "screenshot_full", {"quality": self._quality}, timeout=5.0
22
+ )
23
+ except BridgeError as e:
24
+ raise ScreenCaptureError(f"Bridge screenshot failed: {e}") from e
25
+
26
+ return ScreenState(
27
+ image_bytes=base64.b64decode(result["image_b64"]),
28
+ width=result["width"],
29
+ height=result["height"],
30
+ scale_factor=result.get("scale_factor", 1.0),
31
+ offset_x=result.get("offset_x", 0),
32
+ offset_y=result.get("offset_y", 0),
33
+ )
34
+
35
+ def capture_region(self, region: Region) -> ScreenState:
36
+ try:
37
+ result = self._client.call(
38
+ "screenshot_region",
39
+ {
40
+ "x": region.x,
41
+ "y": region.y,
42
+ "width": region.width,
43
+ "height": region.height,
44
+ "quality": self._quality,
45
+ },
46
+ timeout=5.0,
47
+ )
48
+ except BridgeError as e:
49
+ raise ScreenCaptureError(f"Bridge region screenshot failed: {e}") from e
50
+
51
+ return ScreenState(
52
+ image_bytes=base64.b64decode(result["image_b64"]),
53
+ width=result["width"],
54
+ height=result["height"],
55
+ scale_factor=result.get("scale_factor", 1.0),
56
+ )
57
+
58
+ def get_screen_size(self) -> tuple[int, int]:
59
+ try:
60
+ result = self._client.call("screen_size", timeout=3.0)
61
+ return (result["width"], result["height"])
62
+ except BridgeError as e:
63
+ raise ScreenCaptureError(f"Bridge screen_size failed: {e}") from e
64
+
65
+ def get_scale_factor(self) -> float:
66
+ try:
67
+ result = self._client.call("scale_factor", timeout=3.0)
68
+ return result.get("factor", 1.0)
69
+ except BridgeError:
70
+ return 1.0
@@ -0,0 +1,187 @@
1
+ """TCP client for the Windows bridge daemon."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import platform
7
+ import socket
8
+ import subprocess
9
+ import threading
10
+ from pathlib import Path
11
+
12
+ from computer_use.bridge.protocol import (
13
+ HEADER_SIZE,
14
+ decode_header,
15
+ encode_message,
16
+ get_port,
17
+ make_request,
18
+ )
19
+
20
+ logger = logging.getLogger("computer_use.bridge.client")
21
+
22
+
23
+ def _is_wsl2_mirrored() -> bool:
24
+ """Check if WSL2 is using mirrored networking mode.
25
+
26
+ With mirrored networking, WSL2 shares the host's network stack so
27
+ localhost reaches Windows directly -- no need for the gateway IP.
28
+ """
29
+ try:
30
+ wslconfig = Path("/mnt/c/Users") / os.environ.get("USER", "") / ".wslconfig"
31
+ if not wslconfig.exists():
32
+ # Try via powershell
33
+ result = subprocess.run(
34
+ ["powershell.exe", "-NoProfile", "-Command", "$env:USERPROFILE"],
35
+ capture_output=True, text=True, timeout=3,
36
+ )
37
+ if result.returncode == 0:
38
+ win_profile = result.stdout.strip()
39
+ wsl_path = subprocess.run(
40
+ ["wslpath", "-u", win_profile],
41
+ capture_output=True, text=True,
42
+ ).stdout.strip()
43
+ wslconfig = Path(wsl_path) / ".wslconfig"
44
+
45
+ if wslconfig.exists():
46
+ content = wslconfig.read_text().lower()
47
+ return "networkingmode=mirrored" in content.replace(" ", "")
48
+ except Exception:
49
+ pass
50
+ return False
51
+
52
+
53
+ def _detect_windows_host() -> str:
54
+ """Auto-detect the Windows host IP when running inside WSL2.
55
+
56
+ On standard WSL2, 127.0.0.1 points to the Linux VM, not Windows.
57
+ The Windows host IP is the default gateway (WSL2 vEthernet adapter).
58
+
59
+ On mirrored networking (networkingMode=mirrored in .wslconfig),
60
+ localhost reaches Windows directly, so 127.0.0.1 works.
61
+
62
+ Returns '127.0.0.1' on non-WSL2 platforms.
63
+ """
64
+ try:
65
+ if "microsoft" in platform.release().lower():
66
+ if _is_wsl2_mirrored():
67
+ logger.debug("WSL2 mirrored networking, using 127.0.0.1")
68
+ return "127.0.0.1"
69
+
70
+ # Default gateway is the Windows host on the WSL2 virtual network
71
+ with open("/proc/net/route") as f:
72
+ for line in f:
73
+ fields = line.strip().split()
74
+ if fields[1] == "00000000": # default route
75
+ # Gateway is in hex, little-endian
76
+ gw_hex = fields[2]
77
+ gw_bytes = bytes.fromhex(gw_hex)
78
+ ip = f"{gw_bytes[3]}.{gw_bytes[2]}.{gw_bytes[1]}.{gw_bytes[0]}"
79
+ logger.debug("WSL2 detected, Windows host IP: %s", ip)
80
+ return ip
81
+ except Exception:
82
+ pass
83
+ return "127.0.0.1"
84
+
85
+
86
+ class BridgeError(Exception):
87
+ pass
88
+
89
+
90
+ class BridgeClient:
91
+ """Connects to the Windows bridge daemon over TCP.
92
+
93
+ On WSL2, auto-discovers the Windows host IP.
94
+ Thread-safe. Reconnects automatically on socket errors (one retry).
95
+ """
96
+
97
+ def __init__(self, host: str | None = None, port: int | None = None):
98
+ self._host = host or _detect_windows_host()
99
+ self._port = port or get_port()
100
+ self._sock: socket.socket | None = None
101
+ self._lock = threading.Lock()
102
+
103
+ def is_available(self) -> bool:
104
+ try:
105
+ result = self.call("ping", timeout=2.0)
106
+ return result.get("pong", False)
107
+ except Exception:
108
+ return False
109
+
110
+ def handshake(self) -> dict | None:
111
+ """Return the raw `ping` response dict, or None if the daemon is unreachable.
112
+
113
+ Separate from `is_available()` because callers (supervisor) need the
114
+ full response to read `version_hash` for drift detection. A running
115
+ daemon returns at least `{"pong": True}`; absent fields are treated
116
+ as drift by the supervisor.
117
+ """
118
+ try:
119
+ return self.call("ping", timeout=2.0)
120
+ except Exception:
121
+ return None
122
+
123
+ def call(self, method: str, params: dict | None = None, timeout: float = 10.0) -> dict:
124
+ with self._lock:
125
+ return self._call_locked(method, params, timeout, retry=True)
126
+
127
+ def _call_locked(
128
+ self, method: str, params: dict | None, timeout: float, retry: bool
129
+ ) -> dict:
130
+ try:
131
+ self._ensure_connected(timeout)
132
+ request = make_request(method, params)
133
+ self._send(encode_message(request))
134
+ response = self._receive(timeout)
135
+ if not response.get("ok", False):
136
+ raise BridgeError(response.get("error", "Unknown daemon error"))
137
+ return response.get("result", {})
138
+ except (socket.error, OSError, ConnectionError) as e:
139
+ self._close_socket()
140
+ if retry:
141
+ logger.debug("Connection lost, retrying: %s", e)
142
+ return self._call_locked(method, params, timeout, retry=False)
143
+ raise BridgeError(f"Bridge connection failed: {e}") from e
144
+
145
+ def _ensure_connected(self, timeout: float) -> None:
146
+ if self._sock is not None:
147
+ return
148
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
149
+ sock.settimeout(timeout)
150
+ sock.connect((self._host, self._port))
151
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
152
+ self._sock = sock
153
+ logger.debug("Connected to bridge at %s:%d", self._host, self._port)
154
+
155
+ def _send(self, data: bytes) -> None:
156
+ self._sock.sendall(data)
157
+
158
+ def _receive(self, timeout: float) -> dict:
159
+ self._sock.settimeout(timeout)
160
+ header = self._recv_exact(HEADER_SIZE)
161
+ length = decode_header(header)
162
+ payload = self._recv_exact(length)
163
+ return json.loads(payload)
164
+
165
+ def _recv_exact(self, n: int) -> bytes:
166
+ buf = bytearray()
167
+ while len(buf) < n:
168
+ chunk = self._sock.recv(n - len(buf))
169
+ if not chunk:
170
+ raise ConnectionError("Bridge daemon closed connection")
171
+ buf.extend(chunk)
172
+ return bytes(buf)
173
+
174
+ def _close_socket(self) -> None:
175
+ if self._sock is not None:
176
+ try:
177
+ self._sock.close()
178
+ except OSError:
179
+ pass
180
+ self._sock = None
181
+
182
+ def close(self) -> None:
183
+ with self._lock:
184
+ self._close_socket()
185
+
186
+ def __del__(self):
187
+ self._close_socket()