explorbot 0.1.12 → 0.1.13

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.
Files changed (59) hide show
  1. package/bin/explorbot-cli.ts +21 -21
  2. package/dist/bin/explorbot-cli.js +3 -3
  3. package/dist/package.json +3 -2
  4. package/dist/rules/researcher/container-rules.md +2 -0
  5. package/dist/src/action-result.js +2 -1
  6. package/dist/src/action.js +0 -6
  7. package/dist/src/ai/captain.js +0 -2
  8. package/dist/src/ai/driller.js +1108 -0
  9. package/dist/src/ai/pilot.js +31 -22
  10. package/dist/src/ai/rules.js +3 -5
  11. package/dist/src/ai/session-analyst.js +117 -0
  12. package/dist/src/ai/tester.js +13 -2
  13. package/dist/src/commands/base-command.js +6 -6
  14. package/dist/src/commands/drill-command.js +3 -2
  15. package/dist/src/commands/exit-command.js +1 -0
  16. package/dist/src/commands/explore-command.js +1 -0
  17. package/dist/src/components/AddRule.js +1 -1
  18. package/dist/src/explorbot.js +48 -8
  19. package/dist/src/explorer.js +9 -8
  20. package/dist/src/reporter.js +64 -3
  21. package/dist/src/state-manager.js +4 -3
  22. package/dist/src/stats.js +5 -0
  23. package/dist/src/utils/aria.js +354 -529
  24. package/dist/src/utils/hooks-runner.js +2 -8
  25. package/dist/src/utils/html.js +371 -0
  26. package/dist/src/utils/unique-names.js +12 -1
  27. package/dist/src/utils/url-matcher.js +6 -1
  28. package/dist/src/utils/web-element.js +27 -24
  29. package/dist/src/utils/xpath.js +1 -1
  30. package/package.json +3 -2
  31. package/rules/researcher/container-rules.md +2 -0
  32. package/src/action-result.ts +2 -1
  33. package/src/action.ts +0 -8
  34. package/src/ai/captain.ts +0 -2
  35. package/src/ai/driller.ts +1194 -0
  36. package/src/ai/pilot.ts +31 -21
  37. package/src/ai/rules.ts +3 -5
  38. package/src/ai/session-analyst.ts +133 -0
  39. package/src/ai/tester.ts +15 -2
  40. package/src/commands/base-command.ts +6 -6
  41. package/src/commands/drill-command.ts +3 -2
  42. package/src/commands/exit-command.ts +1 -0
  43. package/src/commands/explore-command.ts +1 -0
  44. package/src/components/AddRule.tsx +1 -1
  45. package/src/config.ts +4 -0
  46. package/src/explorbot.ts +55 -10
  47. package/src/explorer.ts +9 -8
  48. package/src/reporter.ts +64 -3
  49. package/src/state-manager.ts +4 -3
  50. package/src/stats.ts +7 -0
  51. package/src/utils/aria.ts +367 -537
  52. package/src/utils/hooks-runner.ts +2 -6
  53. package/src/utils/html.ts +381 -0
  54. package/src/utils/unique-names.ts +13 -0
  55. package/src/utils/url-matcher.ts +5 -1
  56. package/src/utils/web-element.ts +31 -28
  57. package/src/utils/xpath.ts +1 -1
  58. package/dist/src/ai/bosun.js +0 -456
  59. package/src/ai/bosun.ts +0 -571
@@ -1,4 +1,5 @@
1
1
  import { createDebug } from "./logger.js";
2
+ import { extractStatePath } from "./url-matcher.js";
2
3
  import { matchesUrl } from "./url-matcher.js";
3
4
  const debugLog = createDebug('explorbot:hooks');
4
5
  export class HooksRunner {
@@ -64,13 +65,6 @@ export class HooksRunner {
64
65
  return 'type' in config && 'hook' in config;
65
66
  }
66
67
  extractPath(url) {
67
- if (url.startsWith('/'))
68
- return url;
69
- try {
70
- return new URL(url).pathname;
71
- }
72
- catch {
73
- return url;
74
- }
68
+ return extractStatePath(url);
75
69
  }
76
70
  }
