@dynamicu/chromedebug-mcp 2.2.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 +344 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/background.js +3917 -0
- package/chrome-extension/chrome-session-manager.js +706 -0
- package/chrome-extension/content.css +181 -0
- package/chrome-extension/content.js +3022 -0
- package/chrome-extension/data-buffer.js +435 -0
- package/chrome-extension/dom-tracker.js +411 -0
- package/chrome-extension/extension-config.js +78 -0
- package/chrome-extension/firebase-client.js +278 -0
- package/chrome-extension/firebase-config.js +32 -0
- package/chrome-extension/firebase-config.module.js +22 -0
- package/chrome-extension/firebase-config.module.template.js +27 -0
- package/chrome-extension/firebase-config.template.js +36 -0
- package/chrome-extension/frame-capture.js +407 -0
- package/chrome-extension/icon128.png +1 -0
- package/chrome-extension/icon16.png +1 -0
- package/chrome-extension/icon48.png +1 -0
- package/chrome-extension/license-helper.js +181 -0
- package/chrome-extension/logger.js +23 -0
- package/chrome-extension/manifest.json +73 -0
- package/chrome-extension/network-tracker.js +510 -0
- package/chrome-extension/offscreen.html +10 -0
- package/chrome-extension/options.html +203 -0
- package/chrome-extension/options.js +282 -0
- package/chrome-extension/pako.min.js +2 -0
- package/chrome-extension/performance-monitor.js +533 -0
- package/chrome-extension/pii-redactor.js +405 -0
- package/chrome-extension/popup.html +532 -0
- package/chrome-extension/popup.js +2446 -0
- package/chrome-extension/upload-manager.js +323 -0
- package/chrome-extension/web-vitals.iife.js +1 -0
- package/config/api-keys.json +11 -0
- package/config/chrome-pilot-config.json +45 -0
- package/package.json +126 -0
- package/scripts/cleanup-processes.js +109 -0
- package/scripts/config-manager.js +280 -0
- package/scripts/generate-extension-config.js +53 -0
- package/scripts/setup-security.js +64 -0
- package/src/capture/architecture.js +426 -0
- package/src/capture/error-handling-tests.md +38 -0
- package/src/capture/error-handling-types.ts +360 -0
- package/src/capture/index.js +508 -0
- package/src/capture/interfaces.js +625 -0
- package/src/capture/memory-manager.js +713 -0
- package/src/capture/types.js +342 -0
- package/src/chrome-controller.js +2658 -0
- package/src/cli.js +19 -0
- package/src/config-loader.js +303 -0
- package/src/database.js +2178 -0
- package/src/firebase-license-manager.js +462 -0
- package/src/firebase-privacy-guard.js +397 -0
- package/src/http-server.js +1516 -0
- package/src/index-direct.js +157 -0
- package/src/index-modular.js +219 -0
- package/src/index-monolithic-backup.js +2230 -0
- package/src/index.js +305 -0
- package/src/legacy/chrome-controller-old.js +1406 -0
- package/src/legacy/index-express.js +625 -0
- package/src/legacy/index-old.js +977 -0
- package/src/legacy/routes.js +260 -0
- package/src/legacy/shared-storage.js +101 -0
- package/src/logger.js +10 -0
- package/src/mcp/handlers/chrome-tool-handler.js +306 -0
- package/src/mcp/handlers/element-tool-handler.js +51 -0
- package/src/mcp/handlers/frame-tool-handler.js +957 -0
- package/src/mcp/handlers/request-handler.js +104 -0
- package/src/mcp/handlers/workflow-tool-handler.js +636 -0
- package/src/mcp/server.js +68 -0
- package/src/mcp/tools/index.js +701 -0
- package/src/middleware/auth.js +371 -0
- package/src/middleware/security.js +267 -0
- package/src/port-discovery.js +258 -0
- package/src/routes/admin.js +182 -0
- package/src/services/browser-daemon.js +494 -0
- package/src/services/chrome-service.js +375 -0
- package/src/services/failover-manager.js +412 -0
- package/src/services/git-safety-service.js +675 -0
- package/src/services/heartbeat-manager.js +200 -0
- package/src/services/http-client.js +195 -0
- package/src/services/process-manager.js +318 -0
- package/src/services/process-tracker.js +574 -0
- package/src/services/profile-manager.js +449 -0
- package/src/services/project-manager.js +415 -0
- package/src/services/session-manager.js +497 -0
- package/src/services/session-registry.js +491 -0
- package/src/services/unified-session-manager.js +678 -0
- package/src/shared-storage-old.js +267 -0
- package/src/standalone-server.js +53 -0
- package/src/utils/extension-path.js +145 -0
- package/src/utils.js +187 -0
- package/src/validation/log-transformer.js +125 -0
- package/src/validation/schemas.js +391 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
export function createRoutes(chromeController) {
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
|
|
6
|
+
const asyncHandler = (fn) => (req, res, next) => {
|
|
7
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
router.post('/chrome-pilot/launch', asyncHandler(async (req, res) => {
|
|
11
|
+
const result = await chromeController.launch();
|
|
12
|
+
res.json(result);
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
router.post('/chrome-pilot/navigate', asyncHandler(async (req, res) => {
|
|
16
|
+
const { url } = req.body;
|
|
17
|
+
if (!url) {
|
|
18
|
+
return res.status(400).json({ error: 'URL is required' });
|
|
19
|
+
}
|
|
20
|
+
const result = await chromeController.navigateTo(url);
|
|
21
|
+
res.json(result);
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
router.post('/chrome-pilot/pause', asyncHandler(async (req, res) => {
|
|
25
|
+
const result = await chromeController.pause();
|
|
26
|
+
res.json(result);
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
router.post('/chrome-pilot/resume', asyncHandler(async (req, res) => {
|
|
30
|
+
const result = await chromeController.resume();
|
|
31
|
+
res.json(result);
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
router.post('/chrome-pilot/step-over', asyncHandler(async (req, res) => {
|
|
35
|
+
const result = await chromeController.stepOver();
|
|
36
|
+
res.json(result);
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
router.post('/chrome-pilot/evaluate', asyncHandler(async (req, res) => {
|
|
40
|
+
const { expression } = req.body;
|
|
41
|
+
if (!expression) {
|
|
42
|
+
return res.status(400).json({ error: 'Expression is required' });
|
|
43
|
+
}
|
|
44
|
+
const result = await chromeController.evaluate(expression);
|
|
45
|
+
res.json(result);
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
router.get('/chrome-pilot/scopes', asyncHandler(async (req, res) => {
|
|
49
|
+
const result = await chromeController.getScopes();
|
|
50
|
+
res.json(result);
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
router.post('/chrome-pilot/set-breakpoint', asyncHandler(async (req, res) => {
|
|
54
|
+
const { url, lineNumber } = req.body;
|
|
55
|
+
if (!url || lineNumber === undefined) {
|
|
56
|
+
return res.status(400).json({ error: 'URL and lineNumber are required' });
|
|
57
|
+
}
|
|
58
|
+
const result = await chromeController.setBreakpoint(url, lineNumber);
|
|
59
|
+
res.json(result);
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
router.get('/chrome-pilot/logs', asyncHandler(async (req, res) => {
|
|
63
|
+
const result = chromeController.getLogs();
|
|
64
|
+
res.json(result);
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
router.post('/chrome-pilot/screenshot', asyncHandler(async (req, res) => {
|
|
68
|
+
const { type, fullPage, path } = req.body || {};
|
|
69
|
+
const options = {};
|
|
70
|
+
if (type) options.type = type;
|
|
71
|
+
if (fullPage !== undefined) options.fullPage = fullPage;
|
|
72
|
+
if (path) options.path = path;
|
|
73
|
+
|
|
74
|
+
const result = await chromeController.takeScreenshot(options);
|
|
75
|
+
res.json(result);
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
router.post('/chrome-pilot/dom-intent', asyncHandler(async (req, res) => {
|
|
79
|
+
const { selector, instruction, elementInfo } = req.body;
|
|
80
|
+
if (!selector) {
|
|
81
|
+
return res.status(400).json({ error: 'Selector is required' });
|
|
82
|
+
}
|
|
83
|
+
const result = await chromeController.selectElement(selector, instruction, elementInfo);
|
|
84
|
+
res.json(result);
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
router.get('/chrome-pilot/status', asyncHandler(async (req, res) => {
|
|
88
|
+
const isConnected = await chromeController.isConnected();
|
|
89
|
+
res.json({ connected: isConnected, status: 'ok' });
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
// Port discovery endpoint for Claude to find the server
|
|
93
|
+
router.get('/chrome-pilot/port', (req, res) => {
|
|
94
|
+
const serverPort = process.env.SERVER_PORT || process.env.PORT || 3000;
|
|
95
|
+
res.json({
|
|
96
|
+
port: serverPort,
|
|
97
|
+
message: 'Chrome Debug server port',
|
|
98
|
+
timestamp: new Date().toISOString()
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
router.get('/chrome-pilot/recording/:id', asyncHandler(async (req, res) => {
|
|
103
|
+
// Get recording for Claude to analyze
|
|
104
|
+
const result = await chromeController.getRecording(req.params.id);
|
|
105
|
+
if (result.error) {
|
|
106
|
+
return res.status(404).json(result);
|
|
107
|
+
}
|
|
108
|
+
res.json(result);
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
router.delete('/chrome-pilot/recording/:id', asyncHandler(async (req, res) => {
|
|
112
|
+
// Delete recording
|
|
113
|
+
const result = await chromeController.deleteRecording(req.params.id);
|
|
114
|
+
if (result.error) {
|
|
115
|
+
return res.status(404).json(result);
|
|
116
|
+
}
|
|
117
|
+
res.json(result);
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
// New frame capture endpoints
|
|
121
|
+
router.post('/chrome-pilot/frame-batch', (req, res, next) => {
|
|
122
|
+
// Use multer middleware for this specific route
|
|
123
|
+
chromeController.upload(req, res, async (err) => {
|
|
124
|
+
if (err) {
|
|
125
|
+
console.error('Multer error:', err);
|
|
126
|
+
return res.status(400).json({ error: true, message: err.message });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Extract data from req.body (populated by multer)
|
|
131
|
+
const { sessionId, frames } = req.body;
|
|
132
|
+
|
|
133
|
+
console.log('Route received sessionId:', sessionId);
|
|
134
|
+
console.log('Route received frames (type):', typeof frames);
|
|
135
|
+
|
|
136
|
+
if (!sessionId || !frames) {
|
|
137
|
+
return res.status(400).json({ error: 'Missing sessionId or frames in request body' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Parse the stringified frames and call the controller method correctly
|
|
141
|
+
const parsedFrames = JSON.parse(frames);
|
|
142
|
+
console.log('Parsed frames count:', parsedFrames.length);
|
|
143
|
+
|
|
144
|
+
const result = await chromeController.storeFrameBatch(sessionId, parsedFrames);
|
|
145
|
+
res.json(result);
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('Error processing frame batch:', error);
|
|
149
|
+
res.status(500).json({ error: 'Failed to store frame batch', details: error.message });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
router.get('/chrome-pilot/check-session/:sessionId', asyncHandler(async (req, res) => {
|
|
155
|
+
const { sessionId } = req.params;
|
|
156
|
+
const result = await chromeController.checkFrameSession(sessionId);
|
|
157
|
+
if (result.found) {
|
|
158
|
+
return res.status(200).json({ status: 'found', sessionId, ...result });
|
|
159
|
+
} else {
|
|
160
|
+
return res.status(404).json({ status: 'not_found', sessionId });
|
|
161
|
+
}
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
router.post('/chrome-pilot/associate-logs', asyncHandler(async (req, res) => {
|
|
165
|
+
const { sessionId, logs } = req.body;
|
|
166
|
+
if (!sessionId || !logs) {
|
|
167
|
+
return res.status(400).json({ error: 'sessionId and logs are required' });
|
|
168
|
+
}
|
|
169
|
+
const result = await chromeController.associateLogsWithFrames(sessionId, logs);
|
|
170
|
+
res.json(result);
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
router.post('/chrome-pilot/save-edited-session', asyncHandler(async (req, res) => {
|
|
174
|
+
const { sessionId, sessionData } = req.body;
|
|
175
|
+
if (!sessionId || !sessionData) {
|
|
176
|
+
return res.status(400).json({ error: 'sessionId and sessionData are required' });
|
|
177
|
+
}
|
|
178
|
+
const result = await chromeController.saveEditedSession(sessionId, sessionData);
|
|
179
|
+
res.json(result);
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
router.get('/chrome-pilot/session/:sessionId', asyncHandler(async (req, res) => {
|
|
183
|
+
const { sessionId } = req.params;
|
|
184
|
+
const result = await chromeController.getFrameSession(sessionId);
|
|
185
|
+
if (result) {
|
|
186
|
+
res.json(result);
|
|
187
|
+
} else {
|
|
188
|
+
res.status(404).json({ error: 'Session not found' });
|
|
189
|
+
}
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
// Import session from Chrome extension storage format
|
|
193
|
+
router.post('/chrome-pilot/import-session', asyncHandler(async (req, res) => {
|
|
194
|
+
const { sessionId, sessionData } = req.body;
|
|
195
|
+
if (!sessionId || !sessionData) {
|
|
196
|
+
return res.status(400).json({ error: 'sessionId and sessionData are required' });
|
|
197
|
+
}
|
|
198
|
+
const result = await chromeController.importSessionFromChrome(sessionId, sessionData);
|
|
199
|
+
res.json(result);
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
// Workflow recording endpoints
|
|
203
|
+
router.post('/chrome-pilot/workflow-recording', asyncHandler(async (req, res) => {
|
|
204
|
+
const { sessionId, url, title, includeLogs, actions, logs } = req.body;
|
|
205
|
+
if (!sessionId || !actions) {
|
|
206
|
+
return res.status(400).json({ error: 'sessionId and actions are required' });
|
|
207
|
+
}
|
|
208
|
+
const result = await chromeController.storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs);
|
|
209
|
+
res.json(result);
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
router.get('/chrome-pilot/workflow-recording/:sessionId', asyncHandler(async (req, res) => {
|
|
213
|
+
const { sessionId } = req.params;
|
|
214
|
+
const result = await chromeController.getWorkflowRecording(sessionId);
|
|
215
|
+
if (result.error) {
|
|
216
|
+
return res.status(404).json(result);
|
|
217
|
+
}
|
|
218
|
+
res.json(result);
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
router.get('/chrome-pilot/workflow-recordings', asyncHandler(async (req, res) => {
|
|
222
|
+
const result = await chromeController.listWorkflowRecordings();
|
|
223
|
+
res.json(result);
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
// Restore point endpoints
|
|
227
|
+
router.post('/chrome-pilot/restore-point', asyncHandler(async (req, res) => {
|
|
228
|
+
const result = await chromeController.saveRestorePoint(req.body);
|
|
229
|
+
res.json(result);
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
router.get('/chrome-pilot/restore-point/:id', asyncHandler(async (req, res) => {
|
|
233
|
+
const result = await chromeController.getRestorePoint(req.params.id);
|
|
234
|
+
if (result.error) {
|
|
235
|
+
res.status(404).json(result);
|
|
236
|
+
} else {
|
|
237
|
+
res.json(result);
|
|
238
|
+
}
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
router.get('/chrome-pilot/restore-points/:workflowId', asyncHandler(async (req, res) => {
|
|
242
|
+
const result = await chromeController.listRestorePoints(req.params.workflowId);
|
|
243
|
+
res.json(result);
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
router.delete('/chrome-pilot/restore-point/:id', asyncHandler(async (req, res) => {
|
|
247
|
+
const result = await chromeController.deleteRestorePoint(req.params.id);
|
|
248
|
+
res.json(result);
|
|
249
|
+
}));
|
|
250
|
+
|
|
251
|
+
router.use((error, req, res, next) => {
|
|
252
|
+
console.error('Error:', error);
|
|
253
|
+
res.status(500).json({
|
|
254
|
+
error: true,
|
|
255
|
+
message: error.message || 'Internal server error'
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return router;
|
|
260
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// New shared storage implementation using SQLite database
|
|
2
|
+
import { database } from './database.js';
|
|
3
|
+
|
|
4
|
+
class SharedRecordingStorage {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.initialized = false;
|
|
7
|
+
this.initPromise = this.init();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async init() {
|
|
11
|
+
database.init();
|
|
12
|
+
this.initialized = true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async ensureInitialized() {
|
|
16
|
+
if (!this.initialized) {
|
|
17
|
+
await this.initPromise;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Legacy method - no longer needed with SQLite
|
|
22
|
+
async loadFromDisk() {
|
|
23
|
+
// SQLite handles persistence automatically
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Legacy method - no longer needed with SQLite
|
|
27
|
+
async saveToDisk() {
|
|
28
|
+
// SQLite handles persistence automatically
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async get(id) {
|
|
32
|
+
await this.ensureInitialized();
|
|
33
|
+
return database.getRecording(id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async delete(id) {
|
|
37
|
+
await this.ensureInitialized();
|
|
38
|
+
return database.deleteRecording(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async list() {
|
|
42
|
+
await this.ensureInitialized();
|
|
43
|
+
return database.listRecordings();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Store frame batch for frame capture sessions
|
|
47
|
+
async storeFrameBatch(sessionId, frames) {
|
|
48
|
+
await this.ensureInitialized();
|
|
49
|
+
return database.storeFrameBatch(sessionId, frames);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get frame session info
|
|
53
|
+
async getFrameSessionInfo(sessionId) {
|
|
54
|
+
await this.ensureInitialized();
|
|
55
|
+
return database.getFrameSessionInfo(sessionId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get specific frame from session
|
|
59
|
+
async getFrame(sessionId, frameIndex) {
|
|
60
|
+
await this.ensureInitialized();
|
|
61
|
+
return database.getFrame(sessionId, frameIndex);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get entire frame session
|
|
65
|
+
async getFrameSession(sessionId) {
|
|
66
|
+
await this.ensureInitialized();
|
|
67
|
+
return database.getFrameSession(sessionId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Update frame session - for SQLite this is handled by associateLogsWithFrames
|
|
71
|
+
async updateFrameSession(sessionId, updatedSession) {
|
|
72
|
+
await this.ensureInitialized();
|
|
73
|
+
|
|
74
|
+
// With SQLite, we don't update the entire session at once
|
|
75
|
+
// Instead, we use the database's structured approach
|
|
76
|
+
// This method is kept for backward compatibility
|
|
77
|
+
|
|
78
|
+
if (updatedSession.frames) {
|
|
79
|
+
// Store the frames
|
|
80
|
+
const result = database.storeFrameBatch(sessionId, updatedSession.frames);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error('Frame session update not supported in SQLite mode');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Associate logs with frames
|
|
88
|
+
async associateLogsWithFrames(sessionId, logs) {
|
|
89
|
+
await this.ensureInitialized();
|
|
90
|
+
return database.associateLogsWithFrames(sessionId, logs);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get database statistics
|
|
94
|
+
async getStats() {
|
|
95
|
+
await this.ensureInitialized();
|
|
96
|
+
return database.getStats();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Export singleton instance
|
|
101
|
+
export const sharedStorage = new SharedRecordingStorage();
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Simple conditional logger for Chrome Debug
|
|
2
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
log: (...args) => { if (isDev) console.log(...args); },
|
|
6
|
+
error: (...args) => console.error(...args),
|
|
7
|
+
warn: (...args) => { if (isDev) console.warn(...args); },
|
|
8
|
+
info: (...args) => { if (isDev) console.info(...args); },
|
|
9
|
+
debug: (...args) => { if (isDev) console.debug(...args); }
|
|
10
|
+
};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome Tool Handler - Handles Chrome browser control tools
|
|
3
|
+
* Extracted from original index.js with preserved functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class ChromeToolHandler {
|
|
7
|
+
constructor(chromeController) {
|
|
8
|
+
this.chromeController = chromeController;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handles Chrome-related tool calls with session-aware error recovery
|
|
13
|
+
* @param {string} name - Tool name
|
|
14
|
+
* @param {Object} args - Tool arguments
|
|
15
|
+
* @returns {Object} Tool execution result
|
|
16
|
+
*/
|
|
17
|
+
async handle(name, args) {
|
|
18
|
+
// Critical operations that need session isolation error recovery
|
|
19
|
+
const criticalOps = ['launch_chrome', 'connect_to_existing_chrome', 'force_reset'];
|
|
20
|
+
|
|
21
|
+
if (criticalOps.includes(name)) {
|
|
22
|
+
return await this.handleWithSessionRecovery(name, args);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
switch (name) {
|
|
26
|
+
case 'launch_chrome':
|
|
27
|
+
return await this.chromeController.launch();
|
|
28
|
+
|
|
29
|
+
case 'connect_to_existing_chrome':
|
|
30
|
+
const port = args.port || 9222;
|
|
31
|
+
return await this.chromeController.connectToExisting(port);
|
|
32
|
+
|
|
33
|
+
case 'navigate_to':
|
|
34
|
+
return await this.chromeController.navigateTo(args.url);
|
|
35
|
+
|
|
36
|
+
case 'pause_execution':
|
|
37
|
+
return await this.chromeController.pause();
|
|
38
|
+
|
|
39
|
+
case 'resume_execution':
|
|
40
|
+
return await this.chromeController.resume();
|
|
41
|
+
|
|
42
|
+
case 'step_over':
|
|
43
|
+
return await this.chromeController.stepOver();
|
|
44
|
+
|
|
45
|
+
case 'evaluate_expression':
|
|
46
|
+
return await this.chromeController.evaluate(args.expression);
|
|
47
|
+
|
|
48
|
+
case 'get_scopes':
|
|
49
|
+
return await this.chromeController.getScopes();
|
|
50
|
+
|
|
51
|
+
case 'set_breakpoint':
|
|
52
|
+
return await this.chromeController.setBreakpoint(args.url, args.lineNumber);
|
|
53
|
+
|
|
54
|
+
case 'get_logs':
|
|
55
|
+
return this.chromeController.getLogs();
|
|
56
|
+
|
|
57
|
+
case 'check_connection':
|
|
58
|
+
return await this.handleCheckConnection();
|
|
59
|
+
|
|
60
|
+
case 'force_reset':
|
|
61
|
+
return await this.chromeController.forceReset();
|
|
62
|
+
|
|
63
|
+
case 'take_screenshot':
|
|
64
|
+
return await this.handleTakeScreenshot(args);
|
|
65
|
+
|
|
66
|
+
case 'get_page_content':
|
|
67
|
+
return await this.handleGetPageContent(args);
|
|
68
|
+
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(`Unknown Chrome tool: ${name}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handle check connection tool
|
|
76
|
+
* @returns {Object} Connection status
|
|
77
|
+
*/
|
|
78
|
+
async handleCheckConnection() {
|
|
79
|
+
const status = await this.chromeController.getConnectionStatus();
|
|
80
|
+
return {
|
|
81
|
+
connected: status.connected,
|
|
82
|
+
debugPort: status.debugPort,
|
|
83
|
+
wsEndpoint: status.browserWSEndpoint
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Handle take screenshot tool with custom formatting
|
|
89
|
+
* @param {Object} args - Screenshot arguments
|
|
90
|
+
* @returns {Object} Screenshot result
|
|
91
|
+
*/
|
|
92
|
+
async handleTakeScreenshot(args) {
|
|
93
|
+
const result = await this.chromeController.takeScreenshot(args);
|
|
94
|
+
|
|
95
|
+
if (result.saved) {
|
|
96
|
+
return {
|
|
97
|
+
message: `Screenshot saved successfully!`,
|
|
98
|
+
path: result.path,
|
|
99
|
+
type: result.type,
|
|
100
|
+
fullPage: result.fullPage
|
|
101
|
+
};
|
|
102
|
+
} else if (result.truncated) {
|
|
103
|
+
return {
|
|
104
|
+
message: `Screenshot preview (truncated)`,
|
|
105
|
+
size: result.size,
|
|
106
|
+
type: result.type,
|
|
107
|
+
fullPage: result.fullPage,
|
|
108
|
+
note: result.message
|
|
109
|
+
};
|
|
110
|
+
} else if (result.error) {
|
|
111
|
+
throw new Error(`Error taking screenshot: ${result.message}`);
|
|
112
|
+
} else if (result.lowRes) {
|
|
113
|
+
return {
|
|
114
|
+
message: `Low-resolution screenshot captured for AI parsing`,
|
|
115
|
+
size: result.size,
|
|
116
|
+
type: result.type,
|
|
117
|
+
fullPage: result.fullPage,
|
|
118
|
+
quality: result.quality || 'N/A',
|
|
119
|
+
data: `data:image/${result.type};base64,${result.screenshot}`
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
return {
|
|
123
|
+
message: `Screenshot captured!`,
|
|
124
|
+
size: result.size,
|
|
125
|
+
type: result.type,
|
|
126
|
+
fullPage: result.fullPage,
|
|
127
|
+
preview: result.screenshot.substring(0, 100) + '...',
|
|
128
|
+
note: 'Full screenshot is too large to display. Use lowRes: true for AI-parseable screenshots or path to save to file.'
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Handle get page content tool with formatted output
|
|
135
|
+
* @param {Object} args - Page content arguments
|
|
136
|
+
* @returns {Object} Page content result
|
|
137
|
+
*/
|
|
138
|
+
async handleGetPageContent(args) {
|
|
139
|
+
const result = await this.chromeController.getPageContent(args);
|
|
140
|
+
|
|
141
|
+
if (result.error) {
|
|
142
|
+
throw new Error(`Error getting page content: ${result.message}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const formattedResult = {
|
|
146
|
+
url: result.url,
|
|
147
|
+
title: result.title,
|
|
148
|
+
meta: result.meta,
|
|
149
|
+
statistics: {
|
|
150
|
+
forms: result.statistics.forms,
|
|
151
|
+
images: result.statistics.images,
|
|
152
|
+
links: result.statistics.links,
|
|
153
|
+
scripts: result.statistics.scripts
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (result.text !== undefined) {
|
|
158
|
+
formattedResult.text = result.text;
|
|
159
|
+
formattedResult.textLength = result.textLength;
|
|
160
|
+
if (result.textLength > 1000) {
|
|
161
|
+
formattedResult.textPreview = result.text.substring(0, 1000) + '...';
|
|
162
|
+
formattedResult.note = `Showing first 1000 of ${result.textLength} characters`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (result.structure !== undefined) {
|
|
167
|
+
formattedResult.structure = result.structure;
|
|
168
|
+
// Format structure for better readability
|
|
169
|
+
formattedResult.structureFormatted = this.formatDOMStructure(result.structure);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (result.html !== undefined) {
|
|
173
|
+
formattedResult.htmlLength = result.html.length;
|
|
174
|
+
if (result.html.length > 2000) {
|
|
175
|
+
formattedResult.htmlPreview = result.html.substring(0, 2000) + '...';
|
|
176
|
+
formattedResult.htmlNote = `Showing first 2000 of ${result.html.length} characters`;
|
|
177
|
+
} else {
|
|
178
|
+
formattedResult.html = result.html;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return formattedResult;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Format DOM structure for readable output
|
|
187
|
+
* @param {Object} node - DOM node structure
|
|
188
|
+
* @param {string} indent - Indentation level
|
|
189
|
+
* @returns {string} Formatted structure string
|
|
190
|
+
*/
|
|
191
|
+
formatDOMStructure(node, indent = '') {
|
|
192
|
+
if (!node) return '';
|
|
193
|
+
|
|
194
|
+
let output = `${indent}<${node.tag}`;
|
|
195
|
+
|
|
196
|
+
if (node.id) output += ` id="${node.id}"`;
|
|
197
|
+
if (node.class) output += ` class="${node.class}"`;
|
|
198
|
+
if (node.href) output += ` href="${node.href}"`;
|
|
199
|
+
if (node.src) output += ` src="${node.src}"`;
|
|
200
|
+
if (node.type) output += ` type="${node.type}"`;
|
|
201
|
+
|
|
202
|
+
output += '>';
|
|
203
|
+
|
|
204
|
+
if (node.text && node.text.trim()) {
|
|
205
|
+
output += ` ${node.text.trim()}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
output += '\n';
|
|
209
|
+
|
|
210
|
+
if (node.children && node.children.length > 0) {
|
|
211
|
+
for (const child of node.children) {
|
|
212
|
+
output += this.formatDOMStructure(child, indent + ' ');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return output;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Handles critical operations with session-aware error recovery
|
|
221
|
+
* Provides retry logic for SingletonLock and other session conflicts
|
|
222
|
+
* @param {string} name - Tool name
|
|
223
|
+
* @param {Object} args - Tool arguments
|
|
224
|
+
* @returns {Object} Tool execution result
|
|
225
|
+
*/
|
|
226
|
+
async handleWithSessionRecovery(name, args) {
|
|
227
|
+
const maxRetries = 3;
|
|
228
|
+
let lastError = null;
|
|
229
|
+
|
|
230
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
231
|
+
try {
|
|
232
|
+
console.log(`[ChromeToolHandler] Attempt ${attempt}/${maxRetries} for ${name} (Session: ${this.chromeController.sessionId})`);
|
|
233
|
+
|
|
234
|
+
// Execute the operation based on name
|
|
235
|
+
switch (name) {
|
|
236
|
+
case 'launch_chrome':
|
|
237
|
+
return await this.chromeController.launch();
|
|
238
|
+
|
|
239
|
+
case 'connect_to_existing_chrome':
|
|
240
|
+
const port = args.port || 9222;
|
|
241
|
+
return await this.chromeController.connectToExisting(port);
|
|
242
|
+
|
|
243
|
+
case 'force_reset':
|
|
244
|
+
return await this.chromeController.forceReset();
|
|
245
|
+
|
|
246
|
+
default:
|
|
247
|
+
throw new Error(`Unknown critical operation: ${name}`);
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
lastError = error;
|
|
251
|
+
console.error(`[ChromeToolHandler] Attempt ${attempt} failed for ${name}:`, error.message);
|
|
252
|
+
|
|
253
|
+
// Check for known session conflict errors
|
|
254
|
+
const isSessionConflict = this.isSessionConflictError(error);
|
|
255
|
+
|
|
256
|
+
if (isSessionConflict && attempt < maxRetries) {
|
|
257
|
+
console.log(`[ChromeToolHandler] Detected session conflict, attempting recovery...`);
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
// Force cleanup current session and create new one
|
|
261
|
+
await this.chromeController.forceReset();
|
|
262
|
+
|
|
263
|
+
// Wait a bit before retrying to let system stabilize
|
|
264
|
+
await new Promise(resolve => setTimeout(resolve, 1000 + (attempt * 500)));
|
|
265
|
+
|
|
266
|
+
console.log(`[ChromeToolHandler] Session recovery completed, retrying...`);
|
|
267
|
+
} catch (recoveryError) {
|
|
268
|
+
console.error(`[ChromeToolHandler] Session recovery failed:`, recoveryError.message);
|
|
269
|
+
}
|
|
270
|
+
} else if (attempt === maxRetries) {
|
|
271
|
+
// Last attempt failed
|
|
272
|
+
console.error(`[ChromeToolHandler] All ${maxRetries} attempts failed for ${name}`);
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Return detailed error information
|
|
279
|
+
throw new Error(`${name} failed after ${maxRetries} attempts. Last error: ${lastError.message}. Session ID: ${this.chromeController.sessionId}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Checks if an error indicates a session conflict (SingletonLock, port conflicts, etc.)
|
|
284
|
+
* @param {Error} error - The error to check
|
|
285
|
+
* @returns {boolean} True if this is a session conflict error
|
|
286
|
+
*/
|
|
287
|
+
isSessionConflictError(error) {
|
|
288
|
+
const message = error.message.toLowerCase();
|
|
289
|
+
|
|
290
|
+
// Known session conflict patterns
|
|
291
|
+
const conflictPatterns = [
|
|
292
|
+
'singletonlock',
|
|
293
|
+
'singleton lock',
|
|
294
|
+
'address already in use',
|
|
295
|
+
'eaddrinuse',
|
|
296
|
+
'port already in use',
|
|
297
|
+
'chrome already running',
|
|
298
|
+
'debug port',
|
|
299
|
+
'user data directory',
|
|
300
|
+
'profile in use',
|
|
301
|
+
'cannot start chrome'
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
return conflictPatterns.some(pattern => message.includes(pattern));
|
|
305
|
+
}
|
|
306
|
+
}
|