chrome-cdp-cli 1.7.9 → 1.8.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.
@@ -373,6 +373,9 @@ class CLIInterface {
373
373
  }
374
374
  if (result.data && typeof result.data === 'object') {
375
375
  const data = result.data;
376
+ if (data.snapshot && typeof data.snapshot === 'string') {
377
+ return data.snapshot;
378
+ }
376
379
  if (data.messages && Array.isArray(data.messages)) {
377
380
  output += dataSourceInfo;
378
381
  if (data.messages.length === 0) {
@@ -47,6 +47,20 @@ class TakeSnapshotHandler {
47
47
  }
48
48
  };
49
49
  }
50
+ if (snapshotArgs.format !== 'html' && typeof processedSnapshot === 'object' && processedSnapshot !== null) {
51
+ const snapshotObj = processedSnapshot;
52
+ if (snapshotObj.snapshot && typeof snapshotObj.snapshot === 'string') {
53
+ return {
54
+ success: true,
55
+ data: {
56
+ snapshot: snapshotObj.snapshot,
57
+ format: 'text',
58
+ documentsCount: response.documents.length,
59
+ nodesCount: response.documents[0]?.nodes?.nodeName?.length || 0
60
+ }
61
+ };
62
+ }
63
+ }
50
64
  return {
51
65
  success: true,
52
66
  data: {
@@ -62,29 +76,24 @@ class TakeSnapshotHandler {
62
76
  if (!docResponse || !docResponse.root) {
63
77
  throw new Error('Failed to get document root');
64
78
  }
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
- }
79
+ const url = await this.getCurrentURL(client);
80
+ const title = await this.getCurrentTitle(client);
71
81
  let processedSnapshot;
72
82
  if (snapshotArgs.format === 'html') {
83
+ const htmlResponse = await client.send('DOM.getOuterHTML', {
84
+ nodeId: docResponse.root.nodeId
85
+ });
86
+ if (!htmlResponse || !htmlResponse.outerHTML) {
87
+ throw new Error('Failed to get document HTML');
88
+ }
73
89
  processedSnapshot = htmlResponse.outerHTML;
74
90
  }
75
91
  else {
92
+ const textSnapshot = this.buildTextFromDOMNode(docResponse.root, url, title);
76
93
  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
- }]
94
+ url,
95
+ title,
96
+ snapshot: textSnapshot
88
97
  };
89
98
  }
90
99
  if (snapshotArgs.filename) {
@@ -98,6 +107,18 @@ class TakeSnapshotHandler {
98
107
  }
99
108
  };
100
109
  }
