patchright-cli 0.9.1__tar.gz → 0.10.0__tar.gz

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.
Files changed (73) hide show
  1. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/.claude-plugin/marketplace.json +1 -1
  2. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/CLAUDE.md +6 -3
  3. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/PKG-INFO +70 -8
  4. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/README.md +69 -7
  5. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/docs/daemon-protocol.md +38 -0
  6. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/pyproject.toml +2 -2
  7. patchright_cli-0.10.0/scripts/gen_skill.py +79 -0
  8. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/skills/patchright-cli/SKILL.md +169 -39
  9. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/src/patchright_cli/__init__.py +1 -1
  10. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/src/patchright_cli/__main__.py +1 -1
  11. patchright_cli-0.10.0/src/patchright_cli/_entry.py +26 -0
  12. patchright_cli-0.10.0/src/patchright_cli/agent_hooks.py +470 -0
  13. patchright_cli-0.10.0/src/patchright_cli/cli.py +1991 -0
  14. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/src/patchright_cli/daemon.py +173 -58
  15. patchright_cli-0.10.0/src/patchright_cli/daemon_pid.py +168 -0
  16. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/src/patchright_cli/ref_registry.py +7 -9
  17. patchright_cli-0.10.0/src/patchright_cli/toon.py +281 -0
  18. patchright_cli-0.10.0/tests/test_agent_hooks.py +416 -0
  19. patchright_cli-0.10.0/tests/test_cli_axi.py +539 -0
  20. patchright_cli-0.10.0/tests/test_cli_home.py +256 -0
  21. patchright_cli-0.10.0/tests/test_daemon_axi.py +485 -0
  22. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_daemon_handlers.py +4 -2
  23. patchright_cli-0.10.0/tests/test_daemon_pid.py +245 -0
  24. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_e2e.py +1 -1
  25. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_navigation_policy.py +1 -1
  26. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_profiles.py +16 -2
  27. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_ref_registry.py +36 -10
  28. patchright_cli-0.10.0/tests/test_skill_sync.py +60 -0
  29. patchright_cli-0.10.0/tests/test_toon.py +275 -0
  30. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/uv.lock +1 -1
  31. patchright_cli-0.9.1/src/patchright_cli/cli.py +0 -987
  32. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/.claude-plugin/skills/browser-automation.json +0 -0
  33. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/.github/dependabot.yml +0 -0
  34. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/.github/workflows/ci.yml +0 -0
  35. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/.github/workflows/publish-docker.yml +0 -0
  36. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/.github/workflows/publish.yml +0 -0
  37. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/.gitignore +0 -0
  38. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/.pre-commit-config.yaml +0 -0
  39. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/CONTRIBUTING.md +0 -0
  40. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/Dockerfile +0 -0
  41. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/GLOSSARY.md +0 -0
  42. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/LICENSE +0 -0
  43. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/Makefile +0 -0
  44. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/compose.yml +0 -0
  45. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/docs/installation.md +0 -0
  46. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/docs/mcp-architecture.svg +0 -0
  47. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/patchright-cli +0 -0
  48. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/patchright-cli.cmd +0 -0
  49. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/skills/patchright-cli/references/request-mocking.md +0 -0
  50. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/skills/patchright-cli/references/running-code.md +0 -0
  51. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/skills/patchright-cli/references/session-management.md +0 -0
  52. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/skills/patchright-cli/references/snapshot-refs.md +0 -0
  53. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/skills/patchright-cli/references/storage-state.md +0 -0
  54. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/skills/patchright-cli/references/tracing.md +0 -0
  55. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/skills/patchright-cli/references/video-recording.md +0 -0
  56. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/src/patchright_cli/dashboard.py +0 -0
  57. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/src/patchright_cli/mcp_server.py +0 -0
  58. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/src/patchright_cli/snapshot.py +0 -0
  59. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/__init__.py +0 -0
  60. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/fixture.html +0 -0
  61. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_annotate.py +0 -0
  62. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_attach.py +0 -0
  63. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_cli_config.py +0 -0
  64. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_daemon_info.py +0 -0
  65. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_daemon_read.py +0 -0
  66. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_daemon_token.py +0 -0
  67. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_docker.py +0 -0
  68. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_emulation.py +0 -0
  69. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_mcp.py +0 -0
  70. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_network_guard.py +0 -0
  71. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_packaging.py +0 -0
  72. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_supply_chain.py +0 -0
  73. {patchright_cli-0.9.1 → patchright_cli-0.10.0}/tests/test_tabs.py +0 -0
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Anti-detect browser automation CLI & Skills for AI agents, powered by Patchright",
8
- "version": "0.9.1"
8
+ "version": "0.10.0"
9
9
  },
