Metadata-Version: 2.4
Name: mobilerun-core
Version: 1.0.1
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.
Project-URL: Homepage, https://github.com/droidrun/mobilerun-core
Project-URL: Repository, https://github.com/droidrun/mobilerun-core
Project-URL: Issues, https://github.com/droidrun/mobilerun-core/issues
Author: droidrun
License: Apache-2.0
Keywords: adb,android,automation,device,ios,mobile,ui-automation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Testing
Requires-Python: <3.14,>=3.11
Requires-Dist: mobilerun-sdk>=3.1.0
Provides-Extra: local
Requires-Dist: mobilerun-core-cli>=0.2.0; extra == 'local'
Description-Content-Type: text/markdown

<picture align="center">
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun-dark.png">
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png">
  <img src="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png" width="full">
</picture>

<p align="center">
  <strong>mobilerun-core is the programmatic Python API behind Mobilerun.</strong><br>
  One sync facade — `Mobilerun()` — for driving Android and iOS devices, whether they live in the Mobilerun cloud, on USB, or behind a local Portal HTTP URL. Pick a backend, get a `Device`, call <code>tap_text</code>, <code>scroll_until</code>, <code>wait_for_app</code>. No async/await ceremony, no SDK juggling.
</p>

<div align="center">

<a href="https://docs.mobilerun.ai">📕 Documentation</a>
·
<a href="https://cloud.mobilerun.ai">☁️ Mobilerun Cloud</a>
·
<a href="https://github.com/droidrun/mobilerun">🤖 Mobilerun Framework</a>

