Skip to content

Commit fa407ff

Browse files
berkozeroclaude
andcommitted
Fix input reliability: ensureFocus, CGAssociate, conditional CGEvent, verbose logging
Three root causes fixed for reliable tap/swipe input: 1. Restore CGAssociateMouseAndMouseCursorPosition in tap(), swipe(), drag() Without disassociating before CGWarp, macOS suppresses relative HID events, silently dropping clicks even though cursor position is correct. 2. Make CGEvent focus click conditional (only when wasActive=false) Sending CGEvent when session is already established disrupts HID routing. Now only sends focus click when transitioning from inactive to active. 3. Poll for activation instead of fixed sleep app.activate() is async — poll app.isActive with RunLoop processing instead of Thread.sleep. Add detailed verbose logging for diagnostics. Also includes: ensureFocus() with 5s cooldown in InputInjector, windowManager pattern for auto-focus, new commands (drag, scroll, doctor, wait-for), ActionResult JSON envelope, grid screenshot, OCR waitForText, and open-source infrastructure (CI, CONTRIBUTING, CHANGELOG, SECURITY). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a8260f4 commit fa407ff

39 files changed

Lines changed: 3236 additions & 468 deletions

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## What
2+
3+
Brief description of changes.
4+
5+
## Why
6+
7+
Motivation and context.
8+
9+
## How to Test
10+
11+
Steps to verify — ideally against a real iPhone Mirroring session.
12+
13+
## Checklist
14+
15+
- [ ] `swift build` compiles without warnings
16+
- [ ] `swift test` passes
17+
- [ ] Tested against real iPhone Mirroring (if applicable)
18+
- [ ] `--json` output works for new/modified commands
19+
- [ ] Updated `skills/iphonebase/SKILL.md` if command interface changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ jobs:
1616
run: swift build
1717

1818
- name: Run tests
19-
run: swift test
19+
run: make test

