cdp-skill 1.0.2 → 1.0.4

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 (78) hide show
  1. package/README.md +3 -0
  2. package/SKILL.md +34 -5
  3. package/package.json +2 -1
  4. package/src/capture/console-capture.js +241 -0
  5. package/src/capture/debug-capture.js +144 -0
  6. package/src/capture/error-aggregator.js +151 -0
  7. package/src/capture/eval-serializer.js +320 -0
  8. package/src/capture/index.js +40 -0
  9. package/src/capture/network-capture.js +211 -0
  10. package/src/capture/pdf-capture.js +256 -0
  11. package/src/capture/screenshot-capture.js +325 -0
  12. package/src/cdp/browser.js +569 -0
  13. package/src/cdp/connection.js +369 -0
  14. package/src/cdp/discovery.js +138 -0
  15. package/src/cdp/index.js +29 -0
  16. package/src/cdp/target-and-session.js +439 -0
  17. package/src/cdp-skill.js +25 -11
  18. package/src/constants.js +79 -0
  19. package/src/dom/actionability.js +638 -0
  20. package/src/dom/click-executor.js +923 -0
  21. package/src/dom/element-handle.js +496 -0
  22. package/src/dom/element-locator.js +475 -0
  23. package/src/dom/element-validator.js +120 -0
  24. package/src/dom/fill-executor.js +489 -0
  25. package/src/dom/index.js +248 -0
  26. package/src/dom/input-emulator.js +406 -0
  27. package/src/dom/keyboard-executor.js +202 -0
  28. package/src/dom/quad-helpers.js +89 -0
  29. package/src/dom/react-filler.js +94 -0
  30. package/src/dom/wait-executor.js +423 -0
  31. package/src/index.js +6 -6
  32. package/src/page/cookie-manager.js +202 -0
  33. package/src/page/dom-stability.js +181 -0
  34. package/src/page/index.js +36 -0
  35. package/src/{page.js → page/page-controller.js} +109 -839
  36. package/src/page/wait-utilities.js +302 -0
  37. package/src/page/web-storage-manager.js +108 -0
  38. package/src/runner/context-helpers.js +224 -0
  39. package/src/runner/execute-browser.js +518 -0
  40. package/src/runner/execute-form.js +315 -0
  41. package/src/runner/execute-input.js +308 -0
  42. package/src/runner/execute-interaction.js +672 -0
  43. package/src/runner/execute-navigation.js +180 -0
  44. package/src/runner/execute-query.js +771 -0
  45. package/src/runner/index.js +51 -0
  46. package/src/runner/step-executors.js +421 -0
  47. package/src/runner/step-validator.js +641 -0
  48. package/src/tests/Actionability.test.js +613 -0
  49. package/src/tests/BrowserClient.test.js +1 -1
  50. package/src/tests/ChromeDiscovery.test.js +1 -1
  51. package/src/tests/ClickExecutor.test.js +554 -0
  52. package/src/tests/ConsoleCapture.test.js +1 -1
  53. package/src/tests/ContextHelpers.test.js +453 -0
  54. package/src/tests/CookieManager.test.js +450 -0
  55. package/src/tests/DebugCapture.test.js +307 -0
  56. package/src/tests/ElementHandle.test.js +1 -1
  57. package/src/tests/ElementLocator.test.js +1 -1
  58. package/src/tests/ErrorAggregator.test.js +1 -1
  59. package/src/tests/EvalSerializer.test.js +391 -0
  60. package/src/tests/FillExecutor.test.js +611 -0
  61. package/src/tests/InputEmulator.test.js +1 -1
  62. package/src/tests/KeyboardExecutor.test.js +430 -0
  63. package/src/tests/NetworkErrorCapture.test.js +1 -1
  64. package/src/tests/PageController.test.js +1 -1
  65. package/src/tests/PdfCapture.test.js +333 -0
  66. package/src/tests/ScreenshotCapture.test.js +1 -1
  67. package/src/tests/SessionRegistry.test.js +1 -1
  68. package/src/tests/StepValidator.test.js +527 -0
  69. package/src/tests/TargetManager.test.js +1 -1
  70. package/src/tests/TestRunner.test.js +1 -1
  71. package/src/tests/WaitStrategy.test.js +1 -1
  72. package/src/tests/WaitUtilities.test.js +508 -0
  73. package/src/tests/WebStorageManager.test.js +333 -0
  74. package/src/types.js +309 -0
  75. package/src/capture.js +0 -1400
  76. package/src/cdp.js +0 -1286
  77. package/src/dom.js +0 -4379
  78. package/src/runner.js +0 -3676
