macos-computer-use-kit 0.2.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.
- macos_computer_use/__init__.py +9 -0
- macos_computer_use/__main__.py +4 -0
- macos_computer_use/ax.py +363 -0
- macos_computer_use/cli.py +204 -0
- macos_computer_use/darwin.py +261 -0
- macos_computer_use/input_events.py +172 -0
- macos_computer_use/jev.py +223 -0
- macos_computer_use/overlay.py +148 -0
- macos_computer_use/paste.py +111 -0
- macos_computer_use/shot.py +87 -0
- macos_computer_use_kit-0.2.0.dist-info/METADATA +308 -0
- macos_computer_use_kit-0.2.0.dist-info/RECORD +15 -0
- macos_computer_use_kit-0.2.0.dist-info/WHEEL +4 -0
- macos_computer_use_kit-0.2.0.dist-info/entry_points.txt +2 -0
- macos_computer_use_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Transient visual overlay: a ring (+ optional label) at a screen point.
|
|
2
|
+
|
|
3
|
+
Absorbed from Grok Bot's cursor/drag overlays: the user should be able to see
|
|
4
|
+
where the agent is acting. The overlay is click-through, floats above normal
|
|
5
|
+
windows, and fades out after ``--duration`` seconds.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from . import darwin
|
|
15
|
+
|
|
16
|
+
COLORS = {
|
|
17
|
+
"cyan": (0.20, 0.80, 1.00),
|
|
18
|
+
"green": (0.20, 0.85, 0.40),
|
|
19
|
+
"orange": (1.00, 0.65, 0.10),
|
|
20
|
+
"red": (1.00, 0.30, 0.30),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _appkit():
|
|
25
|
+
darwin.require_macos()
|
|
26
|
+
import objc
|
|
27
|
+
from AppKit import (
|
|
28
|
+
NSApplication,
|
|
29
|
+
NSBackingStoreBuffered,
|
|
30
|
+
NSBezierPath,
|
|
31
|
+
NSColor,
|
|
32
|
+
NSFont,
|
|
33
|
+
NSInsetRect,
|
|
34
|
+
NSMakeRect,
|
|
35
|
+
NSScreen,
|
|
36
|
+
NSScreenSaverWindowLevel,
|
|
37
|
+
NSString,
|
|
38
|
+
NSView,
|
|
39
|
+
NSWindow,
|
|
40
|
+
NSWindowStyleMaskBorderless,
|
|
41
|
+
)
|
|
42
|
+
from Foundation import NSDate, NSRunLoop
|
|
43
|
+
|
|
44
|
+
return objc, dict(
|
|
45
|
+
NSApplication=NSApplication,
|
|
46
|
+
NSBackingStoreBuffered=NSBackingStoreBuffered,
|
|
47
|
+
NSBezierPath=NSBezierPath,
|
|
48
|
+
NSColor=NSColor,
|
|
49
|
+
NSFont=NSFont,
|
|
50
|
+
NSInsetRect=NSInsetRect,
|
|
51
|
+
NSMakeRect=NSMakeRect,
|
|
52
|
+
NSScreen=NSScreen,
|
|
53
|
+
NSScreenSaverWindowLevel=NSScreenSaverWindowLevel,
|
|
54
|
+
NSString=NSString,
|
|
55
|
+
NSView=NSView,
|
|
56
|
+
NSWindow=NSWindow,
|
|
57
|
+
NSWindowStyleMaskBorderless=NSWindowStyleMaskBorderless,
|
|
58
|
+
NSDate=NSDate,
|
|
59
|
+
NSRunLoop=NSRunLoop,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _ring_view_class():
|
|
64
|
+
objc, K = _appkit()
|
|
65
|
+
|
|
66
|
+
class RingView(K["NSView"]):
|
|
67
|
+
def initWithFrame_(self, frame):
|
|
68
|
+
self = objc.super(RingView, self).initWithFrame_(frame)
|
|
69
|
+
if self is None:
|
|
70
|
+
return None
|
|
71
|
+
self._label = ""
|
|
72
|
+
self._rgb = COLORS["cyan"]
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def setLabel_(self, label):
|
|
76
|
+
self._label = label
|
|
77
|
+
|
|
78
|
+
def setRGB_(self, rgb):
|
|
79
|
+
self._rgb = rgb
|
|
80
|
+
|
|
81
|
+
def drawRect_(self, rect):
|
|
82
|
+
r, g, b = self._rgb
|
|
83
|
+
K["NSColor"].colorWithCalibratedRed_green_blue_alpha_(r, g, b, 0.95).set()
|
|
84
|
+
ring = K["NSBezierPath"].bezierPathWithOvalInRect_(K["NSInsetRect"](self.bounds(), 8, 8))
|
|
85
|
+
ring.setLineWidth_(5.0)
|
|
86
|
+
ring.stroke()
|
|
87
|
+
K["NSColor"].colorWithCalibratedRed_green_blue_alpha_(r, g, b, 0.9).set()
|
|
88
|
+
dot = K["NSBezierPath"].bezierPathWithOvalInRect_(K["NSInsetRect"](self.bounds(), 30, 30))
|
|
89
|
+
dot.fill()
|
|
90
|
+
if self._label:
|
|
91
|
+
K["NSColor"].whiteColor().set()
|
|
92
|
+
attrs = {
|
|
93
|
+
"NSFont": K["NSFont"].boldSystemFontOfSize_(13),
|
|
94
|
+
"NSForegroundColor": K["NSColor"].whiteColor(),
|
|
95
|
+
"NSBackgroundColor": K["NSColor"].colorWithCalibratedWhite_alpha_(0.0, 0.65),
|
|
96
|
+
}
|
|
97
|
+
K["NSString"].stringWithString_(self._label).drawAtPoint_withAttributes_((4, -6), attrs)
|
|
98
|
+
|
|
99
|
+
return RingView
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cocoa_point(ax_x: float, ax_y: float) -> tuple[float, float]:
|
|
103
|
+
"""Convert an AX top-left screen point to a Cocoa bottom-left window origin.
|
|
104
|
+
|
|
105
|
+
The AX space is anchored at the primary display's top-left, so the primary
|
|
106
|
+
display's height is the correct mirror axis even on multi-display setups.
|
|
107
|
+
"""
|
|
108
|
+
_w, height = darwin.primary_screen_point_size()
|
|
109
|
+
return float(ax_x), float(height - ax_y)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def show(x: float, y: float, label: str = "", duration: float = 1.5, color: str = "cyan") -> int:
|
|
113
|
+
RingView = _ring_view_class() # noqa: N806
|
|
114
|
+
_objc, K = _appkit()
|
|
115
|
+
|
|
116
|
+
app = K["NSApplication"].sharedApplication()
|
|
117
|
+
app.setActivationPolicy_(1) # accessory: no dock icon
|
|
118
|
+
size = 96
|
|
119
|
+
cx, cy = cocoa_point(x, y)
|
|
120
|
+
frame = K["NSMakeRect"](cx - size / 2, cy - size / 2, size, size)
|
|
121
|
+
win = K["NSWindow"].alloc().initWithContentRect_styleMask_backing_defer_(
|
|
122
|
+
frame, K["NSWindowStyleMaskBorderless"], K["NSBackingStoreBuffered"], False
|
|
123
|
+
)
|
|
124
|
+
win.setOpaque_(False)
|
|
125
|
+
win.setBackgroundColor_(K["NSColor"].clearColor())
|
|
126
|
+
win.setLevel_(K["NSScreenSaverWindowLevel"])
|
|
127
|
+
win.setIgnoresMouseEvents_(True)
|
|
128
|
+
win.setHasShadow_(False)
|
|
129
|
+
view = RingView.alloc().initWithFrame_(K["NSMakeRect"](0, 0, size, size))
|
|
130
|
+
view.setLabel_(label or "")
|
|
131
|
+
view.setRGB_(COLORS.get(color, COLORS["cyan"]))
|
|
132
|
+
win.setContentView_(view)
|
|
133
|
+
win.orderFrontRegardless()
|
|
134
|
+
|
|
135
|
+
deadline = time.time() + duration
|
|
136
|
+
while time.time() < deadline:
|
|
137
|
+
K["NSRunLoop"].currentRunLoop().runUntilDate_(K["NSDate"].dateWithTimeIntervalSinceNow_(0.05))
|
|
138
|
+
win.orderOut_(None)
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run(args: argparse.Namespace) -> int:
|
|
143
|
+
if args.cmd == "clear":
|
|
144
|
+
return 0
|
|
145
|
+
if args.x is None or args.y is None:
|
|
146
|
+
print("--x/--y required", file=sys.stderr)
|
|
147
|
+
return 2
|
|
148
|
+
return show(args.x, args.y, args.label, args.duration, args.color)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Clipboard-safe paste with takeover detection and clipboard restore.
|
|
2
|
+
|
|
3
|
+
Absorbed from ZCode's ``providedPaste`` pipeline (begin -> markDispatched ->
|
|
4
|
+
awaitRead -> finish) and its error taxonomy:
|
|
5
|
+
|
|
6
|
+
- ``pasteboard_write_failed`` nothing was sent
|
|
7
|
+
- ``pasteboard_changed_during_paste`` someone else took over the clipboard;
|
|
8
|
+
verify the app did not paste the user's content
|
|
9
|
+
- ``pasteboard_read_timed_out`` the app never consumed the paste
|
|
10
|
+
|
|
11
|
+
The user's clipboard is saved before writing and restored afterwards, so an
|
|
12
|
+
agent pasting Chinese/CJK text never destroys what the user had copied.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from . import darwin
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _pb():
|
|
26
|
+
_AS, _NSWorkspace, Quartz = darwin._pyobjc() # noqa: N806
|
|
27
|
+
from AppKit import NSPasteboard
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from AppKit import NSPasteboardTypeString as string_type
|
|
31
|
+
except ImportError: # pragma: no cover - legacy pyobjc
|
|
32
|
+
from AppKit import NSStringPboardType as string_type
|
|
33
|
+
return Quartz, NSPasteboard.generalPasteboard(), string_type
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_text() -> str:
|
|
37
|
+
_Quartz, board, string_type = _pb() # noqa: N806
|
|
38
|
+
return board.stringForType_(string_type) or ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def write_text(text: str) -> bool:
|
|
42
|
+
_Quartz, board, string_type = _pb() # noqa: N806
|
|
43
|
+
board.clearContents()
|
|
44
|
+
return bool(board.setString_forType_(text, string_type))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def post_paste(pid: int | None, mode: str) -> None:
|
|
48
|
+
Quartz, _board, _string_type = _pb() # noqa: N806
|
|
49
|
+
for is_down in (True, False):
|
|
50
|
+
ev = Quartz.CGEventCreateKeyboardEvent(None, 9, is_down) # 9 = V
|
|
51
|
+
Quartz.CGEventSetFlags(ev, Quartz.kCGEventFlagMaskCommand)
|
|
52
|
+
if mode == "pid" and pid is not None:
|
|
53
|
+
Quartz.CGEventPostToPid(pid, ev)
|
|
54
|
+
else:
|
|
55
|
+
Quartz.CGEventPost(Quartz.kCGHIDEventTap, ev)
|
|
56
|
+
time.sleep(0.02)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run(args: argparse.Namespace) -> int:
|
|
60
|
+
Quartz, board, _string_type = _pb() # noqa: N806
|
|
61
|
+
result = {"steps": []}
|
|
62
|
+
|
|
63
|
+
previous = read_text()
|
|
64
|
+
before_count = board.changeCount()
|
|
65
|
+
result["previous_len"] = len(previous)
|
|
66
|
+
result["steps"].append("begin")
|
|
67
|
+
|
|
68
|
+
if not write_text(args.text):
|
|
69
|
+
result.update(ok=False, reason="pasteboard_write_failed", action_sent=False)
|
|
70
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
71
|
+
return 3
|
|
72
|
+
after_count = board.changeCount()
|
|
73
|
+
result["steps"].append("written")
|
|
74
|
+
|
|
75
|
+
pid = args.pid
|
|
76
|
+
if pid is None and args.app:
|
|
77
|
+
pid = darwin.resolve_pid(args.app)
|
|
78
|
+
if args.mode == "pid" and pid is None:
|
|
79
|
+
result.update(ok=False, reason="target_app_not_found", action_sent=False)
|
|
80
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
81
|
+
return 4
|
|
82
|
+
post_paste(pid, args.mode)
|
|
83
|
+
result["steps"].append(f"dispatched({args.mode})")
|
|
84
|
+
|
|
85
|
+
time.sleep(args.wait)
|
|
86
|
+
taken_over = board.changeCount() != after_count
|
|
87
|
+
result["clipboard_taken_over"] = taken_over
|
|
88
|
+
result["action_sent"] = True
|
|
89
|
+
|
|
90
|
+
if taken_over:
|
|
91
|
+
result.update(
|
|
92
|
+
ok=False,
|
|
93
|
+
reason="pasteboard_changed_during_paste",
|
|
94
|
+
hint="someone else wrote the clipboard during the paste; check the app to make sure "
|
|
95
|
+
"the user's content was not pasted instead",
|
|
96
|
+
restored=False,
|
|
97
|
+
)
|
|
98
|
+
elif args.keep:
|
|
99
|
+
result.update(ok=True, restored=False)
|
|
100
|
+
else:
|
|
101
|
+
if previous:
|
|
102
|
+
write_text(previous)
|
|
103
|
+
result["restored"] = True
|
|
104
|
+
else:
|
|
105
|
+
board.clearContents()
|
|
106
|
+
result["restored"] = False
|
|
107
|
+
result.update(ok=True)
|
|
108
|
+
|
|
109
|
+
result["steps"].append("finished")
|
|
110
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
111
|
+
return 0 if result.get("ok") else 2
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Screenshot capture with blank-frame detection.
|
|
2
|
+
|
|
3
|
+
Absorbed from ZCode's ``screenshot_blank`` / ``screenshot_bounds``: never reason
|
|
4
|
+
on a blank or permission-starved frame; detect it and say so. Agents that skip
|
|
5
|
+
this step hallucinate UI state from black pixels.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
|
|
17
|
+
from . import darwin
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def analyze(path: str) -> dict:
|
|
21
|
+
from PIL import Image, ImageStat
|
|
22
|
+
|
|
23
|
+
img = Image.open(path).convert("L")
|
|
24
|
+
stat = ImageStat.Stat(img)
|
|
25
|
+
mean = float(stat.mean[0])
|
|
26
|
+
std = float(stat.stddev[0])
|
|
27
|
+
verdict = "ok"
|
|
28
|
+
hint = ""
|
|
29
|
+
blank = std < 2.0
|
|
30
|
+
if blank and mean < 5:
|
|
31
|
+
verdict = "all_black"
|
|
32
|
+
hint = "screen recording permission may be missing, or the window is fully occluded"
|
|
33
|
+
elif blank and mean > 250:
|
|
34
|
+
verdict = "all_white"
|
|
35
|
+
hint = "the window may be blank or still loading"
|
|
36
|
+
elif blank:
|
|
37
|
+
verdict = "uniform"
|
|
38
|
+
hint = "no visual detail; check the target window state"
|
|
39
|
+
return {
|
|
40
|
+
"path": path,
|
|
41
|
+
"width": img.width,
|
|
42
|
+
"height": img.height,
|
|
43
|
+
"blank": blank,
|
|
44
|
+
"mean": round(mean, 2),
|
|
45
|
+
"std": round(std, 2),
|
|
46
|
+
"verdict": verdict,
|
|
47
|
+
"hint": hint,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run(args: argparse.Namespace) -> int:
|
|
52
|
+
if args.cmd == "check":
|
|
53
|
+
if not args.file:
|
|
54
|
+
print(json.dumps({"error": "--file required"}), file=sys.stderr)
|
|
55
|
+
return 2
|
|
56
|
+
print(json.dumps(analyze(args.file), ensure_ascii=False))
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
if args.cmd == "windows":
|
|
60
|
+
print(json.dumps({"windows": darwin.all_windows(args.app)}, ensure_ascii=False))
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
if not darwin.permissions()["screen_recording"]:
|
|
64
|
+
print(json.dumps({"error": "screen_recording_not_granted", "hint": darwin.permission_hint("screen_recording")}), file=sys.stderr)
|
|
65
|
+
return 2
|
|
66
|
+
|
|
67
|
+
out = args.out or os.path.join(tempfile.gettempdir(), "macos-cu-shot.png")
|
|
68
|
+
cmd = ["screencapture", "-x"]
|
|
69
|
+
if args.window_id:
|
|
70
|
+
cmd += ["-l", str(args.window_id), "-o"]
|
|
71
|
+
elif args.region:
|
|
72
|
+
cmd += ["-R", args.region]
|
|
73
|
+
elif args.app:
|
|
74
|
+
wins = [w for w in darwin.all_windows(args.app) if w["layer"] == 0 and w["bounds"][2] > 100]
|
|
75
|
+
if not wins:
|
|
76
|
+
print(json.dumps({"error": "no_window_for_app", "app": args.app}, ensure_ascii=False))
|
|
77
|
+
return 3
|
|
78
|
+
wins.sort(key=lambda w: w["bounds"][2] * w["bounds"][3], reverse=True)
|
|
79
|
+
cmd += ["-l", str(wins[0]["id"]), "-o"]
|
|
80
|
+
cmd.append(out)
|
|
81
|
+
|
|
82
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
83
|
+
if proc.returncode != 0 or not os.path.exists(out):
|
|
84
|
+
print(json.dumps({"error": "capture_failed", "stderr": proc.stderr[-200:]}, ensure_ascii=False))
|
|
85
|
+
return 4
|
|
86
|
+
print(json.dumps(analyze(out), ensure_ascii=False))
|
|
87
|
+
return 0
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: macos-computer-use-kit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AX-first computer-use toolkit for AI agents on macOS: accessibility-tree targeting, process-scoped input, clipboard-safe paste, action read-back verification, visual feedback, and optional Jev semantic guards.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sur-Cai/macos-computer-use-kit
|
|
6
|
+
Project-URL: Repository, https://github.com/Sur-Cai/macos-computer-use-kit
|
|
7
|
+
Project-URL: Issues, https://github.com/Sur-Cai/macos-computer-use-kit/issues
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Sur-Cai
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: accessibility,agent-tools,ai-agent,automation,computer-use,dsh-plugin,macos,opencode,pi-package
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Environment :: MacOS X :: Cocoa
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
42
|
+
Requires-Python: >=3.10
|
|
43
|
+
Requires-Dist: pillow>=10.0
|
|
44
|
+
Requires-Dist: pyobjc-framework-applicationservices>=10.0; sys_platform == 'darwin'
|
|
45
|
+
Requires-Dist: pyobjc-framework-cocoa>=10.0; sys_platform == 'darwin'
|
|
46
|
+
Requires-Dist: pyobjc-framework-quartz>=10.0; sys_platform == 'darwin'
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# macos-computer-use-kit
|
|
52
|
+
|
|
53
|
+
**AX-first computer use for AI agents on macOS.** Instead of screenshot → eyeball
|
|
54
|
+
coordinates → click and hope, read the accessibility tree, get each element's
|
|
55
|
+
semantics and exact geometry, act on it, then verify the action actually changed
|
|
56
|
+
the UI.
|
|
57
|
+
|
|
58
|
+
A small, composable toolkit: accessibility-tree targeting, process- and
|
|
59
|
+
window-scoped input, clipboard-safe pasting, action read-back verification,
|
|
60
|
+
blank-frame detection, visual feedback, and optional Jev (TypeSafe System One)
|
|
61
|
+
semantic guards.
|
|
62
|
+
|
|
63
|
+
Works with any agent that can run a shell command, and ships first-class
|
|
64
|
+
packages for [pi](#pi) and [DeepSeek Harness](#deepseek-harness-dsh).
|
|
65
|
+
|
|
66
|
+
## Why
|
|
67
|
+
|
|
68
|
+
These mechanisms were distilled from three mature implementations rather than
|
|
69
|
+
invented from scratch:
|
|
70
|
+
|
|
71
|
+
| Source | Mechanism absorbed |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| Codex CUA (`@oai/cua` / Sky service) | AX state + element index, `setValue`, batch actions, event delivery via `CGEventPostToPid` |
|
|
74
|
+
| ZCode Computer Use | `*_to_window` window-scoped input, `target_changed` validation, clipboard-safe paste pipeline, `screenshot_blank` |
|
|
75
|
+
| Grok Bot (`CUGrokBotService`) | snapshots with stable element ids + text budget + drill-down, action read-back, coordinate fallback on failure |
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
macOS 12+, Python 3.10+.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install macos-computer-use-kit # or: pipx install macos-computer-use-kit
|
|
83
|
+
macos-cu doctor # check permissions, displays, dependencies
|
|
84
|
+
|
|
85
|
+
# from a checkout (editable install + agent skill)
|
|
86
|
+
git clone https://github.com/Sur-Cai/macos-computer-use-kit && cd macos-computer-use-kit
|
|
87
|
+
./install.sh
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Grant both permissions to the process that runs the agent (your terminal, or the
|
|
91
|
+
agent app). `macos-cu doctor` reports what is missing and where to enable it:
|
|
92
|
+
|
|
93
|
+
- **Accessibility** — AX reads, `AXPress`, `setValue`, posted events
|
|
94
|
+
- **Screen Recording** — `shot` (without it every capture is black)
|
|
95
|
+
|
|
96
|
+
## Quickstart
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# semantic targeting: exact geometry, zero visual reasoning
|
|
100
|
+
macos-cu ax find --app com.apple.finder --role AXButton --title Size
|
|
101
|
+
macos-cu ax tree --app com.apple.finder --depth 16 --max 200
|
|
102
|
+
|
|
103
|
+
# token-efficient snapshot with stable ids, trimmed to a budget
|
|
104
|
+
macos-cu ax snapshot --app com.apple.finder --budget 1200 --file /tmp/ax.json
|
|
105
|
+
macos-cu ax resolve --file /tmp/ax.json --id 0.1.0.6.0.0.0.0.5.8
|
|
106
|
+
|
|
107
|
+
# native AX action + read-back verification
|
|
108
|
+
macos-cu ax press --app com.apple.finder --role AXButton --title Size
|
|
109
|
+
# {"verified":true,"state_changed":true,...}
|
|
110
|
+
|
|
111
|
+
# window-scoped input: the user's cursor never moves, target is validated
|
|
112
|
+
macos-cu input windows --app "Google Chrome"
|
|
113
|
+
macos-cu input click --window-id 12345 --x 171 --y 28 --show
|
|
114
|
+
macos-cu input click --window-id 12345 --x 171 --y 28 --expect "37040:12345:642:244:824:640"
|
|
115
|
+
# mismatch -> {"ok":false,"reason":"target_changed"} and exit code 5
|
|
116
|
+
|
|
117
|
+
# clipboard-safe paste (saves and restores the user's clipboard)
|
|
118
|
+
macos-cu paste --app com.google.Chrome --text "你好" --mode pid
|
|
119
|
+
|
|
120
|
+
# screenshot with blank-frame detection
|
|
121
|
+
macos-cu shot capture --app "Google Chrome" --out /tmp/shot.png
|
|
122
|
+
macos-cu shot check --file /tmp/shot.png
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## CLI
|
|
126
|
+
|
|
127
|
+
One binary, JSON output, stable exit codes (`0` ok, `2` usage/permission,
|
|
128
|
+
`3` not found, `4` capture failed, `5` target changed).
|
|
129
|
+
|
|
130
|
+
| Group | Commands |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `macos-cu ax` | `tree`, `find`, `click-info`, `snapshot`, `resolve`, `press`, `setvalue` |
|
|
133
|
+
| `macos-cu input` | `windows`, `cursor`, `pid`, `click`, `key`, `scroll`, `move` |
|
|
134
|
+
| `macos-cu paste` | clipboard-safe paste (`--mode pid\|hid`, `--keep`) |
|
|
135
|
+
| `macos-cu shot` | `capture`, `check`, `windows` |
|
|
136
|
+
| `macos-cu overlay` | `show`, `clear` |
|
|
137
|
+
| `macos-cu jev` | `guard`, `select` (optional, JSON on stdin) |
|
|
138
|
+
| `macos-cu doctor` | permissions, displays, dependencies, Jev setup |
|
|
139
|
+
|
|
140
|
+
### Coordinate spaces
|
|
141
|
+
|
|
142
|
+
| Space | Source | Used by |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `screen[x, y]` | AX/CoreGraphics points, origin at the primary display's top-left | `input --x --y`, `overlay` |
|
|
145
|
+
| window-relative | element point − window origin | `input click --window-id N --x --y` |
|
|
146
|
+
| `shot[x, y]` | `center_screen × --shot-scale` | only for harnesses whose screenshots are scaled differently from screen points; there is deliberately no default |
|
|
147
|
+
|
|
148
|
+
Secondary displays placed left of or above the primary produce **negative**
|
|
149
|
+
coordinates. That is normal. `macos-cu doctor` prints the layout.
|
|
150
|
+
|
|
151
|
+
## Agent integrations
|
|
152
|
+
|
|
153
|
+
| Harness | What you get | Install |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| any agent with a shell | the full CLI | `pip install macos-computer-use-kit` |
|
|
156
|
+
| [opencode](https://opencode.ai) | skill `macos-computer-use` (auto-discovered) | `./install.sh` |
|
|
157
|
+
| [pi](https://pi.dev) | skill + 9 native tools | `pi install npm:pi-macos-computer-use` |
|
|
158
|
+
| [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | plugin bundle, 5 tools | `dsh plugin --profile <name> add dsh-macos-computer-use` |
|
|
159
|
+
|
|
160
|
+
<a name="pi"></a>
|
|
161
|
+
### pi package
|
|
162
|
+
|
|
163
|
+
`packages/pi` — `pi-macos-computer-use` (npm, `pi-package` keyword). Registers
|
|
164
|
+
`macos_cu_doctor`, `macos_ax_find`, `macos_ax_press`, `macos_input_windows`,
|
|
165
|
+
`macos_input_click`, `macos_input_key`, `macos_paste`, `macos_shot`,
|
|
166
|
+
`macos_jev_guard`. Every tool shells out with an argv array (`shell: false`), so
|
|
167
|
+
model-supplied text can never reach a shell.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
pi install npm:pi-macos-computer-use
|
|
171
|
+
pi -e ./packages/pi # try it for one run without installing
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
App launchers do not inherit your interactive shell's `PATH`. If the CLI is
|
|
175
|
+
installed but pi cannot find it, set `MACOS_CU_BIN=/abs/path/to/macos-cu` and
|
|
176
|
+
restart pi (the dsh plugin honours the same variable).
|
|
177
|
+
|
|
178
|
+
<a name="deepseek-harness-dsh"></a>
|
|
179
|
+
### DeepSeek Harness plugin
|
|
180
|
+
|
|
181
|
+
`packages/dsh` — `dsh-macos-computer-use`, a Cordis bundle
|
|
182
|
+
(`dsh.bundle.patch` → `cordis.patch.yml`). Registers `macos_cu_doctor`,
|
|
183
|
+
`macos_ax_find`, `macos_ax_press`, `macos_input_click`, `macos_shot`.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
dsh plugin --profile demo add dsh-macos-computer-use
|
|
187
|
+
dsh --profile demo --dump-config # verify the layer before booting
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
It deliberately does **not** claim the exclusive `ctx.computerUse` provider slot:
|
|
191
|
+
it adds tools rather than owning desktop operations, so it cannot block the
|
|
192
|
+
in-box Cua Driver provider. See `packages/dsh/README.md`.
|
|
193
|
+
|
|
194
|
+
### opencode skill
|
|
195
|
+
|
|
196
|
+
`./install.sh` installs `skill/SKILL.md` to
|
|
197
|
+
`~/.config/opencode/skills/macos-computer-use/`, where opencode discovers it
|
|
198
|
+
automatically.
|
|
199
|
+
|
|
200
|
+
## The four capabilities that matter
|
|
201
|
+
|
|
202
|
+
**1. Window-scoped input with target validation.** Events are posted straight to
|
|
203
|
+
the target process (`CGEventPostToPid`), so the physical cursor never moves and
|
|
204
|
+
the user can keep working. `--expect pid:wid:x:y:w:h` refuses to act when the
|
|
205
|
+
window moved or lost focus since you looked at it.
|
|
206
|
+
|
|
207
|
+
**2. Native AX actions with read-back verification.** `press`/`setvalue` compare
|
|
208
|
+
the window's visible-text fingerprint and focused element before and after, and
|
|
209
|
+
report `verified` separately from `action_sent`. When AX cannot act (custom-drawn
|
|
210
|
+
UI), the result carries a `hint` telling you to fall back to a coordinate click
|
|
211
|
+
or a clipboard paste — you find out from evidence, not from guessing.
|
|
212
|
+
|
|
213
|
+
**3. Clipboard safety.** The user's clipboard is saved before and restored after.
|
|
214
|
+
Takeover and non-consumption are reported explicitly, so pasting CJK text never
|
|
215
|
+
silently destroys what the user had copied.
|
|
216
|
+
|
|
217
|
+
**4. Never reason on a blank frame.** `shot` classifies captures as
|
|
218
|
+
`ok` / `all_black` / `all_white` / `uniform` with a hint, so a missing permission
|
|
219
|
+
or an occluded window is reported instead of hallucinated UI state.
|
|
220
|
+
|
|
221
|
+
## Design principles
|
|
222
|
+
|
|
223
|
+
1. **AX-first.** Semantic + exact geometry beats visual inference. Screenshots
|
|
224
|
+
verify; they do not target.
|
|
225
|
+
2. **`action_sent` ≠ `verified`.** Keep "we emitted the event" and "the UI
|
|
226
|
+
changed" as separate facts.
|
|
227
|
+
3. **The clipboard is a shared resource.** Save, detect interference, restore.
|
|
228
|
+
4. **Targets must be explicit and checkable.** Window input carries a signature;
|
|
229
|
+
a mismatch is `target_changed`, not a misclick.
|
|
230
|
+
5. **Small models judge, code decides.** Jev returns calibrated probabilities;
|
|
231
|
+
thresholds and side effects stay in code. Cost ladder: deterministic code
|
|
232
|
+
(µs) < Jev (~1 s) < visual reasoning (seconds to tens of seconds).
|
|
233
|
+
6. **Make it visible.** Action points draw a ring, so the user is never watching
|
|
234
|
+
a black box.
|
|
235
|
+
|
|
236
|
+
## Jev semantic guards (optional)
|
|
237
|
+
|
|
238
|
+
The only part that needs a key. Everything else works without it.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
echo '{"task":"send the report to Alice",
|
|
242
|
+
"expected":{"recipient":"Alice","message":"Q3 numbers"},
|
|
243
|
+
"observed":{"chat_title":"Bob","input_text":"Q3 numbers"}}' | macos-cu jev guard
|
|
244
|
+
# {"answers":{"right_target":0.02,"input_ok":0.98,"blocker":"wrong_target"},
|
|
245
|
+
# "decision":"switch_target"}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
One request fans out independent judgments and **code** applies the policy:
|
|
249
|
+
proceed only when `blocker=none` and both probabilities clear the threshold; the
|
|
250
|
+
model may only suggest the two safe recoveries (`switch_target`, `retype_input`);
|
|
251
|
+
anything else asks the user. `macos-cu jev select` picks one candidate element
|
|
252
|
+
with a `none` escape hatch and a confidence gate.
|
|
253
|
+
|
|
254
|
+
Key: `TYPESAFE_API_KEY` or `~/.config/typesafe/api_key`
|
|
255
|
+
(<https://console.typesafe.ai/keys>). Pin `TYPESAFE_MODEL` for automation;
|
|
256
|
+
`jev-latest` is the friendly default. Question-design guidance lives in
|
|
257
|
+
[`skill/reference/jev-best-practices.md`](skill/reference/jev-best-practices.md).
|
|
258
|
+
|
|
259
|
+
## Known limitations
|
|
260
|
+
|
|
261
|
+
- macOS only (AX, CGEvent, ScreenCaptureKit are macOS APIs).
|
|
262
|
+
- Custom-drawn UIs (some Electron apps, games, chat apps) expose shallow or
|
|
263
|
+
uncooperative AX trees. Fall back to screenshots **after** read-back fails,
|
|
264
|
+
not before.
|
|
265
|
+
- Process-targeted key events are accepted by most apps but not all: browsers
|
|
266
|
+
usually accept background keystrokes; some chat apps require the app to be
|
|
267
|
+
frontmost for typing and pasting.
|
|
268
|
+
- AX coordinate scale is display-dependent. `center_shot` is opt-in via
|
|
269
|
+
`--shot-scale` for exactly this reason.
|
|
270
|
+
- The user may be using the machine at the same time. Concurrent automation is
|
|
271
|
+
risky; one extra verification before an irreversible action is cheap.
|
|
272
|
+
|
|
273
|
+
## Repository layout
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
src/macos_computer_use/ the CLI implementation (pip-installable)
|
|
277
|
+
tools/*.py compatibility shims -> the same modules
|
|
278
|
+
skill/ agent skill (SKILL.md + Jev reference)
|
|
279
|
+
packages/pi/ pi package (skill + native tools)
|
|
280
|
+
packages/dsh/ DeepSeek Harness plugin bundle
|
|
281
|
+
install.sh local installer (venv + CLI + skill + Jev key)
|
|
282
|
+
tests/ unit tests for the safety-relevant logic
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for the
|
|
286
|
+
conventions (JSON on stdout, stable exit codes, no machine-specific defaults).
|
|
287
|
+
Release steps and catalog-listing criteria live in
|
|
288
|
+
[PUBLISHING.md](PUBLISHING.md); notable changes are in
|
|
289
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
290
|
+
|
|
291
|
+
## 中文说明
|
|
292
|
+
|
|
293
|
+
给 AI agent 用的 macOS 电脑控制工具箱:**AX 语义定位**(不靠截图目测坐标)、
|
|
294
|
+
进程/窗口级输入(物理光标不动)、剪贴板安全粘贴、动作回读校验、空白帧检测、
|
|
295
|
+
可视反馈,以及可选的 Jev 语义护栏。
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
pip install macos-computer-use-kit
|
|
299
|
+
macos-cu doctor # 检查辅助功能 / 屏幕录制权限、显示器、依赖、Jev
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Agent 集成:`./install.sh`(opencode skill)、`pi install npm:pi-macos-computer-use`(pi)、
|
|
303
|
+
`dsh plugin --profile <名> add dsh-macos-computer-use`(DeepSeek Harness)。
|
|
304
|
+
完整流程与避坑见 [`skill/SKILL.md`](skill/SKILL.md)。
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
macos_computer_use/__init__.py,sha256=pnsVo5RePGCNIf-Bmem94LbdCu8Tyck06Eej8GtBAjQ,271
|
|
2
|
+
macos_computer_use/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
|
|
3
|
+
macos_computer_use/ax.py,sha256=2VVlCP4huDEGS6N3TNS_zNwK4ihtEO24mHC0jKPgK1s,12505
|
|
4
|
+
macos_computer_use/cli.py,sha256=1j8Bx2MGb9ATlYJyMJXtZs-gFcIcTef1nelhW-NagH8,8529
|
|
5
|
+
macos_computer_use/darwin.py,sha256=wtJJ8cTocszMn8npCA-5kmx1F6VsLhAkarBuDe39Pko,8657
|
|
6
|
+
macos_computer_use/input_events.py,sha256=le2e3mQ61VKrCkZbEu58EqpS7Q9gD_IDRjczWxT7j_c,6313
|
|
7
|
+
macos_computer_use/jev.py,sha256=KN5KcHpgG0Nmy-SqwLiRZ-3i8TDICvbuqxeqDabEvO0,8477
|
|
8
|
+
macos_computer_use/overlay.py,sha256=YE80fSwHKUc0PGpzmmP9-y7MyWDPaJ5K0g_trU5yFXc,4868
|
|
9
|
+
macos_computer_use/paste.py,sha256=U-KIZ4YCWOcnFsVWXLtUec9j1zdYjww40lnqSu4zZuE,3713
|
|
10
|
+
macos_computer_use/shot.py,sha256=PNFv0ZqEOUIjXWZ7SBRbhbkPzwGV3TdVTzBHs85Og8A,2868
|
|
11
|
+
macos_computer_use_kit-0.2.0.dist-info/METADATA,sha256=MuhWftOqyW7hHHqwErPqoiZwHNf9u6h9PRLMjEuFKWo,14349
|
|
12
|
+
macos_computer_use_kit-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
13
|
+
macos_computer_use_kit-0.2.0.dist-info/entry_points.txt,sha256=t1fChWY0jYxD-O3-VeYHFfoSrliWBiBM-0iIGzdb5jY,57
|
|
14
|
+
macos_computer_use_kit-0.2.0.dist-info/licenses/LICENSE,sha256=8iFQQD9uGhSSaaKuof5c1QiBoaktQ2r8llKpesYrwag,1064
|
|
15
|
+
macos_computer_use_kit-0.2.0.dist-info/RECORD,,
|