noracu 0.1.0 → 0.1.1
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.
- package/CHANGELOG.md +36 -2
- package/README.md +13 -9
- package/README.zh-CN.md +13 -9
- package/docs/architecture.md +6 -5
- package/docs/tools.md +15 -15
- package/docs/usage.md +35 -5
- package/imctl/LICENSE +21 -0
- package/imctl/README.md +140 -0
- package/imctl/pyproject.toml +37 -0
- package/imctl/src/imctl/__init__.py +31 -0
- package/imctl/src/imctl/cli.py +236 -0
- package/imctl/src/imctl/gestures.py +151 -0
- package/imctl/src/imctl/input.py +246 -0
- package/imctl/src/imctl/verify.py +77 -0
- package/imctl/src/imctl/vision.py +138 -0
- package/imctl/src/imctl/window.py +248 -0
- package/package.json +9 -3
- package/src/server.mjs +54 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.1] — 2026-11-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **iPhone driver bundled.** imctl (the Python driver behind the 14 `phone_*`
|
|
7
|
+
tools) now ships inside the npm package at `imctl/` (MIT, ~1,000 lines).
|
|
8
|
+
No companion checkout is needed; `NORACU_IMCTL_DIR` still overrides the
|
|
9
|
+
location for development. The layer stays EXPERIMENTAL.
|
|
10
|
+
- `NORACU_PYTHON`: pick the python3 that runs imctl. Without it, noracu
|
|
11
|
+
probes `/opt/homebrew/bin/python3`, `/usr/local/bin/python3`, `python3`
|
|
12
|
+
and `/usr/bin/python3` for one that imports pyobjc (Quartz, Vision,
|
|
13
|
+
AppKit) — probed once, lazily, on the first `phone_*` call. When none
|
|
14
|
+
qualifies the tool returns one actionable line with the `pip install`
|
|
15
|
+
command.
|
|
16
|
+
- `phone_status` now reports `window_id`, `onscreen`, `frontmost` and
|
|
17
|
+
`paused` (true when the Mirroring window shows "Connection Paused").
|
|
18
|
+
- `phone_launch` resumes a paused session (clicks Resume) after bringing the
|
|
19
|
+
window to the front. imctl gains `screenshot [PATH]` and `resume`
|
|
20
|
+
commands.
|
|
21
|
+
- e2e: 35 checks (new CI-safe guard that the bundled imctl is present).
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `phone_screenshot` / `phone_ocr` / `phone_find` captured whatever was on
|
|
25
|
+
screen at the window's rectangle, so a covered or background Mirroring
|
|
26
|
+
window produced screenshots and OCR of other apps. Capture is now by
|
|
27
|
+
CGWindow id (`screencapture -l`), which returns the window's own pixels
|
|
28
|
+
even when it is covered; region capture remains as a fallback.
|
|
29
|
+
- imctl no longer writes `__pycache__` into the package directory
|
|
30
|
+
(`PYTHONDONTWRITEBYTECODE`).
|
|
31
|
+
|
|
32
|
+
### Notes
|
|
33
|
+
- Mac and browser layers are unchanged.
|
|
34
|
+
- The iPhone layer needs macOS 15+, iPhone Mirroring connected, and a
|
|
35
|
+
python3 with pyobjc; Apple's `/usr/bin/python3` usually lacks pyobjc.
|
|
36
|
+
|
|
3
37
|
## [0.1.0] — 2026-11-02
|
|
4
38
|
|
|
5
39
|
### Added
|
|
@@ -36,8 +70,8 @@
|
|
|
36
70
|
|
|
37
71
|
### Notes
|
|
38
72
|
- macOS 15+ required for Mac/iPhone layers; Browser layer is cross-platform.
|
|
39
|
-
- imctl
|
|
40
|
-
phone tools degrade gracefully when absent.
|
|
73
|
+
- imctl was a companion checkout in 0.1.0 (sibling directory or `NORACU_IMCTL_DIR`);
|
|
74
|
+
phone tools degrade gracefully when absent. Bundled since 0.1.1.
|
|
41
75
|
- Tool descriptions are English; every JSON-schema property is single-typed
|
|
42
76
|
(no union/`oneOf`) for reliable LLM tool-calling across clients.
|
|
43
77
|
- `mac_click` / `mac_drag` refuse an `element_index` when a different app is
|
package/README.md
CHANGED
|
@@ -23,9 +23,9 @@ One MCP server, three control surfaces:
|
|
|
23
23
|
|---|---|---|---|
|
|
24
24
|
| **Mac** | 9 (`mac_*`) | cliclick, screencapture, JXA System Events | stable |
|
|
25
25
|
| **Browser** | 12 (`browser_*`) | Playwright (persistent profile) | stable |
|
|
26
|
-
| **iPhone** | 14 (`phone_*`) | iPhone Mirroring via
|
|
26
|
+
| **iPhone** | 14 (`phone_*`) | iPhone Mirroring via the bundled imctl driver (Python + pyobjc) | experimental |
|
|
27
27
|
|
|
28
|
-
The iPhone layer is experimental and
|
|
28
|
+
The iPhone layer is experimental and ships with its driver, imctl (bundled in `imctl/`). It needs an active iPhone Mirroring session on this Mac and a python3 with pyobjc; when no such python is found, `phone_*` tools return a one-line `imctl unavailable` error with the install command, and the Mac and browser layers are unaffected (`phone_home` / `phone_app_switcher` work through the iPhone Mirroring menu even then). The observe/act vocabulary follows the action/observation split popularized by ChatGPT's computer-use, re-implemented on open local tooling.
|
|
29
29
|
|
|
30
30
|
## Why noracu
|
|
31
31
|
|
|
@@ -38,7 +38,7 @@ noracu is the local hands, not the model. Your MCP client (Claude Desktop, Claud
|
|
|
38
38
|
- **Off switches that are actually off.** `NORACU_READ_ONLY=1` (observation-only, default-deny), `NORACU_DISABLE_EVAL=1`, and `file://` blocked by default. See [Security](#security).
|
|
39
39
|
- **Verifiable.** 34 end-to-end checks (`npm test`), CI on macos-latest, MIT, and the whole server is one file: `src/server.mjs`.
|
|
40
40
|
|
|
41
|
-
**Not a good fit if** you need multiple browser tabs or sessions (single session for now), you're not on macOS (the Mac and iPhone layers are macOS-only; the browser layer is cross-platform), or you need
|
|
41
|
+
**Not a good fit if** you need multiple browser tabs or sessions (single session for now), you're not on macOS (the Mac and iPhone layers are macOS-only; the browser layer is cross-platform), or you need a finished iPhone layer today (it ships, but experimental). If you only need browser automation, playwright-mcp has tab management and selector APIs that noracu v0.1 lacks; noracu is for when one agent also has to leave the browser and drive Mac apps.
|
|
42
42
|
|
|
43
43
|
## Quickstart
|
|
44
44
|
|
|
@@ -87,7 +87,7 @@ Then **quit the host app completely (including any menu-bar icon) and reopen it.
|
|
|
87
87
|
| `ENOENT` mentioning cliclick | `brew install cliclick` (or set `NORACU_CLICLICK`) |
|
|
88
88
|
| `spawn node` / `npx` `ENOENT` from a GUI host | use an absolute `node` path in the config — nvm/volta/homebrew shims are not on the GUI `PATH` |
|
|
89
89
|
| `browser_launch` hangs on first use | one-time Chromium download; or auto-install disabled (`NORACU_DISABLE_AUTO_INSTALL=1`) → run `npx playwright install chromium` |
|
|
90
|
-
| `phone_*` says `imctl unavailable
|
|
90
|
+
| `phone_*` says `imctl unavailable: no python3 with pyobjc found` | install pyobjc for a python3 (`python3 -m pip install pyobjc-framework-Quartz pyobjc-framework-Vision pyobjc-framework-AppKit`; Homebrew python3 is the usual choice) or set `NORACU_PYTHON` — see [iPhone layer](#iphone-layer-experimental) |
|
|
91
91
|
| Typed text comes out re-spaced (e.g. "no ra cu") | a CJK input method is active — use `mac_paste` instead of `mac_type` |
|
|
92
92
|
| `element_no_longer_valid` | the snapshot is older than 30 s or the UI changed — re-run `mac_ax_state` / `browser_snapshot` and use a fresh index |
|
|
93
93
|
|
|
@@ -97,12 +97,15 @@ Full worked examples: [docs/usage.md](docs/usage.md).
|
|
|
97
97
|
|
|
98
98
|
## iPhone layer (experimental)
|
|
99
99
|
|
|
100
|
-
The
|
|
100
|
+
The 14 `phone_*` tools drive iPhone Mirroring through **imctl**, a small Python driver that ships inside this package (`imctl/`, MIT, about 1,000 lines). Nothing else to install on noracu's side; the layer stays experimental because it depends on a live mirror and an unlocked phone.
|
|
101
101
|
|
|
102
|
-
-
|
|
103
|
-
- **Without imctl:** most `phone_*` tools return a structured `imctl unavailable` error. Two tools still work when the mirror is connected, because they go through the iPhone Mirroring menu: `phone_home` (Home Screen) and `phone_app_switcher` (App Switcher).
|
|
102
|
+
**Requirements:** macOS 15+ · iPhone Mirroring connected (same Apple Account, Bluetooth + Wi-Fi on) · Accessibility + Screen Recording granted to the host app · a python3 that imports pyobjc (Quartz, Vision, AppKit).
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
**Python:** set `NORACU_PYTHON=/path/to/python3`, or let noracu probe `/opt/homebrew/bin/python3`, `/usr/local/bin/python3`, `python3`, `/usr/bin/python3` once, on the first `phone_*` call. Apple's `/usr/bin/python3` usually lacks pyobjc; the common working setup is Homebrew's python3 plus `python3 -m pip install pyobjc-framework-Quartz pyobjc-framework-Vision pyobjc-framework-AppKit`. When nothing qualifies, every `phone_*` tool returns one line — `imctl unavailable: no python3 with pyobjc found (tried …) — run …` — and the Mac and browser layers are unaffected.
|
|
105
|
+
|
|
106
|
+
**First success:** `phone_status` → if `paused` is `true`, `phone_launch` (brings the mirror to the front and clicks Resume) → `phone_screenshot` → `phone_ocr` → `phone_find` / `phone_tap`. Screenshots and OCR capture the Mirroring window by window id, so they are the phone's own pixels even when another window covers the mirror.
|
|
107
|
+
|
|
108
|
+
**Known limits:** the iPhone must be unlocked (unlocking cannot be driven from the Mac); `paused` reads `null` on non-English macOS; `phone_open` relies on Spotlight + OCR and can miss apps whose labels are truncated. `phone_home` / `phone_app_switcher` go through the iPhone Mirroring menu and work without python when the mirror is connected.
|
|
106
109
|
|
|
107
110
|
## Environment variables
|
|
108
111
|
|
|
@@ -113,7 +116,8 @@ No `phone_*` tool runs without an active iPhone Mirroring session on this Mac. T
|
|
|
113
116
|
| `NORACU_ALLOW_FILE_URL` | unset | `=1` lets `browser_navigate` open `file://` URLs (blocked by default) |
|
|
114
117
|
| `NORACU_DISABLE_AUTO_INSTALL` | unset | `=1` skips the Chromium auto-install on first `browser_launch` |
|
|
115
118
|
| `NORACU_CLICLICK` | `/opt/homebrew/bin/cliclick` | cliclick binary path |
|
|
116
|
-
| `
|
|
119
|
+
| `NORACU_PYTHON` | auto-probe | python3 used for the bundled iPhone driver; must import pyobjc (Quartz, Vision, AppKit). Unset → first working of `/opt/homebrew/bin/python3`, `/usr/local/bin/python3`, `python3`, `/usr/bin/python3` |
|
|
120
|
+
| `NORACU_IMCTL_DIR` | bundled `imctl/` | override the driver location (development only) |
|
|
117
121
|
|
|
118
122
|
## Security
|
|
119
123
|
|
package/README.zh-CN.md
CHANGED
|
@@ -23,9 +23,9 @@ For English, see [README.md](README.md).
|
|
|
23
23
|
|---|---|---|---|
|
|
24
24
|
| **Mac** | 9 个(`mac_*`) | cliclick、screencapture、JXA System Events | stable |
|
|
25
25
|
| **Browser** | 12 个(`browser_*`) | Playwright(持久化 profile) | stable |
|
|
26
|
-
| **iPhone** | 14 个(`phone_*`) |
|
|
26
|
+
| **iPhone** | 14 个(`phone_*`) | 经随包附带的 imctl 驱动 iPhone Mirroring(Python + pyobjc) | experimental |
|
|
27
27
|
|
|
28
|
-
iPhone
|
|
28
|
+
iPhone 层是实验性的,驱动 imctl 随包附带(在 `imctl/`)。它需要本 Mac 上有活动的 iPhone Mirroring 会话和一个带 pyobjc 的 python3;找不到这样的 python 时,`phone_*` 工具返回一行带安装命令的 `imctl unavailable` 错误,Mac 层和 Browser 层不受影响(`phone_home` / `phone_app_switcher` 即便如此仍可经 iPhone Mirroring 菜单工作)。observe/act 词汇表沿用 ChatGPT computer-use 推广的 action/observation 拆分思路,在开放的本地工具上重新实现。
|
|
29
29
|
|
|
30
30
|
## 为什么用 noracu
|
|
31
31
|
|
|
@@ -38,7 +38,7 @@ noracu 是本地的"手",不是"脑"。决策由你的 MCP client(Claude Des
|
|
|
38
38
|
- **真正能关掉的开关。** `NORACU_READ_ONLY=1`(只观察,默认拒绝)、`NORACU_DISABLE_EVAL=1`、以及默认封禁 `file://`。见 [安全](#安全)。
|
|
39
39
|
- **可验证。** 34 项端到端检查(`npm test`)、CI 跑在 macos-latest、MIT 许可,整个 server 就一个文件:`src/server.mjs`。
|
|
40
40
|
|
|
41
|
-
**不适合的场景:** 你需要多个浏览器标签页或会话(当前仅单会话)、你不在 macOS 上(Mac 与 iPhone 层仅限 macOS;Browser
|
|
41
|
+
**不适合的场景:** 你需要多个浏览器标签页或会话(当前仅单会话)、你不在 macOS 上(Mac 与 iPhone 层仅限 macOS;Browser 层跨平台)、或你现在就需要一个成品级的 iPhone 层(它已随包附带,但仍是实验性)。如果你只需要浏览器自动化,playwright-mcp 有 noracu v0.1 还没有的 tab 管理和 selector API;noracu 的价值在于:同一个 agent 还得离开浏览器去操作 Mac 应用。
|
|
42
42
|
|
|
43
43
|
## 快速开始
|
|
44
44
|
|
|
@@ -87,7 +87,7 @@ Claude Code:`claude mcp add noracu -- /绝对路径/TO/node /绝对路径/nora
|
|
|
87
87
|
| `ENOENT` 提到 cliclick | `brew install cliclick`(或设 `NORACU_CLICLICK`) |
|
|
88
88
|
| GUI 宿主报 `spawn node` / `npx` `ENOENT` | 配置里改用绝对 `node` 路径——nvm/volta/homebrew 的 shim 不在 GUI `PATH` 上 |
|
|
89
89
|
| 首次 `browser_launch` 卡住 | 一次性 Chromium 下载;或自动安装被关闭(`NORACU_DISABLE_AUTO_INSTALL=1`)→ 执行 `npx playwright install chromium` |
|
|
90
|
-
| `phone_*` 报 `imctl unavailable
|
|
90
|
+
| `phone_*` 报 `imctl unavailable: no python3 with pyobjc found` | 给某个 python3 装 pyobjc(`python3 -m pip install pyobjc-framework-Quartz pyobjc-framework-Vision pyobjc-framework-AppKit`,通常选 Homebrew 的 python3)或设 `NORACU_PYTHON`——见[iPhone 层](#iphone-层实验性) |
|
|
91
91
|
| 输入的文本被重新空格化(如 "no ra cu") | 有 CJK 输入法在激活——用 `mac_paste` 代替 `mac_type` |
|
|
92
92
|
| `element_no_longer_valid` | 快照超过 30 秒或界面变了——重跑 `mac_ax_state` / `browser_snapshot` 并用新索引 |
|
|
93
93
|
|
|
@@ -97,12 +97,15 @@ Claude Code:`claude mcp add noracu -- /绝对路径/TO/node /绝对路径/nora
|
|
|
97
97
|
|
|
98
98
|
## iPhone 层(实验性)
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
14 个 `phone_*` 工具经 **imctl** 驱动 iPhone Mirroring——一个随本包附带的小型 Python 驱动(`imctl/`,MIT,约 1,000 行)。noracu 这边不需要再装别的;该层仍为实验性,因为它依赖在线的镜像会话和已解锁的手机。
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
- **没有 imctl 时:** 多数 `phone_*` 返回结构化的 `imctl unavailable` 错误。有两个工具在镜像已连接时仍可用,因为它们走 iPhone Mirroring 菜单:`phone_home`(回主屏)和 `phone_app_switcher`(打开 App 切换器)。
|
|
102
|
+
**要求:** macOS 15+ · iPhone Mirroring 已连接(同一 Apple 账户,蓝牙 + Wi-Fi 打开)· Accessibility + Screen Recording 授给宿主应用 · 一个能导入 pyobjc(Quartz、Vision、AppKit)的 python3。
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
**Python:** 设置 `NORACU_PYTHON=/path/to/python3`,或由 noracu 在首次调用 `phone_*` 时依次探测一次 `/opt/homebrew/bin/python3`、`/usr/local/bin/python3`、`python3`、`/usr/bin/python3`。Apple 自带的 `/usr/bin/python3` 通常没有 pyobjc;常见可用配置是 Homebrew 的 python3 加 `python3 -m pip install pyobjc-framework-Quartz pyobjc-framework-Vision pyobjc-framework-AppKit`。都不可用时,每个 `phone_*` 工具返回一行错误——`imctl unavailable: no python3 with pyobjc found (tried …) — run …`——Mac 层与 Browser 层不受影响。
|
|
105
|
+
|
|
106
|
+
**首次成功路径:** `phone_status` → 若 `paused` 为 `true`,先 `phone_launch`(置前并点击 Resume)→ `phone_screenshot` → `phone_ocr` → `phone_find` / `phone_tap`。截图与 OCR 按窗口 id 截取镜像窗口,即使被其它窗口遮挡,拿到的也是手机自身的画面。
|
|
107
|
+
|
|
108
|
+
**已知限制:** 手机须处于解锁状态(解锁无法从 Mac 侧代做);非英文 macOS 下 `paused` 读为 `null`;`phone_open` 依赖 Spotlight + OCR,标签被截断的 app 可能找不到。`phone_home` / `phone_app_switcher` 走 iPhone Mirroring 菜单,镜像已连接时无需 python 也能用。
|
|
106
109
|
|
|
107
110
|
## 环境变量
|
|
108
111
|
|
|
@@ -113,7 +116,8 @@ iPhone 层有 14 个 `phone_*` 工具,经 **imctl**(尚未公开的伴生工
|
|
|
113
116
|
| `NORACU_ALLOW_FILE_URL` | 未设 | `=1` 允许 `browser_navigate` 打开 `file://` URL(默认封禁) |
|
|
114
117
|
| `NORACU_DISABLE_AUTO_INSTALL` | 未设 | `=1` 跳过首次 `browser_launch` 的 Chromium 自动安装 |
|
|
115
118
|
| `NORACU_CLICLICK` | `/opt/homebrew/bin/cliclick` | cliclick 二进制路径 |
|
|
116
|
-
| `
|
|
119
|
+
| `NORACU_PYTHON` | 自动探测 | 运行随包 iPhone 驱动的 python3;须能导入 pyobjc(Quartz、Vision、AppKit)。未设置时依次尝试 `/opt/homebrew/bin/python3`、`/usr/local/bin/python3`、`python3`、`/usr/bin/python3` |
|
|
120
|
+
| `NORACU_IMCTL_DIR` | 包内 `imctl/` | 覆盖驱动位置(仅开发用) |
|
|
117
121
|
|
|
118
122
|
## 安全
|
|
119
123
|
|
package/docs/architecture.md
CHANGED
|
@@ -39,17 +39,18 @@ A single persistent Playwright 1.58 session (one browser, one context), held in
|
|
|
39
39
|
|
|
40
40
|
### iPhone layer (14 tools)
|
|
41
41
|
|
|
42
|
-
imctl subprocess (
|
|
42
|
+
The bundled imctl driver (`imctl/`, Python) runs as a subprocess (`<python3> -m imctl.cli`, PYTHONPATH = `imctl/src`, no bytecode written). The interpreter is `NORACU_PYTHON` or the first of `/opt/homebrew/bin/python3`, `/usr/local/bin/python3`, `python3`, `/usr/bin/python3` that imports pyobjc (Quartz, Vision, AppKit); probed once, lazily, on the first `phone_*` call. **Experimental**: when no such python exists, tools return a structured one-line `imctl unavailable` error with the install command rather than crashing the server (`phone_home`/`phone_app_switcher` still work via the iPhone Mirroring menu).
|
|
43
43
|
|
|
44
|
-
- Observe: `phone_ocr` (coordinates + text), `phone_screenshot` (captures the Mirroring window region), `phone_find` (locates text)
|
|
44
|
+
- Observe: `phone_ocr` (coordinates + text), `phone_screenshot` (captures the Mirroring window by CGWindow id, so a covered window still yields its own pixels; region capture is the fallback), `phone_find` (locates text)
|
|
45
45
|
- Act: `phone_tap/longpress/swipe` (normalized 0-1 coordinates), `phone_home` / `phone_app_switcher` (iPhone Mirroring menu first, imctl gesture fallback), `phone_scroll`, `phone_type` (clipboard-based, IME-safe), `phone_key`
|
|
46
|
-
- Session: `phone_status
|
|
46
|
+
- Session: `phone_status` (running, window, window_id, onscreen, frontmost, paused), `phone_launch` (ensure running + frontmost, clicks Resume when the session shows "Connection Paused"), `phone_open` (Spotlight + OCR + tap)
|
|
47
47
|
|
|
48
48
|
## Configuration
|
|
49
49
|
|
|
50
50
|
| Environment variable | Default | Description |
|
|
51
51
|
|---|---|---|
|
|
52
|
-
| `
|
|
52
|
+
| `NORACU_PYTHON` | auto-probe (see iPhone layer) | python3 that runs the bundled driver; must import pyobjc |
|
|
53
|
+
| `NORACU_IMCTL_DIR` | bundled `imctl/` (`src/../imctl`) | override the driver location (development only) |
|
|
53
54
|
| `NORACU_CLICLICK` | `/opt/homebrew/bin/cliclick` | path to cliclick |
|
|
54
55
|
| `NORACU_READ_ONLY` | unset (full control) | `=1` observation-only mode: a default-deny allowlist of 12 tools — the observe tools (`mac_screenshot`, `mac_ax_state`, `browser_snapshot`, `browser_screenshot`, `browser_read`, `phone_status`, `phone_screenshot`, `phone_ocr`, `phone_find`) plus browser session lifecycle (`browser_launch`, `browser_navigate`, `browser_close`). `browser_navigate` still issues a page-load request. All input-synthesis tools (click/drag/type/key/paste/tap/swipe/evaluate/…) are rejected |
|
|
55
56
|
| `NORACU_DISABLE_EVAL` | unset (enabled) | `=1` makes `browser_evaluate` return an error on every call (kill switch for arbitrary JS execution; it stays listed) |
|
|
@@ -61,7 +62,7 @@ imctl subprocess (`python3 -m imctl.cli`, PYTHONPATH pointing at imctl/src) driv
|
|
|
61
62
|
- Mac-layer actions spawn subprocesses (cliclick/osascript); per-action latency is tens of milliseconds (subprocess spawn; not benchmarked in this release) — a resident FFI process could cut it (see roadmap)
|
|
62
63
|
- No axState diffing (every observation is a full snapshot)
|
|
63
64
|
- Single browser session (no tab-level management)
|
|
64
|
-
- The iPhone layer requires iPhone Mirroring to be online; OCR accuracy depends on screen content
|
|
65
|
+
- The iPhone layer requires iPhone Mirroring to be online and the phone unlocked; OCR accuracy depends on screen content; `paused` detection reads the English "Connection Paused" label
|
|
65
66
|
- macOS-only (Mac/iPhone layers); the Browser layer is cross-platform
|
|
66
67
|
|
|
67
68
|
## Roadmap (v2 candidates)
|
package/docs/tools.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Tool reference
|
|
2
2
|
|
|
3
3
|
> Auto-generated from the running server (`node scripts/gen-tools.mjs`) — always in sync with the implementation.
|
|
4
|
-
> noracu v0.1.
|
|
4
|
+
> noracu v0.1.1 · 35 tools
|
|
5
5
|
|
|
6
6
|
## Mac layer (computer-use)
|
|
7
7
|
|
|
@@ -44,18 +44,18 @@
|
|
|
44
44
|
|
|
45
45
|
| Tool | Arguments | Description |
|
|
46
46
|
|---|---|---|
|
|
47
|
-
| `phone_status` | — | EXPERIMENTAL.
|
|
48
|
-
| `phone_launch` | — | EXPERIMENTAL.
|
|
49
|
-
| `phone_screenshot` | — | EXPERIMENTAL.
|
|
50
|
-
| `phone_ocr` | — | EXPERIMENTAL.
|
|
51
|
-
| `phone_find` | text:string? | EXPERIMENTAL.
|
|
52
|
-
| `phone_tap` | x:number?, y:number? | EXPERIMENTAL.
|
|
53
|
-
| `phone_longpress` | x:number?, y:number?, duration:number? | EXPERIMENTAL.
|
|
54
|
-
| `phone_swipe` | from:array?, to:array?, ms:number? | EXPERIMENTAL.
|
|
55
|
-
| `phone_home` | — | EXPERIMENTAL. Go to the iPhone Home Screen via the iPhone Mirroring menu (works without
|
|
56
|
-
| `phone_app_switcher` | — | EXPERIMENTAL. Open the iPhone App Switcher via the iPhone Mirroring menu (works without
|
|
57
|
-
| `phone_scroll` | direction:string?, distance:number? | EXPERIMENTAL.
|
|
58
|
-
| `phone_type` | text:string? | EXPERIMENTAL.
|
|
59
|
-
| `phone_key` | key:string? | EXPERIMENTAL.
|
|
60
|
-
| `phone_open` | app:string? | EXPERIMENTAL.
|
|
47
|
+
| `phone_status` | — | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Return the iPhone Mirroring session status as JSON (running, window, onscreen, frontmost, paused). |
|
|
48
|
+
| `phone_launch` | — | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Ensure iPhone Mirroring is running, brought to the front, and resumed if the connection is paused. |
|
|
49
|
+
| `phone_screenshot` | — | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Screenshot the iPhone screen (the Mirroring window, captured by window id so it works even when covered). |
|
|
50
|
+
| `phone_ocr` | — | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. OCR the iPhone screen into text with coordinates. |
|
|
51
|
+
| `phone_find` | text:string? | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Locate text on the iPhone screen. |
|
|
52
|
+
| `phone_tap` | x:number?, y:number? | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Tap at {x, y} in normalized 0-1 coordinates. |
|
|
53
|
+
| `phone_longpress` | x:number?, y:number?, duration:number? | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Long-press at {x, y} for an optional duration in seconds. |
|
|
54
|
+
| `phone_swipe` | from:array?, to:array?, ms:number? | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Swipe from [x,y] to [x,y] over an optional duration in ms. |
|
|
55
|
+
| `phone_home` | — | EXPERIMENTAL. Go to the iPhone Home Screen via the iPhone Mirroring menu (works without python/pyobjc when the mirror is connected); falls back to an imctl swipe. |
|
|
56
|
+
| `phone_app_switcher` | — | EXPERIMENTAL. Open the iPhone App Switcher via the iPhone Mirroring menu (works without python/pyobjc when the mirror is connected); falls back to an imctl gesture. |
|
|
57
|
+
| `phone_scroll` | direction:string?, distance:number? | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Scroll up or down by a distance. |
|
|
58
|
+
| `phone_type` | text:string? | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Type text via the clipboard (IME-safe). |
|
|
59
|
+
| `phone_key` | key:string? | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Press a key: return, delete, up, down, left, or right. |
|
|
60
|
+
| `phone_open` | app:string? | EXPERIMENTAL. Needs iPhone Mirroring and a python3 with pyobjc. Open an app via Spotlight search plus OCR-guided tap. |
|
|
61
61
|
|
package/docs/usage.md
CHANGED
|
@@ -161,7 +161,8 @@ model — don't let it override the task.
|
|
|
161
161
|
| `NORACU_ALLOW_FILE_URL` | unset | `=1` allows `browser_navigate` to open `file://` |
|
|
162
162
|
| `NORACU_DISABLE_AUTO_INSTALL` | unset | `=1` skips Chromium auto-install on first `browser_launch` |
|
|
163
163
|
| `NORACU_CLICLICK` | `/opt/homebrew/bin/cliclick` | cliclick binary path |
|
|
164
|
-
| `
|
|
164
|
+
| `NORACU_PYTHON` | auto-probe | python3 for the bundled iPhone driver (must import pyobjc: Quartz, Vision, AppKit) |
|
|
165
|
+
| `NORACU_IMCTL_DIR` | bundled `imctl/` | override the driver location (development only) |
|
|
165
166
|
| `NORACU_CI` | unset | `=1` only affects `npm test` (skips hardware-gated checks) |
|
|
166
167
|
|
|
167
168
|
## 8. Error reference
|
|
@@ -174,10 +175,39 @@ model — don't let it override the task.
|
|
|
174
175
|
| `ENOENT` cliclick | `brew install cliclick` or set `NORACU_CLICLICK` |
|
|
175
176
|
| `… blocked by NORACU_READ_ONLY=1` | you're in observation-only mode — unset it to act |
|
|
176
177
|
| `browser_evaluate disabled by NORACU_DISABLE_EVAL=1` | eval kill-switch is on |
|
|
177
|
-
| `imctl unavailable …` |
|
|
178
|
+
| `imctl unavailable: no python3 with pyobjc found …` | install pyobjc for a python3 (`python3 -m pip install pyobjc-framework-Quartz pyobjc-framework-Vision pyobjc-framework-AppKit`) or set `NORACU_PYTHON` |
|
|
178
179
|
|
|
179
180
|
## 9. iPhone layer
|
|
180
181
|
|
|
181
|
-
Experimental
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
Experimental. The driver (imctl, Python) ships inside the package; the tools
|
|
183
|
+
need iPhone Mirroring connected and a python3 with pyobjc (`NORACU_PYTHON`, or
|
|
184
|
+
the automatic probe described in the README). A typical session:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
phone_status
|
|
188
|
+
→ {"running": true, "window": {...}, "window_id": 275104, "onscreen": true,
|
|
189
|
+
"frontmost": false, "paused": true}
|
|
190
|
+
|
|
191
|
+
phone_launch # brings the mirror to the front and clicks Resume
|
|
192
|
+
→ iPhone Mirroring ready
|
|
193
|
+
|
|
194
|
+
phone_status
|
|
195
|
+
→ {... "paused": false ...}
|
|
196
|
+
|
|
197
|
+
phone_ocr # OCR of the Mirroring window (captured by window id)
|
|
198
|
+
→ (0.50,0.06) 'Settings' [1.00]
|
|
199
|
+
(0.18,0.31) 'FaceTime' [1.00]
|
|
200
|
+
-- 2 hits
|
|
201
|
+
|
|
202
|
+
phone_find {"text": "Settings"}
|
|
203
|
+
→ (0.50,0.06) 'Settings' [1.00]
|
|
204
|
+
|
|
205
|
+
phone_tap {"x": 0.50, "y": 0.06}
|
|
206
|
+
→ tapped (0.5, 0.06)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Coordinates are normalized 0–1 over the mirror window. `phone_screenshot`
|
|
210
|
+
returns a PNG of the window's own pixels even when another window covers it.
|
|
211
|
+
If the phone is locked, unlock it on the phone; that step cannot be driven
|
|
212
|
+
from the Mac. `phone_home` / `phone_app_switcher` go through the iPhone
|
|
213
|
+
Mirroring menu and work without python when the mirror is connected.
|
package/imctl/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 imctl contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/imctl/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# imctl — Control your iPhone from macOS
|
|
2
|
+
|
|
3
|
+
> Ships inside [noracu](https://github.com/sgInnora/noracu) (`imctl/`) as the driver behind its
|
|
4
|
+
> `phone_*` MCP tools; it also works standalone from this directory.
|
|
5
|
+
|
|
6
|
+
`imctl` drives the **iPhone Mirroring** app on macOS (Sequoia and later)
|
|
7
|
+
from the command line: tap, swipe, type, OCR the screen, and open apps —
|
|
8
|
+
all locally, no network, no API keys.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
$ imctl status
|
|
12
|
+
$ imctl tap 0.5 0.5
|
|
13
|
+
$ imctl home
|
|
14
|
+
$ imctl type "hello"
|
|
15
|
+
$ imctl ocr
|
|
16
|
+
(0.50,0.06) 'Settings'
|
|
17
|
+
(0.18,0.31) 'FaceTime'
|
|
18
|
+
...
|
|
19
|
+
$ imctl open Photos
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
┌────────────┐ CGEvent (HID tap) ┌───────────────────┐
|
|
26
|
+
│ imctl │ ────────────────────▶ │ iPhone Mirroring │
|
|
27
|
+
│ │ │ (video stream) │
|
|
28
|
+
│ Vision ◀──┼─── screencapture ─────│ │
|
|
29
|
+
│ OCR │ └─────────┬─────────┘
|
|
30
|
+
└────────────┘ │ Continuity
|
|
31
|
+
▼
|
|
32
|
+
┌─────────┐
|
|
33
|
+
│ iPhone │
|
|
34
|
+
└─────────┘
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **Input** — synthesized CoreGraphics mouse/keyboard events posted to
|
|
38
|
+
the HID event tap. iPhone Mirroring translates them into touches.
|
|
39
|
+
- **Vision** — the macOS Vision framework runs OCR locally on a
|
|
40
|
+
window screenshot (captured by CGWindow id, so it is the window's own
|
|
41
|
+
pixels even when covered) and returns text with normalized coordinates.
|
|
42
|
+
- **Verification** — every gesture can be followed by a downsampled
|
|
43
|
+
pixel-hash comparison to confirm the screen actually changed.
|
|
44
|
+
|
|
45
|
+
## Why these specific parameters
|
|
46
|
+
|
|
47
|
+
Every constant in `input.py` is backed by empirical testing against
|
|
48
|
+
the mirroring gesture recognizer:
|
|
49
|
+
|
|
50
|
+
| Parameter | Value | Why |
|
|
51
|
+
|-----------|-------|-----|
|
|
52
|
+
| tap down-up interval | 50 ms | shorter intervals are sometimes dropped |
|
|
53
|
+
| swipe steps | 20 | single-shot drags are **not** recognized as swipes |
|
|
54
|
+
| initial nudge | 3 px | disambiguates swipe from icon long-press-drag |
|
|
55
|
+
| swipe curve | quadratic ease-in-out | stays inside the recognizer's velocity window |
|
|
56
|
+
| home gesture start | 0.95 height | higher start points are treated as in-app scrolls |
|
|
57
|
+
| text input | clipboard + Cmd+V | direct keystroke synthesis breaks under CJK IMEs |
|
|
58
|
+
|
|
59
|
+
## Requirements
|
|
60
|
+
|
|
61
|
+
- macOS 15 (Sequoia) or later with **iPhone Mirroring** configured
|
|
62
|
+
(iPhone signed into the same Apple Account, Bluetooth + Wi-Fi on)
|
|
63
|
+
- Python 3.10+ with pyobjc: `python3 -m pip install pyobjc-framework-Quartz pyobjc-framework-Vision pyobjc-framework-AppKit`
|
|
64
|
+
(Apple's `/usr/bin/python3` usually lacks it; Homebrew's `python3` is the common working choice)
|
|
65
|
+
- **Accessibility** and **Screen Recording** permissions granted to
|
|
66
|
+
your terminal app (System Settings → Privacy & Security)
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cd imctl
|
|
72
|
+
pip install -e . # or: pip install -e ".[pyobjc]" if pyobjc missing
|
|
73
|
+
imctl status
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Commands
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
|---------|-------------|
|
|
80
|
+
| `status` | session + window info (JSON: running, window, window_id, onscreen, frontmost, paused) |
|
|
81
|
+
| `screenshot [PATH]` | capture the mirroring window to PNG by window id (works when covered) |
|
|
82
|
+
| `resume` | click Resume when the window shows Connection Paused |
|
|
83
|
+
| `launch` | ensure mirroring is running and frontmost |
|
|
84
|
+
| `tap X Y` | tap at normalized coords (0-1) |
|
|
85
|
+
| `longpress X Y [D]` | long-press (default 0.8s) |
|
|
86
|
+
| `swipe FX FY TX TY [--ms]` | eased swipe between two points |
|
|
87
|
+
| `home` | home gesture (bottom-edge swipe up) |
|
|
88
|
+
| `app-switcher` | swipe up and hold |
|
|
89
|
+
| `scroll up/down [D]` | scroll by normalized distance |
|
|
90
|
+
| `page left/right` | home-screen paging |
|
|
91
|
+
| `type TEXT` | type text (clipboard-based, IME-safe) |
|
|
92
|
+
| `key K` | return / delete / up / down / left / right |
|
|
93
|
+
| `ocr` | OCR the screen with coordinates |
|
|
94
|
+
| `find TEXT` | locate text on screen |
|
|
95
|
+
| `open APP` | open an app via Spotlight + OCR + tap |
|
|
96
|
+
| `verify ACTION` | run a gesture and verify the screen changed |
|
|
97
|
+
|
|
98
|
+
## Python API
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from imctl import gestures, input, vision
|
|
102
|
+
|
|
103
|
+
gestures.launch()
|
|
104
|
+
gestures.home()
|
|
105
|
+
|
|
106
|
+
input.tap(0.5, 0.5)
|
|
107
|
+
input.swipe(0.5, 0.8, 0.5, 0.2)
|
|
108
|
+
input.type_text("hello")
|
|
109
|
+
|
|
110
|
+
hits = vision.read_screen()
|
|
111
|
+
for h in hits:
|
|
112
|
+
print(h.nx, h.ny, h.text)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Design notes
|
|
116
|
+
|
|
117
|
+
**Dynamic window tracking.** Window bounds are re-resolved before every
|
|
118
|
+
gesture — the window can move, close, or be re-created between actions.
|
|
119
|
+
A cached position means tapping the wrong screen region.
|
|
120
|
+
|
|
121
|
+
**Observe → act → re-observe.** `verify.verify_action()` implements the
|
|
122
|
+
observe → act → re-observe discipline: an action is only considered
|
|
123
|
+
successful when the screen content actually changed. Dropped events are
|
|
124
|
+
common enough with synthesized input that silent failure is the default
|
|
125
|
+
assumption.
|
|
126
|
+
|
|
127
|
+
**Local-only.** No telemetry, no network calls, no API keys. OCR runs
|
|
128
|
+
through the Vision framework on-device.
|
|
129
|
+
|
|
130
|
+
## Limitations
|
|
131
|
+
|
|
132
|
+
- The iPhone must stay unlocked and connected; lock-screen state is
|
|
133
|
+
surfaced by `status` only indirectly (window content freezes).
|
|
134
|
+
- Drag-and-drop between Mac and iPhone is not supported.
|
|
135
|
+
- Multi-display setups: the window is located by AX query, so it works
|
|
136
|
+
on any display, but coordinates are always screen-global.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "imctl"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Control iPhone Mirroring on macOS from the CLI — CGEvent input, Vision OCR, pixel verification"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "imctl contributors" }]
|
|
13
|
+
keywords = ["iphone", "mirroring", "macos", "automation", "cgevent", "vision-ocr"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: MacOS X",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Utilities",
|
|
21
|
+
]
|
|
22
|
+
# pyobjc comes with macOS system Python; declare as optional to avoid
|
|
23
|
+
# clobbering a user's pyobjc installation.
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
pyobjc = [
|
|
28
|
+
"pyobjc-framework-Quartz>=10.0",
|
|
29
|
+
"pyobjc-framework-Vision>=10.0",
|
|
30
|
+
"pyobjc-framework-AppKit>=10.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
imctl = "imctl.cli:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""imctl — Control your iPhone from macOS via iPhone Mirroring.
|
|
2
|
+
|
|
3
|
+
A CLI tool that drives the iPhone Mirroring app on macOS with
|
|
4
|
+
CGEvent input, Vision OCR, and pixel-level verification.
|
|
5
|
+
|
|
6
|
+
Architecture:
|
|
7
|
+
|
|
8
|
+
- window.py — AX-based dynamic window tracking (never assume position)
|
|
9
|
+
- input.py — CGEvent engine: tap / long-press / swipe / scroll / key
|
|
10
|
+
- vision.py — macOS Vision framework OCR with normalized coordinates
|
|
11
|
+
- verify.py — pixel-hash diff to confirm actions took effect
|
|
12
|
+
- gestures.py — composed high-level gestures (home, app switcher, ...)
|
|
13
|
+
- cli.py — argparse CLI entry point
|
|
14
|
+
|
|
15
|
+
Key design decisions (each backed by empirical testing):
|
|
16
|
+
|
|
17
|
+
1. Swipe uses 20-step ease-in-out interpolation with an initial 3px
|
|
18
|
+
nudge. Single-shot drags are dropped by iPhone Mirroring's gesture
|
|
19
|
+
recognizer; stepped drags are recognized reliably.
|
|
20
|
+
2. Text input goes through the clipboard (Cmd+V) to bypass IME
|
|
21
|
+
interference, restoring the user's clipboard afterwards.
|
|
22
|
+
3. Tap uses a 50ms down-up interval (empirically the minimum that
|
|
23
|
+
iPhone Mirroring registers reliably).
|
|
24
|
+
4. OCR runs locally via the Vision framework — no network, no API
|
|
25
|
+
keys, and returns normalized (0-1) coordinates ready for tap().
|
|
26
|
+
5. Every window lookup is dynamic: the window may move or close
|
|
27
|
+
between actions, so we re-resolve bounds before each gesture.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
31
|
+
__all__ = ["cli", "window", "input", "vision", "verify", "gestures"]
|