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 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 is a companion checkout (sibling directory or `NORACU_IMCTL_DIR`);
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 imctl (not public; 2 tools work without it) | experimental |
26
+ | **iPhone** | 14 (`phone_*`) | iPhone Mirroring via the bundled imctl driver (Python + pyobjc) | experimental |
27
27
 
28
- The iPhone layer is experimental and gated by imctl, a companion tool that is not yet public. Without imctl, most `phone_*` tools return a structured error; the Mac and browser layers are unaffected (`phone_home` / `phone_app_switcher` still work through the iPhone Mirroring menu when a phone is connected). The observe/act vocabulary follows the action/observation split popularized by ChatGPT's computer-use, re-implemented on open local tooling.
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 the iPhone layer today. 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.
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 … missing Quartz` | expected — imctl is not public; ignore the phone tools (two menu tools still work, see below) |
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 iPhone layer has 14 `phone_*` tools that drive iPhone Mirroring through **imctl**, a companion tool that is not yet public.
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
- - **With imctl** installed and an iPhone mirrored to this Mac: all 14 tools are available.
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
- No `phone_*` tool runs without an active iPhone Mirroring session on this Mac. Tool names and behavior may change before imctl is released. Do not set `NORACU_IMCTL_DIR` unless you already have imctl from its author.
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
- | `NORACU_IMCTL_DIR` | sibling `../imctl` | imctl checkout location (do not set unless you have imctl) |
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_*`) | 经 imctl 驱动 iPhone Mirroring(尚未公开;2 个工具无需它也能用) | experimental |
26
+ | **iPhone** | 14 个(`phone_*`) | 经随包附带的 imctl 驱动 iPhone Mirroring(Python + pyobjc) | experimental |
27
27
 
28
- iPhone 层是实验性的,依赖 imctl——一个尚未公开的伴生工具。没有 imctl 时,多数 `phone_*` 工具返回结构化错误;Mac 层和 Browser 层不受影响(`phone_home` / `phone_app_switcher` 在镜像已连接时仍可经 iPhone Mirroring 菜单工作)。observe/act 词汇表沿用 ChatGPT computer-use 推广的 action/observation 拆分思路,在开放的本地工具上重新实现。
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 层跨平台)、或你现在就要用 iPhone 层。如果你只需要浏览器自动化,playwright-mcp 有 noracu v0.1 还没有的 tab 管理和 selector API;noracu 的价值在于:同一个 agent 还得离开浏览器去操作 Mac 应用。
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 … missing Quartz` | 正常——imctl 未公开;忽略 phone 工具(两个菜单工具仍可用,见下) |
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
- iPhone 层有 14 个 `phone_*` 工具,经 **imctl**(尚未公开的伴生工具)驱动 iPhone Mirroring。
100
+ 14 个 `phone_*` 工具经 **imctl** 驱动 iPhone Mirroring——一个随本包附带的小型 Python 驱动(`imctl/`,MIT,约 1,000 行)。noracu 这边不需要再装别的;该层仍为实验性,因为它依赖在线的镜像会话和已解锁的手机。
101
101
 
102
- - **装了 imctl** 且 iPhone 已镜像到本 Mac 时:14 个工具全部可用。
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
- 任何 `phone_*` 工具都需要本 Mac 上有活动的 iPhone Mirroring 会话。imctl 发布前,工具名和行为可能变化。除非你已从作者处拿到 imctl,否则不要设 `NORACU_IMCTL_DIR`。
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
- | `NORACU_IMCTL_DIR` | 同级 `../imctl` | imctl 检出位置(没有 imctl 就别设) |
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
 
@@ -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 (`python3 -m imctl.cli`, PYTHONPATH pointing at imctl/src) drives iPhone Mirroring. **Experimental**: when imctl or its Python dependencies (pyobjc/Quartz) are absent, most tools return a structured `imctl unavailable` error rather than crashing the server (`phone_home`/`phone_app_switcher` still work via the iPhone Mirroring menu).
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`, `phone_launch`, `phone_open` (Spotlight + OCR + tap)
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
- | `NORACU_IMCTL_DIR` | `../imctl` beside the repo root (`src/../../imctl`) | imctl checkout location |
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.0 · 35 tools
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. Requires imctl (not yet public). Return the iPhone Mirroring session status as JSON. |
48
- | `phone_launch` | — | EXPERIMENTAL. Requires imctl (not yet public). Ensure iPhone Mirroring is running and brought to the front. |
49
- | `phone_screenshot` | — | EXPERIMENTAL. Requires imctl (not yet public). Screenshot the iPhone screen (the Mirroring window region). |
50
- | `phone_ocr` | — | EXPERIMENTAL. Requires imctl (not yet public). OCR the iPhone screen into text with coordinates. |
51
- | `phone_find` | text:string? | EXPERIMENTAL. Requires imctl (not yet public). Locate text on the iPhone screen. |
52
- | `phone_tap` | x:number?, y:number? | EXPERIMENTAL. Requires imctl (not yet public). Tap at {x, y} in normalized 0-1 coordinates. |
53
- | `phone_longpress` | x:number?, y:number?, duration:number? | EXPERIMENTAL. Requires imctl (not yet public). Long-press at {x, y} for an optional duration in seconds. |
54
- | `phone_swipe` | from:array?, to:array?, ms:number? | EXPERIMENTAL. Requires imctl (not yet public). 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 imctl 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 imctl when the mirror is connected); falls back to an imctl gesture. |
57
- | `phone_scroll` | direction:string?, distance:number? | EXPERIMENTAL. Requires imctl (not yet public). Scroll up or down by a distance. |
58
- | `phone_type` | text:string? | EXPERIMENTAL. Requires imctl (not yet public). Type text via the clipboard (IME-safe). |
59
- | `phone_key` | key:string? | EXPERIMENTAL. Requires imctl (not yet public). Press a key: return, delete, up, down, left, or right. |
60
- | `phone_open` | app:string? | EXPERIMENTAL. Requires imctl (not yet public). Open an app via Spotlight search plus OCR-guided tap. |
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
- | `NORACU_IMCTL_DIR` | sibling `../imctl` | imctl location (leave unset unless you have imctl) |
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 …` | expected — imctl is not public; the iPhone layer is experimental |
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 and gated by imctl (not public). There are no worked examples
182
- here; see the [iPhone layer section in the README](../README.md#iphone-layer-experimental)
183
- for exactly which tools work without imctl and what the structured error means.
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.
@@ -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"]