chrometools-mcp 3.3.6 → 3.3.9

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/index.js CHANGED
@@ -72,6 +72,12 @@ import {PlaywrightPythonGenerator} from './utils/code-generators/playwright-pyth
72
72
  import {SeleniumPythonGenerator} from './utils/code-generators/selenium-python.js';
73
73
  import {SeleniumJavaGenerator} from './utils/code-generators/selenium-java.js';
74
74
  import {FileAppender} from './utils/code-generators/file-appender.js';
75
+ import {parsePomFile} from './utils/code-generators/pom-integrator.js';
76
+
77
+ // Import OpenAPI / Swagger tools
78
+ import {OpenAPIParser} from './utils/openapi/parser.js';
79
+ import {ApiModelsTypeScriptGenerator} from './utils/api-generators/api-models-typescript.js';
80
+ import {ApiModelsPythonGenerator} from './utils/api-generators/api-models-python.js';
75
81
  // Import Figma tools
76
82
  import {
77
83
  collectAllText,
@@ -343,6 +349,9 @@ async function executeToolInternal(name, args) {
343
349
 
344
350
  let hintsText = '\n\n** AI HINTS **';
345
351
  hintsText += `\nPage type: ${hints.pageType}`;
352
+ if (hints.heading) {
353
+ hintsText += `\nPage heading: "${hints.heading}"`;
354
+ }
346
355
  if (hints.availableActions.length > 0) {
347
356
  hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
348
357
  }
@@ -521,10 +530,32 @@ async function executeToolInternal(name, args) {
521
530
 
522
531
  // 3. Format output with hints and diagnostics
523
532
  let hintsText = '\n\n** AI HINTS **';
524
- if (hints.modalOpened) hintsText += '\nModal opened - interact with it or close';
525
- if (hints.newElements.length > 0) {
526
- hintsText += `\nNew elements appeared: ${hints.newElements.map(e => e.type).join(', ')}`;
533
+
534
+ // Modal: show title, body text, and actions
535
+ if (hints.modalOpened && hints.newElements.some(e => e.type === 'modal')) {
536
+ const modal = hints.newElements.find(e => e.type === 'modal');
537
+ let modalText = 'Modal opened';
538
+ if (modal.title) modalText += `: "${modal.title}"`;
539
+ if (modal.text) modalText += `\n ${modal.text}`;
540
+ if (modal.actions?.length) modalText += `\n Actions: [${modal.actions.join('] [')}]`;
541
+ hintsText += '\n' + modalText;
542
+ } else if (hints.modalOpened) {
543
+ hintsText += '\nModal opened - interact with it or close';
544
+ }
545
+
546
+ // Overlay: show items
547
+ const overlay = hints.newElements.find(e => e.type === 'dropdown' || e.type === 'menu');
548
+ if (overlay?.items?.length) {
549
+ const label = overlay.type === 'menu' ? 'Menu' : 'Dropdown';
550
+ hintsText += `\n${label} with ${overlay.totalCount} options: ${overlay.items.join(', ')}`;
527
551
  }
552
+
553
+ // Other new elements (alerts, etc.)
554
+ const otherElements = hints.newElements.filter(e => e.type !== 'modal' && e.type !== 'dropdown' && e.type !== 'menu');
555
+ if (otherElements.length > 0) {
556
+ hintsText += `\nNew elements appeared: ${otherElements.map(e => e.text ? `${e.type}: ${e.text}` : e.type).join(', ')}`;
557
+ }
558
+
528
559
  if (hints.suggestedNext.length > 0) {
529
560
  hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
530
561
  }
@@ -1245,6 +1276,7 @@ async function executeToolInternal(name, args) {
1245
1276
 
1246
1277
  const distance = validatedArgs.distance || 100;
1247
1278
  const duration = validatedArgs.duration || 500;
1279
+ const mode = validatedArgs.mode || 'native';
1248
1280
 
1249
1281
  // Calculate drag deltas based on direction
1250
1282
  let deltaX = 0;
@@ -1281,62 +1313,166 @@ async function executeToolInternal(name, args) {
1281
1313
  break;
1282
1314
  }
1283
1315
 
1284
- // Get element center position for drag start
1285
- const elementInfo = await page.evaluate((selector) => {
1286
- const element = document.querySelector(selector);
1287
- if (!element) {
1288
- return { success: false, error: `Element not found: ${selector}` };
1316
+ if (mode === 'synthetic') {
1317
+ // Synthetic mode: dispatch DOM events for better JS library compatibility
1318
+ const result = await page.evaluate((selector, deltaX, deltaY, duration) => {
1319
+ const element = document.querySelector(selector);
1320
+ if (!element) {
1321
+ return { success: false, error: `Element not found: ${selector}` };
1322
+ }
1323
+
1324
+ const rect = element.getBoundingClientRect();
1325
+ const startX = rect.left + rect.width / 2;
1326
+ const startY = rect.top + rect.height / 2;
1327
+ const endX = startX + deltaX;
1328
+ const endY = startY + deltaY;
1329
+
1330
+ // Helper to create mouse/pointer event
1331
+ const createEvent = (type, clientX, clientY, buttons = 0) => {
1332
+ // Try PointerEvent first (modern browsers)
1333
+ if (typeof PointerEvent !== 'undefined') {
1334
+ return new PointerEvent(type, {
1335
+ bubbles: true,
1336
+ cancelable: true,
1337
+ view: window,
1338
+ clientX,
1339
+ clientY,
1340
+ screenX: clientX,
1341
+ screenY: clientY,
1342
+ buttons,
1343
+ button: 0,
1344
+ pointerId: 1,
1345
+ pointerType: 'mouse',
1346
+ isPrimary: true
1347
+ });
1348
+ }
1349
+ // Fallback to MouseEvent
1350
+ return new MouseEvent(type, {
1351
+ bubbles: true,
1352
+ cancelable: true,
1353
+ view: window,
1354
+ clientX,
1355
+ clientY,
1356
+ screenX: clientX,
1357
+ screenY: clientY,
1358
+ buttons,
1359
+ button: 0
1360
+ });
1361
+ };
1362
+
1363
+ // Dispatch mousedown/pointerdown
1364
+ element.dispatchEvent(createEvent('pointerdown', startX, startY, 1));
1365
+ element.dispatchEvent(createEvent('mousedown', startX, startY, 1));
1366
+
1367
+ // Dispatch intermediate mousemove/pointermove events
1368
+ const steps = Math.max(10, Math.floor(duration / 20));
1369
+ const stepDelay = duration / steps;
1370
+
1371
+ return new Promise((resolve) => {
1372
+ let currentStep = 0;
1373
+
1374
+ const moveInterval = setInterval(() => {
1375
+ currentStep++;
1376
+ const progress = currentStep / steps;
1377
+ const currentX = startX + (deltaX * progress);
1378
+ const currentY = startY + (deltaY * progress);
1379
+
1380
+ element.dispatchEvent(createEvent('pointermove', currentX, currentY, 1));
1381
+ element.dispatchEvent(createEvent('mousemove', currentX, currentY, 1));
1382
+
1383
+ if (currentStep >= steps) {
1384
+ clearInterval(moveInterval);
1385
+
1386
+ // Dispatch mouseup/pointerup
1387
+ element.dispatchEvent(createEvent('pointerup', endX, endY, 0));
1388
+ element.dispatchEvent(createEvent('mouseup', endX, endY, 0));
1389
+ element.dispatchEvent(createEvent('click', endX, endY, 0));
1390
+
1391
+ resolve({
1392
+ success: true,
1393
+ startX: Math.round(startX),
1394
+ startY: Math.round(startY),
1395
+ endX: Math.round(endX),
1396
+ endY: Math.round(endY),
1397
+ mode: 'synthetic'
1398
+ });
1399
+ }
1400
+ }, stepDelay);
1401
+ });
1402
+ }, validatedArgs.selector, deltaX, deltaY, duration);
1403
+
1404
+ if (!result.success) {
1405
+ throw new Error(result.error);
1289
1406
  }
1290
1407
 
1291
- const rect = element.getBoundingClientRect();
1292
1408
  return {
1293
- success: true,
1294
- centerX: rect.left + rect.width / 2,
1295
- centerY: rect.top + rect.height / 2,
1296
- width: rect.width,
1297
- height: rect.height
1409
+ content: [{
1410
+ type: "text",
1411
+ text: `Dragged ${validatedArgs.selector} ${validatedArgs.direction} by ${distance}px (${result.mode} mode):\n` +
1412
+ ` Start position: (${result.startX}, ${result.startY})\n` +
1413
+ ` End position: (${result.endX}, ${result.endY})\n` +
1414
+ ` Delta: (${deltaX}px, ${deltaY}px)\n` +
1415
+ ` Duration: ${duration}ms\n` +
1416
+ ` Events: pointerdown → ${Math.floor(duration / 20)} × pointermove → pointerup`
1417
+ }],
1298
1418
  };
1299
- }, validatedArgs.selector);
1419
+ } else {
1420
+ // Native mode: use Puppeteer mouse API (default, faster)
1421
+ const elementInfo = await page.evaluate((selector) => {
1422
+ const element = document.querySelector(selector);
1423
+ if (!element) {
1424
+ return { success: false, error: `Element not found: ${selector}` };
1425
+ }
1300
1426
 
1301
- if (!elementInfo.success) {
1302
- throw new Error(elementInfo.error);
1303
- }
1427
+ const rect = element.getBoundingClientRect();
1428
+ return {
1429
+ success: true,
1430
+ centerX: rect.left + rect.width / 2,
1431
+ centerY: rect.top + rect.height / 2,
1432
+ width: rect.width,
1433
+ height: rect.height
1434
+ };
1435
+ }, validatedArgs.selector);
1304
1436
 
1305
- // Perform drag: mousedown → mousemove → mouseup
1306
- const startX = elementInfo.centerX;
1307
- const startY = elementInfo.centerY;
1308
- const endX = startX + deltaX;
1309
- const endY = startY + deltaY;
1437
+ if (!elementInfo.success) {
1438
+ throw new Error(elementInfo.error);
1439
+ }
1310
1440
 
1311
- // Move to start position
1312
- await page.mouse.move(startX, startY);
1441
+ const startX = elementInfo.centerX;
1442
+ const startY = elementInfo.centerY;
1443
+ const endX = startX + deltaX;
1444
+ const endY = startY + deltaY;
1313
1445
 
1314
- // Press mouse button (start drag)
1315
- await page.mouse.down();
1446
+ // Move to start position
1447
+ await page.mouse.move(startX, startY);
1316
1448
 
1317
- // Wait a bit to ensure drag is registered
1318
- await new Promise(resolve => setTimeout(resolve, 50));
1449
+ // Press mouse button (start drag)
1450
+ await page.mouse.down();
1319
1451
 
1320
- // Move mouse to end position (drag)
1321
- const steps = Math.max(10, Math.floor(duration / 20)); // Smooth movement
1322
- await page.mouse.move(endX, endY, { steps });
1452
+ // Wait a bit to ensure drag is registered
1453
+ await new Promise(resolve => setTimeout(resolve, 50));
1323
1454
 
1324
- // Wait for duration
1325
- await new Promise(resolve => setTimeout(resolve, Math.max(0, duration - steps * 20)));
1455
+ // Move mouse to end position (drag)
1456
+ const steps = Math.max(10, Math.floor(duration / 20)); // Smooth movement
1457
+ await page.mouse.move(endX, endY, { steps });
1326
1458
 
1327
- // Release mouse button (end drag)
1328
- await page.mouse.up();
1459
+ // Wait for duration
1460
+ await new Promise(resolve => setTimeout(resolve, Math.max(0, duration - steps * 20)));
1329
1461
 
1330
- return {
1331
- content: [{
1332
- type: "text",
1333
- text: `Dragged ${validatedArgs.selector} ${validatedArgs.direction} by ${distance}px:\n` +
1334
- ` Start position: (${Math.round(startX)}, ${Math.round(startY)})\n` +
1335
- ` End position: (${Math.round(endX)}, ${Math.round(endY)})\n` +
1336
- ` Delta: (${deltaX}px, ${deltaY}px)\n` +
1337
- ` Duration: ${duration}ms`
1338
- }],
1339
- };
1462
+ // Release mouse button (end drag)
1463
+ await page.mouse.up();
1464
+
1465
+ return {
1466
+ content: [{
1467
+ type: "text",
1468
+ text: `Dragged ${validatedArgs.selector} ${validatedArgs.direction} by ${distance}px (native mode):\n` +
1469
+ ` Start position: (${Math.round(startX)}, ${Math.round(startY)})\n` +
1470
+ ` End position: (${Math.round(endX)}, ${Math.round(endY)})\n` +
1471
+ ` Delta: (${deltaX}px, ${deltaY}px)\n` +
1472
+ ` Duration: ${duration}ms`
1473
+ }],
1474
+ };
1475
+ }
1340
1476
  }
1341
1477
 
1342
1478
  if (name === "scrollHorizontal") {
@@ -1515,6 +1651,9 @@ async function executeToolInternal(name, args) {
1515
1651
 
1516
1652
  let hintsText = '\n\n** AI HINTS **';
1517
1653
  hintsText += `\nPage type: ${hints.pageType}`;
1654
+ if (hints.heading) {
1655
+ hintsText += `\nPage heading: "${hints.heading}"`;
1656
+ }
1518
1657
  if (hints.availableActions.length > 0) {
1519
1658
  hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
1520
1659
  }
@@ -2105,9 +2244,12 @@ Start coding now.`;
2105
2244
  const maxResults = validatedArgs.maxResults || 5;
2106
2245
 
2107
2246
  // Execute smart search in page context
2108
- const results = await page.evaluate((description, maxResults, utilsCode) => {
2247
+ const results = await page.evaluate((description, maxResults, utilsCode, selectorResolverCode) => {
2109
2248
  // Inject utilities into page context
2110
2249
  eval(utilsCode);
2250
+ if (typeof registerElement === 'undefined') {
2251
+ eval(selectorResolverCode);
2252
+ }
2111
2253
 
2112
2254
  // Determine element type from description
2113
2255
  const elementType = determineElementType(description);
@@ -2163,18 +2305,29 @@ Start coding now.`;
2163
2305
  });
2164
2306
 
2165
2307
  // Filter and sort
2166
- return analyzed
2308
+ const filtered = analyzed
2167
2309
  .filter(r => r.score > 5) // Minimum threshold
2168
2310
  .sort((a, b) => b.score - a.score)
2169
2311
  .slice(0, maxResults);
2170
2312
 
2171
- }, validatedArgs.description, maxResults, elementFinderUtils);
2313
+ // Register found elements in APOM registry and assign IDs
2314
+ filtered.forEach((result, idx) => {
2315
+ const apomId = `smart_${result.type}_${idx}`;
2316
+ result.id = apomId;
2317
+ if (typeof registerElement === 'function') {
2318
+ registerElement(apomId, result.selector, { source: 'smartFindElement' });
2319
+ }
2320
+ });
2321
+
2322
+ return filtered;
2323
+
2324
+ }, validatedArgs.description, maxResults, elementFinderUtils, selectorResolver);
2172
2325
 
2173
2326
  const hints = {
2174
2327
  totalCandidates: results.length,
2175
2328
  bestMatch: results[0] || null,
2176
2329
  suggestion: results.length > 0
2177
- ? `Use selector: ${results[0].selector}`
2330
+ ? `Use id: "${results[0].id}" or selector: ${results[0].selector}`
2178
2331
  : 'No good matches found. Try a different description.',
2179
2332
  };
2180
2333
 
@@ -2212,20 +2365,25 @@ Start coding now.`;
2212
2365
  };
2213
2366
  }
2214
2367
 
2368
+ // Store previous analysis for diff calculation
2369
+ if (!global.previousApomAnalysis) {
2370
+ global.previousApomAnalysis = new Map(); // pageUrl -> analysis data
2371
+ }
2372
+
2215
2373
  if (name === "analyzePage") {
2216
2374
  const validatedArgs = schemas.AnalyzePageSchema.parse(args);
2217
2375
  const page = await getLastOpenPage();
2218
2376
  const pageUrl = page.url();
2219
2377
 
2220
2378
  // APOM Tree format (default) - v2 with tree structure and positioning
2221
- const apomResult = await page.evaluate((apomTreeConverterCode, selectorResolverCode, shouldRegister, includeAll) => {
2379
+ const apomResult = await page.evaluate((apomTreeConverterCode, selectorResolverCode, shouldRegister, includeAll, viewportOnly) => {
2222
2380
  // Inject utilities
2223
2381
  eval(apomTreeConverterCode);
2224
2382
  eval(selectorResolverCode);
2225
2383
 
2226
2384
  // Build APOM tree
2227
2385
  // interactiveOnly = !includeAll (if includeAll is true, we want ALL elements)
2228
- const apomData = buildAPOMTree(!includeAll);
2386
+ const apomData = buildAPOMTree(!includeAll, viewportOnly);
2229
2387
 
2230
2388
  // Register elements in selector resolver if requested
2231
2389
  if (shouldRegister) {
@@ -2258,7 +2416,43 @@ Start coding now.`;
2258
2416
  }
