patchright-cli 0.5.0__py3-none-any.whl → 0.6.0__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.
- patchright_cli/__init__.py +1 -1
- patchright_cli/_skills/patchright-cli/SKILL.md +25 -23
- patchright_cli/cli.py +26 -7
- patchright_cli/daemon.py +304 -16
- {patchright_cli-0.5.0.dist-info → patchright_cli-0.6.0.dist-info}/METADATA +25 -58
- {patchright_cli-0.5.0.dist-info → patchright_cli-0.6.0.dist-info}/RECORD +9 -9
- {patchright_cli-0.5.0.dist-info → patchright_cli-0.6.0.dist-info}/WHEEL +0 -0
- {patchright_cli-0.5.0.dist-info → patchright_cli-0.6.0.dist-info}/entry_points.txt +0 -0
- {patchright_cli-0.5.0.dist-info → patchright_cli-0.6.0.dist-info}/licenses/LICENSE +0 -0
patchright_cli/__init__.py
CHANGED
|
@@ -58,6 +58,8 @@ These go before the command:
|
|
|
58
58
|
--timeout-action=10000 # Default action timeout (ms)
|
|
59
59
|
--timeout-navigation=30000 # Default navigation timeout (ms)
|
|
60
60
|
--show-port=9322 # Dashboard port (default: 9322)
|
|
61
|
+
--raw # Strip page/snapshot decorations from output
|
|
62
|
+
--json # Wrap full response as JSON ({success, output, ...})
|
|
61
63
|
```
|
|
62
64
|
|
|
63
65
|
---
|
|
@@ -71,7 +73,8 @@ open [url] # Launch browser (optionally navigate)
|
|
|
71
73
|
open --persistent # Keep cookies/storage between runs
|
|
72
74
|
open --headless # Headless mode
|
|
73
75
|
attach --cdp=<url> # Attach to existing Chrome via CDP
|
|
74
|
-
close # Close session
|
|
76
|
+
close # Close session (closes the browser)
|
|
77
|
+
detach # Detach an attached session (keeps external browser running)
|
|
75
78
|
```
|
|
76
79
|
|
|
77
80
|
### Navigation
|
|
@@ -91,6 +94,7 @@ snapshot # Full page snapshot
|
|
|
91
94
|
snapshot <ref> # Subtree of a specific element
|
|
92
95
|
snapshot --depth=N # Limit depth
|
|
93
96
|
snapshot -i # Interactive elements only
|
|
97
|
+
snapshot --boxes # Append [box=x,y,w,h] to each [ref=eN] line
|
|
94
98
|
```
|
|
95
99
|
|
|
96
100
|
### Interaction
|
|
@@ -102,6 +106,8 @@ click <ref> --modifiers=Alt # Click with modifier keys
|
|
|
102
106
|
dblclick <ref> # Double-click
|
|
103
107
|
hover <ref> # Hover
|
|
104
108
|
drag <ref> <target-ref> # Drag element to target
|
|
109
|
+
drop <ref> --path=./file.png # Drop file onto element (HTML5 drop from outside)
|
|
110
|
+
drop <ref> --data="text/plain=hello" # Drop arbitrary MIME data
|
|
105
111
|
```
|
|
106
112
|
|
|
107
113
|
### Form input
|
|
@@ -170,8 +176,14 @@ dialog-dismiss # Dismiss next dialog
|
|
|
170
176
|
```bash
|
|
171
177
|
console # Show console messages
|
|
172
178
|
console warning # Filter by level
|
|
173
|
-
network # Show network requests
|
|
179
|
+
network # Show network requests (each line is `#<id> METHOD STATUS URL`)
|
|
180
|
+
requests # Alias for `network`
|
|
174
181
|
network --static # Include static assets
|
|
182
|
+
request <id> # Full detail for one request (headers + body)
|
|
183
|
+
request <id> --body # Also fetch response body
|
|
184
|
+
generate-locator <ref> # Emit Playwright locator string for a ref
|
|
185
|
+
highlight <ref> # Persistent overlay on element [--style=CSS]
|
|
186
|
+
highlight <ref> --hide # Remove overlay; `highlight --hide` clears all
|
|
175
187
|
```
|
|
176
188
|
|
|
177
189
|
### Recording and capture
|
|
@@ -312,30 +324,20 @@ pip install patchright-cli
|
|
|
312
324
|
|
|
313
325
|
### Skill (for AI coding agents)
|
|
314
326
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
| Agent | Install / update command |
|
|
320
|
-
|-------|--------------------------|
|
|
321
|
-
| **Claude Code** | `mkdir -p ~/.claude/skills/patchright-cli && curl -sL https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md -o ~/.claude/skills/patchright-cli/SKILL.md` |
|
|
322
|
-
| **Gemini CLI** | `mkdir -p ~/.gemini/skills/patchright-cli && curl -sL https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md -o ~/.gemini/skills/patchright-cli/SKILL.md` |
|
|
323
|
-
| **Codex CLI** | `mkdir -p ~/.codex/skills/patchright-cli && curl -sL https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md -o ~/.codex/skills/patchright-cli/SKILL.md` |
|
|
324
|
-
| **OpenCode** | `mkdir -p ~/.opencode/skills/patchright-cli && curl -sL https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md -o ~/.opencode/skills/patchright-cli/SKILL.md` |
|
|
325
|
-
|
|
326
|
-
**Windows (PowerShell):**
|
|
327
|
+
```bash
|
|
328
|
+
# Install for all detected agents (Claude Code, Cursor, Gemini, Codex, 40+ more)
|
|
329
|
+
npx skills add AhaiMk01/patchright-cli
|
|
330
|
+
```
|
|
327
331
|
|
|
328
|
-
|
|
329
|
-
|-------|--------------------------|
|
|
330
|
-
| **Claude Code** | `New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.claude\skills\patchright-cli" | Out-Null; Invoke-WebRequest -Uri "https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md" -OutFile "$env:USERPROFILE\.claude\skills\patchright-cli\SKILL.md"` |
|
|
331
|
-
| **Gemini CLI** | `New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini\skills\patchright-cli" | Out-Null; Invoke-WebRequest -Uri "https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md" -OutFile "$env:USERPROFILE\.gemini\skills\patchright-cli\SKILL.md"` |
|
|
332
|
-
| **Codex CLI** | `New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.codex\skills\patchright-cli" | Out-Null; Invoke-WebRequest -Uri "https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md" -OutFile "$env:USERPROFILE\.codex\skills\patchright-cli\SKILL.md"` |
|
|
333
|
-
| **OpenCode** | `New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.opencode\skills\patchright-cli" | Out-Null; Invoke-WebRequest -Uri "https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md" -OutFile "$env:USERPROFILE\.opencode\skills\patchright-cli\SKILL.md"` |
|
|
332
|
+
Other methods:
|
|
334
333
|
|
|
335
|
-
|
|
334
|
+
```bash
|
|
335
|
+
# Via patchright-cli itself
|
|
336
|
+
pip install patchright-cli && patchright-cli install --skills
|
|
336
337
|
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
# Or just tell your agent:
|
|
339
|
+
# "Install patchright-cli skill from https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md"
|
|
340
|
+
```
|
|
339
341
|
|
|
340
342
|
## Specific tasks
|
|
341
343
|
|
patchright_cli/cli.py
CHANGED
|
@@ -180,12 +180,14 @@ COMMANDS_HELP = {
|
|
|
180
180
|
"select": "select <ref> <value> Select dropdown option",
|
|
181
181
|
"check": "check <ref> Check checkbox/radio",
|
|
182
182
|
"uncheck": "uncheck <ref> Uncheck checkbox/radio",
|
|
183
|
-
"snapshot": "snapshot [ref] Take accessibility snapshot [--filename=F] [--depth=N] [-i]",
|
|
183
|
+
"snapshot": "snapshot [ref] Take accessibility snapshot [--filename=F] [--depth=N] [-i] [--boxes]",
|
|
184
184
|
"eval": "eval <expr> [ref] Evaluate JavaScript [--file=F or stdin]",
|
|
185
185
|
"text": "text <ref|selector> Get text content of element",
|
|
186
186
|
"screenshot": "screenshot [ref] Save screenshot [--full-page] [--filename=F]",
|
|
187
187
|
"drag": "drag <from> <to> Drag element to target",
|
|
188
|
+
"drop": "drop <ref> [--path=F] [--data=mime=value] Drop file/data onto element",
|
|
188
189
|
"close": "close Close browser session",
|
|
190
|
+
"detach": "detach Detach attached session (keep external browser running)",
|
|
189
191
|
# Navigation
|
|
190
192
|
"go-back": "go-back Go back",
|
|
191
193
|
"go-forward": "go-forward Go forward",
|
|
@@ -260,6 +262,10 @@ COMMANDS_HELP = {
|
|
|
260
262
|
# DevTools
|
|
261
263
|
"console": "console [level] Show console messages [--clear]",
|
|
262
264
|
"network": "network Show network requests [--static] [--clear]",
|
|
265
|
+
"requests": "requests Alias for `network`",
|
|
266
|
+
"request": "request <id> Show full detail for a network request [--body]",
|
|
267
|
+
"generate-locator": "generate-locator <ref> Emit Playwright locator expression for a ref",
|
|
268
|
+
"highlight": "highlight <ref> Show overlay on element [--style=CSS] [--hide]",
|
|
263
269
|
# Session
|
|
264
270
|
"list": "list List sessions",
|
|
265
271
|
"close-all": "close-all Close all sessions",
|
|
@@ -300,6 +306,7 @@ def _print_help():
|
|
|
300
306
|
click.echo(" --cdp=<url> Attach to Chrome via CDP endpoint")
|
|
301
307
|
click.echo(" --show-port=N Dashboard port (default: 9322)")
|
|
302
308
|
click.echo(" --raw Raw output (strip page/snapshot decorations)")
|
|
309
|
+
click.echo(" --json Wrap response as JSON ({success, output, ...})")
|
|
303
310
|
click.echo(" --version Show version")
|
|
304
311
|
click.echo(" --help Show this help\n")
|
|
305
312
|
click.echo("Commands:")
|
|
@@ -324,7 +331,9 @@ def _print_help():
|
|
|
324
331
|
"text",
|
|
325
332
|
"screenshot",
|
|
326
333
|
"drag",
|
|
334
|
+
"drop",
|
|
327
335
|
"close",
|
|
336
|
+
"detach",
|
|
328
337
|
],
|
|
329
338
|
),
|
|
330
339
|
("Navigation", ["go-back", "go-forward", "reload", "url", "title"]),
|
|
@@ -363,7 +372,7 @@ def _print_help():
|
|
|
363
372
|
("Tracing", ["tracing-start", "tracing-stop"]),
|
|
364
373
|
("Video", ["video-start", "video-stop", "video-chapter"]),
|
|
365
374
|
("PDF", ["pdf"]),
|
|
366
|
-
("DevTools", ["console", "network"]),
|
|
375
|
+
("DevTools", ["console", "network", "requests", "request", "generate-locator", "highlight"]),
|
|
367
376
|
("Session", ["list", "close-all", "kill-all", "delete-data"]),
|
|
368
377
|
("Dashboard", ["show"]),
|
|
369
378
|
("Codegen", ["codegen", "codegen-stop"]),
|
|
@@ -390,6 +399,7 @@ def main():
|
|
|
390
399
|
port = DEFAULT_PORT
|
|
391
400
|
extra_opts: dict = {}
|
|
392
401
|
raw = False
|
|
402
|
+
as_json = False
|
|
393
403
|
|
|
394
404
|
# Extract options
|
|
395
405
|
remaining = []
|
|
@@ -484,6 +494,8 @@ def main():
|
|
|
484
494
|
extra_opts["show-port"] = argv[i]
|
|
485
495
|
elif arg == "--raw":
|
|
486
496
|
raw = True
|
|
497
|
+
elif arg == "--json":
|
|
498
|
+
as_json = True
|
|
487
499
|
elif arg in ("-i", "--interactive"):
|
|
488
500
|
extra_opts["interactive"] = True
|
|
489
501
|
elif arg in ("--version", "-v"):
|
|
@@ -579,13 +591,20 @@ def main():
|
|
|
579
591
|
|
|
580
592
|
# Print output
|
|
581
593
|
output = response.get("output", "")
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
594
|
+
success = response.get("success", True)
|
|
595
|
+
|
|
596
|
+
if as_json:
|
|
597
|
+
payload = dict(response)
|
|
598
|
+
if raw and isinstance(payload.get("output"), str):
|
|
599
|
+
payload["output"] = _strip_raw_output(payload["output"])
|
|
600
|
+
click.echo(json.dumps(payload, default=str))
|
|
601
|
+
else:
|
|
585
602
|
if output:
|
|
586
|
-
|
|
603
|
+
if raw:
|
|
604
|
+
output = _strip_raw_output(output)
|
|
605
|
+
if output:
|
|
606
|
+
click.echo(output)
|
|
587
607
|
|
|
588
|
-
success = response.get("success", True)
|
|
589
608
|
if not success:
|
|
590
609
|
sys.exit(1)
|
|
591
610
|
|
patchright_cli/daemon.py
CHANGED
|
@@ -54,9 +54,11 @@ def register(name: str):
|
|
|
54
54
|
class Session:
|
|
55
55
|
"""A single browser session (one persistent context, multiple pages/tabs)."""
|
|
56
56
|
|
|
57
|
-
def __init__(self, name: str, context, pages: list | None = None):
|
|
57
|
+
def __init__(self, name: str, context, pages: list | None = None, browser=None, is_attached: bool = False):
|
|
58
58
|
self.name = name
|
|
59
59
|
self.context = context
|
|
60
|
+
self.browser = browser # Browser handle for CDP-connected sessions (None for launch_persistent_context)
|
|
61
|
+
self.is_attached = is_attached # True if created via `attach --cdp=...`
|
|
60
62
|
self.pages: list = pages or []
|
|
61
63
|
self.current_tab: int = 0
|
|
62
64
|
self.ref_registry: RefRegistry | None = None
|
|
@@ -106,19 +108,51 @@ class Session:
|
|
|
106
108
|
self._cdp_sessions[id(page)] = cdp
|
|
107
109
|
except Exception:
|
|
108
110
|
pass
|
|
109
|
-
page.on(
|
|
110
|
-
|
|
111
|
-
lambda req: self.network_log.append(
|
|
112
|
-
{
|
|
113
|
-
"method": req.method,
|
|
114
|
-
"url": req.url,
|
|
115
|
-
"resource": req.resource_type,
|
|
116
|
-
"ts": time.time(),
|
|
117
|
-
}
|
|
118
|
-
),
|
|
119
|
-
)
|
|
111
|
+
page.on("request", lambda req: self._on_request(req))
|
|
112
|
+
page.on("response", lambda resp: asyncio.ensure_future(self._on_response(resp)))
|
|
120
113
|
page.on("dialog", lambda dialog: self._handle_dialog(dialog))
|
|
121
114
|
|
|
115
|
+
def _on_request(self, req):
|
|
116
|
+
try:
|
|
117
|
+
entry = {
|
|
118
|
+
"id": len(self.network_log),
|
|
119
|
+
"method": req.method,
|
|
120
|
+
"url": req.url,
|
|
121
|
+
"resource": req.resource_type,
|
|
122
|
+
"ts": time.time(),
|
|
123
|
+
"request_headers": dict(req.headers or {}),
|
|
124
|
+
"post_data": req.post_data,
|
|
125
|
+
"_request": req,
|
|
126
|
+
}
|
|
127
|
+
except Exception:
|
|
128
|
+
entry = {
|
|
129
|
+
"id": len(self.network_log),
|
|
130
|
+
"method": getattr(req, "method", "?"),
|
|
131
|
+
"url": getattr(req, "url", "?"),
|
|
132
|
+
"resource": getattr(req, "resource_type", "?"),
|
|
133
|
+
"ts": time.time(),
|
|
134
|
+
"request_headers": {},
|
|
135
|
+
"post_data": None,
|
|
136
|
+
"_request": req,
|
|
137
|
+
}
|
|
138
|
+
self.network_log.append(entry)
|
|
139
|
+
|
|
140
|
+
async def _on_response(self, resp):
|
|
141
|
+
try:
|
|
142
|
+
req = resp.request
|
|
143
|
+
except Exception:
|
|
144
|
+
return
|
|
145
|
+
for entry in reversed(self.network_log):
|
|
146
|
+
if entry.get("_request") is req:
|
|
147
|
+
try:
|
|
148
|
+
entry["status"] = resp.status
|
|
149
|
+
entry["status_text"] = resp.status_text
|
|
150
|
+
entry["response_headers"] = dict(resp.headers or {})
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
entry["_response"] = resp
|
|
154
|
+
return
|
|
155
|
+
|
|
122
156
|
def _handle_dialog(self, dialog):
|
|
123
157
|
"""Auto-handle dialogs based on pending action or auto-dismiss."""
|
|
124
158
|
import asyncio
|
|
@@ -235,11 +269,12 @@ class DaemonState:
|
|
|
235
269
|
perms = [p.strip() for p in grant_permissions.split(",") if p.strip()]
|
|
236
270
|
context_options["permissions"] = list(set(context_options.get("permissions", []) + perms))
|
|
237
271
|
|
|
272
|
+
attached_browser = None
|
|
238
273
|
if cdp_endpoint:
|
|
239
|
-
|
|
274
|
+
attached_browser = await self.playwright.chromium.connect_over_cdp(
|
|
240
275
|
cdp_endpoint, headers=cdp_headers, timeout=cdp_timeout
|
|
241
276
|
)
|
|
242
|
-
context = await
|
|
277
|
+
context = await attached_browser.new_context(**context_options)
|
|
243
278
|
pages = context.pages or []
|
|
244
279
|
if not pages:
|
|
245
280
|
page = await context.new_page()
|
|
@@ -279,7 +314,13 @@ class DaemonState:
|
|
|
279
314
|
if url:
|
|
280
315
|
await pages[0].goto(url)
|
|
281
316
|
|
|
282
|
-
session = Session(
|
|
317
|
+
session = Session(
|
|
318
|
+
name,
|
|
319
|
+
context,
|
|
320
|
+
list(pages),
|
|
321
|
+
browser=attached_browser,
|
|
322
|
+
is_attached=cdp_endpoint is not None,
|
|
323
|
+
)
|
|
283
324
|
await session.setup_listeners()
|
|
284
325
|
self.sessions[name] = session
|
|
285
326
|
self.profile_dirs[name] = profile_dir
|
|
@@ -487,6 +528,68 @@ async def cmd_drag(session: Session, page, args: list, options: dict, cwd: str |
|
|
|
487
528
|
return await _page_info(session, cwd)
|
|
488
529
|
|
|
489
530
|
|
|
531
|
+
@register("drop")
|
|
532
|
+
async def cmd_drop(session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState) -> dict:
|
|
533
|
+
"""Drop files or data onto an element (simulates HTML5 drop from outside)."""
|
|
534
|
+
if not args:
|
|
535
|
+
return {"success": False, "output": "Usage: drop <ref> --path=F | --data=mime=value"}
|
|
536
|
+
elem = await _resolve_ref(session, page, args[0])
|
|
537
|
+
|
|
538
|
+
path_opt = options.get("path")
|
|
539
|
+
data_opt = options.get("data")
|
|
540
|
+
if not path_opt and not data_opt:
|
|
541
|
+
return {"success": False, "output": "drop requires --path=<file> or --data=<mime=value>"}
|
|
542
|
+
|
|
543
|
+
files: list[dict] = []
|
|
544
|
+
if path_opt:
|
|
545
|
+
import base64
|
|
546
|
+
import mimetypes
|
|
547
|
+
|
|
548
|
+
paths = [path_opt] if isinstance(path_opt, str) else list(path_opt)
|
|
549
|
+
for p in paths:
|
|
550
|
+
fp = Path(p)
|
|
551
|
+
if not fp.exists():
|
|
552
|
+
return {"success": False, "output": f"File not found: {p}"}
|
|
553
|
+
mime = mimetypes.guess_type(fp.name)[0] or "application/octet-stream"
|
|
554
|
+
files.append(
|
|
555
|
+
{
|
|
556
|
+
"name": fp.name,
|
|
557
|
+
"type": mime,
|
|
558
|
+
"b64": base64.b64encode(fp.read_bytes()).decode("ascii"),
|
|
559
|
+
}
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
data_entries: list[tuple[str, str]] = []
|
|
563
|
+
if data_opt:
|
|
564
|
+
raw_list = [data_opt] if isinstance(data_opt, str) else list(data_opt)
|
|
565
|
+
for raw in raw_list:
|
|
566
|
+
if "=" not in raw:
|
|
567
|
+
return {"success": False, "output": f"Invalid --data (need mime=value): {raw}"}
|
|
568
|
+
mime, value = raw.split("=", 1)
|
|
569
|
+
data_entries.append((mime, value))
|
|
570
|
+
|
|
571
|
+
js = """
|
|
572
|
+
async (target, { files, dataEntries }) => {
|
|
573
|
+
const dt = new DataTransfer();
|
|
574
|
+
for (const f of files) {
|
|
575
|
+
const bytes = Uint8Array.from(atob(f.b64), c => c.charCodeAt(0));
|
|
576
|
+
dt.items.add(new File([bytes], f.name, { type: f.type }));
|
|
577
|
+
}
|
|
578
|
+
for (const [mime, value] of dataEntries) {
|
|
579
|
+
dt.setData(mime, value);
|
|
580
|
+
}
|
|
581
|
+
const evt = (type) => new DragEvent(type, {
|
|
582
|
+
bubbles: true, cancelable: true, dataTransfer: dt,
|
|
583
|
+
});
|
|
584
|
+
target.dispatchEvent(evt('dragenter'));
|
|
585
|
+
target.dispatchEvent(evt('dragover'));
|
|
586
|
+
target.dispatchEvent(evt('drop'));
|
|
587
|
+
}
|
|
588
|
+
"""
|
|
589
|
+
await elem.evaluate(js, {"files": files, "dataEntries": data_entries})
|
|
590
|
+
return await _page_info(session, cwd)
|
|
591
|
+
|
|
592
|
+
|
|
490
593
|
# -- Snapshot ----------------------------------------------------------------
|
|
491
594
|
|
|
492
595
|
|
|
@@ -504,6 +607,8 @@ async def cmd_snapshot(session: Session, page, args: list, options: dict, cwd: s
|
|
|
504
607
|
snapshot_text, session.ref_registry = await take_snapshot(
|
|
505
608
|
page, max_depth=max_depth, interactive_only=interactive_only
|
|
506
609
|
)
|
|
610
|
+
if options.get("boxes"):
|
|
611
|
+
snapshot_text = await _annotate_with_boxes(page, snapshot_text, session.ref_registry)
|
|
507
612
|
fn = options.get("filename")
|
|
508
613
|
if fn:
|
|
509
614
|
Path(fn).parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -526,6 +631,111 @@ async def cmd_snapshot(session: Session, page, args: list, options: dict, cwd: s
|
|
|
526
631
|
return {"success": True, "output": "\n".join(output_lines), "snapshot_path": snap_path}
|
|
527
632
|
|
|
528
633
|
|
|
634
|
+
async def _annotate_with_boxes(page, snapshot_text: str, registry) -> str:
|
|
635
|
+
"""Append `[box=x,y,w,h]` to each `[ref=eN]` line in snapshot_text.
|
|
636
|
+
|
|
637
|
+
Refs whose locator can't be measured are left unchanged.
|
|
638
|
+
"""
|
|
639
|
+
if registry is None:
|
|
640
|
+
return snapshot_text
|
|
641
|
+
import re
|
|
642
|
+
|
|
643
|
+
ref_re = re.compile(r"\[ref=(e\d+)\]")
|
|
644
|
+
boxes: dict[str, str] = {}
|
|
645
|
+
for ref in registry.entries:
|
|
646
|
+
try:
|
|
647
|
+
loc = registry.resolve(page, ref)
|
|
648
|
+
box = await loc.bounding_box()
|
|
649
|
+
if box:
|
|
650
|
+
boxes[ref] = f"[box={int(box['x'])},{int(box['y'])},{int(box['width'])},{int(box['height'])}]"
|
|
651
|
+
except Exception:
|
|
652
|
+
continue
|
|
653
|
+
|
|
654
|
+
out = []
|
|
655
|
+
for line in snapshot_text.splitlines():
|
|
656
|
+
m = ref_re.search(line)
|
|
657
|
+
if m and m.group(1) in boxes:
|
|
658
|
+
line = line.rstrip() + " " + boxes[m.group(1)]
|
|
659
|
+
out.append(line)
|
|
660
|
+
return "\n".join(out)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@register("generate-locator")
|
|
664
|
+
async def cmd_generate_locator(
|
|
665
|
+
session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState
|
|
666
|
+
) -> dict:
|
|
667
|
+
"""Emit a Playwright locator expression for a ref."""
|
|
668
|
+
if not args:
|
|
669
|
+
return {"success": False, "output": "Usage: generate-locator <ref>"}
|
|
670
|
+
if session.ref_registry is None:
|
|
671
|
+
return {"success": False, "output": "No snapshot taken yet. Run `snapshot` first."}
|
|
672
|
+
ref = args[0].lstrip("@")
|
|
673
|
+
entry = session.ref_registry.entries.get(ref)
|
|
674
|
+
if entry is None:
|
|
675
|
+
return {"success": False, "output": f"Ref @{ref} not found. Re-run `snapshot`."}
|
|
676
|
+
|
|
677
|
+
name_part = ""
|
|
678
|
+
if entry.name:
|
|
679
|
+
escaped = entry.name.replace("\\", "\\\\").replace("'", "\\'")
|
|
680
|
+
name_part = f", {{ name: '{escaped}', exact: true }}"
|
|
681
|
+
expr = f"getByRole('{entry.role}'{name_part})"
|
|
682
|
+
if entry.nth > 0:
|
|
683
|
+
expr += f".nth({entry.nth})"
|
|
684
|
+
return {"success": True, "output": expr}
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@register("highlight")
|
|
688
|
+
async def cmd_highlight(session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState) -> dict:
|
|
689
|
+
"""Draw or remove a persistent overlay over an element.
|
|
690
|
+
|
|
691
|
+
`highlight <ref>` shows it; `highlight <ref> --hide` hides it; `highlight --hide`
|
|
692
|
+
clears all highlights on the page.
|
|
693
|
+
"""
|
|
694
|
+
hide = bool(options.get("hide"))
|
|
695
|
+
style = options.get("style") or "outline: 2px solid #ff3366; outline-offset: 2px;"
|
|
696
|
+
|
|
697
|
+
if hide and not args:
|
|
698
|
+
await page.evaluate("() => document.querySelectorAll('[data-patchright-highlight]').forEach(el => el.remove())")
|
|
699
|
+
return {"success": True, "output": "All highlights cleared."}
|
|
700
|
+
|
|
701
|
+
if not args:
|
|
702
|
+
return {"success": False, "output": "Usage: highlight <ref> [--style=...] [--hide]"}
|
|
703
|
+
elem = await _resolve_ref(session, page, args[0])
|
|
704
|
+
|
|
705
|
+
if hide:
|
|
706
|
+
await elem.evaluate(
|
|
707
|
+
"el => { const id = el.getAttribute('data-patchright-hl-id');"
|
|
708
|
+
" if (id) {"
|
|
709
|
+
' const o = document.querySelector(`[data-patchright-highlight="${id}"]`);'
|
|
710
|
+
" if (o) o.remove();"
|
|
711
|
+
" el.removeAttribute('data-patchright-hl-id');"
|
|
712
|
+
" } }"
|
|
713
|
+
)
|
|
714
|
+
return {"success": True, "output": f"Hid highlight on {args[0]}."}
|
|
715
|
+
|
|
716
|
+
js = """
|
|
717
|
+
(el, { style }) => {
|
|
718
|
+
const rect = el.getBoundingClientRect();
|
|
719
|
+
const id = el.getAttribute('data-patchright-hl-id') || ('h' + Math.random().toString(36).slice(2));
|
|
720
|
+
el.setAttribute('data-patchright-hl-id', id);
|
|
721
|
+
let overlay = document.querySelector(`[data-patchright-highlight="${id}"]`);
|
|
722
|
+
if (!overlay) {
|
|
723
|
+
overlay = document.createElement('div');
|
|
724
|
+
overlay.setAttribute('data-patchright-highlight', id);
|
|
725
|
+
overlay.style.cssText = 'position: fixed; pointer-events: none; z-index: 2147483647;';
|
|
726
|
+
document.documentElement.appendChild(overlay);
|
|
727
|
+
}
|
|
728
|
+
overlay.style.left = rect.left + 'px';
|
|
729
|
+
overlay.style.top = rect.top + 'px';
|
|
730
|
+
overlay.style.width = rect.width + 'px';
|
|
731
|
+
overlay.style.height = rect.height + 'px';
|
|
732
|
+
overlay.style.cssText += '; ' + style;
|
|
733
|
+
}
|
|
734
|
+
"""
|
|
735
|
+
await elem.evaluate(js, {"style": style})
|
|
736
|
+
return {"success": True, "output": f"Highlighted {args[0]}."}
|
|
737
|
+
|
|
738
|
+
|
|
529
739
|
# -- Eval / Screenshot -------------------------------------------------------
|
|
530
740
|
|
|
531
741
|
|
|
@@ -857,12 +1067,67 @@ async def cmd_network(session: Session, page, args: list, options: dict, cwd: st
|
|
|
857
1067
|
for r in session.network_log[-50:]:
|
|
858
1068
|
if not include_static and r["resource"] in static_types:
|
|
859
1069
|
continue
|
|
860
|
-
|
|
1070
|
+
status = r.get("status")
|
|
1071
|
+
status_part = f" {status}" if status is not None else ""
|
|
1072
|
+
lines.append(f"#{r['id']} {r['method']}{status_part} {r['url']} [{r['resource']}]")
|
|
861
1073
|
if options.get("clear"):
|
|
862
1074
|
session.network_log.clear()
|
|
863
1075
|
return {"success": True, "output": "\n".join(lines) or "(no network requests)"}
|
|
864
1076
|
|
|
865
1077
|
|
|
1078
|
+
@register("request")
|
|
1079
|
+
async def cmd_request(session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState) -> dict:
|
|
1080
|
+
if not args:
|
|
1081
|
+
return {"success": False, "output": "Usage: request <id>"}
|
|
1082
|
+
try:
|
|
1083
|
+
req_id = int(args[0])
|
|
1084
|
+
except ValueError:
|
|
1085
|
+
return {"success": False, "output": f"Invalid request id: {args[0]}"}
|
|
1086
|
+
|
|
1087
|
+
entry = next((e for e in session.network_log if e["id"] == req_id), None)
|
|
1088
|
+
if entry is None:
|
|
1089
|
+
return {"success": False, "output": f"No request with id {req_id}. Use `network` to list."}
|
|
1090
|
+
|
|
1091
|
+
lines = [
|
|
1092
|
+
f"### Request #{entry['id']}",
|
|
1093
|
+
f"- {entry['method']} {entry['url']}",
|
|
1094
|
+
f"- Resource: {entry['resource']}",
|
|
1095
|
+
]
|
|
1096
|
+
status = entry.get("status")
|
|
1097
|
+
if status is not None:
|
|
1098
|
+
status_text = entry.get("status_text") or ""
|
|
1099
|
+
lines.append(f"- Status: {status} {status_text}".rstrip())
|
|
1100
|
+
|
|
1101
|
+
req_headers = entry.get("request_headers") or {}
|
|
1102
|
+
if req_headers:
|
|
1103
|
+
lines.append("### Request Headers")
|
|
1104
|
+
for k, v in req_headers.items():
|
|
1105
|
+
lines.append(f"- {k}: {v}")
|
|
1106
|
+
|
|
1107
|
+
post_data = entry.get("post_data")
|
|
1108
|
+
if post_data:
|
|
1109
|
+
lines.append("### Request Body")
|
|
1110
|
+
lines.append(post_data if len(post_data) < 4000 else post_data[:4000] + "\n... (truncated)")
|
|
1111
|
+
|
|
1112
|
+
resp_headers = entry.get("response_headers") or {}
|
|
1113
|
+
if resp_headers:
|
|
1114
|
+
lines.append("### Response Headers")
|
|
1115
|
+
for k, v in resp_headers.items():
|
|
1116
|
+
lines.append(f"- {k}: {v}")
|
|
1117
|
+
|
|
1118
|
+
if options.get("body"):
|
|
1119
|
+
resp = entry.get("_response")
|
|
1120
|
+
if resp is not None:
|
|
1121
|
+
try:
|
|
1122
|
+
body = await resp.text()
|
|
1123
|
+
lines.append("### Response Body")
|
|
1124
|
+
lines.append(body if len(body) < 8000 else body[:8000] + "\n... (truncated)")
|
|
1125
|
+
except Exception as e:
|
|
1126
|
+
lines.append(f"### Response Body\n(could not read: {e})")
|
|
1127
|
+
|
|
1128
|
+
return {"success": True, "output": "\n".join(lines)}
|
|
1129
|
+
|
|
1130
|
+
|
|
866
1131
|
@register("network-state-set")
|
|
867
1132
|
async def cmd_network_state_set(
|
|
868
1133
|
session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState
|
|
@@ -1392,6 +1657,10 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
1392
1657
|
options = dict(msg.get("options", {}))
|
|
1393
1658
|
cwd = msg.get("cwd")
|
|
1394
1659
|
|
|
1660
|
+
# `requests` is an alias for `network` (matches playwright-cli naming).
|
|
1661
|
+
if cmd == "requests":
|
|
1662
|
+
cmd = "network"
|
|
1663
|
+
|
|
1395
1664
|
session_name = options.pop("session", "default") or "default"
|
|
1396
1665
|
|
|
1397
1666
|
try:
|
|
@@ -1482,6 +1751,25 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
1482
1751
|
closed = await state.close_session(session_name)
|
|
1483
1752
|
return {"success": True, "output": f"Session '{session_name}' closed." if closed else "Session not found."}
|
|
1484
1753
|
|
|
1754
|
+
if cmd == "detach":
|
|
1755
|
+
target = state.sessions.get(session_name)
|
|
1756
|
+
if target is None:
|
|
1757
|
+
return {"success": False, "output": f"Session '{session_name}' not found."}
|
|
1758
|
+
if not target.is_attached:
|
|
1759
|
+
return {
|
|
1760
|
+
"success": False,
|
|
1761
|
+
"output": f"Session '{session_name}' was not attached. Use `close` for sessions created via `open`.",
|
|
1762
|
+
}
|
|
1763
|
+
state.sessions.pop(session_name, None)
|
|
1764
|
+
# Disconnect from external Chrome without killing it. For CDP-attached
|
|
1765
|
+
# sessions, browser.close() only severs the connection.
|
|
1766
|
+
try:
|
|
1767
|
+
if target.browser is not None:
|
|
1768
|
+
await target.browser.close()
|
|
1769
|
+
except Exception:
|
|
1770
|
+
pass
|
|
1771
|
+
return {"success": True, "output": f"Detached from '{session_name}' (external browser kept running)."}
|
|
1772
|
+
|
|
1485
1773
|
if cmd == "close-all":
|
|
1486
1774
|
names = list(state.sessions.keys())
|
|
1487
1775
|
for n in names:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchright-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Anti-detect browser automation CLI using Patchright (undetected Playwright fork)
|
|
5
5
|
Project-URL: Homepage, https://github.com/AhaiMk01/patchright-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/AhaiMk01/patchright-cli
|
|
@@ -331,74 +331,41 @@ patchright-cli --port=9322 open # Custom daemon port
|
|
|
331
331
|
|
|
332
332
|
---
|
|
333
333
|
|
|
334
|
-
## Snapshots
|
|
335
|
-
|
|
336
|
-
After each state-changing command, the CLI outputs page info and a YAML snapshot:
|
|
337
|
-
|
|
338
|
-
```
|
|
339
|
-
### Page
|
|
340
|
-
- Page URL: https://example.com/
|
|
341
|
-
- Page Title: Example Domain
|
|
342
|
-
### Snapshot
|
|
343
|
-
[Snapshot](.patchright-cli/page-1774376207818.yml)
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
The snapshot lists interactive elements with refs you can use in commands:
|
|
347
|
-
|
|
348
|
-
```yaml
|
|
349
|
-
- ref: e1
|
|
350
|
-
role: heading
|
|
351
|
-
name: Example Domain
|
|
352
|
-
level: 1
|
|
353
|
-
- ref: e2
|
|
354
|
-
role: link
|
|
355
|
-
name: Learn more
|
|
356
|
-
url: "https://iana.org/domains/example"
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
> [!NOTE]
|
|
360
|
-
> Use refs directly: `patchright-cli click e2`, `patchright-cli fill e5 "text"`
|
|
361
|
-
|
|
362
|
-
---
|
|
363
|
-
|
|
364
|
-
## Anti-Detect Features
|
|
365
|
-
|
|
366
|
-
> [!CAUTION]
|
|
367
|
-
> This tool is for **authorized testing, security research, and legitimate automation** only.
|
|
368
|
-
|
|
369
|
-
- :white_check_mark: Real Chrome browser (not Chromium)
|
|
370
|
-
- :white_check_mark: Patchright patches `navigator.webdriver` and other detection vectors
|
|
371
|
-
- :white_check_mark: Persistent profiles maintain cookies/sessions across runs
|
|
372
|
-
- :white_check_mark: No custom user-agent or headers (natural fingerprint)
|
|
373
|
-
- :white_check_mark: Headed by default (headless is more detectable)
|
|
374
|
-
|
|
375
|
-
---
|
|
376
|
-
|
|
377
334
|
## Agent Integration
|
|
378
335
|
|
|
379
|
-
Works with
|
|
336
|
+
Works with Claude Code, Cursor, Gemini CLI, Codex, and [40+ other agents](https://github.com/vercel-labs/skills).
|
|
380
337
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
| **Codex CLI** | `mkdir -p ~/.codex/skills/patchright-cli && curl -sL https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md -o ~/.codex/skills/patchright-cli/SKILL.md` |
|
|
386
|
-
| **Gemini CLI** | `mkdir -p ~/.gemini/skills/patchright-cli && curl -sL https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md -o ~/.gemini/skills/patchright-cli/SKILL.md` |
|
|
387
|
-
| **OpenCode** | `mkdir -p ~/.opencode/skills/patchright-cli && curl -sL https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md -o ~/.opencode/skills/patchright-cli/SKILL.md` |
|
|
388
|
-
| **Cursor** | Copy SKILL.md to `.cursor/skills/patchright-cli/` in your project |
|
|
389
|
-
| **Windsurf** | Copy SKILL.md to `.windsurf/skills/patchright-cli/` in your project |
|
|
390
|
-
| **Aider** | Copy SKILL.md to `.aider/skills/patchright-cli/` in your project |
|
|
338
|
+
```bash
|
|
339
|
+
# Install skill for all detected agents (recommended)
|
|
340
|
+
npx skills add AhaiMk01/patchright-cli
|
|
341
|
+
```
|
|
391
342
|
|
|
392
|
-
|
|
343
|
+
<details>
|
|
344
|
+
<summary><b>Other install methods</b></summary>
|
|
393
345
|
|
|
394
346
|
```bash
|
|
347
|
+
# Via patchright-cli itself
|
|
395
348
|
pip install patchright-cli
|
|
396
349
|
patchright-cli install --skills
|
|
350
|
+
|
|
351
|
+
# Or just tell your agent:
|
|
352
|
+
# "Install patchright-cli skill from https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md"
|
|
397
353
|
```
|
|
398
354
|
|
|
399
|
-
|
|
355
|
+
</details>
|
|
400
356
|
|
|
401
|
-
|
|
357
|
+
### Skill docs
|
|
358
|
+
|
|
359
|
+
| File | Topic |
|
|
360
|
+
|------|-------|
|
|
361
|
+
| [`SKILL.md`](skills/patchright-cli/SKILL.md) | Main skill — commands, options, patterns |
|
|
362
|
+
| [`snapshot-refs.md`](skills/patchright-cli/references/snapshot-refs.md) | Snapshots, element refs, `-i` flag |
|
|
363
|
+
| [`session-management.md`](skills/patchright-cli/references/session-management.md) | Named sessions, profiles, CDP, dashboard |
|
|
364
|
+
| [`request-mocking.md`](skills/patchright-cli/references/request-mocking.md) | Route mocking, network state |
|
|
365
|
+
| [`storage-state.md`](skills/patchright-cli/references/storage-state.md) | Cookies, localStorage, sessionStorage |
|
|
366
|
+
| [`running-code.md`](skills/patchright-cli/references/running-code.md) | eval, run-code, `--file`, stdin |
|
|
367
|
+
| [`tracing.md`](skills/patchright-cli/references/tracing.md) | Trace recording and debugging |
|
|
368
|
+
| [`video-recording.md`](skills/patchright-cli/references/video-recording.md) | Video, codegen, screenshot, PDF |
|
|
402
369
|
|
|
403
370
|
---
|
|
404
371
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
patchright_cli/__init__.py,sha256=
|
|
1
|
+
patchright_cli/__init__.py,sha256=4B_Y2Xe4HCJQN3uKjxIYZvB8oR5SUT1RgfhxGLX1fnI,100
|
|
2
2
|
patchright_cli/__main__.py,sha256=iVuBuI_rPOENLhffOEHoS7JWCDBQciqT1kPXjRuhbZ4,127
|
|
3
|
-
patchright_cli/cli.py,sha256=
|
|
4
|
-
patchright_cli/daemon.py,sha256=
|
|
3
|
+
patchright_cli/cli.py,sha256=2oSTctMLENzI_CrXDN8iZTl4f24VlcX0HzhxE6pI9iA,24038
|
|
4
|
+
patchright_cli/daemon.py,sha256=YvaWLWPz2z2H6-fpyQzOKSCPIy_M-5ikJIe6NEDuGDo,75305
|
|
5
5
|
patchright_cli/dashboard.py,sha256=aQHujW9tHTOQhbyqm4M6OknShLaJSnRFHFZRRfs3l20,4022
|
|
6
6
|
patchright_cli/ref_registry.py,sha256=XLqHePO7iOHlnFeNdoQAMrAkTBnvkrnhxkn7f6Vc1pU,2945
|
|
7
7
|
patchright_cli/snapshot.py,sha256=J3oc429_LpOgpYqaJaQJkN_oezoCBChoUDsvAX0z-Qg,1570
|
|
8
|
-
patchright_cli/_skills/patchright-cli/SKILL.md,sha256=
|
|
8
|
+
patchright_cli/_skills/patchright-cli/SKILL.md,sha256=h0EQQ4FPv7Ys0tml9yopR5TosnhAZY3YUUiNzuJkJaU,12183
|
|
9
9
|
patchright_cli/_skills/patchright-cli/references/request-mocking.md,sha256=x4t_5gPxSsYvm0BlrZqbdM3MHqGVzNY_ac6Awg6PToc,2901
|
|
10
10
|
patchright_cli/_skills/patchright-cli/references/running-code.md,sha256=Ls0HhV8gxT5XOHfiv9-B8eBvYn-RR21wZ7Iyh3uskuk,3242
|
|
11
11
|
patchright_cli/_skills/patchright-cli/references/session-management.md,sha256=hsqbDHv0C-BUlId5lymBBCR9DxGUyzRwyaQOhcpR324,3495
|
|
@@ -13,8 +13,8 @@ patchright_cli/_skills/patchright-cli/references/snapshot-refs.md,sha256=VFz6_Ps
|
|
|
13
13
|
patchright_cli/_skills/patchright-cli/references/storage-state.md,sha256=qfL_2v_WHHyVUIPj4efMU46KOMbYyvtLll4ha84mOHw,3348
|
|
14
14
|
patchright_cli/_skills/patchright-cli/references/tracing.md,sha256=6IwbGDsrtuB3bckB_7noyjpU37mdemIC435IuLilBZk,1931
|
|
15
15
|
patchright_cli/_skills/patchright-cli/references/video-recording.md,sha256=x6xnssw3ASrRlKg45Qo6yiOhIDcirWXBxw3UOfq4K5Y,2576
|
|
16
|
-
patchright_cli-0.
|
|
17
|
-
patchright_cli-0.
|
|
18
|
-
patchright_cli-0.
|
|
19
|
-
patchright_cli-0.
|
|
20
|
-
patchright_cli-0.
|
|
16
|
+
patchright_cli-0.6.0.dist-info/METADATA,sha256=k6c4sA-g39iP5uvXvO_TAnOaVEMsuI6ufGtz7q2_JRk,15435
|
|
17
|
+
patchright_cli-0.6.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
18
|
+
patchright_cli-0.6.0.dist-info/entry_points.txt,sha256=Syh904eOQA8ylakyqIvG4LmIHx3LMjgeNcnFap3O0NQ,59
|
|
19
|
+
patchright_cli-0.6.0.dist-info/licenses/LICENSE,sha256=adszqL4YR4pQUrc7LeAFxeg5Mc2cvljXp7QPlzKVWm8,10760
|
|
20
|
+
patchright_cli-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|