patchright-cli 0.4.0__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.
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/.gitignore +1 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/PKG-INFO +2 -1
- patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-cdp-attach.md +297 -0
- patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-cli-enhancements.md +529 -0
- patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-codegen.md +234 -0
- patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-context-config-and-permissions.md +411 -0
- patchright_cli-0.4.1/docs/superpowers/plans/2026-04-14-show-dashboard.md +337 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/pyproject.toml +2 -1
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/skills/patchright-cli/SKILL.md +90 -20
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/src/patchright_cli/__init__.py +1 -1
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/src/patchright_cli/cli.py +111 -9
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/src/patchright_cli/daemon.py +287 -36
- patchright_cli-0.4.1/src/patchright_cli/dashboard.py +135 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/src/patchright_cli/ref_registry.py +7 -1
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/src/patchright_cli/snapshot.py +2 -2
- patchright_cli-0.4.1/tests/test_cli_config.py +74 -0
- patchright_cli-0.4.1/tests/test_daemon_handlers.py +306 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/tests/test_ref_registry.py +11 -0
- patchright_cli-0.4.1/uv.lock +1044 -0
- patchright_cli-0.4.0/tests/test_daemon_handlers.py +0 -124
- patchright_cli-0.4.0/uv.lock +0 -360
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/.claude-plugin/marketplace.json +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/.claude-plugin/skills/browser-automation.json +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/.github/workflows/publish.yml +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/.pre-commit-config.yaml +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/CLAUDE.md +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/CONTRIBUTING.md +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/LICENSE +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/README.md +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/docs/installation.md +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/patchright-cli +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/patchright-cli.cmd +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/src/patchright_cli/__main__.py +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/tests/__init__.py +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/tests/fixture.html +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.4.1}/tests/test_e2e.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchright-cli
|
|
3
|
-
Version: 0.4.
|
|
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.
|