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.
- package/dist/cli/CLIApplication.js +7 -0
- package/dist/cli/CommandRouter.js +5 -0
- package/dist/client/ProxyClient.js +25 -1
- package/dist/handlers/EvaluateScriptHandler.js +82 -65
- package/dist/handlers/InstallCursorCommandHandler.js +76 -76
- package/dist/proxy/server/CDPProxyServer.js +1 -0
- package/dist/proxy/server/CommandExecutionService.js +297 -0
- package/dist/proxy/server/ConnectionPool.js +9 -2
- package/dist/proxy/server/ProxyAPIServer.js +211 -0
- package/dist/proxy/server/WSProxy.js +12 -0
- package/package.json +1 -1
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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:
|
|
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** -
|
|
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** -
|
|
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** -
|
|
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** -
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
316
|
-
- \`--port <number>\`: Chrome DevTools
|
|
317
|
-
- \`--format <json|text>\`:
|
|
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
|
`;
|
|
@@ -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() *
|
|
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
|
-
|
|
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.
|
|
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",
|