meshagent-computers 0.0.7__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.

Potentially problematic release.


This version of meshagent-computers might be problematic. Click here for more details.

@@ -0,0 +1,179 @@
1
+ import subprocess
2
+ import time
3
+ import shlex
4
+ import asyncio
5
+
6
+
7
+ async def _async_check_output(*args, **kwargs):
8
+ proc = await asyncio.create_subprocess_exec(
9
+ *args,
10
+ stdout=asyncio.subprocess.PIPE,
11
+ stderr=asyncio.subprocess.PIPE,
12
+ **kwargs
13
+ )
14
+ stdout, stderr = await proc.communicate()
15
+ if proc.returncode != 0:
16
+ raise subprocess.CalledProcessError(proc.returncode, args, output=stdout, stderr=stderr)
17
+ return stdout
18
+
19
+ class DockerComputer:
20
+ environment = "linux"
21
+ dimensions = (1280, 720) # Default fallback; will be updated in __enter__.
22
+
23
+ def __init__(
24
+ self,
25
+ container_name="cua-sample-app",
26
+ image="ghcr.io/openai/openai-cua-sample-app:latest",
27
+ display=":99",
28
+ port_mapping="5900:5900",
29
+ ):
30
+ self.container_name = container_name
31
+ self.image = image
32
+ self.display = display
33
+ self.port_mapping = port_mapping
34
+
35
+ async def __aenter__(self):
36
+ # Check if the container is running
37
+ result = subprocess.run(
38
+ ["docker", "ps", "-q", "-f", f"name={self.container_name}"],
39
+ capture_output=True,
40
+ text=True,
41
+ )
42
+
43
+ if not result.stdout.strip():
44
+ raise RuntimeError(
45
+ f"Container {self.container_name} is not running. Build and run with:\n"
46
+ f"docker build -t {self.container_name} .\n"
47
+ f"docker run --rm -it --name {self.container_name} "
48
+ f"-p {self.port_mapping} -e DISPLAY={self.display} {self.container_name}"
49
+ )
50
+
51
+ # Fetch display geometry
52
+ geometry = await self._exec(
53
+ f"DISPLAY={self.display} xdotool getdisplaygeometry"
54
+ ).strip()
55
+ if geometry:
56
+ w, h = geometry.split()
57
+ self.dimensions = (int(w), int(h))
58
+ # print("Starting Docker container...")
59
+ # # Run the container detached, removing it automatically when it stops
60
+ # subprocess.check_call(
61
+ # [
62
+ # "docker",
63
+ # "run",
64
+ # "-d",
65
+ # "--rm",
66
+ # "--name",
67
+ # self.container_name,
68
+ # "-p",
69
+ # self.port_mapping,
70
+ # self.image,
71
+ # ]
72
+ # )
73
+ # # Give the container a moment to start
74
+ # time.sleep(3)
75
+ # print("Entering DockerComputer context")
76
+ return self
77
+
78
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
79
+ # print("Stopping Docker container...")
80
+ # subprocess.check_call(["docker", "stop", self.container_name])
81
+ # print("Exiting DockerComputer context")
82
+ pass
83
+
84
+ async def _exec(self, cmd: str) -> str:
85
+ """
86
+ Run 'cmd' in the container.
87
+ We wrap cmd in double quotes and escape any double quotes inside it,
88
+ so spaces or quotes don't break the shell call.
89
+ """
90
+ # Escape any existing double quotes in cmd
91
+ safe_cmd = cmd.replace('"', '\\"')
92
+
93
+ # Then wrap the entire cmd in double quotes for `sh -c`
94
+ docker_cmd = f'docker exec {self.container_name} sh -c "{safe_cmd}"'
95
+
96
+ return (await _async_check_output(docker_cmd, shell=True)).decode(
97
+ "utf-8", errors="ignore"
98
+ )
99
+
100
+ async def screenshot(self) -> str:
101
+ """
102
+ Takes a screenshot with ImageMagick (import), returning base64-encoded PNG.
103
+ Requires 'import'.
104
+ """
105
+ # cmd = (
106
+ # f"export DISPLAY={self.display} && "
107
+ # "import -window root /tmp/screenshot.png && "
108
+ # "base64 /tmp/screenshot.png"
109
+ # )
110
+ cmd = (
111
+ f"export DISPLAY={self.display} && "
112
+ "import -window root png:- | base64 -w 0"
113
+ )
114
+
115
+ return await self._exec(cmd)
116
+
117
+ async def click(self, x: int, y: int, button: str = "left") -> None:
118
+ button_map = {"left": 1, "middle": 2, "right": 3}
119
+ b = button_map.get(button, 1)
120
+ await self._exec(f"DISPLAY={self.display} xdotool mousemove {x} {y} click {b}")
121
+
122
+ async def double_click(self, x: int, y: int) -> None:
123
+ await self._exec(
124
+ f"DISPLAY={self.display} xdotool mousemove {x} {y} click --repeat 2 1"
125
+ )
126
+
127
+ async def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
128
+ """
129
+ For simple vertical scrolling: xdotool click 4 (scroll up) or 5 (scroll down).
130
+ """
131
+ await self._exec(f"DISPLAY={self.display} xdotool mousemove {x} {y}")
132
+ clicks = abs(scroll_y)
133
+ button = 4 if scroll_y < 0 else 5
134
+ for _ in range(clicks):
135
+ await self._exec(f"DISPLAY={self.display} xdotool click {button}")
136
+
137
+ async def type(self, text: str) -> None:
138
+ """
139
+ Type the given text via xdotool, preserving spaces and quotes.
140
+ """
141
+ # Escape single quotes in the user text: ' -> '\'\''
142
+ safe_text = text.replace("'", "'\\''")
143
+ # Then wrap everything in single quotes for xdotool
144
+ cmd = f"DISPLAY={self.display} xdotool type -- '{safe_text}'"
145
+ await self._exec(cmd)
146
+
147
+ async def wait(self, ms: int = 1000) -> None:
148
+ time.sleep(ms / 1000)
149
+
150
+ async def move(self, x: int, y: int) -> None:
151
+ await self._exec(f"DISPLAY={self.display} xdotool mousemove {x} {y}")
152
+
153
+ async def keypress(self, keys: list[str]) -> None:
154
+ mapping = {
155
+ "ENTER": "Return",
156
+ "LEFT": "Left",
157
+ "RIGHT": "Right",
158
+ "UP": "Up",
159
+ "DOWN": "Down",
160
+ "ESC": "Escape",
161
+ "SPACE": "space",
162
+ "BACKSPACE": "BackSpace",
163
+ "TAB": "Tab",
164
+ }
165
+ mapped_keys = [mapping.get(key, key) for key in keys]
166
+ combo = "+".join(mapped_keys)
167
+ await self._exec(f"DISPLAY={self.display} xdotool key {combo}")
168
+
169
+ async def drag(self, path: list[dict[str, int]]) -> None:
170
+ if not path:
171
+ return
172
+ start_x = path[0]["x"]
173
+ start_y = path[0]["y"]
174
+ self._exec(
175
+ f"DISPLAY={self.display} xdotool mousemove {start_x} {start_y} mousedown 1"
176
+ )
177
+ for point in path[1:]:
178
+ await self._exec(f"DISPLAY={self.display} xdotool mousemove {point['x']} {point['y']}")
179
+ await self._exec(f"DISPLAY={self.display} xdotool mouseup 1")
@@ -0,0 +1,24 @@
1
+ from playwright.async_api import Browser, Page
2
+ from .base_playwright import BasePlaywrightComputer
3
+
4
+
5
+ class LocalPlaywrightComputer(BasePlaywrightComputer):
6
+ """Launches a local Chromium instance using Playwright."""
7
+
8
+ def __init__(self, headless: bool = False):
9
+ super().__init__()
10
+ self.headless = headless
11
+
12
+ async def _get_browser_and_page(self) -> tuple[Browser, Page]:
13
+ width, height = self.dimensions
14
+ launch_args = [f"--window-size={width},{height}", "--disable-extensions", "--disable-file-system"]
15
+ browser = await self._playwright.chromium.launch(
16
+ chromium_sandbox=True,
17
+ headless=self.headless,
18
+ args=launch_args,
19
+ env={}
20
+ )
21
+ page = await browser.new_page()
22
+ await page.set_viewport_size({"width": width, "height": height})
23
+ await page.goto("https://google.com")
24
+ return browser, page
@@ -0,0 +1,78 @@
1
+ from .computer import Computer
2
+ from .utils import check_blocklisted_url
3
+ import json
4
+
5
+ class Operator:
6
+ def __init__(self):
7
+ self.print_steps = False
8
+ self.show_images = False
9
+
10
+ async def acknowledge_safety_check_callback(self, data: dict):
11
+ return True
12
+
13
+ async def show_image(self, base_64: str):
14
+ pass
15
+
16
+ async def play(self, *, computer: Computer, item: dict) -> list:
17
+ """Handle each item; may cause a computer action + screenshot."""
18
+ if item["type"] == "message":
19
+ if self.print_steps:
20
+ print(item["content"][0]["text"])
21
+
22
+ if item["type"] == "function_call":
23
+ name, args = item["name"], json.loads(item["arguments"])
24
+ if self.print_steps:
25
+ print(f"{name}({args})")
26
+
27
+ if hasattr(computer, name): # if function exists on computer, call it
28
+ method = getattr(computer, name)
29
+ await method(**args)
30
+ return [
31
+ {
32
+ "type": "function_call_output",
33
+ "call_id": item["call_id"],
34
+ "output": "success", # hard-coded output for demo
35
+ }
36
+ ]
37
+
38
+ if item["type"] == "computer_call":
39
+ action = item["action"]
40
+ action_type = action["type"]
41
+ action_args = {k: v for k, v in action.items() if k != "type"}
42
+ if self.print_steps:
43
+ print(f"{action_type}({action_args})")
44
+
45
+ method = getattr(computer, action_type)
46
+ await method(**action_args)
47
+
48
+ screenshot_base64 = await computer.screenshot()
49
+ if self.show_images:
50
+ self.show_image(screenshot_base64)
51
+
52
+ # if user doesn't ack all safety checks exit with error
53
+ pending_checks = item.get("pending_safety_checks", [])
54
+ for check in pending_checks:
55
+ message = check["message"]
56
+ if not await self.acknowledge_safety_check_callback(message):
57
+ raise ValueError(
58
+ f"Safety check failed: {message}. Cannot continue with unacknowledged safety checks."
59
+ )
60
+
61
+ call_output = {
62
+ "type": "computer_call_output",
63
+ "call_id": item["call_id"],
64
+ "acknowledged_safety_checks": pending_checks,
65
+ "output": {
66
+ "type": "input_image",
67
+ "image_url": f"data:image/png;base64,{screenshot_base64}",
68
+ },
69
+ }
70
+
71
+ # additional URL safety checks for browser environments
72
+ if computer.environment == "browser":
73
+ current_url = await computer.get_current_url()
74
+ check_blocklisted_url(current_url)
75
+ call_output["output"]["current_url"] = current_url
76
+
77
+ return [call_output]
78
+ return []
@@ -0,0 +1,212 @@
1
+ import os
2
+ import time
3
+ from dotenv import load_dotenv
4
+ from scrapybara import AsyncScrapybara
5
+ from playwright.async_api import async_playwright, Browser, Page
6
+ from meshagent.computers.utils import BLOCKED_DOMAINS
7
+
8
+ load_dotenv()
9
+
10
+ CUA_KEY_TO_SCRAPYBARA_KEY = {
11
+ "/": "slash",
12
+ "\\": "backslash",
13
+ "arrowdown": "Down",
14
+ "arrowleft": "Left",
15
+ "arrowright": "Right",
16
+ "arrowup": "Up",
17
+ "backspace": "BackSpace",
18
+ "capslock": "Caps_Lock",
19
+ "cmd": "Meta_L",
20
+ "delete": "Delete",
21
+ "end": "End",
22
+ "enter": "Return",
23
+ "esc": "Escape",
24
+ "home": "Home",
25
+ "insert": "Insert",
26
+ "option": "Alt_L",
27
+ "pagedown": "Page_Down",
28
+ "pageup": "Page_Up",
29
+ "tab": "Tab",
30
+ "win": "Meta_L",
31
+ }
32
+
33
+
34
+ class ScrapybaraBrowser:
35
+ """
36
+ Scrapybara provides virtual desktops and browsers in the cloud. https://scrapybara.com
37
+ You can try OpenAI CUA for free at https://computer.new or read our CUA Quickstart at https://computer.new/cua.
38
+ """
39
+
40
+ def __init__(self):
41
+ self.client = AsyncScrapybara(api_key=os.getenv("SCRAPYBARA_API_KEY"))
42
+ self.environment = "browser"
43
+ self.dimensions = (1024, 768)
44
+ self._playwright = None
45
+ self._browser: Browser | None = None
46
+ self._page: Page | None = None
47
+
48
+ async def __aenter__(self):
49
+ print("Starting scrapybara browser")
50
+ blocked_domains = [
51
+ domain.replace("https://", "").replace("www.", "")
52
+ for domain in BLOCKED_DOMAINS
53
+ ]
54
+ self.instance = await self.client.start_browser(blocked_domains=blocked_domains)
55
+ print("Scrapybara browser started ₍ᐢ•(ܫ)•ᐢ₎")
56
+ print(
57
+ f"You can view and interact with the stream at {self.instance.get_stream_url().stream_url}"
58
+ )
59
+ self._playwright_context = async_playwright()
60
+ self._playwright = await self._playwright_context.__aenter__()
61
+ self._browser = await self._playwright.chromium.connect_over_cdp(
62
+ (await self.instance.get_cdp_url()).cdp_url
63
+ )
64
+ self._page = self._browser.contexts[0].pages[0]
65
+ return self
66
+
67
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
68
+ await self._playwright_context.__aexit__(exc_type, exc_val, exc_tb)
69
+
70
+ print("Stopping scrapybara browser")
71
+ await self.instance.stop()
72
+ print("Scrapybara browser stopped ₍ᐢ-(ェ)-ᐢ₎")
73
+
74
+ async def goto(self, url: str) -> None:
75
+ await self._page.goto(url)
76
+
77
+ async def get_current_url(self) -> str:
78
+ return (await self.instance.get_current_url()).current_url
79
+
80
+ async def screenshot(self) -> str:
81
+ return (await self.instance.screenshot()).base_64_image
82
+
83
+ async def click(self, x: int, y: int, button: str = "left") -> None:
84
+ button = "middle" if button == "wheel" else button
85
+ await self.instance.computer(
86
+ action="click_mouse",
87
+ click_type="click",
88
+ button=button,
89
+ coordinates=[x, y],
90
+ num_clicks=1,
91
+ )
92
+
93
+ async def double_click(self, x: int, y: int) -> None:
94
+ await self.instance.computer(
95
+ action="click_mouse",
96
+ click_type="click",
97
+ button="left",
98
+ coordinates=[x, y],
99
+ num_clicks=2,
100
+ )
101
+
102
+ async def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
103
+ await self.instance.computer(
104
+ action="scroll",
105
+ coordinates=[x, y],
106
+ delta_x=scroll_x // 20,
107
+ delta_y=scroll_y // 20,
108
+ )
109
+
110
+ async def type(self, text: str) -> None:
111
+ await self.instance.computer(action="type_text", text=text)
112
+
113
+ async def wait(self, ms: int = 1000) -> None:
114
+ time.sleep(ms / 1000)
115
+ # Scrapybara also has `self.instance.computer(action="wait", duration=ms / 1000)`
116
+
117
+ async def move(self, x: int, y: int) -> None:
118
+ await self.instance.computer(action="move_mouse", coordinates=[x, y])
119
+
120
+ async def keypress(self, keys: list[str]) -> None:
121
+ mapped_keys = [
122
+ CUA_KEY_TO_SCRAPYBARA_KEY.get(key.lower(), key.lower()) for key in keys
123
+ ]
124
+ await self.instance.computer(action="press_key", keys=mapped_keys)
125
+
126
+ async def drag(self, path: list[dict[str, int]]) -> None:
127
+ if not path:
128
+ return
129
+ path = [[point["x"], point["y"]] for point in path]
130
+ await self.instance.computer(action="drag_mouse", path=path)
131
+
132
+
133
+ class ScrapybaraUbuntu:
134
+ """
135
+ Scrapybara provides virtual desktops and browsers in the cloud.
136
+ You can try OpenAI CUA for free at https://computer.new or read our CUA Quickstart at https://computer.new/cua.
137
+ """
138
+
139
+ def __init__(self):
140
+ self.client = AsyncScrapybara(api_key=os.getenv("SCRAPYBARA_API_KEY"))
141
+ self.environment = "linux" # "windows", "mac", "linux", or "browser"
142
+ self.dimensions = (1024, 768)
143
+
144
+ async def __aenter__(self):
145
+ print("Starting Scrapybara Ubuntu instance")
146
+ blocked_domains = [
147
+ domain.replace("https://", "").replace("www.", "")
148
+ for domain in BLOCKED_DOMAINS
149
+ ]
150
+ self.instance = await self.client.start_ubuntu(blocked_domains=blocked_domains)
151
+ print("Scrapybara Ubuntu instance started ₍ᐢ•(ܫ)•ᐢ₎")
152
+ print(
153
+ f"You can view and interact with the stream at {self.instance.get_stream_url().stream_url}"
154
+ )
155
+ return self
156
+
157
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
158
+ print("Stopping Scrapybara Ubuntu instance")
159
+ (await self.instance).stop()
160
+ print("Scrapybara Ubuntu instance stopped ₍ᐢ-(ェ)-ᐢ₎")
161
+
162
+ async def screenshot(self) -> str:
163
+ return (await self.instance.screenshot()).base_64_image
164
+
165
+ async def click(self, x: int, y: int, button: str = "left") -> None:
166
+ button = "middle" if button == "wheel" else button
167
+ await self.instance.computer(
168
+ action="click_mouse",
169
+ click_type="click",
170
+ button=button,
171
+ coordinates=[x, y],
172
+ num_clicks=1,
173
+ )
174
+
175
+ async def double_click(self, x: int, y: int) -> None:
176
+ await self.instance.computer(
177
+ action="click_mouse",
178
+ click_type="click",
179
+ button="left",
180
+ coordinates=[x, y],
181
+ num_clicks=2,
182
+ )
183
+
184
+ async def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
185
+ await self.instance.computer(
186
+ action="scroll",
187
+ coordinates=[x, y],
188
+ delta_x=scroll_x // 20,
189
+ delta_y=scroll_y // 20,
190
+ )
191
+
192
+ async def type(self, text: str) -> None:
193
+ await self.instance.computer(action="type_text", text=text)
194
+
195
+ async def wait(self, ms: int = 1000) -> None:
196
+ time.sleep(ms / 1000)
197
+ # Scrapybara also has `self.instance.computer(action="wait", duration=ms / 1000)`
198
+
199
+ async def move(self, x: int, y: int) -> None:
200
+ await self.instance.computer(action="move_mouse", coordinates=[x, y])
201
+
202
+ async def keypress(self, keys: list[str]) -> None:
203
+ mapped_keys = [
204
+ CUA_KEY_TO_SCRAPYBARA_KEY.get(key.lower(), key.lower()) for key in keys
205
+ ]
206
+ await self.instance.computer(action="press_key", keys=mapped_keys)
207
+
208
+ async def drag(self, path: list[dict[str, int]]) -> None:
209
+ if not path:
210
+ return
211
+ path = [[point["x"], point["y"]] for point in path]
212
+ await self.instance.computer(action="drag_mouse", path=path)
@@ -0,0 +1,78 @@
1
+ import os
2
+ import requests
3
+ from dotenv import load_dotenv
4
+ import json
5
+ import base64
6
+ from PIL import Image
7
+ from io import BytesIO
8
+ import io
9
+ from urllib.parse import urlparse
10
+
11
+ load_dotenv(override=True)
12
+
13
+ BLOCKED_DOMAINS = [
14
+ "maliciousbook.com",
15
+ "evilvideos.com",
16
+ "darkwebforum.com",
17
+ "shadytok.com",
18
+ "suspiciouspins.com",
19
+ "ilanbigio.com",
20
+ ]
21
+
22
+
23
+ def pp(obj):
24
+ print(json.dumps(obj, indent=4))
25
+
26
+
27
+ def show_image(base_64_image):
28
+ image_data = base64.b64decode(base_64_image)
29
+ image = Image.open(BytesIO(image_data))
30
+ image.show()
31
+
32
+
33
+ def calculate_image_dimensions(base_64_image):
34
+ image_data = base64.b64decode(base_64_image)
35
+ image = Image.open(io.BytesIO(image_data))
36
+ return image.size
37
+
38
+
39
+ def sanitize_message(msg: dict) -> dict:
40
+ """Return a copy of the message with image_url omitted for computer_call_output messages."""
41
+ if msg.get("type") == "computer_call_output":
42
+ output = msg.get("output", {})
43
+ if isinstance(output, dict):
44
+ sanitized = msg.copy()
45
+ sanitized["output"] = {**output, "image_url": "[omitted]"}
46
+ return sanitized
47
+ return msg
48
+
49
+
50
+ def create_response(**kwargs):
51
+ url = "https://api.openai.com/v1/responses"
52
+ headers = {
53
+ "Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
54
+ "Content-Type": "application/json",
55
+ # TODO: remove for launch
56
+ "Openai-beta": "responses=v1",
57
+ }
58
+
59
+ openai_org = os.getenv("OPENAI_ORG")
60
+ if openai_org:
61
+ headers["Openai-Organization"] = openai_org
62
+
63
+ response = requests.post(url, headers=headers, json=kwargs)
64
+
65
+ if response.status_code != 200:
66
+ print(f"Error: {response.status_code} {response.text}")
67
+
68
+ return response.json()
69
+
70
+
71
+ def check_blocklisted_url(url: str) -> None:
72
+ """Raise ValueError if the given URL (including subdomains) is in the blocklist."""
73
+ hostname = urlparse(url).hostname or ""
74
+ if any(
75
+ hostname == blocked or hostname.endswith(f".{blocked}")
76
+ for blocked in BLOCKED_DOMAINS
77
+ ):
78
+ raise ValueError(f"Blocked URL: {url}")
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: meshagent-computers
3
+ Version: 0.0.7
4
+ Summary: Computer Building Blocks for Meshagent
5
+ Home-page:
6
+ License: Apache License 2.0
7
+ Project-URL: Documentation, https://meshagent.com
8
+ Project-URL: Website, https://meshagent.com
9
+ Project-URL: Source, https://github.com/meshagent
10
+ Requires-Python: >=3.9.0
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: pytest>=8.3.4
14
+ Requires-Dist: pytest-asyncio>=0.24.0
15
+ Requires-Dist: openai>=1.66.2
16
+ Requires-Dist: meshagent-api>=0.0.7
17
+ Requires-Dist: meshagent-agents>=0.0.7
18
+ Requires-Dist: meshagent-tools>=0.0.7
19
+ Requires-Dist: playwright~=1.50.0
20
+ Requires-Dist: browserbase~=1.2.0
21
+ Requires-Dist: scrapybara~=2.4.1
22
+ Dynamic: description-content-type
23
+ Dynamic: license
24
+ Dynamic: license-file
25
+ Dynamic: project-url
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
@@ -0,0 +1,15 @@
1
+ meshagent/computers/__init__.py,sha256=4aVM46U1kVLeN8rurNulc6_24a-rHSMIY0v0rc1ydOI,288
2
+ meshagent/computers/agent.py,sha256=w9Yg9I2cfx-fNo9Y0TOcndxcoyOTz2wxRoijxGt9dB4,9089
3
+ meshagent/computers/base_playwright.py,sha256=NhQoOzWmekBmobQ2kvN7w4I3hZeyMIuMKI2eHbG1_mk,6017
4
+ meshagent/computers/browserbase.py,sha256=ZT-ZDndkBD-MHD3UjxXpwpx7-_VCkkanjW1rIJr7-98,8161
5
+ meshagent/computers/computer.py,sha256=465GKDV3cJgzp1SB5B0xzLoftMKU0AxAuBVrZ9drBBc,1072
6
+ meshagent/computers/docker.py,sha256=BGkrlspIYlg3WViaqctyRUye-fONfF40AvLXOQuXPQ8,6331
7
+ meshagent/computers/local_playwright.py,sha256=W7VhxIYEXn2BO3wZZVsc-hH-CU7NTssxFGO7pwz_n1I,919
8
+ meshagent/computers/operator.py,sha256=JTyGvoPnKOqUDaZv58p_2n5Q6m6dm8V214VMIhVEu08,2913
9
+ meshagent/computers/scrapybara.py,sha256=IoeV02QcBDkpW05A1NH-lc4jnBEmZ4SPsRo3nnXC3UE,7502
10
+ meshagent/computers/utils.py,sha256=t9G9dzfJ5a0DSyrW1KNdzlhLJW_S6eGUcS39wnZyHU8,2081
11
+ meshagent_computers-0.0.7.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
12
+ meshagent_computers-0.0.7.dist-info/METADATA,sha256=eYIzYXp2Ia_WFN1mjBrF7u9iPk08P-MZLifVB56PqRM,852
13
+ meshagent_computers-0.0.7.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
14
+ meshagent_computers-0.0.7.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
15
+ meshagent_computers-0.0.7.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (77.0.3)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+