browser-pilot 0.0.6 → 0.0.8

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,7 +1,7 @@
1
1
  import { C as CDPClient } from './client-7Nqka5MV.cjs';
2
2
  import { C as ConnectOptions } from './types-D_uDqh0Z.cjs';
3
- import { P as Page } from './types-TVlTA7nH.cjs';
4
- export { d as ActionOptions, e as ActionResult, C as ConsoleHandler, f as ConsoleMessage, g as ConsoleMessageType, h as CustomSelectConfig, D as Dialog, i as DialogHandler, j as DialogType, k as Download, E as ElementInfo, l as ElementNotFoundError, m as EmulationState, n as ErrorHandler, F as FileInput, o as FillOptions, G as GeolocationOptions, I as InteractiveElement, N as NavigationError, p as NetworkIdleOptions, q as PageError, r as PageSnapshot, s as SnapshotNode, t as SubmitOptions, T as TimeoutError, u as TypeOptions, U as UserAgentMetadata, v as UserAgentOptions, V as ViewportOptions, W as WaitForOptions } from './types-TVlTA7nH.cjs';
3
+ import { P as Page } from './types-DklIxnbO.cjs';
4
+ export { d as ActionOptions, e as ActionResult, C as ConsoleHandler, f as ConsoleMessage, g as ConsoleMessageType, h as CustomSelectConfig, D as Dialog, i as DialogHandler, j as DialogType, k as Download, E as ElementInfo, l as ElementNotFoundError, m as EmulationState, n as ErrorHandler, a5 as FailureHint, F as FileInput, o as FillOptions, G as GeolocationOptions, I as InteractiveElement, N as NavigationError, p as NetworkIdleOptions, q as PageError, r as PageSnapshot, s as SnapshotNode, t as SubmitOptions, T as TimeoutError, u as TypeOptions, U as UserAgentMetadata, v as UserAgentOptions, V as ViewportOptions, W as WaitForOptions } from './types-DklIxnbO.cjs';
5
5
 
6
6
  /**
7
7
  * Browser class - manages CDP connection and pages
@@ -11,6 +11,10 @@ interface BrowserOptions extends ConnectOptions {
11
11
  /** Enable debug logging */
12
12
  debug?: boolean;
13
13
  }
