Files
browser-use--browser-use/browser_use/cli.py
T
wehub-resource-sync 4cd2d4af2b
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Waiting to run
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Waiting to run
Test Browser Use CLI Install / uv pip install (ubuntu-latest) (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use from local wheel (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use[cli] from PyPI (push) Failing after 1s
package / pip-install-on-macos-latest-py-3.11 (push) Has been skipped
package / pip-install-on-macos-latest-py-3.13 (push) Has been skipped
package / pip-install-on-ubuntu-latest-py-3.11 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.13 (push) Has been skipped
cloud_evals / trigger_cloud_eval_image_build (push) Failing after 1s
docker / build_publish_image (push) Failing after 1s
Test Browser Use CLI Install / browser-use skill sync (push) Failing after 1s
lint / code-style (push) Failing after 0s
lint / type-checker (push) Failing after 1s
package / pip-build (push) Failing after 1s
lint / syntax-errors (push) Failing after 3s
package / pip-install-on-ubuntu-latest-py-3.13 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.11 (push) Has been skipped
test / ${{ matrix.test_filename }} (push) Has been skipped
test / evaluate-tasks (push) Has been skipped
test / setup-chromium (push) Failing after 2s
test / find_tests (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:02:32 +08:00

465 lines
14 KiB
Python

"""Browser Use CLI backed by Browser Harness"""
from __future__ import annotations
import sys
import time
from contextlib import redirect_stderr, redirect_stdout
from importlib.metadata import PackageNotFoundError, version
from io import StringIO
def _browser_use_version() -> str:
try:
return version('browser-use')
except PackageNotFoundError:
return 'unknown'
def _exit_code(result: int | str | None) -> int:
if result is None:
return 0
if isinstance(result, int):
return result
return 1
def _set_harness_client_env() -> None:
import os
os.environ['BH_CLIENT'] = 'browser-use-cli'
os.environ['BH_CLIENT_VERSION'] = _browser_use_version()
def _capture_via_harness(
*,
command: str,
start_time: float,
result: int | str | None = None,
error_message: str | None = None,
) -> None:
try:
from browser_harness import telemetry as harness_telemetry
capture_cli_event = getattr(harness_telemetry, 'capture_cli_event', None)
if capture_cli_event is None:
return
_set_harness_client_env()
code = _exit_code(result)
capture_cli_event(
action='error' if code else 'completed',
command=command,
duration_seconds=time.monotonic() - start_time,
exit_code=code,
error_message=error_message,
)
except Exception:
pass
def _run_mcp_stdio_server(module_name: str) -> None:
"""Silence all logging"""
import asyncio
import importlib
import logging
import os
os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'critical'
os.environ['BROWSER_USE_SETUP_LOGGING'] = 'false'
logging.disable(logging.CRITICAL)
main = importlib.import_module(module_name).main
asyncio.run(main())
def _run_mcp_server() -> None:
_run_mcp_stdio_server('browser_use.mcp.server')
def _run_cli_mcp_server() -> None:
_run_mcp_stdio_server('browser_use.mcp.cli_mcp')
def _run_install_command(argv: list[str]) -> int:
if any(arg in {'-h', '--help'} for arg in argv):
print('usage: browser-use install')
print()
print('Install Chromium browser and system dependencies.')
return 0
import platform
import subprocess
print('Installing Chromium browser + system dependencies...')
print('This may take a few minutes...\n')
cmd = ['uvx', 'playwright', 'install', 'chromium']
if platform.system() == 'Linux':
cmd.append('--with-deps')
cmd.append('--no-shell')
result = subprocess.run(cmd)
if result.returncode == 0:
print('\nInstallation complete.')
print('Ready to use. Run: uvx browser-use')
return 0
print('\nInstallation failed', file=sys.stderr)
return result.returncode or 1
def _run_init_command(argv: list[str]) -> int | None:
from browser_use.init_cmd import main as init_main
original_argv = sys.argv
try:
sys.argv = [original_argv[0], *argv]
init_main()
except SystemExit as exc:
if exc.code is None:
return 0
if isinstance(exc.code, int):
return exc.code
print(exc.code, file=sys.stderr)
return 1
finally:
sys.argv = original_argv
return 0
def _as_browser_use_cli_text(text: str) -> str:
return text.replace('Browser Harness', 'Browser Use').replace('browser-harness', 'browser-use')
def _normalize_captured_cli_output(func, argv: list[str]) -> int:
stdout = StringIO()
stderr = StringIO()
try:
with redirect_stdout(stdout), redirect_stderr(stderr):
result = func(argv)
except SystemExit as exc:
result = exc.code
out = stdout.getvalue()
err = stderr.getvalue()
if out:
print(_as_browser_use_cli_text(out), end='')
if err:
print(_as_browser_use_cli_text(err), end='', file=sys.stderr)
if result is None:
return 0
if isinstance(result, int):
return result
if isinstance(result, str):
print(_as_browser_use_cli_text(result), file=sys.stderr)
return 1
return 1
def _patch_browser_harness_cli_text() -> None:
from browser_harness import auth, run, telemetry
run.HELP = _as_browser_use_cli_text(run.HELP)
run.USAGE = _as_browser_use_cli_text(run.USAGE)
original_auth_cli = auth.run_auth_cli
original_telemetry_cli = telemetry.run_telemetry_cli
def run_auth_cli(argv: list[str]) -> int:
if any(arg in {'-h', '--help'} for arg in argv):
return _normalize_captured_cli_output(original_auth_cli, argv)
return original_auth_cli(argv)
def run_telemetry_cli(argv: list[str]) -> int:
if argv and argv != ['status'] and argv != ['enable'] and argv != ['disable']:
return _normalize_captured_cli_output(original_telemetry_cli, argv)
return original_telemetry_cli(argv)
auth.run_auth_cli = run_auth_cli
telemetry.run_telemetry_cli = run_telemetry_cli
_delegated_to_harness = False
def _run_browser_harness() -> int | None:
from browser_harness import run
global _delegated_to_harness
_set_harness_client_env()
_patch_browser_harness_cli_text()
args = sys.argv[1:]
if args and args[0] == 'doctor' and args[1:]:
if args[1:] in (['--help'], ['-h']):
print('usage: browser-use doctor [--fix-snap]')
return 0
if args[1:] != ['--fix-snap']:
print('usage: browser-use doctor [--fix-snap]', file=sys.stderr)
sys.exit(2)
_delegated_to_harness = True
run.main()
return None
# Subcommands and flags from the pre-3.0 CLI; maps to hint
_LEGACY_HINTS: dict[str, str] = {
'open': 'new_tab("https://example.com")',
'state': 'print(page_info())',
'screenshot': 'print(capture_screenshot())',
'eval': 'print(js("document.title"))',
'cookies': 'print(cdp("Network.getCookies"))',
'python': '# the CLI runs Python directly now — pipe it on stdin as shown below',
'run': '# write the steps as Python using the pre-imported helpers shown below',
'connect': '# connecting is automatic — the default flow attaches to your running Chrome',
'close': '# restart the local daemon with `browser-use --reload`; stop cloud browsers with stop_remote_daemon(name)',
'sessions': '# named local sessions were removed — one default daemon; use BU_NAME=<name> for cloud daemons',
'profile': '# profiles now come from your real Chrome; see the profile-sync interaction skill',
'cloud': '# authenticate with `browser-use auth login`, then start_remote_daemon("<name>")',
'daemon': '# the daemon starts automatically on every call; restart it with `browser-use --reload`',
'record': '# session recording was removed; use capture_screenshot() per step',
'mcp': '# MCP server mode is now the --mcp flag: `browser-use --mcp`',
'--session': '# use BU_NAME=<name> as an env var for named cloud daemons; local runs use one default daemon',
'--headed': '# local control always attaches to your real, visible Chrome — no flag needed',
'--cdp-url': '# use the BU_CDP_URL=<url> env var instead of a flag',
'--profile': '# use your real Chrome profile; for cloud cookie sync see the profile-sync interaction skill',
'--json': '# output is whatever your Python prints — use print(json.dumps(...))',
'-c': "# pipe code on stdin instead: echo 'print(page_info())' | browser-use",
'--code': "# pipe code on stdin instead: echo 'print(page_info())' | browser-use",
}
def _legacy_command(args: list[str]) -> str | None:
if not args:
return None
first = args[0].split('=', 1)[0]
if first in _LEGACY_HINTS:
return first
return None
_CLI3_GUIDE = """Core helpers: new_tab(url), goto_url(url), page_info(), capture_screenshot(),
click_at_xy(x, y), type_text(text), fill_input(selector, text), press_key(key),
scroll(x, y), js(code), cdp(method, ...), wait_for_load(), wait_for_element(selector),
list_tabs(), switch_tab(target), close_tab(target)
Read the full interface now: browser-use skill show
Install the CLI 3.0 skill (upgrades the CLI and replaces any pre-existing browser-use skill):
browser-use skill install
Then load the freshly installed browser-use skill, and follow it to use the latest CLI.
Skill reference: https://github.com/browser-use/browser-use/blob/main/browser_use/skills/browser-use/SKILL.md
Health check: browser-use --doctor"""
def _legacy_migration_message(command: str) -> str:
hint = _LEGACY_HINTS[command]
if hint.startswith('#'):
replacement = f' {hint.lstrip("# ")}\n\nExample:\n browser-use <<\'PY\'\n new_tab("https://example.com")\n print(page_info())\n PY'
else:
replacement = f" browser-use <<'PY'\n {hint}\n PY"
return f"""The browser-use CLI changed in 3.0, and '{command}' was removed.
The old preset subcommands are gone. To use the CLI, you write raw Python and
pipe it on stdin, and it runs in a persistent browser session. Browser management
(daemon startup, Chrome/CDP attach, tabs, waiting) is handled for you.
Replacement for '{command}':
{replacement}
{_CLI3_GUIDE}"""
def _unknown_helper_message(name: str) -> str:
return f"""'{name}' is not defined in the browser-use CLI.
Example:
browser-use <<'PY'
new_tab("https://example.com")
print(page_info())
PY
{_CLI3_GUIDE}"""
def _unknown_exec_name(exc: NameError) -> str | None:
import re
name = getattr(exc, 'name', None)
if name:
return name
m = re.search(r"'([A-Za-z_][A-Za-z0-9_]*)'", str(exc))
return m.group(1) if m else None
def _raised_from_piped_code(exc: BaseException) -> bool:
tb = exc.__traceback__
last = None
while tb is not None:
last = tb
tb = tb.tb_next
return last is not None and last.tb_frame.f_code.co_filename == '<string>'
_QUICKSTART = """Welcome to the Browser Use CLI. Allow your coding agent to reliably control a web browser.
The CLI allows your agent to control the browser via Python, and it manages the browser in the background.
browser-use <<'PY'
new_tab("https://news.ycombinator.com")
print(page_info())
PY
Core helpers: new_tab(url), goto_url(url), page_info(),
capture_screenshot(), click_at_xy(x, y), js(code), cdp(method, ...),
wait_for_load()
Recommended: install the skill so your coding agent remembers this:
browser-use skill install
You can also paste this into your agent to get started:
Install or upgrade browser-use to the latest stable version with uv using
Python 3.12, register the skill from `browser-use skill`, and connect it to
my browser. Follow https://github.com/browser-use/browser-use if setup or
connection fails.
More:
browser-use --doctor check install, daemon, and browser health
browser-use --help full command list
docs: https://github.com/browser-use/browser-use/blob/main/browser_use/skills/browser-use/SKILL.md"""
_EMPTY_STDIN_MESSAGE = """browser-use received empty stdin. This CLI executes Python piped on stdin:
browser-use <<'PY'
print(page_info())
PY"""
def _command_name(args: list[str]) -> str:
if '--cli-mcp' in args:
return 'cli-mcp'
if '--mcp' in args:
return 'mcp'
if args and args[0] == 'install':
return 'install'
if args and args[0] == 'init':
return 'init'
if '--template' in args or '-t' in args:
return 'init'
if args and args[0] == 'skill':
return 'skill'
legacy = _legacy_command(args)
if legacy is not None:
return f'legacy:{legacy}'
return args[0] if args else 'run'
def _dispatch(args: list[str]) -> tuple[int | None, str]:
if '--cli-mcp' in args:
_run_cli_mcp_server()
return 0, 'cli-mcp'
if '--mcp' in args:
_run_mcp_server()
return 0, 'mcp'
if args and args[0] == 'install':
return _run_install_command(args[1:]), 'install'
if args and args[0] == 'init':
return _run_init_command(args[1:]), 'init'
if '--template' in args or '-t' in args:
return _run_init_command(args), 'init'
if args and args[0] == 'skill':
from browser_use.skills.install import handle as handle_skill_command
return handle_skill_command(args[1:]), 'skill'
legacy = _legacy_command(args)
if legacy is not None:
print(_legacy_migration_message(legacy), file=sys.stderr)
return 2, f'legacy:{legacy}'
if not args:
if sys.stdin.isatty():
print(_QUICKSTART)
return 0, 'quickstart'
code = sys.stdin.read()
if not code.strip():
print(_EMPTY_STDIN_MESSAGE, file=sys.stderr)
return 1, 'run'
sys.stdin = StringIO(code)
try:
return _run_browser_harness(), args[0] if args else 'run'
except NameError as exc:
name = _unknown_exec_name(exc)
if name is None or not _raised_from_piped_code(exc):
raise
import traceback
traceback.print_exc()
print(_unknown_helper_message(name), file=sys.stderr)
return 2, args[0] if args else 'run'
class _StderrTail:
"""Pass-through stderr wrapper that remembers the tail as error context."""
def __init__(self, wrapped):
self._wrapped = wrapped
self.tail = ''
def write(self, text):
self.tail = (self.tail + text)[-500:]
return self._wrapped.write(text)
def __getattr__(self, name):
return getattr(self._wrapped, name)
def browser_use_tui_main() -> int | None:
print('browser-use-tui is deprecated; use browser-use instead.', file=sys.stderr)
return main()
def main() -> int | None:
global _delegated_to_harness
_delegated_to_harness = False
args = sys.argv[1:]
start_time = time.monotonic()
command = _command_name(args)
stderr_tail = _StderrTail(sys.stderr)
sys.stderr = stderr_tail
try:
result, command = _dispatch(args)
except SystemExit as exc:
result = exc.code
if not _delegated_to_harness:
_capture_via_harness(
command=command,
start_time=start_time,
result=result,
error_message=str(result) if isinstance(result, str) else stderr_tail.tail.strip() or None,
)
raise
except Exception as exc:
if not _delegated_to_harness:
_capture_via_harness(command=command, start_time=start_time, result=1, error_message=str(exc))
raise
finally:
sys.stderr = stderr_tail._wrapped
if not _delegated_to_harness:
_capture_via_harness(
command=command,
start_time=start_time,
result=result,
error_message=(stderr_tail.tail.strip() or None) if _exit_code(result) else None,
)
return result
if __name__ == '__main__':
result = main()
if result is not None:
sys.exit(result)