2259
2417
 
2260
2418
  return apomData;
2261
- }, apomTreeConverter, selectorResolver, validatedArgs.registerElements !== false, validatedArgs.includeAll || false);
2419
+ }, apomTreeConverter, selectorResolver, validatedArgs.registerElements !== false, validatedArgs.includeAll || false, validatedArgs.viewportOnly || false);
2420
+
2421
+ // Handle diff mode
2422
+ if (validatedArgs.diff) {
2423
+ const previousAnalysis = global.previousApomAnalysis.get(pageUrl);
2424
+
2425
+ if (previousAnalysis) {
2426
+ // Calculate diff
2427
+ const diff = calculateApomDiff(previousAnalysis, apomResult);
2428
+
2429
+ // Store current analysis for next diff
2430
+ global.previousApomAnalysis.set(pageUrl, apomResult);
2431
+
2432
+ return {
2433
+ content: [{
2434
+ type: 'text',
2435
+ text: JSON.stringify({
2436
+ mode: 'diff',
2437
+ pageId: apomResult.pageId,
2438
+ url: apomResult.url,
2439
+ timestamp: apomResult.timestamp,
2440
+ previousTimestamp: previousAnalysis.timestamp,
2441
+ diff,
2442
+ metadata: apomResult.metadata,
2443
+ alerts: apomResult.alerts
2444
+ })
2445
+ }]
2446
+ };
2447
+ } else {
2448
+ // No previous analysis, return full result with note
2449
+ global.previousApomAnalysis.set(pageUrl, apomResult);
2450
+ apomResult._note = 'First analysis for this page, no diff available';
2451
+ }
2452
+ } else {
2453
+ // Store for future diff
2454
+ global.previousApomAnalysis.set(pageUrl, apomResult);
2455
+ }
2262
2456
 