14
+ interface PageOptions {
15
+ /** Specific target ID to attach to */
16
+ targetId?: string;
17
+ }
14
18
  declare class Browser {
15
19
  private cdp;
16
20
  private providerSession;
@@ -24,7 +28,7 @@ declare class Browser {
24
28
  * Get or create a page by name
25
29
  * If no name is provided, returns the first available page or creates a new one
26
30
  */
27
- page(name?: string): Promise<Page>;
31
+ page(name?: string, options?: PageOptions): Promise<Page>;
28
32
  /**
29
33
  * Create a new page (tab)
30
34
  */
package/dist/browser.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { C as CDPClient } from './client-7Nqka5MV.js';
2
2
  import { C as ConnectOptions } from './types-D_uDqh0Z.js';
3
- import { P as Page } from './types-CbdmaocU.js';
4
- export { d as ActionOptions, e as ActionResult, C as ConsoleHandler, f as ConsoleMessage, g as ConsoleMessageType, h as CustomSelectConfig, D as Dialog, i as DialogHandler, j as DialogType, k as Download, E as ElementInfo, l as ElementNotFoundError, m as EmulationState, n as ErrorHandler, F as FileInput, o as FillOptions, G as GeolocationOptions, I as InteractiveElement, N as NavigationError, p as NetworkIdleOptions, q as PageError, r as PageSnapshot, s as SnapshotNode, t as SubmitOptions, T as TimeoutError, u as TypeOptions, U as UserAgentMetadata, v as UserAgentOptions, V as ViewportOptions, W as WaitForOptions } from './types-CbdmaocU.js';
3
+ import { P as Page } from './types-Pv8KzZ6l.js';
4
+ export { d as ActionOptions, e as ActionResult, C as ConsoleHandler, f as ConsoleMessage, g as ConsoleMessageType, h as CustomSelectConfig, D as Dialog, i as DialogHandler, j as DialogType, k as Download, E as ElementInfo, l as ElementNotFoundError, m as EmulationState, n as ErrorHandler, a5 as FailureHint, F as FileInput, o as FillOptions, G as GeolocationOptions, I as InteractiveElement, N as NavigationError, p as NetworkIdleOptions, q as PageError, r as PageSnapshot, s as SnapshotNode, t as SubmitOptions, T as TimeoutError, u as TypeOptions, U as UserAgentMetadata, v as UserAgentOptions, V as ViewportOptions, W as WaitForOptions } from './types-Pv8KzZ6l.js';
5
5
 
6
6
  /**
7
7
  * Browser class - manages CDP connection and pages
@@ -11,6 +11,10 @@ interface BrowserOptions extends ConnectOptions {
11
11
  /** Enable debug logging */
12
12
  debug?: boolean;
13
13
  }
14
+ interface PageOptions {
15
+ /** Specific target ID to attach to */
16
+ targetId?: string;
17
+ }
14
18
  declare class Browser {
15
19
  private cdp;
16
20
  private providerSession;
@@ -24,7 +28,7 @@ declare class Browser {
24
28
  * Get or create a page by name
25
29
  * If no name is provided, returns the first available page or creates a new one
26
30
  */
27
- page(name?: string): Promise<Page>;
31
+ page(name?: string, options?: PageOptions): Promise<Page>;
28
32
  /**
29
33
  * Create a new page (tab)
30
34
  */
package/dist/browser.mjs CHANGED
@@ -1,14 +1,15 @@
1
1
  import {
2
2
  Browser,
3
- ElementNotFoundError,
4
- NavigationError,
5
3
  Page,
6
- TimeoutError,
7
4
  connect
8
- } from "./chunk-PCNEJAJ7.mjs";
5
+ } from "./chunk-JN44FHTK.mjs";
9
6
  import "./chunk-BCOZUKWS.mjs";
10
7
  import "./chunk-R3PS4PCM.mjs";
11
- import "./chunk-6RB3GKQP.mjs";
8
+ import {
9
+ ElementNotFoundError,
10
+ NavigationError,
11
+ TimeoutError
12
+ } from "./chunk-ZTQ37YQT.mjs";
12
13
  export {
13
14
  Browser,
14
15
  ElementNotFoundError,
@@ -5,8 +5,10 @@ import {
5
5
  createProvider
6
6
  } from "./chunk-R3PS4PCM.mjs";
7
7
  import {
8
- BatchExecutor
9
- } from "./chunk-6RB3GKQP.mjs";
8
+ BatchExecutor,
9
+ ElementNotFoundError,
10
+ TimeoutError
11
+ } from "./chunk-ZTQ37YQT.mjs";
10
12
 
11
13
  // src/network/interceptor.ts
12
14
  var RequestInterceptor = class {
@@ -422,33 +424,256 @@ async function waitForNetworkIdle(cdp, options = {}) {
422
424
  });
423
425
  }
424
426
 
425
- // src/browser/types.ts
426
- var ElementNotFoundError = class extends Error {
427
- selectors;
428
- constructor(selectors) {
429
- const selectorList = Array.isArray(selectors) ? selectors : [selectors];
430
- super(`Element not found: ${selectorList.join(", ")}`);
431
- this.name = "ElementNotFoundError";
432
- this.selectors = selectorList;
427
+ // src/browser/fuzzy-match.ts
428
+ function jaroWinkler(a, b) {
429
+ if (a.length === 0 && b.length === 0) return 0;
430
+ if (a.length === 0 || b.length === 0) return 0;
431
+ if (a === b) return 1;
432
+ const s1 = a.toLowerCase();
433
+ const s2 = b.toLowerCase();
434
+ const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
435
+ const s1Matches = new Array(s1.length).fill(false);
436
+ const s2Matches = new Array(s2.length).fill(false);
437
+ let matches = 0;
438
+ let transpositions = 0;
439
+ for (let i = 0; i < s1.length; i++) {
440
+ const start = Math.max(0, i - matchWindow);
441
+ const end = Math.min(i + matchWindow + 1, s2.length);
442
+ for (let j = start; j < end; j++) {
443
+ if (s2Matches[j] || s1[i] !== s2[j]) continue;
444
+ s1Matches[i] = true;
445
+ s2Matches[j] = true;
446
+ matches++;
447
+ break;
448
+ }
449
+ }
450
+ if (matches === 0) return 0;
451
+ let k = 0;
452
+ for (let i = 0; i < s1.length; i++) {
453
+ if (!s1Matches[i]) continue;
454
+ while (!s2Matches[k]) k++;
455
+ if (s1[i] !== s2[k]) transpositions++;
456
+ k++;
457
+ }
458
+ const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
459
+ let prefix = 0;
460
+ for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
461
+ if (s1[i] === s2[i]) {
462
+ prefix++;
463
+ } else {
464
+ break;
465
+ }
433
466
  }
434
- };
435
- var TimeoutError = class extends Error {
436
- constructor(message = "Operation timed out") {
437
- super(message);
438
- this.name = "TimeoutError";
467
+ const WINKLER_SCALING = 0.1;
468
+ return jaro + prefix * WINKLER_SCALING * (1 - jaro);
469
+ }
470
+ function stringSimilarity(a, b) {
471
+ if (a.length === 0 || b.length === 0) return 0;
472
+ const lowerA = a.toLowerCase();
473
+ const lowerB = b.toLowerCase();
474
+ if (lowerA === lowerB) return 1;
475
+ const jw = jaroWinkler(a, b);
476
+ let containsBonus = 0;
477
+ if (lowerB.includes(lowerA)) {
478
+ containsBonus = 0.2;
479
+ } else if (lowerA.includes(lowerB)) {
480
+ containsBonus = 0.1;
481
+ }
482
+ return Math.min(1, jw + containsBonus);
483
+ }
484
+ function scoreElement(query, element) {
485
+ const lowerQuery = query.toLowerCase();
486
+ const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
487
+ let nameScore = 0;
488
+ if (element.name) {
489
+ const lowerName = element.name.toLowerCase();
490
+ if (lowerName === lowerQuery) {
491
+ nameScore = 1;
492
+ } else if (lowerName.includes(lowerQuery)) {
493
+ nameScore = 0.8;
494
+ } else if (words.length > 0) {
495
+ const matchedWords = words.filter((w) => lowerName.includes(w));
496
+ nameScore = matchedWords.length / words.length * 0.7;
497
+ } else {
498
+ nameScore = stringSimilarity(query, element.name) * 0.6;
499
+ }
439
500
  }
440
- };
441
- var NavigationError = class extends Error {
442
- constructor(message) {
443
- super(message);
444
- this.name = "NavigationError";
501
+ let roleScore = 0;
502
+ const lowerRole = element.role.toLowerCase();
503
+ if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
504
+ roleScore = 0.3;
505
+ } else if (words.some((w) => lowerRole.includes(w))) {
506
+ roleScore = 0.2;
507
+ }
508
+ let selectorScore = 0;
509
+ const lowerSelector = element.selector.toLowerCase();
510
+ if (words.some((w) => lowerSelector.includes(w))) {
511
+ selectorScore = 0.2;
445
512
  }
513
+ const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
514
+ return totalScore;
515
+ }
516
+ function explainMatch(query, element, score) {
517
+ const reasons = [];
518
+ const lowerQuery = query.toLowerCase();
519
+ const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
520
+ if (element.name) {
521
+ const lowerName = element.name.toLowerCase();
522
+ if (lowerName === lowerQuery) {
523
+ reasons.push("exact name match");
524
+ } else if (lowerName.includes(lowerQuery)) {
525
+ reasons.push("name contains query");
526
+ } else if (words.some((w) => lowerName.includes(w))) {
527
+ const matchedWords = words.filter((w) => lowerName.includes(w));
528
+ reasons.push(`name contains: ${matchedWords.join(", ")}`);
529
+ } else if (stringSimilarity(query, element.name) > 0.5) {
530
+ reasons.push("similar name");
531
+ }
532
+ }
533
+ const lowerRole = element.role.toLowerCase();
534
+ if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
535
+ reasons.push(`role: ${element.role}`);
536
+ }
537
+ if (words.some((w) => element.selector.toLowerCase().includes(w))) {
538
+ reasons.push("selector match");
539
+ }
540
+ if (reasons.length === 0) {
541
+ reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
542
+ }
543
+ return reasons.join(", ");
544
+ }
545
+ function fuzzyMatchElements(query, elements, maxResults = 5) {
546
+ if (!query || query.length === 0) {
547
+ return [];
548
+ }
549
+ const THRESHOLD = 0.3;
550
+ const scored = elements.map((element) => ({
551
+ element,
552
+ score: scoreElement(query, element)
553
+ }));
554
+ return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
555
+ element: s.element,
556
+ score: s.score,
557
+ matchReason: explainMatch(query, s.element, s.score)
558
+ }));
559
+ }
560
+
561
+ // src/browser/hint-generator.ts
562
+ var ACTION_ROLE_MAP = {
563
+ click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
564
+ fill: ["textbox", "searchbox", "textarea"],
565
+ type: ["textbox", "searchbox", "textarea"],
566
+ submit: ["button", "form"],
567
+ select: ["combobox", "listbox", "option"],
568
+ check: ["checkbox", "radio", "switch"],
569
+ uncheck: ["checkbox", "switch"],
570
+ focus: [],
571
+ // Any focusable element
572
+ hover: [],
573
+ // Any element
574
+ clear: ["textbox", "searchbox", "textarea"]
446
575
  };
