Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
b9b8b09
fix(cli): report command failures
lutz-grex Nov 11, 2026
d1ac750
chore(deps): upgrade device packages
lutz-grex Nov 11, 2026
7e5164e
feat(auth): group provider sign-ins
lutz-grex Nov 11, 2026
7a296ad
feat(cli): unify command surface
lutz-grex Nov 11, 2026
d5e7db4
fix(env): remove legacy names
lutz-grex Nov 11, 2026
e039259
test(cli): cover command contract
lutz-grex Nov 11, 2026
a6ad977
docs(cli): update command examples
lutz-grex Nov 11, 2026
1c6c6eb
style(cli): trim trailing whitespace
lutz-grex Nov 11, 2026
93973ab
fix(deps): validate locked installs
lutz-grex Nov 11, 2026
a994e14
fix(cli): apply reviewed contract
lutz-grex Nov 11, 2026
285357f
test(cli): cover review regressions
lutz-grex Nov 11, 2026
f892c38
docs(cli): correct sign-in hints
lutz-grex Nov 11, 2026
bfaad9d
feat(cli): add qr pairing
lutz-grex Nov 11, 2026
01267cf
fix(config): use platform tokens
lutz-grex Nov 11, 2026
392efad
feat(cli): format ui elements
lutz-grex Nov 11, 2026
eb6ca68
fix(cli): simplify api errors
lutz-grex Nov 11, 2026
cb9e02e
test(cli): verify polish decisions
lutz-grex Nov 11, 2026
1f0422d
chore(cli): merge cli polish
lutz-grex Nov 11, 2026
d563c54
fix(cli): harden qr pairing
lutz-grex Nov 11, 2026
192da17
fix(config): isolate platform tokens
lutz-grex Nov 11, 2026
9398c6c
fix(cli): preserve api messages
lutz-grex Nov 11, 2026
e31d678
fix(cli): align ui output
lutz-grex Nov 11, 2026
bf264a0
test(cli): cover polish regressions
lutz-grex Nov 11, 2026
c784f80
fix(cli): prefer adb autoconnect
lutz-grex Nov 11, 2026
8a20676
chore(cli): merge polish fixes
lutz-grex Nov 11, 2026
41fbe8c
docs(readme): document cli workflow
lutz-grex Nov 11, 2026
b942b61
feat(cli): improve help discovery
lutz-grex Nov 11, 2026
f5366b8
fix(cli): preserve useful output
lutz-grex Nov 11, 2026
68fb2ea
feat(cli): support unattended input
lutz-grex Nov 11, 2026
3478721
feat(cli): add shell completion
lutz-grex Nov 11, 2026
c41a9c7
fix(cli): resolve devx findings
lutz-grex Nov 11, 2026
bc2946c
fix(cli): suggest full command paths
lutz-grex Nov 11, 2026
746f560
fix(cli): explain cloud credential source
lutz-grex Nov 11, 2026
134a79e
perf(cli): defer startup imports
lutz-grex Nov 11, 2026
2e66a24
fix(cli): address runtime findings
lutz-grex Nov 11, 2026
f7a8e25
fix(cli): make browser opt-in
lutz-grex Nov 11, 2026
144eb3f
refactor(cli): use core device api
lutz-grex Nov 12, 2026
063ee14
feat(cli): read MOBILERUN_API_KEY first
lutz-grex Nov 12, 2026
2fdb405
fix(cli): allow adb startup
lutz-grex Nov 12, 2026
1a0cb4a
fix(cli): remove import shims
lutz-grex Nov 12, 2026
a7a6dde
fix(cli): restore subpackage attributes
lutz-grex Nov 12, 2026
280f365
fix(cli): contain import interrupts
lutz-grex Nov 12, 2026
96cb25b
fix(cli): distinguish forbidden credentials
lutz-grex Nov 12, 2026
439e2ed
fix(cli): handle missing credentials
lutz-grex Nov 12, 2026
c96a7ed
fix(cli): unify credential sources
lutz-grex Nov 12, 2026
ca36c08
fix(cli): preserve qr contrast
lutz-grex Nov 12, 2026
60ee06c
fix(cli): simplify command suggestions
lutz-grex Nov 12, 2026
d43e1e9
fix(cli): parse removed flags
lutz-grex Nov 12, 2026
3eb9f26
fix(cli): centralize config paths
lutz-grex Nov 12, 2026
cdcbeb1
fix(cli): declare command examples
lutz-grex Nov 12, 2026
59b17f2
fix(cli): simplify shared policies
lutz-grex Nov 12, 2026
065c0d3
fix(cli): name runtime tests
lutz-grex Nov 12, 2026
d992f7f
fix(cli): align source patches
lutz-grex Nov 12, 2026
ece80b9
fix(cli): align adb guidance
lutz-grex Nov 12, 2026
f568696
chore(cli): pin core release
lutz-grex Nov 12, 2026
16c4700
chore(cli): sync develop stack
lutz-grex Nov 12, 2026
7e5c33d
chore(cli): sync polish stack
lutz-grex Nov 12, 2026
5b435d1
chore(cli): sync qr stack
lutz-grex Nov 12, 2026
bf5cca2
style(cli): apply black
lutz-grex Nov 12, 2026
08cc116
chore(cli): sync polish formatting
lutz-grex Nov 12, 2026
0a6ebcf
style(cli): apply black
lutz-grex Nov 12, 2026
255a395
chore(cli): sync qr formatting
lutz-grex Nov 12, 2026
d6f8125
style(cli): apply black
lutz-grex Nov 12, 2026
b563054
Merge origin/develop into feat/cli-polish
lutz-grex Nov 13, 2026
53f5494
Merge feat/cli-polish into feat/cli-qr
lutz-grex Nov 13, 2026
43e7262
Merge feat/cli-qr into feat/cli-devx
lutz-grex Nov 13, 2026
532660b
fix(cli): review fixes typesafe port
lutz-grex Nov 13, 2026
96af104
Merge feat/cli-polish into feat/cli-qr
lutz-grex Nov 13, 2026
87f3855
Merge feat/cli-qr into feat/cli-devx
lutz-grex Nov 13, 2026
55a0f39
fix(cli): bound retries and diagnostics
lutz-grex Nov 14, 2026
b31355d
fix(cli): review fixes qa round
lutz-grex Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ RUN uv sync --locked --no-dev --no-editable --extra anthropic --no-cache

