chrometools-mcp 1.9.1 → 2.3.2
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/CHANGELOG.md +305 -0
- package/README.md +279 -53
- package/browser/browser-manager.js +206 -0
- package/browser/page-manager.js +298 -0
- package/index.js +625 -1875
- package/package.json +1 -1
- package/recorder/page-object-generator.js +720 -0
- package/recorder/recorder-script.js +63 -9
- package/recorder/scenario-executor.js +47 -27
- package/recorder/scenario-storage.js +251 -29
- package/server/tool-definitions.js +655 -0
- package/server/tool-schemas.js +295 -0
- package/utils/code-generators/code-generator-base.js +61 -0
- package/utils/code-generators/file-appender.js +202 -0
- package/utils/code-generators/playwright-python.js +84 -0
- package/utils/code-generators/playwright-typescript.js +95 -0
- package/utils/code-generators/selenium-java.js +123 -0
- package/utils/code-generators/selenium-python.js +82 -0
- package/utils/css-utils.js +151 -0
- package/utils/image-processing.js +236 -0
- package/utils/platform-utils.js +62 -0
- package/utils/url-to-project.js +141 -0
- package/utils/project-detector.js +0 -87
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser/page-manager.js
|
|
3
|
+
*
|
|
4
|
+
* Page lifecycle and monitoring management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getBrowser } from './browser-manager.js';
|
|
8
|
+
import { injectRecorder } from '../recorder/recorder-script.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Debug log helper (only logs to stderr when DEBUG=1)
|
|
12
|
+
* @param {...any} args - Arguments to log
|
|
13
|
+
*/
|
|
14
|
+
function debugLog(...args) {
|
|
15
|
+
if (process.env.DEBUG === '1') {
|
|
16
|
+
console.error('[page-manager]', ...args);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Global state for pages and monitoring
|
|
21
|
+
export const openPages = new Map();
|
|
22
|
+
export let lastPage = null;
|
|
23
|
+
|
|
24
|
+
// Console logs storage
|
|
25
|
+
export const consoleLogs = [];
|
|
26
|
+
|
|
27
|
+
// Network requests storage
|
|
28
|
+
export const networkRequests = [];
|
|
29
|
+
|
|
30
|
+
// Page analysis cache
|
|
31
|
+
export const pageAnalysisCache = new Map();
|
|
32
|
+
|
|
33
|
+
// Track pages with recorder injected
|
|
34
|
+
export const pagesWithRecorder = new WeakSet();
|
|
35
|
+
|
|
36
|
+
// Track pages with network monitoring to prevent duplicate setup
|
|
37
|
+
const pagesWithNetworkMonitoring = new WeakSet();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Setup network monitoring with auto-reinitialization on navigation
|
|
41
|
+
* @param {Page} page - Puppeteer page instance
|
|
42
|
+
*/
|
|
43
|
+
export async function setupNetworkMonitoring(page) {
|
|
44
|
+
// Prevent duplicate setup on the same page
|
|
45
|
+
if (pagesWithNetworkMonitoring.has(page)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
pagesWithNetworkMonitoring.add(page);
|
|
49
|
+
|
|
50
|
+
const client = await page.target().createCDPSession();
|
|
51
|
+
await client.send('Network.enable');
|
|
52
|
+
|
|
53
|
+
client.on('Network.requestWillBeSent', (event) => {
|
|
54
|
+
const timestamp = new Date().toISOString();
|
|
55
|
+
networkRequests.push({
|
|
56
|
+
requestId: event.requestId,
|
|
57
|
+
url: event.request.url,
|
|
58
|
+
method: event.request.method,
|
|
59
|
+
headers: event.request.headers,
|
|
60
|
+
postData: event.request.postData,
|
|
61
|
+
timestamp,
|
|
62
|
+
type: event.type, // Document, Stylesheet, Image, Media, Font, Script, XHR, Fetch, etc.
|
|
63
|
+
initiator: event.initiator.type, // parser, script, other
|
|
64
|
+
status: 'pending',
|
|
65
|
+
documentURL: event.documentURL
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
client.on('Network.responseReceived', (event) => {
|
|
70
|
+
const req = networkRequests.find(r => r.requestId === event.requestId);
|
|
71
|
+
if (req) {
|
|
72
|
+
req.status = event.response.status;
|
|
73
|
+
req.statusText = event.response.statusText;
|
|
74
|
+
req.responseHeaders = event.response.headers;
|
|
75
|
+
req.mimeType = event.response.mimeType;
|
|
76
|
+
req.fromCache = event.response.fromDiskCache || event.response.fromServiceWorker;
|
|
77
|
+
req.timing = event.response.timing;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
client.on('Network.loadingFinished', (event) => {
|
|
82
|
+
const req = networkRequests.find(r => r.requestId === event.requestId);
|
|
83
|
+
if (req && req.status === 'pending') {
|
|
84
|
+
req.status = 'completed';
|
|
85
|
+
}
|
|
86
|
+
if (req) {
|
|
87
|
+
req.encodedDataLength = event.encodedDataLength;
|
|
88
|
+
req.finishedTimestamp = new Date().toISOString();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
client.on('Network.loadingFailed', (event) => {
|
|
93
|
+
const req = networkRequests.find(r => r.requestId === event.requestId);
|
|
94
|
+
if (req) {
|
|
95
|
+
req.status = 'failed';
|
|
96
|
+
req.errorText = event.errorText;
|
|
97
|
+
req.canceled = event.canceled;
|
|
98
|
+
req.finishedTimestamp = new Date().toISOString();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Auto-reinitialize on navigation (CDP session is reset on navigation)
|
|
103
|
+
let lastUrl = page.url();
|
|
104
|
+
|
|
105
|
+
page.on('framenavigated', async (frame) => {
|
|
106
|
+
// Only handle main frame navigation
|
|
107
|
+
if (frame !== page.mainFrame()) return;
|
|
108
|
+
|
|
109
|
+
const currentUrl = frame.url();
|
|
110
|
+
|
|
111
|
+
// Skip if URL hasn't changed
|
|
112
|
+
if (currentUrl === lastUrl) return;
|
|
113
|
+
lastUrl = currentUrl;
|
|
114
|
+
|
|
115
|
+
// Remove from tracking set to allow re-setup
|
|
116
|
+
pagesWithNetworkMonitoring.delete(page);
|
|
117
|
+
|
|
118
|
+
// Small delay to let navigation settle
|
|
119
|
+
setTimeout(async () => {
|
|
120
|
+
try {
|
|
121
|
+
await setupNetworkMonitoring(page);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
debugLog('Failed to reinitialize network monitoring:', error.message);
|
|
124
|
+
}
|
|
125
|
+
}, 100);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Setup recorder auto-reinjection on navigation
|
|
131
|
+
* @param {Page} page - Puppeteer page instance
|
|
132
|
+
*/
|
|
133
|
+
export async function setupRecorderAutoReinjection(page) {
|
|
134
|
+
let reinjectionTimeout = null;
|
|
135
|
+
let lastUrl = null;
|
|
136
|
+
|
|
137
|
+
// Handle navigation events (form submits, link clicks, history API)
|
|
138
|
+
page.on('framenavigated', async (frame) => {
|
|
139
|
+
// Only handle main frame navigation
|
|
140
|
+
if (frame !== page.mainFrame()) return;
|
|
141
|
+
|
|
142
|
+
// Get current URL
|
|
143
|
+
const currentUrl = frame.url();
|
|
144
|
+
|
|
145
|
+
// Skip if URL hasn't changed (prevents duplicate injections on same page)
|
|
146
|
+
if (currentUrl === lastUrl) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
lastUrl = currentUrl;
|
|
150
|
+
|
|
151
|
+
// Clear any pending reinjection
|
|
152
|
+
if (reinjectionTimeout) {
|
|
153
|
+
clearTimeout(reinjectionTimeout);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Debounce reinjection (wait 100ms for navigation to settle)
|
|
157
|
+
reinjectionTimeout = setTimeout(async () => {
|
|
158
|
+
// Check if this page had recorder before
|
|
159
|
+
if (pagesWithRecorder.has(page)) {
|
|
160
|
+
try {
|
|
161
|
+
// Project ID will be determined from URL in browser context
|
|
162
|
+
await injectRecorder(page);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
debugLog('Failed to re-inject recorder:', error.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}, 100);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Handle page reloads (F5, Ctrl+R) - use 'load' event
|
|
171
|
+
page.on('load', async () => {
|
|
172
|
+
// Check if this page had recorder before
|
|
173
|
+
if (pagesWithRecorder.has(page)) {
|
|
174
|
+
try {
|
|
175
|
+
// Project ID will be determined from URL in browser context
|
|
176
|
+
await injectRecorder(page);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
debugLog('Failed to re-inject recorder after reload:', error.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get or create page for URL
|
|
186
|
+
* @param {string} url - URL to navigate to
|
|
187
|
+
* @returns {Promise<Page>} - Puppeteer page instance
|
|
188
|
+
*/
|
|
189
|
+
export async function getOrCreatePage(url) {
|
|
190
|
+
const browser = await getBrowser();
|
|
191
|
+
|
|
192
|
+
// Check if page for this URL already exists
|
|
193
|
+
if (openPages.has(url)) {
|
|
194
|
+
const existingPage = openPages.get(url);
|
|
195
|
+
if (!existingPage.isClosed()) {
|
|
196
|
+
lastPage = existingPage;
|
|
197
|
+
return existingPage;
|
|
198
|
+
}
|
|
199
|
+
openPages.delete(url);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Create new page
|
|
203
|
+
const page = await browser.newPage();
|
|
204
|
+
|
|
205
|
+
// Set up console log capture
|
|
206
|
+
const client = await page.target().createCDPSession();
|
|
207
|
+
await client.send('Runtime.enable');
|
|
208
|
+
await client.send('Log.enable');
|
|
209
|
+
|
|
210
|
+
client.on('Runtime.consoleAPICalled', (event) => {
|
|
211
|
+
const timestamp = new Date().toISOString();
|
|
212
|
+
const args = event.args.map(arg => {
|
|
213
|
+
if (arg.value !== undefined) return arg.value;
|
|
214
|
+
if (arg.description) return arg.description;
|
|
215
|
+
return String(arg);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
consoleLogs.push({
|
|
219
|
+
type: event.type, // log, warn, error, info, debug
|
|
220
|
+
timestamp,
|
|
221
|
+
message: args.join(' '),
|
|
222
|
+
stackTrace: event.stackTrace
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
client.on('Log.entryAdded', (event) => {
|
|
227
|
+
const entry = event.entry;
|
|
228
|
+
consoleLogs.push({
|
|
229
|
+
type: entry.level, // verbose, info, warning, error
|
|
230
|
+
timestamp: new Date(entry.timestamp).toISOString(),
|
|
231
|
+
message: entry.text,
|
|
232
|
+
source: entry.source,
|
|
233
|
+
url: entry.url,
|
|
234
|
+
lineNumber: entry.lineNumber
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Setup network monitoring with auto-reinitialization on navigation
|
|
239
|
+
await setupNetworkMonitoring(page);
|
|
240
|
+
|
|
241
|
+
// Setup recorder auto-reinjection on navigation
|
|
242
|
+
setupRecorderAutoReinjection(page);
|
|
243
|
+
|
|
244
|
+
await page.goto(url, { waitUntil: 'networkidle2' });
|
|
245
|
+
openPages.set(url, page);
|
|
246
|
+
lastPage = page;
|
|
247
|
+
|
|
248
|
+
return page;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get last opened page (for tools that don't need URL)
|
|
253
|
+
* @returns {Promise<Page>} - Puppeteer page instance
|
|
254
|
+
*/
|
|
255
|
+
export async function getLastOpenPage() {
|
|
256
|
+
// Check if we have lastPage reference
|
|
257
|
+
if (!lastPage) {
|
|
258
|
+
throw new Error('No page is currently open. Use openBrowser first to open a page.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if page is closed with timeout to prevent hanging
|
|
262
|
+
let isClosed = false;
|
|
263
|
+
try {
|
|
264
|
+
// Race between isClosed check and timeout
|
|
265
|
+
isClosed = await Promise.race([
|
|
266
|
+
lastPage.isClosed(),
|
|
267
|
+
new Promise((_, reject) =>
|
|
268
|
+
setTimeout(() => reject(new Error('Page check timeout')), 1000)
|
|
269
|
+
)
|
|
270
|
+
]);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// If timeout or error, assume page is not available
|
|
273
|
+
lastPage = null;
|
|
274
|
+
throw new Error('No page is currently open. Use openBrowser first to open a page.');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (isClosed) {
|
|
278
|
+
lastPage = null;
|
|
279
|
+
throw new Error('No page is currently open. Use openBrowser first to open a page.');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Setup recorder auto-reinjection if not already set up
|
|
283
|
+
// Check if page already has navigation listener
|
|
284
|
+
const listenerCount = lastPage.listenerCount('framenavigated');
|
|
285
|
+
if (listenerCount === 0) {
|
|
286
|
+
setupRecorderAutoReinjection(lastPage);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return lastPage;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Update lastPage reference (for external updates)
|
|
294
|
+
* @param {Page} page - New last page
|
|
295
|
+
*/
|
|
296
|
+
export function setLastPage(page) {
|
|
297
|
+
lastPage = page;
|
|
298
|
+
}
|