10
10
  "plugins": [
11
11
  {
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Project Overview
6
6
 
7
- patchright-cli is an anti-detect browser automation CLI built on [Patchright](https://github.com/kaliiiiiiiiii/patchright-python) (undetected Playwright fork). It provides the same command interface as Microsoft's playwright-cli but bypasses bot detection (Akamai, Cloudflare, etc.).
7
+ patchright-cli is an anti-detect browser automation CLI built on [Patchright](https://github.com/kaliiiiiiiiii/patchright-python) (undetected Playwright fork). Its commands follow Microsoft's playwright-cli, but it bypasses bot detection (Akamai, Cloudflare, etc.) and is not a drop-in replacement: playwright-cli features Patchright cannot support (other browsers, extensions) are left out, and unknown flags are rejected with exit 2 rather than ignored.
8
8
 
9
9
  ## Development Setup
10
10
 
@@ -37,6 +37,9 @@ pytest tests/test_e2e.py -v
37
37
 
38
38
  # Lint
39
39
  pre-commit run --all-files
40
+
41
+ # SKILL.md command block in sync with COMMAND_SPECS (rewrite without --check)
42
+ python scripts/gen_skill.py --check
40
43
  ```
41
44
 
42
45
  ## Architecture
@@ -47,7 +50,7 @@ Three-component client-daemon-browser design:
47
50
  CLI (cli.py) --TCP:9321--> Daemon (daemon.py) --Patchright--> Chrome
48
51
  ```
49
52
 
50
- - **CLI (`src/patchright_cli/cli.py`)**: Stateless thin client. Parses argv manually (not click subcommands), builds a JSON `{command, args, options, cwd}` message, sends it over a length-prefixed TCP socket to the daemon, prints the response, and exits. All commands are defined in the `COMMANDS_HELP` dict and `ALL_COMMANDS` list.
53
+ - **CLI (`src/patchright_cli/cli.py`)**: Stateless thin client. Parses argv manually (not click subcommands), builds a JSON `{command, args, options, cwd}` message, sends it over a length-prefixed TCP socket to the daemon, prints the response, and exits. All commands are defined in the `COMMAND_SPECS` dict (usage, summary, flags, examples; `COMMANDS_HELP` and `ALL_COMMANDS` derive from it) and grouped for the manual in `COMMAND_CATEGORIES`. With no arguments it prints the home view (`_print_home`): live daemon state from an already-running daemon only, never starting one. `--toon` renders a reply's `data` field with `toon.py`.
51
54
 
52
55
  - **Daemon (`src/patchright_cli/daemon.py`)**: Long-running async process managing browser sessions. Auto-spawned on `open` via `ensure_daemon_running()` which launches a subprocess. Uses `DaemonState` → multiple `Session` objects, each owning a Patchright persistent browser context. Command dispatch uses `@register()` decorated handlers in `COMMAND_HANDLERS` dict, dispatched by `handle_command()`. Console capture uses CDP sessions (not Playwright's `page.on('console')`) because Patchright suppresses it.
53
56
 
@@ -59,7 +62,7 @@ CLI (cli.py) --TCP:9321--> Daemon (daemon.py) --Patchright--> Chrome
59
62
  - Headed by default (headless is more detectable)
60
63
  - Default daemon port: 9321, profiles stored at `~/.patchright-cli/profiles/<profile-name>`; a session's profile defaults to its session name (`--profile=<name>` overrides, a value with `/` or `\` is a directory). One session per profile at a time
61
64
  - CLI-daemon protocol: length-prefixed (4-byte big-endian `!I`) JSON over TCP; every message carries a `token` field read from `~/.patchright-cli/daemon-<port>.token` (written 0600 by the daemon before it listens), and the daemon rejects messages without the right one
62
- - CLI parses argv manually in `main()` — click is only used for `click.echo()`, not subcommands. New commands need a `COMMANDS_HELP` entry and a `@register("<name>")` handler in daemon.py; only session and lifecycle commands (open, attach, close, kill-all, ...) live in `handle_command()` itself
65
+ - CLI parses argv manually in `main()` — click is only used for `click.echo()`, not subcommands. New commands need a `COMMAND_SPECS` entry, a place in `COMMAND_CATEGORIES`, and a `@register("<name>")` handler in daemon.py, then `python scripts/gen_skill.py` to refresh the generated command block in `skills/patchright-cli/SKILL.md` (`--check` exits 1 when stale; `tests/test_skill_sync.py` enforces it); only session and lifecycle commands (open, attach, close, kill-all, ...) live in `handle_command()` itself
63
66
  - Version must be updated in `pyproject.toml`, `src/patchright_cli/__init__.py` and `.claude-plugin/marketplace.json` (then run `uv lock` to sync the lock file; `tests/test_packaging.py` checks they match)
64
67
  - `python -m patchright_cli` works via `__main__.py`
65
68
  - Uses hatchling as build backend, ruff for linting (line-length=120, target py310)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchright-cli
3
- Version: 0.9.1
3
+ Version: 0.10.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
@@ -46,7 +46,7 @@ Description-Content-Type: text/markdown
46
46
  [![GitHub issues](https://img.shields.io/github/issues/AhaiMk01/patchright-cli?label=Issues)](https://github.com/AhaiMk01/patchright-cli/issues)
47
47
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
48
48
 
49
- Anti-detect browser automation CLI. Same command interface as [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli) but using [Patchright](https://github.com/kaliiiiiiiiii/patchright-python) (undetected Playwright fork) to bypass bot detection.
49
+ Anti-detect browser automation CLI. Commands follow [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli), but it uses [Patchright](https://github.com/kaliiiiiiiiii/patchright-python) (undetected Playwright fork) to bypass bot detection.
50
50
 
51
51
  [Install](#install) · [Quick Start](#quick-start) · [Commands](#-commands) · [Snapshots](#snapshots) · [Agent Integration](#agent-integration)
52
52
 
@@ -55,7 +55,7 @@ Anti-detect browser automation CLI. Same command interface as [Microsoft's playw
55
55
  | | Feature |
56
56
  |---|---|
57
57
  | :shield: | Bypasses Akamai, Cloudflare, and other anti-bot systems |
58
- | :rocket: | Same command interface as playwright-cli — zero learning curve |
58
+ | :rocket: | playwright-cli's command names and arguments — little to relearn |
59
59
  | :robot: | Built for AI agents (Claude Code, Codex, etc.) with YAML snapshots |
60
60
  | :zap: | Daemon architecture — browser stays open between commands |
61
61
  | :package: | `pip install` / `uvx` — no Docker, no config files |
@@ -246,6 +246,10 @@ The daemon only listens on `127.0.0.1`, but any local process can reach that por
246
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.
247
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.
248
248
 
249
+ ### Stopping a stuck daemon
250
+
251
+ The daemon runs without a window. `kill-all` asks it to close every session and exit. If it does not answer within 20 seconds, or with `--force`, the CLI ends the daemon process and the browsers it launched (`taskkill /T` on Windows, the daemon's process group elsewhere). It finds the process through `~/.patchright-cli/daemon-<port>.pid`, which the daemon writes at startup with its PID and start time, and kills only a process whose start time still matches, so a reused PID is never touched. A daemon from a version that writes no PID file has to be ended by hand: its command line contains `patchright_cli.daemon`.
252
+
249
253
  ### Daemon client protocol
250
254
 
251
255
  Third-party clients can use the daemon TCP interface directly. The
@@ -441,6 +445,35 @@ patchright-cli console --clear # Clear after printing
441
445
  ```bash
442
446
  patchright-cli --raw eval --file=s.js # Raw output (no page/snapshot decoration)
443
447
  patchright-cli --raw snapshot > p.yml # Pipe raw snapshot to file
448
+ patchright-cli --json tab-list # Whole reply as JSON ({success, output, data, ...})
449
+ patchright-cli --toon tab-list # Structured result as TOON
450
+ ```
451
+
452
+ `--toon` prints a reply's structured `data` as [TOON](https://github.com/toon-format/spec), a compact
453
+ format for agents: `list`, `tab-list`, `cookie-list`, `localstorage-list`, `sessionstorage-list`,
454
+ `route-list`, `console`, `network`, `profile-list` and `find` give a table, `url` and `title` a single
455
+ field. A cut-short list adds `total` and a `help:` line. Commands without structured data, and errors,
456
+ print exactly as they do without the flag. `--json` and `--toon` together are a usage error.
457
+
458
+ ```
459
+ tabs[2]{index,title,url,active}:
460
+ 0,Example Domain,"https://example.com/",true
461
+ 1,"","about:blank",false
462
+ ```
463
+
464
+ Run with no arguments, `patchright-cli` prints a short home view instead of the manual (that is
465
+ `--help`): where the binary is, whether the daemon is running on the port (`--port=N` picks another),
466
+ the open sessions with their URL and tab count, and what to run next. It only asks a daemon that is
467
+ already running and never starts one. Add `--toon` or `--json` for the same state as data.
468
+
469
+ ```
470
+ ~/.local/bin/patchright-cli v0.10.0 -- anti-detect Chrome automation CLI (playwright-cli style commands)
471
+ daemon: running on port 9321 (1 session)
472
+ sessions[1]{name,status,url,tabs}:
473
+ default,open,"https://example.com/",2
474
+ help: patchright-cli snapshot (element refs for the current page; -s=<name> for another session)
475
+ help: patchright-cli list (sessions and their tabs)
476
+ help: patchright-cli --help (all commands); patchright-cli <command> --help (one command)
444
477
  ```
445
478
 
446
479
  ### Setup
@@ -455,7 +488,8 @@ patchright-cli -s=mysession click e6
455
488
  patchright-cli -s=mysession close
456
489
  patchright-cli list # List all sessions
457
490
  patchright-cli close-all
458
- patchright-cli kill-all
491
+ patchright-cli kill-all # Stop the daemon; force-stops it if it does not answer
492
+ patchright-cli kill-all --force # Kill the daemon and its browsers without asking
459
493
  patchright-cli delete-data # Delete the profile named after the session
460
494
  patchright-cli profile-list # Profiles, size, and which session uses each
461
495
  patchright-cli profile-delete <name> # Delete an idle named profile
@@ -468,10 +502,36 @@ patchright-cli --port=9322 open # Custom daemon port
468
502
 
469
503
  ## Agent Integration
470
504
 
471
- Works with Claude Code, Cursor, Gemini CLI, Codex, and [40+ other agents](https://github.com/vercel-labs/skills).
505
+ The CLI follows the [AXI](https://github.com/kunchenguid/axi) standard for agent-facing tools: errors and hints on stdout, exit 2 for usage errors, `(no-op)` for repeated actions, and a no-argument home view with live state.
506
+
507
+ There are two ways to teach your agent about patchright-cli. Both get it to the same place, so you only need one.
508
+
509
+ | | Session hook | Skill |
510
+ |---|---|---|
511
+ | Install | `patchright-cli install --hooks` | `npx skills add AhaiMk01/patchright-cli` or `patchright-cli install --skills` |
512
+ | What the agent gets | Live state (open sessions, the commands to drive them) at the start of every session | Full command reference, loaded when a browser task comes up |
513
+ | Cost | A few lines of context in every session | Nothing until it is needed |
514
+ | Agents | Claude Code, Codex, OpenCode | Claude Code, Cursor, Gemini CLI, Codex and [40+ others](https://github.com/vercel-labs/skills) |
515
+
516
+ ### Session hook (recommended)
517
+
518
+ ```bash
519
+ patchright-cli install --hooks # install or repair
520
+ patchright-cli install --hooks --uninstall # remove
521
+ ```
522
+
523
+ At the start of each session the agent runs `patchright-cli` with no arguments, which prints the home view (see [Output](#output)): whether the daemon is up and which sessions are open, so the agent can pick up an open browser session without asking. The hook goes only to apps whose config directory exists, and prints one line per app (`installed`, `updated`, `(no-op)` or `skipped`):
524
+
525
+ - **Claude Code**: a `SessionStart` hook in `~/.claude/settings.json`
526
+ - **Codex**: a `SessionStart` hook in `~/.codex/hooks.json`, plus `hooks = true` under `[features]` in `~/.codex/config.toml`
527
+ - **OpenCode**: a managed plugin at `~/.config/opencode/plugins/patchright-cli.js` that adds the dashboard to the system prompt
528
+
529
+ The hook calls plain `patchright-cli` when that name on your `PATH` is the same binary you ran the install with, and its absolute path otherwise. Re-run `install --hooks` after reinstalling or moving patchright-cli and it rewrites the path. Your other hooks and settings are left as they are, and `--uninstall` removes only patchright-cli's entries (the Codex `[features]` flag stays, since other hooks may need it).
530
+
531
+ ### Skill
472
532
 
473
533
  ```bash
474
- # Install skill for all detected agents (recommended)
534
+ # Install skill for all detected agents
475
535
  npx skills add AhaiMk01/patchright-cli
476
536
  ```
477
537
 
@@ -562,7 +622,7 @@ URLs passed to `patchright_open`, `patchright_goto` and `patchright_tab_new` mus
562
622
  <details>
563
623
  <summary><h2>Differences from playwright-cli</h2></summary>
564
624
 
565
- patchright-cli aims for full command parity with [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli). The following playwright-cli features are **intentionally not implemented** due to Patchright's architecture:
625
+ patchright-cli follows the command names and arguments of [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli) where Patchright allows it, but it is **not a drop-in replacement**. Some playwright-cli features cannot work on Patchright and are not implemented:
566
626
 
567
627
  | Feature | Reason |
568
628
  |---------|--------|
@@ -570,7 +630,7 @@ patchright-cli aims for full command parity with [Microsoft's playwright-cli](ht
570
630
  | `--extension` | Browser extension connection not supported. Patchright uses CDP directly. |
571
631
  | `install` / `install-browser` | Use `python -m patchright install chromium` instead. |
572
632
 
573
- All other commands and options are fully supported.
633
+ Each command accepts only the flags it implements. A playwright-cli flag patchright-cli does not support, such as `open --browser=firefox`, is rejected with `error: unknown flag` and exit code 2 rather than silently ignored, so a script ported from playwright-cli fails loudly where it would otherwise behave differently. `patchright-cli <command> --help` lists a command's flags.
574
634
 
575
635
  </details>
576
636
 
@@ -590,6 +650,8 @@ This project builds on the work of:
590
650
  - **[Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright)** by [@Kaliiiiiiiiii-Vinyzu](https://github.com/Kaliiiiiiiiii-Vinyzu) — the undetected Playwright fork that makes this possible. All anti-detect capabilities come from Patchright.
591
651
  - **[playwright-cli](https://github.com/microsoft/playwright-cli)** by Microsoft — the original CLI that patchright-cli's command interface is modeled after.
592
652
  - **[camoufox-cli](https://github.com/Bin-Huang/camoufox-cli)** by [@Bin-Huang](https://github.com/Bin-Huang) — inspiration for the CLI-first approach to anti-detect browser automation and the SKILL.md agent integration pattern.
653
+ - **[AXI (Agent eXperience Interface)](https://github.com/kunchenguid/axi)** by [@kunchenguid](https://github.com/kunchenguid) — the standard for agent-facing CLIs that shaped the home view, strict per-command flags, `error:`/`help:` output, `(no-op)` replies, explicit empty states and the session-start hook.
654
+ - **[TOON](https://github.com/toon-format/spec)** — the token-oriented output format behind `--toon`.
593
655
 
594
656
  ---
595
657
 
@@ -9,7 +9,7 @@
9
9
  [![GitHub issues](https://img.shields.io/github/issues/AhaiMk01/patchright-cli?label=Issues)](https://github.com/AhaiMk01/patchright-cli/issues)
10
10
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
11
11
 
12
- Anti-detect browser automation CLI. Same command interface as [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli) but using [Patchright](https://github.com/kaliiiiiiiiii/patchright-python) (undetected Playwright fork) to bypass bot detection.
12
+ Anti-detect browser automation CLI. Commands follow [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli), but it uses [Patchright](https://github.com/kaliiiiiiiiii/patchright-python) (undetected Playwright fork) to bypass bot detection.
13
13
 
14
14
  [Install](#install) · [Quick Start](#quick-start) · [Commands](#-commands) · [Snapshots](#snapshots) · [Agent Integration](#agent-integration)
15
15
 
@@ -18,7 +18,7 @@ Anti-detect browser automation CLI. Same command interface as [Microsoft's playw
18
18
  | | Feature |
19
19
  |---|---|
20
20
  | :shield: | Bypasses Akamai, Cloudflare, and other anti-bot systems |
21
- | :rocket: | Same command interface as playwright-cli — zero learning curve |
21
+ | :rocket: | playwright-cli's command names and arguments — little to relearn |
22
22
  | :robot: | Built for AI agents (Claude Code, Codex, etc.) with YAML snapshots |
23
23
  | :zap: | Daemon architecture — browser stays open between commands |
24
24
  | :package: | `pip install` / `uvx` — no Docker, no config files |
@@ -209,6 +209,10 @@ The daemon only listens on `127.0.0.1`, but any local process can reach that por
209
209
  - 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.
210
210
  - 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.
211
211
 
212
+ ### Stopping a stuck daemon
213
+
214
+ The daemon runs without a window. `kill-all` asks it to close every session and exit. If it does not answer within 20 seconds, or with `--force`, the CLI ends the daemon process and the browsers it launched (`taskkill /T` on Windows, the daemon's process group elsewhere). It finds the process through `~/.patchright-cli/daemon-<port>.pid`, which the daemon writes at startup with its PID and start time, and kills only a process whose start time still matches, so a reused PID is never touched. A daemon from a version that writes no PID file has to be ended by hand: its command line contains `patchright_cli.daemon`.
215
+
212
216
  ### Daemon client protocol
213
217
 
214
218
  Third-party clients can use the daemon TCP interface directly. The
@@ -404,6 +408,35 @@ patchright-cli console --clear # Clear after printing
404
408
  ```bash
405
409
  patchright-cli --raw eval --file=s.js # Raw output (no page/snapshot decoration)
406
410
  patchright-cli --raw snapshot > p.yml # Pipe raw snapshot to file
411
+ patchright-cli --json tab-list # Whole reply as JSON ({success, output, data, ...})
412
+ patchright-cli --toon tab-list # Structured result as TOON
413
+ ```
414
+
415
+ `--toon` prints a reply's structured `data` as [TOON](https://github.com/toon-format/spec), a compact
416
+ format for agents: `list`, `tab-list`, `cookie-list`, `localstorage-list`, `sessionstorage-list`,
417
+ `route-list`, `console`, `network`, `profile-list` and `find` give a table, `url` and `title` a single
418
+ field. A cut-short list adds `total` and a `help:` line. Commands without structured data, and errors,
419
+ print exactly as they do without the flag. `--json` and `--toon` together are a usage error.
420
+
421
+ ```
422
+ tabs[2]{index,title,url,active}:
423
+ 0,Example Domain,"https://example.com/",true
424
+ 1,"","about:blank",false
425
+ ```
426
+
427
+ Run with no arguments, `patchright-cli` prints a short home view instead of the manual (that is
428
+ `--help`): where the binary is, whether the daemon is running on the port (`--port=N` picks another),
429
+ the open sessions with their URL and tab count, and what to run next. It only asks a daemon that is
430
+ already running and never starts one. Add `--toon` or `--json` for the same state as data.
431
+
432
+ ```
433
+ ~/.local/bin/patchright-cli v0.10.0 -- anti-detect Chrome automation CLI (playwright-cli style commands)
434
+ daemon: running on port 9321 (1 session)
435
+ sessions[1]{name,status,url,tabs}:
436
+ default,open,"https://example.com/",2
437
+ help: patchright-cli snapshot (element refs for the current page; -s=<name> for another session)
438
+ help: patchright-cli list (sessions and their tabs)
439
+ help: patchright-cli --help (all commands); patchright-cli <command> --help (one command)
407
440
  ```
408
441
 
409
442
  ### Setup
@@ -418,7 +451,8 @@ patchright-cli -s=mysession click e6
418
451
  patchright-cli -s=mysession close
419
452
  patchright-cli list # List all sessions
420
453
  patchright-cli close-all
421
- patchright-cli kill-all
454
+ patchright-cli kill-all # Stop the daemon; force-stops it if it does not answer
455
+ patchright-cli kill-all --force # Kill the daemon and its browsers without asking
422
456
  patchright-cli delete-data # Delete the profile named after the session
423
457
  patchright-cli profile-list # Profiles, size, and which session uses each
424
458
  patchright-cli profile-delete <name> # Delete an idle named profile
@@ -431,10 +465,36 @@ patchright-cli --port=9322 open # Custom daemon port
431
465
 
432
466
  ## Agent Integration
433
467
 
434
- Works with Claude Code, Cursor, Gemini CLI, Codex, and [40+ other agents](https://github.com/vercel-labs/skills).
468
+ The CLI follows the [AXI](https://github.com/kunchenguid/axi) standard for agent-facing tools: errors and hints on stdout, exit 2 for usage errors, `(no-op)` for repeated actions, and a no-argument home view with live state.
469
+
470
+ There are two ways to teach your agent about patchright-cli. Both get it to the same place, so you only need one.
471
+
472
+ | | Session hook | Skill |
473
+ |---|---|---|
474
+ | Install | `patchright-cli install --hooks` | `npx skills add AhaiMk01/patchright-cli` or `patchright-cli install --skills` |
475
+ | What the agent gets | Live state (open sessions, the commands to drive them) at the start of every session | Full command reference, loaded when a browser task comes up |
476
+ | Cost | A few lines of context in every session | Nothing until it is needed |
477
+ | Agents | Claude Code, Codex, OpenCode | Claude Code, Cursor, Gemini CLI, Codex and [40+ others](https://github.com/vercel-labs/skills) |
478
+
479
+ ### Session hook (recommended)
480
+
481
+ ```bash
482
+ patchright-cli install --hooks # install or repair
483
+ patchright-cli install --hooks --uninstall # remove
484
+ ```
485
+
486
+ At the start of each session the agent runs `patchright-cli` with no arguments, which prints the home view (see [Output](#output)): whether the daemon is up and which sessions are open, so the agent can pick up an open browser session without asking. The hook goes only to apps whose config directory exists, and prints one line per app (`installed`, `updated`, `(no-op)` or `skipped`):
487
+
488
+ - **Claude Code**: a `SessionStart` hook in `~/.claude/settings.json`
489
+ - **Codex**: a `SessionStart` hook in `~/.codex/hooks.json`, plus `hooks = true` under `[features]` in `~/.codex/config.toml`
490
+ - **OpenCode**: a managed plugin at `~/.config/opencode/plugins/patchright-cli.js` that adds the dashboard to the system prompt
491
+
492
+ The hook calls plain `patchright-cli` when that name on your `PATH` is the same binary you ran the install with, and its absolute path otherwise. Re-run `install --hooks` after reinstalling or moving patchright-cli and it rewrites the path. Your other hooks and settings are left as they are, and `--uninstall` removes only patchright-cli's entries (the Codex `[features]` flag stays, since other hooks may need it).
493
+
494
+ ### Skill
435
495
 
436
496
  ```bash
437
- # Install skill for all detected agents (recommended)
497
+ # Install skill for all detected agents
438
498
  npx skills add AhaiMk01/patchright-cli
439
499
  ```
440
500
 
@@ -525,7 +585,7 @@ URLs passed to `patchright_open`, `patchright_goto` and `patchright_tab_new` mus
525
585
  <details>
526
586
  <summary><h2>Differences from playwright-cli</h2></summary>
527
587
 
528
- patchright-cli aims for full command parity with [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli). The following playwright-cli features are **intentionally not implemented** due to Patchright's architecture:
588
+ patchright-cli follows the command names and arguments of [Microsoft's playwright-cli](https://github.com/microsoft/playwright-cli) where Patchright allows it, but it is **not a drop-in replacement**. Some playwright-cli features cannot work on Patchright and are not implemented:
529
589
 
530
590
  | Feature | Reason |
531
591
  |---------|--------|
@@ -533,7 +593,7 @@ patchright-cli aims for full command parity with [Microsoft's playwright-cli](ht
533
593
  | `--extension` | Browser extension connection not supported. Patchright uses CDP directly. |
534
594
  | `install` / `install-browser` | Use `python -m patchright install chromium` instead. |
535
595
 
536
- All other commands and options are fully supported.
596
+ Each command accepts only the flags it implements. A playwright-cli flag patchright-cli does not support, such as `open --browser=firefox`, is rejected with `error: unknown flag` and exit code 2 rather than silently ignored, so a script ported from playwright-cli fails loudly where it would otherwise behave differently. `patchright-cli <command> --help` lists a command's flags.
537
597
 
538
598
  </details>
539
599
 
@@ -553,6 +613,8 @@ This project builds on the work of:
553
613
  - **[Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright)** by [@Kaliiiiiiiiii-Vinyzu](https://github.com/Kaliiiiiiiiii-Vinyzu) — the undetected Playwright fork that makes this possible. All anti-detect capabilities come from Patchright.
554
614
  - **[playwright-cli](https://github.com/microsoft/playwright-cli)** by Microsoft — the original CLI that patchright-cli's command interface is modeled after.
555
615
  - **[camoufox-cli](https://github.com/Bin-Huang/camoufox-cli)** by [@Bin-Huang](https://github.com/Bin-Huang) — inspiration for the CLI-first approach to anti-detect browser automation and the SKILL.md agent integration pattern.
616
+ - **[AXI (Agent eXperience Interface)](https://github.com/kunchenguid/axi)** by [@kunchenguid](https://github.com/kunchenguid) — the standard for agent-facing CLIs that shaped the home view, strict per-command flags, `error:`/`help:` output, `(no-op)` replies, explicit empty states and the session-start hook.
617
+ - **[TOON](https://github.com/toon-format/spec)** — the token-oriented output format behind `--toon`.
556
618
 
557
619
  ---
558
620
 
@@ -86,6 +86,44 @@ this table are part of the protocol:
86
86
  | `rejected the auth token` | The supplied token is invalid or stale. Read the token file fresh before retrying. |
87
87
  | `Daemon error: request of` | The request exceeded the 64 MiB limit. |
88
88
 
89
+ ### Idempotent no-ops
90
+
91
+ A mutation whose target state already holds succeeds instead of failing:
92
+ `success` is `true` and `output` ends with ` (no-op)`. This covers `close` and
93
+ `detach` of a session that is not open (the output still contains
94
+ `Session '<name>' is not open`), `close` of a named tab that is not open,
95
+ `video-stop`, `tracing-stop` and `codegen-stop` with nothing running,
96
+ `video-start` and `tracing-start` while already running, `video-hide-actions`
97
+ when callouts are off, `unroute` of a pattern that is not routed (or with no
98
+ routes at all), `cookie-delete`, `localstorage-delete` and
99
+ `sessionstorage-delete` of an absent name or key, `highlight --hide` with
100
+ nothing highlighted, and `profile-delete` of a profile that does not exist.
101
+ `go-back` and `go-forward` at the edge of the history still fail.
102
+
103
+ ### Structured data
104
+
105
+ Some replies carry an optional `data` field next to `output`, holding the same
106
+ result as JSON. `output` stays the human-readable text. When a list reply shows
107
+ fewer items than exist, `total` holds the full count.
108
+
109
+ | Command | `data` |
110
+ | --- | --- |
111
+ | `url` | `{"url"}` |
112
+ | `title` | `{"title"}` |
113
+ | `list` | `[{"name", "status", "url"}]`, where `status` is `open` or `attached` |
114
+ | `tab-list` | `[{"index", "title", "url", "active"}]` |
115
+ | `cookie-list` | `[{"name", "domain", "expires"}]` |
116
+ | `localstorage-list`, `sessionstorage-list` | `[{"key", "value"}]` |
117
+ | `route-list` | `[{"pattern", "status"}]` |
118
+ | `console` | `[{"level", "text"}]`, plus `total` when older messages are cut off |
119
+ | `network` (alias `requests`) | `[{"id", "method", "status", "url"}]`, plus `total` when older requests are cut off |
120
+ | `profile-list` | `[{"name", "size", "session"}]`, where `size` is bytes and `session` is the holding session or `null` |
121
+ | `find` | `[{"ref", "role", "name"}]`, plus `total` when `--limit` cut the hits |
122
+
123
+ An empty list is `[]`, and its `output` states the zero explicitly, for example
124
+ `0 cookies in this session`. Commands not listed here send no `data`. More
125
+ fields or commands can gain `data` later, so read only the keys you need.
126
+
89
127
  Best effort, not covered by the compatibility rules below: a command on a page,
90
128
  context or browser that has closed usually fails with Playwright's own message
91
129
  containing `has been closed`. That text comes from Playwright and can change
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "patchright-cli"
3
- version = "0.9.1"
3
+ version = "0.10.0"
4
4
  description = "Anti-detect browser automation CLI using Patchright (undetected Playwright fork)"
5
5
  license = "Apache-2.0"
6
6
  requires-python = ">=3.10"
@@ -37,7 +37,7 @@ Repository = "https://github.com/AhaiMk01/patchright-cli"
37
37
  Issues = "https://github.com/AhaiMk01/patchright-cli/issues"
38
38
 
39
39
  [project.scripts]
40
- patchright-cli = "patchright_cli.cli:main"
40
+ patchright-cli = "patchright_cli._entry:main"
41
41
 
42
42
  [build-system]
43
43
  requires = ["hatchling"]
@@ -0,0 +1,79 @@
1
+ """Regenerate the command reference block in skills/patchright-cli/SKILL.md.
2
+
3
+ The rest of SKILL.md is hand-written. Only the text between BEGIN and END is
4
+ generated, from COMMAND_SPECS and COMMAND_CATEGORIES in cli.py, so a new or
5
+ renamed command reaches the skill agents read.
6
+
7
+ python scripts/gen_skill.py # rewrite the block in place
8
+ python scripts/gen_skill.py --check # exit 1 when the block is stale
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from patchright_cli.cli import COMMAND_CATEGORIES, COMMAND_SPECS
18
+
19
+ SKILL_PATH = Path(__file__).resolve().parent.parent / "skills" / "patchright-cli" / "SKILL.md"
20
+ BEGIN = "<!-- BEGIN GENERATED: commands (scripts/gen_skill.py from COMMAND_SPECS; do not edit by hand) -->"
21
+ END = "<!-- END GENERATED -->"
22
+ _WIDTH = 40
23
+
24
+
25
+ def _line(name: str) -> str:
26
+ spec = COMMAND_SPECS[name]
27
+ flags = " ".join(f"[--{f}]" for f in spec.get("flags", {}))
28
+ usage = f"{spec['usage']} {flags}".strip()
29
+ return f"{usage}{' ' * max(_WIDTH - len(usage), 2)}# {spec['summary']}"
30
+
31
+
32
+ def render_block() -> str:
33
+ """The generated block, markers included, every command once in manual order."""
34
+ listed = [name for _, names in COMMAND_CATEGORIES for name in names]
35
+ missing = [name for name in COMMAND_SPECS if name not in listed]
36
+ if missing:
37
+ raise SystemExit(f"commands missing from COMMAND_CATEGORIES: {', '.join(missing)}")
38
+ lines = [BEGIN, "```bash"]
39
+ for i, (category, names) in enumerate(COMMAND_CATEGORIES):
40
+ if i:
41
+ lines.append("")
42
+ lines.append(f"# {category}")
43
+ lines.extend(_line(name) for name in names)
44
+ lines += ["```", END]
45
+ return "\n".join(lines)
46
+
47
+
48
+ def sync(text: str) -> str:
49
+ """`text` with its generated block replaced by the current one."""
50
+ start, end = text.find(BEGIN), text.find(END)
51
+ if start < 0 or end < start:
52
+ raise SystemExit(f"SKILL.md has no generated block; add the lines\n{BEGIN}\n{END}")
53
+ return text[:start] + render_block() + text[end + len(END) :]
54
+
55
+
56
+ def main(argv: list[str] | None = None) -> int:
57
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
58
+ parser.add_argument("--check", action="store_true", help="exit 1 when the block is stale; change nothing")
59
+ parser.add_argument("--path", type=Path, default=SKILL_PATH, help="SKILL.md to sync (default: the bundled one)")
60
+ opts = parser.parse_args(argv)
61
+
62
+ raw = opts.path.read_bytes().decode("utf-8")
63
+ newline = "\r\n" if "\r\n" in raw else "\n"
64
+ text = raw.replace("\r\n", "\n")
65
+ synced = sync(text)
66
+ if synced == text:
67
+ print(f"{opts.path.name}: command block is current")
68
+ return 0
69
+ if opts.check:
70
+ print(f"error: {opts.path.name} command block is stale")
71
+ print("help: run `python scripts/gen_skill.py` and commit the result")
72
+ return 1
73
+ opts.path.write_bytes(synced.replace("\n", newline).encode("utf-8"))
74
+ print(f"{opts.path.name}: command block updated")
75
+ return 0
76
+
77
+
78
+ if __name__ == "__main__":
79
+ sys.exit(main())