AGENTS.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# iphonebase
2+
3+
Swift CLI to control iPhone via macOS iPhone Mirroring. Built for AI agents (OpenClaw, Claude Code, MCP) and standalone terminal use.
4+
5+
## Tech Stack
6+
7+
- Swift 5.9+, macOS 14+ (requires Sequoia 15.0+ at runtime)
8+
- ArgumentParser 1.3.0
9+
- Frameworks: ScreenCaptureKit, Vision, CoreGraphics, AppKit
10+
- Karabiner-Elements required (DriverKit virtual HID for input injection)
11+
12+
## Build & Run
13+
14+
```
15+
swift build # debug
16+
swift build -c release # release
17+
swift test # run tests
18+
sudo cp .build/release/iphonebase /usr/local/bin/ # install
19+
```
20+
21+
## Project Structure
22+
23+
```
24+
Sources/
25+
IPhoneBaseCore/ # Library
26+
WindowManager.swift # Find & focus iPhone Mirroring window
27+
ScreenCapture.swift # ScreenCaptureKit capture + grid overlay
28+
OCREngine.swift # Apple Vision text recognition
29+
InputInjector.swift # Karabiner DriverKit virtual HID input
30+
HIDKeyMap.swift # USB HID keycodes & character mappings
31+
ActionResult.swift # Shared JSON response envelope
32+
iphonebase/ # CLI executable
33+
IPhoneBase.swift # Entry point, command registration
34+
Commands/ # One file per command (13 commands)
35+
skills/
36+
iphonebase/SKILL.md # OpenClaw skill definition
37+
Tests/
38+
IPhoneBaseCoreTests/ # Unit tests (HIDKeyMap)
39+
```
40+
41+
## Adding a New Command
42+
43+
1. Create `Sources/iphonebase/Commands/XxxCommand.swift`
44+
2. Implement `AsyncParsableCommand` (or `ParsableCommand` for sync-only)
45+
3. Add `XxxCommand.self` to the `subcommands` array in `IPhoneBase.swift`
46+
4. Include `--json` flag using the shared `ActionResult<T>` envelope
47+
5. Update `skills/iphonebase/SKILL.md` with the new command docs
48+
49+
## Code Conventions
50+
51+
- Import order: ArgumentParser, IPhoneBaseCore, Foundation
52+
- All commands support `--json` via shared `ActionResult<T>` envelope (`Sources/IPhoneBaseCore/ActionResult.swift`)
53+
- Results to stdout, debug/verbose to stderr
54+
- InputInjector pattern: `connect()` then `defer { disconnect() }`; set `windowBounds` for coordinate validation
55+
- Call `wm.bringToFront()` before any input injection
56+
- Errors: typed enums with `CustomStringConvertible`; throw `ExitCode.failure` for user errors
57+
- Commit messages: imperative mood ("Add feature" not "Added feature"), first line under 72 characters
58+
59+
## Coordinate System (critical)
60+
61+
- ScreenCapture captures at 2x retina resolution
62+
- OCR (Vision) returns normalized coords with bottom-left origin — must invert Y
63+
- `tap --text` handles conversion automatically; raw `tap x y` is relative to window (screen points)
64+
- All InputInjector operations use absolute screen coordinates (`window.bounds.origin + offset`)
65+
66+
## Input Injection Gotchas
67+
68+
- iPhone Mirroring blocks CGEvent clicks — only Karabiner virtual HID works
69+
- Tap sequence: `CGWarpMouseCursorPosition` → nudge-sync virtual pointer (3x 1px/-1px) → click via HID
70+
- Timing delays (`usleep`) throughout InputInjector are tuned values, not arbitrary
71+
- Karabiner daemon must be running (not just installed)
72+
73+
## OpenClaw Skill
74+
75+
- Skill at `skills/iphonebase/SKILL.md` follows AgentSkills spec (YAML frontmatter + markdown)
76+
- Requires `bins: ["iphonebase"]`, `os: ["darwin"]`
77+
- Install to `~/.openclaw/skills/` for agent discovery

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6+
7+
## [Unreleased]
8+
9+
### Added
10+
- `doctor` command — diagnostic check of all prerequisites (8 checks)
11+
- `wait-for` command — poll screen via OCR until text appears or timeout
12+
- `scroll` command — scroll up/down via mouse wheel with configurable clicks
13+
- `drag` command — smooth point-to-point drag with configurable steps
14+
- `--json` flag on all commands via shared `ActionResult<T>` envelope
15+
- `AGENTS.md` for cross-agent (Cursor, Codex, Copilot) compatibility
16+
- `SECURITY.md` vulnerability disclosure policy
17+
- `CHANGELOG.md`
18+
- PR template
19+
20+
### Changed
21+
- All commands now return structured `ActionResult` JSON with `success`, `action`, `data`, `error`, `durationMs`
22+
- Improved input injection reliability with tuned timing and nudge-sync sequence
23+
- README rewritten with badges, agent quick start, comparison table
24+
25+
## [0.1.0] - 2025-01-20
26+
27+
### Added
28+
- Initial release
29+
- `status` command — check iPhone Mirroring availability
30+
- `screenshot` command — capture screen as PNG with optional grid overlay
31+
- `describe` command — OCR text detection with coordinates and confidence
32+
- `tap` command — tap by coordinates, text (OCR), or grid cell; double-tap and long-press support
33+
- `swipe` command — directional swipe with configurable start point and distance
34+
- `type` command — character-by-character text input via virtual HID
35+
- `key` command — named key press with modifier support (cmd, shift, opt, ctrl)
36+
- `home` command — navigate to iPhone home screen
37+
- `launch` command — open app by name via Spotlight search
38+
- `IPhoneBaseCore` library: WindowManager, ScreenCapture, OCREngine, InputInjector, HIDKeyMap
39+
- OpenClaw skill definition (`skills/iphonebase/SKILL.md`)
40+
- GitHub Actions CI (build + test on macOS 15)
41+
- Unit tests for HIDKeyMap
42+
43+
[Unreleased]: https://github.com/berkozero/iphonebase/compare/v0.1.0...HEAD
44+
[0.1.0]: https://github.com/berkozero/iphonebase/releases/tag/v0.1.0

CLAUDE.md

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,46 @@
11
# iphonebase
22

3-
Swift CLI to control iPhone via macOS iPhone Mirroring. Built for AI agents (OpenClaw, Claude Code, MCP) and standalone terminal use.
3+
Swift CLI to control iPhone via macOS iPhone Mirroring. See also [AGENTS.md](AGENTS.md) for cross-agent instructions.
44

