chrometools-mcp 3.1.6 → 3.2.4
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 +97 -3
- package/README.md +157 -108
- package/README.ru.md +331 -0
- package/chrome-extension.zip +0 -0
- package/docs/extension-developer-mode.png +0 -0
- package/docs/extension-installed.png +0 -0
- package/index.js +95 -44
- package/package.json +1 -1
- package/pom/apom-tree-converter.js +302 -39
- package/server/tool-definitions.js +23 -30
- package/server/tool-schemas.js +5 -6
- package/test-interactivity.html +178 -0
|
@@ -43,6 +43,11 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
43
43
|
// Build tree from body
|
|
44
44
|
result.tree = buildNode(document.body, null, 0, []);
|
|
45
45
|
|
|
46
|
+
// Prune empty branches (containers without interactive elements)
|
|
47
|
+
if (interactiveOnly && result.tree) {
|
|
48
|
+
result.tree = pruneEmptyBranches(result.tree);
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
// Collect radio and checkbox groups for easier agent access
|
|
47
52
|
result.groups = collectInputGroups(result.tree);
|
|
48
53
|
|
|
@@ -102,29 +107,109 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
/**
|
|
105
|
-
*
|
|
110
|
+
* Prune empty branches (containers without interactive elements)
|
|
111
|
+
* Bottom-up traversal: remove container branches that don't end with interactive leaves
|
|
106
112
|
*/
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
function pruneEmptyBranches(node) {
|
|
114
|
+
if (!node) return null;
|
|
115
|
+
|
|
116
|
+
// Handle compact format containers
|
|
117
|
+
if (typeof node === 'object' && !node.id && !node.tag) {
|
|
118
|
+
// Compact format: { "tag_id": [children] }
|
|
119
|
+
const keys = Object.keys(node);
|
|
120
|
+
if (keys.length === 1 && Array.isArray(node[keys[0]])) {
|
|
121
|
+
const key = keys[0];
|
|
122
|
+
const prunedChildren = node[key]
|
|
123
|
+
.map(child => pruneEmptyBranches(child))
|
|
124
|
+
.filter(child => child !== null);
|
|
125
|
+
|
|
126
|
+
// If no children left after pruning, remove this container
|
|
127
|
+
if (prunedChildren.length === 0) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
111
130
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
]);
|
|
131
|
+
return { [key]: prunedChildren };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
116
134
|
|
|
117
|
-
//
|
|
135
|
+
// Handle regular format (interactive elements or full-mode containers)
|
|
136
|
+
if (node.children && Array.isArray(node.children)) {
|
|
137
|
+
// Prune children recursively
|
|
138
|
+
node.children = node.children
|
|
139
|
+
.map(child => pruneEmptyBranches(child))
|
|
140
|
+
.filter(child => child !== null);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If this is a container (no type = not interactive) with no children, remove it
|
|
144
|
+
if (!node.type && node.children && node.children.length === 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return node;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if cursor:pointer is explicitly set (not inherited)
|
|
153
|
+
*/
|
|
154
|
+
function hasCursorPointerExplicit(element) {
|
|
155
|
+
const computedStyle = window.getComputedStyle(element);
|
|
156
|
+
if (computedStyle.cursor !== 'pointer') {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if cursor is set via inline style
|
|
161
|
+
if (element.style.cursor === 'pointer') {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check if cursor is set via CSS class or direct CSS rule (not inherited)
|
|
166
|
+
// If parent also has cursor:pointer computed, then it's likely inherited
|
|
167
|
+
const parent = element.parentElement;
|
|
168
|
+
if (parent) {
|
|
169
|
+
const parentStyle = window.getComputedStyle(parent);
|
|
170
|
+
if (parentStyle.cursor === 'pointer') {
|
|
171
|
+
// Parent has cursor:pointer, so this is inherited
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Element has cursor:pointer but parent doesn't - it's explicitly set
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Mark interactive elements and their ancestors
|
|
182
|
+
* NOTE: This function is defined before checkInteractivity,
|
|
183
|
+
* so we need to inline the checks or move function definitions
|
|
184
|
+
*/
|
|
185
|
+
function markInteractiveElements(root) {
|
|
186
|
+
// Find all interactive elements using the same logic as checkInteractivity
|
|
118
187
|
const elements = root.querySelectorAll('*');
|
|
119
188
|
const interactiveList = [];
|
|
120
189
|
|
|
121
190
|
elements.forEach(el => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
191
|
+
// Use inline checks (same logic as checkInteractivity)
|
|
192
|
+
const tag = el.tagName.toLowerCase();
|
|
193
|
+
const role = el.getAttribute('role');
|
|
194
|
+
|
|
195
|
+
const isInteractive = (
|
|
196
|
+
// Native HTML interactive elements
|
|
197
|
+
['a', 'button', 'input', 'select', 'textarea', 'label', 'form'].includes(tag) ||
|
|
198
|
+
// Interactive ARIA roles
|
|
199
|
+
(role && ['button', 'link', 'checkbox', 'radio', 'tab', 'menuitem', 'option', 'switch', 'textbox'].includes(role)) ||
|
|
200
|
+
// onclick attribute
|
|
125
201
|
el.hasAttribute('onclick') ||
|
|
126
|
-
|
|
127
|
-
(el.
|
|
202
|
+
// onclick property
|
|
203
|
+
(el.onclick !== null && el.onclick !== undefined) ||
|
|
204
|
+
// cursor: pointer (only if explicitly set, not inherited)
|
|
205
|
+
hasCursorPointerExplicit(el) ||
|
|
206
|
+
// tabindex (except -1)
|
|
207
|
+
(el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1') ||
|
|
208
|
+
// contenteditable
|
|
209
|
+
el.getAttribute('contenteditable') === 'true'
|
|
210
|
+
// Note: We skip event listener check here for performance
|
|
211
|
+
// as querySelectorAll can return thousands of elements
|
|
212
|
+
);
|
|
128
213
|
|
|
129
214
|
if (isInteractive && isVisible(el)) {
|
|
130
215
|
interactiveList.push(el);
|
|
@@ -169,10 +254,17 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
169
254
|
return null;
|
|
170
255
|
}
|
|
171
256
|
|
|
172
|
-
// Generate unique ID
|
|
257
|
+
// Generate unique ID and CSS selector
|
|
173
258
|
const id = generateElementId(element);
|
|
259
|
+
const selector = generateSelector(element);
|
|
174
260
|
elementIds.set(element, id);
|
|
175
261
|
|
|
262
|
+
// Register element in selector resolver (internal only)
|
|
263
|
+
if (typeof window !== 'undefined' && typeof window.registerElement === 'function') {
|
|
264
|
+
const tag = element.tagName.toLowerCase();
|
|
265
|
+
window.registerElement(id, selector, { tag, depth });
|
|
266
|
+
}
|
|
267
|
+
|
|
176
268
|
const currentPath = [...path, id];
|
|
177
269
|
|
|
178
270
|
// Get positioning info
|
|
@@ -184,26 +276,47 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
184
276
|
// Build node - minimize non-interactive parents
|
|
185
277
|
const isInteractive = elementType.isInteractive;
|
|
186
278
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
tag: element.tagName.toLowerCase(),
|
|
190
|
-
selector: generateSelector(element),
|
|
191
|
-
position,
|
|
192
|
-
children: []
|
|
193
|
-
};
|
|
279
|
+
// Build node structure based on mode
|
|
280
|
+
let node;
|
|
194
281
|
|
|
195
|
-
// Add full info only for interactive elements
|
|
196
282
|
if (isInteractive) {
|
|
197
|
-
|
|
198
|
-
node
|
|
283
|
+
// Interactive elements: full structure without selector (unless includeAll)
|
|
284
|
+
node = {
|
|
285
|
+
id,
|
|
286
|
+
tag: element.tagName.toLowerCase(),
|
|
287
|
+
position,
|
|
288
|
+
type: elementType.type,
|
|
289
|
+
children: []
|
|
290
|
+
};
|
|
199
291
|
|
|
200
|
-
// Add
|
|
292
|
+
// Add selector only in includeAll mode
|
|
293
|
+
if (!interactiveOnly) {
|
|
294
|
+
node.selector = selector;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Add metadata for interactive elements
|
|
201
298
|
if (elementType.metadata) {
|
|
202
299
|
node.metadata = elementType.metadata;
|
|
203
300
|
}
|
|
204
301
|
} else {
|
|
205
|
-
//
|
|
206
|
-
|
|
302
|
+
// Containers: compact format "tag_id": [children] when interactiveOnly
|
|
303
|
+
// or full format when includeAll
|
|
304
|
+
if (interactiveOnly) {
|
|
305
|
+
// Compact format - will be converted after processing children
|
|
306
|
+
node = {
|
|
307
|
+
_compact: true,
|
|
308
|
+
_key: `${element.tagName.toLowerCase()}_${id}`,
|
|
309
|
+
children: []
|
|
310
|
+
};
|
|
311
|
+
} else {
|
|
312
|
+
// Full format with selector
|
|
313
|
+
node = {
|
|
314
|
+
id,
|
|
315
|
+
tag: element.tagName.toLowerCase(),
|
|
316
|
+
selector,
|
|
317
|
+
children: []
|
|
318
|
+
};
|
|
319
|
+
}
|
|
207
320
|
}
|
|
208
321
|
|
|
209
322
|
// Update metadata counters
|
|
@@ -231,6 +344,14 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
231
344
|
}
|
|
232
345
|
}
|
|
233
346
|
|
|
347
|
+
// Convert compact containers to final format
|
|
348
|
+
if (node._compact) {
|
|
349
|
+
// Return compact format: { "tag_id": [children] }
|
|
350
|
+
const compactNode = {};
|
|
351
|
+
compactNode[node._key] = node.children;
|
|
352
|
+
return compactNode;
|
|
353
|
+
}
|
|
354
|
+
|
|
234
355
|
return node;
|
|
235
356
|
}
|
|
236
357
|
|
|
@@ -291,6 +412,87 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
291
412
|
};
|
|
292
413
|
}
|
|
293
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Check if element has click event listeners
|
|
417
|
+
*/
|
|
418
|
+
function hasClickListener(element) {
|
|
419
|
+
try {
|
|
420
|
+
// Check for getEventListeners (available in Chrome DevTools context)
|
|
421
|
+
if (typeof getEventListeners === 'function') {
|
|
422
|
+
const listeners = getEventListeners(element);
|
|
423
|
+
return listeners && listeners.click && listeners.click.length > 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Fallback: check for common event listener markers
|
|
427
|
+
// Note: This is not 100% reliable but catches common cases
|
|
428
|
+
return element._events?.click ||
|
|
429
|
+
element.__listeners?.click ||
|
|
430
|
+
element.__eventListeners?.click;
|
|
431
|
+
} catch (e) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Check if element is interactive based on various signals
|
|
438
|
+
*/
|
|
439
|
+
function checkInteractivity(element) {
|
|
440
|
+
const tag = element.tagName.toLowerCase();
|
|
441
|
+
const role = element.getAttribute('role');
|
|
442
|
+
|
|
443
|
+
// 1. Standard interactive HTML elements
|
|
444
|
+
const interactiveTags = ['a', 'button', 'input', 'select', 'textarea'];
|
|
445
|
+
if (interactiveTags.includes(tag)) {
|
|
446
|
+
return { isInteractive: true, reason: 'native-html' };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 2. Interactive ARIA roles
|
|
450
|
+
const interactiveRoles = [
|
|
451
|
+
'button', 'link', 'checkbox', 'radio', 'tab',
|
|
452
|
+
'menuitem', 'option', 'switch', 'textbox'
|
|
453
|
+
];
|
|
454
|
+
if (role && interactiveRoles.includes(role)) {
|
|
455
|
+
return { isInteractive: true, reason: 'aria-role' };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 3. Elements with onclick attribute
|
|
459
|
+
if (element.hasAttribute('onclick')) {
|
|
460
|
+
return { isInteractive: true, reason: 'onclick-attr' };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 4. Elements with onclick property set via JavaScript
|
|
464
|
+
if (element.onclick !== null && element.onclick !== undefined) {
|
|
465
|
+
return { isInteractive: true, reason: 'onclick-prop' };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 5. Elements with cursor: pointer (only if explicitly set, not inherited)
|
|
469
|
+
if (hasCursorPointerExplicit(element)) {
|
|
470
|
+
return { isInteractive: true, reason: 'cursor-pointer' };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 6. Elements with click event listeners
|
|
474
|
+
if (hasClickListener(element)) {
|
|
475
|
+
return { isInteractive: true, reason: 'event-listener' };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 7. Elements with tabindex (except -1)
|
|
479
|
+
const tabindex = element.getAttribute('tabindex');
|
|
480
|
+
if (tabindex !== null && tabindex !== '-1') {
|
|
481
|
+
return { isInteractive: true, reason: 'tabindex' };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 8. Contenteditable elements
|
|
485
|
+
if (element.getAttribute('contenteditable') === 'true') {
|
|
486
|
+
return { isInteractive: true, reason: 'contenteditable' };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return { isInteractive: false, reason: null };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Detect framework-specific attributes on element
|
|
494
|
+
* Returns framework info or null
|
|
495
|
+
*/
|
|
294
496
|
/**
|
|
295
497
|
* Determine element type and metadata
|
|
296
498
|
*/
|
|
@@ -301,14 +503,16 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
301
503
|
|
|
302
504
|
// Form
|
|
303
505
|
if (tag === 'form') {
|
|
506
|
+
const metadata = {
|
|
507
|
+
method: element.method?.toUpperCase() || 'GET',
|
|
508
|
+
action: element.action || '',
|
|
509
|
+
name: element.name || null
|
|
510
|
+
};
|
|
511
|
+
|
|
304
512
|
return {
|
|
305
513
|
type: 'form',
|
|
306
514
|
isInteractive: true,
|
|
307
|
-
metadata
|
|
308
|
-
method: element.method?.toUpperCase() || 'GET',
|
|
309
|
-
action: element.action || '',
|
|
310
|
-
name: element.name || null
|
|
311
|
-
}
|
|
515
|
+
metadata
|
|
312
516
|
};
|
|
313
517
|
}
|
|
314
518
|
|
|
@@ -450,25 +654,32 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
450
654
|
|
|
451
655
|
// Container with semantic role
|
|
452
656
|
if (role) {
|
|
657
|
+
const interactivityCheck = checkInteractivity(element);
|
|
453
658
|
return {
|
|
454
659
|
type: role,
|
|
455
|
-
isInteractive:
|
|
660
|
+
isInteractive: interactivityCheck.isInteractive,
|
|
456
661
|
metadata: {
|
|
457
|
-
ariaLabel: element.getAttribute('aria-label') || null
|
|
662
|
+
ariaLabel: element.getAttribute('aria-label') || null,
|
|
663
|
+
interactivityReason: interactivityCheck.reason || undefined
|
|
458
664
|
}
|
|
459
665
|
};
|
|
460
666
|
}
|
|
461
667
|
|
|
462
|
-
// Generic container
|
|
668
|
+
// Generic container - check for JavaScript interactivity
|
|
669
|
+
const interactivityCheck = checkInteractivity(element);
|
|
463
670
|
return {
|
|
464
671
|
type: 'container',
|
|
465
|
-
isInteractive:
|
|
466
|
-
metadata:
|
|
672
|
+
isInteractive: interactivityCheck.isInteractive,
|
|
673
|
+
metadata: interactivityCheck.isInteractive ? {
|
|
674
|
+
text: element.textContent?.trim().substring(0, 100) || '',
|
|
675
|
+
interactivityReason: interactivityCheck.reason
|
|
676
|
+
} : null
|
|
467
677
|
};
|
|
468
678
|
}
|
|
469
679
|
|
|
470
680
|
/**
|
|
471
681
|
* Generate unique CSS selector
|
|
682
|
+
* Excludes framework-specific dynamic attributes (React, Vue, Angular)
|
|
472
683
|
*/
|
|
473
684
|
function generateSelector(element) {
|
|
474
685
|
// Use ID if available and unique
|
|
@@ -476,6 +687,19 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
476
687
|
return `#${element.id}`;
|
|
477
688
|
}
|
|
478
689
|
|
|
690
|
+
// Try to find stable class name (excluding framework-specific dynamic classes)
|
|
691
|
+
const stableClass = getStableClassName(element);
|
|
692
|
+
if (stableClass) {
|
|
693
|
+
const classSelector = `.${stableClass}`;
|
|
694
|
+
// Verify it's unique within parent context
|
|
695
|
+
if (element.parentElement) {
|
|
696
|
+
const matches = element.parentElement.querySelectorAll(classSelector);
|
|
697
|
+
if (matches.length === 1 && matches[0] === element) {
|
|
698
|
+
return classSelector;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
479
703
|
// Build path from parent
|
|
480
704
|
const path = [];
|
|
481
705
|
let current = element;
|
|
@@ -483,6 +707,12 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
483
707
|
while (current && current !== document.body) {
|
|
484
708
|
let selector = current.tagName.toLowerCase();
|
|
485
709
|
|
|
710
|
+
// Add stable class if available
|
|
711
|
+
const stableClass = getStableClassName(current);
|
|
712
|
+
if (stableClass) {
|
|
713
|
+
selector += `.${stableClass}`;
|
|
714
|
+
}
|
|
715
|
+
|
|
486
716
|
// Add nth-of-type if needed
|
|
487
717
|
if (current.parentElement) {
|
|
488
718
|
const siblings = Array.from(current.parentElement.children).filter(
|
|
@@ -500,6 +730,39 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
500
730
|
|
|
501
731
|
return path.join(' > ');
|
|
502
732
|
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Get stable class name excluding framework-specific dynamic classes
|
|
736
|
+
* Returns first stable class or null
|
|
737
|
+
*/
|
|
738
|
+
function getStableClassName(element) {
|
|
739
|
+
if (!element.className || typeof element.className !== 'string') {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const classes = element.className.split(/\s+/).filter(c => c);
|
|
744
|
+
|
|
745
|
+
// Filter out framework-specific classes
|
|
746
|
+
const stableClasses = classes.filter(className => {
|
|
747
|
+
// React: CSS Modules, Styled Components, Emotion
|
|
748
|
+
if (/^[a-zA-Z0-9_-]+-[a-zA-Z0-9_-]{5,}$/.test(className)) return false;
|
|
749
|
+
if (/^css-[a-z0-9]+(-[a-z0-9]+)?$/i.test(className)) return false;
|
|
750
|
+
if (/^sc-[a-z0-9]+-[a-z0-9]+$/i.test(className)) return false;
|
|
751
|
+
|
|
752
|
+
// Vue: scoped styles
|
|
753
|
+
if (/^data-v-[a-f0-9]{8}$/i.test(className)) return false;
|
|
754
|
+
|
|
755
|
+
// Angular: component styles (no classes starting with _ng)
|
|
756
|
+
if (/^_ng/.test(className)) return false;
|
|
757
|
+
|
|
758
|
+
// Generic hash patterns
|
|
759
|
+
if (/^[a-z0-9]{32,}$/i.test(className)) return false;
|
|
760
|
+
|
|
761
|
+
return true;
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
return stableClasses.length > 0 ? stableClasses[0] : null;
|
|
765
|
+
}
|
|
503
766
|
}
|
|
504
767
|
|
|
505
768
|
// Export for use in both Node.js and browser context
|
|
@@ -28,7 +28,7 @@ export const toolDefinitions = [
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: "click",
|
|
31
|
-
description: "
|
|
31
|
+
description: "Click element by APOM ID (preferred) or CSS selector. Handles React/Vue/Angular events, waits for navigation.",
|
|
32
32
|
inputSchema: {
|
|
33
33
|
type: "object",
|
|
34
34
|
properties: {
|
|
@@ -42,7 +42,7 @@ export const toolDefinitions = [
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: "type",
|
|
45
|
-
description: "
|
|
45
|
+
description: "Type text into input by APOM ID (preferred) or CSS selector. Updates React/Vue/Angular state automatically.",
|
|
46
46
|
inputSchema: {
|
|
47
47
|
type: "object",
|
|
48
48
|
properties: {
|
|
@@ -55,16 +55,6 @@ export const toolDefinitions = [
|
|
|
55
55
|
required: ["text"],
|
|
56
56
|
},
|
|
57
57
|
},
|
|
58
|
-
{
|
|
59
|
-
name: "getElement",
|
|
60
|
-
description: "Get HTML markup of element. Prefer analyzePage for better efficiency.",
|
|
61
|
-
inputSchema: {
|
|
62
|
-
type: "object",
|
|
63
|
-
properties: {
|
|
64
|
-
selector: { type: "string", description: "CSS selector (default: body)" },
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
58
|
{
|
|
69
59
|
name: "getComputedCss",
|
|
70
60
|
description: "Get computed CSS styles for element. For layout debugging and responsive design.",
|
|
@@ -102,7 +92,7 @@ export const toolDefinitions = [
|
|
|
102
92
|
},
|
|
103
93
|
{
|
|
104
94
|
name: "screenshot",
|
|
105
|
-
description: "Capture element image (15-25k tokens).
|
|
95
|
+
description: "Capture element image (15-25k tokens). Use analyzePage for form data/validation (8-10k tokens).",
|
|
106
96
|
inputSchema: {
|
|
107
97
|
type: "object",
|
|
108
98
|
properties: {
|
|
@@ -223,7 +213,7 @@ export const toolDefinitions = [
|
|
|
223
213
|
},
|
|
224
214
|
{
|
|
225
215
|
name: "hover",
|
|
226
|
-
description: "Hover over element
|
|
216
|
+
description: "Hover over element by APOM ID or CSS selector. For hover effects, tooltips, :hover states.",
|
|
227
217
|
inputSchema: {
|
|
228
218
|
type: "object",
|
|
229
219
|
properties: {
|
|
@@ -234,7 +224,7 @@ export const toolDefinitions = [
|
|
|
234
224
|
},
|
|
235
225
|
{
|
|
236
226
|
name: "selectOption",
|
|
237
|
-
description: "Select
|
|
227
|
+
description: "Select dropdown option by APOM ID or CSS selector. Specify value, text, or index.",
|
|
238
228
|
inputSchema: {
|
|
239
229
|
type: "object",
|
|
240
230
|
properties: {
|
|
@@ -248,7 +238,7 @@ export const toolDefinitions = [
|
|
|
248
238
|
},
|
|
249
239
|
{
|
|
250
240
|
name: "drag",
|
|
251
|
-
description: "Drag element
|
|
241
|
+
description: "Drag element in any direction. For maps, charts, SVG, canvas, sliders. Use scrollHorizontal for scrollbars.",
|
|
252
242
|
inputSchema: {
|
|
253
243
|
type: "object",
|
|
254
244
|
properties: {
|
|
@@ -262,7 +252,7 @@ export const toolDefinitions = [
|
|
|
262
252
|
},
|
|
263
253
|
{
|
|
264
254
|
name: "scrollHorizontal",
|
|
265
|
-
description: "Scroll element horizontally. For tables, carousels,
|
|
255
|
+
description: "Scroll element horizontally by pixels or to end. For tables, carousels, scrollable containers.",
|
|
266
256
|
inputSchema: {
|
|
267
257
|
type: "object",
|
|
268
258
|
properties: {
|
|
@@ -487,12 +477,12 @@ export const toolDefinitions = [
|
|
|
487
477
|
},
|
|
488
478
|
{
|
|
489
479
|
name: "analyzePage",
|
|
490
|
-
description: "PRIMARY tool for reading page state. Returns APOM
|
|
480
|
+
description: "PRIMARY tool for reading page state. Returns APOM tree: {tree, metadata, groups}. Compact format (default): containers as \"tag_id\":[children] keys, interactive elements as {id, tag, type, position, metadata} without selectors. Use element IDs (e.g., button_45, input_20) with click/type tools. Selectors registered internally for resolution. Use refresh:true after clicks. Efficient: 8-10k tokens vs screenshot 15-25k.",
|
|
491
481
|
inputSchema: {
|
|
492
482
|
type: "object",
|
|
493
483
|
properties: {
|
|
494
484
|
refresh: { type: "boolean", description: "Refresh cache (default: false)" },
|
|
495
|
-
includeAll: { type: "boolean", description: "Include all elements
|
|
485
|
+
includeAll: { type: "boolean", description: "Include all elements with selectors - full debug format (default: false for compact format)" },
|
|
496
486
|
useLegacyFormat: { type: "boolean", description: "Return legacy format instead of APOM (default: false - APOM is now default)" },
|
|
497
487
|
registerElements: { type: "boolean", description: "Auto-register elements in selector resolver (default: true)" },
|
|
498
488
|
groupBy: { type: "string", description: "Group elements: 'type' or 'flat' (default: 'type')", enum: ["type", "flat"] },
|
|
@@ -500,12 +490,15 @@ export const toolDefinitions = [
|
|
|
500
490
|
},
|
|
501
491
|
},
|
|
502
492
|
{
|
|
503
|
-
name: "
|
|
504
|
-
description: "Get detailed information about element by its APOM ID
|
|
493
|
+
name: "getElementDetails",
|
|
494
|
+
description: "Get detailed information about element by its APOM ID. Returns full element details including bounds, CSS selector, position, attributes, and computed styles. Can also analyze children elements tree structure. Use this when analyzePage output was simplified and you need complete information about specific element or analyze specific sections in detail.",
|
|
505
495
|
inputSchema: {
|
|
506
496
|
type: "object",
|
|
507
497
|
properties: {
|
|
508
498
|
id: { type: "string", description: "APOM element ID (e.g., 'input_20', 'button_45') from analyzePage result" },
|
|
499
|
+
analyzeChildren: { type: "boolean", description: "Analyze children elements tree structure (default: false)" },
|
|
500
|
+
includeAll: { type: "boolean", description: "When analyzing children, include all elements, not just interactive ones (default: false)" },
|
|
501
|
+
refresh: { type: "boolean", description: "Force refresh of cached analysis (default: false)" },
|
|
509
502
|
},
|
|
510
503
|
required: ["id"],
|
|
511
504
|
},
|
|
@@ -564,7 +557,7 @@ export const toolDefinitions = [
|
|
|
564
557
|
},
|
|
565
558
|
{
|
|
566
559
|
name: "enableRecorder",
|
|
567
|
-
description: "Check ChromeTools Extension connection
|
|
560
|
+
description: "Check ChromeTools Extension connection for scenario recording. Use Chrome Extension popup (CT icon) for recording.",
|
|
568
561
|
inputSchema: {
|
|
569
562
|
type: "object",
|
|
570
563
|
properties: {},
|
|
@@ -572,7 +565,7 @@ export const toolDefinitions = [
|
|
|
572
565
|
},
|
|
573
566
|
{
|
|
574
567
|
name: "startRecording",
|
|
575
|
-
description: "Start recording user actions
|
|
568
|
+
description: "Start recording user actions. Follows active tab automatically. Use stopRecording to finish.",
|
|
576
569
|
inputSchema: {
|
|
577
570
|
type: "object",
|
|
578
571
|
properties: {
|
|
@@ -641,7 +634,7 @@ export const toolDefinitions = [
|
|
|
641
634
|
},
|
|
642
635
|
{
|
|
643
636
|
name: "executeScenario",
|
|
644
|
-
description: "Execute
|
|
637
|
+
description: "Execute scenario by name with dependency resolution. Use projectId to disambiguate duplicate names.",
|
|
645
638
|
inputSchema: {
|
|
646
639
|
type: "object",
|
|
647
640
|
properties: {
|
|
@@ -655,7 +648,7 @@ export const toolDefinitions = [
|
|
|
655
648
|
},
|
|
656
649
|
{
|
|
657
650
|
name: "listScenarios",
|
|
658
|
-
description: "List all scenarios with metadata.
|
|
651
|
+
description: "List all scenarios with metadata.",
|
|
659
652
|
inputSchema: {
|
|
660
653
|
type: "object",
|
|
661
654
|
properties: {
|
|
@@ -665,7 +658,7 @@ export const toolDefinitions = [
|
|
|
665
658
|
},
|
|
666
659
|
{
|
|
667
660
|
name: "searchScenarios",
|
|
668
|
-
description: "Search scenarios by text or tags.
|
|
661
|
+
description: "Search scenarios by text or tags.",
|
|
669
662
|
inputSchema: {
|
|
670
663
|
type: "object",
|
|
671
664
|
properties: {
|
|
@@ -677,7 +670,7 @@ export const toolDefinitions = [
|
|
|
677
670
|
},
|
|
678
671
|
{
|
|
679
672
|
name: "getScenarioInfo",
|
|
680
|
-
description: "Get scenario details: actions, parameters, dependencies.
|
|
673
|
+
description: "Get scenario details: actions, parameters, dependencies.",
|
|
681
674
|
inputSchema: {
|
|
682
675
|
type: "object",
|
|
683
676
|
properties: {
|
|
@@ -689,7 +682,7 @@ export const toolDefinitions = [
|
|
|
689
682
|
},
|
|
690
683
|
{
|
|
691
684
|
name: "deleteScenario",
|
|
692
|
-
description: "Delete scenario and secrets.
|
|
685
|
+
description: "Delete scenario and secrets.",
|
|
693
686
|
inputSchema: {
|
|
694
687
|
type: "object",
|
|
695
688
|
properties: {
|
|
@@ -700,7 +693,7 @@ export const toolDefinitions = [
|
|
|
700
693
|
},
|
|
701
694
|
{
|
|
702
695
|
name: "exportScenarioAsCode",
|
|
703
|
-
description: "Export
|
|
696
|
+
description: "Export scenario as test code for NEW file. Cleans unstable selectors, optionally generates Page Object. Use appendScenarioToFile for existing files.",
|
|
704
697
|
inputSchema: {
|
|
705
698
|
type: "object",
|
|
706
699
|
properties: {
|
|
@@ -735,7 +728,7 @@ export const toolDefinitions = [
|
|
|
735
728
|
},
|
|
736
729
|
{
|
|
737
730
|
name: "appendScenarioToFile",
|
|
738
|
-
description: "Append
|
|
731
|
+
description: "Append scenario as test code to EXISTING file. Cleans unstable selectors, optionally generates Page Object. Use exportScenarioAsCode for new files.",
|
|
739
732
|
inputSchema: {
|
|
740
733
|
type: "object",
|
|
741
734
|
properties: {
|
package/server/tool-schemas.js
CHANGED
|
@@ -35,10 +35,6 @@ export const TypeSchema = z.object({
|
|
|
35
35
|
message: "Either 'id' or 'selector' must be provided, but not both"
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
export const GetElementSchema = z.object({
|
|
39
|
-
selector: z.string().optional().describe("CSS selector (optional, defaults to body)"),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
38
|
export const HoverSchema = z.object({
|
|
43
39
|
id: z.string().optional().describe("APOM element ID from analyzePage. Mutually exclusive with selector."),
|
|
44
40
|
selector: z.string().optional().describe("CSS selector for element to hover. Mutually exclusive with id."),
|
|
@@ -260,14 +256,17 @@ export const SmartFindElementSchema = z.object({
|
|
|
260
256
|
|
|
261
257
|
export const AnalyzePageSchema = z.object({
|
|
262
258
|
refresh: z.boolean().optional().describe("Force refresh of cached analysis (default: false)"),
|
|
263
|
-
includeAll: z.boolean().optional().describe("Include all elements on page, not just interactive ones (default: false).
|
|
259
|
+
includeAll: z.boolean().optional().describe("Include all elements on page, not just interactive ones (default: false). When false (default), returns compact format: containers as \"tag_id\":[children], interactive elements without selectors. When true, returns full format with selectors for debugging."),
|
|
264
260
|
useLegacyFormat: z.boolean().optional().describe("Return legacy format instead of APOM (default: false - APOM is now the default format)"),
|
|
265
261
|
registerElements: z.boolean().optional().describe("Automatically register elements in selector resolver (default: true)"),
|
|
266
262
|
groupBy: z.enum(['type', 'flat']).optional().describe("Group elements by type or return flat structure (default: 'type')"),
|
|
267
263
|
});
|
|
268
264
|
|
|
269
|
-
export const
|
|
265
|
+
export const GetElementDetailsSchema = z.object({
|
|
270
266
|
id: z.string().describe("APOM element ID (e.g., 'input_20', 'button_45') from analyzePage result"),
|
|
267
|
+
analyzeChildren: z.boolean().optional().describe("Analyze children elements tree structure (default: false)"),
|
|
268
|
+
includeAll: z.boolean().optional().describe("When analyzing children, include all elements, not just interactive ones (default: false)"),
|
|
269
|
+
refresh: z.boolean().optional().describe("Force refresh of cached analysis (default: false)"),
|
|
271
270
|
});
|
|
272
271
|
|
|
273
272
|
export const GetAllInteractiveElementsSchema = z.object({
|