@browserbasehq/stagehand 1.1.2 → 1.3.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.
@@ -1,614 +0,0 @@
1
- (() => {
2
- // lib/dom/xpathUtils.ts
3
- function getParentElement(node) {
4
- return isElementNode(node) ? node.parentElement : node.parentNode;
5
- }
6
- function getCombinations(attributes, size) {
7
- const results = [];
8
- function helper(start, combo) {
9
- if (combo.length === size) {
10
- results.push([...combo]);
11
- return;
12
- }
13
- for (let i = start; i < attributes.length; i++) {
14
- combo.push(attributes[i]);
15
- helper(i + 1, combo);
16
- combo.pop();
17
- }
18
- }
19
- helper(0, []);
20
- return results;
21
- }
22
- function isXPathFirstResultElement(xpath, target) {
23
- try {
24
- const result = document.evaluate(
25
- xpath,
26
- document.documentElement,
27
- null,
28
- XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
29
- null
30
- );
31
- return result.snapshotItem(0) === target;
32
- } catch (error) {
33
- console.warn(`Invalid XPath expression: ${xpath}`, error);
34
- return false;
35
- }
36
- }
37
- function escapeXPathString(value) {
38
- if (value.includes("'")) {
39
- if (value.includes('"')) {
40
- return "concat(" + value.split(/('+)/).map((part) => {
41
- if (part === "'") {
42
- return `"'"`;
43
- } else if (part.startsWith("'") && part.endsWith("'")) {
44
- return `"${part}"`;
45
- } else {
46
- return `'${part}'`;
47
- }
48
- }).join(",") + ")";
49
- } else {
50
- return `"${value}"`;
51
- }
52
- } else {
53
- return `'${value}'`;
54
- }
55
- }
56
- async function generateXPathsForElement(element) {
57
- if (!element) return [];
58
- const [complexXPath, standardXPath, idBasedXPath] = await Promise.all([
59
- generateComplexXPath(element),
60
- generateStandardXPath(element),
61
- generatedIdBasedXPath(element)
62
- ]);
63
- return [...idBasedXPath ? [idBasedXPath] : [], standardXPath, complexXPath];
64
- }
65
- async function generateComplexXPath(element) {
66
- const parts = [];
67
- let currentElement = element;
68
- while (currentElement && (isTextNode(currentElement) || isElementNode(currentElement))) {
69
- if (isElementNode(currentElement)) {
70
- const el = currentElement;
71
- let selector = el.tagName.toLowerCase();
72
- const attributePriority = [
73
- "data-qa",
74
- "data-component",
75
- "data-role",
76
- "role",
77
- "aria-role",
78
- "type",
79
- "name",
80
- "aria-label",
81
- "placeholder",
82
- "title",
83
- "alt"
84
- ];
85
- const attributes = attributePriority.map((attr) => {
86
- let value = el.getAttribute(attr);
87
- if (attr === "href-full" && value) {
88
- value = el.getAttribute("href");
89
- }
90
- return value ? { attr: attr === "href-full" ? "href" : attr, value } : null;
91
- }).filter((attr) => attr !== null);
92
- let uniqueSelector = "";
93
- for (let i = 1; i <= attributes.length; i++) {
94
- const combinations = getCombinations(attributes, i);
95
- for (const combo of combinations) {
96
- const conditions = combo.map((a) => `@${a.attr}=${escapeXPathString(a.value)}`).join(" and ");
97
- const xpath2 = `//${selector}[${conditions}]`;
98
- if (isXPathFirstResultElement(xpath2, el)) {
99
- uniqueSelector = xpath2;
100
- break;
101
- }
102
- }
103
- if (uniqueSelector) break;
104
- }
105
- if (uniqueSelector) {
106
- parts.unshift(uniqueSelector.replace("//", ""));
107
- break;
108
- } else {
109
- const parent = getParentElement(el);
110
- if (parent) {
111
- const siblings = Array.from(parent.children).filter(
112
- (sibling) => sibling.tagName === el.tagName
113
- );
114
- const index = siblings.indexOf(el) + 1;
115
- selector += siblings.length > 1 ? `[${index}]` : "";
116
- }
117
- parts.unshift(selector);
118
- }
119
- }
120
- currentElement = getParentElement(currentElement);
121
- }
122
- const xpath = "//" + parts.join("/");
123
- return xpath;
124
- }
125
- async function generateStandardXPath(element) {
126
- const parts = [];
127
- while (element && (isTextNode(element) || isElementNode(element))) {
128
- let index = 0;
129
- let hasSameTypeSiblings = false;
130
- const siblings = element.parentElement ? Array.from(element.parentElement.childNodes) : [];
131
- for (let i = 0; i < siblings.length; i++) {
132
- const sibling = siblings[i];
133
- if (sibling.nodeType === element.nodeType && sibling.nodeName === element.nodeName) {
134
- index = index + 1;
135
- hasSameTypeSiblings = true;
136
- if (sibling.isSameNode(element)) {
137
- break;
138
- }
139
- }
140
- }
141
- if (element.nodeName !== "#text") {
142
- const tagName = element.nodeName.toLowerCase();
143
- const pathIndex = hasSameTypeSiblings ? `[${index}]` : "";
144
- parts.unshift(`${tagName}${pathIndex}`);
145
- }
146
- element = element.parentElement;
147
- }
148
- return parts.length ? `//${parts.join("//")}` : "";
149
- }
150
- async function generatedIdBasedXPath(element) {
151
- if (isElementNode(element) && element.id) {
152
- return `//*[@id='${element.id}']`;
153
- }
154
- return null;
155
- }
156
-
157
- // lib/dom/process.ts
158
- function isElementNode(node) {
159
- return node.nodeType === Node.ELEMENT_NODE;
160
- }
161
- function isTextNode(node) {
162
- return node.nodeType === Node.TEXT_NODE && Boolean(node.textContent?.trim());
163
- }
164
- async function processDom(chunksSeen) {
165
- const { chunk, chunksArray } = await pickChunk(chunksSeen);
166
- const { outputString, selectorMap } = await processElements2(
167
- chunk,
168
- void 0,
169
- void 0
170
- );
171
- console.log(
172
- `Stagehand (Browser Process): Extracted dom elements:
173
- ${outputString}`
174
- );
175
- return {
176
- outputString,
177
- selectorMap,
178
- chunk,
179
- chunks: chunksArray
180
- };
181
- }
182
- async function processAllOfDom() {
183
- console.log("Stagehand (Browser Process): Processing all of DOM");
184
- const viewportHeight = window.innerHeight;
185
- const documentHeight = document.documentElement.scrollHeight;
186
- const totalChunks = Math.ceil(documentHeight / viewportHeight);
187
- let index = 0;
188
- const results = [];
189
- for (let chunk = 0; chunk < totalChunks; chunk++) {
190
- const result = await processElements2(chunk, true, index);
191
- results.push(result);
192
- index += Object.keys(result.selectorMap).length;
193
- }
194
- await scrollToHeight(0);
195
- const allOutputString = results.map((result) => result.outputString).join("");
196
- const allSelectorMap = results.reduce(
197
- (acc, result) => ({ ...acc, ...result.selectorMap }),
198
- {}
199
- );
200
- console.log(
201
- `Stagehand (Browser Process): All dom elements: ${allOutputString}`
202
- );
203
- return {
204
- outputString: allOutputString,
205
- selectorMap: allSelectorMap
206
- };
207
- }
208
- async function scrollToHeight(height) {
209
- window.scrollTo({ top: height, left: 0, behavior: "smooth" });
210
- await new Promise((resolve) => {
211
- let scrollEndTimer;
212
- const handleScrollEnd = () => {
213
- clearTimeout(scrollEndTimer);
214
- scrollEndTimer = window.setTimeout(() => {
215
- window.removeEventListener("scroll", handleScrollEnd);
216
- resolve();
217
- }, 100);
218
- };
219
- window.addEventListener("scroll", handleScrollEnd, { passive: true });
220
- handleScrollEnd();
221
- });
222
- }
223
- var xpathCache = /* @__PURE__ */ new Map();
224
- async function processElements2(chunk, scrollToChunk = true, indexOffset = 0) {
225
- console.time("processElements:total");
226
- const viewportHeight = window.innerHeight;
227
- const chunkHeight = viewportHeight * chunk;
228
- const maxScrollTop = document.documentElement.scrollHeight - window.innerHeight;
229
- const offsetTop = Math.min(chunkHeight, maxScrollTop);
230
- if (scrollToChunk) {
231
- console.time("processElements:scroll");
232
- await scrollToHeight(offsetTop);
233
- console.timeEnd("processElements:scroll");
234
- }
235
- const candidateElements = [];
236
- const DOMQueue = [...document.body.childNodes];
237
- console.log("Stagehand (Browser Process): Generating candidate elements");
238
- console.time("processElements:findCandidates");
239
- while (DOMQueue.length > 0) {
240
- const element = DOMQueue.pop();
241
- let shouldAddElement = false;
242
- if (element && isElementNode(element)) {
243
- const childrenCount = element.childNodes.length;
244
- for (let i = childrenCount - 1; i >= 0; i--) {
245
- const child = element.childNodes[i];
246
- DOMQueue.push(child);
247
- }
248
- if (isInteractiveElement(element)) {
249
- if (isActive(element) && isVisible(element)) {
250
- shouldAddElement = true;
251
- }
252
- }
253
- if (isLeafElement(element)) {
254
- if (isActive(element) && isVisible(element)) {
255
- shouldAddElement = true;
256
- }
257
- }
258
- }
259
- if (element && isTextNode(element) && isTextVisible(element)) {
260
- shouldAddElement = true;
261
- }
262
- if (shouldAddElement) {
263
- candidateElements.push(element);
264
- }
265
- }
266
- console.timeEnd("processElements:findCandidates");
267
- const selectorMap = {};
268
- let outputString = "";
269
- console.log(
270
- `Stagehand (Browser Process): Processing candidate elements: ${candidateElements.length}`
271
- );
272
- console.time("processElements:processCandidates");
273
- console.time("processElements:generateXPaths");
274
- const xpathLists = await Promise.all(
275
- candidateElements.map(async (element) => {
276
- if (xpathCache.has(element)) {
277
- return xpathCache.get(element);
278
- }
279
- const xpaths = await generateXPathsForElement(element);
280
- xpathCache.set(element, xpaths);
281
- return xpaths;
282
- })
283
- );
284
- console.timeEnd("processElements:generateXPaths");
285
- candidateElements.forEach((element, index) => {
286
- const xpaths = xpathLists[index];
287
- let elementOutput = "";
288
- if (isTextNode(element)) {
289
- const textContent = element.textContent?.trim();
290
- if (textContent) {
291
- elementOutput += `${index + indexOffset}:${textContent}
292
- `;
293
- }
294
- } else if (isElementNode(element)) {
295
- const tagName = element.tagName.toLowerCase();
296
- const attributes = collectEssentialAttributes(element);
297
- const openingTag = `<${tagName}${attributes ? " " + attributes : ""}>`;
298
- const closingTag = `</${tagName}>`;
299
- const textContent = element.textContent?.trim() || "";
300
- elementOutput += `${index + indexOffset}:${openingTag}${textContent}${closingTag}
301
- `;
302
- }
303
- outputString += elementOutput;
304
- selectorMap[index + indexOffset] = xpaths;
305
- });
306
- console.timeEnd("processElements:processCandidates");
307
- console.timeEnd("processElements:total");
308
- return {
309
- outputString,
310
- selectorMap
311
- };
312
- }
313
- function collectEssentialAttributes(element) {
314
- const essentialAttributes = [
315
- "id",
316
- "class",
317
- "href",
318
- "src",
319
- "aria-label",
320
- "aria-name",
321
- "aria-role",
322
- "aria-description",
323
- "aria-expanded",
324
- "aria-haspopup"
325
- ];
326
- const attrs = essentialAttributes.map((attr) => {
327
- const value = element.getAttribute(attr);
328
- return value ? `${attr}="${value}"` : "";
329
- }).filter((attr) => attr !== "");
330
- Array.from(element.attributes).forEach((attr) => {
331
- if (attr.name.startsWith("data-")) {
332
- attrs.push(`${attr.name}="${attr.value}"`);
333
- }
334
- });
335
- return attrs.join(" ");
336
- }
337
- window.processDom = processDom;
338
- window.processAllOfDom = processAllOfDom;
339
- window.processElements = processElements2;
340
- window.scrollToHeight = scrollToHeight;
341
- var leafElementDenyList = ["SVG", "IFRAME", "SCRIPT", "STYLE", "LINK"];
342
- var interactiveElementTypes = [
343
- "A",
344
- "BUTTON",
345
- "DETAILS",
346
- "EMBED",
347
- "INPUT",
348
- "LABEL",
349
- "MENU",
350
- "MENUITEM",
351
- "OBJECT",
352
- "SELECT",
353
- "TEXTAREA",
354
- "SUMMARY"
355
- ];
356
- var interactiveRoles = [
357
- "button",
358
- "menu",
359
- "menuitem",
360
- "link",
361
- "checkbox",
362
- "radio",
363
- "slider",
364
- "tab",
365
- "tabpanel",
366
- "textbox",
367
- "combobox",
368
- "grid",
369
- "listbox",
370
- "option",
371
- "progressbar",
372
- "scrollbar",
373
- "searchbox",
374
- "switch",
375
- "tree",
376
- "treeitem",
377
- "spinbutton",
378
- "tooltip"
379
- ];
380
- var interactiveAriaRoles = ["menu", "menuitem", "button"];
381
- var isVisible = (element) => {
382
- const rect = element.getBoundingClientRect();
383
- if (rect.width === 0 || rect.height === 0 || rect.top < 0 || rect.top > window.innerHeight) {
384
- return false;
385
- }
386
- if (!isTopElement(element, rect)) {
387
- return false;
388
- }
389
- const visible = element.checkVisibility({
390
- checkOpacity: true,
391
- checkVisibilityCSS: true
392
- });
393
- return visible;
394
- };
395
- var isTextVisible = (element) => {
396
- const range = document.createRange();
397
- range.selectNodeContents(element);
398
- const rect = range.getBoundingClientRect();
399
- if (rect.width === 0 || rect.height === 0 || rect.top < 0 || rect.top > window.innerHeight) {
400
- return false;
401
- }
402
- const parent = element.parentElement;
403
- if (!parent) {
404
- return false;
405
- }
406
- if (!isTopElement(parent, rect)) {
407
- return false;
408
- }
409
- const visible = parent.checkVisibility({
410
- checkOpacity: true,
411
- checkVisibilityCSS: true
412
- });
413
- return visible;
414
- };
415
- function isTopElement(elem, rect) {
416
- const points = [
417
- { x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.25 },
418
- { x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.25 },
419
- { x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.75 },
420
- { x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.75 },
421
- { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
422
- ];
423
- return points.some((point) => {
424
- const topEl = document.elementFromPoint(point.x, point.y);
425
- let current = topEl;
426
- while (current && current !== document.body) {
427
- if (current.isSameNode(elem)) {
428
- return true;
429
- }
430
- current = current.parentElement;
431
- }
432
- return false;
433
- });
434
- }
435
- var isActive = (element) => {
436
- if (element.hasAttribute("disabled") || element.hasAttribute("hidden") || element.getAttribute("aria-disabled") === "true") {
437
- return false;
438
- }
439
- return true;
440
- };
441
- var isInteractiveElement = (element) => {
442
- const elementType = element.tagName;
443
- const elementRole = element.getAttribute("role");
444
- const elementAriaRole = element.getAttribute("aria-role");
445
- return elementType && interactiveElementTypes.includes(elementType) || elementRole && interactiveRoles.includes(elementRole) || elementAriaRole && interactiveAriaRoles.includes(elementAriaRole);
446
- };
447
- var isLeafElement = (element) => {
448
- if (element.textContent === "") {
449
- return false;
450
- }
451
- if (element.childNodes.length === 0) {
452
- return !leafElementDenyList.includes(element.tagName);
453
- }
454
- if (element.childNodes.length === 1 && isTextNode(element.childNodes[0])) {
455
- return true;
456
- }
457
- return false;
458
- };
459
- async function pickChunk(chunksSeen) {
460
- const viewportHeight = window.innerHeight;
461
- const documentHeight = document.documentElement.scrollHeight;
462
- const chunks = Math.ceil(documentHeight / viewportHeight);
463
- const chunksArray = Array.from({ length: chunks }, (_, i) => i);
464
- const chunksRemaining = chunksArray.filter((chunk2) => {
465
- return !chunksSeen.includes(chunk2);
466
- });
467
- const currentScrollPosition = window.scrollY;
468
- const closestChunk = chunksRemaining.reduce((closest, current) => {
469
- const currentChunkTop = viewportHeight * current;
470
- const closestChunkTop = viewportHeight * closest;
471
- return Math.abs(currentScrollPosition - currentChunkTop) < Math.abs(currentScrollPosition - closestChunkTop) ? current : closest;
472
- }, chunksRemaining[0]);
473
- const chunk = closestChunk;
474
- if (chunk === void 0) {
475
- throw new Error(`No chunks remaining to check: ${chunksRemaining}`);
476
- }
477
- return {
478
- chunk,
479
- chunksArray
480
- };
481
- }
482
-
483
- // lib/dom/utils.ts
484
- async function waitForDomSettle() {
485
- return new Promise((resolve) => {
486
- const createTimeout = () => {
487
- return setTimeout(() => {
488
- resolve();
489
- }, 2e3);
490
- };
491
- let timeout = createTimeout();
492
- const observer = new MutationObserver(() => {
493
- clearTimeout(timeout);
494
- timeout = createTimeout();
495
- });
496
- observer.observe(window.document.body, { childList: true, subtree: true });
497
- });
498
- }
499
- window.waitForDomSettle = waitForDomSettle;
500
-
501
- // lib/dom/debug.ts
502
- async function debugDom() {
503
- window.chunkNumber = 0;
504
- const { selectorMap, outputString } = await window.processElements(
505
- window.chunkNumber
506
- );
507
- drawChunk(selectorMap);
508
- setupChunkNav();
509
- }
510
- function drawChunk(selectorMap) {
511
- cleanupMarkers();
512
- Object.entries(selectorMap).forEach(([_index, selector]) => {
513
- const element = document.evaluate(
514
- selector,
515
- document,
516
- null,
517
- XPathResult.FIRST_ORDERED_NODE_TYPE,
518
- null
519
- ).singleNodeValue;
520
- if (element) {
521
- let rect;
522
- if (element.nodeType === Node.ELEMENT_NODE) {
523
- rect = element.getBoundingClientRect();
524
- } else {
525
- const range = document.createRange();
526
- range.selectNodeContents(element);
527
- rect = range.getBoundingClientRect();
528
- }
529
- const color = "grey";
530
- const overlay = document.createElement("div");
531
- overlay.style.position = "absolute";
532
- overlay.style.left = `${rect.left + window.scrollX}px`;
533
- overlay.style.top = `${rect.top + window.scrollY}px`;
534
- overlay.style.padding = "2px";
535
- overlay.style.width = `${rect.width}px`;
536
- overlay.style.height = `${rect.height}px`;
537
- overlay.style.backgroundColor = color;
538
- overlay.className = "stagehand-marker";
539
- overlay.style.opacity = "0.3";
540
- overlay.style.zIndex = "1000000000";
541
- overlay.style.border = "1px solid";
542
- overlay.style.pointerEvents = "none";
543
- document.body.appendChild(overlay);
544
- }
545
- });
546
- }
547
- async function cleanupDebug() {
548
- cleanupMarkers();
549
- cleanupNav();
550
- }
551
- function cleanupMarkers() {
552
- const markers = document.querySelectorAll(".stagehand-marker");
553
- markers.forEach((marker) => {
554
- marker.remove();
555
- });
556
- }
557
- function cleanupNav() {
558
- const stagehandNavElements = document.querySelectorAll(".stagehand-nav");
559
- stagehandNavElements.forEach((element) => {
560
- element.remove();
561
- });
562
- }
563
- function setupChunkNav() {
564
- const viewportHeight = window.innerHeight;
565
- const documentHeight = document.documentElement.scrollHeight;
566
- const totalChunks = Math.ceil(documentHeight / viewportHeight);
567
- if (window.chunkNumber > 0) {
568
- const prevChunkButton = document.createElement("button");
569
- prevChunkButton.className = "stagehand-nav";
570
- prevChunkButton.textContent = "Previous";
571
- prevChunkButton.style.marginLeft = "50px";
572
- prevChunkButton.style.position = "fixed";
573
- prevChunkButton.style.bottom = "10px";
574
- prevChunkButton.style.left = "50%";
575
- prevChunkButton.style.transform = "translateX(-50%)";
576
- prevChunkButton.style.zIndex = "1000000000";
577
- prevChunkButton.onclick = async () => {
578
- cleanupMarkers();
579
- cleanupNav();
580
- window.chunkNumber -= 1;
581
- window.scrollTo(0, window.chunkNumber * window.innerHeight);
582
- await window.waitForDomSettle();
583
- const { selectorMap } = await processElements(window.chunkNumber);
584
- drawChunk(selectorMap);
585
- setupChunkNav();
586
- };
587
- document.body.appendChild(prevChunkButton);
588
- }
589
- if (totalChunks > window.chunkNumber) {
590
- const nextChunkButton = document.createElement("button");
591
- nextChunkButton.className = "stagehand-nav";
592
- nextChunkButton.textContent = "Next";
593
- nextChunkButton.style.marginRight = "50px";
594
- nextChunkButton.style.position = "fixed";
595
- nextChunkButton.style.bottom = "10px";
596
- nextChunkButton.style.right = "50%";
597
- nextChunkButton.style.transform = "translateX(50%)";
598
- nextChunkButton.style.zIndex = "1000000000";
599
- nextChunkButton.onclick = async () => {
600
- cleanupMarkers();
601
- cleanupNav();
602
- window.chunkNumber += 1;
603
- window.scrollTo(0, window.chunkNumber * window.innerHeight);
604
- await window.waitForDomSettle();
605
- const { selectorMap } = await processElements(window.chunkNumber);
606
- drawChunk(selectorMap);
607
- setupChunkNav();
608
- };
609
- document.body.appendChild(nextChunkButton);
610
- }
611
- }
612
- window.debugDom = debugDom;
613
- window.cleanupDebug = cleanupDebug;
614
- })();