[![GitHub stars](https://img.shields.io/github/stars/droidrun/mobilerun-core?style=social)](https://github.com/droidrun/mobilerun-core/stargazers)
[![PyPI](https://img.shields.io/pypi/v/mobilerun-core?color=white)](https://pypi.org/project/mobilerun-core/)
[![Python](https://img.shields.io/pypi/pyversions/mobilerun-core?color=white)](https://pypi.org/project/mobilerun-core/)
[![mobilerun.ai](https://img.shields.io/badge/mobilerun.ai-white)](https://mobilerun.ai)
[![Twitter Follow](https://img.shields.io/twitter/follow/mobilerun_ai?style=social)](https://x.com/mobilerun_ai)
[![Discord](https://img.shields.io/discord/1360219330318696488?color=white&label=Discord&logo=discord&logoColor=white)](https://discord.gg/ZZbKEZZkwK)

</div>

- 🧰 **One API for cloud and local** — same helpers run against cloud, local Android ADB+Portal, local Android Portal HTTP-only, or local iOS Portal HTTP.
- 🪄 **Explicit backends with auto-detection** — pass a UUID/ADB serial, or pin `backend="cloud"`, `backend="local-android-adb"`, `backend="local-android-http"`, or `backend="local-ios-http"`.
- 🎯 **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.
- 🛡️ **HITL gate** — destructive verbs (`uninstall_app`, local `install_app`) route through a callable you control. Default denies; you opt in per turn.
- 🧭 **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.
- 🪶 **Pure library** — sync, heredoc-friendly, no daemon, no server, no agent loop.

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.

## 📦 Installation

> **Note:** Python 3.14 is not currently supported. Please use Python `>=3.11,<3.14`.

```bash
# cloud-only is enough to start
uv add mobilerun-core
```

```bash
# add local Android/iOS driver support via the `[local]` extra
uv add 'mobilerun-core[local]'
# equivalent to: uv add mobilerun-core mobilerun-core-cli
```

Local requirements:
- `adb` on `PATH` and the device showing as `device` (not `unauthorized` / `offline`) in `adb devices`.
- The screen **unlocked** before driving the device. The library can't bypass the keyguard, and a locked screen makes the a11y tree return only the lockscreen overlay regardless of what activity you launched.
- Android Portal HTTP-only requires `url=` and `token=` or `MOBILERUN_ANDROID_PORTAL_URL` plus `MOBILERUN_ANDROID_PORTAL_TOKEN`.
- iOS Portal HTTP requires `url=` or `MOBILERUN_IOS_PORTAL_URL`; the portal must already be running.

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.

```bash
# only needed for cloud
export MOBILERUN_CLOUD_API_KEY=...
export MOBILERUN_API_BASE_URL=https://api.mobilerun.ai/v1
```

## 🚀 Quickstart

```python
from mobilerun_core import Mobilerun

m = Mobilerun()

# auto-detect legacy cloud/local from the device id
d = m.connect("550e8400-e29b-41d4-a716-446655440000")   # cloud (UUID)
d = m.connect("R5CT123456")                              # local Android ADB serial
d = m.connect(some_id, cloud=True)                       # explicit compatibility override

# explicit local backends
d = m.connect("R5CT123456", backend="local-android-adb")
d = m.connect(
    backend="local-android-http",
    url="http://127.0.0.1:18080",
    token="...",
)
d = m.connect(backend="local-ios-http", url="http://127.0.0.1:6643")

# drive the device
d.start_app("com.instagram.android")
d.tap_text("Search")
d.type("droidrun")
d.key("enter")
png_b64 = d.screenshot()

if d.supports("stop_app"):
    d.stop_app("com.instagram.android")
```

Cloud-side discovery is supported too:

```python
m = Mobilerun()
d = m.ensure_device(filters={"name": ["pixel - test"]})  # one matching ready device
all_devices = m.list_devices(filters={"state": ["ready"]})
local_devices = m.list_devices(scope="local")
```

## 🧱 Concepts

```
Mobilerun()                             single user-facing facade
   │
   │  .connect(id)  →  Device           helpers (tap_text, scroll_until, …)
   │                     │
   │                     ▼
   │              Connection (Protocol)
   │                     │
   │             ┌──────────┴──────────┐
   │             ▼                     ▼
   │      MobilerunCloud       Local driver connections
   │      (mobilerun-sdk)      (mobilerun-core-cli)
   │      id = UUID            ADB serial or Portal URL
```

- **`Mobilerun`** — the only class users construct. Lazy cloud creds; local-only use needs no cloud env vars.
- **`Device`** — what you get back from `.connect()` / `.ensure_device()`. All the helpers live here.
- **`Connection`** — sync per-device contract (`tap`, `swipe`, `type`, `ui`, `screenshot`, `app_*`). One implementation per transport.

## 🪄 Backend selection

Explicit override always wins. Otherwise:

| Selector                           | Result        |
| ---------------------------------- | ------------- |
| UUID                               | cloud         |
| ADB serial / emulator / `IP:port`  | local-android-adb |
| `backend="local-android-http"`     | local-android-http |
| `backend="local-ios-http"`         | local-ios-http |
| `url=...` without `platform=`      | error; pass `platform="android"` or `platform="ios"` |

`adb devices` lookups filter `state=="device"` — `offline` / `unauthorized` rows don't count.

## 🛡️ HITL gate

Destructive verbs route through a `HitlGate` callable. Default is `deny_all` — pass your own gate to allow specific actions per turn.

Gated today:
- `device.uninstall_app(app_id)`
- `device.install_app(path, …)`

Not gated: `tap`, `swipe`, `type`, `key("power")`, `stop_app`. Those are either non-destructive or trivially reversible.

```python
from mobilerun_core import Mobilerun, HitlDenied

def my_gate(action: str, args: dict) -> None:
    if not user_approved(action, args):
        raise HitlDenied(action)

m = Mobilerun(hitl_gate=my_gate)
```

## 🧭 Agent-friendly unsupported verbs

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:

```python
from mobilerun_core import UnsupportedOperation

try:
    device.install_app("/tmp/app.apk")   # not supported on a cloud device
except UnsupportedOperation as e:
    payload = e.as_dict()
    # {
    #   "error": "unsupported_operation",
    #   "verb": "install_app",
    #   "backend": "cloud",
    #   "reason": "...",
    #   "alternative": null,
    #   "hint": null,
    # }
```

Introspect connection-level methods without calling:

```python
UnsupportedOperation.is_supported(MobilerunCloud, "app_install")  # False
UnsupportedOperation.describe(MobilerunCloud, "app_install")
```

## ⚙️ Features

- **Backend-neutral control** — `MobilerunCloud` wraps `mobilerun-sdk`; local connections wrap `mobilerun-core-cli` drivers.
- **Sync API** — heredoc-friendly. No `await`, no event loop.
- **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`, …
- **Capabilities** — `device.capabilities` and `device.supports(action)` tell agents which verbs work on the active backend.
- **Normalized return shapes** — `ui()` always returns a plain `dict`; `screenshot()` always returns base64 PNG.
- **Lazy cloud credentials** — `Mobilerun()` doesn't touch the env until you make a cloud call.
- **Single ctor** — `Device(connection, hitl_gate)`. The 0.2.x three-argument form (`Device(device_id, sdk_client, hitl_gate)`) was removed in 0.5.0.

## ☁️ Framework vs Cloud vs Core

| | mobilerun-core (this lib) | [Mobilerun Framework](https://github.com/droidrun/mobilerun) | [Mobilerun Cloud](https://cloud.mobilerun.ai) |
| --- | --- | --- | --- |
| What | Programmatic device-control API | Full LLM agent + CLI | Hosted devices + REST + dashboard |
| Best for | Code-level scripting, custom tools, custom agents | Natural-language tasks, reasoning, vision | Managed phones, fleet workflows, APIs |
| Where it runs | Wherever your Python runs | Wherever your Python runs | Managed by Mobilerun |
| LLM included? | No (you bring it) | Yes (OpenAI / Anthropic / etc) | N/A |

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.

## 💡 Example use cases

- Mobile app QA and regression testing.
- End-to-end flows in CI that target a real device or an emulator.
- Hybrid dev/CI workflows: same script targets your phone over USB locally, and a cloud device in CI.
- Building higher-level agent frameworks on top of a stable device API.
- Recording / replaying user flows for benchmarking.

## 🤝 Contributing

Issues and PRs welcome. The library aims to stay small and sharply-scoped — please open an issue before adding new surface.

```bash
git clone https://github.com/droidrun/mobilerun-core.git
cd mobilerun-core
uv venv
uv pip install -e . pytest ruff
.venv/bin/python -m pytest tests/test_abstraction.py -v
.venv/bin/ruff check mobilerun_core tests
```

## 📄 License

Apache-2.0. See [LICENSE](./LICENSE).
