mobilerun-core 0.3.0__tar.gz

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,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.11", "3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v3
21
+
22
+ - name: Set up Python ${{ matrix.python-version }}
23
+ run: uv python install ${{ matrix.python-version }}
24
+
25
+ - name: Create venv and install
26
+ run: |
27
+ uv venv --python ${{ matrix.python-version }}
28
+ uv pip install -e . pytest
29
+
30
+ - name: Run tests
31
+ run: .venv/bin/python -m pytest tests/test_abstraction.py -v
32
+
33
+ - name: Lint with ruff
34
+ run: |
35
+ uv pip install ruff
36
+ .venv/bin/ruff check mobilerun_core tests
@@ -0,0 +1,90 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+ workflow_dispatch:
8
+ inputs:
9
+ target:
10
+ description: "Where to publish"
11
+ required: true
12
+ default: "pypi"
13
+ type: choice
14
+ options: [pypi, testpypi]
15
+
16
+ jobs:
17
+ build:
18
+ name: Build distribution
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v3
25
+
26
+ - name: Set up Python
27
+ run: uv python install 3.12
28
+
29
+ - name: Verify tag matches pyproject version
30
+ if: startsWith(github.ref, 'refs/tags/v')
31
+ run: |
32
+ TAG="${GITHUB_REF_NAME#v}"
33
+ VER=$(uv run --with tomli python -c "import tomli; print(tomli.load(open('pyproject.toml','rb'))['project']['version'])")
34
+ if [ "$TAG" != "$VER" ]; then
35
+ echo "::error::Tag v$TAG does not match pyproject version $VER"
36
+ exit 1
37
+ fi
38
+ echo "tag and pyproject agree on $VER"
39
+
40
+ - name: Build sdist + wheel
41
+ run: uv build
42
+
43
+ - name: Show built artifacts
44
+ run: ls -la dist/
45
+
46
+ - uses: actions/upload-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+ if-no-files-found: error
51
+
52
+ publish-pypi:
53
+ name: Publish to PyPI
54
+ needs: build
55
+ runs-on: ubuntu-latest
56
+ if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
57
+ environment:
58
+ name: pypi
59
+ url: https://pypi.org/p/mobilerun-core
60
+ permissions:
61
+ id-token: write # required for trusted publishing
62
+ steps:
63
+ - uses: actions/download-artifact@v4
64
+ with:
65
+ name: dist
66
+ path: dist/
67
+
68
+ - name: Publish via Trusted Publishing
69
+ uses: pypa/gh-action-pypi-publish@release/v1
70
+
71
+ publish-testpypi:
72
+ name: Publish to TestPyPI
73
+ needs: build
74
+ runs-on: ubuntu-latest
75
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
76
+ environment:
77
+ name: testpypi
78
+ url: https://test.pypi.org/p/mobilerun-core
79
+ permissions:
80
+ id-token: write
81
+ steps:
82
+ - uses: actions/download-artifact@v4
83
+ with:
84
+ name: dist
85
+ path: dist/
86
+
87
+ - name: Publish to TestPyPI
88
+ uses: pypa/gh-action-pypi-publish@release/v1
89
+ with:
90
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,26 @@
1
+ # Secrets — never commit
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.pyc
9
+ *.pyo
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
15
+
16
+ # Virtualenvs / build artifacts
17
+ .venv/
18
+ venv/
19
+ build/
20
+ dist/
21
+ *.egg-info/
22
+
23
+ # Editor / OS noise
24
+ .DS_Store
25
+ .idea/
26
+ .vscode/
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.4
2
+ Name: mobilerun-core
3
+ Version: 0.3.0
4
+ Summary: Unified Python facade for mobilerun cloud and local device connections, providing high-level automation actions, human-in-the-loop approval hooks for destructive operations, and structured capability-aware error handling.
5
+ Project-URL: Homepage, https://github.com/droidrun/mobilerun-core
6
+ Project-URL: Repository, https://github.com/droidrun/mobilerun-core
7
+ Project-URL: Issues, https://github.com/droidrun/mobilerun-core/issues
8
+ Author: droidrun
9
+ License: Apache-2.0
10
+ Keywords: adb,android,automation,device,ios,mobile,ui-automation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Requires-Python: <3.14,>=3.11
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: mobilerun-sdk>=3.1.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ <picture align="center">
27
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun-dark.png">
28
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png">
29
+ <img src="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png" width="full">
30
+ </picture>
31
+
32
+ <p align="center">
33
+ <strong>mobilerun-core is the programmatic Python API behind Mobilerun.</strong><br>
34
+ One sync facade — `Mobilerun()` — for driving Android (and iOS, on cloud) devices, whether they live in the Mobilerun cloud or on a USB cable. Pick a device id, get a `Device`, call <code>tap_text</code>, <code>scroll_until</code>, <code>wait_for_app</code>. No async/await ceremony, no SDK juggling.
35
+ </p>
36
+
37
+ <div align="center">
38
+
39
+ <a href="https://docs.mobilerun.ai">📕 Documentation</a>
40
+ ·
41
+ <a href="https://cloud.mobilerun.ai">☁️ Mobilerun Cloud</a>
42
+ ·
43
+ <a href="https://github.com/droidrun/mobilerun">🤖 Mobilerun Framework</a>
44
+
45
+ [![GitHub stars](https://img.shields.io/github/stars/droidrun/mobilerun-core?style=social)](https://github.com/droidrun/mobilerun-core/stargazers)
46
+ [![PyPI](https://img.shields.io/pypi/v/mobilerun-core?color=white)](https://pypi.org/project/mobilerun-core/)
47
+ [![Python](https://img.shields.io/pypi/pyversions/mobilerun-core?color=white)](https://pypi.org/project/mobilerun-core/)
48
+ [![mobilerun.ai](https://img.shields.io/badge/mobilerun.ai-white)](https://mobilerun.ai)
49
+ [![Twitter Follow](https://img.shields.io/twitter/follow/mobilerun_ai?style=social)](https://x.com/mobilerun_ai)
50
+ [![Discord](https://img.shields.io/discord/1360219330318696488?color=white&label=Discord&logo=discord&logoColor=white)](https://discord.gg/ZZbKEZZkwK)
51
+
52
+ </div>
53
+
54
+ - 🧰 **One API for two transports** — same helpers run against a cloud device (over `mobilerun-sdk`) or a local Android phone (over `mobilerun-core-cli`).
55
+ - 🪄 **Auto-detection** — pass any device id, the library figures out cloud vs local. Or override explicitly.
56
+ - 🎯 **High-level helpers** — `tap_text`, `tap_node`, `scroll_until`, `wait_for_app`, `open_and_settle`, `find_nodes`, `assert_on`, `screen_size`, … built on top of the raw verbs.
57
+ - 🛡️ **HITL gate** — destructive verbs (`uninstall`, local `install_apk`) route through a callable you control. Default denies; you opt in per turn.
58
+ - 🧭 **Agent-friendly errors** — when a backend doesn't support a verb, you get a structured `UnsupportedOperation` with `verb`, `backend`, and an `alternative` field instead of an opaque traceback.
59
+ - 🪶 **Pure library** — sync, heredoc-friendly, no daemon, no server, no agent loop.
60
+
61
+ Use the library when you want to script a device directly from Python — locally or in the cloud. Use [Mobilerun Framework](https://github.com/droidrun/mobilerun) when you want a full LLM agent driving the device. Use [Mobilerun Cloud](https://cloud.mobilerun.ai) when you want hosted devices and managed infrastructure.
62
+
63
+ ## 📦 Installation
64
+
65
+ > **Note:** Python 3.14 is not currently supported. Please use Python `>=3.11,<3.14`.
66
+
67
+ ```bash
68
+ # cloud-only is enough to start
69
+ uv add mobilerun-core
70
+ ```
71
+
72
+ ```bash
73
+ # add local-Android (ADB) support
74
+ uv add mobilerun-core mobilerun-core-cli
75
+ ```
76
+
77
+ Cloud credentials are picked up lazily — `Mobilerun()` does not touch the environment until you make a cloud call. For local-only use, no env vars are required.
78
+
79
+ ```bash
80
+ # only needed for cloud
81
+ export MOBILERUN_CLOUD_API_KEY=...
82
+ export MOBILERUN_API_BASE_URL=https://api.mobilerun.ai/v1
83
+ ```
84
+
85
+ ## 🚀 Quickstart
86
+
87
+ ```python
88
+ from mobilerun_core import Mobilerun
89
+
90
+ m = Mobilerun()
91
+
92
+ # auto-detect cloud vs local from the device id
93
+ d = m.connect("550e8400-e29b-41d4-a716-446655440000") # cloud (UUID)
94
+ d = m.connect("R5CT123456") # local (ADB serial)
95
+ d = m.connect(some_id, cloud=True) # explicit override
96
+
97
+ # drive the device
98
+ d.open_and_settle("com.instagram.android")
99
+ d.tap_text("Search")
100
+ d.type("droidrun")
101
+ d.key("enter")
102
+ png_b64 = d.screenshot()
103
+ ```
104
+
105
+ Cloud-side discovery is supported too:
106
+
107
+ ```python
108
+ m = Mobilerun()
109
+ d = m.ensure_device(filters={"name": ["pixel - test"]}) # one matching ready device
110
+ all_devices = m.list_devices(filters={"state": ["ready"]})
111
+ ```
112
+
113
+ ## 🧱 Concepts
114
+
115
+ ```
116
+ Mobilerun() single user-facing facade
117
+
118
+ │ .connect(id) → Device helpers (tap_text, scroll_until, …)
119
+ │ │
120
+ │ ▼
121
+ │ Connection (Protocol)
122
+ │ │
123
+ │ ┌───────┴────────┐
124
+ │ ▼ ▼
125
+ │ MobilerunCloud MobilerunFramework
126
+ │ (mobilerun-sdk) (mobilerun-core-cli)
127
+ │ id = UUID id = ADB serial
128
+ ```
129
+
130
+ - **`Mobilerun`** — the only class users construct. Lazy cloud creds; framework-only use needs no env vars.
131
+ - **`Device`** — what you get back from `.connect()` / `.ensure_device()`. All the helpers live here.
132
+ - **`Connection`** — sync per-device contract (`tap`, `swipe`, `type`, `ui`, `screenshot`, `app_*`). One implementation per transport.
133
+
134
+ ## 🪄 Backend selection
135
+
136
+ Explicit override always wins. Otherwise:
137
+
138
+ | `device_id` shape | `adb devices` shows it? | Result |
139
+ | ---------------------------------- | ------------------------ | ------------- |
140
+ | UUID | no | cloud |
141
+ | UUID | yes, cloud creds set | error (pin it)|
142
+ | UUID | yes, no cloud creds | framework |
143
+ | ADB serial / emulator / `IP:port` | yes | framework |
144
+ | anything else | no | error |
145
+
146
+ `adb devices` lookups filter `state=="device"` — `offline` / `unauthorized` rows don't count.
147
+
148
+ ## 🛡️ HITL gate
149
+
150
+ Destructive verbs route through a `HitlGate` callable. Default is `deny_all` — pass your own gate to allow specific actions per turn.
151
+
152
+ Gated today:
153
+ - `device.uninstall(package)`
154
+ - `device.install_apk(path, …)` (framework / local APK installs only)
155
+
156
+ Not gated: `tap`, `swipe`, `type`, `key("power")`, `app_stop`. Those are either non-destructive or trivially reversible.
157
+
158
+ ```python
159
+ from mobilerun_core import Mobilerun, HitlDenied
160
+
161
+ def my_gate(action: str, args: dict) -> None:
162
+ if not user_approved(action, args):
163
+ raise HitlDenied(action)
164
+
165
+ m = Mobilerun(hitl_gate=my_gate)
166
+ ```
167
+
168
+ ## 🧭 Agent-friendly unsupported verbs
169
+
170
+ The `Connection` Protocol is one surface, but each backend supports only a subset by design. When a verb isn't supported on the active backend, `UnsupportedOperation` is raised — subclasses `NotImplementedError` for back-compat, but carries structured fields an agent can branch on:
171
+
172
+ ```python
173
+ from mobilerun_core import UnsupportedOperation
174
+
175
+ try:
176
+ device.install_apk("/tmp/app.apk") # not supported on a cloud device
177
+ except UnsupportedOperation as e:
178
+ payload = e.as_dict()
179
+ # {
180
+ # "error": "unsupported_operation",
181
+ # "verb": "app_install_apk",
182
+ # "backend": "cloud",
183
+ # "reason": "...",
184
+ # "alternative": null,
185
+ # "hint": null,
186
+ # }
187
+ ```
188
+
189
+ Introspect without calling:
190
+
191
+ ```python
192
+ UnsupportedOperation.is_supported(MobilerunCloud, "app_install_apk") # False
193
+ UnsupportedOperation.describe(MobilerunCloud, "app_install_apk")
194
+ ```
195
+
196
+ ## ⚙️ Features
197
+
198
+ - **Two interchangeable transports** — `MobilerunCloud` wraps `mobilerun-sdk`; `MobilerunFramework` wraps `mobilerun-core-cli` for USB / wireless Android.
199
+ - **Sync API** — heredoc-friendly. No `await`, no event loop.
200
+ - **Helpers, not just verbs** — `tap_text`, `tap_and_wait`, `scroll_until`, `wait_for_app`, `wait_for_idle`, `open_and_settle`, `find_nodes`, `assert_on`, `screen_size`, …
201
+ - **Normalized return shapes** — `ui()` always returns a plain `dict`; `screenshot()` always returns base64 PNG.
202
+ - **Lazy cloud credentials** — `Mobilerun()` doesn't touch the env until you make a cloud call.
203
+ - **Back-compat** — `Device(device_id, sdk_client, hitl_gate)` constructor still works for callers pinned to 0.2.x.
204
+
205
+ ## ☁️ Framework vs Cloud vs Core
206
+
207
+ | | mobilerun-core (this lib) | [Mobilerun Framework](https://github.com/droidrun/mobilerun) | [Mobilerun Cloud](https://cloud.mobilerun.ai) |
208
+ | --- | --- | --- | --- |
209
+ | What | Programmatic device-control API | Full LLM agent + CLI | Hosted devices + REST + dashboard |
210
+ | Best for | Code-level scripting, custom tools, custom agents | Natural-language tasks, reasoning, vision | Managed phones, fleet workflows, APIs |
211
+ | Where it runs | Wherever your Python runs | Wherever your Python runs | Managed by Mobilerun |
212
+ | LLM included? | No (you bring it) | Yes (OpenAI / Anthropic / etc) | N/A |
213
+
214
+ Most users start with the Framework. Reach for `mobilerun-core` when you want to build something the Framework doesn't ship — a custom agent loop, a test runner, a recording / replay tool, or batch automation.
215
+
216
+ ## 💡 Example use cases
217
+
218
+ - Mobile app QA and regression testing.
219
+ - End-to-end flows in CI that target a real device or an emulator.
220
+ - Hybrid dev/CI workflows: same script targets your phone over USB locally, and a cloud device in CI.
221
+ - Building higher-level agent frameworks on top of a stable device API.
222
+ - Recording / replaying user flows for benchmarking.
223
+
224
+ ## 🤝 Contributing
225
+
226
+ Issues and PRs welcome. The library aims to stay small and sharply-scoped — please open an issue before adding new surface.
227
+
228
+ ```bash
229
+ git clone https://github.com/droidrun/mobilerun-core.git
230
+ cd mobilerun-core
231
+ uv venv
232
+ uv pip install -e . pytest ruff
233
+ .venv/bin/python -m pytest tests/test_abstraction.py -v
234
+ .venv/bin/ruff check mobilerun_core tests
235
+ ```
236
+
237
+ ## 📄 License
238
+
239
+ Apache-2.0. See [LICENSE](./LICENSE).
@@ -0,0 +1,214 @@
1
+ <picture align="center">
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun-dark.png">
3
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png">
4
+ <img src="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png" width="full">
5
+ </picture>
6
+
7
+ <p align="center">
8
+ <strong>mobilerun-core is the programmatic Python API behind Mobilerun.</strong><br>
9
+ One sync facade — `Mobilerun()` — for driving Android (and iOS, on cloud) devices, whether they live in the Mobilerun cloud or on a USB cable. Pick a device id, get a `Device`, call <code>tap_text</code>, <code>scroll_until</code>, <code>wait_for_app</code>. No async/await ceremony, no SDK juggling.
10
+ </p>
11
+
12
+ <div align="center">
13
+
14
+ <a href="https://docs.mobilerun.ai">📕 Documentation</a>
15
+ ·
16
+ <a href="https://cloud.mobilerun.ai">☁️ Mobilerun Cloud</a>
17
+ ·
18
+ <a href="https://github.com/droidrun/mobilerun">🤖 Mobilerun Framework</a>
19
+
20
+ [![GitHub stars](https://img.shields.io/github/stars/droidrun/mobilerun-core?style=social)](https://github.com/droidrun/mobilerun-core/stargazers)
21
+ [![PyPI](https://img.shields.io/pypi/v/mobilerun-core?color=white)](https://pypi.org/project/mobilerun-core/)
22
+ [![Python](https://img.shields.io/pypi/pyversions/mobilerun-core?color=white)](https://pypi.org/project/mobilerun-core/)
23
+ [![mobilerun.ai](https://img.shields.io/badge/mobilerun.ai-white)](https://mobilerun.ai)
24
+ [![Twitter Follow](https://img.shields.io/twitter/follow/mobilerun_ai?style=social)](https://x.com/mobilerun_ai)
25
+ [![Discord](https://img.shields.io/discord/1360219330318696488?color=white&label=Discord&logo=discord&logoColor=white)](https://discord.gg/ZZbKEZZkwK)
26
+
27
+ </div>
28
+
29
+ - 🧰 **One API for two transports** — same helpers run against a cloud device (over `mobilerun-sdk`) or a local Android phone (over `mobilerun-core-cli`).
30
+ - 🪄 **Auto-detection** — pass any device id, the library figures out cloud vs local. Or override explicitly.
31
+ - 🎯 **High-level helpers** — `tap_text`, `tap_node`, `scroll_until`, `wait_for_app`, `open_and_settle`, `find_nodes`, `assert_on`, `screen_size`, … built on top of the raw verbs.
32
+ - 🛡️ **HITL gate** — destructive verbs (`uninstall`, local `install_apk`) route through a callable you control. Default denies; you opt in per turn.
33
+ - 🧭 **Agent-friendly errors** — when a backend doesn't support a verb, you get a structured `UnsupportedOperation` with `verb`, `backend`, and an `alternative` field instead of an opaque traceback.
34
+ - 🪶 **Pure library** — sync, heredoc-friendly, no daemon, no server, no agent loop.
35
+
36
+ Use the library when you want to script a device directly from Python — locally or in the cloud. Use [Mobilerun Framework](https://github.com/droidrun/mobilerun) when you want a full LLM agent driving the device. Use [Mobilerun Cloud](https://cloud.mobilerun.ai) when you want hosted devices and managed infrastructure.
37
+
38
+ ## 📦 Installation
39
+
40
+ > **Note:** Python 3.14 is not currently supported. Please use Python `>=3.11,<3.14`.
41
+
42
+ ```bash
43
+ # cloud-only is enough to start
44
+ uv add mobilerun-core
45
+ ```
46
+
47
+ ```bash
48
+ # add local-Android (ADB) support
49
+ uv add mobilerun-core mobilerun-core-cli
50
+ ```
51
+
52
+ Cloud credentials are picked up lazily — `Mobilerun()` does not touch the environment until you make a cloud call. For local-only use, no env vars are required.
53
+
54
+ ```bash
55
+ # only needed for cloud
56
+ export MOBILERUN_CLOUD_API_KEY=...
57
+ export MOBILERUN_API_BASE_URL=https://api.mobilerun.ai/v1
58
+ ```
59
+
60
+ ## 🚀 Quickstart
61
+
62
+ ```python
63
+ from mobilerun_core import Mobilerun
64
+
65
+ m = Mobilerun()
66
+
67
+ # auto-detect cloud vs local from the device id
68
+ d = m.connect("550e8400-e29b-41d4-a716-446655440000") # cloud (UUID)
69
+ d = m.connect("R5CT123456") # local (ADB serial)
70
+ d = m.connect(some_id, cloud=True) # explicit override
71
+
72
+ # drive the device
73
+ d.open_and_settle("com.instagram.android")
74
+ d.tap_text("Search")
75
+ d.type("droidrun")
76
+ d.key("enter")
77
+ png_b64 = d.screenshot()
78
+ ```
79
+
80
+ Cloud-side discovery is supported too:
81
+
82
+ ```python
83
+ m = Mobilerun()
84
+ d = m.ensure_device(filters={"name": ["pixel - test"]}) # one matching ready device
85
+ all_devices = m.list_devices(filters={"state": ["ready"]})
86
+ ```
87
+
88
+ ## 🧱 Concepts
89
+
90
+ ```
91
+ Mobilerun() single user-facing facade
92
+
93
+ │ .connect(id) → Device helpers (tap_text, scroll_until, …)
94
+ │ │
95
+ │ ▼
96
+ │ Connection (Protocol)
97
+ │ │
98
+ │ ┌───────┴────────┐
99
+ │ ▼ ▼
100
+ │ MobilerunCloud MobilerunFramework
101
+ │ (mobilerun-sdk) (mobilerun-core-cli)
102
+ │ id = UUID id = ADB serial
103
+ ```
104
+
105
+ - **`Mobilerun`** — the only class users construct. Lazy cloud creds; framework-only use needs no env vars.
106
+ - **`Device`** — what you get back from `.connect()` / `.ensure_device()`. All the helpers live here.
107
+ - **`Connection`** — sync per-device contract (`tap`, `swipe`, `type`, `ui`, `screenshot`, `app_*`). One implementation per transport.
108
+
109
+ ## 🪄 Backend selection
110
+
111
+ Explicit override always wins. Otherwise:
112
+
113
+ | `device_id` shape | `adb devices` shows it? | Result |
114
+ | ---------------------------------- | ------------------------ | ------------- |
115
+ | UUID | no | cloud |
116
+ | UUID | yes, cloud creds set | error (pin it)|
117
+ | UUID | yes, no cloud creds | framework |
118
+ | ADB serial / emulator / `IP:port` | yes | framework |
119
+ | anything else | no | error |
120
+
121
+ `adb devices` lookups filter `state=="device"` — `offline` / `unauthorized` rows don't count.
122
+
123
+ ## 🛡️ HITL gate
124
+
125
+ Destructive verbs route through a `HitlGate` callable. Default is `deny_all` — pass your own gate to allow specific actions per turn.
126
+
127
+ Gated today:
128
+ - `device.uninstall(package)`
129
+ - `device.install_apk(path, …)` (framework / local APK installs only)
130
+
131
+ Not gated: `tap`, `swipe`, `type`, `key("power")`, `app_stop`. Those are either non-destructive or trivially reversible.
132
+
133
+ ```python
134
+ from mobilerun_core import Mobilerun, HitlDenied
135
+
136
+ def my_gate(action: str, args: dict) -> None:
137
+ if not user_approved(action, args):
138
+ raise HitlDenied(action)
139
+
140
+ m = Mobilerun(hitl_gate=my_gate)
141
+ ```
142
+
143
+ ## 🧭 Agent-friendly unsupported verbs
144
+
145
+ The `Connection` Protocol is one surface, but each backend supports only a subset by design. When a verb isn't supported on the active backend, `UnsupportedOperation` is raised — subclasses `NotImplementedError` for back-compat, but carries structured fields an agent can branch on:
146
+
147
+ ```python
148
+ from mobilerun_core import UnsupportedOperation
149
+
150
+ try:
151
+ device.install_apk("/tmp/app.apk") # not supported on a cloud device
152
+ except UnsupportedOperation as e:
153
+ payload = e.as_dict()
154
+ # {
155
+ # "error": "unsupported_operation",
156
+ # "verb": "app_install_apk",
157
+ # "backend": "cloud",
158
+ # "reason": "...",
159
+ # "alternative": null,
160
+ # "hint": null,
161
+ # }
162
+ ```
163
+
164
+ Introspect without calling:
165
+
166
+ ```python
167
+ UnsupportedOperation.is_supported(MobilerunCloud, "app_install_apk") # False
168
+ UnsupportedOperation.describe(MobilerunCloud, "app_install_apk")
169
+ ```
170
+
171
+ ## ⚙️ Features
172
+
173
+ - **Two interchangeable transports** — `MobilerunCloud` wraps `mobilerun-sdk`; `MobilerunFramework` wraps `mobilerun-core-cli` for USB / wireless Android.
174
+ - **Sync API** — heredoc-friendly. No `await`, no event loop.
175
+ - **Helpers, not just verbs** — `tap_text`, `tap_and_wait`, `scroll_until`, `wait_for_app`, `wait_for_idle`, `open_and_settle`, `find_nodes`, `assert_on`, `screen_size`, …
176
+ - **Normalized return shapes** — `ui()` always returns a plain `dict`; `screenshot()` always returns base64 PNG.
177
+ - **Lazy cloud credentials** — `Mobilerun()` doesn't touch the env until you make a cloud call.
178
+ - **Back-compat** — `Device(device_id, sdk_client, hitl_gate)` constructor still works for callers pinned to 0.2.x.
179
+
180
+ ## ☁️ Framework vs Cloud vs Core
181
+
182
+ | | mobilerun-core (this lib) | [Mobilerun Framework](https://github.com/droidrun/mobilerun) | [Mobilerun Cloud](https://cloud.mobilerun.ai) |
183
+ | --- | --- | --- | --- |
184
+ | What | Programmatic device-control API | Full LLM agent + CLI | Hosted devices + REST + dashboard |
185
+ | Best for | Code-level scripting, custom tools, custom agents | Natural-language tasks, reasoning, vision | Managed phones, fleet workflows, APIs |
186
+ | Where it runs | Wherever your Python runs | Wherever your Python runs | Managed by Mobilerun |
187
+ | LLM included? | No (you bring it) | Yes (OpenAI / Anthropic / etc) | N/A |
188
+
189
+ Most users start with the Framework. Reach for `mobilerun-core` when you want to build something the Framework doesn't ship — a custom agent loop, a test runner, a recording / replay tool, or batch automation.
190
+
191
+ ## 💡 Example use cases
192
+
193
+ - Mobile app QA and regression testing.
194
+ - End-to-end flows in CI that target a real device or an emulator.
195
+ - Hybrid dev/CI workflows: same script targets your phone over USB locally, and a cloud device in CI.
196
+ - Building higher-level agent frameworks on top of a stable device API.
197
+ - Recording / replaying user flows for benchmarking.
198
+
199
+ ## 🤝 Contributing
200
+
201
+ Issues and PRs welcome. The library aims to stay small and sharply-scoped — please open an issue before adding new surface.
202
+
203
+ ```bash
204
+ git clone https://github.com/droidrun/mobilerun-core.git
205
+ cd mobilerun-core
206
+ uv venv
207
+ uv pip install -e . pytest ruff
208
+ .venv/bin/python -m pytest tests/test_abstraction.py -v
209
+ .venv/bin/ruff check mobilerun_core tests
210
+ ```
211
+
212
+ ## 📄 License
213
+
214
+ Apache-2.0. See [LICENSE](./LICENSE).
@@ -0,0 +1,14 @@
1
+ from mobilerun_core.connection import UnsupportedOperation
2
+ from mobilerun_core.device import Device, DeviceHandle
3
+ from mobilerun_core.hitl import HitlDenied, HitlGate
4
+ from mobilerun_core.mobilerun import Mobilerun
5
+
6
+ __all__ = [
7
+ "Mobilerun",
8
+ "Device",
9
+ "DeviceHandle",
10
+ "HitlGate",
11
+ "HitlDenied",
12
+ "UnsupportedOperation",
13
+ ]
14
+ __version__ = "0.3.0"