@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.
Files changed (95) hide show
  1. package/CLAUDE.md +344 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/chrome-extension/README.md +41 -0
  5. package/chrome-extension/background.js +3917 -0
  6. package/chrome-extension/chrome-session-manager.js +706 -0
  7. package/chrome-extension/content.css +181 -0
  8. package/chrome-extension/content.js +3022 -0
  9. package/chrome-extension/data-buffer.js +435 -0
  10. package/chrome-extension/dom-tracker.js +411 -0
  11. package/chrome-extension/extension-config.js +78 -0
  12. package/chrome-extension/firebase-client.js +278 -0
  13. package/chrome-extension/firebase-config.js +32 -0
  14. package/chrome-extension/firebase-config.module.js +22 -0
  15. package/chrome-extension/firebase-config.module.template.js +27 -0
  16. package/chrome-extension/firebase-config.template.js +36 -0
  17. package/chrome-extension/frame-capture.js +407 -0
  18. package/chrome-extension/icon128.png +1 -0
  19. package/chrome-extension/icon16.png +1 -0
  20. package/chrome-extension/icon48.png +1 -0
  21. package/chrome-extension/license-helper.js +181 -0
  22. package/chrome-extension/logger.js +23 -0
  23. package/chrome-extension/manifest.json +73 -0
  24. package/chrome-extension/network-tracker.js +510 -0
  25. package/chrome-extension/offscreen.html +10 -0
  26. package/chrome-extension/options.html +203 -0
  27. package/chrome-extension/options.js +282 -0
  28. package/chrome-extension/pako.min.js +2 -0
  29. package/chrome-extension/performance-monitor.js +533 -0
  30. package/chrome-extension/pii-redactor.js +405 -0
  31. package/chrome-extension/popup.html +532 -0
  32. package/chrome-extension/popup.js +2446 -0
  33. package/chrome-extension/upload-manager.js +323 -0
  34. package/chrome-extension/web-vitals.iife.js +1 -0
  35. package/config/api-keys.json +11 -0
  36. package/config/chrome-pilot-config.json +45 -0
  37. package/package.json +126 -0
  38. package/scripts/cleanup-processes.js +109 -0
  39. package/scripts/config-manager.js +280 -0
  40. package/scripts/generate-extension-config.js +53 -0
  41. package/scripts/setup-security.js +64 -0
  42. package/src/capture/architecture.js +426 -0
  43. package/src/capture/error-handling-tests.md +38 -0
  44. package/src/capture/error-handling-types.ts +360 -0
  45. package/src/capture/index.js +508 -0
  46. package/src/capture/interfaces.js +625 -0
  47. package/src/capture/memory-manager.js +713 -0
  48. package/src/capture/types.js +342 -0
  49. package/src/chrome-controller.js +2658 -0
  50. package/src/cli.js +19 -0
  51. package/src/config-loader.js +303 -0
  52. package/src/database.js +2178 -0
  53. package/src/firebase-license-manager.js +462 -0
  54. package/src/firebase-privacy-guard.js +397 -0
  55. package/src/http-server.js +1516 -0
  56. package/src/index-direct.js +157 -0
  57. package/src/index-modular.js +219 -0
  58. package/src/index-monolithic-backup.js +2230 -0
  59. package/src/index.js +305 -0
  60. package/src/legacy/chrome-controller-old.js +1406 -0
  61. package/src/legacy/index-express.js +625 -0
  62. package/src/legacy/index-old.js +977 -0
  63. package/src/legacy/routes.js +260 -0
  64. package/src/legacy/shared-storage.js +101 -0
  65. package/src/logger.js +10 -0
  66. package/src/mcp/handlers/chrome-tool-handler.js +306 -0
  67. package/src/mcp/handlers/element-tool-handler.js +51 -0
  68. package/src/mcp/handlers/frame-tool-handler.js +957 -0
  69. package/src/mcp/handlers/request-handler.js +104 -0
  70. package/src/mcp/handlers/workflow-tool-handler.js +636 -0
  71. package/src/mcp/server.js +68 -0
  72. package/src/mcp/tools/index.js +701 -0
  73. package/src/middleware/auth.js +371 -0
  74. package/src/middleware/security.js +267 -0
  75. package/src/port-discovery.js +258 -0
  76. package/src/routes/admin.js +182 -0
  77. package/src/services/browser-daemon.js +494 -0
  78. package/src/services/chrome-service.js +375 -0
  79. package/src/services/failover-manager.js +412 -0
  80. package/src/services/git-safety-service.js +675 -0
  81. package/src/services/heartbeat-manager.js +200 -0
  82. package/src/services/http-client.js +195 -0
  83. package/src/services/process-manager.js +318 -0
  84. package/src/services/process-tracker.js +574 -0
  85. package/src/services/profile-manager.js +449 -0
  86. package/src/services/project-manager.js +415 -0
  87. package/src/services/session-manager.js +497 -0
  88. package/src/services/session-registry.js +491 -0
  89. package/src/services/unified-session-manager.js +678 -0
  90. package/src/shared-storage-old.js +267 -0
  91. package/src/standalone-server.js +53 -0
  92. package/src/utils/extension-path.js +145 -0
  93. package/src/utils.js +187 -0
  94. package/src/validation/log-transformer.js +125 -0
  95. 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
+ }