@@ -65,6 +65,377 @@ const INTERACTIVE_TAGS = new Set(['a', 'button', 'details', 'input', 'option', '
65
65
  const INTERACTIVE_ROLES = new Set(['button', 'checkbox', 'combobox', 'link', 'listbox', 'radio', 'search', 'switch', 'tab', 'textbox']);
66
66
  const INTERACTIVE_EVENT_ATTRIBUTES = new Set(['onclick', 'onchange', 'onblur', 'onfocus', 'onmousedown', 'onmouseup']);
67
67
  const HIDDEN_CLASSES = new Set(['hidden', 'invisible', 'd-none', 'hide', 'dn', 'u-hidden', 'is-hidden', 'visually-hidden', 'sr-only', 'screen-reader-only', 'visuallyhidden', 'opacity-0']);
68
+ export const EXPLORBOT_ATTRS = {
69
+ area: 'data-explorbot-area',
70
+ context: 'data-explorbot-context',
71
+ eidx: 'data-explorbot-eidx',
72
+ variant: 'data-explorbot-variant',
73
+ };
74
+ export const HTML_SELECTORS = {
75
+ headingLabel: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"]',
76
+ interactiveContent: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="option"], [role="menuitem"], [role="switch"], [role="checkbox"], [role="radio"], [aria-label], [tabindex]',
77
+ interactiveControl: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"]',
78
+ labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]',
79
+ semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]',
80
+ semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])'],
81
+ };
82
+ export const HTML_VISIBILITY_LIMITS = {
83
+ maxViewportOverlayRatio: 0.95,
84
+ minOpacity: 0.1,
85
+ minOverlayHeight: 40,
86
+ minOverlayWidth: 80,
87
+ };
88
+ export const HTML_EXTRACTION_LIMITS = {
89
+ componentScopeHtmlLength: 8000,
90
+ maxOverlayCount: 3,
91
+ maxScopeInteractiveCount: 16,
92
+ overlayHtmlLength: 6000,
93
+ };
94
+ export const CODE_EDITOR_MARKERS = ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'];
95
+ export const HTML_INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'iframe', 'code-editor', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']);
96
+ export const HTML_FORM_CONTROL_ROLES = new Set(['checkbox', 'radio', 'switch', 'combobox', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox']);
97
+ export const HTML_COMPOSITE_TARGET_ROLES = new Set(['tab', 'option', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'treeitem']);
98
+ export const HTML_COMPOSITE_AREA_HINTS = new Set(['role:tab', 'role:option', 'role:menuitem', 'role:menuitemcheckbox', 'role:menuitemradio', 'role:treeitem']);
99
+ export const HTML_FORM_CONTROL_TAGS = new Set(['input', 'select', 'textarea']);
100
+ export function inferHtmlRole(data) {
101
+ if (data.tag === 'iframe' && data.variantHints?.includes('code-editor'))
102
+ return 'code-editor';
103
+ if (data.role)
104
+ return data.role.toLowerCase();
105
+ const explicitRole = data.attrs.role;
106
+ if (explicitRole)
107
+ return explicitRole.toLowerCase();
108
+ if (data.tag === 'a' && data.attrs.href)
109
+ return 'link';
110
+ if (data.tag === 'button')
111
+ return 'button';
112
+ if (data.tag === 'iframe')
113
+ return 'iframe';
114
+ if (data.tag === 'select')
115
+ return 'combobox';
116
+ if (data.tag === 'textarea')
117
+ return 'textbox';
118
+ if (data.tag === 'input') {
119
+ const type = (data.attrs.type || 'text').toLowerCase();
120
+ if (type === 'checkbox')
121
+ return 'checkbox';
122
+ if (type === 'radio')
123
+ return 'radio';
124
+ return 'textbox';
125
+ }
126
+ return data.tag;
127
+ }
128
+ export const ELEMENT_EXTRACTION_CONFIG = {
129
+ attrs: EXPLORBOT_ATTRS,
130
+ codeEditorMarkers: CODE_EDITOR_MARKERS,
131
+ maxAreaDepth: 5,
132
+ maxContextLength: 120,
133
+ maxOuterHTMLLength: 2000,
134
+ maxTextLength: 80,
135
+ minOpacity: HTML_VISIBILITY_LIMITS.minOpacity,
136
+ selectors: {
137
+ headingLabel: HTML_SELECTORS.headingLabel,
138
+ labelLike: HTML_SELECTORS.labelLike,
139
+ semanticContextContainer: HTML_SELECTORS.semanticContextContainer,
140
+ },
141
+ };
142
+ export function extractElementData(el, config) {
143
+ const cfg = config ||
144
+ {
145
+ attrs: {
146
+ area: 'data-explorbot-area',
147
+ context: 'data-explorbot-context',
148
+ eidx: 'data-explorbot-eidx',
149
+ variant: 'data-explorbot-variant',
150
+ },
151
+ codeEditorMarkers: ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'],
152
+ maxAreaDepth: 5,
153
+ maxContextLength: 120,
154
+ maxOuterHTMLLength: 2000,
155
+ maxTextLength: 80,
156
+ minOpacity: 0.1,
157
+ selectors: {
158
+ headingLabel: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"]',
159
+ labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]',
160
+ semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]',
161
+ },
162
+ };
163
+ function normalizeText(value) {
164
+ return value.replace(/\s+/g, ' ').trim();
165
+ }
166
+ function readText(node) {
167
+ if (!node)
168
+ return '';
169
+ return normalizeText(node.textContent || '').slice(0, cfg.maxContextLength);
170
+ }
171
+ function getLabelLikeText(node) {
172
+ if (!node)
173
+ return '';
174
+ const direct = readText(node);
175
+ if (direct)
176
+ return direct;
177
+ const labelLike = node.querySelector(cfg.selectors.labelLike);
178
+ return readText(labelLike);
179
+ }
180
+ function collectVariantHints(target) {
181
+ const tokens = new Set();
182
+ const className = target.getAttribute('class') || '';
183
+ const tagName = target.tagName.toLowerCase();
184
+ for (const cls of className.split(/\s+/).filter(Boolean)) {
185
+ const lower = cls.toLowerCase();
186
+ if (/^(xs|sm|md|lg|xl|xxl)$/.test(lower))
187
+ tokens.add(lower);
188
+ if (/^(mini|small|medium|large|xlarge|xl|compact|dense)$/.test(lower))
189
+ tokens.add(lower);
190
+ if (/(^|[-_])(xs|sm|md|lg|xl|xxl|mini|small|medium|large|compact|dense)([-_]|$)/.test(lower))
191
+ tokens.add(lower);
192
+ if (/(selected|disabled|primary|secondary|tertiary|danger|success|warning|outline|ghost|icon|dropdown)/.test(lower))
193
+ tokens.add(lower);
194
+ }
195
+ const type = (target.getAttribute('type') || '').toLowerCase();
196
+ if (type)
197
+ tokens.add(type);
198
+ if (target.hasAttribute('disabled') || target.getAttribute('aria-disabled') === 'true')
199
+ tokens.add('disabled');
200
+ if (className.toLowerCase().includes('selected') || target.getAttribute('aria-pressed') === 'true')
201
+ tokens.add('selected');
202
+ if (tagName === 'iframe')
203
+ tokens.add('iframe');
204
+ if (tagName === 'iframe' && isEmbeddedCodeEditorFrame(target))
205
+ tokens.add('code-editor');
206
+ const svgCount = target.querySelectorAll('svg').length;
207
+ if (svgCount > 0)
208
+ tokens.add('has-icon');
209
+ if (svgCount > 1)
210
+ tokens.add('double-icon');
211
+ const normalizedText = normalizeText(target.textContent || '');
212
+ if (!normalizedText && svgCount > 0)
213
+ tokens.add('icon-only');
214
+ if (normalizedText && svgCount > 0) {
215
+ const first = target.firstElementChild?.tagName.toLowerCase();
216
+ const last = target.lastElementChild?.tagName.toLowerCase();
217
+ if (first === 'svg')
218
+ tokens.add('leading-icon');
219
+ if (last === 'svg')
220
+ tokens.add('trailing-icon');
221
+ }
222
+ if (tagName === 'a' && target.getAttribute('href'))
223
+ tokens.add('navigates');
224
+ return Array.from(tokens).slice(0, 8);
225
+ }
226
+ function isEmbeddedCodeEditorFrame(target) {
227
+ const src = (target.getAttribute('src') || '').toLowerCase();
228
+ const markerSelector = cfg.codeEditorMarkers.map((marker) => `[class*="${marker}"]`).join(', ');
229
+ const ancestorClasses = (target.closest(markerSelector)?.getAttribute('class') || '').toLowerCase();
230
+ return cfg.codeEditorMarkers.some((marker) => src.includes(marker) || ancestorClasses.includes(marker));
231
+ }
232
+ function findContextLabel(target) {
233
+ const labelledby = target.getAttribute('aria-labelledby');
234
+ const candidates = [];
235
+ if (labelledby) {
236
+ for (const id of labelledby.split(/\s+/).filter(Boolean)) {
237
+ const ref = document.getElementById(id);
238
+ const text = readText(ref);
239
+ if (text)
240
+ candidates.push(text);
241
+ }
242
+ }
243
+ const semanticContainer = target.closest(cfg.selectors.semanticContextContainer);
244
+ if (semanticContainer) {
245
+ const ownHeading = semanticContainer.querySelector(cfg.selectors.headingLabel);
246
+ const ownHeadingText = readText(ownHeading);
247
+ if (ownHeadingText)
248
+ candidates.push(ownHeadingText);
249
+ let previous = semanticContainer.previousElementSibling;
250
+ let hops = 0;
251
+ while (previous && hops < 3) {
252
+ const previousText = getLabelLikeText(previous);
253
+ if (previousText) {
254
+ candidates.push(previousText);
255
+ break;
256
+ }
257
+ previous = previous.previousElementSibling;
258
+ hops++;
259
+ }
260
+ }
261
+ let parent = target.parentElement;
262
+ let depth = 0;
263
+ while (parent && depth < 4) {
264
+ let sibling = parent.previousElementSibling;
265
+ let hops = 0;
266
+ while (sibling && hops < 2) {
267
+ const siblingText = getLabelLikeText(sibling);
268
+ if (siblingText) {
269
+ candidates.push(siblingText);
270
+ sibling = null;
271
+ break;
272
+ }
273
+ sibling = sibling.previousElementSibling;
274
+ hops++;
275
+ }
276
+ parent = parent.parentElement;
277
+ depth++;
278
+ }
279
+ const ownText = normalizeText(target.textContent || '');
280
+ for (const candidate of candidates) {
281
+ if (!candidate)
282
+ continue;
283
+ if (candidate === ownText)
284
+ continue;
285
+ if (candidate.toLowerCase().includes('title should not be empty'))
286
+ continue;
287
+ return candidate.slice(0, cfg.maxContextLength);
288
+ }
289
+ return '';
290
+ }
291
+ const rect = el.getBoundingClientRect();
292
+ if (rect.width === 0 && rect.height === 0)
293
+ return null;
294
+ const style = window.getComputedStyle(el);
295
+ if (style.display === 'none' || style.visibility === 'hidden')
296
+ return null;
297
+ if (Number.parseFloat(style.opacity || '1') < cfg.minOpacity)
298
+ return null;
299
+ if (el.getAttribute('aria-hidden') === 'true' || el.hasAttribute('hidden'))
300
+ return null;
301
+ if (el.offsetParent === null && style.position !== 'fixed')
302
+ return null;
303
+ const allAttrs = {};
304
+ for (let i = 0; i < el.attributes.length; i++) {
305
+ const attr = el.attributes[i];
306
+ allAttrs[attr.name] = attr.value;
307
+ }
308
+ const areaHints = [];
309
+ let current = el;
310
+ let depth = 0;
311
+ while (current && depth < cfg.maxAreaDepth) {
312
+ const tag = current.tagName.toLowerCase();
313
+ areaHints.push(tag);
314
+ const role = current.getAttribute('role');
315
+ if (role)
316
+ areaHints.push(`role:${role.toLowerCase()}`);
317
+ const id = current.getAttribute('id');
318
+ if (id)
319
+ areaHints.push(`id:${id.toLowerCase()}`);
320
+ const className = current.getAttribute('class');
321
+ if (className) {
322
+ for (const cls of className.split(/\s+/).filter(Boolean)) {
323
+ areaHints.push(`class:${cls.toLowerCase()}`);
324
+ }
325
+ }
326
+ current = current.parentElement;
327
+ depth++;
328
+ }
329
+ allAttrs[cfg.attrs.area] = areaHints.join('|');
330
+ allAttrs[cfg.attrs.context] = findContextLabel(el);
331
+ allAttrs[cfg.attrs.variant] = collectVariantHints(el).join('|');
332
+ return {
333
+ tag: el.tagName.toLowerCase(),
334
+ text: normalizeText(el.textContent || '').slice(0, cfg.maxTextLength),
335
+ allAttrs,
336
+ outerHTML: el.outerHTML.slice(0, cfg.maxOuterHTMLLength),
337
+ x: Math.round(rect.x + rect.width / 2),
338
+ y: Math.round(rect.y + rect.height / 2),
339
+ };
340
+ }
341
+ export function getElementDataExtractorSource() {
342
+ return extractElementData.toString();
343
+ }
344
+ export function extractVisibleOverlayHtml(config) {
345
+ function isVisible(element) {
346
+ const html = element;
347
+ const style = window.getComputedStyle(html);
348
+ const rect = html.getBoundingClientRect();
349
+ if (rect.width === 0 && rect.height === 0)
350
+ return false;
351
+ if (style.display === 'none' || style.visibility === 'hidden')
352
+ return false;
353
+ if (Number.parseFloat(style.opacity || '1') < config.visibilityLimits.minOpacity)
354
+ return false;
355
+ return true;
356
+ }
357
+ function getUsefulContent(element) {
358
+ const text = (element.textContent || '').replace(/\s+/g, ' ').trim();
359
+ const interactiveCount = element.querySelectorAll(config.interactiveContentSelector).length;
360
+ return { interactiveCount, text };
361
+ }
362
+ function isLikelyFloatingOverlay(element) {
363
+ const html = element;
364
+ const style = window.getComputedStyle(html);
365
+ const rect = html.getBoundingClientRect();
366
+ const zIndex = Number.parseInt(style.zIndex || '0', 10);
367
+ const isFloating = style.position === 'fixed' || style.position === 'absolute' || style.position === 'sticky' || zIndex > 0;
368
+ if (!isFloating)
369
+ return false;
370
+ if (rect.width < config.visibilityLimits.minOverlayWidth || rect.height < config.visibilityLimits.minOverlayHeight)
371
+ return false;
372
+ if (rect.bottom < 0 || rect.right < 0 || rect.top > window.innerHeight || rect.left > window.innerWidth)
373
+ return false;
374
+ if (rect.width >= window.innerWidth * config.visibilityLimits.maxViewportOverlayRatio && rect.height >= window.innerHeight * config.visibilityLimits.maxViewportOverlayRatio)
375
+ return false;
376
+ const { interactiveCount, text } = getUsefulContent(element);
377
+ return interactiveCount > 0 || text.length > 0;
378
+ }
379
+ const overlays = [];
380
+ const seen = new Set();
381
+ for (const selector of config.overlaySelectors) {
382
+ for (const element of Array.from(document.querySelectorAll(selector))) {
383
+ if (seen.has(element))
384
+ continue;
385
+ seen.add(element);
386
+ if (!isVisible(element))
387
+ continue;
388
+ const { interactiveCount, text } = getUsefulContent(element);
389
+ if (interactiveCount === 0 && text.length === 0)
390
+ continue;
391
+ overlays.push(element.outerHTML.slice(0, config.limits.overlayHtmlLength));
392
+ }
393
+ }
394
+ if (overlays.length === 0) {
395
+ const floatingCandidates = Array.from(document.body.querySelectorAll('*'))
396
+ .filter((element) => !seen.has(element) && isVisible(element) && isLikelyFloatingOverlay(element))
397
+ .sort((left, right) => {
398
+ const leftStyle = window.getComputedStyle(left);
399
+ const rightStyle = window.getComputedStyle(right);
400
+ const leftZ = Number.parseInt(leftStyle.zIndex || '0', 10) || 0;
401
+ const rightZ = Number.parseInt(rightStyle.zIndex || '0', 10) || 0;
402
+ if (leftZ !== rightZ)
403
+ return rightZ - leftZ;
404
+ const leftRect = left.getBoundingClientRect();
405
+ const rightRect = right.getBoundingClientRect();
406
+ return leftRect.width * leftRect.height - rightRect.width * rightRect.height;
407
+ });
408
+ for (const element of floatingCandidates.slice(0, config.limits.maxOverlayCount)) {
409
+ overlays.push(element.outerHTML.slice(0, config.limits.overlayHtmlLength));
410
+ }
411
+ }
412
+ return overlays.slice(0, config.limits.maxOverlayCount).join('\n\n--- overlay ---\n\n');
413
+ }
414
+ export function extractComponentScopeHtml(eidx, config) {
415
+ const element = document.querySelector(`[${config.eidxAttr}="${eidx}"]`);
416
+ if (!element)
417
+ return '';
418
+ function countInteractive(node) {
419
+ return node.querySelectorAll(config.interactiveControlSelector).length;
420
+ }
421
+ let current = element.parentElement;
422
+ while (current) {
423
+ const count = countInteractive(current);
424
+ if (count > 0 && count <= config.limits.maxScopeInteractiveCount) {
425
+ return current.outerHTML.slice(0, config.limits.componentScopeHtmlLength);
426
+ }
427
+ current = current.parentElement;
428
+ }
429
+ if (element instanceof HTMLElement)
430
+ return element.outerHTML.slice(0, config.limits.componentScopeHtmlLength);
431
+ return '';
432
+ }
433
+ export function getVisibleOverlayHtmlExtractorSource() {
434
+ return extractVisibleOverlayHtml.toString();
435
+ }
436
+ export function getComponentScopeHtmlExtractorSource() {
437
+ return extractComponentScopeHtml.toString();
438
+ }
68
439
  export const TRASH_HTML_CLASSES = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/;
