browser-workspace 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +21 -0
- package/README.md +138 -0
- package/SKILL.md +301 -0
- package/agent-workspace/agent_helpers.py +718 -0
- package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
- package/bin/browser-workspace +75 -0
- package/extension/manifest.json +26 -0
- package/extension/service-worker.mjs +126 -0
- package/extension/workspace-manager.mjs +680 -0
- package/extension/workspace-page.mjs +7 -0
- package/extension/workspace.html +8 -0
- package/interaction-skills/connection.md +52 -0
- package/interaction-skills/cookies.md +3 -0
- package/interaction-skills/cross-origin-iframes.md +3 -0
- package/interaction-skills/dialogs.md +64 -0
- package/interaction-skills/downloads.md +3 -0
- package/interaction-skills/drag-and-drop.md +3 -0
- package/interaction-skills/dropdowns.md +3 -0
- package/interaction-skills/iframes.md +3 -0
- package/interaction-skills/network-requests.md +3 -0
- package/interaction-skills/print-as-pdf.md +3 -0
- package/interaction-skills/profile-sync.md +90 -0
- package/interaction-skills/screenshots.md +17 -0
- package/interaction-skills/scrolling.md +22 -0
- package/interaction-skills/shadow-dom.md +3 -0
- package/interaction-skills/tabs.md +73 -0
- package/interaction-skills/uploads.md +1 -0
- package/interaction-skills/viewport.md +16 -0
- package/licenses/browser-harness-MIT.txt +21 -0
- package/managed_chrome.py +244 -0
- package/node/admin.mjs +44 -0
- package/package.json +36 -0
- package/platform_runner.py +187 -0
- package/platforms/chatgpt/README.md +60 -0
- package/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
- package/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
- package/platforms/chatgpt/actions/_mcp_app.py +80 -0
- package/platforms/chatgpt/actions/_new_thread.py +85 -0
- package/platforms/chatgpt/actions/_new_turn.py +33 -0
- package/platforms/chatgpt/actions/_project_setup.py +353 -0
- package/platforms/chatgpt/actions/_readiness.py +52 -0
- package/platforms/chatgpt/actions/_submit_thread.py +129 -0
- package/platforms/chatgpt/actions/_temporary_submit.py +571 -0
- package/platforms/chatgpt/actions/_thread_turn.py +362 -0
- package/platforms/chatgpt/manifest.yaml +34 -0
- package/platforms/chatgpt/tests/test_contract.py +176 -0
- package/platforms/chrome-web-store/README.md +49 -0
- package/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
- package/platforms/chrome-web-store/flows/check-status.md +7 -0
- package/platforms/chrome-web-store/flows/open-item.md +10 -0
- package/platforms/chrome-web-store/flows/submit-review.md +10 -0
- package/platforms/chrome-web-store/flows/update-listing.md +18 -0
- package/platforms/chrome-web-store/flows/upload-package.md +11 -0
- package/platforms/chrome-web-store/flows/verify-published.md +9 -0
- package/platforms/chrome-web-store/manifest.yaml +33 -0
- package/platforms/chrome-web-store/tests/test_contract.py +83 -0
- package/platforms/microsoft-teams/README.md +32 -0
- package/platforms/microsoft-teams/actions/_meeting.py +210 -0
- package/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
- package/platforms/microsoft-teams/manifest.yaml +25 -0
- package/platforms/microsoft-teams/tests/test_contract.py +24 -0
- package/platforms/tiktok/README.md +10 -0
- package/platforms/tiktok/actions/_post.py +180 -0
- package/platforms/tiktok/manifest.yaml +99 -0
- package/platforms/wechat-channels/README.md +10 -0
- package/platforms/wechat-channels/actions/_common.py +163 -0
- package/platforms/wechat-channels/actions/_manage.py +130 -0
- package/platforms/wechat-channels/actions/_post.py +232 -0
- package/platforms/wechat-channels/manifest.yaml +118 -0
- package/platforms/wechat-channels/tests/test_contract.py +36 -0
- package/platforms/xhs/README.md +10 -0
- package/platforms/xhs/actions/_manage.py +355 -0
- package/platforms/xhs/actions/_post.py +192 -0
- package/platforms/xhs/manifest.yaml +98 -0
- package/platforms/youtube/README.md +10 -0
- package/platforms/youtube/actions/_post.py +100 -0
- package/platforms/youtube/manifest.yaml +94 -0
- package/pyproject.toml +29 -0
- package/session_client.py +97 -0
- package/session_daemon.py +325 -0
- package/src/browser_harness/__init__.py +2 -0
- package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/browser_harness/__pycache__/_ipc.cpython-311.pyc +0 -0
- package/src/browser_harness/__pycache__/admin.cpython-311.pyc +0 -0
- package/src/browser_harness/__pycache__/auth.cpython-311.pyc +0 -0
- package/src/browser_harness/__pycache__/daemon.cpython-311.pyc +0 -0
- package/src/browser_harness/__pycache__/helpers.cpython-311.pyc +0 -0
- package/src/browser_harness/__pycache__/paths.cpython-311.pyc +0 -0
- package/src/browser_harness/__pycache__/recorder.cpython-311.pyc +0 -0
- package/src/browser_harness/_ipc.py +201 -0
- package/src/browser_harness/admin.py +1588 -0
- package/src/browser_harness/auth.py +546 -0
- package/src/browser_harness/daemon.py +894 -0
- package/src/browser_harness/helpers.py +668 -0
- package/src/browser_harness/macos.py +131 -0
- package/src/browser_harness/mcp_cli.py +16 -0
- package/src/browser_harness/paths.py +49 -0
- package/src/browser_harness/recorder.py +343 -0
- package/src/browser_harness/run.py +413 -0
- package/src/browser_harness/telemetry.py +308 -0
- package/src/browser_harness/video-template.html +970 -0
- package/src/browser_harness/video.py +749 -0
- package/src/browser_harness/video_render.py +524 -0
- package/src/browser_workspace.egg-info/PKG-INFO +10 -0
- package/src/browser_workspace.egg-info/SOURCES.txt +28 -0
- package/src/browser_workspace.egg-info/dependency_links.txt +1 -0
- package/src/browser_workspace.egg-info/requires.txt +5 -0
- package/src/browser_workspace.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import json, os, sys, time, urllib.request
|
|
2
|
+
from io import StringIO
|
|
3
|
+
|
|
4
|
+
# Windows default stdout/stderr encoding is cp1252
|
|
5
|
+
# which can't encode the 🐴 marker helpers prepend to tab titles (or anything
|
|
6
|
+
# else outside the locale charset). Force UTF-8 so `print(page_info())` and
|
|
7
|
+
# tracebacks carrying page titles don't UnicodeEncodeError on Windows. #124(4).
|
|
8
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
9
|
+
if hasattr(_stream, "reconfigure"):
|
|
10
|
+
try: _stream.reconfigure(encoding="utf-8", errors="replace")
|
|
11
|
+
except Exception: pass
|
|
12
|
+
|
|
13
|
+
from .admin import (
|
|
14
|
+
_version,
|
|
15
|
+
NAME,
|
|
16
|
+
daemon_alive,
|
|
17
|
+
daemon_browser_kind,
|
|
18
|
+
ensure_daemon,
|
|
19
|
+
list_cloud_profiles,
|
|
20
|
+
list_local_profiles,
|
|
21
|
+
print_update_banner,
|
|
22
|
+
require_existing_daemon,
|
|
23
|
+
restart_daemon,
|
|
24
|
+
run_doctor,
|
|
25
|
+
run_doctor_fix_snap,
|
|
26
|
+
run_doctor_json,
|
|
27
|
+
run_update,
|
|
28
|
+
start_remote_daemon,
|
|
29
|
+
stop_remote_daemon,
|
|
30
|
+
sync_local_profile,
|
|
31
|
+
)
|
|
32
|
+
from . import auth, recorder, telemetry
|
|
33
|
+
from .helpers import *
|
|
34
|
+
|
|
35
|
+
HELP = """Browser Harness
|
|
36
|
+
|
|
37
|
+
Read SKILL.md for the default workflow and examples.
|
|
38
|
+
|
|
39
|
+
Typical usage:
|
|
40
|
+
browser-harness <<'PY'
|
|
41
|
+
ensure_real_tab()
|
|
42
|
+
print(page_info())
|
|
43
|
+
PY
|
|
44
|
+
|
|
45
|
+
Helpers are pre-imported. The daemon auto-starts and connects to the running browser.
|
|
46
|
+
|
|
47
|
+
Commands:
|
|
48
|
+
browser-harness --version print the installed version
|
|
49
|
+
browser-harness --doctor diagnose install, daemon, and browser state
|
|
50
|
+
browser-harness doctor same as --doctor
|
|
51
|
+
browser-harness doctor --json [--require-existing-daemon]
|
|
52
|
+
print machine-readable runtime health
|
|
53
|
+
browser-harness doctor --fix-snap print how to fix Snap Chromium blocking CDP (Linux)
|
|
54
|
+
browser-harness mac-approve approve Chrome's macOS remote debugging sheet
|
|
55
|
+
browser-harness auth login sign in to Browser Use Cloud for cloud browsers
|
|
56
|
+
browser-harness auth login --device-code sign in from SSH/headless environments
|
|
57
|
+
browser-harness auth status show Browser Use Cloud auth state
|
|
58
|
+
browser-harness auth logout remove stored Browser Use Cloud auth
|
|
59
|
+
browser-harness skill print the browser-harness skill text
|
|
60
|
+
browser-harness recordings show recording status and recent sessions
|
|
61
|
+
browser-harness recordings --latest print the newest recording directory
|
|
62
|
+
browser-harness recordings enable save browser actions locally by default
|
|
63
|
+
browser-harness recordings disable stop saving browser actions by default
|
|
64
|
+
browser-harness video init <recording> prepare a recording for editing
|
|
65
|
+
browser-harness video review <recording> compile and review the video
|
|
66
|
+
browser-harness video export <recording> --reviewed export a verified MP4
|
|
67
|
+
browser-harness telemetry status show anonymous telemetry opt-out state
|
|
68
|
+
browser-harness --update [-y] pull the latest version (agents: pass -y)
|
|
69
|
+
browser-harness --reload stop the daemon so next call picks up code changes
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
USAGE = """Usage:
|
|
73
|
+
browser-harness <<'PY'
|
|
74
|
+
print(page_info())
|
|
75
|
+
PY
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Probe /json/version (not a bare TCP connect) so a non-Chrome process bound to
|
|
80
|
+
# 9222/9223 doesn't masquerade as Chrome and skip the cloud bootstrap. Mirrors
|
|
81
|
+
# daemon.py's fallback probe.
|
|
82
|
+
def _local_chrome_listening():
|
|
83
|
+
for port in (9222, 9223):
|
|
84
|
+
try:
|
|
85
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=0.3) as response:
|
|
86
|
+
version = json.loads(response.read())
|
|
87
|
+
if isinstance(version, dict) and isinstance(version.get("webSocketDebuggerUrl"), str) and version["webSocketDebuggerUrl"]:
|
|
88
|
+
return True
|
|
89
|
+
except (OSError, TypeError, ValueError):
|
|
90
|
+
pass
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# BU_CDP_URL / BU_CDP_WS are documented to override local Chrome discovery
|
|
95
|
+
# (install.md:58-59), so they must also block cloud auto-bootstrap. Without this
|
|
96
|
+
# guard, start_remote_daemon() in admin.py overwrites BU_CDP_WS in the daemon
|
|
97
|
+
# env with a cloud WebSocket URL, silently replacing the user's explicit endpoint
|
|
98
|
+
# *and* billing them for a cloud browser they never asked for.
|
|
99
|
+
def _explicit_cdp_configured():
|
|
100
|
+
return bool(os.environ.get("BU_CDP_URL") or os.environ.get("BU_CDP_WS"))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _cloud_auth_configured():
|
|
104
|
+
try:
|
|
105
|
+
auth.get_browser_use_api_key()
|
|
106
|
+
return True
|
|
107
|
+
except (auth.CloudAuthRequired, auth.AuthError, OSError):
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _print_skill():
|
|
112
|
+
from importlib import resources
|
|
113
|
+
# SKILL.md is UTF-8 (contains emoji); locale-codec read crashes on gbk Windows
|
|
114
|
+
print(resources.files("browser_harness").joinpath("SKILL.md").read_text(encoding="utf-8"), end="")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _telemetry_command(args):
|
|
118
|
+
if not args:
|
|
119
|
+
return "script"
|
|
120
|
+
first = args[0]
|
|
121
|
+
if first in {"-h", "--help"}:
|
|
122
|
+
return "help"
|
|
123
|
+
if first == "--version":
|
|
124
|
+
return "version"
|
|
125
|
+
if first in {"--doctor", "doctor"}:
|
|
126
|
+
return "doctor"
|
|
127
|
+
if first == "--update":
|
|
128
|
+
return "update"
|
|
129
|
+
if first == "--reload":
|
|
130
|
+
return "reload"
|
|
131
|
+
if first == "--debug-clicks":
|
|
132
|
+
return "debug-clicks"
|
|
133
|
+
if first in {"auth", "skill", "mac-approve", "recordings", "telemetry", "video"}:
|
|
134
|
+
return first
|
|
135
|
+
return "usage"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _exit_code(result) -> int:
|
|
139
|
+
if result is None:
|
|
140
|
+
return 0
|
|
141
|
+
if isinstance(result, int):
|
|
142
|
+
return result
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
_MAX_TRACED_STEPS = 500
|
|
146
|
+
_MAX_STEP_ARGS_LENGTH = 300
|
|
147
|
+
_helper_trace = []
|
|
148
|
+
_helper_call_count = 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _step_args(args, kwargs):
|
|
152
|
+
parts = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
|
|
153
|
+
return ", ".join(parts)[:_MAX_STEP_ARGS_LENGTH]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _traced(name, fn):
|
|
157
|
+
import functools
|
|
158
|
+
|
|
159
|
+
@functools.wraps(fn)
|
|
160
|
+
def wrapper(*args, **kwargs):
|
|
161
|
+
global _helper_call_count
|
|
162
|
+
_helper_call_count += 1
|
|
163
|
+
entry = {"helper": name, "args": _step_args(args, kwargs)}
|
|
164
|
+
if len(_helper_trace) < _MAX_TRACED_STEPS:
|
|
165
|
+
_helper_trace.append(entry)
|
|
166
|
+
step_start = time.monotonic()
|
|
167
|
+
try:
|
|
168
|
+
result = fn(*args, **kwargs)
|
|
169
|
+
except BaseException as exc:
|
|
170
|
+
entry["duration_seconds"] = round(time.monotonic() - step_start, 3)
|
|
171
|
+
entry["error"] = str(exc)[:300]
|
|
172
|
+
raise
|
|
173
|
+
entry["duration_seconds"] = round(time.monotonic() - step_start, 3)
|
|
174
|
+
recorder.observe(name, args, kwargs, entry["duration_seconds"])
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
wrapper.__bh_traced__ = True
|
|
178
|
+
return wrapper
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _install_helper_trace():
|
|
182
|
+
from . import helpers
|
|
183
|
+
|
|
184
|
+
g = globals()
|
|
185
|
+
for name in dir(helpers):
|
|
186
|
+
if name.startswith("_"):
|
|
187
|
+
continue
|
|
188
|
+
fn = g.get(name)
|
|
189
|
+
if callable(fn) and not isinstance(fn, type) and not getattr(fn, "__bh_traced__", False):
|
|
190
|
+
g[name] = _traced(name, fn)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
_MAX_OUTPUT_LENGTH = 20_000
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class _StreamTail:
|
|
197
|
+
"""Pass-through stream wrapper that remembers the tail and total length."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, wrapped, limit=500):
|
|
200
|
+
self._wrapped = wrapped
|
|
201
|
+
self._limit = limit
|
|
202
|
+
self.tail = ""
|
|
203
|
+
self.length = 0
|
|
204
|
+
|
|
205
|
+
def write(self, text):
|
|
206
|
+
text = str(text)
|
|
207
|
+
self.length += len(text)
|
|
208
|
+
self.tail = (self.tail + text)[-self._limit :]
|
|
209
|
+
return self._wrapped.write(text)
|
|
210
|
+
|
|
211
|
+
def __getattr__(self, name):
|
|
212
|
+
return getattr(self._wrapped, name)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _read_task(args):
|
|
216
|
+
if args and args[0] == "--debug-clicks":
|
|
217
|
+
args = args[1:]
|
|
218
|
+
if args or sys.stdin.isatty():
|
|
219
|
+
return None
|
|
220
|
+
code = sys.stdin.read()
|
|
221
|
+
sys.stdin = StringIO(code)
|
|
222
|
+
return code
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _traced_steps():
|
|
226
|
+
return _helper_trace or None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _telemetry_browser(task):
|
|
230
|
+
"""'cloud' | 'cdp' | 'local', self-reported by the daemon the task ran on.
|
|
231
|
+
None when no browser was involved (non-script commands, daemon never up)."""
|
|
232
|
+
if not task or not telemetry.is_enabled():
|
|
233
|
+
return None
|
|
234
|
+
try:
|
|
235
|
+
return daemon_browser_kind()
|
|
236
|
+
except Exception:
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main():
|
|
241
|
+
global _helper_call_count
|
|
242
|
+
args = sys.argv[1:]
|
|
243
|
+
if args and args[0] == "telemetry":
|
|
244
|
+
sys.exit(telemetry.run_telemetry_cli(args[1:]))
|
|
245
|
+
_helper_trace.clear()
|
|
246
|
+
_helper_call_count = 0
|
|
247
|
+
start_time = time.monotonic()
|
|
248
|
+
command = _telemetry_command(args)
|
|
249
|
+
task = _read_task(args)
|
|
250
|
+
stderr_tail = _StreamTail(sys.stderr)
|
|
251
|
+
stdout_tail = _StreamTail(sys.stdout, limit=_MAX_OUTPUT_LENGTH)
|
|
252
|
+
sys.stderr = stderr_tail
|
|
253
|
+
sys.stdout = stdout_tail
|
|
254
|
+
try:
|
|
255
|
+
_run(args)
|
|
256
|
+
except SystemExit as exc:
|
|
257
|
+
code = _exit_code(exc.code)
|
|
258
|
+
telemetry.capture_cli_event(
|
|
259
|
+
action="error" if code else "completed",
|
|
260
|
+
command=command,
|
|
261
|
+
task=task,
|
|
262
|
+
browser=_telemetry_browser(task),
|
|
263
|
+
output=stdout_tail.tail or None,
|
|
264
|
+
output_length=stdout_tail.length or None,
|
|
265
|
+
steps=_traced_steps(),
|
|
266
|
+
step_count=_helper_call_count or None,
|
|
267
|
+
duration_seconds=time.monotonic() - start_time,
|
|
268
|
+
exit_code=code,
|
|
269
|
+
error_message=str(exc.code) if isinstance(exc.code, str) else (stderr_tail.tail.strip() or None) if code else None,
|
|
270
|
+
)
|
|
271
|
+
raise
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
telemetry.capture_cli_event(
|
|
274
|
+
action="error",
|
|
275
|
+
command=command,
|
|
276
|
+
task=task,
|
|
277
|
+
browser=_telemetry_browser(task),
|
|
278
|
+
output=stdout_tail.tail or None,
|
|
279
|
+
output_length=stdout_tail.length or None,
|
|
280
|
+
steps=_traced_steps(),
|
|
281
|
+
step_count=_helper_call_count or None,
|
|
282
|
+
duration_seconds=time.monotonic() - start_time,
|
|
283
|
+
exit_code=1,
|
|
284
|
+
error_message=str(exc),
|
|
285
|
+
)
|
|
286
|
+
raise
|
|
287
|
+
finally:
|
|
288
|
+
sys.stderr = stderr_tail._wrapped
|
|
289
|
+
sys.stdout = stdout_tail._wrapped
|
|
290
|
+
telemetry.capture_cli_event(
|
|
291
|
+
action="completed",
|
|
292
|
+
command=command,
|
|
293
|
+
task=task,
|
|
294
|
+
browser=_telemetry_browser(task),
|
|
295
|
+
output=stdout_tail.tail or None,
|
|
296
|
+
output_length=stdout_tail.length or None,
|
|
297
|
+
steps=_traced_steps(),
|
|
298
|
+
step_count=_helper_call_count or None,
|
|
299
|
+
duration_seconds=time.monotonic() - start_time,
|
|
300
|
+
exit_code=0,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _run(args):
|
|
305
|
+
if args and args[0] in {"-h", "--help"}:
|
|
306
|
+
print(HELP)
|
|
307
|
+
return
|
|
308
|
+
if args and args[0] == "--version":
|
|
309
|
+
print(_version() or "unknown")
|
|
310
|
+
return
|
|
311
|
+
if args and args[0] == "--doctor":
|
|
312
|
+
sys.exit(run_doctor())
|
|
313
|
+
if args and args[0] == "doctor":
|
|
314
|
+
rest = args[1:]
|
|
315
|
+
if rest == ["--fix-snap"]:
|
|
316
|
+
sys.exit(run_doctor_fix_snap())
|
|
317
|
+
if rest and set(rest).issubset({"--json", "--require-existing-daemon"}) \
|
|
318
|
+
and "--json" in rest and len(rest) == len(set(rest)):
|
|
319
|
+
sys.exit(run_doctor_json(require_existing_daemon="--require-existing-daemon" in rest))
|
|
320
|
+
if rest:
|
|
321
|
+
print("usage: browser-harness doctor [--fix-snap|--json [--require-existing-daemon]]", file=sys.stderr)
|
|
322
|
+
sys.exit(2)
|
|
323
|
+
sys.exit(run_doctor())
|
|
324
|
+
if args and args[0] == "auth":
|
|
325
|
+
sys.exit(auth.run_auth_cli(args[1:]))
|
|
326
|
+
if args and args[0] == "mac-approve":
|
|
327
|
+
from . import macos
|
|
328
|
+
|
|
329
|
+
sys.exit(macos.run_cli(args[1:]))
|
|
330
|
+
if args and args[0] == "skill":
|
|
331
|
+
if len(args) != 1:
|
|
332
|
+
print("usage: browser-harness skill", file=sys.stderr)
|
|
333
|
+
sys.exit(2)
|
|
334
|
+
_print_skill()
|
|
335
|
+
return
|
|
336
|
+
if args and args[0] == "recordings":
|
|
337
|
+
rest = args[1:]
|
|
338
|
+
if rest == ["--latest"]:
|
|
339
|
+
latest = recorder.latest_recording()
|
|
340
|
+
if latest is None:
|
|
341
|
+
print("no recordings found", file=sys.stderr)
|
|
342
|
+
sys.exit(1)
|
|
343
|
+
print(latest)
|
|
344
|
+
return
|
|
345
|
+
if rest in (["enable"], ["disable"]):
|
|
346
|
+
enabled = rest == ["enable"]
|
|
347
|
+
recorder.set_auto_recording(enabled)
|
|
348
|
+
print(f"auto-recording preference {'enabled' if enabled else 'disabled'}")
|
|
349
|
+
return
|
|
350
|
+
if rest:
|
|
351
|
+
print("usage: browser-harness recordings [--latest|enable|disable]", file=sys.stderr)
|
|
352
|
+
sys.exit(2)
|
|
353
|
+
enabled, source = recorder.auto_recording_setting()
|
|
354
|
+
print(f"auto-recording: {'on' if enabled else 'off'} ({source})")
|
|
355
|
+
active = recorder.recording_dir()
|
|
356
|
+
print(f"active: {active or 'none'}")
|
|
357
|
+
recent = recorder.recordings()
|
|
358
|
+
print(f"latest: {recent[0] if recent else 'none'}")
|
|
359
|
+
return
|
|
360
|
+
if args and args[0] == "video":
|
|
361
|
+
from . import video
|
|
362
|
+
|
|
363
|
+
sys.exit(video.run_cli(args[1:]))
|
|
364
|
+
if args and args[0] == "--update":
|
|
365
|
+
rest = args[1:]
|
|
366
|
+
if not set(rest).issubset({"-y", "--yes"}):
|
|
367
|
+
print("usage: browser-harness --update [-y|--yes]", file=sys.stderr)
|
|
368
|
+
sys.exit(2)
|
|
369
|
+
sys.exit(run_update(yes=bool(rest)))
|
|
370
|
+
if args and args[0] == "--reload":
|
|
371
|
+
restart_daemon()
|
|
372
|
+
print("daemon stopped — will restart fresh on next call")
|
|
373
|
+
return
|
|
374
|
+
if args and args[0] == "--debug-clicks":
|
|
375
|
+
os.environ["BH_DEBUG_CLICKS"] = "1"
|
|
376
|
+
args = args[1:]
|
|
377
|
+
if not args and not sys.stdin.isatty():
|
|
378
|
+
code = sys.stdin.read()
|
|
379
|
+
if not code.strip():
|
|
380
|
+
sys.exit(USAGE)
|
|
381
|
+
else:
|
|
382
|
+
sys.exit(USAGE)
|
|
383
|
+
print_update_banner()
|
|
384
|
+
# Auto-bootstrap a cloud browser is opt-in via BU_AUTOSPAWN — BROWSER_USE_API_KEY alone
|
|
385
|
+
# is not enough, since the key is commonly set for unrelated reasons (profile sync,
|
|
386
|
+
# cloud API calls, parent agents managing their own session). An explicit BU_CDP_URL
|
|
387
|
+
# or BU_CDP_WS also blocks the spawn so we honour the precedence install.md promises.
|
|
388
|
+
cloud_admin = code.lstrip().startswith(("start_remote_daemon(", "stop_remote_daemon("))
|
|
389
|
+
if not cloud_admin:
|
|
390
|
+
require_existing = os.environ.get("BH_REQUIRE_EXISTING_DAEMON") == "1"
|
|
391
|
+
try:
|
|
392
|
+
if require_existing:
|
|
393
|
+
require_existing_daemon()
|
|
394
|
+
else:
|
|
395
|
+
if (
|
|
396
|
+
not daemon_alive()
|
|
397
|
+
and not _local_chrome_listening()
|
|
398
|
+
and not _explicit_cdp_configured()
|
|
399
|
+
and _cloud_auth_configured()
|
|
400
|
+
and os.environ.get("BU_AUTOSPAWN")
|
|
401
|
+
):
|
|
402
|
+
start_remote_daemon(NAME)
|
|
403
|
+
ensure_daemon()
|
|
404
|
+
except RuntimeError as e:
|
|
405
|
+
# Setup/permission errors are instructions for calling agent
|
|
406
|
+
print(f"browser-harness: {e}", file=sys.stderr)
|
|
407
|
+
sys.exit(1)
|
|
408
|
+
_install_helper_trace()
|
|
409
|
+
exec(code, globals())
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
if __name__ == "__main__":
|
|
413
|
+
main()
|