mobilerun-viewer 0.1.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.
- mobilerun_viewer-0.1.0/.github/workflows/publish.yml +73 -0
- mobilerun_viewer-0.1.0/.gitignore +10 -0
- mobilerun_viewer-0.1.0/PKG-INFO +73 -0
- mobilerun_viewer-0.1.0/README.md +46 -0
- mobilerun_viewer-0.1.0/mobilerun_viewer/__init__.py +3 -0
- mobilerun_viewer-0.1.0/mobilerun_viewer/cli.py +144 -0
- mobilerun_viewer-0.1.0/mobilerun_viewer/creds.py +45 -0
- mobilerun_viewer-0.1.0/mobilerun_viewer/server.py +434 -0
- mobilerun_viewer-0.1.0/mobilerun_viewer/static/index.html +781 -0
- mobilerun_viewer-0.1.0/pyproject.toml +42 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Publish 📦 to PyPI
|
|
2
|
+
on: push
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
version-check:
|
|
6
|
+
name: Check version matches tag
|
|
7
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- name: Compare tag to pyproject version
|
|
12
|
+
run: |
|
|
13
|
+
TAG="${GITHUB_REF#refs/tags/v}"
|
|
14
|
+
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
|
|
15
|
+
if [ "$TAG" != "$VERSION" ]; then
|
|
16
|
+
echo "::error::Tag v$TAG does not match pyproject.toml version $VERSION"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
echo "Version check passed: v$VERSION"
|
|
20
|
+
|
|
21
|
+
build:
|
|
22
|
+
name: Build distribution 📦
|
|
23
|
+
needs:
|
|
24
|
+
- version-check
|
|
25
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
persist-credentials: false
|
|
31
|
+
- name: Set up Python
|
|
32
|
+
uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.x"
|
|
35
|
+
- name: Install pypa/build
|
|
36
|
+
run: python3 -m pip install build --user
|
|
37
|
+
- name: Build
|
|
38
|
+
run: python3 -m build
|
|
39
|
+
- name: Verify the bundled viewer HTML is in the wheel
|
|
40
|
+
run: |
|
|
41
|
+
python3 - <<'PY'
|
|
42
|
+
import glob, zipfile, sys
|
|
43
|
+
whl = glob.glob("dist/mobilerun_viewer-*.whl")[0]
|
|
44
|
+
names = zipfile.ZipFile(whl).namelist()
|
|
45
|
+
assert any(n.endswith("static/index.html") for n in names), \
|
|
46
|
+
f"static/index.html missing from {whl}: {names}"
|
|
47
|
+
print(f"OK: {whl} contains the viewer HTML")
|
|
48
|
+
PY
|
|
49
|
+
- name: Store the distribution packages
|
|
50
|
+
uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: python-package-distributions
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
publish-to-pypi:
|
|
56
|
+
name: Publish to PyPI
|
|
57
|
+
needs:
|
|
58
|
+
- build
|
|
59
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
environment:
|
|
62
|
+
name: pypi
|
|
63
|
+
url: https://pypi.org/p/mobilerun-viewer
|
|
64
|
+
permissions:
|
|
65
|
+
id-token: write # OIDC for PyPI Trusted Publishing — no token needed.
|
|
66
|
+
steps:
|
|
67
|
+
- name: Download all the dists
|
|
68
|
+
uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: python-package-distributions
|
|
71
|
+
path: dist/
|
|
72
|
+
- name: Publish to PyPI
|
|
73
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mobilerun-viewer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Native desktop viewer for Mobilerun cloud devices — live WebRTC stream, system nav buttons, and an action event panel.
|
|
5
|
+
Project-URL: Homepage, https://github.com/droidrun/mobilerun-viewer
|
|
6
|
+
Project-URL: Issues, https://github.com/droidrun/mobilerun-viewer/issues
|
|
7
|
+
Author: Mobilerun
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: android,automation,mobilerun,viewer,webrtc
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: MacOS X :: Cocoa
|
|
12
|
+
Classifier: Environment :: X11 Applications
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: <3.14,>=3.11
|
|
21
|
+
Requires-Dist: click>=8.0
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: platformdirs>=4.0
|
|
24
|
+
Requires-Dist: pywebview>=5.0
|
|
25
|
+
Requires-Dist: websockets>=14
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# mobilerun-viewer
|
|
29
|
+
|
|
30
|
+
Native desktop viewer for [Mobilerun](https://mobilerun.ai) cloud devices.
|
|
31
|
+
Renders the live WebRTC stream of a cloud-hosted phone in a desktop window
|
|
32
|
+
with system nav buttons (BACK / HOME / RECENTS) and a side panel that
|
|
33
|
+
shows every tool call (tap, swipe, press, …) in real time.
|
|
34
|
+
|
|
35
|
+
Reuses the same credential as the `mobilerun` CLI — read from
|
|
36
|
+
`MOBILERUN_API_KEY` or the file `mobilerun login` writes — so authenticating
|
|
37
|
+
in either command works for both.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv tool install mobilerun-viewer
|
|
43
|
+
# or:
|
|
44
|
+
pip install mobilerun-viewer
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Use
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Discover your devices via the main CLI:
|
|
51
|
+
mobilerun devices --cloud
|
|
52
|
+
|
|
53
|
+
# Open the viewer:
|
|
54
|
+
mobilerun-viewer -d <device-id>
|
|
55
|
+
|
|
56
|
+
# Against a non-default API base (dev / staging):
|
|
57
|
+
mobilerun-viewer -d <device-id> --base-url https://dev-api.mobilerun.ai/v1
|
|
58
|
+
|
|
59
|
+
# Track an agent's task trajectory in the events panel:
|
|
60
|
+
mobilerun-viewer -d <device-id> --task-id <task-id>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If the main `mobilerun` CLI is installed alongside this package, the same
|
|
64
|
+
viewer is also reachable as `mobilerun stream …`.
|
|
65
|
+
|
|
66
|
+
## Flags
|
|
67
|
+
|
|
68
|
+
- `-d, --device-id` (required) — cloud device id.
|
|
69
|
+
- `--base-url` — Mobilerun API base; default `https://api.mobilerun.ai/v1`.
|
|
70
|
+
- `--task-id` — task whose trajectory feeds the events panel. Defaults to
|
|
71
|
+
the device's `activeTaskId` if one is running.
|
|
72
|
+
- `--no-window` — don't open a desktop window; just print the local
|
|
73
|
+
viewer URL (useful for debugging).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# mobilerun-viewer
|
|
2
|
+
|
|
3
|
+
Native desktop viewer for [Mobilerun](https://mobilerun.ai) cloud devices.
|
|
4
|
+
Renders the live WebRTC stream of a cloud-hosted phone in a desktop window
|
|
5
|
+
with system nav buttons (BACK / HOME / RECENTS) and a side panel that
|
|
6
|
+
shows every tool call (tap, swipe, press, …) in real time.
|
|
7
|
+
|
|
8
|
+
Reuses the same credential as the `mobilerun` CLI — read from
|
|
9
|
+
`MOBILERUN_API_KEY` or the file `mobilerun login` writes — so authenticating
|
|
10
|
+
in either command works for both.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv tool install mobilerun-viewer
|
|
16
|
+
# or:
|
|
17
|
+
pip install mobilerun-viewer
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Discover your devices via the main CLI:
|
|
24
|
+
mobilerun devices --cloud
|
|
25
|
+
|
|
26
|
+
# Open the viewer:
|
|
27
|
+
mobilerun-viewer -d <device-id>
|
|
28
|
+
|
|
29
|
+
# Against a non-default API base (dev / staging):
|
|
30
|
+
mobilerun-viewer -d <device-id> --base-url https://dev-api.mobilerun.ai/v1
|
|
31
|
+
|
|
32
|
+
# Track an agent's task trajectory in the events panel:
|
|
33
|
+
mobilerun-viewer -d <device-id> --task-id <task-id>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If the main `mobilerun` CLI is installed alongside this package, the same
|
|
37
|
+
viewer is also reachable as `mobilerun stream …`.
|
|
38
|
+
|
|
39
|
+
## Flags
|
|
40
|
+
|
|
41
|
+
- `-d, --device-id` (required) — cloud device id.
|
|
42
|
+
- `--base-url` — Mobilerun API base; default `https://api.mobilerun.ai/v1`.
|
|
43
|
+
- `--task-id` — task whose trajectory feeds the events panel. Defaults to
|
|
44
|
+
the device's `activeTaskId` if one is running.
|
|
45
|
+
- `--no-window` — don't open a desktop window; just print the local
|
|
46
|
+
viewer URL (useful for debugging).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""``mobilerun-viewer`` — open a desktop window with a live cloud device."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from mobilerun_viewer.creds import (
|
|
12
|
+
CLOUD_API_KEY_ENV,
|
|
13
|
+
DEFAULT_CLOUD_BASE_URL,
|
|
14
|
+
resolve_cloud_api_key,
|
|
15
|
+
)
|
|
16
|
+
from mobilerun_viewer.server import StreamServer
|
|
17
|
+
|
|
18
|
+
APP_NAME = "Mobilerun Viewer"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _active_task_id(api_base: str, api_key: str, device_id: str) -> Optional[str]:
|
|
22
|
+
"""Best-effort: read the device's currently-running task id, if any."""
|
|
23
|
+
try:
|
|
24
|
+
resp = httpx.get(
|
|
25
|
+
f"{api_base.rstrip('/')}/devices/{device_id}",
|
|
26
|
+
headers={
|
|
27
|
+
"Authorization": f"Bearer {api_key}",
|
|
28
|
+
"Accept": "application/json",
|
|
29
|
+
},
|
|
30
|
+
timeout=10.0,
|
|
31
|
+
)
|
|
32
|
+
if resp.status_code >= 400:
|
|
33
|
+
return None
|
|
34
|
+
data = resp.json()
|
|
35
|
+
return data.get("activeTaskId") or data.get("active_task_id")
|
|
36
|
+
except Exception:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _set_macos_app_name(name: str) -> None:
|
|
41
|
+
"""Make the Dock + menu bar show *name* instead of "python3.13".
|
|
42
|
+
|
|
43
|
+
pywebview's Cocoa backend reads ``CFBundleName`` from the main bundle's
|
|
44
|
+
info dict, so injecting it before ``webview.start()`` takes effect for
|
|
45
|
+
every menu label. Best-effort: silently no-op if PyObjC is missing or
|
|
46
|
+
we're not on macOS.
|
|
47
|
+
"""
|
|
48
|
+
if sys.platform != "darwin":
|
|
49
|
+
return
|
|
50
|
+
try:
|
|
51
|
+
from Foundation import NSBundle # type: ignore
|
|
52
|
+
|
|
53
|
+
bundle = NSBundle.mainBundle()
|
|
54
|
+
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
|
|
55
|
+
if info is not None:
|
|
56
|
+
info["CFBundleName"] = name
|
|
57
|
+
info["CFBundleDisplayName"] = name
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
try:
|
|
61
|
+
sys.argv[0] = name
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@click.command()
|
|
67
|
+
@click.option(
|
|
68
|
+
"--device-id", "-d", "device_id", required=True, help="Cloud device id to stream"
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--base-url",
|
|
72
|
+
"base_url",
|
|
73
|
+
default=None,
|
|
74
|
+
help=f"Cloud API base URL (default {DEFAULT_CLOUD_BASE_URL})",
|
|
75
|
+
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--task-id",
|
|
78
|
+
"task_id",
|
|
79
|
+
default=None,
|
|
80
|
+
help="Task id whose actions to show in the events panel (default: device's active task)",
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--no-window",
|
|
84
|
+
is_flag=True,
|
|
85
|
+
default=False,
|
|
86
|
+
help="Don't open a window; print the local viewer URL",
|
|
87
|
+
)
|
|
88
|
+
def stream(
|
|
89
|
+
device_id: str,
|
|
90
|
+
base_url: Optional[str],
|
|
91
|
+
task_id: Optional[str],
|
|
92
|
+
no_window: bool,
|
|
93
|
+
):
|
|
94
|
+
"""Open a live view of a Mobilerun cloud device."""
|
|
95
|
+
api_key = resolve_cloud_api_key()
|
|
96
|
+
if not api_key:
|
|
97
|
+
raise click.ClickException(
|
|
98
|
+
f"No cloud API key found. Set {CLOUD_API_KEY_ENV} or run `mobilerun login`."
|
|
99
|
+
)
|
|
100
|
+
api_base = base_url or DEFAULT_CLOUD_BASE_URL
|
|
101
|
+
|
|
102
|
+
if not task_id:
|
|
103
|
+
task_id = _active_task_id(api_base, api_key, device_id)
|
|
104
|
+
|
|
105
|
+
server = StreamServer(
|
|
106
|
+
api_base_url=api_base,
|
|
107
|
+
api_key=api_key,
|
|
108
|
+
device_id=device_id,
|
|
109
|
+
task_id=task_id,
|
|
110
|
+
)
|
|
111
|
+
server.start()
|
|
112
|
+
try:
|
|
113
|
+
if no_window:
|
|
114
|
+
click.echo(server.url)
|
|
115
|
+
click.echo("Press Ctrl+C to stop.")
|
|
116
|
+
try:
|
|
117
|
+
import time
|
|
118
|
+
|
|
119
|
+
while True:
|
|
120
|
+
time.sleep(1)
|
|
121
|
+
except KeyboardInterrupt:
|
|
122
|
+
pass
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
import webview # type: ignore
|
|
127
|
+
except ImportError:
|
|
128
|
+
raise click.ClickException(
|
|
129
|
+
"pywebview is missing. Reinstall mobilerun-viewer.\n"
|
|
130
|
+
"Or use --no-window."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Rename the Dock + menu bar before webview.start() spins up NSApp.
|
|
134
|
+
_set_macos_app_name(APP_NAME)
|
|
135
|
+
|
|
136
|
+
webview.create_window(
|
|
137
|
+
f"{APP_NAME} • {device_id[:8]}",
|
|
138
|
+
server.url,
|
|
139
|
+
width=800,
|
|
140
|
+
height=920,
|
|
141
|
+
)
|
|
142
|
+
webview.start() # blocks until the window is closed
|
|
143
|
+
finally:
|
|
144
|
+
server.stop()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Resolve the Mobilerun cloud API credential.
|
|
2
|
+
|
|
3
|
+
Standalone: no dependency on the ``mobilerun`` framework. Reads the same
|
|
4
|
+
file ``mobilerun login`` writes, so a single login authenticates both
|
|
5
|
+
tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import platformdirs
|
|
16
|
+
|
|
17
|
+
# Defaults shared with the mobilerun framework.
|
|
18
|
+
CLOUD_API_KEY_ENV = "MOBILERUN_API_KEY"
|
|
19
|
+
DEFAULT_CLOUD_BASE_URL = "https://api.mobilerun.ai/v1"
|
|
20
|
+
|
|
21
|
+
# Same APP_NAME the mobilerun framework uses — they share one credentials dir.
|
|
22
|
+
_APP_NAME = "droidrun"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _credential_file() -> Path:
|
|
26
|
+
return (
|
|
27
|
+
Path(platformdirs.user_config_dir(_APP_NAME))
|
|
28
|
+
/ "credentials"
|
|
29
|
+
/ "mobilerun-cloud.json"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_cloud_api_key() -> Optional[str]:
|
|
34
|
+
"""``MOBILERUN_API_KEY`` env var → saved login file → None."""
|
|
35
|
+
env_key = os.environ.get(CLOUD_API_KEY_ENV)
|
|
36
|
+
if env_key:
|
|
37
|
+
return env_key
|
|
38
|
+
try:
|
|
39
|
+
path = _credential_file()
|
|
40
|
+
if path.exists():
|
|
41
|
+
data = json.loads(path.read_text())
|
|
42
|
+
return data.get("api_key") or data.get("access_token") or None
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
return None
|