Files
ShawnPana 92de59c6cc fix(cli): fix pyright type errors in CLI
- Remove invalid cdp_url parameter from create_browser_session calls
- Replace deleted GetCookiesEvent with direct _cdp_get_cookies() call
- Add type assertion for cookies_file after error check
- Use unique variable names for CDP results to avoid type conflicts
- Add type ignores for CDP typed return values used as dicts
2026-02-02 13:05:11 -08:00

1231 lines
38 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fast CLI for browser-use. STDLIB ONLY - must start in <50ms.
This is the main entry point for the browser-use CLI. It uses only stdlib
imports to ensure fast startup, delegating heavy operations to the session
server which loads once and stays running.
"""
import argparse
import hashlib
import json
import os
import socket
import subprocess
import sys
import tempfile
import time
from pathlib import Path
# =============================================================================
# Early command interception (before heavy imports)
# These commands don't need the session server infrastructure
# =============================================================================
# Handle --mcp flag early to prevent logging initialization
if '--mcp' in sys.argv:
import logging
os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'critical'
os.environ['BROWSER_USE_SETUP_LOGGING'] = 'false'
logging.disable(logging.CRITICAL)
import asyncio
from browser_use.mcp.server import main as mcp_main
asyncio.run(mcp_main())
sys.exit(0)
# Helper to find the subcommand (first non-flag argument)
def _get_subcommand() -> str | None:
"""Get the first non-flag argument (the subcommand)."""
for arg in sys.argv[1:]:
if not arg.startswith('-'):
return arg
return None
# Handle 'install' command - installs Chromium browser + system dependencies
if _get_subcommand() == 'install':
import platform
print('📦 Installing Chromium browser + system dependencies...')
print('⏳ This may take a few minutes...\n')
# Build command - only use --with-deps on Linux (it fails on Windows/macOS)
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('\n✅ Installation complete!')
print('🚀 Ready to use! Run: uvx browser-use')
else:
print('\n❌ Installation failed')
sys.exit(1)
sys.exit(0)
# Handle 'init' command - generate template files
# Uses _get_subcommand() to check if 'init' is the actual subcommand,
# not just anywhere in argv (prevents hijacking: browser-use run "init something")
if _get_subcommand() == 'init':
from browser_use.init_cmd import main as init_main
# Check if --template or -t flag is present without a value
# If so, just remove it and let init_main handle interactive mode
if '--template' in sys.argv or '-t' in sys.argv:
try:
template_idx = sys.argv.index('--template') if '--template' in sys.argv else sys.argv.index('-t')
template = sys.argv[template_idx + 1] if template_idx + 1 < len(sys.argv) else None
# If template is not provided or is another flag, remove the flag and use interactive mode
if not template or template.startswith('-'):
if '--template' in sys.argv:
sys.argv.remove('--template')
else:
sys.argv.remove('-t')
except (ValueError, IndexError):
pass
# Remove 'init' from sys.argv so click doesn't see it as an unexpected argument
sys.argv.remove('init')
init_main()
sys.exit(0)
# Handle --template flag directly (without 'init' subcommand)
# Delegate to init_main() which handles full template logic (directories, manifests, etc.)
if '--template' in sys.argv:
from browser_use.init_cmd import main as init_main
# Build clean argv for init_main: keep only init-relevant flags
new_argv = [sys.argv[0]] # program name
i = 1
while i < len(sys.argv):
arg = sys.argv[i]
# Keep --template/-t and its value
if arg in ('--template', '-t'):
new_argv.append(arg)
if i + 1 < len(sys.argv) and not sys.argv[i + 1].startswith('-'):
new_argv.append(sys.argv[i + 1])
i += 1
# Keep --output/-o and its value
elif arg in ('--output', '-o'):
new_argv.append(arg)
if i + 1 < len(sys.argv) and not sys.argv[i + 1].startswith('-'):
new_argv.append(sys.argv[i + 1])
i += 1
# Keep --force/-f and --list/-l flags
elif arg in ('--force', '-f', '--list', '-l'):
new_argv.append(arg)
# Skip other flags (--session, --browser, --headed, etc.)
i += 1
sys.argv = new_argv
init_main()
sys.exit(0)
# =============================================================================
# Utility functions (inlined to avoid imports)
# =============================================================================
def get_socket_path(session: str) -> str:
"""Get socket path for session."""
if sys.platform == 'win32':
port = 49152 + (int(hashlib.md5(session.encode()).hexdigest()[:4], 16) % 16383)
return f'tcp://localhost:{port}'
return str(Path(tempfile.gettempdir()) / f'browser-use-{session}.sock')
def get_pid_path(session: str) -> Path:
"""Get PID file path for session."""
return Path(tempfile.gettempdir()) / f'browser-use-{session}.pid'
def is_server_running(session: str) -> bool:
"""Check if server is running for session."""
pid_path = get_pid_path(session)
if not pid_path.exists():
return False
try:
pid = int(pid_path.read_text().strip())
os.kill(pid, 0)
return True
except (OSError, ValueError):
return False
def connect_to_server(session: str, timeout: float = 60.0) -> socket.socket:
"""Connect to session server."""
sock_path = get_socket_path(session)
if sock_path.startswith('tcp://'):
# Windows: TCP connection
_, hostport = sock_path.split('://', 1)
host, port = hostport.split(':')
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((host, int(port)))
else:
# Unix socket
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect(sock_path)
return sock
def ensure_server(session: str, browser: str, headed: bool, profile: str | None, api_key: str | None) -> bool:
"""Start server if not running. Returns True if started."""
# Check if server is already running and responsive
if is_server_running(session):
try:
sock = connect_to_server(session, timeout=0.1)
sock.close()
return False # Already running
except Exception:
pass # Server dead, restart
# Build server command
cmd = [
sys.executable,
'-m',
'browser_use.skill_cli.server',
'--session',
session,
'--browser',
browser,
]
if headed:
cmd.append('--headed')
if profile:
cmd.extend(['--profile', profile])
# Set up environment
env = os.environ.copy()
if api_key:
env['BROWSER_USE_API_KEY'] = api_key
# Start server as background process
if sys.platform == 'win32':
# Windows: use CREATE_NEW_PROCESS_GROUP
subprocess.Popen(
cmd,
env=env,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
else:
# Unix: use start_new_session
subprocess.Popen(
cmd,
env=env,
start_new_session=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# Wait for server to be ready
for _ in range(100): # 5 seconds max
if is_server_running(session):
try:
sock = connect_to_server(session, timeout=0.1)
sock.close()
return True
except Exception:
pass
time.sleep(0.05)
print('Error: Failed to start session server', file=sys.stderr)
sys.exit(1)
def send_command(session: str, action: str, params: dict) -> dict:
"""Send command to server and get response."""
request = {
'id': f'r{int(time.time() * 1000000) % 1000000}',
'action': action,
'session': session,
'params': params,
}
sock = connect_to_server(session)
try:
# Send request
sock.sendall((json.dumps(request) + '\n').encode())
# Read response
data = b''
while not data.endswith(b'\n'):
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
if not data:
return {'id': request['id'], 'success': False, 'error': 'No response from server'}
return json.loads(data.decode())
finally:
sock.close()
# =============================================================================
# CLI Commands
# =============================================================================
def build_parser() -> argparse.ArgumentParser:
"""Build argument parser with all commands."""
parser = argparse.ArgumentParser(
prog='browser-use',
description='Browser automation CLI for browser-use',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
browser-use install # Install Chromium browser
browser-use init # Generate template file (interactive)
browser-use init --template default # Generate specific template
browser-use --mcp # Run as MCP server
browser-use open https://example.com
browser-use click 5
browser-use type "Hello World"
browser-use python "print(browser.url)"
browser-use run "Fill the contact form"
browser-use sessions
browser-use close
""",
)
# Global flags
parser.add_argument('--session', '-s', default='default', help='Session name (default: default)')
parser.add_argument('--browser', '-b', choices=['chromium', 'real', 'remote'], default='chromium', help='Browser mode')
parser.add_argument('--headed', action='store_true', help='Show browser window')
parser.add_argument('--profile', help='Chrome profile (real browser mode)')
parser.add_argument('--json', action='store_true', help='Output as JSON')
parser.add_argument('--api-key', help='Browser-Use API key')
parser.add_argument('--mcp', action='store_true', help='Run as MCP server (JSON-RPC via stdin/stdout)')
parser.add_argument('--template', help='Generate template file (use with --output for custom path)')
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
# -------------------------------------------------------------------------
# Setup Commands (handled early, before argparse)
# -------------------------------------------------------------------------
# install
subparsers.add_parser('install', help='Install Chromium browser + system dependencies')
# init
p = subparsers.add_parser('init', help='Generate browser-use template file')
p.add_argument('--template', '-t', help='Template name (interactive if not specified)')
p.add_argument('--output', '-o', help='Output file path')
p.add_argument('--force', '-f', action='store_true', help='Overwrite existing files')
p.add_argument('--list', '-l', action='store_true', help='List available templates')
# -------------------------------------------------------------------------
# Browser Control Commands
# -------------------------------------------------------------------------
# open <url>
p = subparsers.add_parser('open', help='Navigate to URL')
p.add_argument('url', help='URL to navigate to')
# click <index>
p = subparsers.add_parser('click', help='Click element by index')
p.add_argument('index', type=int, help='Element index from state')
# type <text>
p = subparsers.add_parser('type', help='Type text')
p.add_argument('text', help='Text to type')
# input <index> <text>
p = subparsers.add_parser('input', help='Type text into specific element')
p.add_argument('index', type=int, help='Element index')
p.add_argument('text', help='Text to type')
# scroll [up|down]
p = subparsers.add_parser('scroll', help='Scroll page')
p.add_argument('direction', nargs='?', default='down', choices=['up', 'down'], help='Scroll direction')
p.add_argument('--amount', type=int, default=500, help='Scroll amount in pixels')
# back
subparsers.add_parser('back', help='Go back in history')
# screenshot [path]
p = subparsers.add_parser('screenshot', help='Take screenshot')
p.add_argument('path', nargs='?', help='Save path (outputs base64 if not provided)')
p.add_argument('--full', action='store_true', help='Full page screenshot')
# state
subparsers.add_parser('state', help='Get browser state (URL, title, elements)')
# switch <tab>
p = subparsers.add_parser('switch', help='Switch to tab')
p.add_argument('tab', type=int, help='Tab index')
# close-tab [tab]
p = subparsers.add_parser('close-tab', help='Close tab')
p.add_argument('tab', type=int, nargs='?', help='Tab index (current if not specified)')
# keys <keys>
p = subparsers.add_parser('keys', help='Send keyboard keys')
p.add_argument('keys', help='Keys to send (e.g., "Enter", "Control+a")')
# select <index> <value>
p = subparsers.add_parser('select', help='Select dropdown option')
p.add_argument('index', type=int, help='Element index')
p.add_argument('value', help='Value to select')
# eval <js>
p = subparsers.add_parser('eval', help='Execute JavaScript')
p.add_argument('js', help='JavaScript code to execute')
# extract <query>
p = subparsers.add_parser('extract', help='Extract data using LLM')
p.add_argument('query', help='What to extract')
# hover <index>
p = subparsers.add_parser('hover', help='Hover over element')
p.add_argument('index', type=int, help='Element index')
# dblclick <index>
p = subparsers.add_parser('dblclick', help='Double-click element')
p.add_argument('index', type=int, help='Element index')
# rightclick <index>
p = subparsers.add_parser('rightclick', help='Right-click element')
p.add_argument('index', type=int, help='Element index')
# -------------------------------------------------------------------------
# Cookies Commands
# -------------------------------------------------------------------------
cookies_p = subparsers.add_parser('cookies', help='Cookie operations')
cookies_sub = cookies_p.add_subparsers(dest='cookies_command')
# cookies get [--url URL]
p = cookies_sub.add_parser('get', help='Get all cookies')
p.add_argument('--url', help='Filter by URL')
# cookies set <name> <value>
p = cookies_sub.add_parser('set', help='Set a cookie')
p.add_argument('name', help='Cookie name')
p.add_argument('value', help='Cookie value')
p.add_argument('--domain', help='Cookie domain')
p.add_argument('--path', default='/', help='Cookie path')
p.add_argument('--secure', action='store_true', help='Secure cookie')
p.add_argument('--http-only', action='store_true', help='HTTP-only cookie')
p.add_argument('--same-site', choices=['Strict', 'Lax', 'None'], help='SameSite attribute')
p.add_argument('--expires', type=float, help='Expiration timestamp')
# cookies clear [--url URL]
p = cookies_sub.add_parser('clear', help='Clear cookies')
p.add_argument('--url', help='Clear only for URL')
# cookies export <file>
p = cookies_sub.add_parser('export', help='Export cookies to JSON file')
p.add_argument('file', help='Output file path')
p.add_argument('--url', help='Filter by URL')
# cookies import <file>
p = cookies_sub.add_parser('import', help='Import cookies from JSON file')
p.add_argument('file', help='Input file path')
# -------------------------------------------------------------------------
# Wait Commands
# -------------------------------------------------------------------------
wait_p = subparsers.add_parser('wait', help='Wait for conditions')
wait_sub = wait_p.add_subparsers(dest='wait_command')
# wait selector <css>
p = wait_sub.add_parser('selector', help='Wait for CSS selector')
p.add_argument('selector', help='CSS selector')
p.add_argument('--timeout', type=int, default=30000, help='Timeout in ms')
p.add_argument('--state', choices=['attached', 'detached', 'visible', 'hidden'], default='visible', help='Element state')
# wait text <text>
p = wait_sub.add_parser('text', help='Wait for text')
p.add_argument('text', help='Text to wait for')
p.add_argument('--timeout', type=int, default=30000, help='Timeout in ms')
# -------------------------------------------------------------------------
# Get Commands (info retrieval)
# -------------------------------------------------------------------------
get_p = subparsers.add_parser('get', help='Get information')
get_sub = get_p.add_subparsers(dest='get_command')
# get title
get_sub.add_parser('title', help='Get page title')
# get html [--selector SELECTOR]
p = get_sub.add_parser('html', help='Get page HTML')
p.add_argument('--selector', help='CSS selector to scope HTML')
# get text <index>
p = get_sub.add_parser('text', help='Get element text')
p.add_argument('index', type=int, help='Element index')
# get value <index>
p = get_sub.add_parser('value', help='Get input element value')
p.add_argument('index', type=int, help='Element index')
# get attributes <index>
p = get_sub.add_parser('attributes', help='Get element attributes')
p.add_argument('index', type=int, help='Element index')
# get bbox <index>
p = get_sub.add_parser('bbox', help='Get element bounding box')
p.add_argument('index', type=int, help='Element index')
# -------------------------------------------------------------------------
# Python Execution
# -------------------------------------------------------------------------
p = subparsers.add_parser('python', help='Execute Python code')
p.add_argument('code', nargs='?', help='Python code to execute')
p.add_argument('--file', '-f', help='Execute Python file')
p.add_argument('--reset', action='store_true', help='Reset Python namespace')
p.add_argument('--vars', action='store_true', help='Show defined variables')
# -------------------------------------------------------------------------
# Agent Tasks
# -------------------------------------------------------------------------
p = subparsers.add_parser('run', help='Run agent task (requires API key)')
p.add_argument('task', help='Task description')
p.add_argument('--max-steps', type=int, default=100, help='Maximum steps')
# -------------------------------------------------------------------------
# Session Management
# -------------------------------------------------------------------------
# sessions
subparsers.add_parser('sessions', help='List active sessions')
# close
p = subparsers.add_parser('close', help='Close session')
p.add_argument('--all', action='store_true', help='Close all sessions')
# -------------------------------------------------------------------------
# Server Control
# -------------------------------------------------------------------------
server_p = subparsers.add_parser('server', help='Server control')
server_sub = server_p.add_subparsers(dest='server_command')
server_sub.add_parser('status', help='Check server status')
server_sub.add_parser('stop', help='Stop server')
server_sub.add_parser('logs', help='View server logs')
# -------------------------------------------------------------------------
# Profile Management
# -------------------------------------------------------------------------
profile_p = subparsers.add_parser('profile', help='Manage browser profiles')
profile_sub = profile_p.add_subparsers(dest='profile_command')
# profile list-local
profile_sub.add_parser('list-local', help='List local Chrome profiles')
# Cloud profile commands
# profile list
p = profile_sub.add_parser('list', help='List cloud profiles')
p.add_argument('--page', type=int, default=1, help='Page number')
p.add_argument('--page-size', type=int, default=10, help='Items per page')
# profile create
p = profile_sub.add_parser('create', help='Create cloud profile')
p.add_argument('--name', help='Profile name')
# profile get <id>
p = profile_sub.add_parser('get', help='Get cloud profile details')
p.add_argument('id', help='Profile ID')
# profile update <id>
p = profile_sub.add_parser('update', help='Update cloud profile')
p.add_argument('id', help='Profile ID')
p.add_argument('--name', required=True, help='New profile name')
# profile delete <id>
p = profile_sub.add_parser('delete', help='Delete cloud profile')
p.add_argument('id', help='Profile ID')
# profile cookies <profile> - list cookies by domain in a local profile
p = profile_sub.add_parser('cookies', help='List cookies by domain in a local profile')
p.add_argument('profile', help='Local profile name (e.g. "Default", "Profile 1")')
# profile sync - sync local profile to cloud
p = profile_sub.add_parser('sync', help='Sync local Chrome profile to cloud')
p.add_argument('--from', dest='from_profile', help='Local profile name (e.g. "Default", "Profile 1")')
p.add_argument('--name', help='Cloud profile name (default: auto-generated)')
p.add_argument('--domain', help='Only sync cookies for this domain (e.g. "youtube.com")')
return parser
def cloud_api_request(method: str, endpoint: str, body: dict | None = None) -> dict:
"""Make authenticated request to Browser-Use Cloud API.
Returns dict with 'success', 'data' or 'error'.
"""
import urllib.error
import urllib.request
from browser_use.skill_cli.api_key import APIKeyRequired, require_api_key
try:
api_key = require_api_key('Cloud profiles')
except APIKeyRequired as e:
return {'success': False, 'error': str(e)}
url = f'https://api.browser-use.com/api/v2{endpoint}'
headers = {
'X-Browser-Use-API-Key': api_key,
'Content-Type': 'application/json',
}
data = json.dumps(body).encode() if body else None
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req) as resp:
if resp.status == 204: # No content (e.g., delete)
return {'success': True, 'data': {}}
return {'success': True, 'data': json.loads(resp.read().decode())}
except urllib.error.HTTPError as e:
try:
error_body = json.loads(e.read().decode())
error_msg = error_body.get('detail', str(e))
except Exception:
error_msg = str(e)
return {'success': False, 'error': f'{e.code}: {error_msg}'}
except urllib.error.URLError as e:
return {'success': False, 'error': f'Connection error: {e.reason}'}
def handle_profile_command(args: argparse.Namespace) -> int:
"""Handle profile subcommands."""
if args.profile_command == 'list-local':
profiles = list_local_chrome_profiles()
if args.json:
print(json.dumps({'profiles': profiles}))
else:
if profiles:
print('Local Chrome profiles:')
for p in profiles:
email_str = f' ({p["email"]})' if p['email'] != 'local' else ''
print(f' {p["id"]}: {p["name"]}{email_str}')
else:
print('No Chrome profiles found')
return 0
elif args.profile_command == 'list':
# List cloud profiles
endpoint = f'/profiles?pageNumber={args.page}&pageSize={args.page_size}'
result = cloud_api_request('GET', endpoint)
if not result['success']:
print(f'Error: {result["error"]}', file=sys.stderr)
return 1
data = result['data']
if args.json:
print(json.dumps(data))
else:
items = data.get('items', [])
total = data.get('totalItems', 0)
if items:
print(f'Cloud profiles ({len(items)}/{total}):')
for p in items:
name = p.get('name') or '(unnamed)'
domains = p.get('cookieDomains') or []
domain_str = f' [{len(domains)} domains]' if domains else ''
print(f' {p["id"]}: {name}{domain_str}')
else:
print('No cloud profiles found')
return 0
elif args.profile_command == 'create':
# Create cloud profile
body = {}
if args.name:
body['name'] = args.name
result = cloud_api_request('POST', '/profiles', body)
if not result['success']:
print(f'Error: {result["error"]}', file=sys.stderr)
return 1
data = result['data']
if args.json:
print(json.dumps(data))
else:
name = data.get('name') or '(unnamed)'
print(f'Created profile: {data["id"]}')
print(f' Name: {name}')
return 0
elif args.profile_command == 'get':
# Get cloud profile details
result = cloud_api_request('GET', f'/profiles/{args.id}')
if not result['success']:
print(f'Error: {result["error"]}', file=sys.stderr)
return 1
data = result['data']
if args.json:
print(json.dumps(data))
else:
print(f'Profile: {data["id"]}')
print(f' Name: {data.get("name") or "(unnamed)"}')
print(f' Created: {data.get("createdAt", "unknown")}')
print(f' Updated: {data.get("updatedAt", "unknown")}')
print(f' Last used: {data.get("lastUsedAt") or "never"}')
domains = data.get('cookieDomains') or []
if domains:
print(f' Cookie domains ({len(domains)}):')
for d in domains[:10]:
print(f' - {d}')
if len(domains) > 10:
print(f' ... and {len(domains) - 10} more')
return 0
elif args.profile_command == 'update':
# Update cloud profile
body = {'name': args.name}
result = cloud_api_request('PATCH', f'/profiles/{args.id}', body)
if not result['success']:
print(f'Error: {result["error"]}', file=sys.stderr)
return 1
data = result['data']
if args.json:
print(json.dumps(data))
else:
print(f'Updated profile: {data["id"]}')
print(f' Name: {data.get("name") or "(unnamed)"}')
return 0
elif args.profile_command == 'delete':
# Delete cloud profile
result = cloud_api_request('DELETE', f'/profiles/{args.id}')
if not result['success']:
print(f'Error: {result["error"]}', file=sys.stderr)
return 1
if args.json:
print(json.dumps({'deleted': args.id}))
else:
print(f'Deleted profile: {args.id}')
return 0
elif args.profile_command == 'cookies':
# List cookies by domain in a local profile
return handle_profile_cookies(args)
elif args.profile_command == 'sync':
# Sync local profile to cloud
return handle_profile_sync(args)
# No subcommand specified
print('Usage: browser-use profile <command>')
print('Commands: list-local, list, create, get, update, delete, cookies, sync')
return 1
def list_local_chrome_profiles() -> list[dict]:
"""List local Chrome profiles from the Local State file."""
local_state_paths = [
Path.home() / 'Library/Application Support/Google/Chrome/Local State', # macOS
Path.home() / '.config/google-chrome/Local State', # Linux
Path.home() / 'AppData/Local/Google/Chrome/User Data/Local State', # Windows
]
local_state = None
for path in local_state_paths:
if path.exists():
local_state = path
break
if not local_state:
return []
try:
data = json.loads(local_state.read_text())
profiles_info = data.get('profile', {}).get('info_cache', {})
profiles = []
for profile_id, info in profiles_info.items():
profiles.append(
{
'id': profile_id,
'name': info.get('name', 'Unknown'),
'email': info.get('user_name') or info.get('gaia_name') or 'local',
}
)
return profiles
except Exception:
return []
def handle_profile_cookies(args: argparse.Namespace) -> int:
"""List cookies by domain in a local Chrome profile."""
import asyncio
from collections import defaultdict
# Get local profiles
local_profiles = list_local_chrome_profiles()
if not local_profiles:
print('Error: No local Chrome profiles found', file=sys.stderr)
return 1
# Find the matching profile
profile_arg = args.profile
selected_profile = None
for p in local_profiles:
if p['id'] == profile_arg or p['name'] == profile_arg:
selected_profile = p
break
if not selected_profile:
print(f'Error: Profile "{profile_arg}" not found', file=sys.stderr)
print('Available profiles:')
for p in local_profiles:
print(f' {p["id"]}: {p["name"]}')
return 1
profile_id = selected_profile['id']
print(f'Loading cookies from: {selected_profile["name"]} ({selected_profile["email"]})')
async def get_cookies() -> list:
from browser_use.skill_cli.sessions import create_browser_session
# Start local browser headless
local_session = await create_browser_session('real', headed=False, profile=profile_id)
await local_session.start()
try:
return await local_session._cdp_get_cookies() or []
finally:
await local_session.kill()
try:
cookies = asyncio.get_event_loop().run_until_complete(get_cookies())
except RuntimeError:
cookies = asyncio.run(get_cookies())
if not cookies:
print('No cookies found')
return 0
# Group by domain
domain_counts: dict[str, int] = defaultdict(int)
for c in cookies:
domain = c.get('domain', 'unknown')
# Remove leading dot for grouping
if domain.startswith('.'):
domain = domain[1:]
domain_counts[domain] += 1
# Sort by count (descending)
sorted_domains = sorted(domain_counts.items(), key=lambda x: (-x[1], x[0]))
if args.json:
print(json.dumps({'domains': dict(sorted_domains), 'total': len(cookies)}))
else:
print(f'\nCookies by domain ({len(cookies)} total):')
for domain, count in sorted_domains:
print(f' {domain}: {count}')
print()
print('To sync a specific domain:')
print(f' browser-use profile sync --from "{profile_id}" --domain <domain>')
return 0
def handle_profile_sync(args: argparse.Namespace) -> int:
"""Sync a local Chrome profile to Browser-Use Cloud.
This command:
1. Lists local profiles (if --from not specified)
2. Creates a cloud profile
3. Exports cookies from local Chrome (headless)
4. Imports cookies to cloud profile
"""
import asyncio
import tempfile
from browser_use.skill_cli.api_key import APIKeyRequired, require_api_key
# Check API key first
try:
require_api_key('Profile sync')
except APIKeyRequired as e:
print(f'Error: {e}', file=sys.stderr)
return 1
# Get local profiles
local_profiles = list_local_chrome_profiles()
if not local_profiles:
print('Error: No local Chrome profiles found', file=sys.stderr)
return 1
# Determine which profile to sync
from_profile = args.from_profile
if not from_profile:
# Show available profiles and ask user to specify
print('Available local profiles:')
for p in local_profiles:
print(f' {p["id"]}: {p["name"]} ({p["email"]})')
print()
print('Use --from to specify a profile:')
print(' browser-use profile sync --from "Default"')
print(' browser-use profile sync --from "Profile 1"')
return 1
# Find the matching profile
selected_profile = None
for p in local_profiles:
if p['id'] == from_profile or p['name'] == from_profile:
selected_profile = p
break
if not selected_profile:
print(f'Error: Profile "{from_profile}" not found', file=sys.stderr)
print('Available profiles:')
for p in local_profiles:
print(f' {p["id"]}: {p["name"]}')
return 1
profile_id = selected_profile['id']
profile_name = selected_profile['name']
domain_filter = args.domain
# Generate cloud profile name
if args.name:
cloud_name = args.name
elif domain_filter:
cloud_name = f'Chrome - {profile_name} ({domain_filter})'
else:
cloud_name = f'Chrome - {profile_name}'
if domain_filter:
print(f'Syncing: {profile_name} → {domain_filter} cookies only')
else:
print(f'Syncing: {profile_name} ({selected_profile["email"]})')
# Step 1: Create cloud profile
print(' Creating cloud profile...')
result = cloud_api_request('POST', '/profiles', {'name': cloud_name})
if not result['success']:
print(f'Error creating cloud profile: {result["error"]}', file=sys.stderr)
return 1
cloud_profile_id = result['data']['id']
print(f' ✓ Created: {cloud_profile_id}')
# Step 2: Export cookies from local Chrome (headless)
print(' Extracting cookies from local Chrome...')
async def sync_cookies() -> tuple[int, str | None]:
from browser_use.skill_cli.sessions import create_browser_session
# Start local browser headless
local_session = await create_browser_session('real', headed=False, profile=profile_id)
await local_session.start()
try:
# Get cookies via direct CDP
cookies = await local_session._cdp_get_cookies()
if not cookies:
return 0, 'No cookies found in local profile'
# Filter by domain if specified
if domain_filter:
filtered = []
for c in cookies:
cookie_domain = c.get('domain', '')
# Remove leading dot for comparison
if cookie_domain.startswith('.'):
cookie_domain = cookie_domain[1:]
# Match if domain ends with filter (e.g. "youtube.com" matches ".youtube.com" and "www.youtube.com")
if cookie_domain == domain_filter or cookie_domain.endswith('.' + domain_filter):
filtered.append(c)
cookies = filtered
if not cookies:
return 0, f'No cookies found for domain: {domain_filter}'
# Save to temp file
cookies_file = Path(tempfile.gettempdir()) / f'browser-use-sync-{cloud_profile_id}.json'
cookies_file.write_text(json.dumps(cookies, indent=2))
return len(cookies), str(cookies_file)
finally:
await local_session.kill()
try:
cookie_count, cookies_file = asyncio.get_event_loop().run_until_complete(sync_cookies())
except RuntimeError:
# No event loop running
cookie_count, cookies_file = asyncio.run(sync_cookies())
if cookie_count == 0:
print(f' ⚠ {cookies_file}') # cookies_file contains error message
return 1
assert cookies_file is not None # Type guard: if cookie_count > 0, cookies_file is a valid path
print(f' ✓ Extracted {cookie_count} cookies')
# Step 3: Import cookies to cloud profile
print(' Uploading cookies to cloud...')
async def upload_cookies(cookies_path: str) -> tuple[int, str | None]:
from browser_use.skill_cli.sessions import create_browser_session
# Start remote browser with the new profile
remote_session = await create_browser_session('remote', headed=False, profile=cloud_profile_id)
await remote_session.start()
try:
# Load cookies
cookies = json.loads(Path(cookies_path).read_text())
# Set cookies via CDP
cdp_session = await remote_session.get_or_create_cdp_session(target_id=None, focus=False)
if not cdp_session:
return 0, 'Failed to connect to remote browser'
# Build cookie list for bulk set
cookie_list = []
for c in cookies:
cookie_params = {
'name': c['name'],
'value': c['value'],
'domain': c.get('domain'),
'path': c.get('path', '/'),
'secure': c.get('secure', False),
'httpOnly': c.get('httpOnly', False),
}
if c.get('sameSite'):
cookie_params['sameSite'] = c['sameSite']
if c.get('expires'):
cookie_params['expires'] = c['expires']
cookie_list.append(cookie_params)
# Set all cookies in one call
try:
await cdp_session.cdp_client.send.Network.setCookies(
params={'cookies': cookie_list},
session_id=cdp_session.session_id,
)
return len(cookie_list), None
except Exception as e:
return 0, f'Failed to set cookies: {e}'
finally:
await remote_session.kill()
try:
uploaded_count, error = asyncio.get_event_loop().run_until_complete(upload_cookies(cookies_file))
except RuntimeError:
uploaded_count, error = asyncio.run(upload_cookies(cookies_file))
# Clean up temp file
Path(cookies_file).unlink(missing_ok=True)
if error:
print(f' Error: {error}', file=sys.stderr)
return 1
print(f' ✓ Uploaded {uploaded_count} cookies')
print()
print('✓ Profile synced successfully!')
print(f' Cloud profile ID: {cloud_profile_id}')
print(f' Name: {cloud_name}')
print()
print('Use with:')
print(f' browser-use --browser remote --profile {cloud_profile_id} open <url>')
if args.json:
print(
json.dumps(
{
'profile_id': cloud_profile_id,
'name': cloud_name,
'cookies_synced': uploaded_count,
}
)
)
return 0
def handle_server_command(args: argparse.Namespace) -> int:
"""Handle server subcommands."""
if args.server_command == 'status':
if is_server_running(args.session):
print(f'Server for session "{args.session}" is running')
return 0
else:
print(f'Server for session "{args.session}" is not running')
return 1
elif args.server_command == 'stop':
if not is_server_running(args.session):
print(f'Server for session "{args.session}" is not running')
return 0
response = send_command(args.session, 'shutdown', {})
if response.get('success'):
print(f'Server for session "{args.session}" stopped')
return 0
else:
print(f'Error: {response.get("error")}', file=sys.stderr)
return 1
elif args.server_command == 'logs':
log_path = Path(tempfile.gettempdir()) / f'browser-use-{args.session}.log'
if log_path.exists():
print(log_path.read_text())
else:
print('No logs found')
return 0
return 0
def main() -> int:
"""Main entry point."""
parser = build_parser()
args = parser.parse_args()
if not args.command:
parser.print_help()
return 0
# Handle server subcommands without starting server
if args.command == 'server':
return handle_server_command(args)
# Handle profile subcommands without starting server
if args.command == 'profile':
return handle_profile_command(args)
# Handle sessions list - find all running sessions
if args.command == 'sessions':
from browser_use.skill_cli.utils import find_all_sessions
session_names = find_all_sessions()
sessions = [{'name': name, 'status': 'running'} for name in session_names]
if args.json:
print(json.dumps(sessions))
else:
if sessions:
for s in sessions:
print(f' {s["name"]}: {s["status"]}')
else:
print('No active sessions')
return 0
# Handle close --all by closing all running sessions
if args.command == 'close' and getattr(args, 'all', False):
from browser_use.skill_cli.utils import find_all_sessions
session_names = find_all_sessions()
closed = []
for name in session_names:
try:
response = send_command(name, 'close', {})
if response.get('success'):
closed.append(name)
except Exception:
pass # Server may already be stopping
if args.json:
print(json.dumps({'closed': closed, 'count': len(closed)}))
else:
if closed:
print(f'Closed {len(closed)} session(s): {", ".join(closed)}')
else:
print('No active sessions')
return 0
# Set API key in environment if provided
if args.api_key:
os.environ['BROWSER_USE_API_KEY'] = args.api_key
# Validate API key for remote browser mode upfront
if args.browser == 'remote':
from browser_use.skill_cli.api_key import APIKeyRequired, require_api_key
try:
api_key = require_api_key('Remote browser')
# Ensure it's in environment for the cloud client
os.environ['BROWSER_USE_API_KEY'] = api_key
except APIKeyRequired as e:
print(f'Error: {e}', file=sys.stderr)
return 1
# Ensure server is running
ensure_server(args.session, args.browser, args.headed, args.profile, args.api_key)
# Build params from args
params = {}
skip_keys = {'command', 'session', 'browser', 'headed', 'profile', 'json', 'api_key', 'server_command'}
for key, value in vars(args).items():
if key not in skip_keys and value is not None:
params[key] = value
# Send command to server
response = send_command(args.session, args.command, params)
# Output response
if args.json:
print(json.dumps(response))
else:
if response.get('success'):
data = response.get('data')
if data is not None:
if isinstance(data, dict):
# Special case: raw text output (e.g., state command)
if '_raw_text' in data:
print(data['_raw_text'])
else:
for key, value in data.items():
# Skip internal fields
if key.startswith('_'):
continue
if key == 'screenshot' and len(str(value)) > 100:
print(f'{key}: <{len(value)} bytes>')
else:
print(f'{key}: {value}')
elif isinstance(data, str):
print(data)
else:
print(data)
else:
print(f'Error: {response.get("error")}', file=sys.stderr)
return 1
return 0
if __name__ == '__main__':
sys.exit(main())