ENTRYPOINT [".venv/bin/mobilerun"]

CMD ["setup"]
CMD ["device", "setup"]
320 changes: 291 additions & 29 deletions README.md

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions compat/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
[project]
name = "droidrun"
version = "0.7.4"
version = "0.8.0"
description = "Compatibility shim — droidrun has been renamed to mobilerun"
requires-python = ">=3.11,<3.14"
dependencies = ["mobilerun==0.7.4"]
dependencies = ["mobilerun==0.8.0"]

[project.optional-dependencies]
anthropic = ["mobilerun[anthropic]==0.7.4"]
deepseek = ["mobilerun[deepseek]==0.7.4"]
langfuse = ["mobilerun[langfuse]==0.7.4"]
typesafe = ["mobilerun[typesafe]==0.7.4"]
dev = ["mobilerun[dev]==0.7.4"]
anthropic = ["mobilerun[anthropic]==0.8.0"]
deepseek = ["mobilerun[deepseek]==0.8.0"]
langfuse = ["mobilerun[langfuse]==0.8.0"]
typesafe = ["mobilerun[typesafe]==0.8.0"]
dev = ["mobilerun[dev]==0.8.0"]

[project.scripts]
droidrun = "droidrun.cli_shim:main"
Expand Down
112 changes: 75 additions & 37 deletions mobilerun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,24 @@
"""

import logging
from importlib import import_module
from importlib.metadata import version
from typing import TYPE_CHECKING

from mobilerun.agent.utils.errors import HtmlErrorPageFilter
from mobilerun.log_handlers import CLILogHandler

__version__ = version("mobilerun")

# Attach a default CLILogHandler so that every consumer (CLI, SDK,
# tools-only) gets visible output without explicit setup. CLI flows can
# replace this with their own handler via ``configure_logging()``.
from mobilerun.log_handlers import CLILogHandler

_logger = logging.getLogger("mobilerun")
_logger.addHandler(CLILogHandler())
_logger.setLevel(logging.INFO)
_logger.propagate = False

# Import main classes for easier access
from mobilerun.agent import ResultEvent # noqa: E402
from mobilerun.agent.droid import MobileAgent, MobileAgentState # noqa: E402
from mobilerun.agent.utils.decision_loader import load_decision_model # noqa: E402
from mobilerun.agent.utils.errors import HtmlErrorPageFilter # noqa: E402
from mobilerun.agent.utils.llm_picker import load_llm # noqa: E402

# llama-index's retry warnings include provider error bodies verbatim.
for _retry_logger in (
Expand All @@ -31,35 +29,64 @@
):
logging.getLogger(_retry_logger).addFilter(HtmlErrorPageFilter())

# Import configuration classes
from mobilerun.config_manager import ( # noqa: E402
# Agent configs
AgentConfig,
AppCardConfig,
CredentialsConfig,
DecisionConfig,
DecisionProfile,
DelegationConfig,
# Feature configs
DeviceConfig,
ExecutorConfig,
FastAgentConfig,
LLMProfile,
LoggingConfig,
ManagerConfig,
MobileConfig,
TelemetryConfig,
ToolsConfig,
TracingConfig,
)

# Import macro functionality
from mobilerun.macro import ( # noqa: E402
MacroPlayer,
replay_macro_file,
replay_macro_folder,
)
from mobilerun.tools import AndroidDriver, DeviceDriver, RecordingDriver # noqa: E402
if TYPE_CHECKING:
from mobilerun.agent import ResultEvent
from mobilerun.agent.droid import MobileAgent, MobileAgentState
from mobilerun.agent.utils.decision_loader import load_decision_model
from mobilerun.agent.utils.llm_picker import load_llm
from mobilerun.config_manager import (
AgentConfig,
AppCardConfig,
CredentialsConfig,
DecisionConfig,
DecisionProfile,
DelegationConfig,
DeviceConfig,
ExecutorConfig,
FastAgentConfig,
LLMProfile,
LoggingConfig,
ManagerConfig,
MobileConfig,
TelemetryConfig,
ToolsConfig,
TracingConfig,
)
from mobilerun.macro import MacroPlayer, replay_macro_file, replay_macro_folder
from mobilerun.tools import AndroidDriver, DeviceDriver, RecordingDriver

_EXPORTS = {
"ResultEvent": ("mobilerun.agent", "ResultEvent"),
"MobileAgent": ("mobilerun.agent.droid", "MobileAgent"),
"MobileAgentState": ("mobilerun.agent.droid", "MobileAgentState"),
"load_decision_model": (
"mobilerun.agent.utils.decision_loader",
"load_decision_model",
),
"load_llm": ("mobilerun.agent.utils.llm_picker", "load_llm"),
"AgentConfig": ("mobilerun.config_manager", "AgentConfig"),
"AppCardConfig": ("mobilerun.config_manager", "AppCardConfig"),
"CredentialsConfig": ("mobilerun.config_manager", "CredentialsConfig"),
"DecisionConfig": ("mobilerun.config_manager", "DecisionConfig"),
"DecisionProfile": ("mobilerun.config_manager", "DecisionProfile"),
"DelegationConfig": ("mobilerun.config_manager", "DelegationConfig"),
"DeviceConfig": ("mobilerun.config_manager", "DeviceConfig"),
"ExecutorConfig": ("mobilerun.config_manager", "ExecutorConfig"),
"FastAgentConfig": ("mobilerun.config_manager", "FastAgentConfig"),
"LLMProfile": ("mobilerun.config_manager", "LLMProfile"),
"LoggingConfig": ("mobilerun.config_manager", "LoggingConfig"),
"ManagerConfig": ("mobilerun.config_manager", "ManagerConfig"),
"MobileConfig": ("mobilerun.config_manager", "MobileConfig"),
"TelemetryConfig": ("mobilerun.config_manager", "TelemetryConfig"),
"ToolsConfig": ("mobilerun.config_manager", "ToolsConfig"),
"TracingConfig": ("mobilerun.config_manager", "TracingConfig"),
"MacroPlayer": ("mobilerun.macro", "MacroPlayer"),
"replay_macro_file": ("mobilerun.macro", "replay_macro_file"),
"replay_macro_folder": ("mobilerun.macro", "replay_macro_folder"),
"AndroidDriver": ("mobilerun.tools", "AndroidDriver"),
"DeviceDriver": ("mobilerun.tools", "DeviceDriver"),
"RecordingDriver": ("mobilerun.tools", "RecordingDriver"),
}

# Make main components available at package level
__all__ = [
Expand Down Expand Up @@ -115,5 +142,16 @@ def __getattr__(name):
DeprecationWarning,
stacklevel=2,
)
return globals()[new_name]
return getattr(import_module(__name__), new_name)
if name in _EXPORTS:
module, attribute = _EXPORTS[name]
value = getattr(import_module(module), attribute)
globals()[name] = value
return value
if name in {"agent", "tools", "macro", "config_manager"}:
return import_module(f"{__name__}.{name}")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
return sorted(set(globals()) | set(__all__))
25 changes: 24 additions & 1 deletion mobilerun/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
from mobilerun.agent.droid.events import ResultEvent
from importlib import import_module
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from mobilerun.agent.droid.events import ResultEvent

_EXPORTS = {
"ResultEvent": ("mobilerun.agent.droid.events", "ResultEvent"),
}

__all__ = ["ResultEvent"]


def __getattr__(name):
if name in _EXPORTS:
module, attribute = _EXPORTS[name]
value = getattr(import_module(module), attribute)
globals()[name] = value
return value
if name in {"droid", "utils"}:
return import_module(f"{__name__}.{name}")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
return sorted(set(globals()) | set(__all__))
33 changes: 22 additions & 11 deletions mobilerun/agent/droid/__init__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
"""
Mobilerun Agent Module.
"""Mobilerun Agent Module.

This module provides a ReAct agent for automating Android devices using reasoning and acting.
"""

