patchright-cli 0.3.1__tar.gz → 0.4.1__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 (37) hide show
  1. patchright_cli-0.4.1/.claude-plugin/marketplace.json +21 -0
  2. patchright_cli-0.4.1/.claude-plugin/skills/browser-automation.json +5 -0
  3. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/.gitignore +1 -0
  4. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/PKG-INFO +2 -1
  5. patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-cdp-attach.md +297 -0
  6. patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-cli-enhancements.md +529 -0
  7. patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-codegen.md +234 -0
  8. patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-context-config-and-permissions.md +411 -0
  9. patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-show-dashboard.md +337 -0
  10. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/pyproject.toml +14 -1
  11. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/skills/patchright-cli/SKILL.md +126 -17
  12. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/src/patchright_cli/__init__.py +1 -1
  13. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/src/patchright_cli/cli.py +128 -29
  14. patchright_cli-0.4.1/src/patchright_cli/daemon.py +1713 -0
  15. patchright_cli-0.4.1/src/patchright_cli/dashboard.py +135 -0
  16. patchright_cli-0.4.1/src/patchright_cli/ref_registry.py +80 -0
  17. patchright_cli-0.4.1/src/patchright_cli/snapshot.py +46 -0
  18. patchright_cli-0.4.1/tests/__init__.py +0 -0
  19. patchright_cli-0.4.1/tests/fixture.html +30 -0
  20. patchright_cli-0.4.1/tests/test_cli_config.py +74 -0
  21. patchright_cli-0.4.1/tests/test_daemon_handlers.py +306 -0
  22. patchright_cli-0.4.1/tests/test_e2e.py +144 -0
  23. patchright_cli-0.4.1/tests/test_ref_registry.py +86 -0
  24. patchright_cli-0.4.1/uv.lock +1044 -0
  25. patchright_cli-0.3.1/src/patchright_cli/daemon.py +0 -1094
  26. patchright_cli-0.3.1/src/patchright_cli/snapshot.py +0 -346
  27. patchright_cli-0.3.1/uv.lock +0 -205
  28. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/.github/workflows/publish.yml +0 -0
  29. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/.pre-commit-config.yaml +0 -0
  30. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/CLAUDE.md +0 -0
  31. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/CONTRIBUTING.md +0 -0
  32. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/LICENSE +0 -0
  33. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/README.md +0 -0
  34. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/docs/installation.md +0 -0
  35. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/patchright-cli +0 -0
  36. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/patchright-cli.cmd +0 -0
  37. {patchright_cli-0.3.1 → patchright_cli-0.4.1}/src/patchright_cli/__main__.py +0 -0
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "patchright-cli",
3
+ "owner": {
4
+ "name": "zpahai"
5
+ },
6
+ "metadata": {
7
+ "description": "Anti-detect browser automation CLI using Patchright (undetected Playwright fork)",
8
+ "version": "0.4.0"
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "patchright-cli",
13
+ "description": "Anti-detect browser automation CLI for AI agents",
14
+ "source": "./",
15
+ "skills": [
16
+ "./skills/patchright-cli",
17
+ "./.claude-plugin/skills/browser-automation"
18
+ ]
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "browser-automation",
3
+ "description": "Control a headful or headless Chrome browser via patchright-cli commands",
4
+ "documentation": "./skills/patchright-cli/SKILL.md"
5
+ }
@@ -6,3 +6,4 @@ __pycache__/
6
6
  dist/
7
7
  build/
8
8
  docs/reddit-post.md
9
+ .worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchright-cli
3
- Version: 0.3.1
3
+ Version: 0.4.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
@@ -21,6 +21,7 @@ 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
@@ -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.