explorbot 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/bin/explorbot-cli.ts +4 -3
  2. package/dist/bin/explorbot-cli.js +4 -3
  3. package/dist/src/action.js +14 -11
  4. package/dist/src/ai/planner/subpages.js +42 -6
  5. package/dist/src/ai/planner.js +15 -3
  6. package/dist/src/ai/researcher/cache.js +13 -8
  7. package/dist/src/ai/researcher/coordinates.js +4 -2
  8. package/dist/src/ai/researcher/deep-analysis.js +16 -19
  9. package/dist/src/ai/researcher/locators.js +1 -1
  10. package/dist/src/ai/researcher/parser.js +4 -3
  11. package/dist/src/ai/researcher/research-result.js +2 -0
  12. package/dist/src/ai/researcher.js +6 -5
  13. package/dist/src/ai/tools.js +4 -0
  14. package/dist/src/commands/context-command.js +2 -2
  15. package/dist/src/commands/explore-command.js +1 -1
  16. package/dist/src/commands/init-command.js +4 -2
  17. package/dist/src/commands/plan-command.js +6 -1
  18. package/dist/src/explorbot.js +1 -1
  19. package/dist/src/explorer.js +58 -16
  20. package/dist/src/utils/web-element.js +6 -4
  21. package/package.json +2 -2
  22. package/src/action.ts +14 -10
  23. package/src/ai/planner/subpages.ts +37 -7
  24. package/src/ai/planner.ts +16 -3
  25. package/src/ai/researcher/cache.ts +14 -8
  26. package/src/ai/researcher/coordinates.ts +8 -7
  27. package/src/ai/researcher/deep-analysis.ts +18 -21
  28. package/src/ai/researcher/locators.ts +3 -3
  29. package/src/ai/researcher/parser.ts +4 -4
  30. package/src/ai/researcher/research-result.ts +1 -0
  31. package/src/ai/researcher.ts +6 -5
  32. package/src/ai/tools.ts +5 -0
  33. package/src/commands/context-command.ts +2 -2
  34. package/src/commands/explore-command.ts +1 -1
  35. package/src/commands/init-command.ts +5 -2
  36. package/src/commands/plan-command.ts +6 -1
  37. package/src/config.ts +1 -0
  38. package/src/explorbot.ts +1 -1
  39. package/src/explorer.ts +67 -20
  40. package/src/utils/web-element.ts +12 -10
package/src/explorer.ts CHANGED
@@ -19,6 +19,7 @@ import { Test } from './test-plan.ts';
19
19
  import { RequestStore } from './api/request-store.ts';
20
20
  import { XhrCapture } from './api/xhr-capture.ts';
21
21
  import { createDebug, log, tag } from './utils/logger.js';
22
+ import { WebElement, extractElementData } from './utils/web-element.ts';
22
23
 
