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.
@@ -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 scenario index with metadata
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} baseDir - Base directory for storage
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, baseDir) {
217
+ export async function saveScenario(scenario, urlProjectId) {
71
218
  try {
72
- await initializeStorage(baseDir);
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(baseDir, 'scenarios');
85
- const secretsDir = path.join(baseDir, 'secrets');
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: 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, metadata, scenariosDir);
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} baseDir - Base directory for storage
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, baseDir) {
289
+ export async function loadScenario(name, includeSecrets = false, projectId = null) {
127
290
  try {
128
- const scenariosDir = path.join(baseDir, 'scenarios');
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(baseDir, 'secrets');
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} baseDir - Base directory for storage
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(baseDir) {
240
- const scenariosDir = path.join(baseDir, 'scenarios');
241
- const index = await loadIndex(scenariosDir);
242
- return Object.values(index);
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} baseDir - Base directory for storage
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, baseDir) {
252
- const scenariosDir = path.join(baseDir, 'scenarios');
253
- const index = await loadIndex(scenariosDir);
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} baseDir - Base directory for storage
499
+ * @param {string|null} projectId - Optional project ID filter
288
500
  * @returns {boolean} - Success
289
501
  */
290
- export async function deleteScenario(name, baseDir) {
502
+ export async function deleteScenario(name, projectId = null) {
291
503
  try {
292
- const scenariosDir = path.join(baseDir, 'scenarios');
293
- const secretsDir = path.join(baseDir, 'secrets');
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);