2263
2457
  return {
2264
2458
  content: [{
@@ -2268,6 +2462,82 @@ Start coding now.`;
2268
2462
  };
2269
2463
  }
2270
2464
 
2465
+ /**
2466
+ * Calculate diff between two APOM analyses
2467
+ */
2468
+ function calculateApomDiff(previous, current) {
2469
+ const previousElements = flattenApomTree(previous.tree);
2470
+ const currentElements = flattenApomTree(current.tree);
2471
+
2472
+ const previousIds = new Set(previousElements.map(e => e.id));
2473
+ const currentIds = new Set(currentElements.map(e => e.id));
2474
+
2475
+ const added = currentElements.filter(e => !previousIds.has(e.id));
2476
+ const removed = previousElements.filter(e => !currentIds.has(e.id));
2477
+
2478
+ // Find changed elements (same ID but different content)
2479
+ const changed = [];
2480
+ for (const curr of currentElements) {
2481
+ if (previousIds.has(curr.id)) {
2482
+ const prev = previousElements.find(e => e.id === curr.id);
2483
+ if (prev && JSON.stringify(prev.metadata) !== JSON.stringify(curr.metadata)) {
2484
+ changed.push({
2485
+ id: curr.id,
2486
+ type: curr.type,
2487
+ before: prev.metadata,
2488
+ after: curr.metadata
2489
+ });
2490
+ }
2491
+ }
2492
+ }
2493
+
2494
+ return {
2495
+ added: added.length > 0 ? added : undefined,
2496
+ removed: removed.length > 0 ? removed : undefined,
2497
+ changed: changed.length > 0 ? changed : undefined,
2498
+ summary: {
2499
+ addedCount: added.length,
2500
+ removedCount: removed.length,
2501
+ changedCount: changed.length
2502
+ }
2503
+ };
2504
+ }
2505
+
2506
+ /**
2507
+ * Flatten APOM tree to array of elements
2508
+ */
2509
+ function flattenApomTree(node, result = []) {
2510
+ if (!node) return result;
2511
+
2512
+ // Handle compact format: { "tag_id": [children] }
2513
+ if (typeof node === 'object' && !node.id && !node.tag) {
2514
+ const keys = Object.keys(node);
2515
+ for (const key of keys) {
2516
+ if (Array.isArray(node[key])) {
2517
+ node[key].forEach(child => flattenApomTree(child, result));
2518
+ }
2519
+ }
2520
+ return result;
2521
+ }
2522
+
2523
+ // Interactive element with id
2524
+ if (node.id) {
2525
+ result.push({
2526
+ id: node.id,
2527
+ tag: node.tag,
2528
+ type: node.type,
2529
+ metadata: node.metadata
2530
+ });
2531
+ }
2532
+
2533
+ // Process children
2534
+ if (node.children) {
2535
+ node.children.forEach(child => flattenApomTree(child, result));
2536
+ }
2537
+
2538
+ return result;
2539
+ }
2540
+
2271
2541
  if (name === "getElementDetails") {
2272
2542
  const validatedArgs = schemas.GetElementDetailsSchema.parse(args);
2273
2543
  const page = await getLastOpenPage();
@@ -2419,8 +2689,11 @@ Start coding now.`;
2419
2689
  const validatedArgs = schemas.FindElementsByTextSchema.parse(args);
2420
2690
  const page = await getLastOpenPage();
2421
2691
 
2422
- const elements = await page.evaluate((text, exact, caseSensitive, utilsCode) => {
2692
+ const elements = await page.evaluate((text, exact, caseSensitive, utilsCode, selectorResolverCode) => {
2423
2693
  eval(utilsCode);
2694
+ if (typeof registerElement === 'undefined') {
2695
+ eval(selectorResolverCode);
2696
+ }
2424
2697
 
2425
2698
  const results = [];
2426
2699
  const searchText = caseSensitive ? text : text.toLowerCase();
@@ -2447,9 +2720,16 @@ Start coding now.`;
2447
2720
  : compareText.includes(searchText);
2448
2721
 
2449
2722
  if (matches) {
2723
+ const selector = getUniqueSelectorInPage(el);
2724
+ const type = el.tagName.toLowerCase();
2725
+ const apomId = `text_${type}_${results.length}`;
2726
+ if (typeof registerElement === 'function') {
2727
+ registerElement(apomId, selector, { source: 'findElementsByText' });
2728
+ }
2450
2729
  results.push({
2451
- selector: getUniqueSelectorInPage(el),
2452
- type: el.tagName.toLowerCase(),
2730
+ id: apomId,
2731
+ selector,
2732
+ type,
2453
2733
  text: elementText.substring(0, 100), // Only first 100 chars for preview
2454
2734
  visible: el.offsetParent !== null, // Add visibility check
2455
2735
  });
@@ -2457,7 +2737,7 @@ Start coding now.`;
2457
2737
  });
2458
2738
 
2459
2739
  return results;
2460
- }, validatedArgs.text, validatedArgs.exact || false, validatedArgs.caseSensitive || false, elementFinderUtils);
2740
+ }, validatedArgs.text, validatedArgs.exact || false, validatedArgs.caseSensitive || false, elementFinderUtils, selectorResolver);
2461
2741
 
2462
2742
  // Prioritize visible elements and limit results to prevent token overflow
2463
2743
  const visibleElements = elements.filter(el => el.visible);
@@ -2972,13 +3252,102 @@ Start coding now.`;
2972
3252
  };
2973
3253
  }
2974
3254
 
3255
+ // Resolve pageObjectMode (backward compat: generatePageObject: true -> 'generate')
3256
+ const pageObjectMode = args.pageObjectMode || (args.generatePageObject ? 'generate' : 'none');
3257
+
2975
3258
  // Select generator based on language
2976
- let generator;
2977
3259
  const options = {
2978
3260
  cleanSelectors: args.cleanSelectors !== false, // default true
2979
3261
  includeComments: args.includeComments !== false, // default true
2980
3262
  };
2981
3263
 
3264
+ // Resolve POM elements for integrated modes
3265
+ let pomElements = null;
3266
+ let pomClassName = null;
3267
+ let pomImportPath = null;
3268
+ let pageObjectData = null;
3269
+
3270
+ if (pageObjectMode === 'generate-integrated' || pageObjectMode === 'generate') {
3271
+ try {
3272
+ const entryUrl = scenario.metadata?.entryUrl;
3273
+ if (entryUrl) {
3274
+ let page;
3275
+ try {
3276
+ page = await getLastOpenPage();
3277
+ const currentUrl = page.url();
3278
+ if (currentUrl !== entryUrl) {
3279
+ await page.goto(entryUrl, { waitUntil: 'networkidle2' });
3280
+ }
3281
+ } catch (error) {
3282
+ page = await getOrCreatePage(entryUrl);
3283
+ }
3284
+
3285
+ const pageObjectOptions = {
3286
+ className: args.pageObjectClassName || null,
3287
+ framework: args.language,
3288
+ includeComments: args.includeComments !== false,
3289
+ groupElements: true
3290
+ };
3291
+
3292
+ const pageObjectResult = await generatePageObject(page, pageObjectOptions);
3293
+ if (pageObjectResult.success) {
3294
+ const extension = args.language.includes('typescript') ? '.ts' :
3295
+ args.language.includes('java') ? '.java' : '.py';
3296
+ pageObjectData = {
3297
+ code: pageObjectResult.code,
3298
+ className: pageObjectResult.className,
3299
+ suggestedFileName: `${pageObjectResult.className}${extension}`,
3300
+ elementCount: pageObjectResult.elementCount
3301
+ };
3302
+
3303
+ if (pageObjectMode === 'generate-integrated') {
3304
+ pomElements = pageObjectResult.elements;
3305
+ pomClassName = pageObjectResult.className;
3306
+ }
3307
+ }
3308
+ }
3309
+ } catch (error) {
3310
+ // Page Object generation failed, continue without it
3311
+ }
3312
+ } else if (pageObjectMode === 'use-existing') {
3313
+ if (!args.pageObjectFile) {
3314
+ return {
3315
+ content: [{
3316
+ type: 'text',
3317
+ text: JSON.stringify({
3318
+ error: "pageObjectFile is required for 'use-existing' mode"
3319
+ }, null, 2)
3320
+ }],
3321
+ isError: true
3322
+ };
3323
+ }
3324
+
3325
+ try {
3326
+ const pomContent = FileAppender.readFile(args.pageObjectFile);
3327
+ const parsed = parsePomFile(pomContent, args.language);
3328
+ pomElements = parsed.elements;
3329
+ pomClassName = parsed.className;
3330
+ } catch (error) {
3331
+ return {
3332
+ content: [{
3333
+ type: 'text',
3334
+ text: JSON.stringify({
3335
+ error: `Failed to parse POM file: ${error.message}`
3336
+ }, null, 2)
3337
+ }],
3338
+ isError: true
3339
+ };
3340
+ }
3341
+ }
3342
+
3343
+ // Add POM options to generator
3344
+ if (pomElements && pomClassName) {
3345
+ options.pomElements = pomElements;
3346
+ options.pomClassName = pomClassName;
3347
+ options.pomImportPath = pomImportPath;
3348
+ }
3349
+
3350
+ let generator;
2982
3351
  switch (args.language) {
2983
3352
  case 'playwright-typescript':
2984
3353
  generator = new PlaywrightTypeScriptGenerator(options);
@@ -3017,59 +3386,21 @@ Start coding now.`;
3017
3386
  referenceTestName: args.referenceTestName
3018
3387
  };