576
+ function extractIntent(selectors) {
577
+ const patterns = [];
578
+ let text = "";
579
+ for (const selector of selectors) {
580
+ if (selector.startsWith("ref:")) {
581
+ continue;
582
+ }
583
+ const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
584
+ if (idMatch) {
585
+ patterns.push(idMatch[1]);
586
+ }
587
+ const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
588
+ if (ariaMatch) {
589
+ patterns.push(ariaMatch[1]);
590
+ }
591
+ const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
592
+ if (testidMatch) {
593
+ patterns.push(testidMatch[1]);
594
+ }
595
+ const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
596
+ if (classMatch) {
597
+ patterns.push(classMatch[1]);
598
+ }
599
+ }
600
+ patterns.sort((a, b) => b.length - a.length);
601
+ text = patterns[0] ?? selectors[0] ?? "";
602
+ return { text, patterns };
603
+ }
604
+ function getHintType(selector) {
605
+ if (selector.startsWith("ref:")) return "ref";
606
+ if (selector.includes("data-testid")) return "testid";
607
+ if (selector.includes("aria-label")) return "aria";
608
+ if (selector.startsWith("#")) return "id";
609
+ return "css";
610
+ }
611
+ function getConfidence(score) {
612
+ if (score >= 0.8) return "high";
613
+ if (score >= 0.5) return "medium";
614
+ return "low";
615
+ }
616
+ function diversifyHints(candidates, maxHints) {
617
+ const hints = [];
618
+ const usedTypes = /* @__PURE__ */ new Set();
619
+ for (const candidate of candidates) {
620
+ if (hints.length >= maxHints) break;
621
+ const refSelector = `ref:${candidate.element.ref}`;
622
+ const hintType = getHintType(refSelector);
623
+ if (!usedTypes.has(hintType)) {
624
+ hints.push({
625
+ selector: refSelector,
626
+ reason: candidate.matchReason,
627
+ confidence: getConfidence(candidate.score),
628
+ element: {
629
+ ref: candidate.element.ref,
630
+ role: candidate.element.role,
631
+ name: candidate.element.name,
632
+ disabled: candidate.element.disabled
633
+ }
634
+ });
635
+ usedTypes.add(hintType);
636
+ } else if (hints.length < maxHints) {
637
+ hints.push({
638
+ selector: refSelector,
639
+ reason: candidate.matchReason,
640
+ confidence: getConfidence(candidate.score),
641
+ element: {
642
+ ref: candidate.element.ref,
643
+ role: candidate.element.role,
644
+ name: candidate.element.name,
645
+ disabled: candidate.element.disabled
646
+ }
647
+ });
648
+ }
649
+ }
650
+ return hints;
651
+ }
652
+ async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
653
+ let snapshot;
654
+ try {
655
+ snapshot = await page.snapshot();
656
+ } catch {
657
+ return [];
658
+ }
659
+ const intent = extractIntent(failedSelectors);
660
+ const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
661
+ let candidates = snapshot.interactiveElements;
662
+ if (roleFilter.length > 0) {
663
+ candidates = candidates.filter((el) => roleFilter.includes(el.role));
664
+ }
665
+ const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
666
+ if (matches.length === 0) {
667
+ return [];
668
+ }
669
+ return diversifyHints(matches, maxHints);
670
+ }
447
671
 
