@dynamicu/chromedebug-mcp 2.6.6 → 2.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.
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/chrome-extension/activation-manager.js +18 -4
- package/chrome-extension/background.js +1044 -552
- package/chrome-extension/browser-recording-manager.js +256 -0
- package/chrome-extension/chrome-debug-logger.js +168 -0
- package/chrome-extension/console-interception-library.js +430 -0
- package/chrome-extension/content.css +16 -16
- package/chrome-extension/content.js +617 -215
- package/chrome-extension/data-buffer.js +206 -17
- package/chrome-extension/extension-config.js +1 -1
- package/chrome-extension/frame-capture.js +52 -15
- package/chrome-extension/license-helper.js +26 -0
- package/chrome-extension/manifest.free.json +3 -6
- package/chrome-extension/options.js +1 -1
- package/chrome-extension/popup.html +315 -181
- package/chrome-extension/popup.js +673 -526
- package/chrome-extension/pro/enhanced-capture.js +406 -0
- package/chrome-extension/pro/frame-editor.html +410 -0
- package/chrome-extension/pro/frame-editor.js +1496 -0
- package/chrome-extension/pro/function-tracker.js +843 -0
- package/chrome-extension/pro/jszip.min.js +13 -0
- package/config/chromedebug-config.json +101 -0
- package/dist/chromedebug-extension-free.zip +0 -0
- package/package.json +3 -1
- package/scripts/package-pro-extension.js +1 -1
- package/scripts/webpack.config.free.cjs +11 -8
- package/scripts/webpack.config.pro.cjs +5 -0
- package/src/chrome-controller.js +7 -7
- package/src/cli.js +2 -2
- package/src/database.js +61 -9
- package/src/http-server.js +3 -2
- package/src/index.js +9 -6
- package/src/mcp/server.js +2 -2
- package/src/services/process-manager.js +10 -6
- package/src/services/process-tracker.js +10 -5
- package/src/services/profile-manager.js +17 -2
- package/src/validation/schemas.js +36 -6
- package/src/index-direct.js +0 -157
- package/src/index-modular.js +0 -219
- package/src/index-monolithic-backup.js +0 -2230
- package/src/legacy/chrome-controller-old.js +0 -1406
- package/src/legacy/index-express.js +0 -625
- package/src/legacy/index-old.js +0 -977
- package/src/legacy/routes.js +0 -260
- package/src/legacy/shared-storage.js +0 -101
|
@@ -1,1406 +0,0 @@
|
|
|
1
|
-
import puppeteer from 'puppeteer';
|
|
2
|
-
import multer from 'multer';
|
|
3
|
-
import fs from 'fs/promises';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { findAvailablePort } from './utils.js';
|
|
7
|
-
import { sharedStorage } from './shared-storage.js';
|
|
8
|
-
|
|
9
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
|
|
11
|
-
export class ChromeController {
|
|
12
|
-
constructor() {
|
|
13
|
-
this.browser = null;
|
|
14
|
-
this.page = null;
|
|
15
|
-
this.client = null;
|
|
16
|
-
this.paused = false;
|
|
17
|
-
this.logs = [];
|
|
18
|
-
this.debugPort = null;
|
|
19
|
-
this.defaultTimeout = 10000; // 10 seconds default timeout
|
|
20
|
-
|
|
21
|
-
// Store currently selected element information
|
|
22
|
-
this.selectedElement = null;
|
|
23
|
-
|
|
24
|
-
// Recording storage
|
|
25
|
-
this.recordings = new Map();
|
|
26
|
-
|
|
27
|
-
// Connection monitoring
|
|
28
|
-
this.heartbeatInterval = null;
|
|
29
|
-
|
|
30
|
-
// Setup multer for handling file uploads
|
|
31
|
-
this.upload = multer({
|
|
32
|
-
storage: multer.memoryStorage(),
|
|
33
|
-
limits: { fileSize: 100 * 1024 * 1024 } // 100MB limit
|
|
34
|
-
}).fields([
|
|
35
|
-
{ name: 'logs', maxCount: 1 },
|
|
36
|
-
{ name: 'duration', maxCount: 1 }
|
|
37
|
-
]);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Helper method to add timeout to any promise
|
|
41
|
-
withTimeout(promise, timeoutMs = this.defaultTimeout, operation = 'operation') {
|
|
42
|
-
return Promise.race([
|
|
43
|
-
promise,
|
|
44
|
-
new Promise((_, reject) => {
|
|
45
|
-
setTimeout(() => {
|
|
46
|
-
reject(new Error(`${operation} timed out after ${timeoutMs}ms`));
|
|
47
|
-
}, timeoutMs);
|
|
48
|
-
})
|
|
49
|
-
]);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Helper method to kill Chrome processes using the debug port
|
|
53
|
-
async killChromeProcesses() {
|
|
54
|
-
try {
|
|
55
|
-
if (this.debugPort) {
|
|
56
|
-
const { exec } = await import('child_process');
|
|
57
|
-
const { promisify } = await import('util');
|
|
58
|
-
const execAsync = promisify(exec);
|
|
59
|
-
|
|
60
|
-
// Kill any Chrome processes using our debug port
|
|
61
|
-
await execAsync(`lsof -ti:${this.debugPort} | xargs kill -9`).catch(() => {
|
|
62
|
-
// Ignore errors if no processes found
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error('Error killing Chrome processes:', error.message);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async launch() {
|
|
71
|
-
try {
|
|
72
|
-
// Check if browser is still connected
|
|
73
|
-
if (this.browser) {
|
|
74
|
-
try {
|
|
75
|
-
// Test if the browser is still responsive
|
|
76
|
-
await this.browser.version();
|
|
77
|
-
} catch (e) {
|
|
78
|
-
// Browser is disconnected, clean up
|
|
79
|
-
console.error('Previous browser instance is disconnected, cleaning up...');
|
|
80
|
-
this.browser = null;
|
|
81
|
-
this.page = null;
|
|
82
|
-
this.client = null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Close any existing browser instance
|
|
87
|
-
if (this.browser) {
|
|
88
|
-
await this.close();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Try to connect to existing Chrome instance first
|
|
92
|
-
console.error('Attempting to connect to existing Chrome instance...');
|
|
93
|
-
|
|
94
|
-
// Common debugging ports
|
|
95
|
-
const commonPorts = [9222, 9223, 9224, 9225, 9229];
|
|
96
|
-
let connected = false;
|
|
97
|
-
|
|
98
|
-
for (const port of commonPorts) {
|
|
99
|
-
try {
|
|
100
|
-
console.error(`Trying to connect to Chrome on port ${port}...`);
|
|
101
|
-
this.browser = await puppeteer.connect({
|
|
102
|
-
browserURL: `http://localhost:${port}`,
|
|
103
|
-
defaultViewport: null
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
this.debugPort = port;
|
|
107
|
-
connected = true;
|
|
108
|
-
console.error(`Successfully connected to existing Chrome on port ${port}`);
|
|
109
|
-
break;
|
|
110
|
-
} catch (e) {
|
|
111
|
-
// Continue to next port
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!connected) {
|
|
116
|
-
// If no existing Chrome found, launch a new one
|
|
117
|
-
console.error('No existing Chrome found with debugging enabled. Launching new instance...');
|
|
118
|
-
console.error('To connect to existing Chrome, start it with: --remote-debugging-port=9222');
|
|
119
|
-
|
|
120
|
-
// Find an available port for Chrome debugging
|
|
121
|
-
this.debugPort = await findAvailablePort(9222, 9322);
|
|
122
|
-
|
|
123
|
-
// Kill any stuck Chrome processes on this port
|
|
124
|
-
await this.killChromeProcesses();
|
|
125
|
-
|
|
126
|
-
console.error(`Launching Chrome with debugging port ${this.debugPort}`);
|
|
127
|
-
|
|
128
|
-
// Detect Chrome executable path based on platform
|
|
129
|
-
let executablePath;
|
|
130
|
-
const platform = process.platform;
|
|
131
|
-
|
|
132
|
-
if (platform === 'darwin') {
|
|
133
|
-
// macOS
|
|
134
|
-
executablePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
135
|
-
} else if (platform === 'win32') {
|
|
136
|
-
// Windows
|
|
137
|
-
executablePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
|
|
138
|
-
// Try alternative paths if the first doesn't exist
|
|
139
|
-
const fs = await import('fs');
|
|
140
|
-
if (!fs.existsSync(executablePath)) {
|
|
141
|
-
executablePath = 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe';
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
// Linux
|
|
145
|
-
executablePath = '/usr/bin/google-chrome';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
console.error(`Using Chrome executable: ${executablePath}`);
|
|
149
|
-
|
|
150
|
-
this.browser = await this.withTimeout(
|
|
151
|
-
puppeteer.launch({
|
|
152
|
-
headless: false,
|
|
153
|
-
executablePath: executablePath,
|
|
154
|
-
args: [
|
|
155
|
-
`--remote-debugging-port=${this.debugPort}`,
|
|
156
|
-
'--no-first-run',
|
|
157
|
-
'--no-default-browser-check'
|
|
158
|
-
],
|
|
159
|
-
defaultViewport: null,
|
|
160
|
-
ignoreDefaultArgs: ['--disable-extensions']
|
|
161
|
-
}),
|
|
162
|
-
30000, // 30 seconds for browser launch
|
|
163
|
-
'browser launch'
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Set up disconnect handler
|
|
168
|
-
this.browser.on('disconnected', () => {
|
|
169
|
-
console.error('Browser disconnected');
|
|
170
|
-
this.browser = null;
|
|
171
|
-
this.page = null;
|
|
172
|
-
this.client = null;
|
|
173
|
-
this.stopHeartbeat();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Start heartbeat monitoring
|
|
177
|
-
this.startHeartbeat();
|
|
178
|
-
} catch (error) {
|
|
179
|
-
throw new Error(`Failed to connect to Chrome: ${error.message}. Make sure Chrome is running with --remote-debugging-port=9222`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const pages = await this.withTimeout(
|
|
183
|
-
this.browser.pages(),
|
|
184
|
-
this.defaultTimeout,
|
|
185
|
-
'get browser pages'
|
|
186
|
-
);
|
|
187
|
-
this.page = pages[0] || await this.withTimeout(
|
|
188
|
-
this.browser.newPage(),
|
|
189
|
-
this.defaultTimeout,
|
|
190
|
-
'create new page'
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
this.client = await this.withTimeout(
|
|
194
|
-
this.page.createCDPSession(),
|
|
195
|
-
this.defaultTimeout,
|
|
196
|
-
'create CDP session'
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
await this.withTimeout(
|
|
200
|
-
Promise.all([
|
|
201
|
-
this.client.send('Debugger.enable'),
|
|
202
|
-
this.client.send('Runtime.enable'),
|
|
203
|
-
this.client.send('Console.enable')
|
|
204
|
-
]),
|
|
205
|
-
this.defaultTimeout,
|
|
206
|
-
'enable CDP domains'
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
this.client.on('Debugger.paused', () => {
|
|
210
|
-
this.paused = true;
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
this.client.on('Debugger.resumed', () => {
|
|
214
|
-
this.paused = false;
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
this.client.on('Console.messageAdded', (params) => {
|
|
218
|
-
this.logs.push({
|
|
219
|
-
level: params.message.level,
|
|
220
|
-
text: params.message.text,
|
|
221
|
-
timestamp: new Date().toISOString()
|
|
222
|
-
});
|
|
223
|
-
if (this.logs.length > 100) {
|
|
224
|
-
this.logs.shift();
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const browserProcess = this.browser.process();
|
|
229
|
-
const processId = browserProcess ? browserProcess.pid : 'unknown';
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
browserWSEndpoint: this.browser.wsEndpoint(),
|
|
233
|
-
debuggerUrl: `ws://localhost:${this.debugPort}/devtools/browser/${processId}`,
|
|
234
|
-
debugPort: this.debugPort
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async connectToExisting(port = 9222) {
|
|
239
|
-
try {
|
|
240
|
-
// Close any existing connection
|
|
241
|
-
if (this.browser) {
|
|
242
|
-
await this.close();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
console.error(`Attempting to connect to Chrome on port ${port}...`);
|
|
246
|
-
|
|
247
|
-
// Try to connect to the specified port
|
|
248
|
-
this.browser = await puppeteer.connect({
|
|
249
|
-
browserURL: `http://localhost:${port}`,
|
|
250
|
-
defaultViewport: null
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
this.debugPort = port;
|
|
254
|
-
console.error(`Successfully connected to existing Chrome on port ${port}`);
|
|
255
|
-
|
|
256
|
-
// Set up disconnect handler
|
|
257
|
-
this.browser.on('disconnected', () => {
|
|
258
|
-
console.error('Browser disconnected');
|
|
259
|
-
this.browser = null;
|
|
260
|
-
this.page = null;
|
|
261
|
-
this.client = null;
|
|
262
|
-
this.stopHeartbeat();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// Start heartbeat monitoring
|
|
266
|
-
this.startHeartbeat();
|
|
267
|
-
|
|
268
|
-
// Get pages and set up CDP
|
|
269
|
-
const pages = await this.withTimeout(
|
|
270
|
-
this.browser.pages(),
|
|
271
|
-
this.defaultTimeout,
|
|
272
|
-
'get browser pages'
|
|
273
|
-
);
|
|
274
|
-
this.page = pages[0] || await this.withTimeout(
|
|
275
|
-
this.browser.newPage(),
|
|
276
|
-
this.defaultTimeout,
|
|
277
|
-
'create new page'
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
this.client = await this.withTimeout(
|
|
281
|
-
this.page.createCDPSession(),
|
|
282
|
-
this.defaultTimeout,
|
|
283
|
-
'create CDP session'
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
await this.withTimeout(
|
|
287
|
-
Promise.all([
|
|
288
|
-
this.client.send('Debugger.enable'),
|
|
289
|
-
this.client.send('Runtime.enable'),
|
|
290
|
-
this.client.send('Console.enable')
|
|
291
|
-
]),
|
|
292
|
-
this.defaultTimeout,
|
|
293
|
-
'enable CDP domains'
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
this.client.on('Debugger.paused', () => {
|
|
297
|
-
this.paused = true;
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
this.client.on('Debugger.resumed', () => {
|
|
301
|
-
this.paused = false;
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
this.client.on('Console.messageAdded', (params) => {
|
|
305
|
-
this.logs.push({
|
|
306
|
-
level: params.message.level,
|
|
307
|
-
text: params.message.text,
|
|
308
|
-
timestamp: new Date().toISOString()
|
|
309
|
-
});
|
|
310
|
-
if (this.logs.length > 100) {
|
|
311
|
-
this.logs.shift();
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
success: true,
|
|
317
|
-
message: `Connected to existing Chrome instance on port ${port}`,
|
|
318
|
-
browserWSEndpoint: this.browser.wsEndpoint(),
|
|
319
|
-
debugPort: this.debugPort
|
|
320
|
-
};
|
|
321
|
-
} catch (error) {
|
|
322
|
-
throw new Error(`Failed to connect to Chrome on port ${port}: ${error.message}. Make sure Chrome is running with --remote-debugging-port=${port}`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async isConnected() {
|
|
327
|
-
if (!this.browser) return false;
|
|
328
|
-
try {
|
|
329
|
-
await this.withTimeout(
|
|
330
|
-
this.browser.version(),
|
|
331
|
-
5000,
|
|
332
|
-
'browser version check'
|
|
333
|
-
);
|
|
334
|
-
return true;
|
|
335
|
-
} catch (e) {
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async ensureConnected() {
|
|
341
|
-
if (!await this.isConnected()) {
|
|
342
|
-
// Try to reconnect to the last known port if we had a connection before
|
|
343
|
-
if (this.debugPort) {
|
|
344
|
-
console.log(`Connection lost. Attempting to reconnect to Chrome on port ${this.debugPort}...`);
|
|
345
|
-
try {
|
|
346
|
-
await this.connectToExisting(this.debugPort);
|
|
347
|
-
console.log('Successfully reconnected to Chrome');
|
|
348
|
-
return;
|
|
349
|
-
} catch (error) {
|
|
350
|
-
console.error(`Failed to reconnect: ${error.message}`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
throw new Error('Chrome not connected. Please launch Chrome first using launch_chrome or connect to an existing instance using connect_to_existing_chrome.');
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async navigateTo(url) {
|
|
359
|
-
await this.ensureConnected();
|
|
360
|
-
if (!this.page) throw new Error('No page available');
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
await this.withTimeout(
|
|
364
|
-
this.page.goto(url, { waitUntil: 'domcontentloaded' }),
|
|
365
|
-
30000,
|
|
366
|
-
`navigate to ${url}`
|
|
367
|
-
);
|
|
368
|
-
return { status: 'success', url };
|
|
369
|
-
} catch (error) {
|
|
370
|
-
throw new Error(`Failed to navigate to ${url}: ${error.message}`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async pause() {
|
|
375
|
-
await this.ensureConnected();
|
|
376
|
-
if (!this.client) throw new Error('Chrome not connected');
|
|
377
|
-
await this.withTimeout(
|
|
378
|
-
this.client.send('Debugger.pause'),
|
|
379
|
-
this.defaultTimeout,
|
|
380
|
-
'pause execution'
|
|
381
|
-
);
|
|
382
|
-
return { status: 'paused' };
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
async resume() {
|
|
386
|
-
await this.ensureConnected();
|
|
387
|
-
if (!this.client) throw new Error('Chrome not connected');
|
|
388
|
-
await this.withTimeout(
|
|
389
|
-
this.client.send('Debugger.resume'),
|
|
390
|
-
this.defaultTimeout,
|
|
391
|
-
'resume execution'
|
|
392
|
-
);
|
|
393
|
-
return { status: 'resumed' };
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async stepOver() {
|
|
397
|
-
await this.ensureConnected();
|
|
398
|
-
if (!this.client) throw new Error('Chrome not connected');
|
|
399
|
-
await this.withTimeout(
|
|
400
|
-
this.client.send('Debugger.stepOver'),
|
|
401
|
-
this.defaultTimeout,
|
|
402
|
-
'step over'
|
|
403
|
-
);
|
|
404
|
-
return { status: 'stepped' };
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
async evaluate(expression) {
|
|
408
|
-
await this.ensureConnected();
|
|
409
|
-
if (!this.client) throw new Error('Chrome not connected');
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
const result = await this.withTimeout(
|
|
413
|
-
this.client.send('Runtime.evaluate', {
|
|
414
|
-
expression: expression,
|
|
415
|
-
returnByValue: true,
|
|
416
|
-
generatePreview: true
|
|
417
|
-
}),
|
|
418
|
-
this.defaultTimeout,
|
|
419
|
-
'JavaScript evaluation'
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
if (result.exceptionDetails) {
|
|
423
|
-
return {
|
|
424
|
-
error: true,
|
|
425
|
-
message: result.exceptionDetails.text || 'Evaluation error',
|
|
426
|
-
exception: result.exceptionDetails
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
result: result.result.value,
|
|
432
|
-
type: result.result.type,
|
|
433
|
-
className: result.result.className
|
|
434
|
-
};
|
|
435
|
-
} catch (error) {
|
|
436
|
-
return {
|
|
437
|
-
error: true,
|
|
438
|
-
message: error.message
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async getScopes() {
|
|
444
|
-
await this.ensureConnected();
|
|
445
|
-
if (!this.client) throw new Error('Chrome not connected');
|
|
446
|
-
|
|
447
|
-
try {
|
|
448
|
-
const { callFrames } = await this.client.send('Debugger.getStackTrace');
|
|
449
|
-
|
|
450
|
-
if (!callFrames || callFrames.length === 0) {
|
|
451
|
-
return { scopes: [], message: 'No active call frames' };
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const topFrame = callFrames[0];
|
|
455
|
-
const scopes = [];
|
|
456
|
-
|
|
457
|
-
for (const scope of topFrame.scopeChain) {
|
|
458
|
-
const scopeInfo = {
|
|
459
|
-
type: scope.type,
|
|
460
|
-
name: scope.name,
|
|
461
|
-
variables: {}
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
if (scope.object && scope.object.objectId) {
|
|
465
|
-
const { result } = await this.client.send('Runtime.getProperties', {
|
|
466
|
-
objectId: scope.object.objectId,
|
|
467
|
-
ownProperties: true
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
for (const prop of result) {
|
|
471
|
-
if (prop.value) {
|
|
472
|
-
scopeInfo.variables[prop.name] = {
|
|
473
|
-
value: prop.value.value,
|
|
474
|
-
type: prop.value.type
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
scopes.push(scopeInfo);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return { scopes };
|
|
484
|
-
} catch (error) {
|
|
485
|
-
return {
|
|
486
|
-
error: true,
|
|
487
|
-
message: error.message || 'Failed to get scopes'
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
async setBreakpoint(url, lineNumber) {
|
|
493
|
-
await this.ensureConnected();
|
|
494
|
-
if (!this.client) throw new Error('Chrome not connected');
|
|
495
|
-
|
|
496
|
-
try {
|
|
497
|
-
const result = await this.withTimeout(
|
|
498
|
-
this.client.send('Debugger.setBreakpointByUrl', {
|
|
499
|
-
url: url,
|
|
500
|
-
lineNumber: lineNumber
|
|
501
|
-
}),
|
|
502
|
-
this.defaultTimeout,
|
|
503
|
-
'set breakpoint'
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
return {
|
|
507
|
-
breakpointId: result.breakpointId,
|
|
508
|
-
locations: result.locations
|
|
509
|
-
};
|
|
510
|
-
} catch (error) {
|
|
511
|
-
return {
|
|
512
|
-
error: true,
|
|
513
|
-
message: error.message
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
getLogs() {
|
|
519
|
-
return { logs: this.logs };
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
async takeScreenshot(options = {}) {
|
|
523
|
-
await this.ensureConnected();
|
|
524
|
-
if (!this.page) throw new Error('Chrome not connected');
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
const screenshotOptions = {
|
|
528
|
-
type: options.type || 'jpeg', // Default to JPEG for better compression
|
|
529
|
-
fullPage: options.fullPage !== false
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
// Always use lowRes mode unless explicitly disabled
|
|
533
|
-
// This MCP is designed for AI processing, not human viewing
|
|
534
|
-
if (options.lowRes !== false) {
|
|
535
|
-
// Get current viewport
|
|
536
|
-
const viewport = this.page.viewport();
|
|
537
|
-
|
|
538
|
-
// Set ultra aggressive quality for JPEG (matching frame capture settings)
|
|
539
|
-
if (screenshotOptions.type === 'jpeg' || screenshotOptions.type === 'jpg') {
|
|
540
|
-
screenshotOptions.quality = options.quality || 30; // 30% quality for consistency with frame capture
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// For low-res mode, always resize viewport to small size
|
|
544
|
-
let originalViewport = null;
|
|
545
|
-
const targetWidth = 320; // Mobile-sized width for minimal tokens
|
|
546
|
-
const targetHeight = 568; // iPhone SE sized height
|
|
547
|
-
|
|
548
|
-
if (viewport) {
|
|
549
|
-
originalViewport = viewport;
|
|
550
|
-
// Always set to our target size for consistency
|
|
551
|
-
await this.page.setViewport({
|
|
552
|
-
width: targetWidth,
|
|
553
|
-
height: targetHeight
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// For full page screenshots, aggressively limit the capture area
|
|
558
|
-
if (screenshotOptions.fullPage) {
|
|
559
|
-
// Inject JavaScript to limit page height for screenshot
|
|
560
|
-
await this.page.evaluate(() => {
|
|
561
|
-
const maxHeight = 600; // Very limited height for AI
|
|
562
|
-
// Force document to be smaller
|
|
563
|
-
document.documentElement.style.maxHeight = `${maxHeight}px`;
|
|
564
|
-
document.documentElement.style.overflow = 'hidden';
|
|
565
|
-
document.body.style.maxHeight = `${maxHeight}px`;
|
|
566
|
-
document.body.style.overflow = 'hidden';
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// Also limit fullPage to just capture visible viewport
|
|
570
|
-
screenshotOptions.fullPage = false;
|
|
571
|
-
screenshotOptions.clip = {
|
|
572
|
-
x: 0,
|
|
573
|
-
y: 0,
|
|
574
|
-
width: targetWidth,
|
|
575
|
-
height: Math.min(targetHeight, 600)
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
screenshotOptions.encoding = 'base64';
|
|
580
|
-
const screenshot = await this.withTimeout(
|
|
581
|
-
this.page.screenshot(screenshotOptions),
|
|
582
|
-
30000,
|
|
583
|
-
'take screenshot'
|
|
584
|
-
);
|
|
585
|
-
|
|
586
|
-
// Restore page state
|
|
587
|
-
if (options.fullPage !== false) {
|
|
588
|
-
await this.page.evaluate(() => {
|
|
589
|
-
document.documentElement.style.maxHeight = '';
|
|
590
|
-
document.documentElement.style.overflow = '';
|
|
591
|
-
document.body.style.maxHeight = '';
|
|
592
|
-
document.body.style.overflow = '';
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Restore original viewport if it was changed
|
|
597
|
-
if (originalViewport) {
|
|
598
|
-
await this.page.setViewport(originalViewport);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const sizeInBytes = Math.ceil(screenshot.length * 3/4);
|
|
602
|
-
const sizeInKB = Math.round(sizeInBytes / 1024);
|
|
603
|
-
|
|
604
|
-
// If still too large, return error with size info
|
|
605
|
-
if (sizeInKB > 50) { // 50KB limit for AI processing
|
|
606
|
-
return {
|
|
607
|
-
error: true,
|
|
608
|
-
message: `Screenshot too large for AI processing (${sizeInKB}KB). Maximum recommended size is 50KB. Try disabling fullPage or using path parameter to save to file.`,
|
|
609
|
-
size: `${sizeInKB}KB`,
|
|
610
|
-
recommendation: 'Use fullPage: false or path parameter to save to file'
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
return {
|
|
615
|
-
screenshot: screenshot,
|
|
616
|
-
size: `${sizeInKB}KB`,
|
|
617
|
-
type: screenshotOptions.type,
|
|
618
|
-
fullPage: screenshotOptions.fullPage,
|
|
619
|
-
lowRes: true,
|
|
620
|
-
quality: screenshotOptions.quality,
|
|
621
|
-
message: 'Low-resolution screenshot optimized for AI parsing'
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// If a path is provided, save to file and return metadata only
|
|
626
|
-
if (options.path) {
|
|
627
|
-
screenshotOptions.path = options.path;
|
|
628
|
-
if (options.quality && (screenshotOptions.type === 'jpeg' || screenshotOptions.type === 'jpg')) {
|
|
629
|
-
screenshotOptions.quality = options.quality || 80; // Higher quality for file saves
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const screenshot = await this.withTimeout(
|
|
633
|
-
this.page.screenshot(screenshotOptions),
|
|
634
|
-
30000,
|
|
635
|
-
'take screenshot'
|
|
636
|
-
);
|
|
637
|
-
|
|
638
|
-
// When saving to file, just return metadata
|
|
639
|
-
return {
|
|
640
|
-
saved: true,
|
|
641
|
-
path: options.path,
|
|
642
|
-
type: screenshotOptions.type,
|
|
643
|
-
fullPage: screenshotOptions.fullPage
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// For regular screenshots without path (non-lowRes mode)
|
|
648
|
-
// This should rarely be used as we default to lowRes for AI
|
|
649
|
-
screenshotOptions.encoding = 'base64';
|
|
650
|
-
if (options.quality && (screenshotOptions.type === 'jpeg' || screenshotOptions.type === 'jpg')) {
|
|
651
|
-
screenshotOptions.quality = options.quality || 80;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const screenshot = await this.withTimeout(
|
|
655
|
-
this.page.screenshot(screenshotOptions),
|
|
656
|
-
30000,
|
|
657
|
-
'take screenshot'
|
|
658
|
-
);
|
|
659
|
-
|
|
660
|
-
// Calculate size
|
|
661
|
-
const sizeInBytes = Math.ceil(screenshot.length * 3/4);
|
|
662
|
-
const sizeInKB = Math.round(sizeInBytes / 1024);
|
|
663
|
-
|
|
664
|
-
// For large screenshots, warn the user
|
|
665
|
-
if (sizeInKB > 100) {
|
|
666
|
-
return {
|
|
667
|
-
screenshot: screenshot.substring(0, 1000) + '... [truncated]',
|
|
668
|
-
size: `${sizeInKB}KB`,
|
|
669
|
-
type: screenshotOptions.type,
|
|
670
|
-
fullPage: screenshotOptions.fullPage,
|
|
671
|
-
truncated: true,
|
|
672
|
-
message: 'Screenshot too large. Use path parameter to save to file or lowRes: true for AI parsing.'
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
return {
|
|
677
|
-
screenshot: screenshot,
|
|
678
|
-
size: `${sizeInKB}KB`,
|
|
679
|
-
type: screenshotOptions.type,
|
|
680
|
-
fullPage: screenshotOptions.fullPage
|
|
681
|
-
};
|
|
682
|
-
} catch (error) {
|
|
683
|
-
return {
|
|
684
|
-
error: true,
|
|
685
|
-
message: error.message
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
async forceReset() {
|
|
691
|
-
console.error('Force resetting Chrome...');
|
|
692
|
-
|
|
693
|
-
// Kill all Chrome processes
|
|
694
|
-
await this.killChromeProcesses();
|
|
695
|
-
|
|
696
|
-
// Clear internal state
|
|
697
|
-
this.browser = null;
|
|
698
|
-
this.page = null;
|
|
699
|
-
this.client = null;
|
|
700
|
-
this.paused = false;
|
|
701
|
-
this.logs = [];
|
|
702
|
-
|
|
703
|
-
return { status: 'reset complete' };
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
async close() {
|
|
707
|
-
this.stopHeartbeat(); // Stop monitoring when closing
|
|
708
|
-
if (this.browser) {
|
|
709
|
-
try {
|
|
710
|
-
await this.browser.close();
|
|
711
|
-
} catch (e) {
|
|
712
|
-
// Browser might already be closed
|
|
713
|
-
console.error('Error closing browser:', e.message);
|
|
714
|
-
} finally {
|
|
715
|
-
this.browser = null;
|
|
716
|
-
this.page = null;
|
|
717
|
-
this.client = null;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
async isConnected() {
|
|
723
|
-
try {
|
|
724
|
-
if (!this.browser || !this.page) {
|
|
725
|
-
return false;
|
|
726
|
-
}
|
|
727
|
-
await this.browser.version();
|
|
728
|
-
return true;
|
|
729
|
-
} catch (e) {
|
|
730
|
-
return false;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
async selectElement(selector, instruction, elementInfo) {
|
|
735
|
-
await this.ensureConnected();
|
|
736
|
-
if (!this.client || !this.page) throw new Error('Chrome not connected');
|
|
737
|
-
|
|
738
|
-
try {
|
|
739
|
-
// Sanitize and validate inputs
|
|
740
|
-
if (!selector || typeof selector !== 'string') {
|
|
741
|
-
throw new Error('Invalid selector provided');
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Extract element information using CDP
|
|
745
|
-
const elementData = await this.page.evaluate((sel) => {
|
|
746
|
-
try {
|
|
747
|
-
const element = document.querySelector(sel);
|
|
748
|
-
if (!element) {
|
|
749
|
-
return { error: `Element not found: ${sel}` };
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Highlight the selected element
|
|
753
|
-
element.style.outline = '3px solid #E91E63';
|
|
754
|
-
element.style.outlineOffset = '2px';
|
|
755
|
-
|
|
756
|
-
// Get computed styles
|
|
757
|
-
const computedStyles = window.getComputedStyle(element);
|
|
758
|
-
const relevantStyles = {};
|
|
759
|
-
|
|
760
|
-
// Extract key style properties
|
|
761
|
-
const styleProps = [
|
|
762
|
-
'width', 'height', 'padding', 'margin', 'backgroundColor',
|
|
763
|
-
'color', 'fontSize', 'fontWeight', 'border', 'borderRadius',
|
|
764
|
-
'display', 'position', 'top', 'left', 'right', 'bottom',
|
|
765
|
-
'transform', 'opacity', 'boxShadow', 'textAlign'
|
|
766
|
-
];
|
|
767
|
-
|
|
768
|
-
styleProps.forEach(prop => {
|
|
769
|
-
relevantStyles[prop] = computedStyles[prop];
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
return {
|
|
773
|
-
outerHTML: element.outerHTML,
|
|
774
|
-
tagName: element.tagName.toLowerCase(),
|
|
775
|
-
id: element.id,
|
|
776
|
-
className: element.className,
|
|
777
|
-
computedStyles: relevantStyles,
|
|
778
|
-
boundingBox: element.getBoundingClientRect(),
|
|
779
|
-
textContent: element.textContent.substring(0, 100)
|
|
780
|
-
};
|
|
781
|
-
} catch (error) {
|
|
782
|
-
return { error: error.message };
|
|
783
|
-
}
|
|
784
|
-
}, selector);
|
|
785
|
-
|
|
786
|
-
if (elementData.error) {
|
|
787
|
-
throw new Error(elementData.error);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Include React component info if provided
|
|
791
|
-
if (elementInfo && elementInfo.reactComponent) {
|
|
792
|
-
elementData.reactComponent = elementInfo.reactComponent;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Store the selected element information
|
|
796
|
-
this.selectedElement = {
|
|
797
|
-
selector,
|
|
798
|
-
instruction: instruction || '',
|
|
799
|
-
elementData,
|
|
800
|
-
timestamp: new Date().toISOString()
|
|
801
|
-
};
|
|
802
|
-
|
|
803
|
-
// Log the selection
|
|
804
|
-
const reactInfo = elementData.reactComponent ?
|
|
805
|
-
` [React: ${elementData.reactComponent.componentName}]` : '';
|
|
806
|
-
this.logs.push({
|
|
807
|
-
timestamp: new Date().toISOString(),
|
|
808
|
-
type: 'element-selected',
|
|
809
|
-
message: `Selected: ${selector} - ${elementData.tagName}${elementData.id ? '#' + elementData.id : ''}${reactInfo}`
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
const response = {
|
|
813
|
-
success: true,
|
|
814
|
-
message: 'Element selected and ready for MCP commands',
|
|
815
|
-
element: {
|
|
816
|
-
selector,
|
|
817
|
-
tagName: elementData.tagName,
|
|
818
|
-
id: elementData.id,
|
|
819
|
-
classes: elementData.className,
|
|
820
|
-
size: `${elementData.boundingBox.width}px × ${elementData.boundingBox.height}px`,
|
|
821
|
-
instruction: instruction || 'No instruction provided'
|
|
822
|
-
}
|
|
823
|
-
};
|
|
824
|
-
|
|
825
|
-
// Add React component info to response if available
|
|
826
|
-
if (elementData.reactComponent) {
|
|
827
|
-
response.element.reactComponent = elementData.reactComponent;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
return response;
|
|
831
|
-
} catch (error) {
|
|
832
|
-
this.logs.push({
|
|
833
|
-
timestamp: new Date().toISOString(),
|
|
834
|
-
type: 'selection-error',
|
|
835
|
-
message: error.message
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
return {
|
|
839
|
-
success: false,
|
|
840
|
-
error: error.message
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
async getSelectedElement() {
|
|
846
|
-
if (!this.selectedElement) {
|
|
847
|
-
return {
|
|
848
|
-
selected: false,
|
|
849
|
-
message: 'No element currently selected'
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
return {
|
|
854
|
-
selected: true,
|
|
855
|
-
...this.selectedElement
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
async applyToSelectedElement(cssRules) {
|
|
860
|
-
if (!this.selectedElement) {
|
|
861
|
-
throw new Error('No element currently selected');
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
await this.ensureConnected();
|
|
865
|
-
if (!this.page) throw new Error('Chrome not connected');
|
|
866
|
-
|
|
867
|
-
const { selector } = this.selectedElement;
|
|
868
|
-
|
|
869
|
-
try {
|
|
870
|
-
// Inject styles using CDP Runtime.evaluate
|
|
871
|
-
const injectionResult = await this.page.evaluate((sel, cssRules) => {
|
|
872
|
-
try {
|
|
873
|
-
// Check if our style element already exists
|
|
874
|
-
let styleEl = document.getElementById('chrome-pilot-styles');
|
|
875
|
-
if (!styleEl) {
|
|
876
|
-
styleEl = document.createElement('style');
|
|
877
|
-
styleEl.id = 'chrome-pilot-styles';
|
|
878
|
-
document.head.appendChild(styleEl);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Append new rules
|
|
882
|
-
styleEl.innerHTML += `\n/* Chrome Debug MCP - ${new Date().toISOString()} */\n${sel} { ${cssRules} }\n`;
|
|
883
|
-
|
|
884
|
-
return { success: true, message: 'Styles applied successfully' };
|
|
885
|
-
} catch (error) {
|
|
886
|
-
return { success: false, error: error.message };
|
|
887
|
-
}
|
|
888
|
-
}, selector, cssRules);
|
|
889
|
-
|
|
890
|
-
if (!injectionResult.success) {
|
|
891
|
-
throw new Error(injectionResult.error);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Log the result
|
|
895
|
-
this.logs.push({
|
|
896
|
-
timestamp: new Date().toISOString(),
|
|
897
|
-
type: 'css-applied',
|
|
898
|
-
message: `Applied to ${selector}: ${cssRules}`
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
return {
|
|
902
|
-
success: true,
|
|
903
|
-
applied: {
|
|
904
|
-
selector,
|
|
905
|
-
css: cssRules
|
|
906
|
-
}
|
|
907
|
-
};
|
|
908
|
-
} catch (error) {
|
|
909
|
-
return {
|
|
910
|
-
success: false,
|
|
911
|
-
error: error.message
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
async executeOnSelectedElement(jsCode) {
|
|
917
|
-
if (!this.selectedElement) {
|
|
918
|
-
throw new Error('No element currently selected');
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
await this.ensureConnected();
|
|
922
|
-
if (!this.page) throw new Error('Chrome not connected');
|
|
923
|
-
|
|
924
|
-
const { selector } = this.selectedElement;
|
|
925
|
-
|
|
926
|
-
try {
|
|
927
|
-
const result = await this.page.evaluate((sel, code) => {
|
|
928
|
-
try {
|
|
929
|
-
const element = document.querySelector(sel);
|
|
930
|
-
if (!element) {
|
|
931
|
-
throw new Error(`Element not found: ${sel}`);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const fn = new Function('element', code);
|
|
935
|
-
return {
|
|
936
|
-
success: true,
|
|
937
|
-
result: fn(element)
|
|
938
|
-
};
|
|
939
|
-
} catch (error) {
|
|
940
|
-
return {
|
|
941
|
-
success: false,
|
|
942
|
-
error: error.message
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
}, selector, jsCode);
|
|
946
|
-
|
|
947
|
-
if (!result.success) {
|
|
948
|
-
throw new Error(result.error);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
return {
|
|
952
|
-
success: true,
|
|
953
|
-
result: result.result
|
|
954
|
-
};
|
|
955
|
-
} catch (error) {
|
|
956
|
-
return {
|
|
957
|
-
success: false,
|
|
958
|
-
error: error.message
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
async clearSelectedElement() {
|
|
964
|
-
if (!this.selectedElement) {
|
|
965
|
-
return {
|
|
966
|
-
success: true,
|
|
967
|
-
message: 'No element was selected'
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// Remove highlight from the element
|
|
972
|
-
if (this.page) {
|
|
973
|
-
await this.page.evaluate((sel) => {
|
|
974
|
-
const element = document.querySelector(sel);
|
|
975
|
-
if (element) {
|
|
976
|
-
element.style.outline = '';
|
|
977
|
-
element.style.outlineOffset = '';
|
|
978
|
-
}
|
|
979
|
-
}, this.selectedElement.selector).catch(() => {});
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
this.selectedElement = null;
|
|
983
|
-
|
|
984
|
-
return {
|
|
985
|
-
success: true,
|
|
986
|
-
message: 'Selected element cleared'
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
generateCssFromInstruction(instruction, elementInfo) {
|
|
992
|
-
const lowerInstruction = instruction.toLowerCase();
|
|
993
|
-
const styles = {};
|
|
994
|
-
let css = '';
|
|
995
|
-
|
|
996
|
-
// Size modifications
|
|
997
|
-
if (lowerInstruction.includes('larger') || lowerInstruction.includes('bigger')) {
|
|
998
|
-
// If element has explicit width/height, increase them
|
|
999
|
-
if (elementInfo.computedStyles && elementInfo.computedStyles.width !== 'auto') {
|
|
1000
|
-
const currentWidth = parseFloat(elementInfo.computedStyles.width);
|
|
1001
|
-
if (!isNaN(currentWidth)) {
|
|
1002
|
-
styles.width = `${currentWidth * 1.2}px !important`;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
if (elementInfo.computedStyles && elementInfo.computedStyles.height !== 'auto') {
|
|
1006
|
-
const currentHeight = parseFloat(elementInfo.computedStyles.height);
|
|
1007
|
-
if (!isNaN(currentHeight)) {
|
|
1008
|
-
styles.height = `${currentHeight * 1.2}px !important`;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
// Fallback to transform if no explicit dimensions
|
|
1012
|
-
if (!styles.width && !styles.height) {
|
|
1013
|
-
styles.transform = 'scale(1.2)';
|
|
1014
|
-
styles.transformOrigin = 'center';
|
|
1015
|
-
}
|
|
1016
|
-
} else if (lowerInstruction.includes('smaller')) {
|
|
1017
|
-
if (elementInfo.computedStyles && elementInfo.computedStyles.width !== 'auto') {
|
|
1018
|
-
const currentWidth = parseFloat(elementInfo.computedStyles.width);
|
|
1019
|
-
if (!isNaN(currentWidth)) {
|
|
1020
|
-
styles.width = `${currentWidth * 0.8}px !important`;
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
if (elementInfo.computedStyles && elementInfo.computedStyles.height !== 'auto') {
|
|
1024
|
-
const currentHeight = parseFloat(elementInfo.computedStyles.height);
|
|
1025
|
-
if (!isNaN(currentHeight)) {
|
|
1026
|
-
styles.height = `${currentHeight * 0.8}px !important`;
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
if (!styles.width && !styles.height) {
|
|
1030
|
-
styles.transform = 'scale(0.8)';
|
|
1031
|
-
styles.transformOrigin = 'center';
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Color modifications
|
|
1036
|
-
if (lowerInstruction.includes('blue')) {
|
|
1037
|
-
styles.backgroundColor = '#2196F3 !important';
|
|
1038
|
-
if (elementInfo.tagName === 'button' || elementInfo.tagName === 'a') {
|
|
1039
|
-
styles.color = 'white !important';
|
|
1040
|
-
}
|
|
1041
|
-
} else if (lowerInstruction.includes('red')) {
|
|
1042
|
-
styles.backgroundColor = '#F44336 !important';
|
|
1043
|
-
styles.color = 'white !important';
|
|
1044
|
-
} else if (lowerInstruction.includes('green')) {
|
|
1045
|
-
styles.backgroundColor = '#4CAF50 !important';
|
|
1046
|
-
styles.color = 'white !important';
|
|
1047
|
-
} else if (lowerInstruction.includes('yellow')) {
|
|
1048
|
-
styles.backgroundColor = '#FFEB3B !important';
|
|
1049
|
-
styles.color = '#333 !important';
|
|
1050
|
-
} else if (lowerInstruction.includes('purple')) {
|
|
1051
|
-
styles.backgroundColor = '#9C27B0 !important';
|
|
1052
|
-
styles.color = 'white !important';
|
|
1053
|
-
} else if (lowerInstruction.includes('orange')) {
|
|
1054
|
-
styles.backgroundColor = '#FF9800 !important';
|
|
1055
|
-
styles.color = 'white !important';
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// Layout modifications
|
|
1059
|
-
if (lowerInstruction.includes('center')) {
|
|
1060
|
-
if (lowerInstruction.includes('text')) {
|
|
1061
|
-
styles.textAlign = 'center !important';
|
|
1062
|
-
} else {
|
|
1063
|
-
// For block-level centering
|
|
1064
|
-
if (elementInfo.computedStyles && elementInfo.computedStyles.display !== 'inline') {
|
|
1065
|
-
styles.margin = '0 auto !important';
|
|
1066
|
-
styles.display = 'block !important';
|
|
1067
|
-
} else {
|
|
1068
|
-
// For inline elements, center the parent
|
|
1069
|
-
styles.display = 'block !important';
|
|
1070
|
-
styles.textAlign = 'center !important';
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
// Visibility
|
|
1076
|
-
if (lowerInstruction.includes('hide')) {
|
|
1077
|
-
styles.display = 'none !important';
|
|
1078
|
-
} else if (lowerInstruction.includes('show')) {
|
|
1079
|
-
styles.display = 'block !important';
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// Border modifications
|
|
1083
|
-
if (lowerInstruction.includes('border')) {
|
|
1084
|
-
if (lowerInstruction.includes('remove') || lowerInstruction.includes('no')) {
|
|
1085
|
-
styles.border = 'none !important';
|
|
1086
|
-
} else {
|
|
1087
|
-
styles.border = '2px solid #333 !important';
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// Border radius
|
|
1092
|
-
if (lowerInstruction.includes('round')) {
|
|
1093
|
-
if (lowerInstruction.includes('full')) {
|
|
1094
|
-
styles.borderRadius = '50% !important';
|
|
1095
|
-
} else {
|
|
1096
|
-
styles.borderRadius = '8px !important';
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
// Shadow
|
|
1101
|
-
if (lowerInstruction.includes('shadow')) {
|
|
1102
|
-
if (lowerInstruction.includes('remove') || lowerInstruction.includes('no')) {
|
|
1103
|
-
styles.boxShadow = 'none !important';
|
|
1104
|
-
} else if (lowerInstruction.includes('big') || lowerInstruction.includes('large')) {
|
|
1105
|
-
styles.boxShadow = '0 10px 20px rgba(0, 0, 0, 0.2) !important';
|
|
1106
|
-
} else {
|
|
1107
|
-
styles.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1) !important';
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Text modifications
|
|
1112
|
-
if (lowerInstruction.includes('bold')) {
|
|
1113
|
-
styles.fontWeight = 'bold !important';
|
|
1114
|
-
}
|
|
1115
|
-
if (lowerInstruction.includes('italic')) {
|
|
1116
|
-
styles.fontStyle = 'italic !important';
|
|
1117
|
-
}
|
|
1118
|
-
if (lowerInstruction.includes('underline')) {
|
|
1119
|
-
styles.textDecoration = 'underline !important';
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
// Spacing
|
|
1123
|
-
if (lowerInstruction.includes('padding')) {
|
|
1124
|
-
if (lowerInstruction.includes('more') || lowerInstruction.includes('add')) {
|
|
1125
|
-
styles.padding = '20px !important';
|
|
1126
|
-
} else if (lowerInstruction.includes('remove') || lowerInstruction.includes('no')) {
|
|
1127
|
-
styles.padding = '0 !important';
|
|
1128
|
-
} else {
|
|
1129
|
-
styles.padding = '10px !important';
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
if (lowerInstruction.includes('margin')) {
|
|
1134
|
-
if (lowerInstruction.includes('more') || lowerInstruction.includes('add')) {
|
|
1135
|
-
styles.margin = '20px !important';
|
|
1136
|
-
} else if (lowerInstruction.includes('remove') || lowerInstruction.includes('no')) {
|
|
1137
|
-
styles.margin = '0 !important';
|
|
1138
|
-
} else {
|
|
1139
|
-
styles.margin = '10px !important';
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// Animation hints
|
|
1144
|
-
if (lowerInstruction.includes('animate') || lowerInstruction.includes('transition')) {
|
|
1145
|
-
styles.transition = 'all 0.3s ease !important';
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// Generate CSS string
|
|
1149
|
-
if (Object.keys(styles).length > 0) {
|
|
1150
|
-
css = Object.entries(styles).map(([k, v]) => {
|
|
1151
|
-
// Convert camelCase to kebab-case
|
|
1152
|
-
const kebab = k.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
|
|
1153
|
-
return `${kebab}: ${v}`;
|
|
1154
|
-
}).join('; ');
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
return { styles, css };
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// Handle recording upload
|
|
1161
|
-
|
|
1162
|
-
// Get recording for analysis
|
|
1163
|
-
async getRecording(recordingId) {
|
|
1164
|
-
console.log(`Looking for recording: ${recordingId}`);
|
|
1165
|
-
const recording = await sharedStorage.get(recordingId);
|
|
1166
|
-
|
|
1167
|
-
if (!recording) {
|
|
1168
|
-
// List available recordings for debugging
|
|
1169
|
-
const availableRecordings = await sharedStorage.list();
|
|
1170
|
-
console.log('Available recordings:', availableRecordings.map(r => r.id));
|
|
1171
|
-
return { error: true, message: 'Recording not found' };
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// Handle frame capture sessions
|
|
1175
|
-
if (recording.type === 'frame_capture') {
|
|
1176
|
-
const sessionInfo = await sharedStorage.getFrameSessionInfo(recordingId);
|
|
1177
|
-
return {
|
|
1178
|
-
success: true,
|
|
1179
|
-
type: 'frame_capture',
|
|
1180
|
-
sessionInfo: sessionInfo,
|
|
1181
|
-
message: 'This is a frame capture recording. Use get_frame_session_info and get_frame to access the frames.',
|
|
1182
|
-
recording: {
|
|
1183
|
-
id: recordingId,
|
|
1184
|
-
type: 'frame_capture',
|
|
1185
|
-
totalFrames: sessionInfo ? sessionInfo.totalFrames : 0
|
|
1186
|
-
}
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// Handle chunked recordings (legacy - no longer supported)
|
|
1191
|
-
if (recording.type === 'chunked_recording') {
|
|
1192
|
-
return {
|
|
1193
|
-
error: true,
|
|
1194
|
-
message: 'Chunked recordings are no longer supported. Only frame capture recordings are available.'
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// Handle old single-file recordings (if any exist)
|
|
1199
|
-
if (recording.data && recording.data.toString) {
|
|
1200
|
-
return {
|
|
1201
|
-
error: true,
|
|
1202
|
-
message: 'Old recording format no longer supported. Only frame capture recordings are available.'
|
|
1203
|
-
};
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
// Handle frame capture recordings
|
|
1207
|
-
if (recording.type === 'frame_capture') {
|
|
1208
|
-
return {
|
|
1209
|
-
success: true,
|
|
1210
|
-
recording: {
|
|
1211
|
-
id: recording.sessionId,
|
|
1212
|
-
frames: recording.frames,
|
|
1213
|
-
totalFrames: recording.frames.length,
|
|
1214
|
-
timestamp: recording.timestamp,
|
|
1215
|
-
timestamp: recording.timestamp,
|
|
1216
|
-
filename: recording.filename
|
|
1217
|
-
}
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
return { error: true, message: 'Unknown recording format' };
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// Delete recording
|
|
1225
|
-
async deleteRecording(recordingId) {
|
|
1226
|
-
const recording = await sharedStorage.get(recordingId);
|
|
1227
|
-
if (!recording) {
|
|
1228
|
-
return { error: true, message: 'Recording not found' };
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
await sharedStorage.delete(recordingId);
|
|
1232
|
-
console.log(`Deleted recording ${recordingId}`);
|
|
1233
|
-
|
|
1234
|
-
return {
|
|
1235
|
-
success: true,
|
|
1236
|
-
message: 'Recording deleted successfully'
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
// Store frame batch
|
|
1241
|
-
async storeFrameBatch(sessionId, frames) {
|
|
1242
|
-
console.log(`[storeFrameBatch] Received sessionId: ${sessionId}, and a batch of ${frames ? frames.length : 'undefined'} frames.`);
|
|
1243
|
-
|
|
1244
|
-
if (!sessionId || !Array.isArray(frames)) {
|
|
1245
|
-
console.error('[storeFrameBatch] Invalid arguments received.');
|
|
1246
|
-
throw new Error('Invalid arguments: sessionId and frames array are required.');
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
try {
|
|
1250
|
-
console.log(`Storing ${frames.length} frames for session ${sessionId}`);
|
|
1251
|
-
|
|
1252
|
-
// Store frames in shared storage
|
|
1253
|
-
const result = await sharedStorage.storeFrameBatch(sessionId, frames);
|
|
1254
|
-
console.log('Storage result:', result ? 'success' : 'failed');
|
|
1255
|
-
|
|
1256
|
-
return {
|
|
1257
|
-
success: true,
|
|
1258
|
-
sessionId: sessionId,
|
|
1259
|
-
framesStored: frames.length
|
|
1260
|
-
};
|
|
1261
|
-
} catch (error) {
|
|
1262
|
-
console.error('Error storing frame batch:', error);
|
|
1263
|
-
throw error; // Re-throw to be caught by route handler
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
async checkFrameSession(sessionId) {
|
|
1268
|
-
try {
|
|
1269
|
-
const sessionInfo = await sharedStorage.getFrameSessionInfo(sessionId);
|
|
1270
|
-
if (sessionInfo && !sessionInfo.error) {
|
|
1271
|
-
return {
|
|
1272
|
-
found: true,
|
|
1273
|
-
sessionId: sessionId,
|
|
1274
|
-
totalFrames: sessionInfo.totalFrames,
|
|
1275
|
-
duration: sessionInfo.duration,
|
|
1276
|
-
serverPort: process.env.PORT || 3000
|
|
1277
|
-
};
|
|
1278
|
-
} else {
|
|
1279
|
-
return { found: false };
|
|
1280
|
-
}
|
|
1281
|
-
} catch (error) {
|
|
1282
|
-
console.error('Error checking frame session:', error);
|
|
1283
|
-
return { found: false, error: error.message };
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
async associateLogsWithFrames(sessionId, logs) {
|
|
1288
|
-
try {
|
|
1289
|
-
console.log(`Associating ${logs.length} logs with frames for session ${sessionId}`);
|
|
1290
|
-
|
|
1291
|
-
// Use the database's built-in log association method
|
|
1292
|
-
const result = await sharedStorage.associateLogsWithFrames(sessionId, logs);
|
|
1293
|
-
|
|
1294
|
-
return result;
|
|
1295
|
-
} catch (error) {
|
|
1296
|
-
console.error('Error associating logs:', error);
|
|
1297
|
-
return { error: true, message: error.message };
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// Get frame session info
|
|
1302
|
-
async getFrameSessionInfo(sessionId) {
|
|
1303
|
-
return await sharedStorage.getFrameSessionInfo(sessionId);
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
// Get specific frame
|
|
1307
|
-
async getFrame(sessionId, frameIndex) {
|
|
1308
|
-
return await sharedStorage.getFrame(sessionId, frameIndex);
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
// Get chunked recording session info (legacy - no longer supported)
|
|
1312
|
-
async getRecordingInfo(sessionId) {
|
|
1313
|
-
return { error: true, message: 'Chunked recordings are no longer supported. Use get_frame_session_info for frame capture recordings.' };
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
// Get specific chunk from recording session (legacy - no longer supported)
|
|
1317
|
-
async getRecordingChunk(sessionId, chunkIndex) {
|
|
1318
|
-
return { error: true, message: 'Chunked recordings are no longer supported. Use get_frame for frame capture recordings.' };
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
// Get entire frame session
|
|
1322
|
-
async getFrameSession(sessionId) {
|
|
1323
|
-
return await sharedStorage.getFrameSession(sessionId);
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
// Save edited frame session
|
|
1327
|
-
async saveEditedSession(sessionId, sessionData) {
|
|
1328
|
-
try {
|
|
1329
|
-
await sharedStorage.ensureInitialized();
|
|
1330
|
-
|
|
1331
|
-
if (!sessionData.frames || !Array.isArray(sessionData.frames)) {
|
|
1332
|
-
return { error: true, message: 'Invalid session data: frames array required' };
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// Store the frames using the database
|
|
1336
|
-
const result = await sharedStorage.storeFrameBatch(sessionId, sessionData.frames);
|
|
1337
|
-
|
|
1338
|
-
console.log(`Saved edited session ${sessionId} with ${sessionData.frames.length} frames`);
|
|
1339
|
-
|
|
1340
|
-
return {
|
|
1341
|
-
success: true,
|
|
1342
|
-
sessionId: sessionId,
|
|
1343
|
-
totalFrames: sessionData.frames.length
|
|
1344
|
-
};
|
|
1345
|
-
} catch (error) {
|
|
1346
|
-
console.error('Error saving edited session:', error);
|
|
1347
|
-
return { error: true, message: error.message };
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// Import session from Chrome extension storage format
|
|
1352
|
-
async importSessionFromChrome(sessionId, sessionData) {
|
|
1353
|
-
try {
|
|
1354
|
-
await sharedStorage.ensureInitialized();
|
|
1355
|
-
|
|
1356
|
-
if (!sessionData.frames || !Array.isArray(sessionData.frames)) {
|
|
1357
|
-
return { error: true, message: 'Invalid session data: frames array required' };
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
// Store the frames using the database
|
|
1361
|
-
const result = await sharedStorage.storeFrameBatch(sessionId, sessionData.frames);
|
|
1362
|
-
|
|
1363
|
-
console.log(`Imported session ${sessionId} with ${sessionData.frames.length} frames`);
|
|
1364
|
-
|
|
1365
|
-
return {
|
|
1366
|
-
success: true,
|
|
1367
|
-
sessionId: sessionId,
|
|
1368
|
-
totalFrames: sessionData.frames.length,
|
|
1369
|
-
message: 'Session imported successfully'
|
|
1370
|
-
};
|
|
1371
|
-
} catch (error) {
|
|
1372
|
-
console.error('Error importing session:', error);
|
|
1373
|
-
return { error: true, message: error.message };
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Heartbeat methods for connection monitoring
|
|
1378
|
-
startHeartbeat() {
|
|
1379
|
-
this.stopHeartbeat(); // Clear any existing interval
|
|
1380
|
-
|
|
1381
|
-
this.heartbeatInterval = setInterval(async () => {
|
|
1382
|
-
try {
|
|
1383
|
-
if (this.browser && !await this.isConnected()) {
|
|
1384
|
-
console.log('Heartbeat detected disconnection, attempting to reconnect...');
|
|
1385
|
-
if (this.debugPort) {
|
|
1386
|
-
try {
|
|
1387
|
-
await this.connectToExisting(this.debugPort);
|
|
1388
|
-
console.log('Heartbeat reconnection successful');
|
|
1389
|
-
} catch (error) {
|
|
1390
|
-
console.error('Heartbeat reconnection failed:', error.message);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
} catch (error) {
|
|
1395
|
-
console.error('Heartbeat error:', error);
|
|
1396
|
-
}
|
|
1397
|
-
}, 5000); // Check every 5 seconds
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
stopHeartbeat() {
|
|
1401
|
-
if (this.heartbeatInterval) {
|
|
1402
|
-
clearInterval(this.heartbeatInterval);
|
|
1403
|
-
this.heartbeatInterval = null;
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
}
|