chrome-cdp-cli 1.6.0 → 1.7.1

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.
@@ -37,19 +37,26 @@ class CLIApplication {
37
37
  }
38
38
  async run(argv) {
39
39
  try {
40
+ console.log('[DEBUG] CLIApplication.run called with argv:', argv);
40
41
  const command = this.cli.parseArgs(argv);
42
+ console.log('[DEBUG] Parsed command:', command);
41
43
  if (command.config.verbose) {
42
44
  this.proxyManager.setLogging(true);
43
45
  }
46
+ console.log('[DEBUG] Ensuring proxy is ready...');
44
47
  await this.ensureProxyReady();
45
48
  if (this.needsConnection(command.name)) {
49
+ console.log('[DEBUG] Command needs connection, ensuring connection...');
46
50
  await this.ensureConnection(command);
47
51
  }
52
+ console.log('[DEBUG] Executing command via CLI interface...');
48
53
  const result = await this.cli.execute(command);
54
+ console.log('[DEBUG] Command execution result:', result);
49
55
  this.outputResult(result, command);
50
56
  return result.exitCode || (result.success ? CommandRouter_1.ExitCode.SUCCESS : CommandRouter_1.ExitCode.GENERAL_ERROR);
51
57
  }
52
58
  catch (error) {
59
+ console.log('[DEBUG] Error in CLIApplication.run:', error);
53
60
  const errorMessage = error instanceof Error ? error.message : String(error);
54
61
  console.error(`Error: ${errorMessage}`);
55
62
  return CommandRouter_1.ExitCode.GENERAL_ERROR;
@@ -169,14 +169,18 @@ class CommandRouter {
169
169
  }
170
170
  async executeWithTimeout(handler, command) {
171
171
  const timeout = command.config.timeout;
172
+ console.log(`[DEBUG] CommandRouter.executeWithTimeout called for command: ${command.name}, timeout: ${timeout}ms`);
172
173
  const timeoutPromise = new Promise((_, reject) => {
173
174
  setTimeout(() => {
175
+ console.log(`[DEBUG] Command timeout reached for: ${command.name} after ${timeout}ms`);
174
176
  reject(new Error(`Command timeout after ${timeout}ms`));
175
177
  }, timeout);
176
178
  });
179
+ console.log(`[DEBUG] Starting handler execution for: ${command.name}`);
177
180
  const executionPromise = handler.execute(this.client, command.args);
178
181
  try {
179
182
  const result = await Promise.race([executionPromise, timeoutPromise]);
183
+ console.log(`[DEBUG] Command completed successfully for: ${command.name}`);
180
184
  if (!result || typeof result !== 'object') {
181
185
  return {
182
186
  success: false,
@@ -190,6 +194,7 @@ class CommandRouter {
190
194
  return result;
191
195
  }
192
196
  catch (error) {
197
+ console.log(`[DEBUG] Command execution error for: ${command.name}:`, error);
193
198
  if (error instanceof Error && error.message.includes('timeout')) {
194
199
  return {
195
200
  success: false,
@@ -172,25 +172,32 @@ class ProxyClient {
172
172
  if (!this.connectionId) {
173
173
  throw new Error('No active connection. Call connect() first.');
174
174
  }
175
+ console.log(`[DEBUG] Creating WebSocket proxy for connection: ${this.connectionId}`);
175
176
  try {
176
177
  const wsUrl = this.config.proxyUrl.replace('http://', 'ws://').replace('https://', 'wss://');
177
- const ws = new ws_1.WebSocket(`${wsUrl}/ws/${this.connectionId}`);
178
+ const fullWsUrl = `${wsUrl}/ws/${this.connectionId}`;
179
+ console.log(`[DEBUG] WebSocket URL: ${fullWsUrl}`);
180
+ const ws = new ws_1.WebSocket(fullWsUrl);
178
181
  return new Promise((resolve, reject) => {
179
182
  const timeout = setTimeout(() => {
183
+ console.log(`[DEBUG] WebSocket connection timeout for ${this.connectionId}`);
180
184
  reject(new Error('WebSocket connection timeout'));
181
185
  }, 10000);
182
186
  ws.on('open', () => {
187
+ console.log(`[DEBUG] WebSocket connection opened for ${this.connectionId}`);
183
188
  clearTimeout(timeout);
184
189
  this.wsConnection = ws;
185
190
  resolve(ws);
186
191
  });
187
192
  ws.on('error', (error) => {
193
+ console.log(`[DEBUG] WebSocket connection error for ${this.connectionId}:`, error);
188
194
  clearTimeout(timeout);
189
195
  reject(error);
190
196
  });
191
197
  });
192
198
  }
193
199
  catch (error) {
200
+ console.log(`[DEBUG] Failed to create WebSocket proxy for ${this.connectionId}:`, error);
194
201
  throw new Error(`Failed to create WebSocket proxy: ${error instanceof Error ? error.message : error}`);
195
202
  }
196
203
  }
@@ -222,6 +229,23 @@ class ProxyClient {
222
229
  }
223
230
  async disconnect() {
224
231
  try {
232
+ if (this.connectionId) {
233
+ try {
234
+ const controller = new AbortController();
235
+ const timeout = setTimeout(() => controller.abort(), 5000);
236
+ await (0, node_fetch_1.default)(`${this.config.proxyUrl}/api/client/release`, {
237
+ method: 'POST',
238
+ headers: {
239
+ 'Content-Type': 'application/json',
240
+ 'x-client-id': `proxy_client_${Date.now()}`
241
+ },
242
+ signal: controller.signal
243
+ });
244
+ clearTimeout(timeout);
245
+ }
246
+ catch (error) {
247
+ }
248
+ }
225
249
  if (this.wsConnection) {
226
250
  this.wsConnection.close();
227
251
  this.wsConnection = undefined;
@@ -8,12 +8,13 @@ const ProxyClient_1 = require("../client/ProxyClient");
8
8
  const fs_1 = require("fs");
9
9
  const node_fetch_1 = __importDefault(require("node-fetch"));
10
10
  class EvaluateScriptHandler {
11
- constructor(useProxy = true) {
11
+ constructor(useProxy = false) {
12
12
  this.name = 'eval';
13
13
  this.proxyClient = new ProxyClient_1.ProxyClient();
14
14
  this.useProxy = useProxy;
15
15
  }
16
16
  async execute(client, args) {
17
+ console.log('[DEBUG] EvaluateScriptHandler.execute called with args:', args);
17
18
  const scriptArgs = args;
18
19
  if (!scriptArgs.expression && !scriptArgs.file) {
19
20
  return {
@@ -27,12 +28,18 @@ class EvaluateScriptHandler {
27
28
  error: 'Cannot specify both "expression" and "file" arguments'
28
29
  };
29
30
  }
31
+ console.log('[DEBUG] Arguments validated, useProxy:', this.useProxy);
30
32
  try {
31
33
  if (this.useProxy) {
34
+ console.log('[DEBUG] Checking proxy availability...');
32
35
  const proxyAvailable = await this.proxyClient.isProxyAvailable();
36
+ console.log('[DEBUG] Proxy available:', proxyAvailable);
33
37
  if (proxyAvailable) {
34
38
  console.log('[INFO] Using proxy connection for script evaluation');
35
- return await this.executeWithProxy(scriptArgs);
39
+ console.log('[DEBUG] About to call executeWithProxy...');
40
+ const result = await this.executeWithProxy(scriptArgs);
41
+ console.log('[DEBUG] executeWithProxy returned:', result);
42
+ return result;
36
43
  }
37
44
  else {
38
45
  console.warn('[WARN] Proxy not available, falling back to direct CDP connection');
@@ -42,10 +49,12 @@ class EvaluateScriptHandler {
42
49
  catch (error) {
43
50
  console.warn('[WARN] Proxy execution failed, falling back to direct CDP:', error instanceof Error ? error.message : error);
44
51
  }
52
+ console.log('[DEBUG] Falling back to direct CDP');
45
53
  return await this.executeWithDirectCDP(client, scriptArgs);
46
54
  }
47
55
  async executeWithProxy(scriptArgs) {
48
56
  try {
57
+ console.log('[DEBUG] Starting executeWithProxy');
49
58
  let expression;
50
59
  if (scriptArgs.file) {
51
60
  expression = await this.readScriptFile(scriptArgs.file);
@@ -53,83 +62,38 @@ class EvaluateScriptHandler {
53
62
  else {
54
63
  expression = scriptArgs.expression;
55
64
  }
56
- const response = await (0, node_fetch_1.default)('http://localhost:9223/api/connections');
57
- if (!response.ok) {
58
- throw new Error('Failed to get proxy connections');
59
- }
60
- const result = await response.json();
61
- if (!result.success || !result.data.connections || result.data.connections.length === 0) {
62
- throw new Error('No active proxy connections found');
63
- }
64
- const connection = result.data.connections.find((conn) => conn.isHealthy);
65
- if (!connection) {
66
- throw new Error('No healthy proxy connections found');
67
- }
68
- console.log(`[INFO] Using existing proxy connection: ${connection.id}`);
69
- this.proxyClient.connectionId = connection.id;
70
- const ws = await this.proxyClient.createWebSocketProxy();
65
+ console.log('[DEBUG] Expression to execute:', expression.substring(0, 100));
66
+ console.log('[DEBUG] Creating new proxy connection...');
67
+ const connectionId = await this.proxyClient.connect('localhost', 9222);
68
+ console.log(`[DEBUG] Created new proxy connection: ${connectionId}`);
71
69
  try {
72
- const result = await this.executeScriptThroughProxy(ws, expression, scriptArgs);
70
+ const result = await this.executeScriptThroughHTTP(connectionId, expression, scriptArgs);
73
71
  return {
74
72
  success: true,
75
73
  data: result
76
74
  };
77
75
  }
78
76
  finally {
79
- ws.close();
77
+ await this.proxyClient.disconnect();
80
78
  }
81
79
  }
82
80
  catch (error) {
81
+ console.log('[DEBUG] Error in executeWithProxy:', error);
83
82
  return {
84
83
  success: false,
85
84
  error: error instanceof Error ? error.message : String(error)
86
85
  };
87
86
  }
88
87
  }
89
- async executeScriptThroughProxy(ws, expression, args) {
90
- return new Promise((resolve, reject) => {
91
- const commandId = Date.now();
92
- const timeout = args.timeout || 30000;
93
- const awaitPromise = args.awaitPromise ?? true;
94
- const returnByValue = args.returnByValue ?? true;
95
- const timeoutHandle = setTimeout(() => {
96
- reject(new Error(`Script execution timeout after ${timeout}ms`));
97
- }, timeout);
98
- ws.on('message', (data) => {
99
- try {
100
- const response = JSON.parse(data.toString());
101
- if (response.id === commandId) {
102
- clearTimeout(timeoutHandle);
103
- if (response.error) {
104
- reject(new Error(`CDP Error: ${response.error.message}`));
105
- return;
106
- }
107
- const result = response.result;
108
- if (result.exceptionDetails) {
109
- const error = new Error(result.result?.description || 'Script execution failed');
110
- error.exceptionDetails = result.exceptionDetails;
111
- reject(error);
112
- return;
113
- }
114
- let value = result.result?.value;
115
- if (result.result?.type === 'undefined') {
116
- value = undefined;
117
- }
118
- else if (result.result?.unserializableValue) {
119
- value = result.result.unserializableValue;
120
- }
121
- resolve(value);
122
- }
123
- }
124
- catch (error) {
125
- clearTimeout(timeoutHandle);
126
- reject(new Error(`Failed to parse CDP response: ${error instanceof Error ? error.message : error}`));
127
- }
128
- });
129
- ws.on('error', (error) => {
130
- clearTimeout(timeoutHandle);
131
- reject(new Error(`WebSocket error: ${error.message}`));
132
- });
88
+ async executeScriptThroughHTTP(connectionId, expression, args) {
89
+ const timeout = args.timeout || 30000;
90
+ const awaitPromise = args.awaitPromise ?? true;
91
+ const returnByValue = args.returnByValue ?? true;
92
+ console.log(`[DEBUG] Starting HTTP script execution, timeout: ${timeout}ms`);
93
+ console.log(`[DEBUG] Expression: ${expression.substring(0, 100)}${expression.length > 100 ? '...' : ''}`);
94
+ try {
95
+ const proxyUrl = this.proxyClient.getConfig().proxyUrl;
96
+ const commandId = Date.now() + Math.floor(Math.random() * 10000);
133
97
  const command = {
134
98
  id: commandId,
135
99
  method: 'Runtime.evaluate',
@@ -141,8 +105,61 @@ class EvaluateScriptHandler {
141
105
  generatePreview: false
142
106
  }
143
107
  };
144
- ws.send(JSON.stringify(command));
145
- });
108
+ console.log(`[DEBUG] Sending HTTP command to ${proxyUrl}/api/execute/${connectionId}`);
109
+ const controller = new AbortController();
110
+ const timeoutHandle = setTimeout(() => controller.abort(), timeout);
111
+ try {
112
+ const response = await (0, node_fetch_1.default)(`${proxyUrl}/api/execute/${connectionId}`, {
113
+ method: 'POST',
114
+ headers: {
115
+ 'Content-Type': 'application/json',
116
+ 'x-client-id': `eval_handler_${Date.now()}`
117
+ },
118
+ body: JSON.stringify({
119
+ command,
120
+ timeout
121
+ }),
122
+ signal: controller.signal
123
+ });
124
+ clearTimeout(timeoutHandle);
125
+ if (!response.ok) {
126
+ const errorData = await response.json().catch(() => ({}));
127
+ throw new Error(`HTTP ${response.status}: ${errorData.error || response.statusText}`);
128
+ }
129
+ const result = await response.json();
130
+ console.log(`[DEBUG] HTTP command response:`, result);
131
+ if (!result.success) {
132
+ throw new Error(`Command execution failed: ${result.error || 'Unknown error'}`);
133
+ }
134
+ const commandResult = result.data.result;
135
+ if (result.data.error) {
136
+ throw new Error(`CDP Error: ${result.data.error.message}`);
137
+ }
138
+ if (commandResult.exceptionDetails) {
139
+ console.log(`[DEBUG] Exception details:`, commandResult.exceptionDetails);
140
+ const error = new Error(commandResult.result?.description || 'Script execution failed');
141
+ error.exceptionDetails = commandResult.exceptionDetails;
142
+ throw error;
143
+ }
144
+ let value = commandResult.result?.value;
145
+ if (commandResult.result?.type === 'undefined') {
146
+ value = undefined;
147
+ }
148
+ else if (commandResult.result?.unserializableValue) {
149
+ value = commandResult.result.unserializableValue;
150
+ }
151
+ console.log(`[DEBUG] Successful HTTP result:`, value);
152
+ return value;
153
+ }
154
+ catch (error) {
155
+ clearTimeout(timeoutHandle);
156
+ throw error;
157
+ }
158
+ }
159
+ catch (error) {
160
+ console.log(`[DEBUG] Error in HTTP script execution:`, error);
161
+ throw error;
162
+ }
146
163
  }
147
164
  async executeWithDirectCDP(client, scriptArgs) {
148
165
  try {
@@ -126,148 +126,148 @@ Examples:
126
126
  return [
127
127
  {
128
128
  name: 'cdp-cli',
129
- description: 'Chrome DevTools Protocol CLI 工具',
130
- instructions: `通过 Chrome DevTools Protocol 控制 Chrome 浏览器,支持完整的自动化操作。
129
+ description: 'Chrome DevTools Protocol CLI Tool',
130
+ instructions: `Control Chrome browser through Chrome DevTools Protocol, supporting complete automation operations.
131
131
 
132
- ## 完整命令列表
132
+ ## Complete Command List
133
133
 
134
- ### 1. JavaScript 执行
135
- - **eval** - 执行 JavaScript 代码并返回结果,支持异步代码和 Promise
134
+ ### 1. JavaScript Execution
135
+ - **eval** - Execute JavaScript code and return results, supports async code and Promises
136
136
  \`chrome-cdp-cli eval "document.title"\`
137
137
  \`chrome-cdp-cli eval "fetch('/api/data').then(r => r.json())"\`
138
138
 
139
- ### 2. 页面截图和快照
140
- - **screenshot** - 捕获页面截图并保存到文件
139
+ ### 2. Page Screenshots and Snapshots
140
+ - **screenshot** - Capture page screenshot and save to file
141
141
  \`chrome-cdp-cli screenshot --filename page.png\`
142
142
  \`chrome-cdp-cli screenshot --filename fullpage.png --full-page\`
143
143
 
144
- - **snapshot** - 捕获完整 DOM 快照(包含结构、样式、布局)
144
+ - **snapshot** - Capture complete DOM snapshot (including structure, styles, layout)
145
145
  \`chrome-cdp-cli snapshot --filename dom-snapshot.json\`
146
146
 
147
- ### 3. 元素交互
148
- - **click** - 点击页面元素
147
+ ### 3. Element Interaction
148
+ - **click** - Click page elements
149
149
  \`chrome-cdp-cli click "#submit-button"\`
150
150
  \`chrome-cdp-cli click ".menu-item" --timeout 10000\`
151
151
 
152
- - **hover** - 鼠标悬停在元素上
152
+ - **hover** - Mouse hover over elements
153
153
  \`chrome-cdp-cli hover "#dropdown-trigger"\`
154
154
 
155
- - **fill** - 填充表单字段
155
+ - **fill** - Fill form fields
156
156
  \`chrome-cdp-cli fill "#username" "john@example.com"\`
157
157
  \`chrome-cdp-cli fill "input[name='password']" "secret123"\`
158
158
 
159
- - **fill_form** - 批量填充表单
159
+ - **fill_form** - Batch fill forms
160
160
  \`chrome-cdp-cli fill_form '{"#username": "john", "#password": "secret"}'\`
161
161
 
162
- ### 4. 高级交互
163
- - **drag** - 拖拽操作
162
+ ### 4. Advanced Interactions
163
+ - **drag** - Drag and drop operations
164
164
  \`chrome-cdp-cli drag "#draggable" "#dropzone"\`
165
165
 
166
- - **press_key** - 模拟键盘输入
166
+ - **press_key** - Simulate keyboard input
167
167
  \`chrome-cdp-cli press_key "Enter"\`
168
168
  \`chrome-cdp-cli press_key "a" --modifiers Ctrl --selector "#input"\`
169
169
 
170
- - **upload_file** - 文件上传
170
+ - **upload_file** - File upload
171
171
  \`chrome-cdp-cli upload_file "input[type='file']" "./document.pdf"\`
172
172
 
173
- - **wait_for** - 等待元素出现或满足条件
173
+ - **wait_for** - Wait for elements to appear or meet conditions
174
174
  \`chrome-cdp-cli wait_for "#loading" --condition hidden\`
175
175
  \`chrome-cdp-cli wait_for "#submit-btn" --condition enabled\`
176
176
 
177
- - **handle_dialog** - 处理浏览器对话框
177
+ - **handle_dialog** - Handle browser dialogs
178
178
  \`chrome-cdp-cli handle_dialog accept\`
179
179
  \`chrome-cdp-cli handle_dialog accept --text "user input"\`
180
180
 
181
- ### 5. 监控功能
182
- - **get_console_message** - 获取最新控制台消息
181
+ ### 5. Monitoring Features
182
+ - **get_console_message** - Get latest console message
183
183
  \`chrome-cdp-cli get_console_message\`
184
184
 
185
- - **list_console_messages** - 列出所有控制台消息
185
+ - **list_console_messages** - List all console messages
186
186
  \`chrome-cdp-cli list_console_messages --type error\`
187
187
 
188
- - **get_network_request** - 获取最新网络请求
188
+ - **get_network_request** - Get latest network request
189
189
  \`chrome-cdp-cli get_network_request\`
190
190
 
191
- - **list_network_requests** - 列出所有网络请求
191
+ - **list_network_requests** - List all network requests
192
192
  \`chrome-cdp-cli list_network_requests --method POST\`
193
193
 
194
- ### 6. IDE 集成
195
- - **install_cursor_command** - 安装 Cursor 命令
194
+ ### 6. IDE Integration
195
+ - **install_cursor_command** - Install Cursor commands
196
196
  \`chrome-cdp-cli install_cursor_command\`
197
197
 
198
- - **install_claude_skill** - 安装 Claude 技能
198
+ - **install_claude_skill** - Install Claude skills
199
199
  \`chrome-cdp-cli install_claude_skill --skill-type personal\`
200
200
 
201
- ## 常用工作流程
201
+ ## Common Workflows
202
202
 
203
- ### 完整的表单测试流程
203
+ ### Complete Form Testing Workflow
204
204
  \`\`\`bash
205
- # 1. 等待页面加载
205
+ # 1. Wait for page to load
206
206
  chrome-cdp-cli wait_for "#login-form" --condition visible
207
207
 
208
- # 2. 填写表单
208
+ # 2. Fill form
209
209
  chrome-cdp-cli fill "#email" "test@example.com"
210
210
  chrome-cdp-cli fill "#password" "password123"
211
211
 
212
- # 3. 提交表单
212
+ # 3. Submit form
213
213
  chrome-cdp-cli click "#submit-button"
214
214
 
215
- # 4. 等待结果并截图
215
+ # 4. Wait for result and take screenshot
216
216
  chrome-cdp-cli wait_for "#success-message" --condition visible
217
217
  chrome-cdp-cli screenshot --filename login-success.png
218
218
 
219
- # 5. 检查控制台错误
219
+ # 5. Check console errors
220
220
  chrome-cdp-cli list_console_messages --type error
221
221
  \`\`\`
222
222
 
223
- ### 文件上传测试
223
+ ### File Upload Testing
224
224
  \`\`\`bash
225
- # 1. 点击上传按钮
225
+ # 1. Click upload button
226
226
  chrome-cdp-cli click "#upload-trigger"
227
227
 
228
- # 2. 上传文件
228
+ # 2. Upload file
229
229
  chrome-cdp-cli upload_file "input[type='file']" "./test-document.pdf"
230
230
 
231
- # 3. 等待上传完成
231
+ # 3. Wait for upload completion
232
232
  chrome-cdp-cli wait_for ".upload-success" --condition visible
233
233
 
234
- # 4. 验证结果
234
+ # 4. Verify result
235
235
  chrome-cdp-cli eval "document.querySelector('.file-name').textContent"
236
236
  \`\`\`
237
237
 
238
- ### 拖拽交互测试
238
+ ### Drag and Drop Interaction Testing
239
239
  \`\`\`bash
240
- # 1. 等待元素可用
240
+ # 1. Wait for elements to be available
241
241
  chrome-cdp-cli wait_for "#draggable-item" --condition visible
242
242
  chrome-cdp-cli wait_for "#drop-zone" --condition visible
243
243
 
244
- # 2. 执行拖拽
244
+ # 2. Perform drag and drop
245
245
  chrome-cdp-cli drag "#draggable-item" "#drop-zone"
246
246
 
247
- # 3. 验证拖拽结果
247
+ # 3. Verify drag result
248
248
  chrome-cdp-cli eval "document.querySelector('#drop-zone').children.length"
249
249
  \`\`\`
250
250
 
251
- ### 键盘导航测试
251
+ ### Keyboard Navigation Testing
252
252
  \`\`\`bash
253
- # 1. 聚焦到输入框
253
+ # 1. Focus on input field
254
254
  chrome-cdp-cli click "#search-input"
255
255
 
256
- # 2. 输入文本
256
+ # 2. Type text
257
257
  chrome-cdp-cli press_key "t"
258
258
  chrome-cdp-cli press_key "e"
259
259
  chrome-cdp-cli press_key "s"
260
260
  chrome-cdp-cli press_key "t"
261
261
 
262
- # 3. 使用快捷键
263
- chrome-cdp-cli press_key "a" --modifiers Ctrl # 全选
264
- chrome-cdp-cli press_key "Enter" # 提交
262
+ # 3. Use keyboard shortcuts
263
+ chrome-cdp-cli press_key "a" --modifiers Ctrl # Select all
264
+ chrome-cdp-cli press_key "Enter" # Submit
265
265
 
266
- # 4. 处理可能的确认对话框
266
+ # 4. Handle possible confirmation dialog
267
267
  chrome-cdp-cli handle_dialog accept
268
268
  \`\`\`
269
269
 
270
- 命令会自动连接到运行在 localhost:9222 Chrome 实例。`,
270
+ Commands automatically connect to Chrome instance running on localhost:9222.`,
271
271
  examples: [
272
272
  'chrome-cdp-cli eval "document.title"',
273
273
  'chrome-cdp-cli screenshot --filename page.png',
@@ -290,74 +290,74 @@ chrome-cdp-cli handle_dialog accept
290
290
 
291
291
  ${command.instructions}
292
292
 
293
- ## 使用示例
293
+ ## Usage Examples
294
294
 
295
295
  ${examples}
296
296
 
297
- ## 前置条件
297
+ ## Prerequisites
298
298
 
299
- 确保 Chrome 浏览器已启动并开启了远程调试:
299
+ Ensure Chrome browser is started with remote debugging enabled:
300
300
 
301
301
  \`\`\`bash
302
302
  chrome --remote-debugging-port=9222
303
303
  \`\`\`
304
304
 
305
- 或者在 macOS 上:
305
+ Or on macOS:
306
306
 
307
307
  \`\`\`bash
308
308
  /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222
309
309
  \`\`\`
310
310
 
311
- ## 全局选项
311
+ ## Global Options
312
312
 
313
- 所有命令都支持以下全局选项:
313
+ All commands support the following global options:
314
314
 
315
- - \`--host <hostname>\`: Chrome DevTools 主机地址 (默认: localhost)
316
- - \`--port <number>\`: Chrome DevTools 端口 (默认: 9222)
317
- - \`--format <json|text>\`: 输出格式 (默认: json)
318
- - \`--verbose\`: 启用详细日志
319
- - \`--quiet\`: 静默模式
320
- - \`--timeout <ms>\`: 命令超时时间
315
+ - \`--host <hostname>\`: Chrome DevTools host address (default: localhost)
316
+ - \`--port <number>\`: Chrome DevTools port (default: 9222)
317
+ - \`--format <json|text>\`: Output format (default: json)
318
+ - \`--verbose\`: Enable verbose logging
319
+ - \`--quiet\`: Silent mode
320
+ - \`--timeout <ms>\`: Command timeout
321
321
 
322
- ## 常用工作流程
322
+ ## Common Workflows
323
323
 
324
- ### 网页自动化测试
324
+ ### Web Automation Testing
325
325
  \`\`\`bash
326
- # 1. 导航到页面并截图
326
+ # 1. Navigate to page and take screenshot
327
327
  chrome-cdp-cli eval "window.location.href = 'https://example.com'"
328
328
  chrome-cdp-cli screenshot --filename before.png
329
329
 
330
- # 2. 填写表单
330
+ # 2. Fill form
331
331
  chrome-cdp-cli eval "document.querySelector('#email').value = 'test@example.com'"
332
332
  chrome-cdp-cli eval "document.querySelector('#password').value = 'password123'"
333
333
 
334
- # 3. 提交并检查结果
334
+ # 3. Submit and check results
335
335
  chrome-cdp-cli eval "document.querySelector('#submit').click()"
336
336
  chrome-cdp-cli screenshot --filename after.png
337
337
  chrome-cdp-cli list_console_messages --type error
338
338
  \`\`\`
339
339
 
340
- ### API 调用监控
340
+ ### API Call Monitoring
341
341
  \`\`\`bash
342
- # 1. 开始监控网络请求
342
+ # 1. Start monitoring network requests
343
343
  chrome-cdp-cli eval "fetch('/api/users').then(r => r.json())"
344
344
 
345
- # 2. 查看网络请求
345
+ # 2. View network requests
346
346
  chrome-cdp-cli list_network_requests --method POST
347
347
 
348
- # 3. 获取最新请求详情
348
+ # 3. Get latest request details
349
349
  chrome-cdp-cli get_network_request
350
350
  \`\`\`
351
351
 
352
- ### 页面分析
352
+ ### Page Analysis
353
353
  \`\`\`bash
354
- # 1. 获取页面基本信息
354
+ # 1. Get basic page information
355
355
  chrome-cdp-cli eval "({title: document.title, url: location.href, links: document.querySelectorAll('a').length})"
356
356
 
357
- # 2. 捕获完整页面结构
357
+ # 2. Capture complete page structure
358
358
  chrome-cdp-cli snapshot --filename page-analysis.json
359
359
 
360
- # 3. 检查控制台错误
360
+ # 3. Check console errors
361
361
  chrome-cdp-cli list_console_messages --type error
362
362
  \`\`\`
363
363
  `;
@@ -157,6 +157,7 @@ class CDPProxyServer {
157
157
  this.performanceMonitor.stop();
158
158
  await this.eventMonitor.stopAllMonitoring();
159
159
  this.wsProxy.stop();
160
+ this.apiServer.cleanup();
160
161
  if (this.wsServer) {
161
162
  this.wsServer.close();
162
163
  }
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CommandExecutionService = void 0;
4
+ const logger_1 = require("../../utils/logger");
5
+ class CommandExecutionService {
6
+ constructor(connectionPool) {
7
+ this.pendingCommands = new Map();
8
+ this.commandMetrics = {
9
+ totalCommands: 0,
10
+ successfulCommands: 0,
11
+ failedCommands: 0,
12
+ averageExecutionTime: 0,
13
+ timeoutCount: 0
14
+ };
15
+ this.messageHandlers = new Map();
16
+ this.activeCLIClient = null;
17
+ this.connectionPool = connectionPool;
18
+ this.logger = (0, logger_1.createLogger)({ component: 'CommandExecutionService' });
19
+ }
20
+ async executeCommand(request, clientId) {
21
+ const startTime = Date.now();
22
+ const commandId = this.generateCommandId();
23
+ const timeout = request.timeout || 30000;
24
+ try {
25
+ if (this.activeCLIClient && clientId && this.activeCLIClient !== clientId) {
26
+ throw new Error('Another CLI client is already connected. Only one CLI client can use the proxy at a time.');
27
+ }
28
+ if (clientId && !this.activeCLIClient) {
29
+ this.activeCLIClient = clientId;
30
+ this.logger.info(`CLI client ${clientId} is now the active client`);
31
+ }
32
+ const connection = this.connectionPool.getConnectionInfo(request.connectionId);
33
+ if (!connection) {
34
+ throw new Error(`Connection ${request.connectionId} not found`);
35
+ }
36
+ if (!connection.isHealthy) {
37
+ throw new Error(`Connection ${request.connectionId} is not healthy`);
38
+ }
39
+ this.logger.debug(`Executing CDP command: ${request.command.method}`, {
40
+ connectionId: request.connectionId,
41
+ commandId,
42
+ method: request.command.method,
43
+ timeout,
44
+ clientId: clientId || 'unknown'
45
+ });
46
+ const cdpMessage = {
47
+ id: typeof request.command.id === 'number' ? request.command.id : this.generateCDPMessageId(),
48
+ method: request.command.method,
49
+ params: request.command.params
50
+ };
51
+ const result = await this.sendCDPCommand(request.connectionId, cdpMessage, timeout);
52
+ const executionTime = Date.now() - startTime;
53
+ this.updateMetrics(true, executionTime);
54
+ this.logger.debug(`CDP command executed successfully: ${request.command.method}`, {
55
+ connectionId: request.connectionId,
56
+ commandId,
57
+ executionTime,
58
+ clientId: clientId || 'unknown'
59
+ });
60
+ return {
61
+ success: true,
62
+ result,
63
+ executionTime
64
+ };
65
+ }
66
+ catch (error) {
67
+ const executionTime = Date.now() - startTime;
68
+ this.updateMetrics(false, executionTime);
69
+ this.logger.error(`CDP command execution failed: ${request.command.method}`, {
70
+ connectionId: request.connectionId,
71
+ commandId,
72
+ executionTime,
73
+ clientId: clientId || 'unknown',
74
+ error: error instanceof Error ? error.message : String(error)
75
+ });
76
+ let errorCode = 500;
77
+ let errorMessage = error instanceof Error ? error.message : 'Unknown error';
78
+ if (errorMessage.includes('Another CLI client')) {
79
+ errorCode = 409;
80
+ }
81
+ else if (errorMessage.includes('timeout')) {
82
+ errorCode = 408;
83
+ this.commandMetrics.timeoutCount++;
84
+ }
85
+ else if (errorMessage.includes('not found')) {
86
+ errorCode = 404;
87
+ }
88
+ else if (errorMessage.includes('not healthy')) {
89
+ errorCode = 503;
90
+ }
91
+ return {
92
+ success: false,
93
+ error: {
94
+ code: errorCode,
95
+ message: errorMessage
96
+ },
97
+ executionTime
98
+ };
99
+ }
100
+ }
101
+ async sendCDPCommand(connectionId, command, timeout) {
102
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
103
+ if (!connection) {
104
+ throw new Error(`Connection ${connectionId} not found`);
105
+ }
106
+ return new Promise((resolve, reject) => {
107
+ const commandKey = `${connectionId}:${command.id}`;
108
+ const pendingCommand = {
109
+ id: commandKey,
110
+ connectionId,
111
+ command,
112
+ timestamp: Date.now(),
113
+ timeout,
114
+ resolve,
115
+ reject
116
+ };
117
+ this.pendingCommands.set(commandKey, pendingCommand);
118
+ this.setupMessageHandler(connectionId);
119
+ const timeoutHandle = setTimeout(() => {
120
+ if (this.pendingCommands.has(commandKey)) {
121
+ this.pendingCommands.delete(commandKey);
122
+ reject(new Error(`Command timeout after ${timeout}ms: ${command.method}`));
123
+ }
124
+ }, timeout);
125
+ pendingCommand.resolve = (value) => {
126
+ clearTimeout(timeoutHandle);
127
+ resolve(value);
128
+ };
129
+ pendingCommand.reject = (error) => {
130
+ clearTimeout(timeoutHandle);
131
+ reject(error);
132
+ };
133
+ try {
134
+ const messageStr = JSON.stringify(command);
135
+ connection.connection.send(messageStr);
136
+ this.logger.debug(`Sent CDP command to Chrome: ${command.method}`, {
137
+ connectionId,
138
+ commandId: command.id,
139
+ messageLength: messageStr.length
140
+ });
141
+ }
142
+ catch (error) {
143
+ clearTimeout(timeoutHandle);
144
+ this.pendingCommands.delete(commandKey);
145
+ reject(new Error(`Failed to send command: ${error instanceof Error ? error.message : String(error)}`));
146
+ }
147
+ });
148
+ }
149
+ setupMessageHandler(connectionId) {
150
+ if (this.messageHandlers.has(connectionId)) {
151
+ return;
152
+ }
153
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
154
+ if (!connection) {
155
+ return;
156
+ }
157
+ const messageHandler = (data) => {
158
+ try {
159
+ const message = data.toString();
160
+ let cdpResponse;
161
+ try {
162
+ cdpResponse = JSON.parse(message);
163
+ }
164
+ catch (parseError) {
165
+ this.logger.warn(`Invalid JSON from CDP connection ${connectionId}: ${message.substring(0, 100)}`);
166
+ return;
167
+ }
168
+ if (typeof cdpResponse.id === 'undefined') {
169
+ return;
170
+ }
171
+ const commandKey = `${connectionId}:${cdpResponse.id}`;
172
+ const pendingCommand = this.pendingCommands.get(commandKey);
173
+ if (pendingCommand) {
174
+ this.pendingCommands.delete(commandKey);
175
+ this.logger.debug(`Received CDP response for command: ${pendingCommand.command.method}`, {
176
+ connectionId,
177
+ commandId: cdpResponse.id,
178
+ hasResult: !!cdpResponse.result,
179
+ hasError: !!cdpResponse.error
180
+ });
181
+ if (cdpResponse.error) {
182
+ pendingCommand.reject(new Error(`CDP Error: ${cdpResponse.error.message} (Code: ${cdpResponse.error.code})`));
183
+ }
184
+ else {
185
+ pendingCommand.resolve(cdpResponse.result);
186
+ }
187
+ }
188
+ else {
189
+ this.logger.debug(`Received CDP response for unknown command ID: ${cdpResponse.id}`, {
190
+ connectionId
191
+ });
192
+ }
193
+ }
194
+ catch (error) {
195
+ this.logger.error(`Error handling CDP message for connection ${connectionId}:`, error);
196
+ }
197
+ };
198
+ connection.connection.on('message', messageHandler);
199
+ this.messageHandlers.set(connectionId, messageHandler);
200
+ this.logger.debug(`Set up message handler for connection: ${connectionId}`);
201
+ connection.connection.on('close', () => {
202
+ this.cleanupMessageHandler(connectionId);
203
+ });
204
+ }
205
+ cleanupMessageHandler(connectionId) {
206
+ const handler = this.messageHandlers.get(connectionId);
207
+ if (handler) {
208
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
209
+ if (connection) {
210
+ connection.connection.off('message', handler);
211
+ }
212
+ this.messageHandlers.delete(connectionId);
213
+ this.logger.debug(`Cleaned up message handler for connection: ${connectionId}`);
214
+ }
215
+ const commandsToCleanup = [];
216
+ for (const [, pendingCommand] of this.pendingCommands.entries()) {
217
+ if (pendingCommand.connectionId === connectionId) {
218
+ commandsToCleanup.push(pendingCommand.id);
219
+ pendingCommand.reject(new Error('Connection closed'));
220
+ }
221
+ }
222
+ for (const commandKey of commandsToCleanup) {
223
+ this.pendingCommands.delete(commandKey);
224
+ }
225
+ if (commandsToCleanup.length > 0) {
226
+ this.logger.debug(`Cleaned up ${commandsToCleanup.length} pending commands for connection: ${connectionId}`);
227
+ }
228
+ }
229
+ getMetrics() {
230
+ return { ...this.commandMetrics };
231
+ }
232
+ getPendingCommandsCount() {
233
+ return this.pendingCommands.size;
234
+ }
235
+ getPendingCommandsForConnection(connectionId) {
236
+ let count = 0;
237
+ for (const pendingCommand of this.pendingCommands.values()) {
238
+ if (pendingCommand.connectionId === connectionId) {
239
+ count++;
240
+ }
241
+ }
242
+ return count;
243
+ }
244
+ setActiveCLIClient(clientId) {
245
+ if (this.activeCLIClient && this.activeCLIClient !== clientId) {
246
+ this.logger.warn(`Replacing active CLI client ${this.activeCLIClient} with ${clientId}`);
247
+ }
248
+ this.activeCLIClient = clientId;
249
+ this.logger.info(`CLI client ${clientId} is now the active client`);
250
+ }
251
+ releaseActiveCLIClient(clientId) {
252
+ if (clientId && this.activeCLIClient !== clientId) {
253
+ this.logger.warn(`Attempted to release CLI client ${clientId}, but active client is ${this.activeCLIClient}`);
254
+ return;
255
+ }
256
+ const previousClient = this.activeCLIClient;
257
+ this.activeCLIClient = null;
258
+ if (previousClient) {
259
+ this.logger.info(`Released active CLI client: ${previousClient}`);
260
+ }
261
+ }
262
+ getActiveCLIClient() {
263
+ return this.activeCLIClient;
264
+ }
265
+ hasActiveCLIClient() {
266
+ return this.activeCLIClient !== null;
267
+ }
268
+ cleanup() {
269
+ for (const [, pendingCommand] of this.pendingCommands.entries()) {
270
+ pendingCommand.reject(new Error('Service shutting down'));
271
+ }
272
+ this.pendingCommands.clear();
273
+ for (const connectionId of this.messageHandlers.keys()) {
274
+ this.cleanupMessageHandler(connectionId);
275
+ }
276
+ this.activeCLIClient = null;
277
+ this.logger.info('CommandExecutionService cleanup completed');
278
+ }
279
+ generateCommandId() {
280
+ return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
281
+ }
282
+ generateCDPMessageId() {
283
+ return Math.floor(Date.now() % 1000000) + Math.floor(Math.random() * 10000);
284
+ }
285
+ updateMetrics(success, executionTime) {
286
+ this.commandMetrics.totalCommands++;
287
+ if (success) {
288
+ this.commandMetrics.successfulCommands++;
289
+ }
290
+ else {
291
+ this.commandMetrics.failedCommands++;
292
+ }
293
+ const totalTime = this.commandMetrics.averageExecutionTime * (this.commandMetrics.totalCommands - 1) + executionTime;
294
+ this.commandMetrics.averageExecutionTime = totalTime / this.commandMetrics.totalCommands;
295
+ }
296
+ }
297
+ exports.CommandExecutionService = CommandExecutionService;
@@ -172,29 +172,36 @@ class ConnectionPool {
172
172
  connection.isHealthy = false;
173
173
  return false;
174
174
  }
175
- const messageId = Math.floor(Date.now() % 1000000) + Math.floor(Math.random() * 1000);
175
+ const messageId = Math.floor(Date.now() % 1000000) + Math.floor(Math.random() * 10000);
176
176
  const testMessage = {
177
177
  id: messageId,
178
178
  method: 'Runtime.evaluate',
179
179
  params: { expression: '1+1' }
180
180
  };
181
+ console.log(`[DEBUG] Health check starting for connection ${connectionId} with message ID: ${messageId}`);
181
182
  const healthCheckPromise = new Promise((resolve) => {
182
183
  const timeout = setTimeout(() => {
184
+ console.log(`[DEBUG] Health check timeout for connection ${connectionId}, message ID: ${messageId}`);
183
185
  resolve(false);
184
186
  }, 5000);
185
187
  const messageHandler = (data) => {
186
188
  try {
187
189
  const response = JSON.parse(data.toString());
190
+ console.log(`[DEBUG] Health check received message for connection ${connectionId}, response ID: ${response.id}, expected: ${messageId}`);
188
191
  if (response.id === testMessage.id) {
189
192
  clearTimeout(timeout);
190
193
  connection.connection.off('message', messageHandler);
191
- resolve(response.result && !response.error);
194
+ const isHealthy = response.result && !response.error;
195
+ console.log(`[DEBUG] Health check result for connection ${connectionId}: ${isHealthy}`);
196
+ resolve(isHealthy);
192
197
  }
193
198
  }
194
199
  catch (error) {
200
+ console.log(`[DEBUG] Health check message parsing error for connection ${connectionId}:`, error);
195
201
  }
196
202
  };
197
203
  connection.connection.on('message', messageHandler);
204
+ console.log(`[DEBUG] Sending health check command for connection ${connectionId}:`, testMessage);
198
205
  connection.connection.send(JSON.stringify(testMessage));
199
206
  });
200
207
  const isHealthy = await healthCheckPromise;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ProxyAPIServer = void 0;
4
4
  const SecurityManager_1 = require("./SecurityManager");
5
+ const CommandExecutionService_1 = require("./CommandExecutionService");
5
6
  const logger_1 = require("../../utils/logger");
6
7
  class ProxyAPIServer {
7
8
  constructor(connectionPool, messageStore, healthMonitor, performanceMonitor, securityManager) {
@@ -10,6 +11,7 @@ class ProxyAPIServer {
10
11
  this.healthMonitor = healthMonitor;
11
12
  this.performanceMonitor = performanceMonitor;
12
13
  this.securityManager = securityManager || new SecurityManager_1.SecurityManager();
14
+ this.commandExecutionService = new CommandExecutionService_1.CommandExecutionService(connectionPool);
13
15
  this.logger = new logger_1.Logger();
14
16
  }
15
17
  setupRoutes(app) {
@@ -20,6 +22,9 @@ class ProxyAPIServer {
20
22
  app.get('/api/console/:connectionId', this.validateConnectionId.bind(this), this.handleGetConsoleMessages.bind(this));
21
23
  app.get('/api/network/:connectionId', this.validateConnectionId.bind(this), this.handleGetNetworkRequests.bind(this));
22
24
  app.get('/api/health/:connectionId', this.validateConnectionId.bind(this), this.handleHealthCheck.bind(this));
25
+ app.post('/api/execute/:connectionId', this.securityManager.getStrictRateLimiter(), this.validateConnectionId.bind(this), this.validateCommandExecutionRequest.bind(this), this.handleCommandExecution.bind(this));
26
+ app.post('/api/client/release', this.handleReleaseCLIClient.bind(this));
27
+ app.get('/api/client/status', this.handleGetCLIClientStatus.bind(this));
23
28
  app.get('/api/health', this.handleServerHealth.bind(this));
24
29
  app.get('/api/health/detailed', this.handleDetailedHealthCheck.bind(this));
25
30
  app.get('/api/health/statistics', this.handleHealthStatistics.bind(this));
@@ -33,6 +38,13 @@ class ProxyAPIServer {
33
38
  app.get('/api/status', this.handleServerStatus.bind(this));
34
39
  this.logger.info('API routes configured with enhanced security measures');
35
40
  }
41
+ cleanup() {
42
+ this.commandExecutionService.cleanup();
43
+ this.logger.info('ProxyAPIServer cleanup completed');
44
+ }
45
+ getCommandExecutionService() {
46
+ return this.commandExecutionService;
47
+ }
36
48
  async handleConnect(req, res) {
37
49
  try {
38
50
  const { host, port, targetId } = req.body;
@@ -245,6 +257,78 @@ class ProxyAPIServer {
245
257
  });
246
258
  }
247
259
  }
260
+ async handleCommandExecution(req, res) {
261
+ try {
262
+ const { connectionId } = req.params;
263
+ const { command, timeout } = req.body;
264
+ const clientId = req.headers['x-client-id'] || `client_${req.ip}_${Date.now()}`;
265
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
266
+ if (!connection) {
267
+ res.status(404).json({
268
+ success: false,
269
+ error: 'Connection not found',
270
+ timestamp: Date.now()
271
+ });
272
+ return;
273
+ }
274
+ if (!connection.isHealthy) {
275
+ res.status(503).json({
276
+ success: false,
277
+ error: 'Connection is not healthy',
278
+ timestamp: Date.now()
279
+ });
280
+ return;
281
+ }
282
+ this.logger.debug(`Executing CDP command: ${command.method}`, {
283
+ connectionId,
284
+ method: command.method,
285
+ hasParams: !!command.params,
286
+ timeout: timeout || 30000,
287
+ clientId
288
+ });
289
+ const executionRequest = {
290
+ connectionId,
291
+ command,
292
+ timeout
293
+ };
294
+ const result = await this.commandExecutionService.executeCommand(executionRequest, clientId);
295
+ const statusCode = result.success ? 200 : (result.error?.code || 500);
296
+ res.status(statusCode).json({
297
+ success: result.success,
298
+ data: result,
299
+ timestamp: Date.now()
300
+ });
301
+ this.logger.debug(`CDP command execution completed: ${command.method}`, {
302
+ connectionId,
303
+ success: result.success,
304
+ executionTime: result.executionTime,
305
+ statusCode,
306
+ clientId
307
+ });
308
+ }
309
+ catch (error) {
310
+ this.logger.error('Command execution API error:', error);
311
+ let statusCode = 500;
312
+ let errorMessage = error instanceof Error ? error.message : 'Command execution failed';
313
+ if (errorMessage.includes('Another CLI client')) {
314
+ statusCode = 409;
315
+ }
316
+ else if (errorMessage.includes('timeout')) {
317
+ statusCode = 408;
318
+ }
319
+ else if (errorMessage.includes('not found')) {
320
+ statusCode = 404;
321
+ }
322
+ else if (errorMessage.includes('not healthy')) {
323
+ statusCode = 503;
324
+ }
325
+ res.status(statusCode).json({
326
+ success: false,
327
+ error: errorMessage,
328
+ timestamp: Date.now()
329
+ });
330
+ }
331
+ }
248
332
  async handleListConnections(_req, res) {
249
333
  try {
250
334
  const connections = this.connectionPool.getAllConnections();
@@ -665,6 +749,85 @@ class ProxyAPIServer {
665
749
  }
666
750
  next();
667
751
  }
752
+ validateCommandExecutionRequest(req, res, next) {
753
+ const { command, timeout } = req.body;
754
+ if (!command || typeof command !== 'object') {
755
+ this.logger.logSecurityEvent('invalid_command_request', 'Missing or invalid command object', {
756
+ command,
757
+ ip: req.ip
758
+ });
759
+ res.status(400).json({
760
+ success: false,
761
+ error: 'Command is required and must be an object',
762
+ timestamp: Date.now()
763
+ });
764
+ return;
765
+ }
766
+ if (!command.method || typeof command.method !== 'string' || command.method.trim() === '') {
767
+ this.logger.logSecurityEvent('invalid_command_request', 'Missing or invalid command method', {
768
+ method: command.method,
769
+ ip: req.ip
770
+ });
771
+ res.status(400).json({
772
+ success: false,
773
+ error: 'Command method is required and must be a non-empty string',
774
+ timestamp: Date.now()
775
+ });
776
+ return;
777
+ }
778
+ const methodRegex = /^[A-Za-z][A-Za-z0-9]*\.[A-Za-z][A-Za-z0-9]*$/;
779
+ if (!methodRegex.test(command.method)) {
780
+ this.logger.logSecurityEvent('invalid_command_method', 'Command method has invalid format', {
781
+ method: command.method,
782
+ ip: req.ip
783
+ });
784
+ res.status(400).json({
785
+ success: false,
786
+ error: 'Command method must be in format "Domain.method" (e.g., "Runtime.evaluate")',
787
+ timestamp: Date.now()
788
+ });
789
+ return;
790
+ }
791
+ if (command.id !== undefined && (typeof command.id !== 'number' && typeof command.id !== 'string')) {
792
+ this.logger.logSecurityEvent('invalid_command_request', 'Invalid command ID type', {
793
+ commandId: command.id,
794
+ ip: req.ip
795
+ });
796
+ res.status(400).json({
797
+ success: false,
798
+ error: 'Command ID must be a number or string if provided',
799
+ timestamp: Date.now()
800
+ });
801
+ return;
802
+ }
803
+ if (timeout !== undefined) {
804
+ if (typeof timeout !== 'number' || timeout <= 0 || timeout > 300000) {
805
+ this.logger.logSecurityEvent('invalid_command_request', 'Invalid timeout value', {
806
+ timeout,
807
+ ip: req.ip
808
+ });
809
+ res.status(400).json({
810
+ success: false,
811
+ error: 'Timeout must be a positive number between 1 and 300000 (5 minutes)',
812
+ timestamp: Date.now()
813
+ });
814
+ return;
815
+ }
816
+ }
817
+ if (command.params !== undefined && (command.params === null || (typeof command.params !== 'object' && !Array.isArray(command.params)))) {
818
+ this.logger.logSecurityEvent('invalid_command_request', 'Invalid command params', {
819
+ paramsType: typeof command.params,
820
+ ip: req.ip
821
+ });
822
+ res.status(400).json({
823
+ success: false,
824
+ error: 'Command params must be an object or array if provided',
825
+ timestamp: Date.now()
826
+ });
827
+ return;
828
+ }
829
+ next();
830
+ }
668
831
  parseConsoleMessageFilter(query) {
669
832
  try {
670
833
  const filter = {};
@@ -905,5 +1068,53 @@ class ProxyAPIServer {
905
1068
  return `${minutes} minute${minutes > 1 ? 's' : ''}`;
906
1069
  }
907
1070
  }
1071
+ async handleReleaseCLIClient(req, res) {
1072
+ try {
1073
+ const clientId = req.headers['x-client-id'] || `client_${req.ip}_${Date.now()}`;
1074
+ this.commandExecutionService.releaseActiveCLIClient(clientId);
1075
+ this.logger.info(`CLI client released: ${clientId}`);
1076
+ res.json({
1077
+ success: true,
1078
+ data: {
1079
+ message: 'CLI client released successfully',
1080
+ clientId
1081
+ },
1082
+ timestamp: Date.now()
1083
+ });
1084
+ }
1085
+ catch (error) {
1086
+ this.logger.error('Release CLI client API error:', error);
1087
+ res.status(500).json({
1088
+ success: false,
1089
+ error: error instanceof Error ? error.message : 'Failed to release CLI client',
1090
+ timestamp: Date.now()
1091
+ });
1092
+ }
1093
+ }
1094
+ async handleGetCLIClientStatus(_req, res) {
1095
+ try {
1096
+ const activeCLIClient = this.commandExecutionService.getActiveCLIClient();
1097
+ const hasActiveClient = this.commandExecutionService.hasActiveCLIClient();
1098
+ const pendingCommandsCount = this.commandExecutionService.getPendingCommandsCount();
1099
+ res.json({
1100
+ success: true,
1101
+ data: {
1102
+ hasActiveClient,
1103
+ activeCLIClient,
1104
+ pendingCommandsCount,
1105
+ singleClientMode: true
1106
+ },
1107
+ timestamp: Date.now()
1108
+ });
1109
+ }
1110
+ catch (error) {
1111
+ this.logger.error('Get CLI client status API error:', error);
1112
+ res.status(500).json({
1113
+ success: false,
1114
+ error: error instanceof Error ? error.message : 'Failed to get CLI client status',
1115
+ timestamp: Date.now()
1116
+ });
1117
+ }
1118
+ }
908
1119
  }
909
1120
  exports.ProxyAPIServer = ProxyAPIServer;
@@ -236,9 +236,11 @@ class WSProxy {
236
236
  if (cdpConnection.connection.readyState === WebSocket.OPEN) {
237
237
  cdpConnection.connection.send(message);
238
238
  proxyConnection.messageCount++;
239
+ console.log(`[DEBUG] Forwarded CDP command from client ${proxyConnection.id}: ${cdpCommand.method} (id: ${cdpCommand.id})`);
239
240
  this.logger.debug(`Forwarded CDP command from client ${proxyConnection.id}: ${cdpCommand.method} (id: ${cdpCommand.id})`);
240
241
  }
241
242
  else {
243
+ console.log(`[DEBUG] CDP connection ${connectionId} is not open, readyState: ${cdpConnection.connection.readyState}`);
242
244
  this.logger.warn(`CDP connection ${connectionId} is not open, cannot forward command`);
243
245
  clientWs.send(JSON.stringify({
244
246
  error: { code: -32001, message: 'CDP connection unavailable' },
@@ -266,20 +268,30 @@ class WSProxy {
266
268
  return;
267
269
  }
268
270
  if (this.isCDPResponse(cdpMessage)) {
271
+ console.log(`[DEBUG] Received CDP response for client ${proxyConnection.id}: id ${cdpMessage.id}`);
269
272
  if (clientWs.readyState === WebSocket.OPEN) {
270
273
  clientWs.send(message);
271
274
  proxyConnection.messageCount++;
275
+ console.log(`[DEBUG] Forwarded CDP response to client ${proxyConnection.id}: id ${cdpMessage.id}`);
272
276
  this.logger.debug(`Forwarded CDP response to client ${proxyConnection.id}: id ${cdpMessage.id}`);
273
277
  }
278
+ else {
279
+ console.log(`[DEBUG] Client WebSocket is not open for ${proxyConnection.id}, readyState: ${clientWs.readyState}`);
280
+ }
274
281
  }
275
282
  else if (this.isCDPEvent(cdpMessage)) {
283
+ console.log(`[DEBUG] Received CDP event: ${cdpMessage.method}`);
276
284
  this.forwardEventToSubscribedClients(connectionId, cdpMessage, message);
277
285
  }
286
+ else {
287
+ console.log(`[DEBUG] Received unknown CDP message type:`, cdpMessage);
288
+ }
278
289
  }
279
290
  catch (error) {
280
291
  this.logger.error(`Error forwarding message to client ${proxyConnection.id}:`, error);
281
292
  }
282
293
  };
294
+ console.log(`[DEBUG] Adding message handler to CDP connection ${connectionId} for proxy ${proxyConnection.id}`);
283
295
  cdpConnection.connection.on('message', cdpMessageHandler);
284
296
  clientWs.on('close', () => {
285
297
  cdpConnection.connection.off('message', cdpMessageHandler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-cdp-cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "LLM-first browser automation CLI via Chrome DevTools Protocol. Eval-first design optimized for AI assistants - LLMs write JavaScript scripts for rapid validation. Features: JavaScript execution, 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",