cdp-skill 1.0.0
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.
- package/SKILL.md +543 -0
- package/install.js +92 -0
- package/package.json +47 -0
- package/src/aria.js +1302 -0
- package/src/capture.js +1359 -0
- package/src/cdp.js +905 -0
- package/src/cli.js +244 -0
- package/src/dom.js +3525 -0
- package/src/index.js +155 -0
- package/src/page.js +1720 -0
- package/src/runner.js +2111 -0
- package/src/tests/BrowserClient.test.js +588 -0
- package/src/tests/CDPConnection.test.js +598 -0
- package/src/tests/ChromeDiscovery.test.js +181 -0
- package/src/tests/ConsoleCapture.test.js +302 -0
- package/src/tests/ElementHandle.test.js +586 -0
- package/src/tests/ElementLocator.test.js +586 -0
- package/src/tests/ErrorAggregator.test.js +327 -0
- package/src/tests/InputEmulator.test.js +641 -0
- package/src/tests/NetworkErrorCapture.test.js +458 -0
- package/src/tests/PageController.test.js +822 -0
- package/src/tests/ScreenshotCapture.test.js +356 -0
- package/src/tests/SessionRegistry.test.js +257 -0
- package/src/tests/TargetManager.test.js +274 -0
- package/src/tests/TestRunner.test.js +1529 -0
- package/src/tests/WaitStrategy.test.js +406 -0
- package/src/tests/integration.test.js +431 -0
- package/src/utils.js +1034 -0
- package/uninstall.js +44 -0
package/SKILL.md
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cdp-skill
|
|
3
|
+
description: Automate Chrome browser interactions via JSON piped to a Node.js CLI. Use when you need to navigate websites, fill forms, click elements, take screenshots, extract data, or run end-to-end browser tests. Supports accessibility snapshots for resilient element targeting.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Requires Chrome/Chromium running with --remote-debugging-port=9222 and Node.js.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# CDP Browser Automation Skill
|
|
9
|
+
|
|
10
|
+
Automate Chrome browser interactions via JSON piped to a Node.js CLI. Produce JSON step definitions, not JavaScript code.
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
This skill enables **AI-powered browser automation**. The intended workflow:
|
|
15
|
+
|
|
16
|
+
1. **Test definitions** are written as markdown files describing what to test
|
|
17
|
+
2. **An agent** reads the definition, discovers page elements dynamically, and executes using this skill
|
|
18
|
+
3. The agent interprets intent and adapts to page changes - making automation resilient without brittle hardcoded selectors
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
Chrome must be running with remote debugging:
|
|
23
|
+
```bash
|
|
24
|
+
# macOS
|
|
25
|
+
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
|
|
26
|
+
|
|
27
|
+
# Linux
|
|
28
|
+
google-chrome --remote-debugging-port=9222
|
|
29
|
+
|
|
30
|
+
# Windows
|
|
31
|
+
chrome.exe --remote-debugging-port=9222
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Execute steps:
|
|
35
|
+
```bash
|
|
36
|
+
echo '{"steps":[{"goto":"https://example.com"}]}' | node src/cli.js
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Tab Reuse (Critical)
|
|
40
|
+
|
|
41
|
+
The first invocation creates a new tab and returns a `targetId`. **Include this in ALL subsequent calls** to reuse the same tab:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# First call - extract targetId from response
|
|
45
|
+
RESULT=$(echo '{"steps":[{"goto":"https://example.com"}]}' | node src/cli.js)
|
|
46
|
+
TARGET_ID=$(echo "$RESULT" | jq -r '.tab.targetId')
|
|
47
|
+
|
|
48
|
+
# All subsequent calls - include targetId
|
|
49
|
+
echo "{\"config\":{\"targetId\":\"$TARGET_ID\"},\"steps\":[{\"click\":\"#btn\"}]}" | node src/cli.js
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Omitting `targetId` creates orphan tabs that accumulate until Chrome restarts.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Input Schema
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"config": {
|
|
60
|
+
"host": "localhost",
|
|
61
|
+
"port": 9222,
|
|
62
|
+
"targetId": "ABC123...",
|
|
63
|
+
"timeout": 30000
|
|
64
|
+
},
|
|
65
|
+
"steps": [...]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Config is optional on first call. `targetId` required on subsequent calls.
|
|
70
|
+
|
|
71
|
+
## Output Schema
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"status": "passed|failed|error",
|
|
76
|
+
"tab": { "targetId": "ABC123...", "url": "...", "title": "..." },
|
|
77
|
+
"steps": [{ "action": "goto", "status": "passed", "duration": 1234 }],
|
|
78
|
+
"outputs": [{ "step": 2, "action": "query", "output": {...} }],
|
|
79
|
+
"errors": [{ "step": 3, "action": "click", "error": "Element not found" }]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Exit code: `0` = passed, `1` = failed/error.
|
|
84
|
+
|
|
85
|
+
Error types: `PARSE`, `VALIDATION`, `CONNECTION`, `EXECUTION`
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Auto-Waiting
|
|
89
|
+
|
|
90
|
+
All interaction actions (`click`, `fill`, `hover`, `type`) automatically wait for elements to be actionable before proceeding. Retries use exponential backoff with jitter (1.9-2.1x random factor) to avoid thundering herd issues.
|
|
91
|
+
|
|
92
|
+
| Action | Waits For |
|
|
93
|
+
|--------|-----------|
|
|
94
|
+
| `click` | visible, enabled, stable, not covered, pointer-events |
|
|
95
|
+
| `fill`, `type` | visible, enabled, editable |
|
|
96
|
+
| `hover` | visible, stable |
|
|
97
|
+
|
|
98
|
+
**State definitions:**
|
|
99
|
+
- **visible**: In DOM, not `display:none`, not `visibility:hidden`, has dimensions
|
|
100
|
+
- **enabled**: Not disabled, not `aria-disabled="true"`
|
|
101
|
+
- **editable**: Enabled + not readonly + is input/textarea/select/contenteditable
|
|
102
|
+
- **stable**: Position unchanged for 3 consecutive animation frames
|
|
103
|
+
- **not covered**: Element at click coordinates matches target (detects overlays/modals)
|
|
104
|
+
- **pointer-events**: CSS `pointer-events` is not `none`
|
|
105
|
+
|
|
106
|
+
**Force options:**
|
|
107
|
+
- Use `force: true` to bypass all checks immediately
|
|
108
|
+
- **Auto-force**: When actionability times out but element exists, automatically retries with `force: true`. This helps with overlays, cookie banners, and loading spinners that may obscure elements. Outputs include `autoForced: true` when this occurs.
|
|
109
|
+
|
|
110
|
+
**Performance optimizations:**
|
|
111
|
+
- Browser-side polling using MutationObserver (reduces network round-trips)
|
|
112
|
+
- Content quads for accurate click positioning with CSS transforms
|
|
113
|
+
- InsertText API for fast form fills (like paste)
|
|
114
|
+
- IntersectionObserver for efficient viewport detection
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
## Element References
|
|
118
|
+
|
|
119
|
+
The `snapshot` step returns an accessibility tree with refs like `[ref=e4]`. Use refs in subsequent actions:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{"steps":[{"snapshot": true}]}
|
|
123
|
+
// Response includes: - button "Submit" [ref=e4]
|
|
124
|
+
|
|
125
|
+
{"config":{"targetId":"..."},"steps":[{"click":{"ref":"e4"}}]}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Refs work with: `click`, `fill`, `hover`.
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
## Step Reference
|
|
132
|
+
|
|
133
|
+
### Navigation
|
|
134
|
+
|
|
135
|
+
**goto** - Navigate to URL
|
|
136
|
+
```json
|
|
137
|
+
{"goto": "https://example.com"}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**back** / **forward** - History navigation
|
|
141
|
+
```json
|
|
142
|
+
{"back": true}
|
|
143
|
+
{"forward": true}
|
|
144
|
+
```
|
|
145
|
+
Returns: `{url, title}` or `{noHistory: true}` if no history entry exists.
|
|
146
|
+
|
|
147
|
+
**waitForNavigation** - Wait for navigation to complete
|
|
148
|
+
```json
|
|
149
|
+
{"waitForNavigation": true}
|
|
150
|
+
{"waitForNavigation": {"timeout": 5000, "waitUntil": "networkidle"}}
|
|
151
|
+
```
|
|
152
|
+
Options: `timeout`, `waitUntil` (commit|domcontentloaded|load|networkidle)
|
|
153
|
+
|
|
154
|
+
**Note:** For click-then-wait patterns, the system uses a two-step event pattern to prevent race conditions - it subscribes to navigation events BEFORE clicking to ensure fast navigations aren't missed.
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
### Frame/iFrame Navigation
|
|
158
|
+
|
|
159
|
+
**listFrames** - List all frames in the page
|
|
160
|
+
```json
|
|
161
|
+
{"listFrames": true}
|
|
162
|
+
```
|
|
163
|
+
Returns: `{mainFrameId, currentFrameId, frames: [{frameId, url, name, parentId, depth}]}`
|
|
164
|
+
|
|
165
|
+
**switchToFrame** - Switch to an iframe
|
|
166
|
+
```json
|
|
167
|
+
{"switchToFrame": "iframe#content"}
|
|
168
|
+
{"switchToFrame": 0}
|
|
169
|
+
{"switchToFrame": {"selector": "iframe.editor"}}
|
|
170
|
+
{"switchToFrame": {"index": 1}}
|
|
171
|
+
{"switchToFrame": {"name": "myFrame"}}
|
|
172
|
+
```
|
|
173
|
+
Options: CSS selector (string), index (number), or object with `selector`, `index`, `name`, or `frameId`
|
|
174
|
+
|
|
175
|
+
Returns: `{frameId, url, name}`
|
|
176
|
+
|
|
177
|
+
**switchToMainFrame** - Switch back to main frame
|
|
178
|
+
```json
|
|
179
|
+
{"switchToMainFrame": true}
|
|
180
|
+
```
|
|
181
|
+
Returns: `{frameId, url, name}`
|
|
182
|
+
|
|
183
|
+
**Note:** After switching to a frame, all subsequent actions execute in that frame context until you switch to another frame or back to main.
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
### Waiting
|
|
187
|
+
|
|
188
|
+
**wait** - Wait for element
|
|
189
|
+
```json
|
|
190
|
+
{"wait": "#content"}
|
|
191
|
+
{"wait": {"selector": "#loading", "hidden": true}}
|
|
192
|
+
{"wait": {"selector": ".item", "minCount": 10}}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**wait** - Wait for text
|
|
196
|
+
```json
|
|
197
|
+
{"wait": {"text": "Welcome"}}
|
|
198
|
+
{"wait": {"textRegex": "Order #[A-Z0-9]+"}}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**wait** - Wait for URL
|
|
202
|
+
```json
|
|
203
|
+
{"wait": {"urlContains": "/success"}}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**wait** / **delay** - Fixed time (ms)
|
|
207
|
+
```json
|
|
208
|
+
{"wait": 2000}
|
|
209
|
+
{"delay": 500}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Network idle detection:** The `networkidle` wait condition uses a precise counter-based tracker that monitors all network requests. It considers the network "idle" when no requests have been pending for 500ms.
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
### Interaction
|
|
216
|
+
|
|
217
|
+
**click** - Click element
|
|
218
|
+
```json
|
|
219
|
+
{"click": "#submit"}
|
|
220
|
+
{"click": {"selector": "#btn", "verify": true}}
|
|
221
|
+
{"click": {"ref": "e4"}}
|
|
222
|
+
{"click": {"x": 450, "y": 200}}
|
|
223
|
+
```
|
|
224
|
+
Options: `selector`, `ref`, `x`/`y`, `verify`, `force`, `debug`, `timeout`
|
|
225
|
+
|
|
226
|
+
Returns: `{clicked: true}`. With `verify`: adds `{targetReceived: true/false}`. With navigation: adds `{navigated: true, newUrl: "..."}`.
|
|
227
|
+
|
|
228
|
+
**fill** - Fill input (clears first)
|
|
229
|
+
```json
|
|
230
|
+
{"fill": {"selector": "#email", "value": "user@example.com"}}
|
|
231
|
+
{"fill": {"ref": "e3", "value": "text"}}
|
|
232
|
+
```
|
|
233
|
+
Options: `selector`, `ref`, `value`, `clear` (default: true), `react`, `force`, `timeout`
|
|
234
|
+
|
|
235
|
+
**fillForm** - Fill multiple fields
|
|
236
|
+
```json
|
|
237
|
+
{"fillForm": {"#firstName": "John", "#lastName": "Doe"}}
|
|
238
|
+
```
|
|
239
|
+
Returns: `{total, filled, failed, results: [{selector, status, value}]}`
|
|
240
|
+
|
|
241
|
+
**type** - Type text (no clear)
|
|
242
|
+
```json
|
|
243
|
+
{"type": {"selector": "#search", "text": "query", "delay": 50}}
|
|
244
|
+
```
|
|
245
|
+
Returns: `{selector, typed, length}`
|
|
246
|
+
|
|
247
|
+
**press** - Keyboard key/combo
|
|
248
|
+
```json
|
|
249
|
+
{"press": "Enter"}
|
|
250
|
+
{"press": "Control+a"}
|
|
251
|
+
{"press": "Meta+Shift+Enter"}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**select** - Select text in input
|
|
255
|
+
```json
|
|
256
|
+
{"select": "#input"}
|
|
257
|
+
{"select": {"selector": "#input", "start": 0, "end": 5}}
|
|
258
|
+
```
|
|
259
|
+
Returns: `{selector, start, end, selectedText, totalLength}`
|
|
260
|
+
|
|
261
|
+
**hover** - Mouse over element
|
|
262
|
+
```json
|
|
263
|
+
{"hover": "#menu"}
|
|
264
|
+
{"hover": {"selector": "#tooltip", "duration": 500}}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**drag** - Drag element from source to target
|
|
268
|
+
```json
|
|
269
|
+
{"drag": {"source": "#draggable", "target": "#dropzone"}}
|
|
270
|
+
{"drag": {"source": {"x": 100, "y": 100}, "target": {"x": 300, "y": 200}}}
|
|
271
|
+
{"drag": {"source": "#item", "target": "#container", "steps": 20, "delay": 10}}
|
|
272
|
+
```
|
|
273
|
+
Options: `source` (selector or {x,y}), `target` (selector or {x,y}), `steps` (default: 10), `delay` (ms, default: 0)
|
|
274
|
+
|
|
275
|
+
Returns: `{dragged: true, source: {x, y}, target: {x, y}, steps}`
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
### Scrolling
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{"scroll": "top"}
|
|
282
|
+
{"scroll": "bottom"}
|
|
283
|
+
{"scroll": "#element"}
|
|
284
|
+
{"scroll": {"deltaY": 500}}
|
|
285
|
+
{"scroll": {"x": 0, "y": 1000}}
|
|
286
|
+
```
|
|
287
|
+
Returns: `{scrollX, scrollY}`
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
### Data Extraction
|
|
291
|
+
|
|
292
|
+
**query** - Find elements by CSS
|
|
293
|
+
```json
|
|
294
|
+
{"query": "h1"}
|
|
295
|
+
{"query": {"selector": "a", "limit": 5, "output": "href"}}
|
|
296
|
+
{"query": {"selector": "div", "output": ["text", "href"]}}
|
|
297
|
+
{"query": {"selector": "button", "output": {"attribute": "data-id"}}}
|
|
298
|
+
```
|
|
299
|
+
Options: `selector`, `limit` (default: 10), `output` (text|html|href|value|tag|array|attribute object), `clean`, `metadata`
|
|
300
|
+
|
|
301
|
+
Returns: `{selector, total, showing, results: [{index, value}]}`
|
|
302
|
+
|
|
303
|
+
**query** - Find by ARIA role
|
|
304
|
+
```json
|
|
305
|
+
{"query": {"role": "button"}}
|
|
306
|
+
{"query": {"role": "button", "name": "Submit"}}
|
|
307
|
+
{"query": {"role": "heading", "level": 2}}
|
|
308
|
+
{"query": {"role": ["button", "link"], "refs": true}}
|
|
309
|
+
```
|
|
310
|
+
Options: `role`, `name`, `nameExact`, `nameRegex`, `checked`, `disabled`, `level`, `countOnly`, `refs`
|
|
311
|
+
|
|
312
|
+
Supported roles: `button`, `textbox`, `checkbox`, `link`, `heading`, `listitem`, `option`, `combobox`, `radio`, `img`, `tab`, `tabpanel`, `menu`, `menuitem`, `dialog`, `alert`, `navigation`, `main`, `search`, `form`
|
|
313
|
+
|
|
314
|
+
**queryAll** - Multiple queries at once
|
|
315
|
+
```json
|
|
316
|
+
{"queryAll": {"title": "h1", "links": "a", "buttons": {"role": "button"}}}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**inspect** - Page overview
|
|
320
|
+
```json
|
|
321
|
+
{"inspect": true}
|
|
322
|
+
{"inspect": {"selectors": [".item"], "limit": 3}}
|
|
323
|
+
```
|
|
324
|
+
Returns: `{title, url, counts: {links, buttons, inputs, images, headings}, custom: {...}}`
|
|
325
|
+
|
|
326
|
+
**console** - Browser console logs
|
|
327
|
+
```json
|
|
328
|
+
{"console": true}
|
|
329
|
+
{"console": {"level": "error", "limit": 20, "stackTrace": true}}
|
|
330
|
+
```
|
|
331
|
+
Options: `level`, `type`, `since`, `limit`, `clear`, `stackTrace`
|
|
332
|
+
|
|
333
|
+
Returns: `{total, showing, messages: [{level, text, type, url, line, timestamp, stackTrace?}]}`
|
|
334
|
+
|
|
335
|
+
Note: Console logs don't persist across CLI invocations.
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
### Screenshots & PDF
|
|
339
|
+
|
|
340
|
+
**screenshot**
|
|
341
|
+
```json
|
|
342
|
+
{"screenshot": "./result.png"}
|
|
343
|
+
{"screenshot": {"path": "./full.png", "fullPage": true}}
|
|
344
|
+
{"screenshot": {"path": "./element.png", "selector": "#header"}}
|
|
345
|
+
```
|
|
346
|
+
Options: `path`, `fullPage`, `selector`, `format` (png|jpeg|webp), `quality`, `omitBackground`, `clip`
|
|
347
|
+
|
|
348
|
+
Returns: `{path, viewport: {width, height}, format, fullPage, selector}`
|
|
349
|
+
|
|
350
|
+
**pdf**
|
|
351
|
+
```json
|
|
352
|
+
{"pdf": "./report.pdf"}
|
|
353
|
+
{"pdf": {"path": "./report.pdf", "landscape": true, "printBackground": true}}
|
|
354
|
+
```
|
|
355
|
+
Options: `path`, `selector`, `landscape`, `printBackground`, `scale`, `paperWidth`, `paperHeight`, margins, `pageRanges`, `validate`
|
|
356
|
+
|
|
357
|
+
Returns: `{path, fileSize, fileSizeFormatted, pageCount, dimensions, validation?}`
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
### JavaScript Execution
|
|
361
|
+
|
|
362
|
+
**eval** - Execute JS in page context
|
|
363
|
+
```json
|
|
364
|
+
{"eval": "document.title"}
|
|
365
|
+
{"eval": {"expression": "fetch('/api').then(r=>r.json())", "await": true}}
|
|
366
|
+
```
|
|
367
|
+
Options: `expression`, `await`, `timeout`, `serialize`
|
|
368
|
+
|
|
369
|
+
**Shell escaping tip:** For complex expressions with quotes or special characters, use a heredoc or JSON file:
|
|
370
|
+
```bash
|
|
371
|
+
# Heredoc approach
|
|
372
|
+
node src/cli.js <<'EOF'
|
|
373
|
+
{"steps":[{"eval":"document.querySelectorAll('button').length"}]}
|
|
374
|
+
EOF
|
|
375
|
+
|
|
376
|
+
# Or save to file and pipe
|
|
377
|
+
cat steps.json | node src/cli.js
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Returns typed results:
|
|
381
|
+
- Numbers: `{type: "number", repr: "Infinity|NaN|-Infinity"}`
|
|
382
|
+
- Date: `{type: "Date", value: "ISO string", timestamp: N}`
|
|
383
|
+
- Map: `{type: "Map", size: N, entries: [...]}`
|
|
384
|
+
- Set: `{type: "Set", size: N, values: [...]}`
|
|
385
|
+
- Element: `{type: "Element", tagName, id, className, textContent, isConnected}`
|
|
386
|
+
- NodeList: `{type: "NodeList", length: N, items: [...]}`
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
### Accessibility Snapshot
|
|
390
|
+
|
|
391
|
+
**snapshot** - Get accessibility tree
|
|
392
|
+
```json
|
|
393
|
+
{"snapshot": true}
|
|
394
|
+
{"snapshot": {"root": "#container", "maxElements": 500}}
|
|
395
|
+
{"snapshot": {"root": "role=main", "includeText": true}}
|
|
396
|
+
{"snapshot": {"includeFrames": true}}
|
|
397
|
+
```
|
|
398
|
+
Options: `mode` (ai|full), `root` (CSS selector or "role=X"), `maxDepth`, `maxElements`, `includeText`, `includeFrames`
|
|
399
|
+
|
|
400
|
+
Returns YAML with: role, "name", states (`[checked]`, `[disabled]`, `[expanded]`, `[required]`, `[invalid]`, `[level=N]`), `[name=fieldName]` for form inputs, `[ref=eN]` for clicking.
|
|
401
|
+
|
|
402
|
+
```yaml
|
|
403
|
+
- navigation:
|
|
404
|
+
- link "Home" [ref=e1]
|
|
405
|
+
- main:
|
|
406
|
+
- heading "Welcome" [level=1]
|
|
407
|
+
- textbox "Email" [required] [invalid] [name=email] [ref=e3]
|
|
408
|
+
- button "Submit" [ref=e4]
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Use `includeText: true` to capture static text (error messages, etc.). Elements with `role="alert"` or `role="status"` always include text.
|
|
412
|
+
|
|
413
|
+
Use `includeFrames: true` to include same-origin iframe content in the snapshot. Cross-origin iframes are marked with `crossOrigin: true`.
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
### Viewport & Device Emulation
|
|
417
|
+
|
|
418
|
+
**viewport** - Set viewport size
|
|
419
|
+
```json
|
|
420
|
+
{"viewport": "iphone-14"}
|
|
421
|
+
{"viewport": {"width": 1280, "height": 720}}
|
|
422
|
+
{"viewport": {"width": 375, "height": 667, "mobile": true, "hasTouch": true, "isLandscape": true}}
|
|
423
|
+
```
|
|
424
|
+
Options: `width`, `height`, `deviceScaleFactor`, `mobile`, `hasTouch`, `isLandscape`
|
|
425
|
+
|
|
426
|
+
Returns: `{width, height, deviceScaleFactor, mobile, hasTouch}`
|
|
427
|
+
|
|
428
|
+
Presets: `iphone-se`, `iphone-14`, `iphone-15-pro`, `ipad`, `ipad-pro-11`, `pixel-7`, `samsung-galaxy-s23`, `desktop`, `desktop-hd`, `macbook-pro-14`, etc.
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
### Cookie Management
|
|
432
|
+
|
|
433
|
+
**cookies** - Get/set/clear cookies
|
|
434
|
+
```json
|
|
435
|
+
{"cookies": {"get": true}}
|
|
436
|
+
{"cookies": {"get": ["https://example.com"], "name": "session_id"}}
|
|
437
|
+
{"cookies": {"set": [{"name": "token", "value": "abc", "domain": "example.com", "expires": "7d"}]}}
|
|
438
|
+
{"cookies": {"delete": "session_id"}}
|
|
439
|
+
{"cookies": {"clear": true}}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Set options: `name`, `value`, `url` or `domain`, `path`, `secure`, `httpOnly`, `sameSite`, `expires`
|
|
443
|
+
|
|
444
|
+
Expiration formats: `30m`, `1h`, `7d`, `1w`, `1y`, or Unix timestamp.
|
|
445
|
+
|
|
446
|
+
Returns: get → `{cookies: [...]}`, set → `{set: N}`, delete/clear → `{count: N}`
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
### Form Validation
|
|
450
|
+
|
|
451
|
+
**validate** - Check field validation state
|
|
452
|
+
```json
|
|
453
|
+
{"validate": "#email"}
|
|
454
|
+
```
|
|
455
|
+
Returns: `{valid, message, validity: {valueMissing, typeMismatch, ...}}`
|
|
456
|
+
|
|
457
|
+
**submit** - Submit form with validation
|
|
458
|
+
```json
|
|
459
|
+
{"submit": "form"}
|
|
460
|
+
{"submit": {"selector": "#login-form", "reportValidity": true}}
|
|
461
|
+
```
|
|
462
|
+
Returns: `{submitted, valid, errors: [{name, type, message, value}]}`
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
### Assertions
|
|
466
|
+
|
|
467
|
+
**assert** - Validate conditions
|
|
468
|
+
```json
|
|
469
|
+
{"assert": {"url": {"contains": "/success"}}}
|
|
470
|
+
{"assert": {"url": {"matches": "^https://.*\\.example\\.com"}}}
|
|
471
|
+
{"assert": {"text": "Welcome"}}
|
|
472
|
+
{"assert": {"selector": "h1", "text": "Title", "caseSensitive": false}}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
URL options: `contains`, `equals`, `startsWith`, `endsWith`, `matches`
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
### Tab Management
|
|
479
|
+
|
|
480
|
+
**listTabs** - List open tabs
|
|
481
|
+
```json
|
|
482
|
+
{"listTabs": true}
|
|
483
|
+
```
|
|
484
|
+
Returns: `{count, tabs: [{targetId, url, title}]}`
|
|
485
|
+
|
|
486
|
+
**closeTab** - Close a tab
|
|
487
|
+
```json
|
|
488
|
+
{"closeTab": "ABC123..."}
|
|
489
|
+
```
|
|
490
|
+
Returns: `{closed: "<targetId>"}`
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
### Optional Steps
|
|
494
|
+
|
|
495
|
+
Add `"optional": true` to continue on failure:
|
|
496
|
+
```json
|
|
497
|
+
{"click": "#maybe-exists", "optional": true}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
## Debug Mode
|
|
502
|
+
|
|
503
|
+
Capture screenshots/DOM before and after each action:
|
|
504
|
+
```json
|
|
505
|
+
{
|
|
506
|
+
"config": {
|
|
507
|
+
"debug": true,
|
|
508
|
+
"debugOptions": {"outputDir": "./debug", "captureScreenshots": true, "captureDom": true}
|
|
509
|
+
},
|
|
510
|
+
"steps": [...]
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
## Not Supported
|
|
516
|
+
|
|
517
|
+
Handle via multiple invocations:
|
|
518
|
+
- Conditional logic / loops
|
|
519
|
+
- Variables / templating
|
|
520
|
+
- File uploads
|
|
521
|
+
- Dialog handling (alert, confirm)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
## Troubleshooting
|
|
525
|
+
|
|
526
|
+
| Issue | Solution |
|
|
527
|
+
|-------|----------|
|
|
528
|
+
| Tabs accumulating | Include `targetId` in config |
|
|
529
|
+
| CONNECTION error | Start Chrome with `--remote-debugging-port=9222` |
|
|
530
|
+
| Element not found | Add `wait` step first |
|
|
531
|
+
| Clicks not working | Scroll element into view first |
|
|
532
|
+
|
|
533
|
+
## Best Practices
|
|
534
|
+
|
|
535
|
+
1. **Discover before interacting** - Use `inspect` and `snapshot` to understand page structure
|
|
536
|
+
2. **Use website navigation** - Click links and submit forms; don't guess URLs
|
|
537
|
+
3. **Be persistent** - Try alternative selectors, add waits, scroll first
|
|
538
|
+
4. **Prefer refs** - Use `snapshot` + refs over brittle CSS selectors
|
|
539
|
+
|
|
540
|
+
## Feedback
|
|
541
|
+
|
|
542
|
+
If you encounter limitations, bugs, or feature requests that would significantly improve automation capabilities, please report them to the skill maintainer.
|
|
543
|
+
If you spot opportunities for speeding things up raise this in your results as well.
|
package/install.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, basename, join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, symlinkSync, unlinkSync, rmSync, cpSync } from 'fs';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const packageRoot = __dirname;
|
|
12
|
+
const skillName = basename(packageRoot);
|
|
13
|
+
const isDevMode = !packageRoot.includes('node_modules');
|
|
14
|
+
|
|
15
|
+
const targets = [
|
|
16
|
+
{ name: 'claude', path: join(homedir(), '.claude', 'skills', skillName) },
|
|
17
|
+
{ name: 'codex', path: join(homedir(), '.codex', 'skills', skillName) },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const filesToCopy = [
|
|
21
|
+
'SKILL.md',
|
|
22
|
+
'src',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function ensureParentDir(targetPath) {
|
|
26
|
+
const parent = dirname(targetPath);
|
|
27
|
+
if (!existsSync(parent)) {
|
|
28
|
+
mkdirSync(parent, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function removeExisting(targetPath) {
|
|
33
|
+
if (!existsSync(targetPath)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const stat = lstatSync(targetPath);
|
|
39
|
+
if (stat.isSymbolicLink()) {
|
|
40
|
+
unlinkSync(targetPath);
|
|
41
|
+
} else {
|
|
42
|
+
rmSync(targetPath, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.warn(` Warning: Could not remove existing ${targetPath}: ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createSymlink(source, target) {
|
|
50
|
+
const type = process.platform === 'win32' ? 'junction' : 'dir';
|
|
51
|
+
symlinkSync(source, target, type);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function copyFiles(targetPath) {
|
|
55
|
+
mkdirSync(targetPath, { recursive: true });
|
|
56
|
+
|
|
57
|
+
for (const file of filesToCopy) {
|
|
58
|
+
const sourcePath = join(packageRoot, file);
|
|
59
|
+
const destPath = join(targetPath, file);
|
|
60
|
+
|
|
61
|
+
if (!existsSync(sourcePath)) {
|
|
62
|
+
console.warn(` Warning: ${file} not found, skipping`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
cpSync(sourcePath, destPath, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`Installing skill: ${skillName}`);
|
|
71
|
+
console.log(`Mode: ${isDevMode ? 'development (symlink)' : 'production (copy)'}`);
|
|
72
|
+
console.log();
|
|
73
|
+
|
|
74
|
+
for (const target of targets) {
|
|
75
|
+
try {
|
|
76
|
+
ensureParentDir(target.path);
|
|
77
|
+
removeExisting(target.path);
|
|
78
|
+
|
|
79
|
+
if (isDevMode) {
|
|
80
|
+
createSymlink(packageRoot, target.path);
|
|
81
|
+
console.log(`✓ Dev symlink: ${target.path} -> ${packageRoot}`);
|
|
82
|
+
} else {
|
|
83
|
+
copyFiles(target.path);
|
|
84
|
+
console.log(`✓ Installed to ${target.name}: ${target.path}`);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn(`✗ Failed to install to ${target.name}: ${err.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log();
|
|
92
|
+
console.log('Installation complete.');
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cdp-skill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Browser automation skill using Chrome DevTools Protocol for Claude Code and Codex",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cdp-skill": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js",
|
|
12
|
+
"./utils": "./src/utils.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"postinstall": "node install.js",
|
|
19
|
+
"preuninstall": "node uninstall.js",
|
|
20
|
+
"test": "node --test --test-force-exit src/tests/*.test.js",
|
|
21
|
+
"test:run": "node --test --test-force-exit --test-reporter spec src/tests/*.test.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"install.js",
|
|
25
|
+
"uninstall.js",
|
|
26
|
+
"SKILL.md",
|
|
27
|
+
"src/"
|
|
28
|
+
],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"cdp",
|
|
31
|
+
"chrome",
|
|
32
|
+
"devtools",
|
|
33
|
+
"browser",
|
|
34
|
+
"automation",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"skill"
|
|
37
|
+
],
|
|
38
|
+
"skill": {
|
|
39
|
+
"name": "cdp-skill",
|
|
40
|
+
"description": "Automate Chrome browser interactions via CDP",
|
|
41
|
+
"entry": "./SKILL.md"
|
|
42
|
+
},
|
|
43
|
+
"author": "Cezar Lotrean",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"dependencies": {},
|
|
46
|
+
"devDependencies": {}
|
|
47
|
+
}
|