from mobilerun.agent.droid.droid_agent import MobileAgent
from mobilerun.agent.droid.state import MobileAgentState
from importlib import import_module
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from mobilerun.agent.droid.droid_agent import MobileAgent
from mobilerun.agent.droid.state import MobileAgentState

# Legacy aliases for backward compatibility
_LEGACY_ALIASES = {
"DroidAgent": MobileAgent,
"DroidAgentState": MobileAgentState,
_EXPORTS = {
"MobileAgent": ("mobilerun.agent.droid.droid_agent", "MobileAgent"),
"MobileAgentState": ("mobilerun.agent.droid.state", "MobileAgentState"),
}

__all__ = ["MobileAgent", "MobileAgentState", "DroidAgent", "DroidAgentState"]


def __getattr__(name):
if name in _LEGACY_ALIASES:
aliases = {"DroidAgent": "MobileAgent", "DroidAgentState": "MobileAgentState"}
if name in aliases:
import warnings

warnings.warn(
f"{name} has been renamed. Update your imports.",
DeprecationWarning,
stacklevel=2,
)
return _LEGACY_ALIASES[name]
return getattr(import_module(__name__), aliases[name])
if name in _EXPORTS:
module, attribute = _EXPORTS[name]
value = getattr(import_module(module), attribute)
globals()[name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


__all__ = ["MobileAgent", "MobileAgentState", "DroidAgent", "DroidAgentState"]
def __dir__():
return sorted(set(globals()) | set(__all__))
4 changes: 1 addition & 3 deletions mobilerun/agent/droid/droid_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,7 @@ def __init__(
)

self._stream_screenshots = (
os.environ.get("MOBILERUN_STREAM_SCREENSHOTS")
or os.environ.get("DROIDRUN_STREAM_SCREENSHOTS")
or ""
os.environ.get("MOBILERUN_STREAM_SCREENSHOTS") or ""
).lower() in ("1", "true")

self.timeout = timeout
Expand Down
4 changes: 1 addition & 3 deletions mobilerun/agent/fast_agent/fast_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,7 @@ def __init__(
self.state_provider = state_provider
self.save_trajectory = save_trajectory
self._stream_screenshots = (
os.environ.get("MOBILERUN_STREAM_SCREENSHOTS")
or os.environ.get("DROIDRUN_STREAM_SCREENSHOTS")
or ""
os.environ.get("MOBILERUN_STREAM_SCREENSHOTS") or ""
).lower() in ("1", "true")
self.shared_state = shared_state
self.output_model = output_model
Expand Down
4 changes: 1 addition & 3 deletions mobilerun/agent/manager/manager_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ def __init__(
self.state_provider = state_provider
self.save_trajectory = save_trajectory
self._stream_screenshots = (
os.environ.get("MOBILERUN_STREAM_SCREENSHOTS")
or os.environ.get("DROIDRUN_STREAM_SCREENSHOTS")
or ""
os.environ.get("MOBILERUN_STREAM_SCREENSHOTS") or ""
).lower() in ("1", "true")
self.shared_state = shared_state
self.registry = registry
Expand Down
54 changes: 41 additions & 13 deletions mobilerun/agent/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
"""
Utility modules for Mobilerun agents.
"""

from .chat_utils import (
filter_empty_messages,
has_content,
limit_history,
to_chat_messages,
)
from .prompt_resolver import PromptResolver
from .signatures import build_tool_registry
from .trajectory import Trajectory
"""Utility modules for Mobilerun agents."""

from importlib import import_module
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from mobilerun.agent.utils.chat_utils import (
filter_empty_messages,
has_content,
limit_history,
to_chat_messages,
)
from mobilerun.agent.utils.prompt_resolver import PromptResolver
from mobilerun.agent.utils.signatures import build_tool_registry
from mobilerun.agent.utils.trajectory import Trajectory

_EXPORTS = {
"filter_empty_messages": (
"mobilerun.agent.utils.chat_utils",
"filter_empty_messages",
),
"has_content": ("mobilerun.agent.utils.chat_utils", "has_content"),
"limit_history": ("mobilerun.agent.utils.chat_utils", "limit_history"),
"to_chat_messages": ("mobilerun.agent.utils.chat_utils", "to_chat_messages"),
"PromptResolver": ("mobilerun.agent.utils.prompt_resolver", "PromptResolver"),
"build_tool_registry": ("mobilerun.agent.utils.signatures", "build_tool_registry"),
"Trajectory": ("mobilerun.agent.utils.trajectory", "Trajectory"),
}

__all__ = [
# Chat utilities
Expand All @@ -25,3 +40,16 @@
# Trajectory
"Trajectory",
]


def __getattr__(name):
if name in _EXPORTS:
module, attribute = _EXPORTS[name]
value = getattr(import_module(module), attribute)
globals()[name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
return sorted(set(globals()) | set(__all__))
2 changes: 1 addition & 1 deletion mobilerun/agent/utils/android_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def create_android_portal_http_driver(
if not token:
raise ValueError(
"Android Portal HTTP needs the Portal auth token. Copy it from the "
"Portal app and set MOBILERUN_DEVICE_TOKEN or device.auth_token."
"Portal app and set MOBILERUN_ANDROID_PORTAL_TOKEN or device.auth_token."
)
return AndroidPortalHttpDriver(url, token)

Expand Down
2 changes: 1 addition & 1 deletion mobilerun/agent/utils/llm_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def _validate_gemini_oauth_model(model: object) -> None:
raise ValueError(
f"Model '{model_id}' is a Gemini Developer API id and is not valid "
f"for the Antigravity OAuth catalog. Re-run "
f"`mobilerun configure gemini` and pick one of: {supported}."
f"`mobilerun auth login gemini` and pick one of: {supported}."
)


Expand Down
4 changes: 1 addition & 3 deletions mobilerun/agent/utils/oauth/anthropic_oauth_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,9 +530,7 @@ def login(

# Headless environments: skip local server, use hosted callback page
use_headless = _is_headless_environment() or (
os.environ.get("MOBILERUN_OAUTH_MANUAL")
or os.environ.get("DROIDRUN_OAUTH_MANUAL")
or ""
os.environ.get("MOBILERUN_OAUTH_MANUAL") or ""
).lower() in ("1", "true", "yes")
if use_headless:
return self.login_headless(
Expand Down
6 changes: 2 additions & 4 deletions mobilerun/agent/utils/oauth/gemini_oauth_code_assist_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def __init__(
raise ValueError(
f"Gemini OAuth model '{selected_model}' is retired. "
f"Use '{GEMINI_OAUTH_DEFAULT_MODEL}' or re-run "
"`mobilerun configure gemini` to choose an available model."
"`mobilerun auth login gemini` to choose an available model."
)

super().__init__(
Expand Down Expand Up @@ -604,9 +604,7 @@ def login(

# Headless environments: use authcode redirect flow (no local server)
use_authcode = _is_headless_environment() or (
os.environ.get("MOBILERUN_OAUTH_MANUAL")
or os.environ.get("DROIDRUN_OAUTH_MANUAL")
or ""
os.environ.get("MOBILERUN_OAUTH_MANUAL") or ""
).lower() in ("1", "true", "yes")
if use_authcode:
return self.login_headless(
Expand Down
Loading
Loading