browserwright 0.6.2__py3-none-any.whl
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.
- browserwright/__init__.py +33 -0
- browserwright/__main__.py +6 -0
- browserwright/_executor/__init__.py +47 -0
- browserwright/_executor/__main__.py +9 -0
- browserwright/_executor/client.py +127 -0
- browserwright/_executor/process.py +652 -0
- browserwright/_executor/protocol.py +152 -0
- browserwright/api.py +66 -0
- browserwright/cdp.py +285 -0
- browserwright/cli.py +741 -0
- browserwright/daemon/__init__.py +8 -0
- browserwright/daemon/_ipc.py +444 -0
- browserwright/daemon/active_tab.py +183 -0
- browserwright/daemon/auth.py +395 -0
- browserwright/daemon/backends/__init__.py +59 -0
- browserwright/daemon/backends/base.py +120 -0
- browserwright/daemon/backends/cloud.py +222 -0
- browserwright/daemon/backends/env.py +119 -0
- browserwright/daemon/backends/extension.py +185 -0
- browserwright/daemon/backends/rdp.py +214 -0
- browserwright/daemon/cli.py +1437 -0
- browserwright/daemon/config.py +380 -0
- browserwright/daemon/doctor.py +179 -0
- browserwright/daemon/errors.py +34 -0
- browserwright/daemon/launch_chrome.py +353 -0
- browserwright/daemon/observability.py +181 -0
- browserwright/daemon/platforms.py +234 -0
- browserwright/daemon/resolver.py +72 -0
- browserwright/daemon/server/__init__.py +6 -0
- browserwright/daemon/server/daemon.py +229 -0
- browserwright/daemon/server/executor_registry.py +434 -0
- browserwright/daemon/server/extension_upstream.py +677 -0
- browserwright/daemon/server/facade.py +375 -0
- browserwright/daemon/server/facade_extension.py +969 -0
- browserwright/daemon/server/listener.py +1058 -0
- browserwright/daemon/server/proxy.py +1991 -0
- browserwright/daemon/server/relay.py +783 -0
- browserwright/daemon/server/state.py +432 -0
- browserwright/daemon/server/upstream.py +266 -0
- browserwright/daemon/userscripts.py +150 -0
- browserwright/discovery.py +213 -0
- browserwright/errors.py +177 -0
- browserwright/health.py +169 -0
- browserwright/install.py +628 -0
- browserwright/memory/__init__.py +15 -0
- browserwright/memory/_md.py +120 -0
- browserwright/memory/_yaml.py +217 -0
- browserwright/memory/global_mem.py +201 -0
- browserwright/memory/repl_mem.py +28 -0
- browserwright/memory/session_decisions.py +53 -0
- browserwright/memory/site_mem.py +381 -0
- browserwright/mode_b_client.py +590 -0
- browserwright/multitask.py +131 -0
- browserwright/output_schema.py +99 -0
- browserwright/primitives/__init__.py +67 -0
- browserwright/primitives/discovery_api.py +79 -0
- browserwright/primitives/http.py +42 -0
- browserwright/primitives/inspect.py +876 -0
- browserwright/primitives/interact.py +518 -0
- browserwright/primitives/page.py +556 -0
- browserwright/primitives/site.py +143 -0
- browserwright/release_install.py +466 -0
- browserwright/repl/__init__.py +6 -0
- browserwright/repl/_namespace.py +106 -0
- browserwright/repl/_smart_goto.py +236 -0
- browserwright/repl/inline.py +180 -0
- browserwright/repl/playwright_handle.py +449 -0
- browserwright/repl/snapshot.py +150 -0
- browserwright/session.py +229 -0
- browserwright/session_create.py +252 -0
- browserwright/session_ctx.py +24 -0
- browserwright/session_registry.py +133 -0
- browserwright/session_runtime.py +133 -0
- browserwright/site_skills_starter/github.com/SKILL.md +14 -0
- browserwright/site_skills_starter/github.com/memory.md +29 -0
- browserwright/site_skills_starter/github.com/tasks/list_issues.py +55 -0
- browserwright/site_skills_starter/google.com/SKILL.md +16 -0
- browserwright/site_skills_starter/google.com/memory.md +27 -0
- browserwright/site_skills_starter/google.com/tasks/search.py +53 -0
- browserwright/site_skills_starter/producthunt.com/SKILL.md +7 -0
- browserwright/site_skills_starter/producthunt.com/memory.md +26 -0
- browserwright/site_skills_starter/producthunt.com/tasks/today.py +64 -0
- browserwright/site_skills_starter/wikipedia.org/SKILL.md +7 -0
- browserwright/site_skills_starter/wikipedia.org/memory.md +22 -0
- browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +55 -0
- browserwright/site_skills_starter/ycombinator.com/SKILL.md +8 -0
- browserwright/site_skills_starter/ycombinator.com/memory.md +25 -0
- browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +63 -0
- browserwright/skill_doc.py +140 -0
- browserwright/skill_runtime.md +194 -0
- browserwright/subscriptions.py +213 -0
- browserwright/task_runner.py +125 -0
- browserwright/version.py +117 -0
- browserwright-0.6.2.dist-info/METADATA +12 -0
- browserwright-0.6.2.dist-info/RECORD +98 -0
- browserwright-0.6.2.dist-info/WHEEL +5 -0
- browserwright-0.6.2.dist-info/entry_points.txt +3 -0
- browserwright-0.6.2.dist-info/top_level.txt +1 -0
browserwright/cli.py
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
"""Top-level ``browserwright`` CLI dispatch.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
session new | reset | end | list | prune (P2: explicit session creation)
|
|
5
|
+
whoami --session=ID
|
|
6
|
+
task <site>/<name> [--arg=val ...] NOT IN v0.1 ENTRY: minimal stub
|
|
7
|
+
install
|
|
8
|
+
doctor
|
|
9
|
+
list-tasks [--site SITE]
|
|
10
|
+
index rebuild
|
|
11
|
+
memory show [--site SITE | --global]
|
|
12
|
+
version
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from . import __version__
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
HELP = """browserwright — Layer 2 of the browser stack.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
browserwright -s <session-id> -e 'page.goto("https://example.com"); print(page.title())'
|
|
30
|
+
browserwright -s <session-id> -f script.py
|
|
31
|
+
browserwright -s <session-id> --code-stdin < script.py
|
|
32
|
+
|
|
33
|
+
browserwright session new --backend=<extension|rdp> --name=SESSION_LABEL [--create | --attach=PORT]
|
|
34
|
+
browserwright session reset <id>
|
|
35
|
+
browserwright session end --session=ID
|
|
36
|
+
browserwright session list [--json]
|
|
37
|
+
browserwright session prune [--idle=SECONDS]
|
|
38
|
+
browserwright whoami --session=ID
|
|
39
|
+
browserwright userscript {push|list|remove|toggle|logs} ...
|
|
40
|
+
|
|
41
|
+
browserwright task <site>/<name> [--key=value ...] [--isolated]
|
|
42
|
+
browserwright list-tasks [--site SITE] [--query Q] [--json]
|
|
43
|
+
|
|
44
|
+
browserwright sub add <git-url> [--name NAME]
|
|
45
|
+
browserwright sub list [--json]
|
|
46
|
+
browserwright sub update [--name NAME]
|
|
47
|
+
browserwright sub remove --name NAME
|
|
48
|
+
browserwright release {install-local|status|list|activate} ...
|
|
49
|
+
|
|
50
|
+
browserwright install
|
|
51
|
+
browserwright doctor [--json]
|
|
52
|
+
browserwright index rebuild
|
|
53
|
+
browserwright memory show [--site SITE | --global]
|
|
54
|
+
browserwright memory forget --pattern PAT (--site SITE | --global) [--yes]
|
|
55
|
+
browserwright memory replace --pattern PAT --with 'TEXT' (--site SITE | --global) [--yes]
|
|
56
|
+
|
|
57
|
+
browserwright version [--json | check]
|
|
58
|
+
browserwright --print-skill (alias: print-skill)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _coerce(value: str) -> object:
|
|
63
|
+
# try JSON first so callers can pass numbers/lists/etc.
|
|
64
|
+
try:
|
|
65
|
+
return json.loads(value)
|
|
66
|
+
except (TypeError, ValueError):
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_kv_args(args: list[str]) -> dict:
|
|
71
|
+
out: dict[str, object] = {}
|
|
72
|
+
i, n = 0, len(args)
|
|
73
|
+
while i < n:
|
|
74
|
+
a = args[i]
|
|
75
|
+
if not a.startswith("--"):
|
|
76
|
+
i += 1
|
|
77
|
+
continue
|
|
78
|
+
key, eq, value = a[2:].partition("=")
|
|
79
|
+
if eq:
|
|
80
|
+
out[key] = _coerce(value)
|
|
81
|
+
elif i + 1 < n and not args[i + 1].startswith("--"):
|
|
82
|
+
# space form: `--query "hacker news"` (the form --help advertises).
|
|
83
|
+
# Consume the next token as the value unless it's another flag.
|
|
84
|
+
out[key] = _coerce(args[i + 1])
|
|
85
|
+
i += 1
|
|
86
|
+
else:
|
|
87
|
+
out[key] = True
|
|
88
|
+
i += 1
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse_execute_args(args: list[str]) -> tuple[Optional[str], Optional[str], Optional[str]]:
|
|
93
|
+
"""Parse playwriter-style execution flags.
|
|
94
|
+
|
|
95
|
+
Supports both short and long forms:
|
|
96
|
+
browserwright -s 1 -e 'print(snapshot())'
|
|
97
|
+
browserwright --session=1 --execute='print(snapshot())'
|
|
98
|
+
"""
|
|
99
|
+
session_id: Optional[str] = None
|
|
100
|
+
code: Optional[str] = None
|
|
101
|
+
code_file: Optional[str] = None
|
|
102
|
+
code_stdin = False
|
|
103
|
+
i, n = 0, len(args)
|
|
104
|
+
while i < n:
|
|
105
|
+
a = args[i]
|
|
106
|
+
if a in {"-s", "--session"}:
|
|
107
|
+
if i + 1 >= n:
|
|
108
|
+
return None, None, f"{a} requires a value"
|
|
109
|
+
session_id = args[i + 1]
|
|
110
|
+
i += 2
|
|
111
|
+
continue
|
|
112
|
+
if a.startswith("--session="):
|
|
113
|
+
session_id = a.split("=", 1)[1]
|
|
114
|
+
i += 1
|
|
115
|
+
continue
|
|
116
|
+
if a in {"-e", "--execute"}:
|
|
117
|
+
if i + 1 >= n:
|
|
118
|
+
return None, None, f"{a} requires a value"
|
|
119
|
+
if code is not None or code_file is not None or code_stdin:
|
|
120
|
+
return None, None, "pass only one of -e, -f, or --code-stdin"
|
|
121
|
+
value = args[i + 1]
|
|
122
|
+
if value == "-":
|
|
123
|
+
code_stdin = True
|
|
124
|
+
else:
|
|
125
|
+
code = value
|
|
126
|
+
i += 2
|
|
127
|
+
continue
|
|
128
|
+
if a.startswith("--execute="):
|
|
129
|
+
if code is not None or code_file is not None or code_stdin:
|
|
130
|
+
return None, None, "pass only one of -e, -f, or --code-stdin"
|
|
131
|
+
value = a.split("=", 1)[1]
|
|
132
|
+
if value == "-":
|
|
133
|
+
code_stdin = True
|
|
134
|
+
else:
|
|
135
|
+
code = value
|
|
136
|
+
i += 1
|
|
137
|
+
continue
|
|
138
|
+
if a in {"-f", "--code-file"}:
|
|
139
|
+
if i + 1 >= n:
|
|
140
|
+
return None, None, f"{a} requires a value"
|
|
141
|
+
if code is not None or code_file is not None or code_stdin:
|
|
142
|
+
return None, None, "pass only one of -e, -f, or --code-stdin"
|
|
143
|
+
code_file = args[i + 1]
|
|
144
|
+
i += 2
|
|
145
|
+
continue
|
|
146
|
+
if a.startswith("--code-file="):
|
|
147
|
+
if code is not None or code_file is not None or code_stdin:
|
|
148
|
+
return None, None, "pass only one of -e, -f, or --code-stdin"
|
|
149
|
+
code_file = a.split("=", 1)[1]
|
|
150
|
+
i += 1
|
|
151
|
+
continue
|
|
152
|
+
if a == "--code-stdin":
|
|
153
|
+
if code is not None or code_file is not None or code_stdin:
|
|
154
|
+
return None, None, "pass only one of -e, -f, or --code-stdin"
|
|
155
|
+
code_stdin = True
|
|
156
|
+
i += 1
|
|
157
|
+
continue
|
|
158
|
+
return None, None, f"unknown execute argument: {a!r}"
|
|
159
|
+
if not session_id:
|
|
160
|
+
return None, None, "missing session id: pass -s <id>"
|
|
161
|
+
if code_file is not None:
|
|
162
|
+
try:
|
|
163
|
+
code = Path(code_file).read_text()
|
|
164
|
+
except OSError as e:
|
|
165
|
+
return None, None, f"cannot read code file {code_file!r}: {e}"
|
|
166
|
+
elif code_stdin:
|
|
167
|
+
code = sys.stdin.read()
|
|
168
|
+
if code is None:
|
|
169
|
+
return None, None, "missing code: pass -e '<python>', -f <path>, or --code-stdin"
|
|
170
|
+
return session_id, code, None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _cmd_execute(args: list[str]) -> int:
|
|
174
|
+
session_id, code, err = _parse_execute_args(args)
|
|
175
|
+
if err:
|
|
176
|
+
print(f"usage error: {err}", file=sys.stderr)
|
|
177
|
+
print("usage: browserwright -s <session-id> "
|
|
178
|
+
"(-e 'print(snapshot())' | -f script.py | --code-stdin)",
|
|
179
|
+
file=sys.stderr)
|
|
180
|
+
return 1
|
|
181
|
+
from .repl import inline
|
|
182
|
+
return inline.run_code(code or "", session_id=session_id or "")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _cmd_task(args: list[str]) -> int:
|
|
186
|
+
if not args:
|
|
187
|
+
print("usage: browserwright task <site>/<name> [--key=val ...]", file=sys.stderr)
|
|
188
|
+
return 1
|
|
189
|
+
spec = args[0]
|
|
190
|
+
if "/" not in spec:
|
|
191
|
+
print("task spec must be <site>/<name>", file=sys.stderr)
|
|
192
|
+
return 1
|
|
193
|
+
site, name = spec.split("/", 1)
|
|
194
|
+
kwargs = _parse_kv_args(args[1:])
|
|
195
|
+
# JSON-args envelope for Layer 3 callers.
|
|
196
|
+
js = kwargs.pop("json-args", None)
|
|
197
|
+
if js is not None:
|
|
198
|
+
if isinstance(js, str):
|
|
199
|
+
kwargs.update(json.loads(js))
|
|
200
|
+
elif isinstance(js, dict):
|
|
201
|
+
kwargs.update(js)
|
|
202
|
+
from .task_runner import run_task
|
|
203
|
+
try:
|
|
204
|
+
result = run_task(site, name, **kwargs)
|
|
205
|
+
except FileNotFoundError as e:
|
|
206
|
+
print(f"task not found: {e}", file=sys.stderr)
|
|
207
|
+
return 1
|
|
208
|
+
except Exception as e: # noqa: BLE001
|
|
209
|
+
print(f"task crashed: {e!r}", file=sys.stderr)
|
|
210
|
+
return 3
|
|
211
|
+
if "--json-output" in args or kwargs.get("json_output"):
|
|
212
|
+
sys.stdout.write(json.dumps(result, default=str))
|
|
213
|
+
else:
|
|
214
|
+
sys.stdout.write(repr(result))
|
|
215
|
+
sys.stdout.write("\n")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _cmd_doctor(args: list[str]) -> int:
|
|
220
|
+
"""A4: a ``{status, message, fix}`` check table.
|
|
221
|
+
|
|
222
|
+
``doctor --json`` emits the machine form; default prints human-readable.
|
|
223
|
+
Every ``fail`` check carries a non-empty ``fix`` (enforced in
|
|
224
|
+
``doctor_checks``). Exits nonzero (CI-style) if any check fails.
|
|
225
|
+
"""
|
|
226
|
+
from .health import doctor_checks
|
|
227
|
+
|
|
228
|
+
info = doctor_checks()
|
|
229
|
+
info["skill_version"] = __version__
|
|
230
|
+
checks = info.get("checks", [])
|
|
231
|
+
any_fail = any(c.get("status") == "fail" for c in checks)
|
|
232
|
+
|
|
233
|
+
if "--json" in args:
|
|
234
|
+
sys.stdout.write(json.dumps(info, indent=2, default=str) + "\n")
|
|
235
|
+
return 1 if any_fail else 0
|
|
236
|
+
|
|
237
|
+
print(f"browserwright {__version__}")
|
|
238
|
+
print()
|
|
239
|
+
glyph = {"pass": "✓", "warn": "!", "fail": "✗"}
|
|
240
|
+
for c in checks:
|
|
241
|
+
status = c.get("status", "?")
|
|
242
|
+
mark = glyph.get(status, "?")
|
|
243
|
+
name = c.get("name", "?")
|
|
244
|
+
print(f" {mark} {name:14s} {c.get('message', '')}")
|
|
245
|
+
# Always surface the recovery action for non-pass checks so the
|
|
246
|
+
# human/agent reading the output has a next step.
|
|
247
|
+
if status != "pass" and c.get("fix"):
|
|
248
|
+
print(f" fix: {c['fix']}")
|
|
249
|
+
print()
|
|
250
|
+
if any_fail:
|
|
251
|
+
print("doctor: FAIL — address the fixes above.")
|
|
252
|
+
return 1
|
|
253
|
+
print("doctor: ok")
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _cmd_install(_: list[str]) -> int:
|
|
258
|
+
from . import install
|
|
259
|
+
return install.run()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _cmd_list_tasks(args: list[str]) -> int:
|
|
263
|
+
kwargs = _parse_kv_args(args)
|
|
264
|
+
from .discovery import list_tasks
|
|
265
|
+
tasks = list_tasks(site=kwargs.get("site"), query=kwargs.get("query"))
|
|
266
|
+
if kwargs.get("json"):
|
|
267
|
+
sys.stdout.write(json.dumps(tasks, default=str) + "\n")
|
|
268
|
+
return 0
|
|
269
|
+
if not tasks:
|
|
270
|
+
print("(no tasks found)")
|
|
271
|
+
return 0
|
|
272
|
+
for t in tasks:
|
|
273
|
+
print(f" {t['site']}/{t['name']} — {t.get('desc','')}")
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _cmd_index(args: list[str]) -> int:
|
|
278
|
+
if args and args[0] == "rebuild":
|
|
279
|
+
from .discovery import rebuild_index
|
|
280
|
+
out = rebuild_index()
|
|
281
|
+
sys.stdout.write(json.dumps({"sites": len(out.get("sites", []))}, default=str) + "\n")
|
|
282
|
+
return 0
|
|
283
|
+
print("usage: browserwright index rebuild", file=sys.stderr)
|
|
284
|
+
return 1
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _cmd_sub(args: list[str]) -> int:
|
|
288
|
+
"""``browserwright sub {add|list|update|remove} ...``."""
|
|
289
|
+
if not args:
|
|
290
|
+
print("usage: browserwright sub {add|list|update|remove} ...", file=sys.stderr)
|
|
291
|
+
return 1
|
|
292
|
+
sub = args[0]
|
|
293
|
+
rest = args[1:]
|
|
294
|
+
from . import subscriptions
|
|
295
|
+
|
|
296
|
+
if sub == "add":
|
|
297
|
+
if not rest or rest[0].startswith("--"):
|
|
298
|
+
print("usage: browserwright sub add <git-url> [--name NAME]", file=sys.stderr)
|
|
299
|
+
return 1
|
|
300
|
+
url = rest[0]
|
|
301
|
+
kw = _parse_kv_args(rest[1:])
|
|
302
|
+
try:
|
|
303
|
+
r = subscriptions.add(url, name=kw.get("name"))
|
|
304
|
+
except subscriptions.SubscriptionError as e:
|
|
305
|
+
print(f"sub add failed: {e}", file=sys.stderr)
|
|
306
|
+
return 1
|
|
307
|
+
if kw.get("json"):
|
|
308
|
+
sys.stdout.write(json.dumps(r, default=str) + "\n")
|
|
309
|
+
else:
|
|
310
|
+
print(f"{r['status']}: {r['name']} → {r['path']}")
|
|
311
|
+
return 0
|
|
312
|
+
|
|
313
|
+
if sub == "list":
|
|
314
|
+
kw = _parse_kv_args(rest)
|
|
315
|
+
rows = subscriptions.list_all()
|
|
316
|
+
if kw.get("json"):
|
|
317
|
+
sys.stdout.write(json.dumps(rows, indent=2, default=str) + "\n")
|
|
318
|
+
return 0
|
|
319
|
+
if not rows:
|
|
320
|
+
print("(no subscriptions)")
|
|
321
|
+
return 0
|
|
322
|
+
for r in rows:
|
|
323
|
+
tag = "" if r["exists"] else " [MISSING]"
|
|
324
|
+
print(f" {r['name']:24s} {r['url']}{tag}")
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
if sub == "update":
|
|
328
|
+
kw = _parse_kv_args(rest)
|
|
329
|
+
names = [kw["name"]] if kw.get("name") else None
|
|
330
|
+
try:
|
|
331
|
+
results = subscriptions.update(names)
|
|
332
|
+
except subscriptions.SubscriptionError as e:
|
|
333
|
+
print(f"sub update failed: {e}", file=sys.stderr)
|
|
334
|
+
return 1
|
|
335
|
+
for r in results:
|
|
336
|
+
print(f" {r['name']:24s} {r['status']}: {r.get('detail','')}")
|
|
337
|
+
return 0 if all(r["status"] in ("updated", "missing") for r in results) else 1
|
|
338
|
+
|
|
339
|
+
if sub == "remove":
|
|
340
|
+
kw = _parse_kv_args(rest)
|
|
341
|
+
name = kw.get("name")
|
|
342
|
+
if not name:
|
|
343
|
+
print("usage: browserwright sub remove --name NAME", file=sys.stderr)
|
|
344
|
+
return 1
|
|
345
|
+
try:
|
|
346
|
+
subscriptions.remove(name)
|
|
347
|
+
except subscriptions.SubscriptionError as e:
|
|
348
|
+
print(f"sub remove failed: {e}", file=sys.stderr)
|
|
349
|
+
return 1
|
|
350
|
+
print(f"removed {name}")
|
|
351
|
+
return 0
|
|
352
|
+
|
|
353
|
+
print(f"unknown sub subcommand: {sub}", file=sys.stderr)
|
|
354
|
+
return 1
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _cmd_memory(args: list[str]) -> int:
|
|
358
|
+
if not args:
|
|
359
|
+
print("usage: browserwright memory {show|forget|replace} ...", file=sys.stderr)
|
|
360
|
+
return 1
|
|
361
|
+
sub = args[0]
|
|
362
|
+
rest = args[1:]
|
|
363
|
+
kwargs = _parse_kv_args(rest)
|
|
364
|
+
from .memory import global_memory, site_memory
|
|
365
|
+
|
|
366
|
+
if sub == "show":
|
|
367
|
+
if kwargs.get("global"):
|
|
368
|
+
out = global_memory().read()
|
|
369
|
+
sys.stdout.write(json.dumps(out, indent=2, default=str) + "\n")
|
|
370
|
+
return 0
|
|
371
|
+
site = kwargs.get("site")
|
|
372
|
+
if not site:
|
|
373
|
+
print("specify --site=SITE or --global", file=sys.stderr)
|
|
374
|
+
return 1
|
|
375
|
+
sys.stdout.write(json.dumps(site_memory(site).read(),
|
|
376
|
+
indent=2, default=str) + "\n")
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
if sub == "forget":
|
|
380
|
+
pattern = kwargs.get("pattern")
|
|
381
|
+
if not pattern:
|
|
382
|
+
print("usage: memory forget --pattern=PAT (--site=SITE | --global) [--yes]",
|
|
383
|
+
file=sys.stderr)
|
|
384
|
+
return 1
|
|
385
|
+
target_global = bool(kwargs.get("global"))
|
|
386
|
+
site = kwargs.get("site")
|
|
387
|
+
if not target_global and not site:
|
|
388
|
+
print("specify --site=SITE or --global", file=sys.stderr)
|
|
389
|
+
return 1
|
|
390
|
+
mem = global_memory() if target_global else site_memory(site)
|
|
391
|
+
matches = mem.forget(pattern, confirm=True)
|
|
392
|
+
if not matches:
|
|
393
|
+
print("(no matching bullets)")
|
|
394
|
+
return 0
|
|
395
|
+
print(f"would remove {len(matches)} line(s):")
|
|
396
|
+
for ln in matches:
|
|
397
|
+
print(f" {ln}")
|
|
398
|
+
if not kwargs.get("yes"):
|
|
399
|
+
print("\nrun again with --yes to confirm.")
|
|
400
|
+
return 0
|
|
401
|
+
removed = mem.forget(pattern, confirm=False)
|
|
402
|
+
print(f"removed {len(removed)} line(s).")
|
|
403
|
+
return 0
|
|
404
|
+
|
|
405
|
+
if sub == "replace":
|
|
406
|
+
pattern = kwargs.get("pattern")
|
|
407
|
+
replacement = kwargs.get("with")
|
|
408
|
+
if not pattern or not replacement:
|
|
409
|
+
print("usage: memory replace --pattern=PAT --with='new line' "
|
|
410
|
+
"(--site=SITE | --global) [--yes]", file=sys.stderr)
|
|
411
|
+
return 1
|
|
412
|
+
target_global = bool(kwargs.get("global"))
|
|
413
|
+
site = kwargs.get("site")
|
|
414
|
+
if not target_global and not site:
|
|
415
|
+
print("specify --site=SITE or --global", file=sys.stderr)
|
|
416
|
+
return 1
|
|
417
|
+
mem = global_memory() if target_global else site_memory(site)
|
|
418
|
+
matches = mem.forget(pattern, confirm=True)
|
|
419
|
+
if not matches:
|
|
420
|
+
print("(no matching bullets)")
|
|
421
|
+
return 0
|
|
422
|
+
print(f"would remove {len(matches)} line(s) and append: - {replacement}")
|
|
423
|
+
for ln in matches:
|
|
424
|
+
print(f" {ln}")
|
|
425
|
+
if not kwargs.get("yes"):
|
|
426
|
+
print("\nrun again with --yes to confirm.")
|
|
427
|
+
return 0
|
|
428
|
+
mem.forget(pattern, confirm=False)
|
|
429
|
+
mem.append(replacement)
|
|
430
|
+
print("replaced.")
|
|
431
|
+
return 0
|
|
432
|
+
|
|
433
|
+
print(f"unknown memory subcommand: {sub}", file=sys.stderr)
|
|
434
|
+
return 1
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _cmd_session(args: list[str]) -> int:
|
|
438
|
+
"""``browserwright session {new|reset|end|list|prune} ...`` (P2)."""
|
|
439
|
+
from . import session_create
|
|
440
|
+
from . import session_registry as reg
|
|
441
|
+
|
|
442
|
+
if not args:
|
|
443
|
+
print("usage: browserwright session {new|reset|end|list|prune} ...", file=sys.stderr)
|
|
444
|
+
return 1
|
|
445
|
+
sub = args[0]
|
|
446
|
+
kw = _parse_kv_args(args[1:])
|
|
447
|
+
|
|
448
|
+
if sub == "new":
|
|
449
|
+
backend = kw.get("backend")
|
|
450
|
+
if backend not in ("extension", "rdp"):
|
|
451
|
+
print("usage: browserwright session new --backend=<extension|rdp> "
|
|
452
|
+
"--name=SESSION_LABEL [--create | --attach=PORT]", file=sys.stderr)
|
|
453
|
+
print("--name is a short task-specific session label. Extension sessions "
|
|
454
|
+
"use it as the Chrome tab group title; RDP sessions use it only "
|
|
455
|
+
"to label the isolated browser session.",
|
|
456
|
+
file=sys.stderr)
|
|
457
|
+
return 1
|
|
458
|
+
try:
|
|
459
|
+
sid = session_create.new(
|
|
460
|
+
backend=backend, create=bool(kw.get("create")),
|
|
461
|
+
attach=kw.get("attach"), name=kw.get("name"),
|
|
462
|
+
)
|
|
463
|
+
except ValueError as e:
|
|
464
|
+
print(str(e), file=sys.stderr)
|
|
465
|
+
return 1
|
|
466
|
+
print(sid) # token-frugal: bare id
|
|
467
|
+
return 0
|
|
468
|
+
|
|
469
|
+
if sub == "end":
|
|
470
|
+
from .errors import NoSession
|
|
471
|
+
from .session_ctx import resolve_session
|
|
472
|
+
try:
|
|
473
|
+
rec = resolve_session(kw.get("session"))
|
|
474
|
+
except NoSession as e:
|
|
475
|
+
print(str(e), file=sys.stderr)
|
|
476
|
+
return e.exit_code
|
|
477
|
+
print(session_create.end(rec))
|
|
478
|
+
return 0
|
|
479
|
+
|
|
480
|
+
if sub == "reset":
|
|
481
|
+
from .errors import NoSession
|
|
482
|
+
from .session_ctx import resolve_session
|
|
483
|
+
raw_sid = args[1] if len(args) > 1 and not args[1].startswith("--") else kw.get("session")
|
|
484
|
+
try:
|
|
485
|
+
rec = resolve_session(raw_sid)
|
|
486
|
+
except NoSession as e:
|
|
487
|
+
print(str(e), file=sys.stderr)
|
|
488
|
+
return e.exit_code
|
|
489
|
+
print(session_create.reset_executor(rec))
|
|
490
|
+
return 0
|
|
491
|
+
|
|
492
|
+
if sub == "list":
|
|
493
|
+
rows = reg.list_all()
|
|
494
|
+
if kw.get("json"):
|
|
495
|
+
sys.stdout.write(json.dumps(rows, indent=2, default=str) + "\n")
|
|
496
|
+
return 0
|
|
497
|
+
if not rows:
|
|
498
|
+
print("(no sessions)")
|
|
499
|
+
return 0
|
|
500
|
+
for r in rows:
|
|
501
|
+
print(f" {r['id']:>3} {r['backend']:9s} {r['owner']:6s} "
|
|
502
|
+
f"{(r.get('name') or '-'):16s}")
|
|
503
|
+
return 0
|
|
504
|
+
|
|
505
|
+
if sub == "prune":
|
|
506
|
+
idle = kw.get("idle", 3600)
|
|
507
|
+
pruned = session_create.reap(idle_seconds=float(idle))
|
|
508
|
+
print(f"pruned {len(pruned)} idle session(s).")
|
|
509
|
+
return 0
|
|
510
|
+
|
|
511
|
+
print(f"unknown session subcommand: {sub}", file=sys.stderr)
|
|
512
|
+
return 1
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _cmd_userscript(args: list[str]) -> int:
|
|
516
|
+
# ``--verify`` is a browserwright-level convenience on ``push``: after a
|
|
517
|
+
# successful push, reload the live tab and screenshot it so the agent sees
|
|
518
|
+
# the effect in one step instead of the manual push→reload→screenshot
|
|
519
|
+
# ritual. It is NOT a daemon flag, so strip it before delegating.
|
|
520
|
+
verify = "--verify" in args
|
|
521
|
+
fwd = [a for a in args if a != "--verify"]
|
|
522
|
+
|
|
523
|
+
result = subprocess.run(["browserwright-daemon", "userscript", *fwd])
|
|
524
|
+
if result.returncode != 0:
|
|
525
|
+
# Push failed — don't reload/screenshot a stale state. Surface the
|
|
526
|
+
# push failure so the agent fixes the script first.
|
|
527
|
+
return result.returncode
|
|
528
|
+
|
|
529
|
+
if verify and fwd and fwd[0] in ("push", "install"):
|
|
530
|
+
# Push succeeded; reload the live tab (reload() waits for load) and
|
|
531
|
+
# screenshot so the agent sees the effect in one step. This is a
|
|
532
|
+
# convenience: if there's no drivable session/tab (e.g. run outside a
|
|
533
|
+
# session), report that the push still SUCCEEDED rather than letting an
|
|
534
|
+
# opaque reload error look like a push failure.
|
|
535
|
+
try:
|
|
536
|
+
# These are internal driving helpers (no longer on the agent
|
|
537
|
+
# EXPORTS surface — Phase C PR3); the userscript --verify
|
|
538
|
+
# convenience still uses them directly from the primitive modules.
|
|
539
|
+
from .primitives.inspect import capture_screenshot
|
|
540
|
+
from .primitives.page import reload
|
|
541
|
+
|
|
542
|
+
reload()
|
|
543
|
+
print(capture_screenshot())
|
|
544
|
+
except Exception as e:
|
|
545
|
+
print(f"pushed OK — --verify skipped (no drivable tab): {e}",
|
|
546
|
+
file=sys.stderr)
|
|
547
|
+
|
|
548
|
+
return result.returncode
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _cmd_whoami(args: list[str]) -> int:
|
|
552
|
+
"""``browserwright whoami --session=ID`` — the ledger view of a session.
|
|
553
|
+
|
|
554
|
+
Live-browser fields (group/tab count/sample URL) are filled by a daemon
|
|
555
|
+
round-trip in Phase 5/6; for now we print only ledger-known fields.
|
|
556
|
+
"""
|
|
557
|
+
from .errors import NoSession
|
|
558
|
+
from .session_ctx import resolve_session
|
|
559
|
+
|
|
560
|
+
kw = _parse_kv_args(args)
|
|
561
|
+
try:
|
|
562
|
+
rec = resolve_session(kw.get("session"))
|
|
563
|
+
except NoSession as e:
|
|
564
|
+
print(str(e), file=sys.stderr)
|
|
565
|
+
return e.exit_code
|
|
566
|
+
view = {k: rec.get(k) for k in ("id", "backend", "owner", "name")}
|
|
567
|
+
sys.stdout.write(json.dumps(view, default=str) + "\n")
|
|
568
|
+
return 0
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _cmd_print_skill(_: list[str]) -> int:
|
|
572
|
+
"""D1: emit the agent-facing skill doc assembled from the running code.
|
|
573
|
+
|
|
574
|
+
The version stamp and primitive surface are generated at runtime from
|
|
575
|
+
``browserwright.__version__`` and ``browserwright.EXPORTS`` respectively,
|
|
576
|
+
so the printed instructions can never silently drift from the installed
|
|
577
|
+
binary.
|
|
578
|
+
"""
|
|
579
|
+
from . import skill_doc
|
|
580
|
+
|
|
581
|
+
sys.stdout.write(skill_doc.render())
|
|
582
|
+
return 0
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _cmd_version(args: list[str]) -> int:
|
|
586
|
+
from .version import version_info
|
|
587
|
+
|
|
588
|
+
info = version_info()
|
|
589
|
+
if args and args[0] == "check":
|
|
590
|
+
if "--json" in args:
|
|
591
|
+
sys.stdout.write(json.dumps(info, sort_keys=True) + "\n")
|
|
592
|
+
elif info["ok"]:
|
|
593
|
+
print(f"browserwright {info['version']} (versions ok)")
|
|
594
|
+
else:
|
|
595
|
+
for issue in info["issues"]:
|
|
596
|
+
print(f"{issue['code']}: {issue['message']}", file=sys.stderr)
|
|
597
|
+
return 0 if info["ok"] else 1
|
|
598
|
+
if args and args[0] == "--json":
|
|
599
|
+
sys.stdout.write(json.dumps(info, sort_keys=True) + "\n")
|
|
600
|
+
return 0
|
|
601
|
+
print(__version__)
|
|
602
|
+
return 0
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _cmd_release(args: list[str]) -> int:
|
|
606
|
+
if not args:
|
|
607
|
+
print(
|
|
608
|
+
"usage: browserwright release {install-local|status|list|activate} ...",
|
|
609
|
+
file=sys.stderr,
|
|
610
|
+
)
|
|
611
|
+
return 1
|
|
612
|
+
from . import release_install
|
|
613
|
+
|
|
614
|
+
sub = args[0]
|
|
615
|
+
rest = args[1:]
|
|
616
|
+
kw = _parse_kv_args(rest)
|
|
617
|
+
try:
|
|
618
|
+
if sub == "install-local":
|
|
619
|
+
info = release_install.install_local(
|
|
620
|
+
force=bool(kw.get("force")),
|
|
621
|
+
activate_release=not bool(kw.get("no-activate")),
|
|
622
|
+
)
|
|
623
|
+
if kw.get("restart-daemon") and info["actions"].get("restart_daemon"):
|
|
624
|
+
subprocess.run(["browserwright-daemon", "restart"], check=False)
|
|
625
|
+
if kw.get("json"):
|
|
626
|
+
sys.stdout.write(json.dumps(info, indent=2, sort_keys=True) + "\n")
|
|
627
|
+
else:
|
|
628
|
+
print(f"installed browserwright {info['version']} -> {info['path']}")
|
|
629
|
+
if info.get("activated"):
|
|
630
|
+
print("activated global entry points")
|
|
631
|
+
if info["actions"].get("restart_daemon"):
|
|
632
|
+
print("next: restart daemon (`browserwright-daemon restart`)")
|
|
633
|
+
if info["actions"].get("reload_chrome_extension"):
|
|
634
|
+
print(
|
|
635
|
+
"next: reload Chrome unpacked extension from "
|
|
636
|
+
f"{info.get('chrome_extension_sync', {}).get('path') or info['path'] + '/chrome-extension'}"
|
|
637
|
+
)
|
|
638
|
+
return 0
|
|
639
|
+
|
|
640
|
+
if sub == "status":
|
|
641
|
+
info = release_install.status()
|
|
642
|
+
if kw.get("json"):
|
|
643
|
+
sys.stdout.write(json.dumps(info, indent=2, sort_keys=True) + "\n")
|
|
644
|
+
return 0
|
|
645
|
+
version = info.get("installed_version") or "(none)"
|
|
646
|
+
print(f"installed release: {version}")
|
|
647
|
+
daemon = info.get("daemon") or {}
|
|
648
|
+
daemon_version = daemon.get("version") or "(not running)"
|
|
649
|
+
suffix = " restart required" if daemon.get("restart_required") else ""
|
|
650
|
+
print(f"running daemon: {daemon_version}{suffix}")
|
|
651
|
+
ok = all(row.get("ok") for row in info.get("skill", []))
|
|
652
|
+
print(f"skill install: {'copied ok' if ok else 'needs reinstall'}")
|
|
653
|
+
return 0
|
|
654
|
+
|
|
655
|
+
if sub == "list":
|
|
656
|
+
rows = release_install.list_releases()
|
|
657
|
+
if kw.get("json"):
|
|
658
|
+
sys.stdout.write(json.dumps(rows, indent=2, sort_keys=True) + "\n")
|
|
659
|
+
return 0
|
|
660
|
+
if not rows:
|
|
661
|
+
print("(no releases installed)")
|
|
662
|
+
return 0
|
|
663
|
+
for row in rows:
|
|
664
|
+
mark = "*" if row.get("active") else " "
|
|
665
|
+
print(f"{mark} {row.get('version')} {row.get('path')}")
|
|
666
|
+
return 0
|
|
667
|
+
|
|
668
|
+
if sub == "activate":
|
|
669
|
+
if not rest or rest[0].startswith("--"):
|
|
670
|
+
print("usage: browserwright release activate <version>", file=sys.stderr)
|
|
671
|
+
return 1
|
|
672
|
+
info = release_install.activate(rest[0])
|
|
673
|
+
if kw.get("json"):
|
|
674
|
+
sys.stdout.write(json.dumps(info, indent=2, sort_keys=True) + "\n")
|
|
675
|
+
else:
|
|
676
|
+
print(f"activated browserwright {info['version']} -> {info['path']}")
|
|
677
|
+
print("next: restart daemon and reload Chrome extension if that release differs")
|
|
678
|
+
return 0
|
|
679
|
+
except release_install.ReleaseError as e:
|
|
680
|
+
print(f"release error: {e}", file=sys.stderr)
|
|
681
|
+
return 1
|
|
682
|
+
|
|
683
|
+
print(f"unknown release subcommand: {sub}", file=sys.stderr)
|
|
684
|
+
return 1
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def main(argv: Optional[list[str]] = None) -> None:
|
|
688
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
689
|
+
|
|
690
|
+
# `--print-skill` is a flag (leading dash) but is a real command, not help;
|
|
691
|
+
# intercept it before the help check below.
|
|
692
|
+
if argv and argv[0] in {"--print-skill", "print-skill"}:
|
|
693
|
+
sys.exit(_cmd_print_skill(argv[1:]))
|
|
694
|
+
|
|
695
|
+
if not argv and not sys.stdin.isatty():
|
|
696
|
+
print("usage: browserwright -s <session-id> -e 'print(snapshot())'",
|
|
697
|
+
file=sys.stderr)
|
|
698
|
+
sys.exit(1)
|
|
699
|
+
|
|
700
|
+
if not argv or argv[0] in {"-h", "--help"}:
|
|
701
|
+
sys.stdout.write(HELP)
|
|
702
|
+
sys.exit(0 if argv else 1)
|
|
703
|
+
|
|
704
|
+
if argv and (
|
|
705
|
+
argv[0] in {"-s", "--session", "-e", "--execute"}
|
|
706
|
+
or argv[0].startswith("--session=")
|
|
707
|
+
or argv[0].startswith("--execute=")
|
|
708
|
+
):
|
|
709
|
+
sys.exit(_cmd_execute(argv))
|
|
710
|
+
|
|
711
|
+
cmd = argv[0]
|
|
712
|
+
rest = argv[1:]
|
|
713
|
+
|
|
714
|
+
if cmd in {"--version", "version"}:
|
|
715
|
+
sys.exit(_cmd_version(rest))
|
|
716
|
+
if cmd == "task":
|
|
717
|
+
sys.exit(_cmd_task(rest))
|
|
718
|
+
if cmd == "doctor":
|
|
719
|
+
sys.exit(_cmd_doctor(rest))
|
|
720
|
+
if cmd == "install":
|
|
721
|
+
sys.exit(_cmd_install(rest))
|
|
722
|
+
if cmd == "list-tasks":
|
|
723
|
+
sys.exit(_cmd_list_tasks(rest))
|
|
724
|
+
if cmd == "index":
|
|
725
|
+
sys.exit(_cmd_index(rest))
|
|
726
|
+
if cmd == "memory":
|
|
727
|
+
sys.exit(_cmd_memory(rest))
|
|
728
|
+
if cmd == "sub":
|
|
729
|
+
sys.exit(_cmd_sub(rest))
|
|
730
|
+
if cmd == "release":
|
|
731
|
+
sys.exit(_cmd_release(rest))
|
|
732
|
+
if cmd == "session":
|
|
733
|
+
sys.exit(_cmd_session(rest))
|
|
734
|
+
if cmd == "whoami":
|
|
735
|
+
sys.exit(_cmd_whoami(rest))
|
|
736
|
+
if cmd == "userscript":
|
|
737
|
+
sys.exit(_cmd_userscript(rest))
|
|
738
|
+
|
|
739
|
+
print(f"unknown command: {cmd!r}", file=sys.stderr)
|
|
740
|
+
print(HELP, file=sys.stderr)
|
|
741
|
+
sys.exit(1)
|