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
|
@@ -4,21 +4,168 @@
|
|
|
4
4
|
* Manages scenario and secrets storage:
|
|
5
5
|
* 1. Save/load scenarios to/from files
|
|
6
6
|
* 2. Save/load secrets separately
|
|
7
|
-
* 3. Maintain
|
|
7
|
+
* 3. Maintain project-specific and global indexes
|
|
8
8
|
* 4. Ensure .gitignore for secrets directory
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import fs from 'fs/promises';
|
|
12
|
+
import fssync from 'fs';
|
|
12
13
|
import path from 'path';
|
|
14
|
+
import { homedir } from 'os';
|
|
13
15
|
|
|
14
16
|
// Constants
|
|
15
17
|
const INDEX_FILE = 'index.json';
|
|
16
18
|
const GITIGNORE_FILE = '.gitignore';
|
|
19
|
+
const BASE_STORAGE_DIR = path.join(homedir(), '.config', 'chrometools-mcp');
|
|
20
|
+
const GLOBAL_INDEX_PATH = path.join(BASE_STORAGE_DIR, 'index.json');
|
|
21
|
+
const PROJECTS_DIR = path.join(BASE_STORAGE_DIR, 'projects');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load global index from ~/.config/chrometools-mcp/index.json
|
|
25
|
+
* @returns {object} - Global index object
|
|
26
|
+
*/
|
|
27
|
+
function loadGlobalIndex() {
|
|
28
|
+
try {
|
|
29
|
+
const data = fssync.readFileSync(GLOBAL_INDEX_PATH, 'utf-8');
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Index doesn't exist yet, return empty structure
|
|
33
|
+
return {
|
|
34
|
+
version: '2.0',
|
|
35
|
+
projects: {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Save global index to ~/.config/chrometools-mcp/index.json
|
|
42
|
+
* @param {object} index - Global index object
|
|
43
|
+
*/
|
|
44
|
+
async function saveGlobalIndex(index) {
|
|
45
|
+
await fs.mkdir(BASE_STORAGE_DIR, { recursive: true });
|
|
46
|
+
await fs.writeFile(GLOBAL_INDEX_PATH, JSON.stringify(index, null, 2), 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Update global index with scenario metadata
|
|
51
|
+
* @param {string} projectId - Project identifier
|
|
52
|
+
* @param {string} projectPath - Full path to project root
|
|
53
|
+
* @param {string} scenarioName - Scenario name
|
|
54
|
+
* @param {object} metadata - Scenario metadata
|
|
55
|
+
*/
|
|
56
|
+
async function updateGlobalIndex(projectId, projectPath, scenarioName, metadata) {
|
|
57
|
+
const index = loadGlobalIndex();
|
|
58
|
+
|
|
59
|
+
if (!index.projects[projectId]) {
|
|
60
|
+
index.projects[projectId] = {
|
|
61
|
+
projectPath,
|
|
62
|
+
lastAccessed: new Date().toISOString(),
|
|
63
|
+
scenarios: {}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
index.projects[projectId].scenarios[scenarioName] = {
|
|
68
|
+
name: scenarioName,
|
|
69
|
+
description: metadata.description || '',
|
|
70
|
+
tags: metadata.tags || [],
|
|
71
|
+
dependencies: metadata.dependencies || [],
|
|
72
|
+
parameters: metadata.parameters || {},
|
|
73
|
+
outputs: metadata.outputs || [],
|
|
74
|
+
entryUrl: metadata.entryUrl || null,
|
|
75
|
+
exitUrl: metadata.exitUrl || null,
|
|
76
|
+
createdAt: index.projects[projectId].scenarios[scenarioName]?.createdAt || new Date().toISOString(),
|
|
77
|
+
updatedAt: new Date().toISOString()
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
index.projects[projectId].lastAccessed = new Date().toISOString();
|
|
81
|
+
|
|
82
|
+
await saveGlobalIndex(index);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Remove scenario from global index
|
|
87
|
+
* @param {string} projectId - Project identifier
|
|
88
|
+
* @param {string} scenarioName - Scenario name
|
|
89
|
+
*/
|
|
90
|
+
async function removeFromGlobalIndex(projectId, scenarioName) {
|
|
91
|
+
const index = loadGlobalIndex();
|
|
92
|
+
|
|
93
|
+
if (index.projects[projectId]?.scenarios[scenarioName]) {
|
|
94
|
+
delete index.projects[projectId].scenarios[scenarioName];
|
|
95
|
+
|
|
96
|
+
// Remove project if no scenarios left
|
|
97
|
+
if (Object.keys(index.projects[projectId].scenarios).length === 0) {
|
|
98
|
+
delete index.projects[projectId];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await saveGlobalIndex(index);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Find scenario in global index
|
|
107
|
+
* @param {string} scenarioName - Scenario name
|
|
108
|
+
* @param {string|null} projectId - Optional project filter
|
|
109
|
+
* @returns {object|null} - { projectId, metadata } or null
|
|
110
|
+
*/
|
|
111
|
+
/**
|
|
112
|
+
* Find all scenarios with the given name across all projects
|
|
113
|
+
* @param {string} scenarioName - Scenario name
|
|
114
|
+
* @returns {Array} - Array of { projectId, metadata } objects
|
|
115
|
+
*/
|
|
116
|
+
function findAllScenariosWithName(scenarioName) {
|
|
117
|
+
const index = loadGlobalIndex();
|
|
118
|
+
const results = [];
|
|
119
|
+
|
|
120
|
+
for (const [pid, project] of Object.entries(index.projects)) {
|
|
121
|
+
if (project.scenarios[scenarioName]) {
|
|
122
|
+
results.push({
|
|
123
|
+
projectId: pid,
|
|
124
|
+
metadata: project.scenarios[scenarioName]
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function findScenarioInGlobalIndex(scenarioName, projectId = null) {
|
|
133
|
+
const index = loadGlobalIndex();
|
|
134
|
+
|
|
135
|
+
if (projectId) {
|
|
136
|
+
// Search in specific project
|
|
137
|
+
if (index.projects[projectId]?.scenarios[scenarioName]) {
|
|
138
|
+
return {
|
|
139
|
+
projectId,
|
|
140
|
+
metadata: index.projects[projectId].scenarios[scenarioName]
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Search in all projects - check for collisions
|
|
147
|
+
const allMatches = findAllScenariosWithName(scenarioName);
|
|
148
|
+
|
|
149
|
+
if (allMatches.length === 0) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (allMatches.length > 1) {
|
|
154
|
+
// Multiple scenarios with same name - return collision error
|
|
155
|
+
return {
|
|
156
|
+
collision: true,
|
|
157
|
+
matches: allMatches.map(m => m.projectId)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Single match - return it
|
|
162
|
+
return allMatches[0];
|
|
163
|
+
}
|
|
17
164
|
|
|
18
165
|
/**
|
|
19
166
|
* Initialize storage directories
|
|
20
167
|
* Creates directories and ensures .gitignore exists
|
|
21
|
-
* @param {string} baseDir - Base directory for scenarios and secrets
|
|
168
|
+
* @param {string} baseDir - Base directory for scenarios and secrets (project directory)
|
|
22
169
|
*/
|
|
23
170
|
export async function initializeStorage(baseDir) {
|
|
24
171
|
const scenariosDir = path.join(baseDir, 'scenarios');
|
|
@@ -64,12 +211,18 @@ async function ensureSecretsGitignore(secretsDir) {
|
|
|
64
211
|
/**
|
|
65
212
|
* Save scenario to file
|
|
66
213
|
* @param {Object} scenario - Scenario data
|
|
67
|
-
* @param {string}
|
|
214
|
+
* @param {string} urlProjectId - Project identifier extracted from URL (e.g., "google", "localhost-3000")
|
|
68
215
|
* @returns {Object} - { success: boolean, path: string, error?: string }
|
|
69
216
|
*/
|
|
70
|
-
export async function saveScenario(scenario,
|
|
217
|
+
export async function saveScenario(scenario, urlProjectId) {
|
|
71
218
|
try {
|
|
72
|
-
|
|
219
|
+
// Use URL-based projectId directly
|
|
220
|
+
const projectId = urlProjectId;
|
|
221
|
+
const projectPath = `url://${urlProjectId}`;
|
|
222
|
+
|
|
223
|
+
// Get project directory
|
|
224
|
+
const projectDir = path.join(PROJECTS_DIR, projectId);
|
|
225
|
+
await initializeStorage(projectDir);
|
|
73
226
|
|
|
74
227
|
const { name, metadata, chain, secrets } = scenario;
|
|
75
228
|
|
|
@@ -81,13 +234,20 @@ export async function saveScenario(scenario, baseDir) {
|
|
|
81
234
|
};
|
|
82
235
|
}
|
|
83
236
|
|
|
84
|
-
const scenariosDir = path.join(
|
|
85
|
-
const secretsDir = path.join(
|
|
237
|
+
const scenariosDir = path.join(projectDir, 'scenarios');
|
|
238
|
+
const secretsDir = path.join(projectDir, 'secrets');
|
|
239
|
+
|
|
240
|
+
// Add project info to metadata
|
|
241
|
+
const enhancedMetadata = {
|
|
242
|
+
...(metadata || {}),
|
|
243
|
+
projectId,
|
|
244
|
+
projectPath
|
|
245
|
+
};
|
|
86
246
|
|
|
87
247
|
// Save main scenario file (without secrets)
|
|
88
248
|
const scenarioData = {
|
|
89
249
|
name,
|
|
90
|
-
metadata:
|
|
250
|
+
metadata: enhancedMetadata,
|
|
91
251
|
chain,
|
|
92
252
|
version: '1.0',
|
|
93
253
|
createdAt: new Date().toISOString()
|
|
@@ -101,8 +261,11 @@ export async function saveScenario(scenario, baseDir) {
|
|
|
101
261
|
await saveSecrets(name, secrets, secretsDir);
|
|
102
262
|
}
|
|
103
263
|
|
|
104
|
-
// Update index
|
|
105
|
-
await updateIndex(name,
|
|
264
|
+
// Update project-local index
|
|
265
|
+
await updateIndex(name, enhancedMetadata, scenariosDir);
|
|
266
|
+
|
|
267
|
+
// Update global index
|
|
268
|
+
await updateGlobalIndex(projectId, projectPath, name, enhancedMetadata);
|
|
106
269
|
|
|
107
270
|
return {
|
|
108
271
|
success: true,
|
|
@@ -120,19 +283,40 @@ export async function saveScenario(scenario, baseDir) {
|
|
|
120
283
|
* Load scenario from file
|
|
121
284
|
* @param {string} name - Scenario name
|
|
122
285
|
* @param {boolean} includeSecrets - Whether to load secrets
|
|
123
|
-
* @param {string}
|
|
286
|
+
* @param {string|null} projectId - Optional project ID filter
|
|
124
287
|
* @returns {Object} - Scenario data or null
|
|
125
288
|
*/
|
|
126
|
-
export async function loadScenario(name, includeSecrets = false,
|
|
289
|
+
export async function loadScenario(name, includeSecrets = false, projectId = null) {
|
|
127
290
|
try {
|
|
128
|
-
|
|
291
|
+
// Find scenario in global index
|
|
292
|
+
const result = findScenarioInGlobalIndex(name, projectId);
|
|
293
|
+
|
|
294
|
+
if (!result) {
|
|
295
|
+
console.error(`Scenario "${name}" not found${projectId ? ` in project "${projectId}"` : ''}`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check for name collision
|
|
300
|
+
if (result.collision) {
|
|
301
|
+
const error = {
|
|
302
|
+
collision: true,
|
|
303
|
+
message: `Multiple scenarios named "${name}" found. Please specify projectId.`,
|
|
304
|
+
availableProjectIds: result.matches
|
|
305
|
+
};
|
|
306
|
+
console.error(error.message, 'Available projectIds:', result.matches);
|
|
307
|
+
return error;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Load from project directory
|
|
311
|
+
const projectDir = path.join(PROJECTS_DIR, result.projectId);
|
|
312
|
+
const scenariosDir = path.join(projectDir, 'scenarios');
|
|
129
313
|
const scenarioPath = path.join(scenariosDir, `${name}.json`);
|
|
130
314
|
const content = await fs.readFile(scenarioPath, 'utf-8');
|
|
131
315
|
const scenario = JSON.parse(content);
|
|
132
316
|
|
|
133
317
|
// Load secrets if requested
|
|
134
318
|
if (includeSecrets) {
|
|
135
|
-
const secretsDir = path.join(
|
|
319
|
+
const secretsDir = path.join(projectDir, 'secrets');
|
|
136
320
|
const secrets = await loadSecrets(name, secretsDir);
|
|
137
321
|
if (secrets) {
|
|
138
322
|
scenario.secrets = secrets;
|
|
@@ -194,6 +378,8 @@ async function updateIndex(scenarioName, metadata, scenariosDir) {
|
|
|
194
378
|
dependencies: metadata.dependencies || [],
|
|
195
379
|
parameters: metadata.parameters || {},
|
|
196
380
|
outputs: metadata.outputs || [],
|
|
381
|
+
entryUrl: metadata.entryUrl || null,
|
|
382
|
+
exitUrl: metadata.exitUrl || null,
|
|
197
383
|
createdAt: index[scenarioName]?.createdAt || new Date().toISOString(),
|
|
198
384
|
updatedAt: new Date().toISOString()
|
|
199
385
|
};
|
|
@@ -233,25 +419,51 @@ async function saveIndex(index, scenariosDir) {
|
|
|
233
419
|
|
|
234
420
|
/**
|
|
235
421
|
* List all available scenarios
|
|
236
|
-
* @param {string}
|
|
422
|
+
* @param {string} currentProjectId - Current project identifier
|
|
423
|
+
* @param {boolean} allProjects - Whether to list scenarios from all projects
|
|
237
424
|
* @returns {Array} - Array of scenario metadata
|
|
238
425
|
*/
|
|
239
|
-
export async function listScenarios(
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
426
|
+
export async function listScenarios(currentProjectId, allProjects = false) {
|
|
427
|
+
const globalIndex = loadGlobalIndex();
|
|
428
|
+
const results = [];
|
|
429
|
+
|
|
430
|
+
if (allProjects) {
|
|
431
|
+
// Return scenarios from all projects
|
|
432
|
+
for (const [projectId, project] of Object.entries(globalIndex.projects)) {
|
|
433
|
+
for (const scenario of Object.values(project.scenarios)) {
|
|
434
|
+
results.push({
|
|
435
|
+
...scenario,
|
|
436
|
+
projectId,
|
|
437
|
+
projectPath: project.projectPath
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
// Return scenarios from current project only
|
|
443
|
+
if (globalIndex.projects[currentProjectId]) {
|
|
444
|
+
for (const scenario of Object.values(globalIndex.projects[currentProjectId].scenarios)) {
|
|
445
|
+
results.push({
|
|
446
|
+
...scenario,
|
|
447
|
+
projectId: currentProjectId,
|
|
448
|
+
projectPath: globalIndex.projects[currentProjectId].projectPath
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return results;
|
|
243
455
|
}
|
|
244
456
|
|
|
245
457
|
/**
|
|
246
458
|
* Search scenarios by query
|
|
247
459
|
* @param {Object} query - Search query { tags?, text?, dependencies? }
|
|
248
|
-
* @param {string}
|
|
460
|
+
* @param {string} currentProjectId - Current project identifier
|
|
461
|
+
* @param {boolean} allProjects - Whether to search in all projects
|
|
249
462
|
* @returns {Array} - Matching scenarios
|
|
250
463
|
*/
|
|
251
|
-
export async function searchScenarios(query,
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
const scenarios = Object.values(index);
|
|
464
|
+
export async function searchScenarios(query, currentProjectId, allProjects = false) {
|
|
465
|
+
// Get all scenarios based on allProjects flag
|
|
466
|
+
const scenarios = await listScenarios(currentProjectId, allProjects);
|
|
255
467
|
|
|
256
468
|
let results = scenarios;
|
|
257
469
|
|
|
@@ -284,13 +496,22 @@ export async function searchScenarios(query, baseDir) {
|
|
|
284
496
|
/**
|
|
285
497
|
* Delete scenario
|
|
286
498
|
* @param {string} name - Scenario name
|
|
287
|
-
* @param {string}
|
|
499
|
+
* @param {string|null} projectId - Optional project ID filter
|
|
288
500
|
* @returns {boolean} - Success
|
|
289
501
|
*/
|
|
290
|
-
export async function deleteScenario(name,
|
|
502
|
+
export async function deleteScenario(name, projectId = null) {
|
|
291
503
|
try {
|
|
292
|
-
|
|
293
|
-
const
|
|
504
|
+
// Find scenario in global index
|
|
505
|
+
const result = findScenarioInGlobalIndex(name, projectId);
|
|
506
|
+
|
|
507
|
+
if (!result) {
|
|
508
|
+
console.error(`Scenario "${name}" not found${projectId ? ` in project "${projectId}"` : ''}`);
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const projectDir = path.join(PROJECTS_DIR, result.projectId);
|
|
513
|
+
const scenariosDir = path.join(projectDir, 'scenarios');
|
|
514
|
+
const secretsDir = path.join(projectDir, 'secrets');
|
|
294
515
|
|
|
295
516
|
// Delete scenario file
|
|
296
517
|
const scenarioPath = path.join(scenariosDir, `${name}.json`);
|
|
@@ -304,11 +525,14 @@ export async function deleteScenario(name, baseDir) {
|
|
|
304
525
|
// Secrets file may not exist
|
|
305
526
|
}
|
|
306
527
|
|
|
307
|
-
// Remove from index
|
|
528
|
+
// Remove from project-local index
|
|
308
529
|
const index = await loadIndex(scenariosDir);
|
|
309
530
|
delete index[name];
|
|
310
531
|
await saveIndex(index, scenariosDir);
|
|
311
532
|
|
|
533
|
+
// Remove from global index
|
|
534
|
+
await removeFromGlobalIndex(result.projectId, name);
|
|
535
|
+
|
|
312
536
|
return true;
|
|
313
537
|
} catch (error) {
|
|
314
538
|
console.error(`Error deleting scenario "${name}":`, error.message);
|