@browserflow-ai/exploration 0.0.6

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 (58) hide show
  1. package/dist/adapters/claude-cli.d.ts +57 -0
  2. package/dist/adapters/claude-cli.d.ts.map +1 -0
  3. package/dist/adapters/claude-cli.js +195 -0
  4. package/dist/adapters/claude-cli.js.map +1 -0
  5. package/dist/adapters/claude.d.ts +54 -0
  6. package/dist/adapters/claude.d.ts.map +1 -0
  7. package/dist/adapters/claude.js +160 -0
  8. package/dist/adapters/claude.js.map +1 -0
  9. package/dist/adapters/index.d.ts +6 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters/index.js +4 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/types.d.ts +196 -0
  14. package/dist/adapters/types.d.ts.map +1 -0
  15. package/dist/adapters/types.js +3 -0
  16. package/dist/adapters/types.js.map +1 -0
  17. package/dist/agent-browser-session.d.ts +62 -0
  18. package/dist/agent-browser-session.d.ts.map +1 -0
  19. package/dist/agent-browser-session.js +272 -0
  20. package/dist/agent-browser-session.js.map +1 -0
  21. package/dist/evidence.d.ts +111 -0
  22. package/dist/evidence.d.ts.map +1 -0
  23. package/dist/evidence.js +144 -0
  24. package/dist/evidence.js.map +1 -0
  25. package/dist/explorer.d.ts +180 -0
  26. package/dist/explorer.d.ts.map +1 -0
  27. package/dist/explorer.js +393 -0
  28. package/dist/explorer.js.map +1 -0
  29. package/dist/index.d.ts +15 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +15 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/locator-candidates.d.ts +127 -0
  34. package/dist/locator-candidates.d.ts.map +1 -0
  35. package/dist/locator-candidates.js +358 -0
  36. package/dist/locator-candidates.js.map +1 -0
  37. package/dist/step-executor.d.ts +99 -0
  38. package/dist/step-executor.d.ts.map +1 -0
  39. package/dist/step-executor.js +646 -0
  40. package/dist/step-executor.js.map +1 -0
  41. package/package.json +34 -0
  42. package/src/adapters/claude-cli.test.ts +134 -0
  43. package/src/adapters/claude-cli.ts +240 -0
  44. package/src/adapters/claude.test.ts +195 -0
  45. package/src/adapters/claude.ts +190 -0
  46. package/src/adapters/index.ts +21 -0
  47. package/src/adapters/types.ts +207 -0
  48. package/src/agent-browser-session.test.ts +369 -0
  49. package/src/agent-browser-session.ts +349 -0
  50. package/src/evidence.test.ts +239 -0
  51. package/src/evidence.ts +203 -0
  52. package/src/explorer.test.ts +321 -0
  53. package/src/explorer.ts +565 -0
  54. package/src/index.ts +51 -0
  55. package/src/locator-candidates.test.ts +602 -0
  56. package/src/locator-candidates.ts +441 -0
  57. package/src/step-executor.test.ts +696 -0
  58. package/src/step-executor.ts +783 -0
