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/src/cli.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CDP Skill CLI
|
|
4
|
+
*
|
|
5
|
+
* JSON interpreter for browser automation. Reads JSON from stdin,
|
|
6
|
+
* executes browser automation steps, and outputs JSON results.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* echo '{"steps":[{"goto":"https://example.com"}]}' | node src/cli.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createBrowser } from './cdp.js';
|
|
13
|
+
import { createPageController } from './page.js';
|
|
14
|
+
import { createElementLocator, createInputEmulator } from './dom.js';
|
|
15
|
+
import { createScreenshotCapture, createConsoleCapture, createPdfCapture } from './capture.js';
|
|
16
|
+
import { createAriaSnapshot } from './aria.js';
|
|
17
|
+
import { createCookieManager } from './page.js';
|
|
18
|
+
import { runSteps } from './runner.js';
|
|
19
|
+
|
|
20
|
+
const ErrorType = {
|
|
21
|
+
PARSE: 'PARSE',
|
|
22
|
+
VALIDATION: 'VALIDATION',
|
|
23
|
+
CONNECTION: 'CONNECTION',
|
|
24
|
+
EXECUTION: 'EXECUTION'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reads entire stdin and returns as string
|
|
29
|
+
*/
|
|
30
|
+
async function readStdin() {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
|
|
34
|
+
process.stdin.setEncoding('utf8');
|
|
35
|
+
|
|
36
|
+
process.stdin.on('data', chunk => {
|
|
37
|
+
chunks.push(chunk);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
process.stdin.on('end', () => {
|
|
41
|
+
resolve(chunks.join(''));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
process.stdin.on('error', err => {
|
|
45
|
+
reject(err);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parses JSON input and validates basic structure
|
|
52
|
+
*/
|
|
53
|
+
function parseInput(input) {
|
|
54
|
+
const trimmed = input.trim();
|
|
55
|
+
|
|
56
|
+
if (!trimmed) {
|
|
57
|
+
throw { type: ErrorType.PARSE, message: 'Empty input' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let json;
|
|
61
|
+
try {
|
|
62
|
+
json = JSON.parse(trimmed);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
throw { type: ErrorType.PARSE, message: `Invalid JSON: ${err.message}` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!json || typeof json !== 'object') {
|
|
68
|
+
throw { type: ErrorType.VALIDATION, message: 'Input must be a JSON object' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!json.steps) {
|
|
72
|
+
throw { type: ErrorType.VALIDATION, message: 'Missing required "steps" array' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!Array.isArray(json.steps)) {
|
|
76
|
+
throw { type: ErrorType.VALIDATION, message: '"steps" must be an array' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (json.steps.length === 0) {
|
|
80
|
+
throw { type: ErrorType.VALIDATION, message: '"steps" array cannot be empty' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return json;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates error response JSON
|
|
88
|
+
*/
|
|
89
|
+
function errorResponse(type, message) {
|
|
90
|
+
return {
|
|
91
|
+
status: 'error',
|
|
92
|
+
error: { type, message }
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Main CLI execution
|
|
98
|
+
*/
|
|
99
|
+
async function main() {
|
|
100
|
+
let browser = null;
|
|
101
|
+
let pageController = null;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Read and parse input
|
|
105
|
+
const input = await readStdin();
|
|
106
|
+
const json = parseInput(input);
|
|
107
|
+
|
|
108
|
+
// Extract config with defaults
|
|
109
|
+
const config = json.config || {};
|
|
110
|
+
const host = config.host || 'localhost';
|
|
111
|
+
const port = config.port || 9222;
|
|
112
|
+
const timeout = config.timeout || 30000;
|
|
113
|
+
|
|
114
|
+
// Connect to browser
|
|
115
|
+
browser = createBrowser({ host, port, connectTimeout: timeout });
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await browser.connect();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
throw {
|
|
121
|
+
type: ErrorType.CONNECTION,
|
|
122
|
+
message: `Chrome not running on ${host}:${port} - ${err.message}`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Get or create page session
|
|
127
|
+
let session;
|
|
128
|
+
if (config.targetId) {
|
|
129
|
+
try {
|
|
130
|
+
session = await browser.attachToPage(config.targetId);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw {
|
|
133
|
+
type: ErrorType.CONNECTION,
|
|
134
|
+
message: `Could not attach to tab ${config.targetId}: ${err.message}`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
try {
|
|
139
|
+
// If Chrome was just started, it has an empty tab - reuse it instead of creating another
|
|
140
|
+
// Otherwise create a new tab for this test session
|
|
141
|
+
// User should pass targetId from response to subsequent calls to reuse the same tab
|
|
142
|
+
const emptyTab = await browser.findPage(/^(about:blank|chrome:\/\/newtab)/);
|
|
143
|
+
if (emptyTab) {
|
|
144
|
+
session = emptyTab;
|
|
145
|
+
} else {
|
|
146
|
+
session = await browser.newPage();
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
// Check if Chrome has no tabs open (common when started with --remote-debugging-port)
|
|
150
|
+
if (err.message.includes('no browser is open')) {
|
|
151
|
+
throw {
|
|
152
|
+
type: ErrorType.CONNECTION,
|
|
153
|
+
message: `Chrome has no tabs open. This can happen when Chrome is started with --remote-debugging-port but no window is visible. Try opening a new Chrome window or restarting Chrome normally.`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
throw {
|
|
157
|
+
type: ErrorType.EXECUTION,
|
|
158
|
+
message: `Failed to create new tab: ${err.message}`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create dependencies
|
|
164
|
+
pageController = createPageController(session);
|
|
165
|
+
const elementLocator = createElementLocator(session);
|
|
166
|
+
const inputEmulator = createInputEmulator(session);
|
|
167
|
+
const screenshotCapture = createScreenshotCapture(session);
|
|
168
|
+
const consoleCapture = createConsoleCapture(session);
|
|
169
|
+
const pdfCapture = createPdfCapture(session);
|
|
170
|
+
const ariaSnapshot = createAriaSnapshot(session);
|
|
171
|
+
const cookieManager = createCookieManager(session);
|
|
172
|
+
|
|
173
|
+
// Initialize page controller (enables required CDP domains)
|
|
174
|
+
await pageController.initialize();
|
|
175
|
+
|
|
176
|
+
// Start console capture to collect logs during execution
|
|
177
|
+
await consoleCapture.startCapture();
|
|
178
|
+
|
|
179
|
+
const deps = {
|
|
180
|
+
browser,
|
|
181
|
+
pageController,
|
|
182
|
+
elementLocator,
|
|
183
|
+
inputEmulator,
|
|
184
|
+
screenshotCapture,
|
|
185
|
+
consoleCapture,
|
|
186
|
+
pdfCapture,
|
|
187
|
+
ariaSnapshot,
|
|
188
|
+
cookieManager
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Run steps
|
|
192
|
+
const result = await runSteps(deps, json.steps, {
|
|
193
|
+
stopOnError: true,
|
|
194
|
+
stepTimeout: timeout
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Build output with tab info
|
|
198
|
+
const viewport = await pageController.getViewport();
|
|
199
|
+
const output = {
|
|
200
|
+
status: result.status,
|
|
201
|
+
tab: {
|
|
202
|
+
targetId: session.targetId,
|
|
203
|
+
url: await pageController.getUrl(),
|
|
204
|
+
title: await pageController.getTitle(),
|
|
205
|
+
viewport
|
|
206
|
+
},
|
|
207
|
+
steps: result.steps,
|
|
208
|
+
outputs: result.outputs,
|
|
209
|
+
errors: result.errors,
|
|
210
|
+
screenshots: result.screenshots
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Output result
|
|
214
|
+
console.log(JSON.stringify(output));
|
|
215
|
+
|
|
216
|
+
// Cleanup
|
|
217
|
+
await consoleCapture.stopCapture();
|
|
218
|
+
pageController.dispose();
|
|
219
|
+
await browser.disconnect();
|
|
220
|
+
|
|
221
|
+
process.exit(result.status === 'passed' ? 0 : 1);
|
|
222
|
+
|
|
223
|
+
} catch (err) {
|
|
224
|
+
// Cleanup on error
|
|
225
|
+
if (pageController) {
|
|
226
|
+
try { pageController.dispose(); } catch (e) { /* ignore */ }
|
|
227
|
+
}
|
|
228
|
+
if (browser) {
|
|
229
|
+
try { await browser.disconnect(); } catch (e) { /* ignore */ }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Handle known error types
|
|
233
|
+
if (err.type) {
|
|
234
|
+
console.log(JSON.stringify(errorResponse(err.type, err.message)));
|
|
235
|
+
} else {
|
|
236
|
+
// Unknown error
|
|
237
|
+
console.log(JSON.stringify(errorResponse(ErrorType.EXECUTION, err.message || String(err))));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
main();
|