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 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
+ }