5-
## Tech Stack
6-
- Swift 5.9+, macOS 14+ (requires Sequoia 15.0+ at runtime)
7-
- ArgumentParser 1.3.0
8-
- Frameworks: ScreenCaptureKit, Vision, CoreGraphics, AppKit
9-
- Karabiner-Elements required (DriverKit virtual HID for input injection)
10-
11-
## Build & Run
12-
swift build # debug
13-
swift build -c release # release
14-
sudo cp .build/release/iphonebase /usr/local/bin/ # install
5+
## Build & Test
6+
```
7+
swift build # debug
8+
swift build -c release # release
9+
swift test # tests
10+
```
1511

1612
## Project Structure
17-
- Sources/IPhoneBaseCore/ — library: WindowManager, ScreenCapture, OCREngine, InputInjector, HIDKeyMap
18-
- Sources/iphonebase/ — CLI entry point + Commands/
19-
- skills/iphonebase/ — OpenClaw skill definition (SKILL.md)
13+
- `Sources/IPhoneBaseCore/` — library: WindowManager, ScreenCapture, OCREngine, InputInjector, HIDKeyMap, ActionResult
14+
- `Sources/iphonebase/` — CLI entry point + Commands/
15+
- `skills/iphonebase/` — OpenClaw skill (SKILL.md)
2016

2117
## Adding a New Command
22-
1. Create Sources/iphonebase/Commands/XxxCommand.swift
23-
2. Implement AsyncParsableCommand (or ParsableCommand for sync-only)
24-
3. Add XxxCommand.self to subcommands array in IPhoneBase.swift
25-
4. Include --json flag for structured output
18+
1. Create `Sources/iphonebase/Commands/XxxCommand.swift`
19+
2. Implement `AsyncParsableCommand` (or `ParsableCommand` for sync-only)
20+
3. Add `XxxCommand.self` to subcommands array in `IPhoneBase.swift`
21+
4. Include `--json` flag for structured output
22+
5. Update `skills/iphonebase/SKILL.md`
2623

2724
## Code Conventions
2825
- Import order: ArgumentParser, IPhoneBaseCore, Foundation
29-
- All commands support --json (use JSONSerialization with .prettyPrinted)
26+
- All commands support `--json` via shared `ActionResult<T>` envelope
3027
- Results to stdout, debug/verbose to stderr
31-
- InputInjector pattern: connect() then defer { disconnect() }
32-
- Call wm.bringToFront() before any input injection
33-
- Errors: typed enums with CustomStringConvertible; throw ExitCode.failure for user errors
28+
- InputInjector pattern: `connect()` then `defer { disconnect() }`; set `windowBounds` for coordinate validation
29+
- Call `wm.bringToFront()` before any input injection
30+
- Errors: typed enums with `CustomStringConvertible`; throw `ExitCode.failure` for user errors
3431

3532
## Coordinate System (critical)
3633
- ScreenCapture captures at 2x retina resolution
3734
- OCR (Vision) returns normalized coords with bottom-left origin — must invert Y
38-
- tap --text handles conversion automatically; raw tap x y is relative to window (screen points)
39-
- All InputInjector operations use absolute screen coordinates (window.bounds.origin + offset)
35+
- `tap --text` handles conversion automatically; raw `tap x y` is relative to window (screen points)
36+
- All InputInjector operations use absolute screen coordinates (`window.bounds.origin + offset`)
4037

4138
## Input Injection Gotchas
4239
- iPhone Mirroring blocks CGEvent clicks — only Karabiner virtual HID works
43-
- Tap sequence: CGWarp cursor → nudge-sync virtual pointer (3x 1px/-1px) → click via HID
44-
- Timing delays (usleep) throughout InputInjector are tuned values, not arbitrary
40+
- Tap sequence: CGWarp cursor → nudge-sync (3x 1px/-1px) → click via HID
41+
- Timing delays (`usleep`) are tuned values, not arbitrary
4542
- Karabiner daemon must be running (not just installed)
4643

47-
## OpenClaw Skill
48-
- Skill at skills/iphonebase/SKILL.md follows AgentSkills spec (YAML frontmatter + markdown)
49-
- Requires bins: ["iphonebase"], os: ["darwin"]
50-
- Install to ~/.openclaw/skills/ for agent discovery
51-
5244
## Commits
5345
- Imperative mood ("Add feature" not "Added feature")
5446
- First line under 72 characters

