patchright-cli 0.9.0__py3-none-any.whl → 0.9.1__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 +34 -2
- patchright_cli/cli.py +45 -2
- patchright_cli/daemon.py +109 -3
- patchright_cli/mcp_server.py +78 -0
- patchright_cli/ref_registry.py +35 -12
- {patchright_cli-0.9.0.dist-info → patchright_cli-0.9.1.dist-info}/METADATA +33 -1
- {patchright_cli-0.9.0.dist-info → patchright_cli-0.9.1.dist-info}/RECORD +11 -11
- {patchright_cli-0.9.0.dist-info → patchright_cli-0.9.1.dist-info}/WHEEL +0 -0
- {patchright_cli-0.9.0.dist-info → patchright_cli-0.9.1.dist-info}/entry_points.txt +0 -0
- {patchright_cli-0.9.0.dist-info → patchright_cli-0.9.1.dist-info}/licenses/LICENSE +0 -0
patchright_cli/__init__.py
CHANGED
|
@@ -59,8 +59,21 @@ patchright-cli goto "https://example.com/search?q=cats&page=2"
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
The same applies to `#` (a bash comment), spaces, and `%` (expanded by
|
|
62
|
-
cmd.exe), and to any argument holding shell metacharacters. Quoting
|
|
63
|
-
|
|
62
|
+
cmd.exe), and to any argument holding shell metacharacters. Quoting works for
|
|
63
|
+
the installed `patchright-cli.exe` in bash, PowerShell and cmd.exe; leave it on.
|
|
64
|
+
|
|
65
|
+
On Windows, two cases need more than quotes:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
# cmd.exe without quotes: escape the & as ^&. Inside quotes ^ is kept
|
|
69
|
+
# literally, so use one or the other, never both.
|
|
70
|
+
patchright-cli goto https://example.com/search?q=cats^&page=2
|
|
71
|
+
|
|
72
|
+
# PowerShell calling a .cmd/.bat wrapper (such as the repo's patchright-cli.cmd):
|
|
73
|
+
# PowerShell drops the quotes and cmd.exe splits at &. Pass the rest verbatim
|
|
74
|
+
# with --%. Don't use --% with patchright-cli.exe: in PowerShell 7 the quotes end up in the URL.
|
|
75
|
+
.\patchright-cli.cmd --% goto "https://example.com/search?q=cats&page=2"
|
|
76
|
+
```
|
|
64
77
|
|
|
65
78
|
## Global options
|
|
66
79
|
|
|
@@ -320,6 +333,25 @@ show # Live dashboard
|
|
|
320
333
|
show --annotate # Ask a human to mark up the page and wait for it
|
|
321
334
|
```
|
|
322
335
|
|
|
336
|
+
### Emulation
|
|
337
|
+
|
|
338
|
+
Each setting holds for the whole session: every open tab, and tabs and popups
|
|
339
|
+
opened later (a popup gets it just after it opens). `clear-*` drops just that
|
|
340
|
+
one, back to the browser default.
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
set-color-scheme dark # dark | light | no-preference
|
|
344
|
+
clear-color-scheme
|
|
345
|
+
set-reduced-motion reduce # reduce | no-preference
|
|
346
|
+
clear-reduced-motion
|
|
347
|
+
set-forced-colors active # active | none
|
|
348
|
+
clear-forced-colors
|
|
349
|
+
set-contrast more # more | no-preference
|
|
350
|
+
clear-contrast
|
|
351
|
+
set-media print # screen | print
|
|
352
|
+
clear-media
|
|
353
|
+
```
|
|
354
|
+
|
|
323
355
|
### Storage
|
|
324
356
|
|
|
325
357
|
```bash
|
patchright_cli/cli.py
CHANGED
|
@@ -17,7 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
import click
|
|
18
18
|
|
|
19
19
|
from patchright_cli import __version__
|
|
20
|
-
from patchright_cli.daemon import DEFAULT_PORT, ensure_daemon_running, read_daemon_token
|
|
20
|
+
from patchright_cli.daemon import DEFAULT_PORT, PROTOCOL_VERSION, ensure_daemon_running, read_daemon_token, token_path
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _soften_output_encoding(*streams) -> None:
|
|
@@ -497,6 +497,17 @@ COMMANDS_HELP = {
|
|
|
497
497
|
# Upload / Resize
|
|
498
498
|
"upload": "upload <file> [ref] Upload file to input",
|
|
499
499
|
"resize": "resize <w> <h> Resize viewport",
|
|
500
|
+
# Emulation
|
|
501
|
+
"set-color-scheme": "set-color-scheme <dark|light|no-preference> Emulate prefers-color-scheme",
|
|
502
|
+
"clear-color-scheme": "clear-color-scheme Stop emulating prefers-color-scheme",
|
|
503
|
+
"set-reduced-motion": "set-reduced-motion <reduce|no-preference> Emulate prefers-reduced-motion",
|
|
504
|
+
"clear-reduced-motion": "clear-reduced-motion Stop emulating prefers-reduced-motion",
|
|
505
|
+
"set-forced-colors": "set-forced-colors <active|none> Emulate forced-colors",
|
|
506
|
+
"clear-forced-colors": "clear-forced-colors Stop emulating forced-colors",
|
|
507
|
+
"set-contrast": "set-contrast <more|no-preference> Emulate prefers-contrast",
|
|
508
|
+
"clear-contrast": "clear-contrast Stop emulating prefers-contrast",
|
|
509
|
+
"set-media": "set-media <screen|print> Emulate CSS media type",
|
|
510
|
+
"clear-media": "clear-media Stop emulating the CSS media type",
|
|
500
511
|
# State
|
|
501
512
|
"state-save": "state-save [file] Save cookies+storage to JSON",
|
|
502
513
|
"state-load": "state-load <file> Load saved state",
|
|
@@ -547,6 +558,7 @@ COMMANDS_HELP = {
|
|
|
547
558
|
# Setup
|
|
548
559
|
"install": "install --skills Install skill files for AI agents",
|
|
549
560
|
"mcp": "mcp [--http] [--host=HOST] [--port=N] Start MCP server (stdio by default)",
|
|
561
|
+
"daemon-info": "daemon-info --json Show daemon client protocol and token path without starting it",
|
|
550
562
|
}
|
|
551
563
|
|
|
552
564
|
ALL_COMMANDS = list(COMMANDS_HELP.keys())
|
|
@@ -619,6 +631,21 @@ def _print_help():
|
|
|
619
631
|
("Tabs", ["tab-list", "tab-new", "tab-close", "tab-select"]),
|
|
620
632
|
("Dialog", ["dialog-accept", "dialog-dismiss"]),
|
|
621
633
|
("Upload/Resize", ["upload", "resize"]),
|
|
634
|
+
(
|
|
635
|
+
"Emulation",
|
|
636
|
+
[
|
|
637
|
+
"set-color-scheme",
|
|
638
|
+
"clear-color-scheme",
|
|
639
|
+
"set-reduced-motion",
|
|
640
|
+
"clear-reduced-motion",
|
|
641
|
+
"set-forced-colors",
|
|
642
|
+
"clear-forced-colors",
|
|
643
|
+
"set-contrast",
|
|
644
|
+
"clear-contrast",
|
|
645
|
+
"set-media",
|
|
646
|
+
"clear-media",
|
|
647
|
+
],
|
|
648
|
+
),
|
|
622
649
|
("State", ["state-save", "state-load"]),
|
|
623
650
|
(
|
|
624
651
|
"Storage",
|
|
@@ -653,7 +680,7 @@ def _print_help():
|
|
|
653
680
|
("Profiles", ["profile-list", "profile-delete"]),
|
|
654
681
|
("Dashboard", ["show"]),
|
|
655
682
|
("Codegen", ["codegen", "codegen-stop"]),
|
|
656
|
-
("Setup", ["install", "mcp"]),
|
|
683
|
+
("Setup", ["install", "mcp", "daemon-info"]),
|
|
657
684
|
]
|
|
658
685
|
for cat_name, cmds in categories:
|
|
659
686
|
click.echo(f"\n {cat_name}:")
|
|
@@ -823,6 +850,22 @@ def main():
|
|
|
823
850
|
_print_help()
|
|
824
851
|
sys.exit(1)
|
|
825
852
|
|
|
853
|
+
if command == "daemon-info":
|
|
854
|
+
if not as_json or args or extra_opts:
|
|
855
|
+
click.echo("Usage: patchright-cli daemon-info --json [--port=N]", err=True)
|
|
856
|
+
sys.exit(1)
|
|
857
|
+
click.echo(
|
|
858
|
+
json.dumps(
|
|
859
|
+
{
|
|
860
|
+
"version": __version__,
|
|
861
|
+
"protocol": PROTOCOL_VERSION,
|
|
862
|
+
"port": port,
|
|
863
|
+
"token_path": str(token_path(port).resolve()),
|
|
864
|
+
}
|
|
865
|
+
)
|
|
866
|
+
)
|
|
867
|
+
return
|
|
868
|
+
|
|
826
869
|
if command == "install":
|
|
827
870
|
_handle_install(args + [k if v is True else f"{k}={v}" for k, v in extra_opts.items()])
|
|
828
871
|
sys.exit(0)
|
patchright_cli/daemon.py
CHANGED
|
@@ -33,6 +33,9 @@ from patchright_cli.snapshot import save_snapshot, take_snapshot
|
|
|
33
33
|
|
|
34
34
|
logger = logging.getLogger("patchright-cli.daemon")
|
|
35
35
|
|
|
36
|
+
# Public client-daemon contract. Bump for incompatible framing, request/reply
|
|
37
|
+
# fields, or authentication changes. Additive optional fields do not require a bump.
|
|
38
|
+
PROTOCOL_VERSION = 1
|
|
36
39
|
DEFAULT_PORT = 9321
|
|
37
40
|
PROFILES_ROOT = Path.home() / ".patchright-cli" / "profiles"
|
|
38
41
|
# Holds daemon-<port>.token, the shared secret every client message must carry.
|
|
@@ -143,6 +146,13 @@ class Session:
|
|
|
143
146
|
self.console_messages: list[dict] = []
|
|
144
147
|
self.network_log: list[dict] = []
|
|
145
148
|
self._pending_dialog_action: tuple | None = None
|
|
149
|
+
# Media features set by `set-color-scheme` and friends, as emulate_media
|
|
150
|
+
# keyword arguments. emulate_media is per Page, so every Page this
|
|
151
|
+
# Session sees gets them, including ones opened later.
|
|
152
|
+
self.emulation: dict[str, str] = {}
|
|
153
|
+
# Held while emulation changes and while it is applied to pages, so two
|
|
154
|
+
# clients setting media at once can't leave pages disagreeing with it.
|
|
155
|
+
self.emulation_lock = asyncio.Lock()
|
|
146
156
|
# The Profile a Launched Session runs on (None for Attached Sessions).
|
|
147
157
|
self.profile: Profile | None = None
|
|
148
158
|
self._cdp_sessions: dict[int, object] = {}
|
|
@@ -172,8 +182,17 @@ class Session:
|
|
|
172
182
|
if page not in self.pages:
|
|
173
183
|
self.pages.append(page)
|
|
174
184
|
self.current_tab = len(self.pages) - 1
|
|
185
|
+
# Emulate before the listener setup awaits, so a popup's own scripts
|
|
186
|
+
# see the session's media features as early as possible.
|
|
187
|
+
await self.apply_emulation(page)
|
|
175
188
|
await self._attach_page_listeners(page)
|
|
176
189
|
|
|
190
|
+
async def apply_emulation(self, page) -> None:
|
|
191
|
+
"""Give `page` this Session's emulated media features, if any are set."""
|
|
192
|
+
async with self.emulation_lock:
|
|
193
|
+
if self.emulation and not page.is_closed():
|
|
194
|
+
await page.emulate_media(**self.emulation)
|
|
195
|
+
|
|
177
196
|
async def _attach_page_listeners(self, page):
|
|
178
197
|
# Use CDP for console messages (Patchright suppresses page.on('console'))
|
|
179
198
|
try:
|
|
@@ -400,6 +419,9 @@ class Session:
|
|
|
400
419
|
"""Open a Page in this Session's context and record it as ours."""
|
|
401
420
|
page = await self.context.new_page()
|
|
402
421
|
self.own_pages.add(page)
|
|
422
|
+
# The context's "page" handler applies this too, but runs as a task;
|
|
423
|
+
# applying here means the caller never navigates an unemulated page.
|
|
424
|
+
await self.apply_emulation(page)
|
|
403
425
|
return page
|
|
404
426
|
|
|
405
427
|
def may_close_page(self, page) -> bool:
|
|
@@ -2249,6 +2271,60 @@ async def cmd_resize(session: Session, page, args: list, options: dict, cwd: str
|
|
|
2249
2271
|
return {"success": True, "output": f"Viewport resized to {w}x{h}"}
|
|
2250
2272
|
|
|
2251
2273
|
|
|
2274
|
+
# -- Emulation ---------------------------------------------------------------
|
|
2275
|
+
|
|
2276
|
+
# Command suffix -> (emulate_media keyword, allowed values, value clear-* restores).
|
|
2277
|
+
# The values match playwright-cli's. emulate_media's "null" would drop the
|
|
2278
|
+
# override and expose the OS setting, but a page starts out with Playwright's
|
|
2279
|
+
# context defaults (light, no-preference, none, ...), so clearing restores those.
|
|
2280
|
+
_EMULATED_MEDIA = {
|
|
2281
|
+
"color-scheme": ("color_scheme", ("dark", "light", "no-preference"), "light"),
|
|
2282
|
+
"reduced-motion": ("reduced_motion", ("reduce", "no-preference"), "no-preference"),
|
|
2283
|
+
"forced-colors": ("forced_colors", ("active", "none"), "none"),
|
|
2284
|
+
"contrast": ("contrast", ("more", "no-preference"), "no-preference"),
|
|
2285
|
+
"media": ("media", ("screen", "print"), "null"),
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
|
|
2289
|
+
async def _emulate_on_all_pages(session: Session, **features: str) -> None:
|
|
2290
|
+
for p in list(session.pages):
|
|
2291
|
+
if p.is_closed():
|
|
2292
|
+
continue
|
|
2293
|
+
try:
|
|
2294
|
+
await p.emulate_media(**features)
|
|
2295
|
+
except Exception:
|
|
2296
|
+
# A page closing mid-loop must not stop the rest; a live page's error still counts.
|
|
2297
|
+
if not p.is_closed():
|
|
2298
|
+
raise
|
|
2299
|
+
|
|
2300
|
+
|
|
2301
|
+
def _register_emulation(feature: str, kwarg: str, allowed: tuple[str, ...], default: str) -> None:
|
|
2302
|
+
async def cmd_set(session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState) -> dict:
|
|
2303
|
+
value = args[0] if args else ""
|
|
2304
|
+
if value not in allowed:
|
|
2305
|
+
return {
|
|
2306
|
+
"success": False,
|
|
2307
|
+
"output": f"Usage: set-{feature} <{'|'.join(allowed)}> (got {value!r})",
|
|
2308
|
+
}
|
|
2309
|
+
async with session.emulation_lock:
|
|
2310
|
+
session.emulation[kwarg] = value
|
|
2311
|
+
await _emulate_on_all_pages(session, **{kwarg: value})
|
|
2312
|
+
return {"success": True, "output": f"Emulating {feature}: {value}"}
|
|
2313
|
+
|
|
2314
|
+
async def cmd_clear(session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState) -> dict:
|
|
2315
|
+
async with session.emulation_lock:
|
|
2316
|
+
session.emulation.pop(kwarg, None)
|
|
2317
|
+
await _emulate_on_all_pages(session, **{kwarg: default})
|
|
2318
|
+
return {"success": True, "output": f"Cleared {feature} emulation"}
|
|
2319
|
+
|
|
2320
|
+
register(f"set-{feature}")(cmd_set)
|
|
2321
|
+
register(f"clear-{feature}")(cmd_clear)
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
for _feature, (_kwarg, _allowed, _default) in _EMULATED_MEDIA.items():
|
|
2325
|
+
_register_emulation(_feature, _kwarg, _allowed, _default)
|
|
2326
|
+
|
|
2327
|
+
|
|
2252
2328
|
# -- Permissions -------------------------------------------------------------
|
|
2253
2329
|
|
|
2254
2330
|
|
|
@@ -2928,6 +3004,7 @@ _CODEGEN_RECORDABLE = {
|
|
|
2928
3004
|
"tab-select",
|
|
2929
3005
|
"upload",
|
|
2930
3006
|
"resize",
|
|
3007
|
+
*(f"{verb}-{feature}" for feature in _EMULATED_MEDIA for verb in ("set", "clear")),
|
|
2931
3008
|
"cookie-set",
|
|
2932
3009
|
"cookie-delete",
|
|
2933
3010
|
"cookie-clear",
|
|
@@ -3421,10 +3498,31 @@ def check_auth(msg: object, expected: str | None, port: int) -> str | None:
|
|
|
3421
3498
|
return None
|
|
3422
3499
|
|
|
3423
3500
|
|
|
3424
|
-
|
|
3425
|
-
|
|
3501
|
+
# A request is read before its token is checked, so any local process can
|
|
3502
|
+
# send one. Bound how long the whole request may take to arrive and how long
|
|
3503
|
+
# it may claim to be, so a client cannot hold a connection open forever or
|
|
3504
|
+
# make the daemon buffer a 4 GB body. Commands and responses are not bounded.
|
|
3505
|
+
REQUEST_READ_TIMEOUT = 30.0
|
|
3506
|
+
# The largest real request is an eval/run-code script read from --file or
|
|
3507
|
+
# stdin (a bundled library is a few MB); other args come from argv, which the
|
|
3508
|
+
# OS caps at about 2 MB. 64 MiB leaves an order of magnitude of headroom.
|
|
3509
|
+
MAX_REQUEST_BYTES = 64 * 1024 * 1024
|
|
3510
|
+
|
|
3511
|
+
|
|
3512
|
+
class RequestTooLarge(Exception):
|
|
3513
|
+
"""A client announced a request longer than MAX_REQUEST_BYTES."""
|
|
3514
|
+
|
|
3515
|
+
|
|
3516
|
+
async def _read_message(reader: asyncio.StreamReader, max_bytes: int = MAX_REQUEST_BYTES) -> dict | None:
|
|
3517
|
+
"""Read a length-prefixed JSON message from the stream.
|
|
3518
|
+
|
|
3519
|
+
Raises RequestTooLarge, without reading the body, when the announced
|
|
3520
|
+
length exceeds `max_bytes`.
|
|
3521
|
+
"""
|
|
3426
3522
|
header = await reader.readexactly(4)
|
|
3427
3523
|
length = struct.unpack("!I", header)[0]
|
|
3524
|
+
if length > max_bytes:
|
|
3525
|
+
raise RequestTooLarge(f"Daemon error: request of {length} bytes exceeds the {max_bytes}-byte limit.")
|
|
3428
3526
|
data = await reader.readexactly(length)
|
|
3429
3527
|
return json.loads(data.decode("utf-8"))
|
|
3430
3528
|
|
|
@@ -3445,7 +3543,15 @@ async def _handle_client(
|
|
|
3445
3543
|
addr = writer.get_extra_info("peername")
|
|
3446
3544
|
logger.debug("Client connected: %s", addr)
|
|
3447
3545
|
try:
|
|
3448
|
-
|
|
3546
|
+
try:
|
|
3547
|
+
msg = await asyncio.wait_for(_read_message(reader), REQUEST_READ_TIMEOUT)
|
|
3548
|
+
except asyncio.TimeoutError:
|
|
3549
|
+
logger.warning("Closed connection from %s: request not received within %ss", addr, REQUEST_READ_TIMEOUT)
|
|
3550
|
+
return
|
|
3551
|
+
except RequestTooLarge as e:
|
|
3552
|
+
logger.warning("Rejected oversized request from %s: %s", addr, e)
|
|
3553
|
+
await _write_message(writer, {"success": False, "output": str(e)})
|
|
3554
|
+
return
|
|
3449
3555
|
if msg is None:
|
|
3450
3556
|
return
|
|
3451
3557
|
port = writer.get_extra_info("sockname")[1]
|
patchright_cli/mcp_server.py
CHANGED
|
@@ -450,6 +450,61 @@ TOOL_SPECS = {
|
|
|
450
450
|
("session", "string", False, "default", "Named Patchright browser session."),
|
|
451
451
|
],
|
|
452
452
|
},
|
|
453
|
+
"patchright_set_color_scheme": {
|
|
454
|
+
"description": "CLI command: set-color-scheme Emulate the prefers-color-scheme media feature on every tab of the session.",
|
|
455
|
+
"params": [
|
|
456
|
+
("value", "string", True, None, "Dark, light or no-preference."),
|
|
457
|
+
("session", "string", False, "default", "Named Patchright browser session."),
|
|
458
|
+
],
|
|
459
|
+
},
|
|
460
|
+
"patchright_clear_color_scheme": {
|
|
461
|
+
"description": "CLI command: clear-color-scheme Stop emulating the prefers-color-scheme media feature.",
|
|
462
|
+
"params": [("session", "string", False, "default", "Named Patchright browser session.")],
|
|
463
|
+
},
|
|
464
|
+
"patchright_set_reduced_motion": {
|
|
465
|
+
"description": "CLI command: set-reduced-motion Emulate the prefers-reduced-motion media feature on every tab of the session.",
|
|
466
|
+
"params": [
|
|
467
|
+
("value", "string", True, None, "Reduce or no-preference."),
|
|
468
|
+
("session", "string", False, "default", "Named Patchright browser session."),
|
|
469
|
+
],
|
|
470
|
+
},
|
|
471
|
+
"patchright_clear_reduced_motion": {
|
|
472
|
+
"description": "CLI command: clear-reduced-motion Stop emulating the prefers-reduced-motion media feature.",
|
|
473
|
+
"params": [("session", "string", False, "default", "Named Patchright browser session.")],
|
|
474
|
+
},
|
|
475
|
+
"patchright_set_forced_colors": {
|
|
476
|
+
"description": "CLI command: set-forced-colors Emulate the forced-colors media feature on every tab of the session.",
|
|
477
|
+
"params": [
|
|
478
|
+
("value", "string", True, None, "Active or none."),
|
|
479
|
+
("session", "string", False, "default", "Named Patchright browser session."),
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
"patchright_clear_forced_colors": {
|
|
483
|
+
"description": "CLI command: clear-forced-colors Stop emulating the forced-colors media feature.",
|
|
484
|
+
"params": [("session", "string", False, "default", "Named Patchright browser session.")],
|
|
485
|
+
},
|
|
486
|
+
"patchright_set_contrast": {
|
|
487
|
+
"description": "CLI command: set-contrast Emulate the prefers-contrast media feature on every tab of the session.",
|
|
488
|
+
"params": [
|
|
489
|
+
("value", "string", True, None, "More or no-preference."),
|
|
490
|
+
("session", "string", False, "default", "Named Patchright browser session."),
|
|
491
|
+
],
|
|
492
|
+
},
|
|
493
|
+
"patchright_clear_contrast": {
|
|
494
|
+
"description": "CLI command: clear-contrast Stop emulating the prefers-contrast media feature.",
|
|
495
|
+
"params": [("session", "string", False, "default", "Named Patchright browser session.")],
|
|
496
|
+
},
|
|
497
|
+
"patchright_set_media": {
|
|
498
|
+
"description": "CLI command: set-media Emulate the CSS media type on every tab of the session.",
|
|
499
|
+
"params": [
|
|
500
|
+
("value", "string", True, None, "Screen or print."),
|
|
501
|
+
("session", "string", False, "default", "Named Patchright browser session."),
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
"patchright_clear_media": {
|
|
505
|
+
"description": "CLI command: clear-media Stop emulating the CSS media type.",
|
|
506
|
+
"params": [("session", "string", False, "default", "Named Patchright browser session.")],
|
|
507
|
+
},
|
|
453
508
|
"patchright_state_save": {
|
|
454
509
|
"description": "CLI command: state-save Save cookies and storage state.",
|
|
455
510
|
"params": [
|
|
@@ -961,6 +1016,16 @@ def _build_args(name: str, p: dict[str, Any]) -> list[str]:
|
|
|
961
1016
|
"patchright_sessionstorage_set": ("sessionstorage-set", ["key", "value"]),
|
|
962
1017
|
"patchright_sessionstorage_delete": ("sessionstorage-delete", ["key"]),
|
|
963
1018
|
"patchright_network_state_set": ("network-state-set", ["state"]),
|
|
1019
|
+
"patchright_set_color_scheme": ("set-color-scheme", ["value"]),
|
|
1020
|
+
"patchright_clear_color_scheme": ("clear-color-scheme", []),
|
|
1021
|
+
"patchright_set_reduced_motion": ("set-reduced-motion", ["value"]),
|
|
1022
|
+
"patchright_clear_reduced_motion": ("clear-reduced-motion", []),
|
|
1023
|
+
"patchright_set_forced_colors": ("set-forced-colors", ["value"]),
|
|
1024
|
+
"patchright_clear_forced_colors": ("clear-forced-colors", []),
|
|
1025
|
+
"patchright_set_contrast": ("set-contrast", ["value"]),
|
|
1026
|
+
"patchright_clear_contrast": ("clear-contrast", []),
|
|
1027
|
+
"patchright_set_media": ("set-media", ["value"]),
|
|
1028
|
+
"patchright_clear_media": ("clear-media", []),
|
|
964
1029
|
"patchright_tracing_stop": ("tracing-stop", []),
|
|
965
1030
|
"patchright_video_chapter": ("video-chapter", ["title"]),
|
|
966
1031
|
"patchright_codegen": ("codegen", []),
|
|
@@ -1270,6 +1335,16 @@ def _python_type(type_name: str) -> type:
|
|
|
1270
1335
|
return {"string": str, "boolean": bool, "number": int}[type_name]
|
|
1271
1336
|
|
|
1272
1337
|
|
|
1338
|
+
# Allowed values for each set-* emulation tool, in the daemon's order.
|
|
1339
|
+
_EMULATION_VALUES = {
|
|
1340
|
+
"patchright_set_color_scheme": ("dark", "light", "no-preference"),
|
|
1341
|
+
"patchright_set_reduced_motion": ("reduce", "no-preference"),
|
|
1342
|
+
"patchright_set_forced_colors": ("active", "none"),
|
|
1343
|
+
"patchright_set_contrast": ("more", "no-preference"),
|
|
1344
|
+
"patchright_set_media": ("screen", "print"),
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
|
|
1273
1348
|
def _parameter_type(tool_name: str, param_name: str, type_name: str) -> Any:
|
|
1274
1349
|
if tool_name == "patchright_attach" and param_name == "context":
|
|
1275
1350
|
return Literal["new", "host"]
|
|
@@ -1289,6 +1364,9 @@ def _parameter_type(tool_name: str, param_name: str, type_name: str) -> Any:
|
|
|
1289
1364
|
if tool_name == "patchright_network_state_set" and param_name == "state":
|
|
1290
1365
|
return Literal["online", "offline"]
|
|
1291
1366
|
|
|
1367
|
+
if tool_name in _EMULATION_VALUES and param_name == "value":
|
|
1368
|
+
return Literal[_EMULATION_VALUES[tool_name]]
|
|
1369
|
+
|
|
1292
1370
|
return _python_type(type_name)
|
|
1293
1371
|
|
|
1294
1372
|
|
patchright_cli/ref_registry.py
CHANGED
|
@@ -13,7 +13,32 @@ from typing import TYPE_CHECKING
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from patchright.async_api import Page
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# Playwright writes each node as `- role "name" [attrs]`, with the name
|
|
17
|
+
# JSON-encoded, except that a name which starts and ends with "/" is written
|
|
18
|
+
# as is (`- link /about/:`). A key containing YAML-special text (": ", " #")
|
|
19
|
+
# is wrapped in YAML single quotes, with ' doubled. A plain key never contains
|
|
20
|
+
# ":" followed by whitespace or the line end, so that marks where its value
|
|
21
|
+
# starts.
|
|
22
|
+
_ITEM_RE = re.compile(r"^\s*-\s+(?:'((?:[^']|'')*)'|((?:[^:]|:(?!\s|$))*))")
|
|
23
|
+
_KEY_RE = re.compile(r"^(\w+)(?:\s+(?:\"((?:[^\"\\]|\\.)*)\"|(/.*/)(?=(?:\s\[[^\]]*\])*$)))?")
|
|
24
|
+
|
|
25
|
+
# Playwright leaves out names longer than 900 characters, so an unnamed node
|
|
26
|
+
# is an element whose accessible name is empty or over that limit.
|
|
27
|
+
UNNAMED_RE = re.compile(r"^$|^[\s\S]{901,}$")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parse_node(line: str) -> tuple[str, str] | None:
|
|
31
|
+
"""Return (role, name) for an aria snapshot node line, or None."""
|
|
32
|
+
item = _ITEM_RE.match(line)
|
|
33
|
+
if not item:
|
|
34
|
+
return None
|
|
35
|
+
key = item.group(1).replace("''", "'") if item.group(1) is not None else item.group(2)
|
|
36
|
+
m = _KEY_RE.match(key)
|
|
37
|
+
if not m:
|
|
38
|
+
return None
|
|
39
|
+
if m.group(2) is not None:
|
|
40
|
+
return m.group(1), _unescape_name(m.group(2))
|
|
41
|
+
return m.group(1), m.group(3) or ""
|
|
17
42
|
|
|
18
43
|
|
|
19
44
|
def _unescape_name(raw: str) -> str:
|
|
@@ -145,8 +170,8 @@ class RefRegistry:
|
|
|
145
170
|
result_lines: list[str] = []
|
|
146
171
|
|
|
147
172
|
for line in aria_text.splitlines():
|
|
148
|
-
|
|
149
|
-
if
|
|
173
|
+
node = _parse_node(line)
|
|
174
|
+
if node is None:
|
|
150
175
|
result_lines.append(line)
|
|
151
176
|
continue
|
|
152
177
|
|
|
@@ -156,12 +181,11 @@ class RefRegistry:
|
|
|
156
181
|
result_lines.append(line)
|
|
157
182
|
continue
|
|
158
183
|
|
|
159
|
-
role =
|
|
184
|
+
role, name = node
|
|
160
185
|
|
|
161
186
|
if interactive_only and role not in INTERACTIVE_ROLES:
|
|
162
187
|
result_lines.append(line)
|
|
163
188
|
continue
|
|
164
|
-
name = _unescape_name(m.group(2)) if m.group(2) else ""
|
|
165
189
|
|
|
166
190
|
self._counter += 1
|
|
167
191
|
ref = f"e{self._counter}"
|
|
@@ -239,9 +263,9 @@ class RefRegistry:
|
|
|
239
263
|
if line_depth >= depth:
|
|
240
264
|
continue
|
|
241
265
|
depth = line_depth
|
|
242
|
-
node =
|
|
266
|
+
node = _parse_node(line)
|
|
243
267
|
if node:
|
|
244
|
-
chain.append(
|
|
268
|
+
chain.append(node)
|
|
245
269
|
if depth == 0:
|
|
246
270
|
break
|
|
247
271
|
|
|
@@ -281,12 +305,11 @@ class RefRegistry:
|
|
|
281
305
|
if not entry:
|
|
282
306
|
raise ValueError(f"Ref @{ref} not found. The page may have changed — run 'snapshot' to refresh.")
|
|
283
307
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
308
|
+
# Always filter by name, even for an unnamed ref: nth counts only the
|
|
309
|
+
# elements the snapshot shows with this name, so an unfiltered role
|
|
310
|
+
# query picks the wrong element when named and unnamed ones share a role.
|
|
288
311
|
scope = self.root if self.root is not None else page
|
|
289
|
-
locator = scope.get_by_role(entry.role,
|
|
312
|
+
locator = scope.get_by_role(entry.role, name=entry.name or UNNAMED_RE, exact=True)
|
|
290
313
|
return locator.nth(entry.nth)
|
|
291
314
|
|
|
292
315
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patchright-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
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
|
|
@@ -211,6 +211,16 @@ patchright-cli screenshot
|
|
|
211
211
|
patchright-cli close
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
Quote URLs that contain `&`; unquoted, bash, PowerShell and cmd.exe all split the command there. On Windows two cases need more than quotes: in cmd.exe without quotes, escape it as `^&` (inside quotes the `^` stays in the URL, so use one or the other), and when PowerShell runs a `.cmd`/`.bat` wrapper such as the repo's `patchright-cli.cmd`, PowerShell drops the quotes and cmd.exe splits at `&`, so pass the rest verbatim with `--%`:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
patchright-cli goto "https://example.com/?a=1&b=2" # any shell, patchright-cli.exe
|
|
218
|
+
patchright-cli goto https://example.com/?a=1^&b=2 # cmd.exe, unquoted
|
|
219
|
+
.\patchright-cli.cmd --% goto "https://example.com/?a=1&b=2" # PowerShell, .cmd wrapper
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Don't use `--%` with the installed `patchright-cli.exe`: in PowerShell 7 the quotes after it reach the CLI and end up in the URL.
|
|
223
|
+
|
|
214
224
|
---
|
|
215
225
|
|
|
216
226
|
## Architecture
|
|
@@ -236,6 +246,12 @@ The daemon only listens on `127.0.0.1`, but any local process can reach that por
|
|
|
236
246
|
- Run the CLI as the same user, with the same home directory, as the daemon. In Docker the MCP server and daemon share a user and `HOME`, so this holds.
|
|
237
247
|
- An older CLI talking to a newer daemon gets an error saying the daemon requires an auth token: upgrade, or stop the daemon (`patchright-cli kill-all` from the newer install, or end the process). A newer CLI talking to an older daemon finds no token file and sends none, which the older daemon accepts.
|
|
238
248
|
|
|
249
|
+
### Daemon client protocol
|
|
250
|
+
|
|
251
|
+
Third-party clients can use the daemon TCP interface directly. The
|
|
252
|
+
[client protocol reference](docs/daemon-protocol.md) defines discovery,
|
|
253
|
+
authentication, message framing, replies, and compatibility rules.
|
|
254
|
+
|
|
239
255
|
---
|
|
240
256
|
|
|
241
257
|
<details>
|
|
@@ -322,6 +338,22 @@ patchright-cli upload ./file.pdf e5 # Upload to specific input
|
|
|
322
338
|
patchright-cli resize 1920 1080 # Resize viewport
|
|
323
339
|
```
|
|
324
340
|
|
|
341
|
+
### Emulation
|
|
342
|
+
```bash
|
|
343
|
+
patchright-cli set-color-scheme dark # dark | light | no-preference
|
|
344
|
+
patchright-cli clear-color-scheme
|
|
345
|
+
patchright-cli set-reduced-motion reduce # reduce | no-preference
|
|
346
|
+
patchright-cli clear-reduced-motion
|
|
347
|
+
patchright-cli set-forced-colors active # active | none
|
|
348
|
+
patchright-cli clear-forced-colors
|
|
349
|
+
patchright-cli set-contrast more # more | no-preference
|
|
350
|
+
patchright-cli clear-contrast
|
|
351
|
+
patchright-cli set-media print # screen | print
|
|
352
|
+
patchright-cli clear-media
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Each setting holds for the whole session, including tabs and popups opened later; `clear-*` drops just that one, back to the browser default. A popup gets the setting just after it opens, so a script that reads media queries the instant the popup is created may still see the default.
|
|
356
|
+
|
|
325
357
|
### Tabs
|
|
326
358
|
```bash
|
|
327
359
|
patchright-cli tab-list
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
patchright_cli/__init__.py,sha256=
|
|
1
|
+
patchright_cli/__init__.py,sha256=2DOVMaTfG_CvRho_ERA9ANKCkBOJHqm6h7HhxtD9ctE,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=JO_c97zF6QE3O8hkFAt9YCKxBLKfmU1ayYuGNhIsmzM,38798
|
|
4
|
+
patchright_cli/daemon.py,sha256=sov1SzXNEfM_Hov8tkCx09ZGhBkIlBeRw6fRvTRunFM,151073
|
|
5
5
|
patchright_cli/dashboard.py,sha256=AfonvEkjzTR0Ao3iLHSXzy54lMNU6WgEgklaXdBz_is,16317
|
|
6
|
-
patchright_cli/mcp_server.py,sha256=
|
|
7
|
-
patchright_cli/ref_registry.py,sha256=
|
|
6
|
+
patchright_cli/mcp_server.py,sha256=cVhOyk_gRXLgxWTIuFHVUw3lO1IR-wIMdPgLO4zVJ3E,65527
|
|
7
|
+
patchright_cli/ref_registry.py,sha256=zpBH6yrSIyAlbpSDOv5p-HIVTVSx3iieKadPKQPD75A,11525
|
|
8
8
|
patchright_cli/snapshot.py,sha256=e_XAF5FPWFKeSv9HdbSHebRiBuu68o4rkLVauCoCUWY,1587
|
|
9
|
-
patchright_cli/_skills/patchright-cli/SKILL.md,sha256=
|
|
9
|
+
patchright_cli/_skills/patchright-cli/SKILL.md,sha256=RlgLGVvRNbETecVo5sX1uxin1k4IwO4ztQoTZ_hoBvw,20944
|
|
10
10
|
patchright_cli/_skills/patchright-cli/references/request-mocking.md,sha256=x4t_5gPxSsYvm0BlrZqbdM3MHqGVzNY_ac6Awg6PToc,2901
|
|
11
11
|
patchright_cli/_skills/patchright-cli/references/running-code.md,sha256=Ls0HhV8gxT5XOHfiv9-B8eBvYn-RR21wZ7Iyh3uskuk,3242
|
|
12
12
|
patchright_cli/_skills/patchright-cli/references/session-management.md,sha256=FLhKaF9FYAXVwPbz4wuEf73qmZuhoxqfE7Js1ktMzt8,7063
|
|
@@ -14,8 +14,8 @@ patchright_cli/_skills/patchright-cli/references/snapshot-refs.md,sha256=m9Q8G4W
|
|
|
14
14
|
patchright_cli/_skills/patchright-cli/references/storage-state.md,sha256=qfL_2v_WHHyVUIPj4efMU46KOMbYyvtLll4ha84mOHw,3348
|
|
15
15
|
patchright_cli/_skills/patchright-cli/references/tracing.md,sha256=6IwbGDsrtuB3bckB_7noyjpU37mdemIC435IuLilBZk,1931
|
|
16
16
|
patchright_cli/_skills/patchright-cli/references/video-recording.md,sha256=qbWjMmE9-Q6Mp8deG2Cf6QY2fL4GOkx1_-erSGkoN8I,3905
|
|
17
|
-
patchright_cli-0.9.
|
|
18
|
-
patchright_cli-0.9.
|
|
19
|
-
patchright_cli-0.9.
|
|
20
|
-
patchright_cli-0.9.
|
|
21
|
-
patchright_cli-0.9.
|
|
17
|
+
patchright_cli-0.9.1.dist-info/METADATA,sha256=Mw0vvLnmdHV3vGZtkJsPDoAlT3oDxWvXmIGFez9OFLE,30259
|
|
18
|
+
patchright_cli-0.9.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
19
|
+
patchright_cli-0.9.1.dist-info/entry_points.txt,sha256=Syh904eOQA8ylakyqIvG4LmIHx3LMjgeNcnFap3O0NQ,59
|
|
20
|
+
patchright_cli-0.9.1.dist-info/licenses/LICENSE,sha256=adszqL4YR4pQUrc7LeAFxeg5Mc2cvljXp7QPlzKVWm8,10760
|
|
21
|
+
patchright_cli-0.9.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|