use-computer 0.0.1__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.
- mmini/__init__.py +48 -0
- mmini/ax_transpile.py +504 -0
- mmini/client.py +362 -0
- mmini/display.py +39 -0
- mmini/errors.py +68 -0
- mmini/ios/__init__.py +14 -0
- mmini/ios/apps.py +57 -0
- mmini/ios/environment.py +49 -0
- mmini/ios/input.py +118 -0
- mmini/macos/__init__.py +4 -0
- mmini/macos/keyboard.py +59 -0
- mmini/macos/mouse.py +105 -0
- mmini/models.py +79 -0
- mmini/parsers.py +308 -0
- mmini/py.typed +1 -0
- mmini/recording.py +88 -0
- mmini/retry.py +135 -0
- mmini/sandbox.py +419 -0
- mmini/screenshot.py +63 -0
- mmini/tasks/__init__.py +307 -0
- mmini/tasks/templates/pre_command.sh +2 -0
- mmini/tasks/templates/task.toml +18 -0
- mmini/tasks/templates/test_ios.sh +20 -0
- mmini/tasks/templates/test_ios_nograder.sh +4 -0
- mmini/tasks/templates/test_macos.sh +10 -0
- mmini/tasks/templates/test_macos_check.sh +10 -0
- mmini/tasks/templates/test_macos_nograder.sh +7 -0
- use_computer/__init__.py +15 -0
- use_computer/py.typed +1 -0
- use_computer-0.0.1.dist-info/METADATA +133 -0
- use_computer-0.0.1.dist-info/RECORD +32 -0
- use_computer-0.0.1.dist-info/WHEEL +4 -0
mmini/screenshot.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Screenshot:
|
|
7
|
+
def __init__(self, http: httpx.Client, prefix: str):
|
|
8
|
+
self._http = http
|
|
9
|
+
self._prefix = prefix
|
|
10
|
+
|
|
11
|
+
def take_full_screen(self, show_cursor: bool = False) -> bytes:
|
|
12
|
+
resp = self._http.get(f"{self._prefix}/screenshot", params={"show_cursor": show_cursor})
|
|
13
|
+
resp.raise_for_status()
|
|
14
|
+
return resp.content
|
|
15
|
+
|
|
16
|
+
def take_region(self, x: int, y: int, width: int, height: int) -> bytes:
|
|
17
|
+
resp = self._http.get(
|
|
18
|
+
f"{self._prefix}/screenshot/region",
|
|
19
|
+
params={"x": x, "y": y, "width": width, "height": height},
|
|
20
|
+
)
|
|
21
|
+
resp.raise_for_status()
|
|
22
|
+
return resp.content
|
|
23
|
+
|
|
24
|
+
def take_compressed(
|
|
25
|
+
self, format: str = "jpeg", quality: int = 80, scale: float | None = None
|
|
26
|
+
) -> bytes:
|
|
27
|
+
params: dict = {"format": format, "quality": quality}
|
|
28
|
+
if scale is not None:
|
|
29
|
+
params["scale"] = scale
|
|
30
|
+
resp = self._http.get(f"{self._prefix}/screenshot/compressed", params=params)
|
|
31
|
+
resp.raise_for_status()
|
|
32
|
+
return resp.content
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncScreenshot:
|
|
36
|
+
def __init__(self, http: httpx.AsyncClient, prefix: str):
|
|
37
|
+
self._http = http
|
|
38
|
+
self._prefix = prefix
|
|
39
|
+
|
|
40
|
+
async def take_full_screen(self, show_cursor: bool = False) -> bytes:
|
|
41
|
+
resp = await self._http.get(
|
|
42
|
+
f"{self._prefix}/screenshot", params={"show_cursor": show_cursor}
|
|
43
|
+
)
|
|
44
|
+
resp.raise_for_status()
|
|
45
|
+
return resp.content
|
|
46
|
+
|
|
47
|
+
async def take_region(self, x: int, y: int, width: int, height: int) -> bytes:
|
|
48
|
+
resp = await self._http.get(
|
|
49
|
+
f"{self._prefix}/screenshot/region",
|
|
50
|
+
params={"x": x, "y": y, "width": width, "height": height},
|
|
51
|
+
)
|
|
52
|
+
resp.raise_for_status()
|
|
53
|
+
return resp.content
|
|
54
|
+
|
|
55
|
+
async def take_compressed(
|
|
56
|
+
self, format: str = "jpeg", quality: int = 80, scale: float | None = None
|
|
57
|
+
) -> bytes:
|
|
58
|
+
params: dict = {"format": format, "quality": quality}
|
|
59
|
+
if scale is not None:
|
|
60
|
+
params["scale"] = scale
|
|
61
|
+
resp = await self._http.get(f"{self._prefix}/screenshot/compressed", params=params)
|
|
62
|
+
resp.raise_for_status()
|
|
63
|
+
return resp.content
|
mmini/tasks/__init__.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Retrieve and export collected tasks from the gateway."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from mmini.ax_transpile import patch_curl_timeouts, transpile
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_TEMPLATES = Path(__file__).parent / "templates"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _tpl(name: str) -> str:
|
|
22
|
+
return (_TEMPLATES / name).read_text(encoding="utf-8")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _render(text: str, **kwargs: str) -> str:
|
|
26
|
+
out = text
|
|
27
|
+
for k, v in kwargs.items():
|
|
28
|
+
out = out.replace("{{" + k + "}}", v)
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class TaskSummary:
|
|
34
|
+
"""Lightweight task info from the list endpoint."""
|
|
35
|
+
|
|
36
|
+
id: str
|
|
37
|
+
task_name: str
|
|
38
|
+
instruction: str
|
|
39
|
+
category: str
|
|
40
|
+
platform: str
|
|
41
|
+
step_count: int
|
|
42
|
+
has_grader: bool
|
|
43
|
+
created_at: str
|
|
44
|
+
completed_at: str | None = None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def runnable(self) -> bool:
|
|
48
|
+
"""A task is runnable only if it has a grader."""
|
|
49
|
+
return self.has_grader
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Task:
|
|
54
|
+
"""Full task detail from the gateway."""
|
|
55
|
+
|
|
56
|
+
id: str
|
|
57
|
+
sandbox_id: str
|
|
58
|
+
platform: str
|
|
59
|
+
phase: str
|
|
60
|
+
task_name: str
|
|
61
|
+
instruction: str
|
|
62
|
+
category: str
|
|
63
|
+
setup_commands: list
|
|
64
|
+
steps: list
|
|
65
|
+
app_state: dict
|
|
66
|
+
accessibility_tree: dict | None = None
|
|
67
|
+
grader: str = ""
|
|
68
|
+
setup_actions: list = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def runnable(self) -> bool:
|
|
72
|
+
return self.grader != ""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TasksClient:
|
|
76
|
+
"""Client for the gateway's task collection endpoints."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, http: httpx.Client):
|
|
79
|
+
self._http = http
|
|
80
|
+
|
|
81
|
+
def list(self, limit: int = 50, offset: int = 0) -> builtins.list[TaskSummary]:
|
|
82
|
+
resp = self._http.get("/admin/tasks", params={"limit": limit, "offset": offset})
|
|
83
|
+
resp.raise_for_status()
|
|
84
|
+
return [
|
|
85
|
+
TaskSummary(
|
|
86
|
+
id=t["id"],
|
|
87
|
+
task_name=t.get("task_name", ""),
|
|
88
|
+
instruction=t.get("instruction", ""),
|
|
89
|
+
category=t.get("category", ""),
|
|
90
|
+
platform=t.get("platform", "macos"),
|
|
91
|
+
step_count=t.get("step_count", 0),
|
|
92
|
+
has_grader=t.get("has_grader", False),
|
|
93
|
+
created_at=t.get("created_at", ""),
|
|
94
|
+
completed_at=t.get("completed_at"),
|
|
95
|
+
)
|
|
96
|
+
for t in resp.json()
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
def get(self, task_id: str) -> Task:
|
|
100
|
+
resp = self._http.get(f"/admin/tasks/{task_id}")
|
|
101
|
+
resp.raise_for_status()
|
|
102
|
+
d = resp.json()
|
|
103
|
+
meta = d.get("task_meta", {})
|
|
104
|
+
return Task(
|
|
105
|
+
id=d["id"],
|
|
106
|
+
sandbox_id=d.get("sandbox_id", ""),
|
|
107
|
+
platform=d.get("platform", meta.get("platform", "macos")),
|
|
108
|
+
phase=d.get("phase", ""),
|
|
109
|
+
task_name=meta.get("name", ""),
|
|
110
|
+
instruction=meta.get("instruction", ""),
|
|
111
|
+
category=meta.get("category", ""),
|
|
112
|
+
setup_commands=d.get("setup_commands", []),
|
|
113
|
+
steps=d.get("steps", []),
|
|
114
|
+
app_state=d.get("app_state", {}),
|
|
115
|
+
accessibility_tree=d.get("accessibility_tree"),
|
|
116
|
+
grader=d.get("grader", ""),
|
|
117
|
+
setup_actions=d.get("setup_actions") or [],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def export_harbor(
|
|
121
|
+
self,
|
|
122
|
+
task_id: str,
|
|
123
|
+
output_dir: str | Path,
|
|
124
|
+
*,
|
|
125
|
+
overwrite: bool = False,
|
|
126
|
+
) -> Path:
|
|
127
|
+
task = self.get(task_id)
|
|
128
|
+
|
|
129
|
+
# Fetch each persisted upload via /admin/tasks/{id}/files/{name}.
|
|
130
|
+
# Returns None on 404 — task_to_harbor logs + skips.
|
|
131
|
+
def fetch_file(local_name: str) -> bytes | None:
|
|
132
|
+
r = self._http.get(f"/admin/tasks/{task_id}/files/{local_name}")
|
|
133
|
+
if r.status_code != 200:
|
|
134
|
+
return None
|
|
135
|
+
return r.content
|
|
136
|
+
|
|
137
|
+
return task_to_harbor(task, Path(output_dir), overwrite=overwrite, fetch_file=fetch_file)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _build_test_sh(task: Task) -> str:
|
|
141
|
+
"""iOS = JSON checker DSL POSTed to /grade. macOS = bash with perl-alarm wrap."""
|
|
142
|
+
if task.platform == "ios":
|
|
143
|
+
return _build_test_sh_ios(task)
|
|
144
|
+
return _build_test_sh_macos(task)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _build_test_sh_ios(task: Task) -> str:
|
|
148
|
+
grader = (task.grader or "").strip()
|
|
149
|
+
if not grader.startswith("["):
|
|
150
|
+
return _tpl("test_ios_nograder.sh")
|
|
151
|
+
specs = grader.replace("'", "'\\''")
|
|
152
|
+
return _render(_tpl("test_ios.sh"), SPECS=specs)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _build_test_sh_macos(task: Task) -> str:
|
|
156
|
+
grader = (task.grader or "").strip()
|
|
157
|
+
if not grader:
|
|
158
|
+
return _tpl("test_macos_nograder.sh")
|
|
159
|
+
check_tpl = _tpl("test_macos_check.sh")
|
|
160
|
+
checks = _render(check_tpl, N="1", CMD=grader.replace("'", "'\\''"))
|
|
161
|
+
out = _render(_tpl("test_macos.sh"), CHECKS=checks)
|
|
162
|
+
out, _ = transpile(out)
|
|
163
|
+
out, _ = patch_curl_timeouts(out)
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _build_pre_command_sh(task: Task) -> str | None:
|
|
168
|
+
# `defaults delete` fails on fresh VMs (key never set). Tasks recording such
|
|
169
|
+
# commands likely rely on the cleanup; skip the whole task instead of
|
|
170
|
+
# silently shipping a broken pre_command.
|
|
171
|
+
for c in task.setup_commands:
|
|
172
|
+
if "defaults delete" in c:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"task setup_commands contain `defaults delete` — skipping export: {c!r}"
|
|
175
|
+
)
|
|
176
|
+
if not task.setup_commands:
|
|
177
|
+
return None
|
|
178
|
+
return _render(_tpl("pre_command.sh"), COMMANDS="\n".join(task.setup_commands))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def task_to_harbor(
|
|
182
|
+
task: Task,
|
|
183
|
+
output_root: Path,
|
|
184
|
+
*,
|
|
185
|
+
overwrite: bool = False,
|
|
186
|
+
fetch_file=None,
|
|
187
|
+
) -> Path:
|
|
188
|
+
"""Convert a collected Task into a Harbor task directory.
|
|
189
|
+
|
|
190
|
+
Layout:
|
|
191
|
+
{category}__{task_id}/
|
|
192
|
+
instruction.md
|
|
193
|
+
task.toml
|
|
194
|
+
environment/
|
|
195
|
+
tests/
|
|
196
|
+
test.sh
|
|
197
|
+
setup/
|
|
198
|
+
pre_command.sh
|
|
199
|
+
files/
|
|
200
|
+
manifest.json
|
|
201
|
+
<local_name>...
|
|
202
|
+
|
|
203
|
+
`fetch_file(local_name) -> bytes | None` fetches collect-time file uploads
|
|
204
|
+
from the gateway so the runner's setup.py can re-upload them. Pass None to
|
|
205
|
+
skip — the resulting harbor dir will have no file uploads.
|
|
206
|
+
"""
|
|
207
|
+
output_root = Path(output_root)
|
|
208
|
+
category = task.category or "uncategorized"
|
|
209
|
+
short_id = task.id.replace("col-", "")[:36]
|
|
210
|
+
task_dir = output_root / f"{category}__{short_id}"
|
|
211
|
+
|
|
212
|
+
if task_dir.exists() and not overwrite:
|
|
213
|
+
raise FileExistsError(f"Already exists: {task_dir}")
|
|
214
|
+
|
|
215
|
+
# Build pre_command early so we fail before creating any dirs if all setup
|
|
216
|
+
# commands got stripped (raises ValueError).
|
|
217
|
+
pre_cmd = _build_pre_command_sh(task)
|
|
218
|
+
|
|
219
|
+
if task_dir.exists():
|
|
220
|
+
shutil.rmtree(task_dir)
|
|
221
|
+
|
|
222
|
+
task_dir.mkdir(parents=True)
|
|
223
|
+
(task_dir / "environment").mkdir()
|
|
224
|
+
tests_dir = task_dir / "tests"
|
|
225
|
+
tests_dir.mkdir()
|
|
226
|
+
|
|
227
|
+
# instruction.md
|
|
228
|
+
(task_dir / "instruction.md").write_text(task.instruction + "\n", encoding="utf-8")
|
|
229
|
+
|
|
230
|
+
# task.toml
|
|
231
|
+
tags = ["collected", "gui", task.platform]
|
|
232
|
+
if category:
|
|
233
|
+
tags.append(category)
|
|
234
|
+
toml = _render(
|
|
235
|
+
_tpl("task.toml"),
|
|
236
|
+
TAGS=json.dumps(tags),
|
|
237
|
+
PLATFORM=task.platform,
|
|
238
|
+
RUNNABLE="true" if task.runnable else "false",
|
|
239
|
+
)
|
|
240
|
+
(task_dir / "task.toml").write_text(toml, encoding="utf-8")
|
|
241
|
+
|
|
242
|
+
# actions.json — flat {steps: [{function, args}, ...]} for the debug agent's
|
|
243
|
+
# replay path. Pulls the first tool_call from each ATIF step; non-tool steps
|
|
244
|
+
# (text-only assistant messages) drop out.
|
|
245
|
+
actions = []
|
|
246
|
+
for step in task.steps or []:
|
|
247
|
+
tcs = step.get("tool_calls") if isinstance(step, dict) else None
|
|
248
|
+
if not tcs:
|
|
249
|
+
continue
|
|
250
|
+
tc = tcs[0]
|
|
251
|
+
actions.append({"function": tc.get("function"), "args": tc.get("args") or {}})
|
|
252
|
+
(task_dir / "actions.json").write_text(
|
|
253
|
+
json.dumps({"steps": actions}, indent=2) + "\n", encoding="utf-8"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# tests/setup/pre_command.sh — skip directory entirely if no setup commands
|
|
257
|
+
setup_dir = tests_dir / "setup"
|
|
258
|
+
if pre_cmd:
|
|
259
|
+
setup_dir.mkdir(exist_ok=True)
|
|
260
|
+
pre_cmd_path = setup_dir / "pre_command.sh"
|
|
261
|
+
pre_cmd_path.write_text(pre_cmd, encoding="utf-8")
|
|
262
|
+
pre_cmd_path.chmod(0o755)
|
|
263
|
+
|
|
264
|
+
# tests/setup/files/<local_name> + manifest.json — files captured at
|
|
265
|
+
# collect time. The runner's setup.py reads manifest.json and uploads
|
|
266
|
+
# each entry to its remote_path before pre_command runs.
|
|
267
|
+
upload_actions = [
|
|
268
|
+
sa
|
|
269
|
+
for sa in (task.setup_actions or [])
|
|
270
|
+
if (sa.get("action_type") if isinstance(sa, dict) else None) == "upload_file"
|
|
271
|
+
]
|
|
272
|
+
if upload_actions:
|
|
273
|
+
if fetch_file is None:
|
|
274
|
+
logger.warning(
|
|
275
|
+
"task has %d upload_file actions but no fetch_file callback — "
|
|
276
|
+
"skipping (runner won't have collected files)",
|
|
277
|
+
len(upload_actions),
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
files_dir = setup_dir / "files"
|
|
281
|
+
files_dir.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
manifest = []
|
|
283
|
+
for sa in upload_actions:
|
|
284
|
+
params = sa.get("params") or {}
|
|
285
|
+
remote_path = params.get("remote_path")
|
|
286
|
+
local_name = params.get("local_name")
|
|
287
|
+
if not remote_path or not local_name:
|
|
288
|
+
continue
|
|
289
|
+
data = fetch_file(local_name)
|
|
290
|
+
if data is None:
|
|
291
|
+
logger.warning("collected file missing on gateway: %s", local_name)
|
|
292
|
+
continue
|
|
293
|
+
(files_dir / local_name).write_bytes(data)
|
|
294
|
+
manifest.append({"remote_path": remote_path, "local_name": local_name})
|
|
295
|
+
logger.info("exported file %s → %s (%dB)", local_name, remote_path, len(data))
|
|
296
|
+
if manifest:
|
|
297
|
+
(files_dir / "manifest.json").write_text(
|
|
298
|
+
json.dumps(manifest, indent=2) + "\n",
|
|
299
|
+
encoding="utf-8",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# tests/test.sh
|
|
303
|
+
test_path = tests_dir / "test.sh"
|
|
304
|
+
test_path.write_text(_build_test_sh(task), encoding="utf-8")
|
|
305
|
+
test_path.chmod(0o755)
|
|
306
|
+
|
|
307
|
+
return task_dir
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
author_name = "mmini-collect"
|
|
3
|
+
difficulty = "unknown"
|
|
4
|
+
category = "desktop-automation"
|
|
5
|
+
tags = {{TAGS}}
|
|
6
|
+
platform = "{{PLATFORM}}"
|
|
7
|
+
runnable = {{RUNNABLE}}
|
|
8
|
+
|
|
9
|
+
[verifier]
|
|
10
|
+
timeout_sec = 1800
|
|
11
|
+
|
|
12
|
+
[agent]
|
|
13
|
+
timeout_sec = 1800
|
|
14
|
+
|
|
15
|
+
[environment]
|
|
16
|
+
cpus = 4
|
|
17
|
+
memory_mb = 8192
|
|
18
|
+
allow_internet = true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# iOS test.sh — runs on the harbor host, evaluator runs server-side at /grade.
|
|
3
|
+
REWARD="${HARBOR_REWARD_FILE:-./reward.txt}"
|
|
4
|
+
GRADER_LOG="${HARBOR_GRADER_LOG:-./grader.log}"
|
|
5
|
+
: "${GATEWAY_URL:?GATEWAY_URL is required}"
|
|
6
|
+
: "${SANDBOX_ID:?SANDBOX_ID is required}"
|
|
7
|
+
: "${MMINI_API_KEY:?MMINI_API_KEY is required}"
|
|
8
|
+
|
|
9
|
+
SPECS='{{SPECS}}'
|
|
10
|
+
PAYLOAD=$(printf '{"specs": %s}' "$SPECS")
|
|
11
|
+
RESP=$(curl -sS -H "Authorization: Bearer $MMINI_API_KEY" -H "Content-Type: application/json" -X POST "$GATEWAY_URL/v1/sandboxes/$SANDBOX_ID/grade" --data-binary "$PAYLOAD")
|
|
12
|
+
echo "$RESP" >> "$GRADER_LOG"
|
|
13
|
+
if echo "$RESP" | python3 -c "import sys,json; sys.exit(0 if json.load(sys.stdin).get(\"passed\") else 1)"; then
|
|
14
|
+
echo "1" > "$REWARD"
|
|
15
|
+
echo "Score: 1"
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo "0" > "$REWARD"
|
|
20
|
+
echo "Score: 0 — grader response: $(echo "$RESP" | head -c 500)"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# macOS test.sh — runs inside the VM via Harbor.
|
|
3
|
+
PREFIX=""
|
|
4
|
+
[ -d "/tmp/harbor/logs" ] && PREFIX="/tmp/harbor"
|
|
5
|
+
REWARD="${PREFIX}/logs/verifier/reward.txt"
|
|
6
|
+
GRADER_LOG="${PREFIX}/logs/verifier/grader.log"
|
|
7
|
+
|
|
8
|
+
{{CHECKS}}
|
|
9
|
+
echo "0" > "$REWARD"
|
|
10
|
+
echo "Score: 0"
|
use_computer/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import mmini as _mmini
|
|
4
|
+
from mmini import * # noqa: F403
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Computer(_mmini.Mmini):
|
|
8
|
+
"""use.computer client."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncComputer(_mmini.AsyncMmini):
|
|
12
|
+
"""Async use.computer client."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [*_mmini.__all__, "AsyncComputer", "Computer"]
|
use_computer/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: use-computer
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python SDK for the use.computer macOS and iOS Computer Use API
|
|
5
|
+
Project-URL: Homepage, https://use.computer
|
|
6
|
+
Project-URL: Documentation, https://github.com/josancamon19/mmini-sdk#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/josancamon19/mmini-sdk
|
|
8
|
+
Author: use.computer
|
|
9
|
+
Keywords: automation,computer-use,ios,macos,sandbox
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# use-computer Python SDK
|
|
24
|
+
|
|
25
|
+
Python SDK for the use.computer macOS and iOS Computer Use API.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install use-computer
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from use_computer import Computer
|
|
37
|
+
|
|
38
|
+
client = Computer(api_key="mk_live_...")
|
|
39
|
+
sandbox = client.create()
|
|
40
|
+
|
|
41
|
+
# Screenshot
|
|
42
|
+
img = sandbox.screenshot.take_full_screen()
|
|
43
|
+
|
|
44
|
+
# Mouse
|
|
45
|
+
sandbox.mouse.click(500, 400)
|
|
46
|
+
sandbox.mouse.move(100, 200)
|
|
47
|
+
sandbox.mouse.scroll(500, 400, "down", 3)
|
|
48
|
+
sandbox.mouse.drag(100, 100, 300, 300)
|
|
49
|
+
|
|
50
|
+
# Keyboard
|
|
51
|
+
sandbox.keyboard.type("Hello")
|
|
52
|
+
sandbox.keyboard.press("enter")
|
|
53
|
+
sandbox.keyboard.hotkey("command+space")
|
|
54
|
+
|
|
55
|
+
# Display
|
|
56
|
+
info = sandbox.display.get_info()
|
|
57
|
+
windows = sandbox.display.get_windows()
|
|
58
|
+
|
|
59
|
+
# Recording
|
|
60
|
+
recording = sandbox.recording.start(name="my-recording")
|
|
61
|
+
# ... do stuff ...
|
|
62
|
+
sandbox.recording.stop(recording.id)
|
|
63
|
+
sandbox.recording.download(recording.id, "output.mp4")
|
|
64
|
+
|
|
65
|
+
# Cleanup
|
|
66
|
+
sandbox.close()
|
|
67
|
+
client.close()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Environment
|
|
71
|
+
|
|
72
|
+
`Computer()` reads these environment variables when explicit values are not passed:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
USE_COMPUTER_API_KEY=mk_live_...
|
|
76
|
+
USE_COMPUTER_BASE_URL=https://api.use.computer
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`USE_COMPUTER_BASE_URL` is optional. The default is `https://api.use.computer`.
|
|
80
|
+
|
|
81
|
+
## Async
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from use_computer import AsyncComputer
|
|
85
|
+
|
|
86
|
+
client = AsyncComputer(api_key="mk_live_...")
|
|
87
|
+
sandbox = await client.create()
|
|
88
|
+
img = await sandbox.screenshot.take_full_screen()
|
|
89
|
+
await sandbox.close()
|
|
90
|
+
await client.close()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Local Gateway
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from use_computer import Computer
|
|
97
|
+
|
|
98
|
+
client = Computer(
|
|
99
|
+
api_key="sk-local",
|
|
100
|
+
base_url="http://localhost:8080",
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Backward Compatibility
|
|
105
|
+
|
|
106
|
+
The original import path still works:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from mmini import Mmini
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
New code should prefer:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from use_computer import Computer
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Publishing
|
|
119
|
+
|
|
120
|
+
CI publishes `use-computer` from the `main` branch. The first release is
|
|
121
|
+
`0.0.1`; after that the workflow reads the latest `vX.Y.Z` tag and bumps the
|
|
122
|
+
patch version.
|
|
123
|
+
|
|
124
|
+
For CI, prefer PyPI Trusted Publishing. If using an API token instead, set a
|
|
125
|
+
repo secret named `PYPI_API_TOKEN`; do not commit tokens to `.env`.
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
uv run --group dev pre-commit install
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Pre-commit runs `ruff format`, `ruff check --fix`, and `ty check`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
mmini/__init__.py,sha256=6Z7ivHUDYwCMxJXk_C7txP7qYBh4Me6EsIcn-DjCtQ8,1006
|
|
2
|
+
mmini/ax_transpile.py,sha256=pidZiyM3cykYNL9xSeKlxW5qte4KQGiMokFlg5dvSlY,18600
|
|
3
|
+
mmini/client.py,sha256=6Zvn4AVReqiZG7G_HrxKpUtnOPZg-eoRXNpiHPG3CdA,11971
|
|
4
|
+
mmini/display.py,sha256=nWJOr91nmz5xx6ZTh11HAXgVeDhgC3CvdZWG3ABwQCY,1125
|
|
5
|
+
mmini/errors.py,sha256=PpVtvTJsUQuyTwfq0qKn_s4HrKKAKrb4hp70D4Ov6l0,2173
|
|
6
|
+
mmini/models.py,sha256=yShbRgdngN2zyoUrvXfCifoPrSV2FHr3dtaLkZXRNf0,1649
|
|
7
|
+
mmini/parsers.py,sha256=bjdUBoT3uaUY73eDqm4uzFlqFotNZ9PLvYS8SO18Ifc,9368
|
|
8
|
+
mmini/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
9
|
+
mmini/recording.py,sha256=if3L9rbt3onmg5YM3F6wioLiRzvPgS8QylZAKEmcpcI,3522
|
|
10
|
+
mmini/retry.py,sha256=6owrGNsAUfmoj-4SZlgG_scRDe4kw3flcd3IwXqdCG4,4058
|
|
11
|
+
mmini/sandbox.py,sha256=2dHtIz91oePxdoDfDqlDbmGXRVfesLQ5kGzth6Z-12Q,15168
|
|
12
|
+
mmini/screenshot.py,sha256=8AhbSOA647KqPNB6YhOjLunOiM9c9aYL14hwxnPPin0,2229
|
|
13
|
+
mmini/ios/__init__.py,sha256=Rn60cHhHnpjGBIGkojSgkZbkl8lFJKmMm7lcDnDoX9s,309
|
|
14
|
+
mmini/ios/apps.py,sha256=R6nI7mkv6TY_tSohT642j3Pc8kPnAP4aIoqaFI462hM,2182
|
|
15
|
+
mmini/ios/environment.py,sha256=UCfr4B5A8NRl5gFT4zh75P5rl2Jr0UllT9jp4KtGbR8,1838
|
|
16
|
+
mmini/ios/input.py,sha256=TRYC7TMRO-xenl_k4IYZHyu5jc-Mo9Dzs6AWP-H2qMg,3984
|
|
17
|
+
mmini/macos/__init__.py,sha256=rYsFsjsRTy5Ios8Cf3suBq4sXgu8bAM4LcrIi0YZQxQ,169
|
|
18
|
+
mmini/macos/keyboard.py,sha256=eLOqngkHmoOg8oOskVmkO4d1yPi04Kv4sGXQfrEPvqI,2218
|
|
19
|
+
mmini/macos/mouse.py,sha256=QGUJdOvqysZREYLy__N0DQnJchZW2xRZDs_1g-eHi58,3634
|
|
20
|
+
mmini/tasks/__init__.py,sha256=fLzrLX2JAbqExDUAKNgR6s370maLWxNAWBskajD9RFY,10062
|
|
21
|
+
mmini/tasks/templates/pre_command.sh,sha256=bwNnVohjDOfWoOnPKgNgOaH1RrZdUA-RpBcrRZeytmQ,25
|
|
22
|
+
mmini/tasks/templates/task.toml,sha256=JiWb7gnvKfLpZfisySlH-WsXIX5c7jOuYS8nTHvdbfA,284
|
|
23
|
+
mmini/tasks/templates/test_ios.sh,sha256=n8xVC9nOEztT5bCo0BS3ZDfH_oI5JVSzly7sCQYNr2o,841
|
|
24
|
+
mmini/tasks/templates/test_ios_nograder.sh,sha256=5WwKY2X4SasDDzn9eMR3f1_01d08fUm9nMAot84wq8c,110
|
|
25
|
+
mmini/tasks/templates/test_macos.sh,sha256=WXriAun7h5Udo-8wOx4NUcV7MwVistd8iFul9js9Qmc,264
|
|
26
|
+
mmini/tasks/templates/test_macos_check.sh,sha256=dJEILS8jW4lLVkhH2oBUJZOOaY0MR5M9mo7qb7_mHVw,226
|
|
27
|
+
mmini/tasks/templates/test_macos_nograder.sh,sha256=kZ7LDD6tb9SJmqgVWLZxX2RudRl6xyeWmQvGXWala-0,226
|
|
28
|
+
use_computer/__init__.py,sha256=e8ynfrOySnQBJGn5riiaG06tvqPAxHfkOhYmxwlcAZc,294
|
|
29
|
+
use_computer/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
30
|
+
use_computer-0.0.1.dist-info/METADATA,sha256=OXfyVvn31rE90V_Ts9PAnRg4MZMZtAubr3KycZlQNTI,3098
|
|
31
|
+
use_computer-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
32
|
+
use_computer-0.0.1.dist-info/RECORD,,
|