CONTRIBUTING.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Open a [GitHub issue](https://github.com/berkozero/iphonebase/issues/new?templat
1313

1414
If a command fails silently, re-run with `--verbose` (where supported) and include the stderr output.
1515

16+
For security vulnerabilities, see [SECURITY.md](SECURITY.md).
17+
1618
## Suggesting Features
1719

1820
Open a [feature request](https://github.com/berkozero/iphonebase/issues/new?template=feature_request.md). Describe the use case, not just the solution.
@@ -23,6 +25,7 @@ Open a [feature request](https://github.com/berkozero/iphonebase/issues/new?temp
2325
git clone https://github.com/berkozero/iphonebase.git
2426
cd iphonebase
2527
swift build
28+
swift test
2629
```
2730

2831
Requirements:
@@ -35,23 +38,24 @@ Requirements:
3538

3639
1. Fork the repo and create a branch from `main`
3740
2. Make your changes
38-
3. Test against a real iPhone Mirroring session
39-
4. Run `swift build` to verify it compiles
41+
3. Run `swift build` and `swift test`
42+
4. Test against a real iPhone Mirroring session
4043
5. Open a pull request with a clear description of what and why
4144

45+
### When Adding or Changing Commands
46+
47+
- Update `skills/iphonebase/SKILL.md` with the new command docs
48+
- Add an entry to `CHANGELOG.md` under `[Unreleased]`
49+
- Ensure `--json` output works via the `ActionResult<T>` envelope
50+
4251
### Code Style
4352

44-
- Follow existing conventions in the codebase
53+
- Follow existing conventions (see [AGENTS.md](AGENTS.md) for details)
4554
- Keep functions focused and small
46-
- Use `--json` output for any new commands
4755
- Write to stderr for debug/verbose output, stdout for results
4856

4957
### Commit Messages
5058

5159
- Use imperative mood ("Add feature" not "Added feature")
5260
- First line under 72 characters
5361
- Explain *why*, not just *what*, in the body if needed
54-
55-
## Code of Conduct
56-
57-
This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md). Be kind and constructive.

Makefile

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
.PHONY: build release install test clean
1+
.PHONY: build release install test test-device test-all clean
2+
3+
# Swift Testing framework path (ships with Command Line Tools, not in default search path)
4+
TESTING_FW = /Library/Developer/CommandLineTools/Library/Developer/Frameworks
25

36
build:
47
swift build
@@ -10,7 +13,16 @@ install: release
1013
cp .build/release/iphonebase /usr/local/bin/
1114

1215
test:
13-
swift test
16+
swift test \
17+
-Xswiftc -F -Xswiftc $(TESTING_FW) \
18+
-Xlinker -F -Xlinker $(TESTING_FW) \
19+
-Xlinker -rpath -Xlinker $(TESTING_FW) \
20+
--enable-swift-testing --disable-xctest
21+
22+
test-device: build
23+
@bash tests/smoke-test.sh
24+
25+
test-all: test test-device
1426

1527
clean:
1628
swift package clean

Package.resolved

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.9
1+
// swift-tools-version: 6.0
22

33
import PackageDescription
44

@@ -9,13 +9,14 @@ let package = Package(
99
],
1010
dependencies: [
1111
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
12+
.package(url: "https://github.com/apple/swift-testing.git", from: "0.12.0"),
1213
],
1314
targets: [
1415
.target(
1516
name: "IPhoneBaseCore",
1617
dependencies: [],
18+
swiftSettings: [.swiftLanguageMode(.v5)],
1719
linkerSettings: [
18-
.linkedFramework("ScreenCaptureKit"),
1920
.linkedFramework("Vision"),
2021
.linkedFramework("CoreGraphics"),
2122
.linkedFramework("AppKit"),
@@ -26,11 +27,15 @@ let package = Package(
2627
dependencies: [
2728
"IPhoneBaseCore",
2829
.product(name: "ArgumentParser", package: "swift-argument-parser"),
29-
]
30+
],
31+
swiftSettings: [.swiftLanguageMode(.v5)]
3032
),
3133
.testTarget(
3234
name: "IPhoneBaseCoreTests",
33-
dependencies: ["IPhoneBaseCore"]
35+
dependencies: [
36+
"IPhoneBaseCore",
37+
.product(name: "Testing", package: "swift-testing"),
38+
]
3439
),
3540
]
3641
)

0 commit comments

Comments
 (0)