69
440
  export const TAILWIND_CLASS_PATTERNS = [
70
441
  /^m[trblxy]?-/i,
@@ -1,12 +1,23 @@
1
- import { adjectives, colors, uniqueNamesGenerator } from 'unique-names-generator';
1
+ import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator';
2
2
  const nameConfig = {
3
3
  dictionaries: [adjectives, adjectives, colors],
4
4
  separator: '',
5
5
  length: 3,
6
6
  style: 'capital',
7
7
  };
8
+ const explorationConfig = {
9
+ dictionaries: [adjectives, animals],
10
+ separator: '',
11
+ length: 2,
12
+ style: 'capital',
13
+ };
8
14
  export function uniqSessionName() {
9
15
  const name = uniqueNamesGenerator(nameConfig);
10
16
  const randomNum = Math.floor(Math.random() * 999);
11
17
  return `${name}${randomNum}`;
12
18
  }
19
+ export function uniqExplorationName() {
20
+ const name = uniqueNamesGenerator(explorationConfig);
21
+ const randomNum = Math.floor(Math.random() * 999);
22
+ return `${name}${randomNum}`;
23
+ }
@@ -52,6 +52,11 @@ export function generalizeUrl(url) {
52
52
  export function matchesUrl(pattern, path) {
53
53
  if (pattern === '*')
54
54
  return true;
55
+ if (!pattern.includes('?')) {
56
+ const queryIndex = path.indexOf('?');
57
+ if (queryIndex >= 0)
58
+ path = path.slice(0, queryIndex);
59
+ }
55
60
  const norm = (s) => s?.replace(/\/+$/, '').toLowerCase();
56
61
  if (norm(pattern) === norm(path))
57
62
  return true;
@@ -89,7 +94,7 @@ export function extractStatePath(url) {
89
94
  return url;
90
95
  try {
91
96
  const urlObj = new URL(url);
92
- return urlObj.pathname + urlObj.hash;
97
+ return `${urlObj.pathname}${urlObj.search}${urlObj.hash}`;
93
98
  }
94
99
  catch {
95
100
  return url;
@@ -1,4 +1,6 @@
1
+ import { ELEMENT_EXTRACTION_CONFIG, EXPLORBOT_ATTRS, extractElementData, getElementDataExtractorSource } from "./html.js";
1
2
  import { buildClickableXPath, evaluateXPath, isDynamicId, isGenericClass } from "./xpath.js";
3
+ export { extractElementData } from "./html.js";
2
4
  const KEY_DISPLAY_ATTRS = ['role', 'id', 'class', 'aria-label'];
3
5
  const KEY_ATTRS = ['role', 'aria-label', 'id', 'name', 'type', 'href'];
4
6
  export class WebElement {
@@ -35,7 +37,7 @@ export class WebElement {
35
37
  return `(${this.x}, ${this.y})`;
36
38
  }
37
39
  get eidx() {
38
- return this.attrs['data-explorbot-eidx'] || this.attrs.eidx || null;
40
+ return this.attrs[EXPLORBOT_ATTRS.eidx] || this.attrs.eidx || null;
39
41
  }
40
42
  get isNavigationLink() {
41
43
  if (this.tag !== 'a')
@@ -47,6 +49,23 @@ export class WebElement {
47
49
  const cls = this.attrs.class || '';
48
50
  return cls.split(/\s+/).filter((c) => c.length > 2 && !isDynamicId(c) && !isGenericClass(c));
49
51
  }
52
+ get areaHints() {
53
+ const raw = this.attrs[EXPLORBOT_ATTRS.area] || '';
54
+ return raw
55
+ .split('|')
56
+ .map((entry) => entry.trim().toLowerCase())
57
+ .filter(Boolean);
58
+ }
59
+ get contextLabel() {
60
+ return (this.attrs[EXPLORBOT_ATTRS.context] || '').trim();
61
+ }
62
+ get variantHints() {
63
+ const raw = this.attrs[EXPLORBOT_ATTRS.variant] || '';
64
+ return raw
65
+ .split('|')
66
+ .map((entry) => entry.trim().toLowerCase())
67
+ .filter(Boolean);
68
+ }
50
69
  static fromRawData(d, role) {
51
70
  return new WebElement({
52
71
  tag: d.tag,
@@ -55,6 +74,7 @@ export class WebElement {
55
74
  clickXPath: buildClickableXPath({ tag: d.tag, allAttrs: d.allAttrs, text: d.text }),
56
75
  attrs: d.allAttrs,
57
76
  text: d.text,
77
+ outerHTML: d.outerHTML,
58
78
  x: d.x,
59
79
  y: d.y,
60
80
  });
@@ -76,7 +96,7 @@ export class WebElement {
76
96
  const count = await locator.count();
77
97
  if (count === 0)
78
98
  return null;
79
- const data = await locator.first().evaluate(extractElementData);
99
+ const data = await locator.first().evaluate(extractElementData, ELEMENT_EXTRACTION_CONFIG);
80
100
  if (!data)
81
101
  return null;
82
102
  return WebElement.fromRawData(data);
@@ -86,24 +106,24 @@ export class WebElement {
86
106
  }
87
107
  }
88
108
  static async fromEidx(page, eidx) {
89
- return WebElement.fromPlaywrightLocator(page.locator(`[data-explorbot-eidx="${eidx}"]`));
109
+ return WebElement.fromPlaywrightLocator(page.locator(`[${EXPLORBOT_ATTRS.eidx}="${eidx}"]`));
90
110
  }
91
111
  static async fromEidxList(page, eidxList) {
92
112
  if (eidxList.length === 0)
93
113
  return [];
94
- const rawList = await page.evaluate(([list, extractFnStr]) => {
114
+ const rawList = await page.evaluate(([list, extractFnStr, config]) => {
95
115
  const extract = new Function(`return ${extractFnStr}`)();
96
116
  const results = [];
97
117
  for (const eidx of list) {
98
- const el = document.querySelector(`[data-explorbot-eidx="${eidx}"]`);
118
+ const el = document.querySelector(`[${config.attrs.eidx}="${eidx}"]`);
99
119
  if (!el)
100
120
  continue;
101
- const data = extract(el);
121
+ const data = extract(el, config);
102
122
  if (data)
103
123
  results.push(data);
104
124
  }
105
125
  return results;
106
- }, [eidxList, extractElementData.toString()]);
126
+ }, [eidxList, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]);
107
127
  return rawList.map((d) => WebElement.fromRawData(d));
108
128
  }
109
129
  static async findByXPath(html, xpath) {
@@ -113,20 +133,3 @@ export class WebElement {
113
133
  return { totalFound: result.totalFound, elements: result.matches.map((m) => WebElement.fromXPathMatch(m)) };
114
134
  }
115
135
  }
116
- export function extractElementData(el) {
117
- const rect = el.getBoundingClientRect();
118
- if (rect.width === 0 && rect.height === 0)
119
- return null;
120
- const allAttrs = {};
121
- for (let i = 0; i < el.attributes.length; i++) {
122
- const attr = el.attributes[i];
123
- allAttrs[attr.name] = attr.value;
124
- }
125
- return {
126
- tag: el.tagName.toLowerCase(),
127
- text: (el.textContent || '').trim().slice(0, 80),
128
- allAttrs,
129
- x: Math.round(rect.x + rect.width / 2),
130
- y: Math.round(rect.y + rect.height / 2),
131
- };
132
- }
@@ -33,7 +33,7 @@ function getAbsoluteXPath(el) {
33
33
  return `//${parts.join('/')}`;
34
34
  }
35
35
  export const isDynamicId = (id) => /^(ember|react|__next)\d|^\d+$/.test(id);
36
- export const isGenericClass = (cls) => /^ember-view$|^ember\d|^react-|^__next/.test(cls);
36
+ export const isGenericClass = (cls) => /^ember-view$|^ember\d|^ember-|^react-|^__next/.test(cls);
37
37
  export function buildClickableXPath(el) {
38
38
  const a = el.allAttrs;
39
39
  if (a.id && !isDynamicId(a.id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -78,7 +78,7 @@
78
78
  "@opentelemetry/sdk-trace-base": "^2.2.0",
79
79
  "@opentelemetry/semantic-conventions": "^1.38.0",
80
80
  "@scalar/openapi-parser": "^0.25.6",
81
- "@testomatio/reporter": "^2.7.6",
81
+ "@testomatio/reporter": "^2.7.9-beta.2-markdown",
82
82
  "ai": "^6.0.6",
83
83
  "axe-core": "^4.11.1",
84
84
  "bash-tool": "^1.3.15",
@@ -109,6 +109,7 @@
109
109
  "strip-ansi": "^7.1.2",
110
110
  "turndown": "^7.2.1",
111
111
  "unique-names-generator": "^4.7.1",
112
+ "yaml": "^2.8.3",
112
113
  "yargs": "^17.7.2",
113
114
  "zod": "^4.1.8"
114
115
  },
@@ -5,6 +5,8 @@ Container CSS must be a SINGLE semantic selector — one class, one id, or one a
5
5
  - VALID: semantic class names that describe what the section IS (`.product-list`, `.sidebar-menu`, `.user-profile`, `.search-results`), semantic roles (`[role="dialog"]`), semantic ids (`#main-content`)
6
6
 
7
7
  The container must uniquely identify a semantic wrapper, not a path through the DOM.
8
+
9
+ Iframes are not sections — list each iframe as a single row of type `iframe` inside the section that contains it. Never use an iframe selector as a section's `> Container:`.
8
10
  </container_rules>
9
11
 
10
12
  <css_selector_rules>
@@ -457,8 +457,9 @@ export class ActionResult implements ActionResultData {
457
457
  try {
458
458
  const urlObj = new URL(this.url);
459
459
  const path = urlObj.pathname.replace(/\/$/, '') || '/';
460
+ const search = urlObj.search || '';
460
461
  const hash = urlObj.hash || '';
461
- return path + hash;
462
+ return path + search + hash;
462
463
  } catch {
463
464
  // If URL parsing fails, assume it's already a relative URL
464
465
  return this.url;
package/src/action.ts CHANGED
@@ -369,14 +369,6 @@ class Action {
369
369
  return true;
370
370
  } catch (error) {
371
371
  this.lastError = error as Error;
372
-
373
- if (error && typeof error === 'object') {
374
- const errorObj = error as { fetchDetails?: () => Promise<void> };
375
- if (typeof errorObj.fetchDetails === 'function') {
376
- await errorObj.fetchDetails();
377
- }
378
- }
379
-
380
372
  debugLog(`Attempt failed: ${codeBlock}: ${errorToString(error) || this.lastError?.toString()}`);
381
373
  return false;
382
374
  }
package/src/ai/captain.ts CHANGED
@@ -196,8 +196,6 @@ export class Captain extends CaptainBase implements Agent {
196
196
  ${knowledge}
197
197
 
198
198
  ${experience}
199
-
200
- Use runCommand("/research") if you need deeper page understanding or UI element mapping.
201
199
  `;
202
200
  }
203
201