patchright-cli 0.4.0__tar.gz → 0.5.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 (47) hide show
  1. patchright_cli-0.5.0/.claude-plugin/marketplace.json +20 -0
  2. patchright_cli-0.5.0/.github/workflows/ci.yml +34 -0
  3. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/.gitignore +1 -0
  4. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/CLAUDE.md +14 -3
  5. patchright_cli-0.5.0/Makefile +18 -0
  6. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/PKG-INFO +36 -3
  7. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/README.md +30 -2
  8. patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-cdp-attach.md +297 -0
  9. patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-cli-enhancements.md +529 -0
  10. patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-codegen.md +234 -0
  11. patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-context-config-and-permissions.md +411 -0
  12. patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-show-dashboard.md +337 -0
  13. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/pyproject.toml +12 -1
  14. patchright_cli-0.5.0/skills/patchright-cli/SKILL.md +348 -0
  15. patchright_cli-0.5.0/skills/patchright-cli/references/request-mocking.md +103 -0
  16. patchright_cli-0.5.0/skills/patchright-cli/references/running-code.md +129 -0
  17. patchright_cli-0.5.0/skills/patchright-cli/references/session-management.md +106 -0
  18. patchright_cli-0.5.0/skills/patchright-cli/references/snapshot-refs.md +86 -0
  19. patchright_cli-0.5.0/skills/patchright-cli/references/storage-state.md +113 -0
  20. patchright_cli-0.5.0/skills/patchright-cli/references/tracing.md +53 -0
  21. patchright_cli-0.5.0/skills/patchright-cli/references/video-recording.md +85 -0
  22. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/__init__.py +1 -1
  23. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/cli.py +219 -10
  24. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/daemon.py +292 -36
  25. patchright_cli-0.5.0/src/patchright_cli/dashboard.py +135 -0
  26. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/ref_registry.py +33 -1
  27. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/snapshot.py +4 -2
  28. patchright_cli-0.5.0/tests/test_cli_config.py +203 -0
  29. patchright_cli-0.5.0/tests/test_daemon_handlers.py +306 -0
  30. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/tests/test_ref_registry.py +62 -0
  31. patchright_cli-0.5.0/uv.lock +1154 -0
  32. patchright_cli-0.4.0/.claude-plugin/marketplace.json +0 -21
  33. patchright_cli-0.4.0/skills/patchright-cli/SKILL.md +0 -492
  34. patchright_cli-0.4.0/tests/test_daemon_handlers.py +0 -124
  35. patchright_cli-0.4.0/uv.lock +0 -360
  36. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/.claude-plugin/skills/browser-automation.json +0 -0
  37. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/.github/workflows/publish.yml +0 -0
  38. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/.pre-commit-config.yaml +0 -0
  39. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/CONTRIBUTING.md +0 -0
  40. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/LICENSE +0 -0
  41. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/docs/installation.md +0 -0
  42. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/patchright-cli +0 -0
  43. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/patchright-cli.cmd +0 -0
  44. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/__main__.py +0 -0
  45. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/tests/__init__.py +0 -0
  46. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/tests/fixture.html +0 -0
  47. {patchright_cli-0.4.0 → patchright_cli-0.5.0}/tests/test_e2e.py +0 -0
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "patchright-cli",
3
+ "owner": {
4
+ "name": "Han Zhu"
5
+ },
6
+ "metadata": {
7
+ "description": "Anti-detect browser automation CLI & Skills for AI agents, powered by Patchright",
8
+ "version": "0.5.0"
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "patchright-cli",
13
+ "description": "Anti-detect browser automation skill using Patchright (undetected Playwright fork)",
14
+ "source": "./",
15
+ "skills": [
16
+ "./skills/patchright-cli"
17
+ ]
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v6
15
+ with:
16
+ version: ">=0.7,<0.8"
17
+ python-version: "3.12"
18
+ - run: uv venv && uv pip install -e ".[dev]"
19
+ - run: uv run pre-commit run --all-files
20
+
21
+ test:
22
+ strategy:
23
+ matrix:
24
+ os: [ubuntu-latest, windows-latest]
25
+ python-version: ["3.10", "3.12"]
26
+ runs-on: ${{ matrix.os }}
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: astral-sh/setup-uv@v6
30
+ with:
31
+ version: ">=0.7,<0.8"
32
+ python-version: ${{ matrix.python-version }}
33
+ - run: uv venv && uv pip install -e ".[dev]"
34
+ - run: uv run pytest tests/ -v --ignore=tests/test_e2e.py
@@ -6,3 +6,4 @@ __pycache__/
6
6
  dist/
7
7
  build/
8
8
  docs/reddit-post.md
9
+ .worktrees/
@@ -26,7 +26,18 @@ python -m patchright_cli <command> [args...]
26
26
  patchright-cli <command> [args...]
27
27
  ```
28
28
 
29
- There are no automated tests yet — changes are tested manually via the CLI.
29
+ ## Testing
30
+
31
+ ```bash
32
+ # Unit tests (no browser required)
33
+ pytest tests/ -v --ignore=tests/test_e2e.py
34
+
35
+ # E2E tests (requires Chrome)
36
+ pytest tests/test_e2e.py -v
37
+
38
+ # Lint
39
+ pre-commit run --all-files
40
+ ```
30
41
 
31
42
  ## Architecture
32
43
 
@@ -38,9 +49,9 @@ CLI (cli.py) --TCP:9321--> Daemon (daemon.py) --Patchright--> Chrome
38
49
 
39
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.
40
51
 
41
- - **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 is a large `handle_command()` function with if/elif branches. Console capture uses CDP sessions (not Playwright's `page.on('console')`) because Patchright suppresses it.
52
+ - **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.
42
53
 
43
- - **Snapshot (`src/patchright_cli/snapshot.py`)**: Two-pass DOM scanner injected as JavaScript: Pass 1 uses TreeWalker to assign `data-patchright-ref` attributes (`e1`, `e2`, ...) in document order; Pass 2 builds a nested tree. Python side converts to flat YAML. Snapshots are saved to `.patchright-cli/` in the caller's cwd.
54
+ - **Snapshot (`src/patchright_cli/snapshot.py`)**: Uses Playwright's native `aria_snapshot()` to get the accessibility tree. `RefRegistry` annotates each node with sequential refs (`e1`, `e2`, ...). Snapshots are saved as YAML to `.patchright-cli/` in the caller's cwd.
44
55
 
45
56
  ## Key Design Decisions
46
57
 
@@ -0,0 +1,18 @@
1
+ .PHONY: install-dev lint test-unit test-e2e test build
2
+
3
+ install-dev:
4
+ uv venv && uv pip install -e ".[dev]" && pre-commit install
5
+
6
+ lint:
7
+ pre-commit run --all-files
8
+
9
+ test-unit:
10
+ pytest tests/ -v --ignore=tests/test_e2e.py
11
+
12
+ test-e2e:
13
+ pytest tests/test_e2e.py -v
14
+
15
+ test: lint test-unit
16
+
17
+ build:
18
+ uv build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchright-cli
3
- Version: 0.4.0
3
+ Version: 0.5.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
@@ -21,9 +21,14 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
22
22
  Classifier: Topic :: Software Development :: Testing
23
23
  Requires-Python: >=3.10
24
+ Requires-Dist: aiohttp>=3.9
24
25
  Requires-Dist: click
25
26
  Requires-Dist: patchright
26
27
  Requires-Dist: pyyaml
28
+ Provides-Extra: dev
29
+ Requires-Dist: pre-commit; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
32
  Description-Content-Type: text/markdown
28
33
 
29
34
  # patchright-cli
@@ -173,6 +178,8 @@ patchright-cli check <ref> # Check checkbox
173
178
  patchright-cli uncheck <ref> # Uncheck checkbox
174
179
  patchright-cli drag <from> <to> # Drag and drop
175
180
  patchright-cli snapshot # Accessibility snapshot
181
+ patchright-cli snapshot -i # Interactive elements only (recommended)
182
+ patchright-cli snapshot --depth=3 # Limit depth
176
183
  patchright-cli snapshot <ref> # Snapshot element subtree
177
184
  patchright-cli snapshot --filename=f # Save to custom path
178
185
  patchright-cli eval <expr> # Run JavaScript
@@ -297,6 +304,17 @@ patchright-cli console warning # Filter by level
297
304
  patchright-cli console --clear # Clear after printing
298
305
  ```
299
306
 
307
+ ### Output
308
+ ```bash
309
+ patchright-cli --raw eval --file=s.js # Raw output (no page/snapshot decoration)
310
+ patchright-cli --raw snapshot > p.yml # Pipe raw snapshot to file
311
+ ```
312
+
313
+ ### Setup
314
+ ```bash
315
+ patchright-cli install --skills # Auto-install skills for detected AI agents
316
+ ```
317
+
300
318
  ### Sessions
301
319
  ```bash
302
320
  patchright-cli -s=mysession open https://example.com --persistent
@@ -371,6 +389,13 @@ Works with any AI coding agent that supports SKILL.md skills:
371
389
  | **Windsurf** | Copy SKILL.md to `.windsurf/skills/patchright-cli/` in your project |
372
390
  | **Aider** | Copy SKILL.md to `.aider/skills/patchright-cli/` in your project |
373
391
 
392
+ Or auto-install for all detected agents:
393
+
394
+ ```bash
395
+ pip install patchright-cli
396
+ patchright-cli install --skills
397
+ ```
398
+
374
399
  Or just tell your agent:
375
400
 
376
401
  > Install patchright-cli skill from https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md
@@ -397,9 +422,7 @@ patchright-cli aims for full command parity with [Microsoft's playwright-cli](ht
397
422
  | Feature | Reason |
398
423
  |---------|--------|
399
424
  | `--browser=firefox/webkit/msedge` | Patchright only supports Chromium/Chrome. Anti-detect patches are Chrome-specific. |
400
- | `--config=<file>` | No config file system. Options are passed as CLI flags. |
401
425
  | `--extension` | Browser extension connection not supported. Patchright uses CDP directly. |
402
- | `show` / `devtools-start` | DevTools are available natively in headed mode (default). |
403
426
  | `install` / `install-browser` | Use `python -m patchright install chromium` instead. |
404
427
 
405
428
  All other commands and options are fully supported.
@@ -415,6 +438,16 @@ All other commands and options are fully supported.
415
438
 
416
439
  ---
417
440
 
441
+ ## Credits
442
+
443
+ This project builds on the work of:
444
+
445
+ - **[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.
446
+ - **[playwright-cli](https://github.com/microsoft/playwright-cli)** by Microsoft — the original CLI that patchright-cli's command interface is modeled after.
447
+ - **[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.
448
+
449
+ ---
450
+
418
451
  ## License
419
452
 
420
453
  Apache 2.0 — same as [playwright-cli](https://github.com/microsoft/playwright-cli)
@@ -145,6 +145,8 @@ patchright-cli check <ref> # Check checkbox
145
145
  patchright-cli uncheck <ref> # Uncheck checkbox
146
146
  patchright-cli drag <from> <to> # Drag and drop
147
147
  patchright-cli snapshot # Accessibility snapshot
148
+ patchright-cli snapshot -i # Interactive elements only (recommended)
149
+ patchright-cli snapshot --depth=3 # Limit depth
148
150
  patchright-cli snapshot <ref> # Snapshot element subtree
149
151
  patchright-cli snapshot --filename=f # Save to custom path
150
152
  patchright-cli eval <expr> # Run JavaScript
@@ -269,6 +271,17 @@ patchright-cli console warning # Filter by level
269
271
  patchright-cli console --clear # Clear after printing
270
272
  ```
271
273
 
274
+ ### Output
275
+ ```bash
276
+ patchright-cli --raw eval --file=s.js # Raw output (no page/snapshot decoration)
277
+ patchright-cli --raw snapshot > p.yml # Pipe raw snapshot to file
278
+ ```
279
+
280
+ ### Setup
281
+ ```bash
282
+ patchright-cli install --skills # Auto-install skills for detected AI agents
283
+ ```
284
+
272
285
  ### Sessions
273
286
  ```bash
274
287
  patchright-cli -s=mysession open https://example.com --persistent
@@ -343,6 +356,13 @@ Works with any AI coding agent that supports SKILL.md skills:
343
356
  | **Windsurf** | Copy SKILL.md to `.windsurf/skills/patchright-cli/` in your project |
344
357
  | **Aider** | Copy SKILL.md to `.aider/skills/patchright-cli/` in your project |
345
358
 
359
+ Or auto-install for all detected agents:
360
+
361
+ ```bash
362
+ pip install patchright-cli
363
+ patchright-cli install --skills
364
+ ```
365
+
346
366
  Or just tell your agent:
347
367
 
348
368
  > Install patchright-cli skill from https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md
@@ -369,9 +389,7 @@ patchright-cli aims for full command parity with [Microsoft's playwright-cli](ht
369
389
  | Feature | Reason |
370
390
  |---------|--------|
371
391
  | `--browser=firefox/webkit/msedge` | Patchright only supports Chromium/Chrome. Anti-detect patches are Chrome-specific. |
372
- | `--config=<file>` | No config file system. Options are passed as CLI flags. |
373
392
  | `--extension` | Browser extension connection not supported. Patchright uses CDP directly. |
374
- | `show` / `devtools-start` | DevTools are available natively in headed mode (default). |
375
393
  | `install` / `install-browser` | Use `python -m patchright install chromium` instead. |
376
394
 
377
395
  All other commands and options are fully supported.
@@ -387,6 +405,16 @@ All other commands and options are fully supported.
387
405
 
388
406
  ---
389
407
 
408
+ ## Credits
409
+
410
+ This project builds on the work of:
411
+
412
+ - **[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.
413
+ - **[playwright-cli](https://github.com/microsoft/playwright-cli)** by Microsoft — the original CLI that patchright-cli's command interface is modeled after.
414
+ - **[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.
415
+
416
+ ---
417
+
390
418
  ## License
391
419
 
392
420
  Apache 2.0 — same as [playwright-cli](https://github.com/microsoft/playwright-cli)
@@ -0,0 +1,297 @@
1
+ # CDP Attach Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add `attach --cdp=<url>` support so patchright-cli can connect to an already-running Chrome/Edge instance via the Chrome DevTools Protocol.
6
+
7
+ **Architecture:** Add a new `attach` CLI command and corresponding daemon handler. When a `cdp_endpoint` is provided, the daemon uses `chromium.connect_over_cdp()` instead of `launch_persistent_context()`, then creates a fresh browser context and page from the connected `Browser` object.
8
+
9
+ **Tech Stack:** Python 3.10+, Patchright (Playwright)
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ | File | Responsibility |
16
+ |------|----------------|
17
+ | `src/patchright_cli/cli.py` | Add `attach` command help and map `--cdp` flag. |
18
+ | `src/patchright_cli/daemon.py` | Handle `attach` command, update `get_or_create_session` to support `cdp_endpoint`, `cdp_headers`, `cdp_timeout`. |
19
+ | `tests/test_daemon_handlers.py` | Async test for `attach` handler. |
20
+
21
+ ---
22
+
23
+ ### Task 1: CLI command definition
24
+
25
+ **Files:**
26
+ - Modify: `src/patchright_cli/cli.py`
27
+
28
+ - [ ] **Step 1: Add `attach` to command help**
29
+
30
+ In `COMMANDS_HELP`, add:
31
+
32
+ ```python
33
+ "attach": "attach --cdp=<url> Attach to running Chrome via CDP",
34
+ ```
35
+
36
+ In `_print_help()`, under Browser lifecycle (near `open`), change the category to include `attach`:
37
+
38
+ ```python
39
+ (
40
+ "Core",
41
+ [
42
+ "open",
43
+ "attach",
44
+ "goto",
45
+ ...
46
+ ],
47
+ ),
48
+ ```
49
+
50
+ - [ ] **Step 2: Add `--cdp` global option parsing**
51
+
52
+ In `main()`, inside the option-parsing loop, add:
53
+
54
+ ```python
55
+ elif arg.startswith("--cdp="):
56
+ extra_opts["cdp"] = arg.split("=", 1)[1]
57
+ elif arg == "--cdp" and i + 1 < len(argv):
58
+ i += 1
59
+ extra_opts["cdp"] = argv[i]
60
+ ```
61
+
62
+ And in `_print_help()`:
63
+
64
+ ```python
65
+ click.echo(" --cdp=<url> Attach to Chrome via CDP endpoint")
66
+ ```
67
+
68
+ - [ ] **Step 3: Commit**
69
+
70
+ ```bash
71
+ git add src/patchright_cli/cli.py
72
+ git commit -m "feat: add attach command and --cdp flag to CLI"
73
+ ```
74
+
75
+ ---
76
+
77
+ ### Task 2: Daemon handler and session creation
78
+
79
+ **Files:**
80
+ - Modify: `src/patchright_cli/daemon.py`
81
+ - Test: `tests/test_daemon_handlers.py`
82
+
83
+ - [ ] **Step 1: Write the failing test**
84
+
85
+ ```python
86
+ @pytest.mark.asyncio
87
+ async def test_attach_cdp(mock_state, mock_session):
88
+ mock_state.sessions = {}
89
+ mock_browser = MagicMock()
90
+ mock_context = MagicMock()
91
+ mock_page = MagicMock()
92
+ mock_page.url = "about:blank"
93
+ mock_page.title = AsyncMock(return_value="Attached")
94
+ mock_browser.new_context = AsyncMock(return_value=mock_context)
95
+ mock_context.new_page = AsyncMock(return_value=mock_page)
96
+ mock_context.pages = [mock_page]
97
+ mock_state.playwright.chromium.connect_over_cdp = AsyncMock(return_value=mock_browser)
98
+
99
+ with patch("patchright_cli.daemon.take_snapshot", new_callable=AsyncMock) as mock_snap:
100
+ mock_snap.return_value = ("snapshot-text", MagicMock())
101
+ with patch("patchright_cli.daemon.save_snapshot", return_value="/tmp/snap.yml"):
102
+ response = await handle_command(
103
+ mock_state,
104
+ {"command": "attach", "args": [], "options": {"cdp": "http://localhost:9222"}}
105
+ )
106
+ assert response["success"] is True
107
+ mock_state.playwright.chromium.connect_over_cdp.assert_awaited_once_with(
108
+ "http://localhost:9222", headers=None, timeout=30000
109
+ )
110
+ ```
111
+
112
+ - [ ] **Step 2: Run test to verify it fails**
113
+
114
+ Run: `pytest tests/test_daemon_handlers.py::test_attach_cdp -v`
115
+ Expected: FAIL — command unknown or `connect_over_cdp` not called.
116
+
117
+ - [ ] **Step 3: Update `get_or_create_session` for CDP**
118
+
119
+ In `DaemonState.get_or_create_session` signature, add:
120
+
121
+ ```python
122
+ cdp_endpoint: str | None = None,
123
+ cdp_headers: dict | None = None,
124
+ cdp_timeout: int = 30000,
125
+ ```
126
+
127
+ Then replace the `launch_kwargs` and `launch_persistent_context` block with branching logic:
128
+
129
+ ```python
130
+ if cdp_endpoint:
131
+ browser = await self.playwright.chromium.connect_over_cdp(
132
+ cdp_endpoint, headers=cdp_headers, timeout=cdp_timeout
133
+ )
134
+ context = await browser.new_context()
135
+ pages = context.pages or []
136
+ if not pages:
137
+ page = await context.new_page()
138
+ pages = [page]
139
+ else:
140
+ launch_kwargs = {
141
+ "channel": "chrome",
142
+ "headless": use_headless,
143
+ "no_viewport": True,
144
+ "args": ["--disable-blink-features=AutomationControlled"],
145
+ }
146
+ if proxy:
147
+ parsed = urlparse(proxy)
148
+ if parsed.username or parsed.password:
149
+ import base64
150
+ creds = base64.b64encode(f"{parsed.username or ''}:{parsed.password or ''}".encode()).decode()
151
+ launch_kwargs["extra_http_headers"] = {"Proxy-Authorization": f"Basic {creds}"}
152
+ netloc = parsed.hostname or ""
153
+ if parsed.port:
154
+ netloc += f":{parsed.port}"
155
+ cleaned = parsed._replace(netloc=netloc).geturl()
156
+ launch_kwargs["proxy"] = {"server": cleaned}
157
+ else:
158
+ launch_kwargs["proxy"] = {"server": proxy}
159
+ launch_kwargs.update(context_options)
160
+ context = await self.playwright.chromium.launch_persistent_context(
161
+ profile_dir,
162
+ **launch_kwargs,
163
+ )
164
+ pages = context.pages or []
165
+ if not pages:
166
+ page = await context.new_page()
167
+ pages = [page]
168
+ ```
169
+
170
+ Wait, we need to make sure `context_options` is still built for the non-CDP path. And for CDP, device/viewport options should also be applied. Actually, when using `browser.new_context()` after CDP connect, we can pass context options directly to `new_context()`:
171
+
172
+ ```python
173
+ context_options = {}
174
+ if device and self.playwright is not None:
175
+ device_descriptor = self.playwright.devices.get(device)
176
+ if device_descriptor:
177
+ context_options.update(device_descriptor)
178
+ if viewport:
179
+ context_options["viewport"] = {"width": int(viewport["width"]), "height": int(viewport["height"])}
180
+ if locale:
181
+ context_options["locale"] = locale
182
+ if timezone:
183
+ context_options["timezone_id"] = timezone
184
+ if geolocation:
185
+ context_options["geolocation"] = {
186
+ "latitude": float(geolocation["lat"]),
187
+ "longitude": float(geolocation["lon"]),
188
+ }
189
+ context_options["permissions"] = context_options.get("permissions", []) + ["geolocation"]
190
+ if user_agent:
191
+ context_options["user_agent"] = user_agent
192
+ if grant_permissions:
193
+ perms = [p.strip() for p in grant_permissions.split(",") if p.strip()]
194
+ context_options["permissions"] = list(set(context_options.get("permissions", []) + perms))
195
+
196
+ if cdp_endpoint:
197
+ browser = await self.playwright.chromium.connect_over_cdp(
198
+ cdp_endpoint, headers=cdp_headers, timeout=cdp_timeout
199
+ )
200
+ context = await browser.new_context(**context_options)
201
+ pages = context.pages or []
202
+ if not pages:
203
+ page = await context.new_page()
204
+ pages = [page]
205
+ else:
206
+ launch_kwargs = {
207
+ "channel": "chrome",
208
+ "headless": use_headless,
209
+ "no_viewport": True,
210
+ "args": ["--disable-blink-features=AutomationControlled"],
211
+ }
212
+ if proxy:
213
+ parsed = urlparse(proxy)
214
+ if parsed.username or parsed.password:
215
+ import base64
216
+ creds = base64.b64encode(f"{parsed.username or ''}:{parsed.password or ''}".encode()).decode()
217
+ launch_kwargs["extra_http_headers"] = {"Proxy-Authorization": f"Basic {creds}"}
218
+ netloc = parsed.hostname or ""
219
+ if parsed.port:
220
+ netloc += f":{parsed.port}"
221
+ cleaned = parsed._replace(netloc=netloc).geturl()
222
+ launch_kwargs["proxy"] = {"server": cleaned}
223
+ else:
224
+ launch_kwargs["proxy"] = {"server": proxy}
225
+ launch_kwargs.update(context_options)
226
+ context = await self.playwright.chromium.launch_persistent_context(
227
+ profile_dir,
228
+ **launch_kwargs,
229
+ )
230
+ pages = context.pages or []
231
+ if not pages:
232
+ page = await context.new_page()
233
+ pages = [page]
234
+ ```
235
+
236
+ - [ ] **Step 4: Add `attach` handler and update `open` handler**
237
+
238
+ In `handle_command`, add the `attach` branch right after the `open` branch:
239
+
240
+ ```python
241
+ if cmd == "attach":
242
+ cdp = options.get("cdp")
243
+ if not cdp:
244
+ return {"success": False, "output": "Usage: attach --cdp=<url>"}
245
+ headless = options.get("headless", False)
246
+ session = await state.get_or_create_session(
247
+ session_name,
248
+ headless=headless,
249
+ cdp_endpoint=cdp,
250
+ cdp_headers=options.get("cdp-headers"),
251
+ cdp_timeout=int(options.get("cdp-timeout", 30000)),
252
+ device=options.get("device"),
253
+ viewport=options.get("viewport"),
254
+ locale=options.get("locale"),
255
+ timezone=options.get("timezone"),
256
+ geolocation=options.get("geolocation"),
257
+ user_agent=options.get("user-agent") or options.get("userAgent"),
258
+ grant_permissions=options.get("grant-permissions") or options.get("grantPermissions"),
259
+ )
260
+ if session.page:
261
+ session.push_history(session.page.url)
262
+ return await _page_info(session, cwd)
263
+ ```
264
+
265
+ - [ ] **Step 5: Run test**
266
+
267
+ Run: `pytest tests/test_daemon_handlers.py::test_attach_cdp -v`
268
+ Expected: PASS
269
+
270
+ - [ ] **Step 6: Run full handler test suite**
271
+
272
+ Run: `pytest tests/test_daemon_handlers.py -v`
273
+ Expected: All PASS
274
+
275
+ - [ ] **Step 7: Commit**
276
+
277
+ ```bash
278
+ git add tests/test_daemon_handlers.py src/patchright_cli/daemon.py
279
+ git commit -m "feat: attach to running Chrome via CDP endpoint"
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Self-Review
285
+
286
+ **1. Spec coverage:**
287
+ - `attach --cdp=<url>` → Task 1 + Task 2
288
+ - CDP headers/timeout support → Task 2 via `get_or_create_session`
289
+ - All requirements covered.
290
+
291
+ **2. Placeholder scan:**
292
+ - No TBDs or vague steps.
293
+ - Exact code provided for every change.
294
+
295
+ **3. Type consistency:**
296
+ - `cdp_endpoint` is consistently `str | None`.
297
+ - `cdp_timeout` defaults to `30000` (int) throughout.