110
+ if (snapshotArgs.format !== 'html' && typeof processedSnapshot === 'object' && processedSnapshot !== null) {
111
+ const snapshotObj = processedSnapshot;
112
+ if (snapshotObj.snapshot && typeof snapshotObj.snapshot === 'string') {
113
+ return {
114
+ success: true,
115
+ data: {
116
+ snapshot: snapshotObj.snapshot,
117
+ format: 'text'
118
+ }
119
+ };
120
+ }
121
+ }
101
122
  return {
102
123
  success: true,
103
124
  data: {
@@ -148,11 +169,12 @@ class TakeSnapshotHandler {
148
169
  };
149
170
  }
150
171
  const textSnapshot = this.createTextSnapshot(doc, response.strings);
151
- return {
172
+ const result = {
152
173
  url: doc.documentURL,
153
174
  title: doc.title,
154
175
  snapshot: textSnapshot
155
176
  };
177
+ return result;
156
178
  }
157
179
  createTextSnapshot(doc, strings) {
158
180
  const nodes = doc.nodes;
@@ -160,7 +182,7 @@ class TakeSnapshotHandler {
160
182
  return 'Empty document';
161
183
  }
162
184
  const nodeTree = this.buildNodeTree(doc, strings);
163
- let output = `PAGE: ${doc.title || 'Untitled'} (${doc.documentURL})\n`;
185
+ let output = `PAGE: ${doc.title || 'Untitled'}\n`;
164
186
  const bodyNode = this.findBodyNode(nodeTree);
165
187
  if (bodyNode) {
166
188
  output += this.renderNodeAsText(bodyNode, 0);
@@ -256,28 +278,30 @@ class TakeSnapshotHandler {
256
278
  return findNode(nodeTree, 'body') || findNode(nodeTree, 'main') || null;
257
279
  }
258
280
  shouldIncludeNode(node) {
259
- const skipTags = ['script', 'style', 'meta', 'link', 'head', 'noscript', 'svg', 'path'];
281
+ const skipTags = ['script', 'style', 'meta', 'link', 'head', 'noscript'];
260
282
  if (skipTags.includes(node.nodeName)) {
261
283
  return false;
262
284
  }
263
- if (node.nodeType === 3 && !node.textContent) {
264
- return false;
265
- }
266
- if (node.nodeType === 1 && !node.textContent && node.children.length === 0) {
267
- return false;
285
+ if (node.nodeType === 3) {
286
+ const text = (node.textContent || '').trim();
287
+ return text.length > 0;
268
288
  }
269
289
  return true;
270
290
  }
271
- renderNodeAsText(node, depth) {
291
+ renderNodeAsText(node, depth, isLast = false, parentIsLast = []) {
272
292
  if (!this.shouldIncludeNode(node)) {
273
293
  return '';
274
294
  }
275
- const indent = ''.repeat(depth);
276
- const prefix = depth > 0 ? '├── ' : '';
295
+ let indent = '';
296
+ for (let i = 0; i < parentIsLast.length; i++) {
297
+ indent += parentIsLast[i] ? ' ' : '│ ';
298
+ }
299
+ const prefix = depth > 0 ? (isLast ? '└── ' : '├── ') : '';
277
300
  let output = '';
278
301
  if (node.nodeType === 3) {
279
302
  if (node.textContent) {
280
- output += `${indent}${prefix}"${node.textContent}"\n`;
303
+ const truncatedText = this.truncateText(node.textContent.trim(), 40);
304
+ output += `${indent}${prefix}"${truncatedText}"\n`;
281
305
  }
282
306
  }
283
307
  else if (node.nodeType === 1) {
@@ -286,8 +310,12 @@ class TakeSnapshotHandler {
286
310
  const attrs = [];
287
311
  if (node.attributes.id)
288
312
  attrs.push(`#${node.attributes.id}`);
289
- if (node.attributes.class)
290
- attrs.push(`.${node.attributes.class.split(' ')[0]}`);
313
+ if (node.attributes.class) {
314
+ const classes = node.attributes.class.split(/\s+/).filter((c) => c.trim().length > 0);
315
+ classes.forEach((cls) => {
316
+ attrs.push(`.${cls.trim()}`);
317
+ });
318
+ }
291
319
  if (node.attributes.type)
292
320
  attrs.push(`[${node.attributes.type}]`);
293
321
  if (node.attributes.name)
@@ -296,17 +324,27 @@ class TakeSnapshotHandler {
296
324
  description += `(${attrs.join(' ')})`;
297
325
  }
298
326
  if (node.nodeName === 'img' && node.attributes.alt) {
299
- description += `: "${node.attributes.alt}"`;
327
+ const altText = this.truncateText(node.attributes.alt, 40);
328
+ description += `: "${altText}"`;
300
329
  }
301
330
  else if (node.nodeName === 'a' && node.attributes.href) {
302
331
  description += `: "${node.attributes.href}"`;
303
332
  }
304
333
  else if (['input', 'textarea'].includes(node.nodeName)) {
305
334
  if (node.attributes.placeholder) {
306
- description += `: "${node.attributes.placeholder}"`;
335
+ const placeholderText = this.truncateText(node.attributes.placeholder, 40);
336
+ description += `: "${placeholderText}"`;
307
337
  }
308
338
  else if (node.inputValue) {
309
- description += `: "${node.inputValue}"`;
339
+ const inputText = this.truncateText(node.inputValue, 40);
340
+ description += `: "${inputText}"`;
341
+ }
342
+ else if (node.nodeName === 'textarea') {
343
+ const textContent = this.extractTextContent(node);
344
+ if (textContent) {
345
+ const truncatedText = this.truncateText(textContent, 40);
346
+ description += `: "${truncatedText}"`;
347
+ }
310
348
  }
311
349
  if (node.checked)
312
350
  description += ' [checked]';
@@ -314,20 +352,31 @@ class TakeSnapshotHandler {
314
352
  else if (node.nodeName === 'button' || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.nodeName)) {
315
353
  const textContent = this.extractTextContent(node);
316
354
  if (textContent) {
317
- description += `: "${textContent}"`;
355
+ const truncatedText = this.truncateText(textContent, 40);
356
+ description += `: "${truncatedText}"`;
318
357
  }
319
358
  }
320
359
  output += `${indent}${prefix}${description}\n`;
360
+ if (node.nodeName === 'textarea') {
361
+ return output;
362
+ }
321
363
  const meaningfulChildren = node.children.filter((child) => this.shouldIncludeNode(child));
322
- if (['p', 'div', 'span', 'li', 'td', 'th'].includes(node.nodeName)) {
364
+ if (meaningfulChildren.length === 0) {
323
365
  const textContent = this.extractTextContent(node);
324
- if (textContent && meaningfulChildren.length <= 1) {
325
- output += `${indent}│ └── "${textContent}"\n`;
366
+ if (textContent && textContent.trim().length > 0) {
367
+ const truncatedText = this.truncateText(textContent.trim(), 40);
368
+ output += `${indent}│ └── "${truncatedText}"\n`;
326
369
  return output;
327
370
  }
371
+ return output;
328
372
  }
329
- for (const child of meaningfulChildren) {
330
- output += this.renderNodeAsText(child, depth + 1);
373
+ else {
374
+ const newParentIsLast = [...parentIsLast, isLast];
375
+ for (let i = 0; i < meaningfulChildren.length; i++) {
376
+ const child = meaningfulChildren[i];
377
+ const childIsLast = i === meaningfulChildren.length - 1;
378
+ output += this.renderNodeAsText(child, depth + 1, childIsLast, newParentIsLast);
379
+ }
331
380
  }
332
381
  }
333
382
  return output;
@@ -345,6 +394,180 @@ class TakeSnapshotHandler {
345
394
  }
346
395
  return text.trim().replace(/\s+/g, ' ');
347
396
  }
397
+ truncateText(text, maxLength) {
398
+ if (!text)
399
+ return '';
400
+ if (text.length <= maxLength)
401
+ return text;
402
+ return text.substring(0, maxLength) + '...';
403
+ }
404
+ buildTextFromDOMNode(root, _url, title) {
405
+ let output = `PAGE: ${title || 'Untitled'}\n`;
406
+ const bodyNode = this.findBodyInDOMTree(root);
407
+ if (bodyNode) {
408
+ output += this.renderDOMNodeAsText(bodyNode, 0);
409
+ }
410
+ else {
411
+ if (root.children) {
412
+ for (const child of root.children) {
413
+ if (this.shouldIncludeDOMNode(child)) {
414
+ output += this.renderDOMNodeAsText(child, 0);
415
+ }
416
+ }
417
+ }
418
+ }
419
+ return output;
420
+ }
421
+ findBodyInDOMTree(node) {
422
+ if (node.nodeName && node.nodeName.toLowerCase() === 'body') {
423
+ return node;
424
+ }
425
+ if (node.nodeName && node.nodeName.toLowerCase() === 'main') {
426
+ return node;
427
+ }
428
+ if (node.children) {
429
+ for (const child of node.children) {
430
+ const found = this.findBodyInDOMTree(child);
431
+ if (found)
432
+ return found;
433
+ }
434
+ }
435
+ return null;
436
+ }
437
+ shouldIncludeDOMNode(node) {
438
+ if (!node)
439
+ return false;
440
+ const nodeName = node.nodeName ? node.nodeName.toLowerCase() : '';
441
+ const skipTags = ['script', 'style', 'meta', 'link', 'head', 'noscript'];
442
+ if (skipTags.includes(nodeName)) {
443
+ return false;
444
+ }
445
+ if (node.nodeType === 3) {
446
+ const text = node.nodeValue || '';
447
+ return text.trim().length > 0;
448
+ }
449
+ return true;
450
+ }
451
+ renderDOMNodeAsText(node, depth, isLast = false, parentIsLast = []) {
452
+ if (!this.shouldIncludeDOMNode(node)) {
453
+ return '';
454
+ }
455
+ let indent = '';
456
+ for (let i = 0; i < parentIsLast.length; i++) {
457
+ indent += parentIsLast[i] ? ' ' : '│ ';
458
+ }
459
+ const prefix = depth > 0 ? (isLast ? '└── ' : '├── ') : '';
460
+ let output = '';
461
+ if (node.nodeType === 3) {
462
+ const text = (node.nodeValue || '').trim();
463
+ if (text) {
464
+ const truncatedText = this.truncateText(text, 40);
465
+ output += `${indent}${prefix}"${truncatedText}"\n`;
466
+ }
467
+ }
468
+ else if (node.nodeType === 1) {
469
+ const tag = (node.nodeName || '').toUpperCase();
470
+ let description = tag;
471
+ const attrs = [];
472
+ if (node.attributes) {
473
+ for (let i = 0; i < node.attributes.length; i += 2) {
474
+ const name = node.attributes[i];
475
+ const value = node.attributes[i + 1];
476
+ if (['id', 'class', 'type', 'name', 'href', 'src', 'alt', 'placeholder', 'value', 'title'].includes(name)) {
477
+ if (name === 'id')
478
+ attrs.push(`#${value}`);
479
+ else if (name === 'class') {
480
+ const classes = value.split(/\s+/).filter((c) => c.trim().length > 0);
481
+ classes.forEach((cls) => {
482
+ attrs.push(`.${cls.trim()}`);
483
+ });
484
+ }
485
+ else if (name === 'type')
486
+ attrs.push(`[${value}]`);
487
+ else
488
+ attrs.push(`${name}="${value}"`);
489
+ }
490
+ }
491
+ }
492
+ if (attrs.length > 0) {
493
+ description += `(${attrs.join(' ')})`;
494
+ }
495
+ const nodeName = (node.nodeName || '').toLowerCase();
496
+ if (nodeName === 'img' && node.attributes) {
497
+ const altIndex = node.attributes.indexOf('alt');
498
+ if (altIndex >= 0 && altIndex + 1 < node.attributes.length) {
499
+ const altText = this.truncateText(node.attributes[altIndex + 1], 40);
500
+ description += `: "${altText}"`;
501
+ }
502
+ }
503
+ else if (nodeName === 'a' && node.attributes) {
504
+ const hrefIndex = node.attributes.indexOf('href');
505
+ if (hrefIndex >= 0 && hrefIndex + 1 < node.attributes.length) {
506
+ description += `: "${node.attributes[hrefIndex + 1]}"`;
507
+ }
508
+ }
509
+ else if (['input', 'textarea'].includes(nodeName)) {
510
+ if (node.attributes) {
511
+ const placeholderIndex = node.attributes.indexOf('placeholder');
512
+ if (placeholderIndex >= 0 && placeholderIndex + 1 < node.attributes.length) {
513
+ const placeholderText = this.truncateText(node.attributes[placeholderIndex + 1], 40);
514
+ description += `: "${placeholderText}"`;
515
+ }
516
+ }
517
+ if (nodeName === 'textarea') {
518
+ const textContent = this.extractTextFromDOMNode(node);
519
+ if (textContent) {
520
+ const truncatedText = this.truncateText(textContent, 40);
521
+ description += `: "${truncatedText}"`;
522
+ }
523
+ }
524
+ }
525
+ else if (nodeName === 'button' || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(nodeName)) {
526
+ const textContent = this.extractTextFromDOMNode(node);
527
+ if (textContent) {
528
+ const truncatedText = this.truncateText(textContent, 40);
529
+ description += `: "${truncatedText}"`;
530
+ }
531
+ }
532
+ output += `${indent}${prefix}${description}\n`;
533
+ if (nodeName === 'textarea') {
534
+ return output;
535
+ }
536
+ if (node.children) {
537
+ const meaningfulChildren = node.children.filter((child) => this.shouldIncludeDOMNode(child));
538
+ if (meaningfulChildren.length === 0) {
539
+ const textContent = this.extractTextFromDOMNode(node);
540
+ if (textContent && textContent.trim().length > 0) {
541
+ const truncatedText = this.truncateText(textContent.trim(), 40);
542
+ output += `${indent}│ └── "${truncatedText}"\n`;
543
+ return output;
544
+ }
545
+ return output;
546
+ }
547
+ else {
548
+ const newParentIsLast = [...parentIsLast, isLast];
549
+ for (let i = 0; i < meaningfulChildren.length; i++) {
550
+ const child = meaningfulChildren[i];
551
+ const childIsLast = i === meaningfulChildren.length - 1;
552
+ output += this.renderDOMNodeAsText(child, depth + 1, childIsLast, newParentIsLast);
553
+ }
554
+ }
555
+ }
556
+ }
557
+ return output;
558
+ }
559
+ extractTextFromDOMNode(node) {
560
+ if (node.nodeType === 3) {
561
+ return (node.nodeValue || '').trim();
562
+ }
563
+ let text = '';
564
+ if (node.children) {
565
+ for (const child of node.children) {
566
+ text += this.extractTextFromDOMNode(child) + ' ';
567
+ }
568
+ }
569
+ return text.trim().replace(/\s+/g, ' ');
570
+ }
348
571
  convertToHTML(response) {
349
572
  const doc = response.documents[0];
350
573
  if (!doc)
@@ -482,7 +705,7 @@ class TakeSnapshotHandler {
482
705
  if (snapshotArgs.filename !== undefined && typeof snapshotArgs.filename !== 'string') {
483
706
  return false;
484
707
  }
485
- if (snapshotArgs.format !== undefined && !['json', 'html'].includes(snapshotArgs.format)) {
708
+ if (snapshotArgs.format !== undefined && !['json', 'html', 'text'].includes(snapshotArgs.format)) {
486
709
  return false;
487
710
  }
488
711
  if (snapshotArgs.includeStyles !== undefined && typeof snapshotArgs.includeStyles !== 'boolean') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-cdp-cli",
3
- "version": "1.7.9",
3
+ "version": "1.8.0",
4
4
  "description": "Browser automation CLI via Chrome DevTools Protocol. Designed for developers and AI assistants - combines dedicated commands for common tasks with flexible JavaScript execution for complex scenarios. Features: element interaction, screenshots, DOM snapshots, console/network monitoring. Built-in IDE integration for Cursor and Claude.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",