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
465 lines
14 KiB
Python
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)
|