3019
3388
 
3020
- // Generate Page Object if requested
3021
- let pageObjectData = null;
3022
- if (args.generatePageObject) {
3023
- try {
3024
- const entryUrl = scenario.metadata?.entryUrl;
3025
- if (entryUrl) {
3026
- let page;
3027
- try {
3028
- page = await getLastOpenPage();
3029
- const currentUrl = page.url();
3030
- if (currentUrl !== entryUrl) {
3031
- await page.goto(entryUrl, { waitUntil: 'networkidle2' });
3032
- }
3033
- } catch (error) {
3034
- page = await getOrCreatePage(entryUrl);
3035
- }
3036
-
3037
- const pageObjectOptions = {
3038
- className: args.pageObjectClassName || null,
3039
- framework: args.language,
3040
- includeComments: args.includeComments !== false,
3041
- groupElements: true
3042
- };
3043
-
3044
- const pageObjectResult = await generatePageObject(page, pageObjectOptions);
3045
- if (pageObjectResult.success) {
3046
- // Suggest filename based on className
3047
- const extension = args.language.includes('typescript') ? '.ts' :
3048
- args.language.includes('java') ? '.java' : '.py';
3049
- pageObjectData = {
3050
- code: pageObjectResult.code,
3051
- className: pageObjectResult.className,
3052
- suggestedFileName: `${pageObjectResult.className}${extension}`,
3053
- elementCount: pageObjectResult.elementCount
3054
- };
3055
- }
3056
- }
3057
- } catch (error) {
3058
- // Page Object generation failed, continue without it
3059
- }
3060
- }
3061
-
3062
3389
  // Return JSON with instructions for Claude Code to append the test