@@ -0,0 +1,369 @@
1
+ /**
2
+ * CDP Connection Module
3
+ * WebSocket-based Chrome DevTools Protocol connection handling
4
+ *
5
+ * PUBLIC EXPORTS:
6
+ * - createConnection(wsUrl, options?) - Factory for CDP WebSocket connection
7
+ *
8
+ * @module cdp-skill/cdp/connection
9
+ */
10
+
11
+ /**
12
+ * Create a CDP WebSocket connection
13
+ * @param {string} wsUrl - WebSocket URL for CDP endpoint
14
+ * @param {Object} [options] - Connection options
15
+ * @param {number} [options.maxRetries=5] - Max reconnection attempts
16
+ * @param {number} [options.retryDelay=1000] - Base delay between retries
17
+ * @param {number} [options.maxRetryDelay=30000] - Maximum retry delay cap
18
+ * @param {boolean} [options.autoReconnect=false] - Enable auto reconnection
19
+ * @returns {import('../types.js').CDPConnection} Connection interface
20
+ */
21
+ export function createConnection(wsUrl, options = {}) {
22
+ const maxRetries = options.maxRetries ?? 5;
23
+ const retryDelay = options.retryDelay ?? 1000;
24
+ const maxRetryDelay = options.maxRetryDelay ?? 30000;
25
+ const autoReconnect = options.autoReconnect ?? false;
26
+
27
+ let ws = null;
28
+ let messageId = 0;
29
+ const pendingCommands = new Map();
30
+ const eventListeners = new Map();
31
+ let connected = false;
32
+ let connecting = false;
33
+ let onCloseCallback = null;
34
+ let reconnecting = false;
35
+ let retryAttempt = 0;
36
+ let intentionalClose = false;
37
+
38
+ function emit(event, data = {}) {
39
+ const listeners = eventListeners.get(event);
40
+ if (listeners) {
41
+ for (const callback of listeners) {
42
+ try {
43
+ callback(data);
44
+ } catch (err) {
45
+ console.error(`Event handler error for ${event}:`, err);
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ function calculateBackoff(attempt) {
52
+ const delay = retryDelay * Math.pow(2, attempt);
53
+ return Math.min(delay, maxRetryDelay);
54
+ }
55
+
56
+ function sleep(ms) {
57
+ return new Promise(resolve => setTimeout(resolve, ms));
58
+ }
59
+
60
+ function rejectPendingCommands(reason) {
61
+ for (const [id, pending] of pendingCommands) {
62
+ clearTimeout(pending.timer);
63
+ pending.reject(new Error(reason));
64
+ }
65
+ pendingCommands.clear();
66
+ }
67
+
68
+ function handleMessage(data) {
69
+ let message;
70
+ try {
71
+ message = JSON.parse(data.toString());
72
+ } catch {
73
+ return;
74
+ }
75
+
76
+ if (message.id !== undefined) {
77
+ const pending = pendingCommands.get(message.id);
78
+ if (pending) {
79
+ clearTimeout(pending.timer);
80
+ pendingCommands.delete(message.id);
81
+ if (message.error) {
82
+ pending.reject(new Error(`CDP error: ${message.error.message}`));
83
+ } else {
84
+ pending.resolve(message.result);
85
+ }
86
+ }
87
+ return;
88
+ }
89
+
90
+ if (message.method) {
91
+ if (message.sessionId) {
92
+ const sessionEventKey = `${message.sessionId}:${message.method}`;
93
+ const sessionListeners = eventListeners.get(sessionEventKey);
94
+ if (sessionListeners) {
95
+ for (const callback of sessionListeners) {
96
+ try {
97
+ callback(message.params, message.sessionId);
98
+ } catch (err) {
99
+ console.error(`Event handler error for ${sessionEventKey}:`, err);
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ const globalListeners = eventListeners.get(message.method);
106
+ if (globalListeners) {
107
+ for (const callback of globalListeners) {
108
+ try {
109
+ callback(message.params, message.sessionId);
110
+ } catch (err) {
111
+ console.error(`Event handler error for ${message.method}:`, err);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ function setupWebSocketListeners() {
119
+ ws.addEventListener('close', () => {
120
+ const wasConnected = connected;
121
+ connected = false;
122
+ connecting = false;
123
+ rejectPendingCommands('Connection closed');
124
+
125
+ if (wasConnected && !intentionalClose && autoReconnect) {
126
+ attemptReconnect();
127
+ } else if (wasConnected && onCloseCallback && !intentionalClose) {
128
+ onCloseCallback('Connection closed unexpectedly');
129
+ }
130
+ });
131
+
132
+ ws.addEventListener('message', (event) => handleMessage(event.data));
133
+ }
134
+
135
+ async function attemptReconnect() {
136
+ if (reconnecting || intentionalClose) return;
137
+
138
+ reconnecting = true;
139
+ retryAttempt = 0;
140
+
141
+ while (retryAttempt < maxRetries && !intentionalClose) {
142
+ const delay = calculateBackoff(retryAttempt);
143
+ emit('reconnecting', { attempt: retryAttempt + 1, delay });
144
+
145
+ await sleep(delay);
146
+ if (intentionalClose) break;
147
+
148
+ try {
149
+ await doReconnect();
150
+ reconnecting = false;
151
+ retryAttempt = 0;
152
+ emit('reconnected', {});
153
+ return;
154
+ } catch {
155
+ retryAttempt++;
156
+ }
157
+ }
158
+
159
+ reconnecting = false;
160
+ if (!intentionalClose && onCloseCallback) {
161
+ onCloseCallback('Connection closed unexpectedly after max retries');
162
+ }
163
+ }
164
+
165
+ function doReconnect() {
166
+ return new Promise((resolve, reject) => {
167
+ ws = new WebSocket(wsUrl);
168
+ ws.addEventListener('open', () => {
169
+ connected = true;
170
+ setupWebSocketListeners();
171
+ resolve();
172
+ });
173
+ ws.addEventListener('error', (event) => {
174
+ reject(new Error(`CDP reconnection error: ${event.message || 'Connection failed'}`));
175
+ });
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Establish WebSocket connection
181
+ * @returns {Promise<void>}
182
+ */
183
+ async function connect() {
184
+ if (connected) return;
185
+ if (connecting) throw new Error('Connection already in progress');
186
+
187
+ connecting = true;
188
+ intentionalClose = false;
189
+
190
+ return new Promise((resolve, reject) => {
191
+ ws = new WebSocket(wsUrl);
192
+
193
+ ws.addEventListener('open', () => {
194
+ connected = true;
195
+ connecting = false;
196
+ resolve();
197
+ });
198
+
199
+ ws.addEventListener('error', (event) => {
200
+ connecting = false;
201
+ reject(new Error(`CDP connection error: ${event.message || 'Connection failed'}`));
202
+ });
203
+
204
+ ws.addEventListener('close', () => {
205
+ const wasConnected = connected;
206
+ connected = false;
207
+ connecting = false;
208
+ rejectPendingCommands('Connection closed');
209
+
210
+ if (wasConnected && !intentionalClose && autoReconnect) {
211
+ attemptReconnect();
212
+ } else if (wasConnected && onCloseCallback && !intentionalClose) {
213
+ onCloseCallback('Connection closed unexpectedly');
214
+ }
215
+ });
216
+
217
+ ws.addEventListener('message', (event) => handleMessage(event.data));
218
+ });
219
+ }
220
+
221
+ /**
222
+ * Send CDP command
223
+ * @param {string} method - CDP method name
224
+ * @param {Object} [params={}] - Command parameters
225
+ * @param {number} [timeout=30000] - Command timeout in ms
226
+ * @returns {Promise<Object>} Command result
227
+ */
228
+ async function send(method, params = {}, timeout = 30000) {
229
+ if (!connected) throw new Error('Not connected to CDP');
230
+
231
+ const id = ++messageId;
232
+ const message = JSON.stringify({ id, method, params });
233
+
234
+ return new Promise((resolve, reject) => {
235
+ const timer = setTimeout(() => {
236
+ pendingCommands.delete(id);
237
+ reject(new Error(`CDP command timeout: ${method}`));
238
+ }, timeout);
239
+
240
+ pendingCommands.set(id, { resolve, reject, timer });
241
+ ws.send(message);
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Send CDP command to specific session
247
+ * @param {string} sessionId - Target session ID
248
+ * @param {string} method - CDP method name
249
+ * @param {Object} [params={}] - Command parameters
250
+ * @param {number} [timeout=30000] - Command timeout in ms
251
+ * @returns {Promise<Object>} Command result
252
+ */
253
+ async function sendToSession(sessionId, method, params = {}, timeout = 30000) {
254
+ if (!connected) throw new Error('Not connected to CDP');
255
+
256
+ const id = ++messageId;
257
+ const message = JSON.stringify({ id, sessionId, method, params });
258
+
259
+ return new Promise((resolve, reject) => {
260
+ const timer = setTimeout(() => {
261
+ pendingCommands.delete(id);
262
+ reject(new Error(`CDP command timeout: ${method} (session: ${sessionId})`));
263
+ }, timeout);
264
+
265
+ pendingCommands.set(id, { resolve, reject, timer });
266
+ ws.send(message);
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Subscribe to CDP event
272
+ * @param {string} event - Event name
273
+ * @param {function} callback - Event handler
274
+ */
275
+ function on(event, callback) {
276
+ if (!eventListeners.has(event)) {
277
+ eventListeners.set(event, new Set());
278
+ }
279
+ eventListeners.get(event).add(callback);
280
+ }
281
+
282
+ /**
283
+ * Unsubscribe from CDP event
284
+ * @param {string} event - Event name
285
+ * @param {function} callback - Event handler
286
+ */
287
+ function off(event, callback) {
288
+ const listeners = eventListeners.get(event);
289
+ if (listeners) {
290
+ listeners.delete(callback);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Wait for specific event
296
+ * @param {string} event - Event name
297
+ * @param {function} [predicate=()=>true] - Filter predicate
298
+ * @param {number} [timeout=30000] - Timeout in ms
299
+ * @returns {Promise<Object>} Event parameters
300
+ */
301
+ function waitForEvent(event, predicate = () => true, timeout = 30000) {
302
+ return new Promise((resolve, reject) => {
303
+ const timer = setTimeout(() => {
304
+ off(event, handler);
305
+ reject(new Error(`Timeout waiting for event: ${event}`));
306
+ }, timeout);
307
+
308
+ const handler = (params) => {
309
+ if (predicate(params)) {
310
+ clearTimeout(timer);
311
+ off(event, handler);
312
+ resolve(params);
313
+ }
314
+ };
315
+
316
+ on(event, handler);
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Close the connection
322
+ * @returns {Promise<void>}
323
+ */
324
+ async function close() {
325
+ intentionalClose = true;
326
+ reconnecting = false;
327
+ if (ws) {
328
+ rejectPendingCommands('Connection closed');
329
+ ws.close();
330
+ ws = null;
331
+ connected = false;
332
+ eventListeners.clear();
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Remove all event listeners
338
+ * @param {string} [event] - Specific event, or all if omitted
339
+ */
340
+ function removeAllListeners(event) {
341
+ if (event) {
342
+ eventListeners.delete(event);
343
+ } else {
344
+ eventListeners.clear();
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Set close callback
350
+ * @param {function} callback - Callback for unexpected close
351
+ */
352
+ function onClose(callback) {
353
+ onCloseCallback = callback;
354
+ }
355
+
356
+ return {
357
+ connect,
358
+ send,
359
+ sendToSession,
360
+ on,
361
+ off,
362
+ waitForEvent,
363
+ close,
364
+ removeAllListeners,
365
+ onClose,
366
+ isConnected: () => connected,
367
+ getWsUrl: () => wsUrl
368
+ };
369
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * CDP Discovery Module
3
+ * HTTP-based Chrome DevTools Protocol endpoint discovery
4
+ *
5
+ * PUBLIC EXPORTS:
6
+ * - createDiscovery(host?, port?, timeout?) - Factory for CDP discovery
7
+ * - discoverChrome(host?, port?, timeout?) - Convenience function
8
+ *
9
+ * @module cdp-skill/cdp/discovery
10
+ */
11
+
12
+ /**
13
+ * Discover Chrome CDP endpoints via HTTP
14
+ * @param {string} [host='localhost'] - Chrome debugging host
15
+ * @param {number} [port=9222] - Chrome debugging port
16
+ * @param {number} [timeout=5000] - Request timeout in ms
17
+ * @returns {Object} Discovery interface
18
+ */
19
+ export function createDiscovery(host = 'localhost', port = 9222, timeout = 5000) {
20
+ const baseUrl = `http://${host}:${port}`;
21
+
22
+ function createTimeoutController() {
23
+ const controller = new AbortController();
24
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
25
+ return {
26
+ signal: controller.signal,
27
+ clear: () => clearTimeout(timeoutId)
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Get Chrome version information
33
+ * @returns {Promise<{browser: string, protocolVersion: string, webSocketDebuggerUrl: string}>}
34
+ */
35
+ async function getVersion() {
36
+ const timeoutCtrl = createTimeoutController();
37
+ try {
38
+ const response = await fetch(`${baseUrl}/json/version`, { signal: timeoutCtrl.signal });
39
+ if (!response.ok) {
40
+ throw new Error(`Chrome not reachable at ${baseUrl}: ${response.status}`);
41
+ }
42
+ const data = await response.json();
43
+ return {
44
+ browser: data.Browser,
45
+ protocolVersion: data['Protocol-Version'],
46
+ webSocketDebuggerUrl: data.webSocketDebuggerUrl
47
+ };
48
+ } catch (err) {
49
+ if (err.name === 'AbortError') {
50
+ throw new Error(`Chrome discovery timeout at ${baseUrl}`);
51
+ }
52
+ throw err;
53
+ } finally {
54
+ timeoutCtrl.clear();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get all browser targets
60
+ * @returns {Promise<Array>} Array of target info objects
61
+ */
62
+ async function getTargets() {
63
+ const timeoutCtrl = createTimeoutController();
64
+ try {
65
+ const response = await fetch(`${baseUrl}/json/list`, { signal: timeoutCtrl.signal });
66
+ if (!response.ok) {
67
+ throw new Error(`Failed to get targets: ${response.status}`);
68
+ }
69
+ return response.json();
70
+ } catch (err) {
71
+ if (err.name === 'AbortError') {
72
+ throw new Error('Chrome discovery timeout getting targets');
73
+ }
74
+ throw err;
75
+ } finally {
76
+ timeoutCtrl.clear();
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get page targets only
82
+ * @returns {Promise<Array>} Array of page target info objects
83
+ */
84
+ async function getPages() {
85
+ const targets = await getTargets();
86
+ return targets.filter(t => t.type === 'page');
87
+ }
88
+
89
+ /**
90
+ * Find a page by URL pattern
91
+ * @param {string|RegExp} urlPattern - URL pattern to match
92
+ * @returns {Promise<Object|null>} Matching target or null
93
+ */
94
+ async function findPageByUrl(urlPattern) {
95
+ const pages = await getPages();
96
+ const regex = urlPattern instanceof RegExp ? urlPattern : new RegExp(urlPattern);
97
+ return pages.find(p => regex.test(p.url)) || null;
98
+ }
99
+
100
+ /**
101
+ * Check if Chrome is available
102
+ * @returns {Promise<boolean>}
103
+ */
104
+ async function isAvailable() {
105
+ try {
106
+ await getVersion();
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ return {
114
+ getVersion,
115
+ getTargets,
116
+ getPages,
117
+ findPageByUrl,
118
+ isAvailable
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Convenience function to discover Chrome
124
+ * @param {string} [host='localhost'] - Chrome debugging host
125
+ * @param {number} [port=9222] - Chrome debugging port
126
+ * @param {number} [timeout=5000] - Request timeout in ms
127
+ * @returns {Promise<{wsUrl: string, version: Object, targets: Array}>}
128
+ */
129
+ export async function discoverChrome(host = 'localhost', port = 9222, timeout = 5000) {
130
+ const discovery = createDiscovery(host, port, timeout);
131
+ const version = await discovery.getVersion();
132
+ const targets = await discovery.getTargets();
133
+ return {
134
+ wsUrl: version.webSocketDebuggerUrl,
135
+ version,
136
+ targets
137
+ };
138
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * CDP Protocol Module
3
+ * Re-exports all CDP-related functionality
4
+ *
5
+ * @module cdp-skill/cdp
6
+ */
7
+
8
+ // Connection
9
+ export { createConnection } from './connection.js';
10
+
11
+ // Discovery
12
+ export { createDiscovery, discoverChrome } from './discovery.js';
13
+
14
+ // Target and Session Management
15
+ export {
16
+ createTargetManager,
17
+ createSessionRegistry,
18
+ createPageSession
19
+ } from './target-and-session.js';
20
+
21
+ // Browser Client and Launcher
22
+ export {
23
+ createBrowser,
24
+ findChromePath,
25
+ launchChrome,
26
+ getChromeStatus,
27
+ isChromeProcessRunning,
28
+ createNewTab
29
+ } from './browser.js';