23
24
  declare global {
24
25
  namespace NodeJS {
@@ -308,35 +309,23 @@ class Explorer {
308
309
  return action;
309
310
  }
310
311
 
311
- async annotateElements(): Promise<number> {
312
- const page = this.playwrightHelper.page;
313
- const roles = ['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem'];
314
- let idx = 1;
315
- for (const role of roles) {
316
- const elements = await page.getByRole(role).all();
317
- for (const el of elements) {
318
- await el.evaluate((node: Element, i: number) => {
319
- node.setAttribute('data-explorbot-eidx', String(i));
320
- }, idx);
321
- idx++;
322
- }
323
- }
324
- return idx - 1;
312
+ async annotateElements(): Promise<{ ariaSnapshot: string; elements: WebElement[] }> {
313
+ return annotatePageElements(this.playwrightHelper.page);
325
314
  }
326
315
 
327
316
  async visuallyAnnotateElements(opts?: { containers?: Array<{ css: string; label: string }> }): Promise<number> {
328
317
  return visuallyAnnotateContainers(this.playwrightHelper.page, opts?.containers || []);
329
318
  }
330
319
 
331
- async getEidxInContainer(containerCss: string | null): Promise<number[]> {
320
+ async getEidxInContainer(containerCss: string | null): Promise<string[]> {
332
321
  const page = this.playwrightHelper.page;
333
322
  try {
334
323
  const selector = containerCss ? `${containerCss} [data-explorbot-eidx]` : '[data-explorbot-eidx]';
335
324
  const elements = await page.locator(selector).all();
336
- const result: number[] = [];
325
+ const result: string[] = [];
337
326
  for (const el of elements) {
338
327
  const attr = await el.getAttribute('data-explorbot-eidx');
339
- if (attr) result.push(Number.parseInt(attr, 10));
328
+ if (attr) result.push(attr);
340
329
  }
341
330
  return result;
342
331
  } catch (error) {
@@ -348,13 +337,12 @@ class Explorer {
348
337
  }
349
338
  }
350
339
 
351
- async getEidxByLocator(locator: string, container?: string | null): Promise<number | null> {
340
+ async getEidxByLocator(locator: string, container?: string | null): Promise<string | null> {
352
341
  try {
353
342
  const page = this.playwrightHelper.page;
354
343
  const base = container ? page.locator(container) : page;
355
344
  const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator);
356
- const eidx = await el.first().getAttribute('data-explorbot-eidx');
357
- return eidx ? Number.parseInt(eidx, 10) : null;
345
+ return await el.first().getAttribute('data-explorbot-eidx');
358
346
  } catch (error) {
359
347
  if (this.isFatalBrowserError(error)) {
360
348
  tag('warning').log(`getEidxByLocator: ${error instanceof Error ? error.message : error}`);
@@ -710,4 +698,63 @@ function toCodeceptjsTest(test: Test): any {
710
698
  return codeceptjsTest;
711
699
  }
712
700
 
701
+ const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
702
+
703
+ const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
704
+
705
+ function parseAriaRefs(ariaSnapshot: string): Array<{ role: string; name: string; ref: string }> {
706
+ const entries: Array<{ role: string; name: string; ref: string }> = [];
707
+ for (const line of ariaSnapshot.split('\n')) {
708
+ const match = line.match(REF_LINE_PATTERN);
709
+ if (!match) continue;
710
+ if (!ANNOTATABLE_ROLES.has(match[2])) continue;
711
+ entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
712
+ }
713
+ return entries;
714
+ }
715
+
716
+ export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: string; elements: WebElement[] }> {
717
+ const ariaSnapshot: string = await page.locator('body').ariaSnapshot({ forAI: true });
718
+ const refEntries = parseAriaRefs(ariaSnapshot);
719
+
720
+ const byRole = new Map<string, Array<{ name: string; ref: string }>>();
721
+ for (const { role, name, ref } of refEntries) {
722
+ let list = byRole.get(role);
723
+ if (!list) {
724
+ list = [];
725
+ byRole.set(role, list);
726
+ }
727
+ list.push({ name, ref });
728
+ }
729
+
730
+ const elements: WebElement[] = [];
731
+ for (const [role, entries] of byRole) {
732
+ try {
733
+ const rawList = await page.getByRole(role).evaluateAll(
734
+ (domElements: Element[], [data, extractFnStr]: [Array<{ name: string; ref: string }>, string]) => {
735
+ const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any;
736
+ const results: any[] = [];
737
+ let ariaIdx = 0;
738
+ for (const el of domElements) {
739
+ if (ariaIdx >= data.length) break;
740
+ el.setAttribute('data-explorbot-eidx', data[ariaIdx].ref);
741
+ const elData = extract(el);
742
+ if (elData) results.push(elData);
743
+ ariaIdx++;
744
+ }
745
+ return results;
746
+ },
747
+ [entries, extractElementData.toString()]
748
+ );
749
+ for (const raw of rawList) {
750
+ elements.push(WebElement.fromRawData(raw, role));
751
+ }
752
+ } catch {
753
+ debugLog(`Failed to annotate role=${role}`);
754
+ }
755
+ }
756
+
757
+ return { ariaSnapshot, elements };
758
+ }
759
+
713
760
  export default Explorer;
@@ -7,6 +7,7 @@ type RawElementData = NonNullable<ReturnType<typeof extractElementData>>;
7
7
 
8
8
  export class WebElement {
9
9
  tag: string;
10
+ role: string;
10
11
  xpath: string;
11
12
  clickXPath: string;
12
13
  attrs: Record<string, string>;
@@ -14,8 +15,9 @@ export class WebElement {
14
15
  outerHTML: string;
15
16
  x: number;
16
17
  y: number;
17
- constructor(data: { tag: string; xpath: string; clickXPath: string; attrs: Record<string, string>; text: string; outerHTML?: string; x: number; y: number }) {
18
+ constructor(data: { tag: string; role?: string; xpath: string; clickXPath: string; attrs: Record<string, string>; text: string; outerHTML?: string; x: number; y: number }) {
18
19
  this.tag = data.tag;
20
+ this.role = data.role || data.attrs.role || '';
19
21
  this.xpath = data.xpath;
20
22
  this.clickXPath = data.clickXPath;
21
23
  this.attrs = data.attrs;
@@ -40,9 +42,8 @@ export class WebElement {
40
42
  return `(${this.x}, ${this.y})`;
41
43
  }
42
44
 
43
- get eidx(): number | null {
44
- const val = this.attrs['data-explorbot-eidx'] || this.attrs.eidx;
45
- return val ? Number.parseInt(val, 10) : null;
45
+ get eidx(): string | null {
46
+ return this.attrs['data-explorbot-eidx'] || this.attrs.eidx || null;
46
47
  }
47
48
 
48
49
  get isNavigationLink(): boolean {
@@ -56,9 +57,10 @@ export class WebElement {
56
57
  return cls.split(/\s+/).filter((c) => c.length > 2 && !isDynamicId(c) && !isGenericClass(c));
57
58
  }
58
59
 
59
- private static fromRawData(d: RawElementData): WebElement {
60
+ static fromRawData(d: RawElementData, role?: string): WebElement {
60
61
  return new WebElement({
61
62
  tag: d.tag,
63
+ role,
62
64
  xpath: '',
63
65
  clickXPath: buildClickableXPath({ tag: d.tag, allAttrs: d.allAttrs, text: d.text } as XPathMatch),
64
66
  attrs: d.allAttrs,
@@ -93,15 +95,15 @@ export class WebElement {
93
95
  }
94
96
  }
95
97
 
96
- static async fromEidx(page: any, eidx: number): Promise<WebElement | null> {
98
+ static async fromEidx(page: any, eidx: string): Promise<WebElement | null> {
97
99
  return WebElement.fromPlaywrightLocator(page.locator(`[data-explorbot-eidx="${eidx}"]`));
98
100
  }
99
101
 
100
- static async fromEidxList(page: any, eidxList: number[]): Promise<WebElement[]> {
102
+ static async fromEidxList(page: any, eidxList: string[]): Promise<WebElement[]> {
101
103
  if (eidxList.length === 0) return [];
102
104
 
103
105
  const rawList: RawElementData[] = await page.evaluate(
104
- ([list, extractFnStr]: [number[], string]) => {
106
+ ([list, extractFnStr]: [string[], string]) => {
105
107
  const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any;
106
108
  const results: any[] = [];
107
109
  for (const eidx of list) {
@@ -112,7 +114,7 @@ export class WebElement {
112
114
  }
113
115
  return results;
114
116
  },
115
- [eidxList, extractElementData.toString()] as [number[], string]
117
+ [eidxList, extractElementData.toString()] as [string[], string]
116
118
  );
117
119
 
118
120
  return rawList.map((d) => WebElement.fromRawData(d));
@@ -125,7 +127,7 @@ export class WebElement {
125
127
  }
126
128
  }
127
129
 
128
- function extractElementData(el: Element) {
130
+ export function extractElementData(el: Element) {
129
131
  const rect = el.getBoundingClientRect();
130
132
  if (rect.width === 0 && rect.height === 0) return null;
131
133