3063
3390
  const result = {
3064
3391
  action: 'append_test',
3065
3392
  targetFile: args.targetFile,
3066
- testCode: testOnly, // Only test code, no imports
3393
+ testCode: testOnly,
3067
3394
  testName: args.testName || scenario.metadata?.name,
3068
3395
  insertPosition: appendOptions.insertPosition,
3069
3396
  referenceTestName: appendOptions.referenceTestName,
3070
3397
  instruction: `Read file '${args.targetFile}', append the testCode at position '${appendOptions.insertPosition}', then write the file back.`
3071
3398
  };
3072
3399
 
3400
+ if (pomClassName) {
3401
+ result.pomIntegration = { className: pomClassName, mode: pageObjectMode };
3402
+ }
3403
+
3073
3404
  if (pageObjectData) {
3074
3405
  result.pageObject = pageObjectData;
3075
3406
  result.instruction += ` Also create a Page Object file '${pageObjectData.suggestedFileName}' with the provided pageObject.code.`;
@@ -3111,86 +3442,48 @@ Start coding now.`;
3111
3442
  };
3112
3443
  }
3113
3444
 
3114
- // Select generator based on language
3115
- let generator;
3445
+ // Resolve pageObjectMode (backward compat: generatePageObject: true -> 'generate')
3446
+ const pageObjectMode = args.pageObjectMode || (args.generatePageObject ? 'generate' : 'none');
3447
+
3116
3448
  const options = {
3117
3449
  cleanSelectors: args.cleanSelectors !== false, // default true
3118
3450
  includeComments: args.includeComments !== false, // default true
3119
3451
  };
3120
3452
 
3121
- switch (args.language) {
3122
- case 'playwright-typescript':
3123
- generator = new PlaywrightTypeScriptGenerator(options);
3124
- break;
3125
- case 'playwright-python':
3126
- generator = new PlaywrightPythonGenerator(options);
3127
- break;
3128
- case 'selenium-python':
3129
- generator = new SeleniumPythonGenerator(options);
3130
- break;
3131
- case 'selenium-java':
3132
- generator = new SeleniumJavaGenerator(options);
3133
- break;
3134
- default:
3453
+ // Resolve POM elements for integrated modes
3454
+ let pomElements = null;
3455
+ let pomClassName = null;
3456
+ let pageObjectData = null;
3457
+ const entryUrl = scenario.metadata?.entryUrl;
3458
+
3459
+ if (pageObjectMode === 'generate-integrated' || pageObjectMode === 'generate') {
3460
+ if (!entryUrl) {
3135
3461
  return {
3136
3462
  content: [{
3137
3463
  type: 'text',
3138
3464
  text: JSON.stringify({
3139
- error: `Unknown language: ${args.language}. Supported: playwright-typescript, playwright-python, selenium-python, selenium-java`
3465
+ error: 'Cannot generate Page Object: scenario has no entryUrl in metadata'
3140
3466
  }, null, 2)
3141
3467
  }],
3142
3468
  isError: true
3143
3469
  };
3144
- }
3145
-
3146
- // Generate test code with full imports
3147
- const testCode = generator.generate(scenario, options);
3148
-
3149
- // Generate suggested filename
3150
- const testName = scenario.metadata?.name || 'test';
3151
- const extension = args.language.includes('typescript') ? '.spec.ts' :
3152
- args.language.includes('java') ? 'Test.java' :
3153
- args.language.includes('python') ? '_test.py' : '.test.js';
3154
- const suggestedFileName = args.language.includes('java')
3155
- ? testName.charAt(0).toUpperCase() + testName.slice(1) + 'Test.java'
3156
- : testName.replace(/\s+/g, '_').toLowerCase() + extension;
3470
+ }
3157
3471
 
3158
- // If generatePageObject is requested, also generate Page Object class
3159
- if (args.generatePageObject) {
3160
3472
  try {
3161
- // Get page - need to open at scenario's entry URL
3162
3473
  let page;
3163
- const entryUrl = scenario.metadata?.entryUrl;
3164
-
3165
- if (!entryUrl) {
3166
- return {
3167
- content: [{
3168
- type: 'text',
3169
- text: JSON.stringify({
3170
- error: 'Cannot generate Page Object: scenario has no entryUrl in metadata'
3171
- }, null, 2)
3172
- }],
3173
- isError: true
3174
- };
3175
- }
3176
-
3177
- // Try to get existing page or open new one
3178
3474
  try {
3179
3475
  page = await getLastOpenPage();
3180
- // Navigate to entry URL if current page is different
3181
3476
  const currentUrl = page.url();
3182
3477
  if (currentUrl !== entryUrl) {
3183
3478
  await page.goto(entryUrl, { waitUntil: 'networkidle2' });
3184
3479
  }
3185
3480
  } catch (error) {
3186
- // No page open, create new one
3187
3481
  page = await getOrCreatePage(entryUrl);
3188
3482
  }
3189
3483
 
3190
- // Generate Page Object
3191
3484
  const pageObjectOptions = {
3192
3485
  className: args.pageObjectClassName || null,
3193
- framework: args.language, // Use same framework as test
3486
+ framework: args.language,
3194
3487
  includeComments: args.includeComments !== false,
3195
3488
  groupElements: true
3196
3489
  };
@@ -3198,71 +3491,119 @@ Start coding now.`;
3198
3491
  const pageObjectResult = await generatePageObject(page, pageObjectOptions);
3199
3492
 
3200
3493
  if (pageObjectResult.success) {
3201
- // Suggest Page Object filename
3202
3494
  const poExtension = args.language.includes('typescript') ? '.ts' :
3203
3495
  args.language.includes('java') ? '.java' : '.py';
3204
- const pageObjectFileName = `${pageObjectResult.className}${poExtension}`;
3205
-
3206
- // Return both test code and Page Object code
3207
- return {
3208
- content: [{
3209
- type: 'text',
3210
- text: JSON.stringify({
3211
- action: 'create_new_file',
3212
- suggestedFileName: suggestedFileName,
3213
- testCode: testCode,
3214
- pageObject: {
3215
- code: pageObjectResult.code,
3216
- className: pageObjectResult.className,
3217
- suggestedFileName: pageObjectFileName,
3218
- elementCount: pageObjectResult.elementCount
3219
- },
3220
- instruction: `Create a new test file '${suggestedFileName}' with the testCode. Also create a Page Object file '${pageObjectFileName}' with the pageObject.code.`
3221
- }, null, 2)
3222
- }]
3223
- };
3224
- } else {
3225
- // Page Object generation failed, return test code only with warning
3226
- return {
3227
- content: [{
3228
- type: 'text',
3229
- text: JSON.stringify({
3230
- action: 'create_new_file',
3231
- suggestedFileName: suggestedFileName,
3232
- testCode: testCode,
3233
- warning: 'Page Object generation failed: ' + (pageObjectResult.error || 'Unknown error'),
3234
- instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
3235
- }, null, 2)
3236
- }]
3496
+ pageObjectData = {
3497
+ code: pageObjectResult.code,
3498
+ className: pageObjectResult.className,
3499
+ suggestedFileName: `${pageObjectResult.className}${poExtension}`,
3500
+ elementCount: pageObjectResult.elementCount
3237
3501
  };
3502
+
3503
+ if (pageObjectMode === 'generate-integrated') {
3504
+ pomElements = pageObjectResult.elements;
3505
+ pomClassName = pageObjectResult.className;
3506
+ }
3238
3507
  }
3239
3508
  } catch (error) {
3240
- // Page Object generation failed, return test code only with error
3509
+ // Page Object generation failed, continue without it
3510
+ }
3511
+ } else if (pageObjectMode === 'use-existing') {
3512
+ if (!args.pageObjectFile) {
3241
3513
  return {
3242
3514
  content: [{
3243
3515
  type: 'text',
3244
3516
  text: JSON.stringify({
3245
- action: 'create_new_file',
3246
- suggestedFileName: suggestedFileName,
3247
- testCode: testCode,
3248
- warning: 'Page Object generation error: ' + error.message,
3249
- instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
3517
+ error: "pageObjectFile is required for 'use-existing' mode"
3250
3518
  }, null, 2)
3251
- }]
3519
+ }],
3520
+ isError: true
3521
+ };
3522
+ }
3523
+
3524
+ try {
3525
+ const pomContent = FileAppender.readFile(args.pageObjectFile);
3526
+ const parsed = parsePomFile(pomContent, args.language);
3527
+ pomElements = parsed.elements;
3528
+ pomClassName = parsed.className;
3529
+ } catch (error) {
3530
+ return {
3531
+ content: [{
3532
+ type: 'text',
3533
+ text: JSON.stringify({
3534
+ error: `Failed to parse POM file: ${error.message}`
3535
+ }, null, 2)
3536
+ }],
3537
+ isError: true
3252
3538
  };
3253
3539
  }
