chrometools-mcp 1.8.2 → 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/CHANGELOG.md +757 -494
- package/README.md +219 -41
- package/browser/browser-manager.js +206 -0
- package/browser/page-manager.js +298 -0
- package/index.js +525 -1892
- package/package.json +55 -55
- package/recorder/page-object-generator.js +720 -0
- package/recorder/recorder-script.js +118 -12
- package/recorder/scenario-executor.js +970 -946
- package/recorder/scenario-storage.js +253 -29
- package/server/tool-definitions.js +620 -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/index.js.backup +0 -3674
- package/utils/project-detector.js +0 -87
|
@@ -27,6 +27,56 @@ export function generateRecorderScript() {
|
|
|
27
27
|
(function() {
|
|
28
28
|
'use strict';
|
|
29
29
|
|
|
30
|
+
// ==========================
|
|
31
|
+
// URL TO PROJECT UTILITIES
|
|
32
|
+
// ==========================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract project ID from URL (browser version)
|
|
36
|
+
* @param {string} url - Full URL
|
|
37
|
+
* @returns {string} - Project ID
|
|
38
|
+
*/
|
|
39
|
+
function urlToProjectId(url) {
|
|
40
|
+
try {
|
|
41
|
+
if (url.startsWith('file://')) {
|
|
42
|
+
return 'local';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const urlObj = new URL(url);
|
|
46
|
+
let hostname = urlObj.hostname.toLowerCase();
|
|
47
|
+
const port = urlObj.port;
|
|
48
|
+
|
|
49
|
+
hostname = hostname.replace(/^www\\./, '');
|
|
50
|
+
const parts = hostname.split('.');
|
|
51
|
+
|
|
52
|
+
if (parts.length === 1) {
|
|
53
|
+
const projectId = sanitizeProjectId(parts[0]);
|
|
54
|
+
return port ? \`\${projectId}-\${port}\` : projectId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const mainDomain = parts[parts.length - 2];
|
|
58
|
+
const projectId = sanitizeProjectId(mainDomain);
|
|
59
|
+
return port ? \`\${projectId}-\${port}\` : projectId;
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('[url-to-project] Invalid URL:', url, error);
|
|
63
|
+
return 'unknown';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sanitize project ID (browser version)
|
|
69
|
+
* @param {string} id - Raw project ID
|
|
70
|
+
* @returns {string} - Sanitized ID
|
|
71
|
+
*/
|
|
72
|
+
function sanitizeProjectId(id) {
|
|
73
|
+
return id
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
76
|
+
.replace(/-+/g, '-')
|
|
77
|
+
.replace(/^-|-$/g, '');
|
|
78
|
+
}
|
|
79
|
+
|
|
30
80
|
// ==========================
|
|
31
81
|
// RECORDER STATE
|
|
32
82
|
// ==========================
|
|
@@ -91,9 +141,28 @@ export function generateRecorderScript() {
|
|
|
91
141
|
hoverDeletionCandidates: Array.from(state.hoverDeletionCandidates),
|
|
92
142
|
timestamp: Date.now()
|
|
93
143
|
};
|
|
94
|
-
|
|
144
|
+
|
|
145
|
+
// Validate state before stringifying
|
|
146
|
+
let jsonString;
|
|
147
|
+
try {
|
|
148
|
+
jsonString = JSON.stringify(persistentState);
|
|
149
|
+
} catch (stringifyError) {
|
|
150
|
+
console.error('[Recorder] Failed to stringify state:', stringifyError.message);
|
|
151
|
+
console.error('[Recorder] Problematic state:', persistentState);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate JSON string
|
|
156
|
+
if (!jsonString || typeof jsonString !== 'string' || jsonString.length === 0) {
|
|
157
|
+
console.error('[Recorder] Invalid JSON string generated');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Save to localStorage
|
|
162
|
+
localStorage.setItem(STORAGE_KEY, jsonString);
|
|
95
163
|
} catch (error) {
|
|
96
164
|
console.error('[Recorder] Failed to save state:', error);
|
|
165
|
+
// Don't let save errors crash the recorder
|
|
97
166
|
}
|
|
98
167
|
}
|
|
99
168
|
|
|
@@ -108,11 +177,37 @@ export function generateRecorderScript() {
|
|
|
108
177
|
const saved = localStorage.getItem(STORAGE_KEY);
|
|
109
178
|
if (!saved) return null;
|
|
110
179
|
|
|
111
|
-
|
|
180
|
+
// Validate JSON before parsing
|
|
181
|
+
if (typeof saved !== 'string' || saved.trim().length === 0) {
|
|
182
|
+
console.warn('[Recorder] Invalid state data in localStorage, clearing...');
|
|
183
|
+
clearStateFromLocalStorage();
|
|
184
|
+
resetClearingFlag();
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let persistentState;
|
|
189
|
+
try {
|
|
190
|
+
persistentState = JSON.parse(saved);
|
|
191
|
+
} catch (parseError) {
|
|
192
|
+
console.error('[Recorder] Corrupted state data in localStorage:', parseError.message);
|
|
193
|
+
console.error('[Recorder] Data preview:', saved.substring(0, 100));
|
|
194
|
+
clearStateFromLocalStorage();
|
|
195
|
+
resetClearingFlag();
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Validate parsed state structure
|
|
200
|
+
if (!persistentState || typeof persistentState !== 'object') {
|
|
201
|
+
console.warn('[Recorder] Invalid state structure, clearing...');
|
|
202
|
+
clearStateFromLocalStorage();
|
|
203
|
+
resetClearingFlag();
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
112
206
|
|
|
113
207
|
// Check if state is not too old (max 24 hours)
|
|
114
208
|
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
115
|
-
if (Date.now() - persistentState.timestamp > maxAge) {
|
|
209
|
+
if (Date.now() - (persistentState.timestamp || 0) > maxAge) {
|
|
210
|
+
console.log('[Recorder] State too old, clearing...');
|
|
116
211
|
clearStateFromLocalStorage();
|
|
117
212
|
resetClearingFlag();
|
|
118
213
|
return null;
|
|
@@ -121,6 +216,13 @@ export function generateRecorderScript() {
|
|
|
121
216
|
return persistentState;
|
|
122
217
|
} catch (error) {
|
|
123
218
|
console.error('[Recorder] Failed to load state:', error);
|
|
219
|
+
// Clear potentially corrupted state
|
|
220
|
+
try {
|
|
221
|
+
clearStateFromLocalStorage();
|
|
222
|
+
resetClearingFlag();
|
|
223
|
+
} catch (clearError) {
|
|
224
|
+
console.error('[Recorder] Failed to clear corrupted state:', clearError);
|
|
225
|
+
}
|
|
124
226
|
return null;
|
|
125
227
|
}
|
|
126
228
|
}
|
|
@@ -1341,7 +1443,10 @@ export function generateRecorderScript() {
|
|
|
1341
1443
|
// Call MCP server via exposed function
|
|
1342
1444
|
if (window.saveScenarioToMCP) {
|
|
1343
1445
|
try {
|
|
1344
|
-
|
|
1446
|
+
// Extract projectId from URL
|
|
1447
|
+
const urlProjectId = urlToProjectId(state.startUrl || window.location.href);
|
|
1448
|
+
|
|
1449
|
+
const result = await window.saveScenarioToMCP(scenario, urlProjectId);
|
|
1345
1450
|
if (result.success) {
|
|
1346
1451
|
// Set clearing flag to prevent any saves during cleanup
|
|
1347
1452
|
isClearing = true;
|
|
@@ -1599,9 +1704,10 @@ export function generateRecorderScript() {
|
|
|
1599
1704
|
|
|
1600
1705
|
/**
|
|
1601
1706
|
* Inject recorder into page
|
|
1707
|
+
* Project ID will be automatically determined from URL in browser context
|
|
1602
1708
|
* @param {Object} page - Puppeteer page instance
|
|
1603
1709
|
*/
|
|
1604
|
-
export async function injectRecorder(page
|
|
1710
|
+
export async function injectRecorder(page) {
|
|
1605
1711
|
try {
|
|
1606
1712
|
// Check if recorder is already injected
|
|
1607
1713
|
const alreadyInjected = await page.evaluate(() => {
|
|
@@ -1629,20 +1735,20 @@ export async function injectRecorder(page, baseDir) {
|
|
|
1629
1735
|
|
|
1630
1736
|
// Only expose functions if they don't exist yet
|
|
1631
1737
|
if (!functionExists) {
|
|
1632
|
-
|
|
1738
|
+
// saveScenarioToMCP now receives urlProjectId from browser
|
|
1739
|
+
await page.exposeFunction('saveScenarioToMCP', async (scenarioData, urlProjectId) => {
|
|
1633
1740
|
const { saveScenario } = await import('./scenario-storage.js');
|
|
1634
|
-
return await saveScenario(scenarioData,
|
|
1741
|
+
return await saveScenario(scenarioData, urlProjectId);
|
|
1635
1742
|
});
|
|
1636
1743
|
|
|
1744
|
+
// listScenariosFromMCP returns ALL scenarios (no filtering by project)
|
|
1637
1745
|
await page.exposeFunction('listScenariosFromMCP', async () => {
|
|
1638
|
-
const {
|
|
1639
|
-
const path = await import('path');
|
|
1746
|
+
const { listScenarios } = await import('./scenario-storage.js');
|
|
1640
1747
|
try {
|
|
1641
|
-
const
|
|
1642
|
-
const index = await loadIndex(scenariosDir);
|
|
1748
|
+
const scenarios = await listScenarios(null, true); // null projectId, allProjects=true
|
|
1643
1749
|
return {
|
|
1644
1750
|
success: true,
|
|
1645
|
-
scenarios:
|
|
1751
|
+
scenarios: scenarios
|
|
1646
1752
|
};
|
|
1647
1753
|
} catch (error) {
|
|
1648
1754
|
return {
|