chrome-cdp-cli 2.0.3 → 2.0.5

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.
@@ -161,7 +161,8 @@ class ArgumentParser {
161
161
  { name: 'quiet', short: 'q', type: 'boolean', description: 'Enable quiet mode', default: false },
162
162
  { name: 'timeout', short: 't', type: 'number', description: 'Command timeout in milliseconds', default: 30000 },
163
163
  { name: 'debug', short: 'd', type: 'boolean', description: 'Enable debug logging', default: false },
164
- { name: 'config', short: 'c', type: 'string', description: 'Configuration file path' }
164
+ { name: 'config', short: 'c', type: 'string', description: 'Configuration file path' },
165
+ { name: 'target-index', type: 'number', description: 'Target page index (1-based, excludes DevTools windows)' }
165
166
  ];
166
167
  while (i < args.length) {
167
168
  const arg = args[i];
@@ -348,13 +349,14 @@ class ArgumentParser {
348
349
  }
349
350
  const stringValue = String(value);
350
351
  switch (optionDef.type) {
351
- case 'number':
352
+ case 'number': {
352
353
  const numValue = Number(stringValue);
353
354
  if (isNaN(numValue)) {
354
355
  throw new Error(`Option --${optionDef.name} must be a number, got: ${stringValue}`);
355
356
  }
356
357
  return numValue;
357
- case 'boolean':
358
+ }
359
+ case 'boolean': {
358
360
  if (typeof value === 'boolean') {
359
361
  return value;
360
362
  }
@@ -366,6 +368,7 @@ class ArgumentParser {
366
368
  return false;
367
369
  }
368
370
  throw new Error(`Option --${optionDef.name} must be a boolean, got: ${stringValue}`);
371
+ }
369
372
  case 'array':
370
373
  if (Array.isArray(value)) {
371
374
  return value;
@@ -94,7 +94,12 @@ class CLIApplication {
94
94
  await this.ensureProxyReady();
95
95
  if (this.needsConnection(command.name)) {
96
96
  this.logger.debug('Command needs connection, ensuring connection...');
97
- await this.ensureConnection(command);
97
+ try {
98
+ await this.ensureConnection(command);
99
+ }
100
+ catch (connectionError) {
101
+ this.logger.debug('Connection failed, will be handled by command router:', connectionError);
102
+ }
98
103
  }
99
104
  this.logger.debug('Executing command via CLI interface...');
100
105
  const result = await this.cli.execute(command);
@@ -128,6 +133,26 @@ class CLIApplication {
128
133
  ];
129
134
  return !noConnectionCommands.includes(commandName);
130
135
  }
136
+ isDevToolsWindow(target) {
137
+ const url = target.url.toLowerCase();
138
+ const title = target.title.toLowerCase();
139
+ return url.startsWith('chrome-devtools://') ||
140
+ url.startsWith('devtools://') ||
141
+ title.includes('devtools') ||
142
+ title.includes('chrome devtools');
143
+ }
144
+ displayAvailableTargets(targets) {
145
+ console.log('\nAvailable Chrome pages (excluding DevTools windows):');
146
+ targets.forEach((target, index) => {
147
+ const displayUrl = target.url.length > 60 ? target.url.substring(0, 57) + '...' : target.url;
148
+ console.log(` [${index + 1}] ${target.title || '(Untitled)'}`);
149
+ console.log(` ${displayUrl}`);
150
+ });
151
+ console.log('\nOptions:');
152
+ console.log(' 1. Use --target-index <number> to select a specific page');
153
+ console.log(' Example: chrome-cdp-cli --target-index 1 eval "document.title"');
154
+ console.log(' 2. Close other pages until only one page remains\n');
155
+ }
131
156
  async ensureConnection(command) {
132
157
  if (this.client) {
133
158
  return;
@@ -138,14 +163,35 @@ class CLIApplication {
138
163
  throw new Error(`No Chrome targets found at ${command.config.host}:${command.config.port}. ` +
139
164
  'Make sure Chrome is running with --remote-debugging-port=9222');
140
165
  }
141
- const pageTarget = targets.find(target => target.type === 'page');
142
- if (!pageTarget) {
143
- throw new Error('No page targets available. Open a tab in Chrome.');
166
+ const pageTargets = targets.filter(target => target.type === 'page');
167
+ const nonDevToolsTargets = pageTargets.filter(target => !this.isDevToolsWindow(target));
168
+ if (nonDevToolsTargets.length === 0) {
169
+ throw new Error('No page targets available (excluding DevTools windows). Open a tab in Chrome.');
144
170
  }
145
- this.client = await this.connectionManager.connectToTarget(pageTarget);
171
+ let selectedTarget;
172
+ if (command.config.targetIndex !== undefined) {
173
+ const index = command.config.targetIndex - 1;
174
+ if (index < 0 || index >= nonDevToolsTargets.length) {
175
+ this.displayAvailableTargets(nonDevToolsTargets);
176
+ throw new Error(`Invalid target index: ${command.config.targetIndex}. ` +
177
+ `Please choose a number between 1 and ${nonDevToolsTargets.length}.`);
178
+ }
179
+ selectedTarget = nonDevToolsTargets[index];
180
+ }
181
+ else {
182
+ if (nonDevToolsTargets.length === 1) {
183
+ selectedTarget = nonDevToolsTargets[0];
184
+ }
185
+ else {
186
+ this.displayAvailableTargets(nonDevToolsTargets);
187
+ throw new Error(`Multiple Chrome pages found (${nonDevToolsTargets.length}). ` +
188
+ 'Please specify --target-index <number> to select a page, or close other pages until only one remains.');
189
+ }
190
+ }
191
+ this.client = await this.connectionManager.connectToTarget(selectedTarget);
146
192
  this.cli.setClient(this.client);
147
193
  if (command.config.verbose) {
148
- this.logger.info(`Connected to Chrome target: ${pageTarget.title} (${pageTarget.url})`);
194
+ this.logger.info(`Connected to Chrome target: ${selectedTarget.title} (${selectedTarget.url})`);
149
195
  }
150
196
  }
151
197
  catch (error) {
@@ -208,6 +254,11 @@ class CLIApplication {
208
254
  else if (arg === '--debug' || arg === '-d') {
209
255
  options.debug = true;
210
256
  }
257
+ else if (arg === '--target-index') {
258
+ if (i + 1 < args.length) {
259
+ options.targetIndex = parseInt(args[i + 1], 10);
260
+ }
261
+ }
211
262
  }
212
263
  return options;
213
264
  }
@@ -281,13 +281,13 @@ class CommandSchemaRegistry {
281
281
  options: [],
282
282
  arguments: [
283
283
  {
284
- name: 'fromSelector',
284
+ name: 'sourceSelector',
285
285
  description: 'CSS selector for element to drag from',
286
286
  type: 'string',
287
287
  required: true
288
288
  },
289
289
  {
290
- name: 'toSelector',
290
+ name: 'targetSelector',
291
291
  description: 'CSS selector for element to drag to',
292
292
  type: 'string',
293
293
  required: true
@@ -72,7 +72,8 @@ class EnhancedCLIInterface {
72
72
  verbose: globalOptions.verbose || CLIInterface_1.DEFAULT_CLI_CONFIG.verbose,
73
73
  quiet: globalOptions.quiet || CLIInterface_1.DEFAULT_CLI_CONFIG.quiet,
74
74
  timeout: globalOptions.timeout || CLIInterface_1.DEFAULT_CLI_CONFIG.timeout,
75
- debug: globalOptions.debug || CLIInterface_1.DEFAULT_CLI_CONFIG.debug
75
+ debug: globalOptions.debug || CLIInterface_1.DEFAULT_CLI_CONFIG.debug,
76
+ targetIndex: globalOptions['target-index'] !== undefined ? globalOptions['target-index'] : undefined
76
77
  };
77
78
  }
78
79
  buildCommandArguments(parseResult) {
@@ -17,7 +17,7 @@ class OutputFormatter {
17
17
  return this.formatErrorOutput(result, options);
18
18
  }
19
19
  if (options.template) {
20
- return this.applyTemplate(result, options.template, options);
20
+ return this.applyTemplate(result, options.template);
21
21
  }
22
22
  switch (options.format) {
23
23
  case 'json':
@@ -167,7 +167,7 @@ class OutputFormatter {
167
167
  if (obj.type && obj.text !== undefined && obj.timestamp) {
168
168
  const timestamp = new Date(obj.timestamp).toISOString();
169
169
  const typeIcon = this.getConsoleTypeIcon(obj.type);
170
- let output = `${timestamp} ${typeIcon} ${obj.text}`;
170
+ const output = `${timestamp} ${typeIcon} ${obj.text}`;
171
171
  return output;
172
172
  }
173
173
  if (obj.requestId && obj.url && obj.method) {
@@ -234,7 +234,7 @@ class OutputFormatter {
234
234
  }
235
235
  return output;
236
236
  }
237
- applyTemplate(result, templateName, _options) {
237
+ applyTemplate(result, templateName) {
238
238
  const template = this.templates.get(templateName);
239
239
  if (!template) {
240
240
  throw new Error(`Unknown template: ${templateName}`);
@@ -33,6 +33,10 @@ class ClickHandler {
33
33
  };
34
34
  }
35
35
  }
36
+ const inputResult = await this.clickViaInput(client, clickArgs.selector);
37
+ if (inputResult.success) {
38
+ return inputResult;
39
+ }
36
40
  const cdpResult = await this.clickViaCDP(client, clickArgs.selector);
37
41
  if (cdpResult.success) {
38
42
  return cdpResult;
@@ -66,6 +70,87 @@ class ClickHandler {
66
70
  }
67
71
  return false;
68
72
  }
73
+ async clickViaInput(client, selector) {
74
+ try {
75
+ const escapedSelector = selector.replace(/'/g, "\\'").replace(/"/g, '\\"');
76
+ const getCoordsExpression = `
77
+ (function() {
78
+ const element = document.querySelector('${escapedSelector}');
79
+ if (!element) {
80
+ return null;
81
+ }
82
+
83
+ // Scroll element into view if needed
84
+ element.scrollIntoView({ behavior: 'instant', block: 'center' });
85
+
86
+ // Get bounding box
87
+ const rect = element.getBoundingClientRect();
88
+ const x = rect.left + rect.width / 2;
89
+ const y = rect.top + rect.height / 2;
90
+
91
+ return {
92
+ x: Math.round(x),
93
+ y: Math.round(y),
94
+ tagName: element.tagName,
95
+ id: element.id,
96
+ className: element.className
97
+ };
98
+ })()
99
+ `;
100
+ const coordsResponse = await client.send('Runtime.evaluate', {
101
+ expression: getCoordsExpression,
102
+ returnByValue: true
103
+ });
104
+ if (coordsResponse.exceptionDetails) {
105
+ return {
106
+ success: false,
107
+ error: `Failed to get element coordinates: ${coordsResponse.exceptionDetails.exception?.description || coordsResponse.exceptionDetails.text}`
108
+ };
109
+ }
110
+ const coords = coordsResponse.result.value;
111
+ if (!coords) {
112
+ return {
113
+ success: false,
114
+ error: `Element with selector "${selector}" not found`
115
+ };
116
+ }
117
+ await client.send('Input.dispatchMouseEvent', {
118
+ type: 'mousePressed',
119
+ x: coords.x,
120
+ y: coords.y,
121
+ button: 'left',
122
+ clickCount: 1
123
+ });
124
+ await new Promise(resolve => setTimeout(resolve, 10));
125
+ await client.send('Input.dispatchMouseEvent', {
126
+ type: 'mouseReleased',
127
+ x: coords.x,
128
+ y: coords.y,
129
+ button: 'left',
130
+ clickCount: 1
131
+ });
132
+ return {
133
+ success: true,
134
+ data: {
135
+ selector: selector,
136
+ element: {
137
+ success: true,
138
+ tagName: coords.tagName,
139
+ id: coords.id,
140
+ className: coords.className
141
+ },
142
+ method: 'Input.dispatchMouseEvent',
143
+ coordinates: { x: coords.x, y: coords.y }
144
+ }
145
+ };
146
+ }
147
+ catch (error) {
148
+ return {
149
+ success: false,
150
+ error: `Input click failed: ${error instanceof Error ? error.message : String(error)}`
151
+ };
152
+ }
153
+ }
69
154
  async clickViaCDP(client, selector) {
70
155
  try {
71
156
  const documentResponse = await client.send('DOM.getDocument');
@@ -231,10 +316,11 @@ Examples:
231
316
  click "#optional-element" --no-wait
232
317
 
233
318
  Note:
234
- - Uses CDP DOM.querySelector and Runtime.callFunctionOn for precise control
235
- - Falls back to JavaScript eval if CDP approach fails
319
+ - Uses CDP Input.dispatchMouseEvent for most reliable click simulation
320
+ - Falls back to Runtime.callFunctionOn and JavaScript eval if Input fails
236
321
  - Automatically scrolls element into view before clicking
237
- - Triggers actual click events that work with event listeners
322
+ - Triggers complete mouse event sequence (mousePressed, mouseReleased)
323
+ - Works with all event listeners including React/Vue handlers
238
324
  `;
239
325
  }
240
326
  }
@@ -33,6 +33,7 @@ class DragHandler {
33
33
  }
34
34
  try {
35
35
  await client.send('Runtime.enable');
36
+ await client.send('DOM.enable');
36
37
  const timeout = dragArgs.timeout || 5000;
37
38
  const waitForElement = dragArgs.waitForElement !== false;
38
39
  if (waitForElement) {
@@ -51,6 +52,10 @@ class DragHandler {
51
52
  };
52
53
  }
53
54
  }
55
+ const inputResult = await this.dragViaInput(client, dragArgs.sourceSelector, dragArgs.targetSelector);
56
+ if (inputResult.success) {
57
+ return inputResult;
58
+ }
54
59
  const result = await this.dragViaEval(client, dragArgs.sourceSelector, dragArgs.targetSelector);
55
60
  return result;
56
61
  }
@@ -80,6 +85,133 @@ class DragHandler {
80
85
  }
81
86
  return false;
82
87
  }
88
+ async dragViaInput(client, sourceSelector, targetSelector) {
89
+ try {
90
+ const escapedSource = sourceSelector.replace(/'/g, "\\'").replace(/"/g, '\\"');
91
+ const escapedTarget = targetSelector.replace(/'/g, "\\'").replace(/"/g, '\\"');
92
+ const getCoordsExpression = `
93
+ (function() {
94
+ const sourceElement = document.querySelector('${escapedSource}');
95
+ if (!sourceElement) {
96
+ return null;
97
+ }
98
+
99
+ const targetElement = document.querySelector('${escapedTarget}');
100
+ if (!targetElement) {
101
+ return null;
102
+ }
103
+
104
+ // Scroll elements into view if needed
105
+ sourceElement.scrollIntoView({ behavior: 'instant', block: 'center' });
106
+ targetElement.scrollIntoView({ behavior: 'instant', block: 'center' });
107
+
108
+ // Get bounding boxes
109
+ const sourceRect = sourceElement.getBoundingClientRect();
110
+ const targetRect = targetElement.getBoundingClientRect();
111
+
112
+ // Calculate center positions
113
+ const sourceX = Math.round(sourceRect.left + sourceRect.width / 2);
114
+ const sourceY = Math.round(sourceRect.top + sourceRect.height / 2);
115
+ const targetX = Math.round(targetRect.left + targetRect.width / 2);
116
+ const targetY = Math.round(targetRect.top + targetRect.height / 2);
117
+
118
+ return {
119
+ source: {
120
+ x: sourceX,
121
+ y: sourceY,
122
+ tagName: sourceElement.tagName,
123
+ id: sourceElement.id,
124
+ className: sourceElement.className
125
+ },
126
+ target: {
127
+ x: targetX,
128
+ y: targetY,
129
+ tagName: targetElement.tagName,
130
+ id: targetElement.id,
131
+ className: targetElement.className
132
+ }
133
+ };
134
+ })()
135
+ `;
136
+ const coordsResponse = await client.send('Runtime.evaluate', {
137
+ expression: getCoordsExpression,
138
+ returnByValue: true
139
+ });
140
+ if (coordsResponse.exceptionDetails) {
141
+ return {
142
+ success: false,
143
+ error: `Failed to get element coordinates: ${coordsResponse.exceptionDetails.exception?.description || coordsResponse.exceptionDetails.text}`
144
+ };
145
+ }
146
+ const coords = coordsResponse.result.value;
147
+ if (!coords) {
148
+ return {
149
+ success: false,
150
+ error: `Source or target element not found`
151
+ };
152
+ }
153
+ const steps = 10;
154
+ const dx = (coords.target.x - coords.source.x) / steps;
155
+ const dy = (coords.target.y - coords.source.y) / steps;
156
+ await client.send('Input.dispatchMouseEvent', {
157
+ type: 'mousePressed',
158
+ x: coords.source.x,
159
+ y: coords.source.y,
160
+ button: 'left',
161
+ clickCount: 1
162
+ });
163
+ await new Promise(resolve => setTimeout(resolve, 50));
164
+ for (let i = 1; i <= steps; i++) {
165
+ const currentX = Math.round(coords.source.x + dx * i);
166
+ const currentY = Math.round(coords.source.y + dy * i);
167
+ await client.send('Input.dispatchMouseEvent', {
168
+ type: 'mouseMoved',
169
+ x: currentX,
170
+ y: currentY,
171
+ button: 'left',
172
+ buttons: 1
173
+ });
174
+ await new Promise(resolve => setTimeout(resolve, 10));
175
+ }
176
+ await new Promise(resolve => setTimeout(resolve, 50));
177
+ await client.send('Input.dispatchMouseEvent', {
178
+ type: 'mouseReleased',
179
+ x: coords.target.x,
180
+ y: coords.target.y,
181
+ button: 'left',
182
+ clickCount: 1
183
+ });
184
+ return {
185
+ success: true,
186
+ data: {
187
+ sourceSelector: sourceSelector,
188
+ targetSelector: targetSelector,
189
+ result: {
190
+ success: true,
191
+ source: {
192
+ tagName: coords.source.tagName,
193
+ id: coords.source.id,
194
+ className: coords.source.className,
195
+ position: { x: coords.source.x, y: coords.source.y }
196
+ },
197
+ target: {
198
+ tagName: coords.target.tagName,
199
+ id: coords.target.id,
200
+ className: coords.target.className,
201
+ position: { x: coords.target.x, y: coords.target.y }
202
+ }
203
+ },
204
+ method: 'Input.dispatchMouseEvent'
205
+ }
206
+ };
207
+ }
208
+ catch (error) {
209
+ return {
210
+ success: false,
211
+ error: `Input drag failed: ${error instanceof Error ? error.message : String(error)}`
212
+ };
213
+ }
214
+ }
83
215
  async dragViaEval(client, sourceSelector, targetSelector) {
84
216
  try {
85
217
  const escapedSource = sourceSelector.replace(/'/g, "\\'").replace(/"/g, '\\"');
@@ -257,10 +389,11 @@ Examples:
257
389
  drag "#source" "#target" --no-wait
258
390
 
259
391
  Note:
260
- - Uses JavaScript DragEvent simulation for drag and drop
261
- - Dispatches all standard drag events: dragstart, dragenter, dragover, drop, dragend
262
- - Automatically calculates element center positions for drag coordinates
263
- - Works with HTML5 drag and drop API event listeners
392
+ - Uses CDP Input.dispatchMouseEvent for most reliable drag simulation
393
+ - Falls back to JavaScript DragEvent simulation if Input fails
394
+ - Simulates complete mouse sequence: mousePressed mouseMoved → mouseReleased
395
+ - Automatically calculates element center positions and smooth movement path
396
+ - Works with all drag and drop implementations including React/Vue libraries
264
397
  `;
265
398
  }
266
399
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-cdp-cli",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
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",