3254
3540
  }
3255
3541
 
3256
- // Default: return test code only
3542
+ // Add POM options to generator
3543
+ if (pomElements && pomClassName) {
3544
+ options.pomElements = pomElements;
3545
+ options.pomClassName = pomClassName;
3546
+ }
3547
+
3548
+ let generator;
3549
+ switch (args.language) {
3550
+ case 'playwright-typescript':
3551
+ generator = new PlaywrightTypeScriptGenerator(options);
3552
+ break;
3553
+ case 'playwright-python':
3554
+ generator = new PlaywrightPythonGenerator(options);
3555
+ break;
3556
+ case 'selenium-python':
3557
+ generator = new SeleniumPythonGenerator(options);
3558
+ break;
3559
+ case 'selenium-java':
3560
+ generator = new SeleniumJavaGenerator(options);
3561
+ break;
3562
+ default:
3563
+ return {
3564
+ content: [{
3565
+ type: 'text',
3566
+ text: JSON.stringify({
3567
+ error: `Unknown language: ${args.language}. Supported: playwright-typescript, playwright-python, selenium-python, selenium-java`
3568
+ }, null, 2)
3569
+ }],
3570
+ isError: true
3571
+ };
3572
+ }
3573
+
3574
+ // Generate test code with full imports
3575
+ const testCode = generator.generate(scenario, options);
3576
+
3577
+ // Generate suggested filename
3578
+ const testName = scenario.metadata?.name || 'test';
3579
+ const extension = args.language.includes('typescript') ? '.spec.ts' :
3580
+ args.language.includes('java') ? 'Test.java' :
3581
+ args.language.includes('python') ? '_test.py' : '.test.js';
3582
+ const suggestedFileName = args.language.includes('java')
3583
+ ? testName.charAt(0).toUpperCase() + testName.slice(1) + 'Test.java'
3584
+ : testName.replace(/\s+/g, '_').toLowerCase() + extension;
3585
+
3586
+ // Build result
3587
+ const result = {
3588
+ action: 'create_new_file',
3589
+ suggestedFileName: suggestedFileName,
3590
+ testCode: testCode,
3591
+ instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
3592
+ };
3593
+
3594
+ if (pomClassName) {
3595
+ result.pomIntegration = { className: pomClassName, mode: pageObjectMode };
3596
+ }
3597
+
3598
+ if (pageObjectData) {
3599
+ result.pageObject = pageObjectData;
3600
+ result.instruction = `Create a new test file '${suggestedFileName}' with the testCode. Also create a Page Object file '${pageObjectData.suggestedFileName}' with the pageObject.code.`;
3601
+ }
3602
+
3257
3603
  return {
3258
3604
  content: [{
3259
3605
  type: 'text',
3260
- text: JSON.stringify({
3261
- action: 'create_new_file',
3262
- suggestedFileName: suggestedFileName,
3263
- testCode: testCode,
3264
- instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
3265
- }, null, 2)
3606
+ text: JSON.stringify(result, null, 2)
3266
3607
  }]
3267
3608
  };