@@ -0,0 +1,441 @@
1
+ // @browserflow-ai/exploration - Locator candidate generation
2
+
3
+ /**
4
+ * A locator candidate with confidence score
5
+ */
6
+ export interface LocatorCandidate {
7
+ locator: string;
8
+ type: 'ref' | 'css' | 'xpath' | 'text' | 'role' | 'testid';
9
+ confidence: number;
10
+ description?: string;
11
+ }
12
+
13
+ /**
14
+ * Element information from browser snapshot
15
+ */
16
+ export interface ElementInfo {
17
+ ref: string;
18
+ tag: string;
19
+ role?: string;
20
+ text?: string;
21
+ ariaLabel?: string;
22
+ testId?: string;
23
+ className?: string;
24
+ id?: string;
25
+ attributes?: Record<string, string>;
26
+ }
27
+
28
+ /**
29
+ * Configuration for the locator generator
30
+ */
31
+ export interface LocatorCandidateGeneratorConfig {
32
+ preferredStrategies?: ('ref' | 'testid' | 'role' | 'text' | 'css' | 'xpath')[];
33
+ maxCandidates?: number;
34
+ }
35
+
36
+ /**
37
+ * Implicit ARIA roles for common HTML elements
38
+ */
39
+ const IMPLICIT_ROLES: Record<string, string> = {
40
+ a: 'link',
41
+ button: 'button',
42
+ input: 'textbox',
43
+ select: 'combobox',
44
+ textarea: 'textbox',
45
+ img: 'img',
46
+ nav: 'navigation',
47
+ main: 'main',
48
+ header: 'banner',
49
+ footer: 'contentinfo',
50
+ aside: 'complementary',
51
+ article: 'article',
52
+ section: 'region',
53
+ form: 'form',
54
+ table: 'table',
55
+ ul: 'list',
56
+ ol: 'list',
57
+ li: 'listitem',
58
+ dialog: 'dialog',
59
+ menu: 'menu',
60
+ option: 'option',
61
+ progress: 'progressbar',
62
+ meter: 'meter',
63
+ h1: 'heading',
64
+ h2: 'heading',
65
+ h3: 'heading',
66
+ h4: 'heading',
67
+ h5: 'heading',
68
+ h6: 'heading',
69
+ };
70
+
71
+ /**
72
+ * Maximum text length for text-based locators
73
+ */
74
+ const MAX_TEXT_LENGTH = 50;
75
+
76
+ /**
77
+ * LocatorCandidateGenerator - Generates ranked locator options for elements
78
+ *
79
+ * Uses multiple strategies to find elements:
80
+ * - Element refs from agent-browser snapshots
81
+ * - Test IDs (data-testid, data-test, etc.)
82
+ * - ARIA roles and labels
83
+ * - Text content matching
84
+ * - CSS selectors
85
+ * - XPath expressions
86
+ */
87
+ export class LocatorCandidateGenerator {
88
+ private preferredStrategies: string[];
89
+ private maxCandidates: number;
90
+
91
+ constructor(config: LocatorCandidateGeneratorConfig = {}) {
92
+ this.preferredStrategies = config.preferredStrategies ?? [
93
+ 'ref',
94
+ 'testid',
95
+ 'role',
96
+ 'text',
97
+ 'css',
98
+ ];
99
+ this.maxCandidates = config.maxCandidates ?? 5;
100
+ }
101
+
102
+ /**
103
+ * Generate locator candidates for an element based on natural language query
104
+ *
105
+ * @param query - Natural language description (e.g., "Submit button in the form")
106
+ * @param snapshot - Browser snapshot containing element refs
107
+ * @returns Promise resolving to ranked list of locator strings
108
+ */
109
+ async generateCandidates(
110
+ query: string,
111
+ snapshot: Record<string, unknown>
112
+ ): Promise<string[]> {
113
+ const candidates = await this.findCandidates(query, snapshot);
114
+ return candidates.slice(0, this.maxCandidates).map((c) => c.locator);
115
+ }
116
+
117
+ /**
118
+ * Generate detailed locator candidates with metadata
119
+ *
120
+ * @param query - Natural language description
121
+ * @param snapshot - Browser snapshot
122
+ * @returns Promise resolving to ranked list of candidates with scores
123
+ */
124
+ async generateDetailedCandidates(
125
+ query: string,
126
+ snapshot: Record<string, unknown>
127
+ ): Promise<LocatorCandidate[]> {
128
+ const candidates = await this.findCandidates(query, snapshot);
129
+ return candidates.slice(0, this.maxCandidates);
130
+ }
131
+
132
+ /**
133
+ * Generate all applicable locator candidates for a known element
134
+ *
135
+ * @param element - Element information
136
+ * @returns Promise resolving to ranked list of candidates
137
+ */
138
+ async generateCandidatesForElement(element: ElementInfo): Promise<LocatorCandidate[]> {
139
+ const candidates: LocatorCandidate[] = [];
140
+
141
+ // 1. Ref locator (highest confidence - direct reference)
142
+ candidates.push(this.generateRefLocator(element.ref));
143
+
144
+ // 2. Test ID locator (if present)
145
+ const testId = element.testId || element.attributes?.['data-testid'] || element.attributes?.['data-test'];
146
+ if (testId) {
147
+ candidates.push(this.generateTestIdLocator(testId));
148
+ }
149
+
150
+ // 3. Role locator (explicit or implicit)
151
+ const role = element.role || IMPLICIT_ROLES[element.tag];
152
+ if (role) {
153
+ const name = element.ariaLabel || (element.text && element.text.length <= MAX_TEXT_LENGTH ? element.text : undefined);
154
+ candidates.push(this.generateRoleLocator(role, name));
155
+ }
156
+
157
+ // 4. Text locator (if text is short enough)
158
+ if (element.text && element.text.length > 0 && element.text.length <= MAX_TEXT_LENGTH) {
159
+ // Use exact match for shorter text
160
+ const exact = element.text.length <= 30;
161
+ candidates.push(this.generateTextLocator(element.text, exact));
162
+ }
163
+
164
+ // 5. CSS selector (always as fallback)
165
+ const cssSelector = this.generateCssSelectorForElement(element);
166
+ candidates.push({
167
+ locator: cssSelector,
168
+ type: 'css',
169
+ confidence: this.calculateCssConfidence(element),
170
+ description: 'CSS selector',
171
+ });
172
+
173
+ // Sort by confidence (highest first)
174
+ return candidates.sort((a, b) => b.confidence - a.confidence);
175
+ }
176
+
177
+ /**
178
+ * Find candidate locators matching a query
179
+ */
180
+ private async findCandidates(
181
+ query: string,
182
+ snapshot: Record<string, unknown>
183
+ ): Promise<LocatorCandidate[]> {
184
+ // Handle empty query
185
+ if (!query || query.trim() === '') {
186
+ return [];
187
+ }
188
+
189
+ const normalizedQuery = query.toLowerCase().trim();
190
+ const refs = (snapshot.refs || {}) as Record<string, Record<string, unknown>>;
191
+
192
+ // Handle empty snapshot
193
+ if (Object.keys(refs).length === 0) {
194
+ return [];
195
+ }
196
+
197
+ // Find matching elements
198
+ const matchedElements: Array<{ element: ElementInfo; score: number }> = [];
199
+
200
+ for (const [ref, refData] of Object.entries(refs)) {
201
+ const element = this.parseElementFromRef(ref, refData);
202
+ const matchScore = this.calculateMatchScore(element, normalizedQuery);
203
+
204
+ if (matchScore > 0) {
205
+ matchedElements.push({ element, score: matchScore });
206
+ }
207
+ }
208
+
209
+ // Sort by match score (best match first)
210
+ matchedElements.sort((a, b) => b.score - a.score);
211
+
212
+ // Take the best matching element and generate candidates for it
213
+ if (matchedElements.length === 0) {
214
+ return [];
215
+ }
216
+
217
+ const bestMatch = matchedElements[0];
218
+ return this.generateCandidatesForElement(bestMatch.element);
219
+ }
220
+
221
+ /**
222
+ * Parse element info from snapshot ref data
223
+ */
224
+ private parseElementFromRef(ref: string, refData: Record<string, unknown>): ElementInfo {
225
+ return {
226
+ ref,
227
+ tag: (refData.tag as string) || 'div',
228
+ role: refData.role as string | undefined,
229
+ text: refData.text as string | undefined,
230
+ ariaLabel: refData.ariaLabel as string | undefined,
231
+ testId: refData.testId as string | undefined,
232
+ className: refData.className as string | undefined,
233
+ id: refData.id as string | undefined,
234
+ attributes: refData.attributes as Record<string, string> | undefined,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Calculate how well an element matches the query
240
+ * Returns a score from 0 (no match) to 1 (perfect match)
241
+ */
242
+ private calculateMatchScore(element: ElementInfo, normalizedQuery: string): number {
243
+ let score = 0;
244
+ const queryTerms = normalizedQuery.split(/\s+/);
245
+
246
+ // Match by text content
247
+ if (element.text) {
248
+ const normalizedText = element.text.toLowerCase();
249
+ if (normalizedText === normalizedQuery) {
250
+ score += 1.0; // Exact match
251
+ } else if (normalizedText.includes(normalizedQuery)) {
252
+ score += 0.8; // Contains query
253
+ } else {
254
+ // Check if all query terms are in the text
255
+ const matchedTerms = queryTerms.filter((term) => normalizedText.includes(term));
256
+ score += (matchedTerms.length / queryTerms.length) * 0.6;
257
+ }
258
+ }
259
+
260
+ // Match by aria-label
261
+ if (element.ariaLabel) {
262
+ const normalizedLabel = element.ariaLabel.toLowerCase();
263
+ if (normalizedLabel === normalizedQuery) {
264
+ score += 0.9;
265
+ } else if (normalizedLabel.includes(normalizedQuery)) {
266
+ score += 0.7;
267
+ } else {
268
+ const matchedTerms = queryTerms.filter((term) => normalizedLabel.includes(term));
269
+ score += (matchedTerms.length / queryTerms.length) * 0.5;
270
+ }
271
+ }
272
+
273
+ // Match by tag name
274
+ const normalizedTag = element.tag.toLowerCase();
275
+ if (queryTerms.includes(normalizedTag)) {
276
+ score += 0.4;
277
+ }
278
+
279
+ // Match by role
280
+ if (element.role) {
281
+ const normalizedRole = element.role.toLowerCase();
282
+ if (queryTerms.includes(normalizedRole)) {
283
+ score += 0.5;
284
+ }
285
+ } else {
286
+ // Check implicit role
287
+ const implicitRole = IMPLICIT_ROLES[element.tag];
288
+ if (implicitRole && queryTerms.includes(implicitRole)) {
289
+ score += 0.4;
290
+ }
291
+ }
292
+
293
+ // Match by test ID
294
+ if (element.testId) {
295
+ const normalizedTestId = element.testId.toLowerCase().replace(/[-_]/g, ' ');
296
+ const matchedTerms = queryTerms.filter((term) => normalizedTestId.includes(term));
297
+ score += (matchedTerms.length / queryTerms.length) * 0.3;
298
+ }
299
+
300
+ // Match by class name
301
+ if (element.className) {
302
+ const normalizedClasses = element.className.toLowerCase().replace(/[-_]/g, ' ');
303
+ const matchedTerms = queryTerms.filter((term) => normalizedClasses.includes(term));
304
+ score += (matchedTerms.length / queryTerms.length) * 0.2;
305
+ }
306
+
307
+ return Math.min(score, 1.0); // Cap at 1.0
308
+ }
309
+
310
+ /**
311
+ * Generate a CSS selector for an element
312
+ */
313
+ private generateCssSelectorForElement(element: ElementInfo): string {
314
+ // Prefer ID if available (most specific)
315
+ if (element.id) {
316
+ return `#${this.escapeCssSelector(element.id)}`;
317
+ }
318
+
319
+ // Build selector from tag and classes
320
+ let selector = element.tag;
321
+
322
+ if (element.className) {
323
+ const classes = element.className.split(/\s+/).filter((c) => c.length > 0);
324
+ if (classes.length > 0) {
325
+ // Use first 2 classes for specificity without being too brittle
326
+ const selectedClasses = classes.slice(0, 2);
327
+ selector += selectedClasses.map((c) => `.${this.escapeCssSelector(c)}`).join('');
328
+ }
329
+ }
330
+
331
+ return selector;
332
+ }
333
+
334
+ /**
335
+ * Escape special characters in CSS selectors
336
+ */
337
+ private escapeCssSelector(value: string): string {
338
+ return value.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
339
+ }
340
+
341
+ /**
342
+ * Calculate CSS selector confidence based on element attributes
343
+ */
344
+ private calculateCssConfidence(element: ElementInfo): number {
345
+ // ID-based selectors are more stable
346
+ if (element.id) {
347
+ return 0.75;
348
+ }
349
+
350
+ // Class-based selectors are moderately stable
351
+ if (element.className) {
352
+ return 0.6;
353
+ }
354
+
355
+ // Tag-only selectors are least stable
356
+ return 0.4;
357
+ }
358
+
359
+ /**
360
+ * Generate a locator from element ref
361
+ */
362
+ generateRefLocator(ref: string): LocatorCandidate {
363
+ return {
364
+ locator: `@${ref}`,
365
+ type: 'ref',
366
+ confidence: 1.0,
367
+ description: 'Element reference from snapshot',
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Generate a locator from test ID
373
+ */
374
+ generateTestIdLocator(testId: string): LocatorCandidate {
375
+ return {
376
+ locator: `[data-testid="${testId}"]`,
377
+ type: 'testid',
378
+ confidence: 0.95,
379
+ description: 'Test ID selector',
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Generate a locator from ARIA role
385
+ */
386
+ generateRoleLocator(role: string, name?: string): LocatorCandidate {
387
+ const locator = name ? `role=${role}[name="${name}"]` : `role=${role}`;
388
+ return {
389
+ locator,
390
+ type: 'role',
391
+ confidence: name ? 0.9 : 0.85,
392
+ description: `ARIA role: ${role}${name ? ` with name "${name}"` : ''}`,
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Generate a locator from text content
398
+ */
399
+ generateTextLocator(text: string, exact: boolean = false): LocatorCandidate {
400
+ const locator = exact ? `text="${text}"` : `text~="${text}"`;
401
+ return {
402
+ locator,
403
+ type: 'text',
404
+ confidence: exact ? 0.85 : 0.7,
405
+ description: `Text content${exact ? ' (exact)' : ' (partial)'}`,
406
+ };
407
+ }
408
+
409
+ /**
410
+ * Generate a CSS selector locator
411
+ */
412
+ generateCssLocator(selector: string): LocatorCandidate {
413
+ return {
414
+ locator: selector,
415
+ type: 'css',
416
+ confidence: 0.8,
417
+ description: 'CSS selector',
418
+ };
419
+ }
420
+
421
+ /**
422
+ * Get the maximum number of candidates to return
423
+ */
424
+ getMaxCandidates(): number {
425
+ return this.maxCandidates;
426
+ }
427
+
428
+ /**
429
+ * Set the maximum number of candidates to return
430
+ */
431
+ setMaxCandidates(max: number): void {
432
+ this.maxCandidates = max;
433
+ }
434
+
435
+ /**
436
+ * Get the preferred strategies in order
437
+ */
438
+ getPreferredStrategies(): string[] {
439
+ return [...this.preferredStrategies];
440
+ }
441
+ }