chrome-cdp-cli 1.6.0 → 1.7.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.
@@ -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 {
@@ -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.0",
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",