3268
3609
  }
@@ -3424,6 +3765,87 @@ Start coding now.`;
3424
3765
  };
3425
3766
  }
3426
3767
 
3768
+ // ========== API / Swagger Tools ==========
3769
+
3770
+ if (name === "loadSwagger") {
3771
+ const validatedArgs = schemas.LoadSwaggerSchema.parse(args);
3772
+ const parser = await OpenAPIParser.load(validatedArgs.source, validatedArgs.format || 'auto');
3773
+ const summary = parser.getSummary();
3774
+
3775
+ return {
3776
+ content: [{
3777
+ type: 'text',
3778
+ text: JSON.stringify({
3779
+ success: true,
3780
+ ...summary,
3781
+ instruction: 'Use generateApiModels to generate typed models from these schemas.'
3782
+ }, null, 2)
3783
+ }]
3784
+ };
3785
+ }
3786
+
3787
+ if (name === "generateApiModels") {
3788
+ const validatedArgs = schemas.GenerateApiModelsSchema.parse(args);
3789
+ const parser = await OpenAPIParser.load(validatedArgs.source, validatedArgs.format || 'auto');
3790
+ let schemasObj = parser.getSchemas();
3791
+
3792
+ // Filter schemas if specified
3793
+ if (validatedArgs.schemas && validatedArgs.schemas.length > 0) {
3794
+ const filtered = {};
3795
+ for (const schemaName of validatedArgs.schemas) {
3796
+ if (schemasObj[schemaName]) filtered[schemaName] = schemasObj[schemaName];
3797
+ }
3798
+ schemasObj = filtered;
3799
+ }
3800
+
3801
+ const metadata = {
3802
+ title: parser.spec.info?.title || '',
3803
+ source: validatedArgs.source,
3804
+ version: parser.version
3805
+ };
3806
+
3807
+ let code, suggestedFileName;
3808
+
3809
+ if (validatedArgs.language === 'typescript') {
3810
+ const generator = new ApiModelsTypeScriptGenerator(schemasObj, {
3811
+ style: validatedArgs.style || 'interface',
3812
+ includeEnums: validatedArgs.includeEnums !== false,
3813
+ includeValidation: validatedArgs.includeValidation || false,
3814
+ });
3815
+ code = generator.generate(metadata);
3816
+ const titleSlug = (metadata.title || 'api').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
3817
+ suggestedFileName = `${titleSlug}.models.ts`;
3818
+ } else {
3819
+ const generator = new ApiModelsPythonGenerator(schemasObj, {
3820
+ style: validatedArgs.pythonStyle || 'dataclass',
3821
+ includeEnums: validatedArgs.includeEnums !== false,
3822
+ includeValidation: validatedArgs.includeValidation || false,
3823
+ });
3824
+ code = generator.generate(metadata);
3825
+ const titleSlug = (metadata.title || 'api').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
3826
+ suggestedFileName = `${titleSlug}_models.py`;
3827
+ }
3828
+
3829
+ const schemaCount = Object.keys(schemasObj).length;
3830
+ const enumCount = Object.values(schemasObj).filter(s => s.enum && s.type === 'string').length;
3831
+
3832
+ return {
3833
+ content: [{
3834
+ type: 'text',
3835
+ text: JSON.stringify({
3836
+ action: 'create_new_file',
3837
+ suggestedFileName,
3838
+ code,
3839
+ schemaCount,
3840
+ enumCount,
3841
+ language: validatedArgs.language,
3842
+ source: validatedArgs.source,
3843
+ instruction: `Create file '${suggestedFileName}' with the generated code.`
3844
+ }, null, 2)
3845
+ }]
3846
+ };
3847
+ }
3848
+
3427
3849
  return {
3428
3850
  content: [
3429
3851
  {