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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ macos-cu = macos_computer_use.cli:main