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.
@@ -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,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .python-version
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .DS_Store
@@ -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,3 @@
1
+ """Native desktop viewer for Mobilerun cloud devices."""
2
+
3
+ __version__ = "0.1.0"
@@ -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