4cd2d4af2b
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
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Has been cancelled
184 lines
5.1 KiB
Python
184 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
SKILL_NAME = 'browser-use'
|
|
DEFAULT_TARGET = 'all'
|
|
|
|
|
|
def _xdg_config_home() -> Path:
|
|
config_home = os.environ.get('XDG_CONFIG_HOME')
|
|
if config_home:
|
|
return Path(config_home).expanduser()
|
|
return Path.home() / '.config'
|
|
|
|
|
|
def _home_skill_dir(assistant: str) -> Path:
|
|
return Path.home() / f'.{assistant}' / 'skills' / SKILL_NAME
|
|
|
|
|
|
TARGET_DIR_BUILDERS = {
|
|
'agents': lambda: _home_skill_dir('agents'),
|
|
'claude': lambda: _home_skill_dir('claude'),
|
|
'codex': lambda: _home_skill_dir('codex'),
|
|
'copilot': lambda: _home_skill_dir('copilot'),
|
|
'cursor': lambda: _home_skill_dir('cursor'),
|
|
'gemini': lambda: _home_skill_dir('gemini'),
|
|
'opencode': lambda: _xdg_config_home() / 'opencode' / 'skills' / SKILL_NAME,
|
|
}
|
|
|
|
TARGET_NAMES = tuple(TARGET_DIR_BUILDERS)
|
|
|
|
|
|
def _all_target_skill_paths() -> list[Path]:
|
|
paths = [build_dir() / 'SKILL.md' for build_dir in TARGET_DIR_BUILDERS.values()]
|
|
legacy_opencode = Path.home() / '.config' / 'opencode' / 'skills' / SKILL_NAME / 'SKILL.md'
|
|
if legacy_opencode not in paths:
|
|
paths.append(legacy_opencode)
|
|
return paths
|
|
|
|
|
|
def _load_skill_text_from_package() -> str:
|
|
from browser_use.skills.browser_use import skill_text
|
|
|
|
return skill_text()
|
|
|
|
|
|
def _browser_harness_executable() -> str | None:
|
|
exe = shutil.which('browser-harness')
|
|
if exe:
|
|
return exe
|
|
|
|
local_bin = Path.home() / '.local' / 'bin'
|
|
for name in ('browser-harness', 'browser-harness.exe'):
|
|
path = local_bin / name
|
|
if path.exists():
|
|
return str(path)
|
|
return None
|
|
|
|
|
|
def _install_browser_use_tool() -> None:
|
|
uv = shutil.which('uv')
|
|
if not uv:
|
|
raise RuntimeError('Installing the Browser Use skill requires `uv`. Install uv, then rerun `browser-use skill install`.')
|
|
|
|
result = subprocess.run([uv, 'tool', 'install', '--python', '3.12', '--upgrade', '--force', 'browser-use'])
|
|
if result.returncode != 0:
|
|
raise RuntimeError('Failed to install browser-use with `uv tool install --python 3.12 --upgrade --force browser-use`.')
|
|
|
|
|
|
def _load_skill_text_from_browser_harness_cli() -> str:
|
|
exe = _browser_harness_executable()
|
|
if exe is None:
|
|
return _load_skill_text_from_package()
|
|
|
|
result = subprocess.run([exe, 'skill'], capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
error = result.stderr.strip() or result.stdout.strip() or 'unknown error'
|
|
raise RuntimeError(f'Failed to read skill from `{exe} skill`: {error}')
|
|
|
|
from browser_use.skills.browser_use import as_browser_use_skill
|
|
|
|
return as_browser_use_skill(result.stdout)
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog='browser-use skill',
|
|
description='Print or install the Browser Use skill.',
|
|
)
|
|
subparsers = parser.add_subparsers(dest='command')
|
|
|
|
subparsers.add_parser('show', help='Print the skill text to stdout')
|
|
|
|
install = subparsers.add_parser('install', help='Install the skill')
|
|
install.add_argument(
|
|
'--target',
|
|
choices=sorted([*TARGET_NAMES, 'all']),
|
|
default=DEFAULT_TARGET,
|
|
help='Assistant skill directory to install into',
|
|
)
|
|
install.add_argument(
|
|
'--path',
|
|
type=Path,
|
|
help='Custom output directory or SKILL.md path',
|
|
)
|
|
install.add_argument(
|
|
'--force',
|
|
action='store_true',
|
|
help='Accepted for compatibility; install overwrites existing SKILL.md files by default',
|
|
)
|
|
install.add_argument(
|
|
'--no-install',
|
|
action='store_true',
|
|
help='Skip uv tool install/upgrade and use the existing browser-use command or package',
|
|
)
|
|
|
|
return parser
|
|
|
|
|
|
def _resolve_output_paths(target: str, custom_path: Path | None) -> list[Path]:
|
|
if custom_path is None:
|
|
if target == 'all':
|
|
return _all_target_skill_paths()
|
|
return [TARGET_DIR_BUILDERS[target]() / 'SKILL.md']
|
|
|
|
path = custom_path.expanduser()
|
|
if path.name == 'SKILL.md':
|
|
return [path]
|
|
return [path / 'SKILL.md']
|
|
|
|
|
|
def _validate_output_paths(output_paths: list[Path]) -> None:
|
|
for output_path in output_paths:
|
|
if output_path.exists() and output_path.is_dir():
|
|
raise RuntimeError(f'{output_path} is a directory, expected a SKILL.md file path.')
|
|
ancestor = output_path.parent
|
|
while not ancestor.exists():
|
|
if ancestor.parent == ancestor:
|
|
break
|
|
ancestor = ancestor.parent
|
|
if ancestor.exists() and not ancestor.is_dir():
|
|
raise RuntimeError(f'{ancestor} is not a directory.')
|
|
|
|
|
|
def handle(argv: list[str]) -> int:
|
|
parser = _build_parser()
|
|
args = parser.parse_args(argv)
|
|
|
|
command = args.command or 'show'
|
|
|
|
if command == 'show':
|
|
try:
|
|
text = _load_skill_text_from_browser_harness_cli()
|
|
except RuntimeError as exc:
|
|
print(f'Error: {exc}', file=sys.stderr)
|
|
return 1
|
|
print(text, end='')
|
|
return 0
|
|
|
|
if command == 'install':
|
|
try:
|
|
output_paths = _resolve_output_paths(args.target, args.path)
|
|
_validate_output_paths(output_paths)
|
|
if not args.no_install:
|
|
_install_browser_use_tool()
|
|
text = _load_skill_text_from_browser_harness_cli()
|
|
except RuntimeError as exc:
|
|
print(f'Error: {exc}', file=sys.stderr)
|
|
return 1
|
|
|
|
for output_path in output_paths:
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text(text, encoding='utf-8')
|
|
print(f'Installed Browser Use skill to {output_path}')
|
|
return 0
|
|
|
|
parser.print_help()
|
|
return 1
|