448
672
  // src/browser/page.ts
449
673
  var DEFAULT_TIMEOUT = 3e4;
450
674
  var Page = class {
451
675
  cdp;
676
+ _targetId;
452
677
  rootNodeId = null;
453
678
  batchExecutor;
454
679
  emulationState = {};
@@ -467,10 +692,19 @@ var Page = class {
467
692
  frameExecutionContexts = /* @__PURE__ */ new Map();
468
693
  /** Current frame's execution context ID (null = main frame default) */
469
694
  currentFrameContextId = null;
470
- constructor(cdp) {
695
+ /** Last matched selector from findElement (for selectorUsed tracking) */
696
+ _lastMatchedSelector;
697
+ constructor(cdp, targetId) {
471
698
  this.cdp = cdp;
699
+ this._targetId = targetId;
472
700
  this.batchExecutor = new BatchExecutor(this);
473
701
  }
702
+ /**
703
+ * Get the CDP target ID for this page
704
+ */
705
+ get targetId() {
706
+ return this._targetId;
707
+ }
474
708
  /**
475
709
  * Get the underlying CDP client for advanced operations.
476
710
  * Use with caution - prefer high-level Page methods when possible.
@@ -478,6 +712,13 @@ var Page = class {
478
712
  get cdpClient() {
479
713
  return this.cdp;
480
714
  }
715
+ /**
716
+ * Get the last matched selector from findElement (for selectorUsed tracking).
717
+ * Returns undefined if no selector has been matched yet.
718
+ */
719
+ getLastMatchedSelector() {
720
+ return this._lastMatchedSelector;
721
+ }
481
722
  /**
482
723
  * Initialize the page (enable required CDP domains)
483
724
  */
@@ -597,7 +838,9 @@ var Page = class {
597
838
  const element = await this.findElement(selector, options);
598
839
  if (!element) {
599
840
  if (options.optional) return false;
600
- throw new ElementNotFoundError(selector);
841
+ const selectorList = Array.isArray(selector) ? selector : [selector];
842
+ const hints = await generateHints(this, selectorList, "click");
843
+ throw new ElementNotFoundError(selector, hints);
601
844
  }
602
845
  await this.scrollIntoView(element.nodeId);
603
846
  const submitResult = await this.evaluateInFrame(
@@ -633,7 +876,9 @@ var Page = class {
633
876
  const element = await this.findElement(selector, options);
634
877
  if (!element) {
635
878
  if (options.optional) return false;
636
- throw new ElementNotFoundError(selector);
879
+ const selectorList = Array.isArray(selector) ? selector : [selector];
880
+ const hints = await generateHints(this, selectorList, "fill");
881
+ throw new ElementNotFoundError(selector, hints);
637
882
  }
638
883
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
639
884
  if (clear) {
@@ -711,7 +956,9 @@ var Page = class {
711
956
  const element = await this.findElement(selector, options);
712
957
  if (!element) {
713
958
  if (options.optional) return false;
714
- throw new ElementNotFoundError(selector);
959
+ const selectorList = Array.isArray(selector) ? selector : [selector];
960
+ const hints = await generateHints(this, selectorList, "select");
961
+ throw new ElementNotFoundError(selector, hints);
715
962
  }
716
963
  const values = Array.isArray(value) ? value : [value];
717
964
  await this.cdp.send("Runtime.evaluate", {
@@ -772,7 +1019,9 @@ var Page = class {
772
1019
  const element = await this.findElement(selector, options);
773
1020
  if (!element) {
774
1021
  if (options.optional) return false;
775
- throw new ElementNotFoundError(selector);
1022
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1023
+ const hints = await generateHints(this, selectorList, "check");
1024
+ throw new ElementNotFoundError(selector, hints);
776
1025
  }
777
1026
  const result = await this.cdp.send("Runtime.evaluate", {
778
1027
  expression: `(() => {
@@ -792,7 +1041,9 @@ var Page = class {
792
1041
  const element = await this.findElement(selector, options);
793
1042
  if (!element) {
794
1043
  if (options.optional) return false;
795
- throw new ElementNotFoundError(selector);
1044
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1045
+ const hints = await generateHints(this, selectorList, "uncheck");
1046
+ throw new ElementNotFoundError(selector, hints);
796
1047
  }
797
1048
  const result = await this.cdp.send("Runtime.evaluate", {
798
1049
  expression: `(() => {
@@ -812,13 +1063,40 @@ var Page = class {
812
1063
  * - 'auto' (default): Attempt to detect navigation for 1 second, then assume client-side handling
813
1064
  * - true: Wait for full navigation (traditional forms)
814
1065
  * - false: Return immediately (AJAX forms where you'll wait for something else)
1066
+ *
1067
+ * When targeting a <form> element directly, uses form.requestSubmit() which fires
1068
+ * the submit event and triggers HTML5 validation.
815
1069
  */
816
1070
  async submit(selector, options = {}) {
817
1071
  const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
818
1072
  const element = await this.findElement(selector, options);
819
1073
  if (!element) {
820
1074
  if (options.optional) return false;
821
- throw new ElementNotFoundError(selector);
1075
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1076
+ const hints = await generateHints(this, selectorList, "submit");
1077
+ throw new ElementNotFoundError(selector, hints);
1078
+ }
1079
+ const isFormElement = await this.evaluateInFrame(
1080
+ `(() => {
1081
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1082
+ return el instanceof HTMLFormElement;
1083
+ })()`
1084
+ );
1085
+ if (isFormElement.result.value) {
1086
+ await this.evaluateInFrame(
1087
+ `(() => {
1088
+ const form = document.querySelector(${JSON.stringify(element.selector)});
1089
+ if (form && form instanceof HTMLFormElement) {
1090
+ form.requestSubmit();
1091
+ }
1092
+ })()`
1093
+ );
1094
+ if (shouldWait === true) {
1095
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
1096
+ } else if (shouldWait === "auto") {
1097
+ await Promise.race([this.waitForNavigation({ timeout: 1e3, optional: true }), sleep2(500)]);
1098
+ }
1099
+ return true;
822
1100
  }
823
1101
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
824
1102
  if (method.includes("enter")) {
@@ -889,7 +1167,9 @@ var Page = class {
889
1167
  const element = await this.findElement(selector, options);
890
1168
  if (!element) {
891
1169
  if (options.optional) return false;
892
- throw new ElementNotFoundError(selector);
1170
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1171
+ const hints = await generateHints(this, selectorList, "focus");
1172
+ throw new ElementNotFoundError(selector, hints);
893
1173
  }
894
1174
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
895
1175
  return true;
@@ -902,7 +1182,9 @@ var Page = class {
902
1182
  const element = await this.findElement(selector, options);
903
1183
  if (!element) {
904
1184
  if (options.optional) return false;
905
- throw new ElementNotFoundError(selector);
1185
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1186
+ const hints = await generateHints(this, selectorList, "hover");
1187
+ throw new ElementNotFoundError(selector, hints);
906
1188
  }
907
1189
  await this.scrollIntoView(element.nodeId);
908
1190
  const box = await this.getBoxModel(element.nodeId);
@@ -1857,6 +2139,7 @@ var Page = class {
1857
2139
  async findElement(selectors, options = {}) {
1858
2140
  const { timeout = DEFAULT_TIMEOUT } = options;
1859
2141
  const selectorList = Array.isArray(selectors) ? selectors : [selectors];
2142
+ this._lastMatchedSelector = void 0;
1860
2143
  for (const selector of selectorList) {
1861
2144
  if (selector.startsWith("ref:")) {
1862
2145
  const ref = selector.slice(4);
@@ -1873,6 +2156,7 @@ var Page = class {
1873
2156
  }
1874
2157
  );
1875
2158
  if (pushResult.nodeIds?.[0]) {
2159
+ this._lastMatchedSelector = selector;
1876
2160
  return {
1877
2161
  nodeId: pushResult.nodeIds[0],
1878
2162
  backendNodeId,
@@ -1906,6 +2190,7 @@ var Page = class {
1906
2190
  "DOM.describeNode",
1907
2191
  { nodeId: queryResult.nodeId }
1908
2192
  );
2193
+ this._lastMatchedSelector = result.selector;
1909
2194
  return {
1910
2195
  nodeId: queryResult.nodeId,
1911
2196
  backendNodeId: describeResult2.node.backendNodeId,
@@ -1933,6 +2218,7 @@ var Page = class {
1933
2218
  "DOM.describeNode",
1934
2219
  { nodeId: nodeResult.nodeId }
1935
2220
  );
2221
+ this._lastMatchedSelector = result.selector;
1936
2222
  return {
1937
2223
  nodeId: nodeResult.nodeId,
1938
2224
  backendNodeId: describeResult.node.backendNodeId,
@@ -2039,14 +2325,24 @@ var Browser = class _Browser {
2039
2325
  * Get or create a page by name
2040
2326
  * If no name is provided, returns the first available page or creates a new one
2041
2327
  */
2042
- async page(name) {
2328
+ async page(name, options) {
2043
2329
  const pageName = name ?? "default";
2044
2330
  const cached = this.pages.get(pageName);
2045
2331
  if (cached) return cached;
2046
2332
  const targets = await this.cdp.send("Target.getTargets");
2047
2333
  const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
2048
2334
  let targetId;
2049
- if (pageTargets.length > 0) {
2335
+ if (options?.targetId) {
2336
+ const targetExists = pageTargets.some((t) => t.targetId === options.targetId);
2337
+ if (targetExists) {
2338
+ targetId = options.targetId;
2339
+ } else {
2340
+ console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
2341
+ targetId = pageTargets.length > 0 ? pageTargets[0].targetId : (await this.cdp.send("Target.createTarget", {
2342
+ url: "about:blank"
2343
+ })).targetId;
2344
+ }
2345
+ } else if (pageTargets.length > 0) {
2050
2346
  targetId = pageTargets[0].targetId;
2051
2347
  } else {
2052
2348
  const result = await this.cdp.send("Target.createTarget", {
@@ -2055,7 +2351,7 @@ var Browser = class _Browser {
2055
2351
  targetId = result.targetId;
2056
2352
  }
2057
2353
  await this.cdp.attachToTarget(targetId);
2058
- const page = new Page(this.cdp);
2354
+ const page = new Page(this.cdp, targetId);
2059
2355
  await page.init();
2060
2356
  this.pages.set(pageName, page);
2061
2357
  return page;
@@ -2068,7 +2364,7 @@ var Browser = class _Browser {
2068
2364
  url
2069
2365
  });
2070
2366
  await this.cdp.attachToTarget(result.targetId);
2071
- const page = new Page(this.cdp);
2367
+ const page = new Page(this.cdp, result.targetId);
2072
2368
  await page.init();
2073
2369
  const name = `page-${this.pages.size + 1}`;
2074
2370
  this.pages.set(name, page);
@@ -2141,13 +2437,12 @@ function connect(options) {
2141
2437
 
2142
2438
  export {
2143
2439
  RequestInterceptor,
2440
+ DEEP_QUERY_SCRIPT,
2144
2441
  waitForElement,
2145
2442
  waitForAnyElement,
2146
2443
  waitForNavigation,
2147
2444
  waitForNetworkIdle,
2148
- ElementNotFoundError,
2149
- TimeoutError,
2150
- NavigationError,
2445
+ fuzzyMatchElements,
2151
2446
  Page,
2152
2447
  Browser,
2153
2448
  connect
@@ -1,3 +1,28 @@
1
+ // src/browser/types.ts
2
+ var ElementNotFoundError = class extends Error {
3
+ selectors;
4
+ hints;
5
+ constructor(selectors, hints) {
6
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
7
+ super(`Element not found: ${selectorList.join(", ")}`);
8
+ this.name = "ElementNotFoundError";
9
+ this.selectors = selectorList;
10
+ this.hints = hints;
11
+ }
12
+ };
13
+ var TimeoutError = class extends Error {
14
+ constructor(message = "Operation timed out") {
15
+ super(message);
16
+ this.name = "TimeoutError";
17
+ }
18
+ };
19
+ var NavigationError = class extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "NavigationError";
23
+ }
24
+ };
25
+
1
26
  // src/actions/executor.ts
2
27
  var DEFAULT_TIMEOUT = 3e4;
3
28
  var BatchExecutor = class {
@@ -29,13 +54,15 @@ var BatchExecutor = class {
29
54
  });
30
55
  } catch (error) {
31
56
  const errorMessage = error instanceof Error ? error.message : String(error);
57
+ const hints = error instanceof ElementNotFoundError ? error.hints : void 0;
32
58
  results.push({
33
59
  index: i,
34
60
  action: step.action,
35
61
  selector: step.selector,
36
62
  success: false,
37
63
  durationMs: Date.now() - stepStart,
38
- error: errorMessage
64
+ error: errorMessage,
65
+ hints
39
66
  });
40
67
  if (onFail === "stop" && !step.optional) {
41
68
  return {
@@ -231,10 +258,12 @@ var BatchExecutor = class {
231
258
  }
232
259
  }
233
260
  /**
234
- * Get the first selector if multiple were provided
235
- * (actual used selector tracking would need to be implemented in Page)
261
+ * Get the actual selector that matched the element.
262
+ * Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
236
263
  */
237
264
  getUsedSelector(selector) {
265
+ const matched = this.page.getLastMatchedSelector();
266
+ if (matched) return matched;
238
267
  return Array.isArray(selector) ? selector[0] : selector;
239
268
  }
240
269
  };
@@ -246,6 +275,9 @@ function addBatchToPage(page) {
246
275
  }
247
276
 
248
277
  export {
278
+ ElementNotFoundError,
279
+ TimeoutError,
280
+ NavigationError,
249
281
  BatchExecutor,
250
282
  addBatchToPage
251
283
  };