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.
@@ -1,3 +1,3 @@
1
1
  """patchright-cli — Undetected browser automation CLI using Patchright."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -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
- Install the skill so your agent knows how to use patchright-cli. The same command also works to update to the latest version.
316
-
317
- **macOS / Linux (bash/zsh):**
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
- | Agent | Install / update command |
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
- **IDE-based agents (Cursor / Windsurf / Aider):** Copy `SKILL.md` to `.<tool>/skills/patchright-cli/` in your project.
334
+ ```bash
335
+ # Via patchright-cli itself
336
+ pip install patchright-cli && patchright-cli install --skills
336
337
 
337
- **Or just tell your agent:**
338
- > Install patchright-cli skill from https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md
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
- if output:
583
- if raw:
584
- output = _strip_raw_output(output)
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
- click.echo(output)
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
- "request",
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
- browser = await self.playwright.chromium.connect_over_cdp(
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 browser.new_context(**context_options)
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(name, context, list(pages))
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
- lines.append(f"{r['method']} {r['url']} [{r['resource']}]")
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.5.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 any AI coding agent that supports SKILL.md skills:
336
+ Works with Claude Code, Cursor, Gemini CLI, Codex, and [40+ other agents](https://github.com/vercel-labs/skills).
380
337
 
381
- | Agent | Install skill |
382
- |-------|--------------|
383
- | **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` |
384
- | **OpenClaw** | `mkdir -p ~/.openclaw/skills/patchright-cli && curl -sL https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md -o ~/.openclaw/skills/patchright-cli/SKILL.md` |
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
- Or auto-install for all detected agents:
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
- Or just tell your agent:
355
+ </details>
400
356
 
401
- > Install patchright-cli skill from https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md
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=cIPq7urEd7-daD-_CugdNI-L9GyVZrZn5YDVRFZkX1M,100
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=HMGKFWqJGmZ9Tbs3idLud7xqs9-az5Bqwz0fdo1v8MI,22986
4
- patchright_cli/daemon.py,sha256=WoknT_CE1TrfdoFr1zY1sQjmrSAMj0w2VI4Xy_x8ix8,63864
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=8Ch9LTls_O9NOA7o6hJ0K1NKdMP0H0Yd6PKW--MjPsQ,13554
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.5.0.dist-info/METADATA,sha256=kXazUmF8JnUzteHJIEpYVNEivVt6jXfz1BPKv_yAJTI,16738
17
- patchright_cli-0.5.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
18
- patchright_cli-0.5.0.dist-info/entry_points.txt,sha256=Syh904eOQA8ylakyqIvG4LmIHx3LMjgeNcnFap3O0NQ,59
19
- patchright_cli-0.5.0.dist-info/licenses/LICENSE,sha256=adszqL4YR4pQUrc7LeAFxeg5Mc2cvljXp7QPlzKVWm8,10760
20
- patchright_cli-0.5.0.dist-info/RECORD,,
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,,