chrome-cdp-cli 1.2.2 → 1.3.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.
@@ -10,59 +10,131 @@ class TakeSnapshotHandler {
10
10
  async execute(client, args) {
11
11
  const snapshotArgs = args;
12
12
  try {
13
- await client.send('DOMSnapshot.enable');
14
13
  await client.send('DOM.enable');
15
14
  await client.send('CSS.enable');
16
- const params = this.buildSnapshotParams(snapshotArgs);
17
- const response = await client.send('DOMSnapshot.captureSnapshot', params);
18
- if (!response || !response.documents || response.documents.length === 0) {
19
- return {
20
- success: false,
21
- error: 'Failed to capture DOM snapshot: empty response'
22
- };
15
+ try {
16
+ return await this.captureWithDOMSnapshot(client, snapshotArgs);
23
17
  }
24
- const processedSnapshot = this.processSnapshot(response, snapshotArgs);
25
- if (snapshotArgs.filename) {
26
- await this.saveSnapshot(processedSnapshot, snapshotArgs.filename, snapshotArgs.format);
27
- return {
28
- success: true,
29
- data: {
30
- message: `DOM snapshot saved to ${snapshotArgs.filename}`,
31
- filename: snapshotArgs.filename,
32
- format: snapshotArgs.format || 'json',
33
- documentsCount: response.documents.length,
34
- nodesCount: response.documents[0]?.nodes?.nodeName?.length || 0
35
- }
36
- };
18
+ catch (domSnapshotError) {
19
+ return await this.captureWithDOM(client, snapshotArgs);
37
20
  }
21
+ }
22
+ catch (error) {
23
+ return {
24
+ success: false,
25
+ error: error instanceof Error ? error.message : String(error)
26
+ };
27
+ }
28
+ }
29
+ async captureWithDOMSnapshot(client, snapshotArgs) {
30
+ await client.send('DOMSnapshot.enable');
31
+ const params = this.buildSnapshotParams(snapshotArgs);
32
+ const response = await client.send('DOMSnapshot.captureSnapshot', params);
33
+ if (!response || !response.documents || response.documents.length === 0) {
34
+ throw new Error('Failed to capture DOM snapshot: empty response');
35
+ }
36
+ const processedSnapshot = this.processSnapshot(response, snapshotArgs);
37
+ if (snapshotArgs.filename) {
38
+ await this.saveSnapshot(processedSnapshot, snapshotArgs.filename, snapshotArgs.format);
38
39
  return {
39
40
  success: true,
40
41
  data: {
41
- snapshot: processedSnapshot,
42
+ message: `DOM snapshot saved to ${snapshotArgs.filename}`,
43
+ filename: snapshotArgs.filename,
42
44
  format: snapshotArgs.format || 'json',
43
45
  documentsCount: response.documents.length,
44
46
  nodesCount: response.documents[0]?.nodes?.nodeName?.length || 0
45
47
  }
46
48
  };
47
49
  }
48
- catch (error) {
50
+ return {
51
+ success: true,
52
+ data: {
53
+ snapshot: processedSnapshot,
54
+ format: snapshotArgs.format || 'json',
55
+ documentsCount: response.documents.length,
56
+ nodesCount: response.documents[0]?.nodes?.nodeName?.length || 0
57
+ }
58
+ };
59
+ }
60
+ async captureWithDOM(client, snapshotArgs) {
61
+ const docResponse = await client.send('DOM.getDocument', { depth: -1 });
62
+ if (!docResponse || !docResponse.root) {
63
+ throw new Error('Failed to get document root');
64
+ }
65
+ const htmlResponse = await client.send('DOM.getOuterHTML', {
66
+ nodeId: docResponse.root.nodeId
67
+ });
68
+ if (!htmlResponse || !htmlResponse.outerHTML) {
69
+ throw new Error('Failed to get document HTML');
70
+ }
71
+ let processedSnapshot;
72
+ if (snapshotArgs.format === 'html') {
73
+ processedSnapshot = htmlResponse.outerHTML;
74
+ }
75
+ else {
76
+ processedSnapshot = {
77
+ metadata: {
78
+ captureTime: new Date().toISOString(),
79
+ method: 'DOM.getOuterHTML',
80
+ documentsCount: 1
81
+ },
82
+ documents: [{
83
+ url: await this.getCurrentURL(client),
84
+ title: await this.getCurrentTitle(client),
85
+ html: htmlResponse.outerHTML,
86
+ domTree: docResponse.root
87
+ }]
88
+ };
89
+ }
90
+ if (snapshotArgs.filename) {
91
+ await this.saveSnapshot(processedSnapshot, snapshotArgs.filename, snapshotArgs.format);
49
92
  return {
50
- success: false,
51
- error: error instanceof Error ? error.message : String(error)
93
+ success: true,
94
+ data: {
95
+ message: `DOM snapshot saved to ${snapshotArgs.filename}`,
96
+ filename: snapshotArgs.filename,
97
+ format: snapshotArgs.format || 'json'
98
+ }
52
99
  };
53
100
  }
101
+ return {
102
+ success: true,
103
+ data: {
104
+ snapshot: processedSnapshot,
105
+ format: snapshotArgs.format || 'json'
106
+ }
107
+ };
108
+ }
109
+ async getCurrentURL(client) {
110
+ try {
111
+ const result = await client.send('Runtime.evaluate', {
112
+ expression: 'window.location.href',
113
+ returnByValue: true
114
+ });
115
+ return result.result?.value || 'unknown';
116
+ }
117
+ catch {
118
+ return 'unknown';
119
+ }
120
+ }
121
+ async getCurrentTitle(client) {
122
+ try {
123
+ const result = await client.send('Runtime.evaluate', {
124
+ expression: 'document.title',
125
+ returnByValue: true
126
+ });
127
+ return result.result?.value || 'unknown';
128
+ }
129
+ catch {
130
+ return 'unknown';
131
+ }
54
132
  }
55
133
  buildSnapshotParams(args) {
56
134
  const params = {};
57
- params.computedStyles = args.includeStyles !== false;
58
- params.includeDOMRects = args.includeAttributes !== false;
59
135
  if (args.includePaintOrder) {
60
- params.includePaintOrder = true;
61
- }
62
- if (args.includeTextIndex) {
63
- params.includeTextIndex = true;
136
+ console.log('Paint order requested but not yet supported');
64
137
  }
65
- params.computedStyleWhitelist = [];
66
138
  return params;
67
139
  }
68
140
  processSnapshot(response, args) {
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.UploadFileHandler = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ class UploadFileHandler {
40
+ constructor() {
41
+ this.name = 'upload_file';
42
+ }
43
+ async execute(client, args) {
44
+ const uploadArgs = args;
45
+ if (!uploadArgs.selector) {
46
+ return {
47
+ success: false,
48
+ error: 'CSS selector is required for upload_file command'
49
+ };
50
+ }
51
+ if (typeof uploadArgs.selector !== 'string') {
52
+ return {
53
+ success: false,
54
+ error: 'CSS selector must be a string'
55
+ };
56
+ }
57
+ if (!uploadArgs.filePath) {
58
+ return {
59
+ success: false,
60
+ error: 'File path is required for upload_file command'
61
+ };
62
+ }
63
+ if (typeof uploadArgs.filePath !== 'string') {
64
+ return {
65
+ success: false,
66
+ error: 'File path must be a string'
67
+ };
68
+ }
69
+ const absoluteFilePath = path.resolve(uploadArgs.filePath);
70
+ if (!fs.existsSync(absoluteFilePath)) {
71
+ return {
72
+ success: false,
73
+ error: `File not found: ${absoluteFilePath}`
74
+ };
75
+ }
76
+ const stats = fs.statSync(absoluteFilePath);
77
+ if (!stats.isFile()) {
78
+ return {
79
+ success: false,
80
+ error: `Path is not a file: ${absoluteFilePath}`
81
+ };
82
+ }
83
+ try {
84
+ await client.send('DOM.enable');
85
+ await client.send('Runtime.enable');
86
+ const timeout = uploadArgs.timeout || 5000;
87
+ const waitForElement = uploadArgs.waitForElement !== false;
88
+ if (waitForElement) {
89
+ const elementFound = await this.waitForElement(client, uploadArgs.selector, timeout);
90
+ if (!elementFound) {
91
+ return {
92
+ success: false,
93
+ error: `Element with selector "${uploadArgs.selector}" not found within ${timeout}ms`
94
+ };
95
+ }
96
+ }
97
+ const isFileInput = await this.verifyFileInput(client, uploadArgs.selector);
98
+ if (!isFileInput.success) {
99
+ return isFileInput;
100
+ }
101
+ const cdpResult = await this.uploadViaCDP(client, uploadArgs.selector, absoluteFilePath);
102
+ if (cdpResult.success) {
103
+ return cdpResult;
104
+ }
105
+ return {
106
+ success: false,
107
+ error: `File upload failed: ${cdpResult.error}. File uploads require CDP support.`
108
+ };
109
+ }
110
+ catch (error) {
111
+ return {
112
+ success: false,
113
+ error: error instanceof Error ? error.message : String(error)
114
+ };
115
+ }
116
+ }
117
+ async waitForElement(client, selector, timeout) {
118
+ const startTime = Date.now();
119
+ while (Date.now() - startTime < timeout) {
120
+ try {
121
+ const result = await client.send('Runtime.evaluate', {
122
+ expression: `document.querySelector('${selector.replace(/'/g, "\\'")}') !== null`,
123
+ returnByValue: true
124
+ });
125
+ if (result.result.value) {
126
+ return true;
127
+ }
128
+ await new Promise(resolve => setTimeout(resolve, 100));
129
+ }
130
+ catch (error) {
131
+ await new Promise(resolve => setTimeout(resolve, 100));
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+ async verifyFileInput(client, selector) {
137
+ try {
138
+ const escapedSelector = selector.replace(/'/g, "\\'").replace(/"/g, '\\"');
139
+ const expression = `
140
+ (function() {
141
+ const element = document.querySelector('${escapedSelector}');
142
+ if (!element) {
143
+ throw new Error('Element with selector "${escapedSelector}" not found');
144
+ }
145
+
146
+ if (element.tagName.toLowerCase() !== 'input') {
147
+ throw new Error('Element is not an input element');
148
+ }
149
+
150
+ if (element.type.toLowerCase() !== 'file') {
151
+ throw new Error('Element is not a file input (type="file")');
152
+ }
153
+
154
+ return {
155
+ success: true,
156
+ tagName: element.tagName,
157
+ type: element.type,
158
+ id: element.id,
159
+ className: element.className,
160
+ multiple: element.multiple,
161
+ accept: element.accept
162
+ };
163
+ })()
164
+ `;
165
+ const response = await client.send('Runtime.evaluate', {
166
+ expression: expression,
167
+ returnByValue: true
168
+ });
169
+ if (response.exceptionDetails) {
170
+ return {
171
+ success: false,
172
+ error: response.exceptionDetails.exception?.description || response.exceptionDetails.text
173
+ };
174
+ }
175
+ return {
176
+ success: true,
177
+ data: response.result.value
178
+ };
179
+ }
180
+ catch (error) {
181
+ return {
182
+ success: false,
183
+ error: `File input verification failed: ${error instanceof Error ? error.message : String(error)}`
184
+ };
185
+ }
186
+ }
187
+ async uploadViaCDP(client, selector, filePath) {
188
+ try {
189
+ const documentResponse = await client.send('DOM.getDocument');
190
+ const rootNodeId = documentResponse.root.nodeId;
191
+ const queryResponse = await client.send('DOM.querySelector', {
192
+ nodeId: rootNodeId,
193
+ selector: selector
194
+ });
195
+ if (!queryResponse.nodeId || queryResponse.nodeId === 0) {
196
+ return {
197
+ success: false,
198
+ error: `File input element with selector "${selector}" not found`
199
+ };
200
+ }
201
+ await client.send('DOM.setFileInputFiles', {
202
+ nodeId: queryResponse.nodeId,
203
+ files: [filePath]
204
+ });
205
+ const changeEventResult = await this.triggerChangeEvent(client, selector, filePath);
206
+ return {
207
+ success: true,
208
+ data: {
209
+ selector: selector,
210
+ filePath: filePath,
211
+ fileName: path.basename(filePath),
212
+ fileSize: fs.statSync(filePath).size,
213
+ method: 'CDP',
214
+ changeEvent: changeEventResult
215
+ }
216
+ };
217
+ }
218
+ catch (error) {
219
+ return {
220
+ success: false,
221
+ error: `CDP file upload failed: ${error instanceof Error ? error.message : String(error)}`
222
+ };
223
+ }
224
+ }
225
+ async triggerChangeEvent(client, selector, filePath) {
226
+ try {
227
+ const escapedSelector = selector.replace(/'/g, "\\'").replace(/"/g, '\\"');
228
+ const fileName = path.basename(filePath);
229
+ const expression = `
230
+ (function() {
231
+ const element = document.querySelector('${escapedSelector}');
232
+ if (element) {
233
+ // Trigger change event
234
+ const changeEvent = new Event('change', { bubbles: true });
235
+ element.dispatchEvent(changeEvent);
236
+
237
+ // Also trigger input event
238
+ const inputEvent = new Event('input', { bubbles: true });
239
+ element.dispatchEvent(inputEvent);
240
+
241
+ return {
242
+ success: true,
243
+ fileName: '${fileName}',
244
+ filesLength: element.files ? element.files.length : 0
245
+ };
246
+ }
247
+ return { success: false, error: 'Element not found' };
248
+ })()
249
+ `;
250
+ const response = await client.send('Runtime.evaluate', {
251
+ expression: expression,
252
+ returnByValue: true,
253
+ userGesture: true
254
+ });
255
+ return response.result.value;
256
+ }
257
+ catch (error) {
258
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
259
+ }
260
+ }
261
+ validateArgs(args) {
262
+ if (typeof args !== 'object' || args === null) {
263
+ return false;
264
+ }
265
+ const uploadArgs = args;
266
+ if (!uploadArgs.selector || typeof uploadArgs.selector !== 'string') {
267
+ return false;
268
+ }
269
+ if (!uploadArgs.filePath || typeof uploadArgs.filePath !== 'string') {
270
+ return false;
271
+ }
272
+ if (uploadArgs.waitForElement !== undefined && typeof uploadArgs.waitForElement !== 'boolean') {
273
+ return false;
274
+ }
275
+ if (uploadArgs.timeout !== undefined && typeof uploadArgs.timeout !== 'number') {
276
+ return false;
277
+ }
278
+ return true;
279
+ }
280
+ getHelp() {
281
+ return `
282
+ upload_file - Upload a file to a file input element
283
+
284
+ Usage:
285
+ upload_file <selector> <filePath>
286
+ upload_file "#file-input" "./document.pdf"
287
+ upload_file "input[type='file']" "/path/to/image.jpg" --timeout 10000
288
+
289
+ Arguments:
290
+ <selector> CSS selector for the file input element
291
+ <filePath> Path to the file to upload (relative or absolute)
292
+
293
+ Options:
294
+ --wait-for-element Wait for element to be available (default: true)
295
+ --no-wait Don't wait for element (same as --wait-for-element=false)
296
+ --timeout <ms> Timeout for waiting for element (default: 5000ms)
297
+
298
+ Examples:
299
+ # Upload a document
300
+ upload_file "#document-upload" "./contract.pdf"
301
+
302
+ # Upload an image
303
+ upload_file "input[name='avatar']" "/home/user/photo.jpg"
304
+
305
+ # Upload with custom timeout
306
+ upload_file ".file-input" "./large-file.zip" --timeout 15000
307
+
308
+ # Upload without waiting (fail immediately if not found)
309
+ upload_file "#upload" "./file.txt" --no-wait
310
+
311
+ Requirements:
312
+ - Target element must be an <input type="file"> element
313
+ - File must exist and be readable
314
+ - File path can be relative (to current directory) or absolute
315
+
316
+ Note:
317
+ - Uses CDP DOM.setFileInputFiles for reliable file upload
318
+ - Automatically triggers 'change' and 'input' events after upload
319
+ - Verifies that target element is a valid file input before upload
320
+ - Supports both single and multiple file inputs
321
+ - File path is resolved to absolute path before upload
322
+ `;
323
+ }
324
+ }
325
+ exports.UploadFileHandler = UploadFileHandler;