agentic-browser 2.0.1 → 2.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.
@@ -8,7 +8,6 @@ import WebSocket, { WebSocketServer } from "ws";
8
8
  import { URL as URL$1, fileURLToPath } from "node:url";
9
9
  import crypto from "node:crypto";
10
10
  import { z } from "zod";
11
-
12
11
  //#region src/session/chrome-launcher.ts
13
12
  const CANDIDATES = [
14
13
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
@@ -22,7 +21,6 @@ function discoverChrome(explicitPath) {
22
21
  if (!found) throw new Error("No supported Chrome installation found.");
23
22
  return found;
24
23
  }
25
-
26
24
  //#endregion
27
25
  //#region src/session/extension-loader.ts
28
26
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -33,22 +31,93 @@ function loadControlExtension() {
33
31
  loadedAt: (/* @__PURE__ */ new Date()).toISOString()
34
32
  };
35
33
  }
36
-
37
34
  //#endregion
38
35
  //#region src/session/browser-controller.ts
39
36
  var CdpConnection = class CdpConnection {
40
37
  nextId = 0;
38
+ pendingRequests = /* @__PURE__ */ new Map();
39
+ pendingEvents = /* @__PURE__ */ new Map();
40
+ eventHandlers = /* @__PURE__ */ new Map();
41
+ closed = false;
41
42
  constructor(ws) {
42
43
  this.ws = ws;
44
+ this.ensureListening();
43
45
  }
44
46
  static async connect(targetWsUrl) {
45
47
  const ws = new WebSocket(targetWsUrl);
46
48
  await new Promise((resolve, reject) => {
47
- ws.once("open", () => resolve());
48
- ws.once("error", (err) => reject(err));
49
+ const onOpen = () => {
50
+ ws.off("error", onError);
51
+ resolve();
52
+ };
53
+ const onError = (err) => {
54
+ ws.off("open", onOpen);
55
+ reject(err);
56
+ };
57
+ ws.once("open", onOpen);
58
+ ws.once("error", onError);
49
59
  });
50
60
  return new CdpConnection(ws);
51
61
  }
62
+ ensureListening() {
63
+ this.ws.on("message", this.handleMessage);
64
+ this.ws.once("close", this.handleClose);
65
+ this.ws.on("error", this.handleError);
66
+ }
67
+ handleMessage = (raw) => {
68
+ let message;
69
+ try {
70
+ message = JSON.parse(raw.toString("utf8"));
71
+ } catch {
72
+ return;
73
+ }
74
+ if (typeof message.id === "number") {
75
+ const pending = this.pendingRequests.get(message.id);
76
+ if (!pending) return;
77
+ this.pendingRequests.delete(message.id);
78
+ clearTimeout(pending.timeout);
79
+ if (message.error) {
80
+ pending.reject(new Error(message.error.message));
81
+ return;
82
+ }
83
+ pending.resolve(message.result ?? {});
84
+ return;
85
+ }
86
+ if (!message.method) return;
87
+ const params = message.params ?? {};
88
+ const handlers = this.eventHandlers.get(message.method);
89
+ if (handlers) for (const handler of handlers) handler(params);
90
+ const waiters = this.pendingEvents.get(message.method);
91
+ if (!waiters?.length) return;
92
+ this.pendingEvents.delete(message.method);
93
+ for (const waiter of waiters) {
94
+ clearTimeout(waiter.timeout);
95
+ waiter.resolve(params);
96
+ }
97
+ };
98
+ handleError = (err) => {
99
+ this.failAllPending(err);
100
+ };
101
+ handleClose = () => {
102
+ this.failAllPending(/* @__PURE__ */ new Error("CDP connection closed"));
103
+ };
104
+ failAllPending(error) {
105
+ if (this.closed) return;
106
+ this.closed = true;
107
+ this.ws.off("message", this.handleMessage);
108
+ this.ws.off("error", this.handleError);
109
+ for (const pending of this.pendingRequests.values()) {
110
+ clearTimeout(pending.timeout);
111
+ pending.reject(error);
112
+ }
113
+ this.pendingRequests.clear();
114
+ for (const waiters of this.pendingEvents.values()) for (const waiter of waiters) {
115
+ clearTimeout(waiter.timeout);
116
+ waiter.reject(error);
117
+ }
118
+ this.pendingEvents.clear();
119
+ this.eventHandlers.clear();
120
+ }
52
121
  async send(method, params, timeoutMs = 15e3) {
53
122
  if (this.ws.readyState !== WebSocket.OPEN) throw new Error(`WebSocket not open (state=${this.ws.readyState}), cannot send '${method}'`);
54
123
  const id = ++this.nextId;
@@ -59,51 +128,55 @@ var CdpConnection = class CdpConnection {
59
128
  };
60
129
  return await new Promise((resolve, reject) => {
61
130
  const timeout = setTimeout(() => {
62
- this.ws.off("message", onMessage);
131
+ this.pendingRequests.delete(id);
63
132
  reject(/* @__PURE__ */ new Error(`CDP call '${method}' timed out after ${timeoutMs}ms`));
64
133
  }, timeoutMs);
65
- const onMessage = (raw) => {
66
- const message = JSON.parse(raw.toString("utf8"));
67
- if (message.id !== id) return;
68
- clearTimeout(timeout);
69
- this.ws.off("message", onMessage);
70
- if (message.error) {
71
- reject(new Error(message.error.message));
72
- return;
73
- }
74
- resolve(message.result ?? {});
75
- };
76
- this.ws.on("message", onMessage);
134
+ this.pendingRequests.set(id, {
135
+ resolve: (value) => resolve(value),
136
+ reject,
137
+ timeout
138
+ });
77
139
  this.ws.send(JSON.stringify(payload));
78
140
  });
79
141
  }
80
142
  waitForEvent(method, timeoutMs = 5e3) {
81
143
  return new Promise((resolve, reject) => {
82
144
  const timeout = setTimeout(() => {
83
- this.ws.off("message", onMessage);
145
+ const waiters = this.pendingEvents.get(method);
146
+ if (!waiters) {
147
+ reject(/* @__PURE__ */ new Error(`Timed out waiting for ${method}`));
148
+ return;
149
+ }
150
+ const next = waiters.filter((waiter) => waiter.timeout !== timeout);
151
+ if (next.length > 0) this.pendingEvents.set(method, next);
152
+ else this.pendingEvents.delete(method);
84
153
  reject(/* @__PURE__ */ new Error(`Timed out waiting for ${method}`));
85
154
  }, timeoutMs);
86
- const onMessage = (raw) => {
87
- const message = JSON.parse(raw.toString("utf8"));
88
- if (message.method !== method) return;
89
- clearTimeout(timeout);
90
- this.ws.off("message", onMessage);
91
- resolve(message.params ?? {});
92
- };
93
- this.ws.on("message", onMessage);
155
+ const waiters = this.pendingEvents.get(method) ?? [];
156
+ waiters.push({
157
+ resolve: (value) => resolve(value),
158
+ reject,
159
+ timeout
160
+ });
161
+ this.pendingEvents.set(method, waiters);
94
162
  });
95
163
  }
96
164
  onEvent(method, handler) {
97
- const onMessage = (raw) => {
98
- const message = JSON.parse(raw.toString("utf8"));
99
- if (message.method === method) handler(message.params ?? {});
165
+ const handlers = this.eventHandlers.get(method) ?? /* @__PURE__ */ new Set();
166
+ const wrapped = (params) => {
167
+ handler(params);
100
168
  };
101
- this.ws.on("message", onMessage);
169
+ handlers.add(wrapped);
170
+ this.eventHandlers.set(method, handlers);
102
171
  return () => {
103
- this.ws.off("message", onMessage);
172
+ const current = this.eventHandlers.get(method);
173
+ if (!current) return;
174
+ current.delete(wrapped);
175
+ if (current.size === 0) this.eventHandlers.delete(method);
104
176
  };
105
177
  }
106
178
  close() {
179
+ this.failAllPending(/* @__PURE__ */ new Error("CDP connection closed"));
107
180
  this.ws.close();
108
181
  }
109
182
  };
@@ -201,6 +274,214 @@ function resolveDefaultProfileDir() {
201
274
  if (platform === "win32") return path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome", "User Data");
202
275
  return path.join(home, ".config", "google-chrome");
203
276
  }
277
+ const LOCATOR_RUNTIME_HELPERS = String.raw`
278
+ const LOCATOR_SEPARATOR = ' >>> ';
279
+
280
+ function splitLocator(locator) {
281
+ return String(locator ?? '')
282
+ .split(/\s*>>>\s*/g)
283
+ .map((part) => part.trim())
284
+ .filter(Boolean);
285
+ }
286
+
287
+ function getFrameElementForDocument(doc) {
288
+ try {
289
+ return doc && doc.defaultView && doc.defaultView.frameElement ? doc.defaultView.frameElement : null;
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+
295
+ function createComposedChain(node) {
296
+ const chain = [];
297
+ let current = node;
298
+ while (current) {
299
+ chain.push(current);
300
+ if (current.parentNode) {
301
+ current = current.parentNode;
302
+ continue;
303
+ }
304
+ if (current.host) {
305
+ current = current.host;
306
+ continue;
307
+ }
308
+ if (current.nodeType === Node.DOCUMENT_NODE) {
309
+ current = getFrameElementForDocument(current);
310
+ continue;
311
+ }
312
+ current = null;
313
+ }
314
+ return chain;
315
+ }
316
+
317
+ function isSameComposedTarget(left, right) {
318
+ if (!left || !right) return false;
319
+ const seen = new Set(createComposedChain(left));
320
+ for (const current of createComposedChain(right)) {
321
+ if (seen.has(current)) {
322
+ return true;
323
+ }
324
+ }
325
+ return false;
326
+ }
327
+
328
+ function isWithinComposedSubtree(root, candidate) {
329
+ if (!root || !candidate) return false;
330
+ for (const current of createComposedChain(candidate)) {
331
+ if (current === root) {
332
+ return true;
333
+ }
334
+ }
335
+ return false;
336
+ }
337
+
338
+ function resolveLocator(locator) {
339
+ const parts = splitLocator(locator);
340
+ if (parts.length === 0) return null;
341
+
342
+ let root = document;
343
+ let element = null;
344
+ for (let index = 0; index < parts.length; index += 1) {
345
+ element = root.querySelector(parts[index]);
346
+ if (!element) return null;
347
+
348
+ if (index === parts.length - 1) {
349
+ return element;
350
+ }
351
+
352
+ if (element.tagName && element.tagName.toLowerCase() === 'iframe') {
353
+ let nextDocument = null;
354
+ try {
355
+ nextDocument = element.contentDocument;
356
+ } catch {
357
+ nextDocument = null;
358
+ }
359
+ if (!nextDocument) {
360
+ throw new Error('Unable to access iframe content for locator segment: ' + parts[index]);
361
+ }
362
+ root = nextDocument;
363
+ continue;
364
+ }
365
+
366
+ if (element.shadowRoot) {
367
+ root = element.shadowRoot;
368
+ continue;
369
+ }
370
+
371
+ throw new Error('Locator segment does not enter a shadow root or iframe: ' + parts[index]);
372
+ }
373
+
374
+ return element;
375
+ }
376
+
377
+ function resolveWithFallbacks(selector, fallbacks) {
378
+ let lastError = null;
379
+ const locators = [];
380
+ if (selector) locators.push(selector);
381
+ if (Array.isArray(fallbacks)) {
382
+ for (const fallback of fallbacks) {
383
+ locators.push(fallback);
384
+ }
385
+ }
386
+
387
+ for (const locator of locators) {
388
+ try {
389
+ const element = resolveLocator(locator);
390
+ if (element) {
391
+ return { element, locator };
392
+ }
393
+ } catch (error) {
394
+ lastError = error;
395
+ }
396
+ }
397
+
398
+ if (lastError) {
399
+ throw lastError;
400
+ }
401
+ throw new Error('Selector not found');
402
+ }
403
+
404
+ function ensureInView(element) {
405
+ if (element && typeof element.scrollIntoView === 'function') {
406
+ element.scrollIntoView({ block: 'center', inline: 'center' });
407
+ }
408
+ }
409
+
410
+ function toMainFramePoint(element) {
411
+ const rect = element.getBoundingClientRect();
412
+ let x = rect.left + rect.width / 2;
413
+ let y = rect.top + rect.height / 2;
414
+ let doc = element.ownerDocument;
415
+
416
+ while (doc) {
417
+ const frameElement = getFrameElementForDocument(doc);
418
+ if (!frameElement) {
419
+ break;
420
+ }
421
+ const frameRect = frameElement.getBoundingClientRect();
422
+ x += frameRect.left;
423
+ y += frameRect.top;
424
+ doc = frameElement.ownerDocument;
425
+ }
426
+
427
+ return { x, y, width: rect.width, height: rect.height };
428
+ }
429
+
430
+ function getTopFrameElement(element) {
431
+ let topFrame = null;
432
+ let doc = element.ownerDocument;
433
+ while (doc) {
434
+ const frameElement = getFrameElementForDocument(doc);
435
+ if (!frameElement) {
436
+ break;
437
+ }
438
+ topFrame = frameElement;
439
+ doc = frameElement.ownerDocument;
440
+ }
441
+ return topFrame;
442
+ }
443
+
444
+ function describeElement(element) {
445
+ if (!element || !element.tagName) return 'unknown';
446
+ const tag = element.tagName.toLowerCase();
447
+ const id = element.id ? '#' + element.id : '';
448
+ const className =
449
+ element.className && typeof element.className === 'string'
450
+ ? '.' + element.className.trim().split(/\s+/).filter(Boolean).join('.')
451
+ : '';
452
+ return tag + id + className;
453
+ }
454
+
455
+ function deepActiveElement() {
456
+ let current = document.activeElement;
457
+ while (current) {
458
+ if (current.shadowRoot && current.shadowRoot.activeElement) {
459
+ current = current.shadowRoot.activeElement;
460
+ continue;
461
+ }
462
+ if (current.tagName && current.tagName.toLowerCase() === 'iframe') {
463
+ try {
464
+ const frameActive = current.contentDocument && current.contentDocument.activeElement;
465
+ if (frameActive) {
466
+ current = frameActive;
467
+ continue;
468
+ }
469
+ } catch {
470
+ // ignore inaccessible frames
471
+ }
472
+ }
473
+ break;
474
+ }
475
+ return current;
476
+ }
477
+ `;
478
+ function buildLocatorRuntimeExpression(inputName, inputValue, body) {
479
+ return `(async () => {
480
+ ${LOCATOR_RUNTIME_HELPERS}
481
+ const ${inputName} = ${JSON.stringify(inputValue)};
482
+ ${body}
483
+ })()`;
484
+ }
204
485
  var ChromeCdpBrowserController = class {
205
486
  connections = /* @__PURE__ */ new Map();
206
487
  cleanupInterval;
@@ -392,6 +673,115 @@ var ChromeCdpBrowserController = class {
392
673
  return `${result}: ${dialog.message}`;
393
674
  });
394
675
  }
676
+ async preparePointerTarget(conn, payload) {
677
+ const expression = buildLocatorRuntimeExpression("payload", payload, `
678
+ const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
679
+ const el = resolved.element;
680
+ ensureInView(el);
681
+
682
+ const point = toMainFramePoint(el);
683
+ if (point.width === 0 && point.height === 0) {
684
+ throw new Error('Element has zero size - it may be hidden or not rendered');
685
+ }
686
+
687
+ const topEl = document.elementFromPoint(point.x, point.y);
688
+ const topFrame = getTopFrameElement(el);
689
+ if (topEl && !isSameComposedTarget(topEl, el) && (!topFrame || topEl !== topFrame)) {
690
+ throw new Error('Element is covered by another element: ' + describeElement(topEl));
691
+ }
692
+
693
+ return { x: point.x, y: point.y };
694
+ `);
695
+ const point = (await conn.send("Runtime.evaluate", {
696
+ expression,
697
+ returnByValue: true,
698
+ awaitPromise: true
699
+ })).result.value;
700
+ if (!point || typeof point.x !== "number" || typeof point.y !== "number") throw new Error("Unable to resolve pointer target");
701
+ return {
702
+ x: point.x,
703
+ y: point.y
704
+ };
705
+ }
706
+ async prepareTypeTarget(conn, payload) {
707
+ const expression = buildLocatorRuntimeExpression("payload", payload, `
708
+ const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
709
+ const el = resolved.element;
710
+ ensureInView(el);
711
+
712
+ const point = toMainFramePoint(el);
713
+ if (point.width === 0 && point.height === 0) {
714
+ throw new Error('Element has zero size - it may be hidden or not rendered');
715
+ }
716
+
717
+ const tag = el.tagName.toLowerCase();
718
+ const inputType = tag === 'input' ? (el.type || 'text').toLowerCase() : '';
719
+ const editable =
720
+ el.isContentEditable ||
721
+ tag === 'textarea' ||
722
+ (tag === 'input' &&
723
+ !['checkbox', 'radio', 'button', 'submit', 'reset', 'file', 'hidden'].includes(inputType));
724
+
725
+ if (!editable) {
726
+ throw new Error('Element is not text-editable');
727
+ }
728
+
729
+ if (typeof el.focus === 'function') {
730
+ el.focus();
731
+ }
732
+
733
+ if ((payload.text ?? '') === '') {
734
+ if (el.isContentEditable) {
735
+ el.textContent = '';
736
+ } else if ('value' in el) {
737
+ el.value = '';
738
+ }
739
+ el.dispatchEvent(new Event('input', { bubbles: true }));
740
+ return 'cleared';
741
+ }
742
+
743
+ if (el.isContentEditable) {
744
+ const selection = el.ownerDocument.getSelection ? el.ownerDocument.getSelection() : null;
745
+ if (selection) {
746
+ const range = el.ownerDocument.createRange();
747
+ range.selectNodeContents(el);
748
+ selection.removeAllRanges();
749
+ selection.addRange(range);
750
+ }
751
+ } else if (typeof el.select === 'function') {
752
+ el.select();
753
+ }
754
+
755
+ return 'ready';
756
+ `);
757
+ return (await conn.send("Runtime.evaluate", {
758
+ expression,
759
+ returnByValue: true,
760
+ awaitPromise: true
761
+ })).result.value ?? "ready";
762
+ }
763
+ async dispatchMouseClick(conn, x, y) {
764
+ await conn.send("Input.dispatchMouseEvent", {
765
+ type: "mouseMoved",
766
+ x,
767
+ y,
768
+ button: "none"
769
+ });
770
+ await conn.send("Input.dispatchMouseEvent", {
771
+ type: "mousePressed",
772
+ x,
773
+ y,
774
+ button: "left",
775
+ clickCount: 1
776
+ });
777
+ await conn.send("Input.dispatchMouseEvent", {
778
+ type: "mouseReleased",
779
+ x,
780
+ y,
781
+ button: "left",
782
+ clickCount: 1
783
+ });
784
+ }
395
785
  async interact(targetWsUrl, payload) {
396
786
  if (payload.action === "goBack") return await this.withRetry(targetWsUrl, async (conn) => {
397
787
  await this.ensureEnabled(targetWsUrl);
@@ -430,48 +820,35 @@ var ChromeCdpBrowserController = class {
430
820
  return "page refreshed";
431
821
  });
432
822
  if (payload.action === "dialog") return await this.handleDialogAction(targetWsUrl, payload);
433
- const expression = `(async () => {
434
- const payload = ${JSON.stringify(payload)};
435
-
436
- function resolveElement(selector, fallbacks) {
437
- let el = selector ? document.querySelector(selector) : null;
438
- if (el) return el;
439
- if (fallbacks && fallbacks.length) {
440
- for (const fb of fallbacks) {
441
- el = document.querySelector(fb);
442
- if (el) return el;
443
- }
444
- }
445
- throw new Error('Selector not found');
446
- }
447
-
448
- if (payload.action === 'click') {
449
- const el = resolveElement(payload.selector, payload.fallbackSelectors);
450
- const rect = el.getBoundingClientRect();
451
- if (rect.width === 0 && rect.height === 0) {
452
- throw new Error('Element has zero size – it may be hidden or not rendered');
453
- }
454
- const cx = rect.left + rect.width / 2;
455
- const cy = rect.top + rect.height / 2;
456
- const topEl = document.elementFromPoint(cx, cy);
457
- if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
458
- const tag = topEl.tagName.toLowerCase();
459
- const id = topEl.id ? '#' + topEl.id : '';
460
- const cls = topEl.className && typeof topEl.className === 'string' ? '.' + topEl.className.split(' ').join('.') : '';
461
- throw new Error('Element is covered by another element: ' + tag + id + cls);
462
- }
463
- el.click();
464
- return 'clicked';
465
- }
466
- if (payload.action === 'type') {
467
- const el = resolveElement(payload.selector, payload.fallbackSelectors);
468
- el.focus();
469
- el.value = payload.text ?? '';
470
- el.dispatchEvent(new Event('input', { bubbles: true }));
471
- return 'typed';
472
- }
823
+ if (payload.action === "click" || payload.action === "hover" || payload.action === "type") return await this.withRetry(targetWsUrl, async (conn) => {
824
+ await this.ensureEnabled(targetWsUrl);
825
+ if (payload.action === "click") {
826
+ const point = await this.preparePointerTarget(conn, payload);
827
+ await this.dispatchMouseClick(conn, point.x, point.y);
828
+ try {
829
+ await conn.waitForEvent("Page.frameNavigated", 50);
830
+ try {
831
+ await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
832
+ } catch {}
833
+ } catch {}
834
+ return "clicked";
835
+ }
836
+ if (payload.action === "hover") {
837
+ const point = await this.preparePointerTarget(conn, payload);
838
+ await conn.send("Input.dispatchMouseEvent", {
839
+ type: "mouseMoved",
840
+ x: point.x,
841
+ y: point.y,
842
+ button: "none"
843
+ });
844
+ return "hovered";
845
+ }
846
+ if (await this.prepareTypeTarget(conn, payload) === "ready") await conn.send("Input.insertText", { text: payload.text ?? "" });
847
+ return "typed";
848
+ });
849
+ const expression = buildLocatorRuntimeExpression("payload", payload, `
473
850
  if (payload.action === 'press') {
474
- const target = document.activeElement;
851
+ const target = deepActiveElement();
475
852
  if (!target) throw new Error('No active element to press key on');
476
853
  const key = payload.key ?? 'Enter';
477
854
  target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
@@ -482,7 +859,7 @@ var ChromeCdpBrowserController = class {
482
859
  const timeout = payload.timeoutMs ?? 2000;
483
860
  const started = Date.now();
484
861
  while (Date.now() - started < timeout) {
485
- if (document.querySelector(payload.selector)) {
862
+ if (resolveLocator(payload.selector)) {
486
863
  return 'found';
487
864
  }
488
865
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -499,25 +876,15 @@ var ChromeCdpBrowserController = class {
499
876
  }
500
877
  if (payload.action === 'scroll') {
501
878
  if (payload.selector) {
502
- const el = resolveElement(payload.selector, payload.fallbackSelectors);
879
+ const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
503
880
  el.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
504
881
  return 'scrolled element';
505
882
  }
506
883
  window.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
507
884
  return 'scrolled page';
508
885
  }
509
- if (payload.action === 'hover') {
510
- const el = resolveElement(payload.selector, payload.fallbackSelectors);
511
- const rect = el.getBoundingClientRect();
512
- const cx = rect.left + rect.width / 2;
513
- const cy = rect.top + rect.height / 2;
514
- el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, clientX: cx, clientY: cy }));
515
- el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: cx, clientY: cy }));
516
- el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: cx, clientY: cy }));
517
- return 'hovered';
518
- }
519
886
  if (payload.action === 'select') {
520
- const el = resolveElement(payload.selector, payload.fallbackSelectors);
887
+ const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
521
888
  if (el.tagName.toLowerCase() !== 'select') throw new Error('Element is not a <select>');
522
889
  el.value = payload.value ?? '';
523
890
  el.dispatchEvent(new Event('change', { bubbles: true }));
@@ -525,50 +892,42 @@ var ChromeCdpBrowserController = class {
525
892
  return 'selected ' + el.value;
526
893
  }
527
894
  if (payload.action === 'toggle') {
528
- const el = resolveElement(payload.selector, payload.fallbackSelectors);
895
+ const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
529
896
  el.click();
530
897
  const checked = el.checked !== undefined ? el.checked : el.getAttribute('aria-checked') === 'true';
531
898
  return 'toggled to ' + (checked ? 'checked' : 'unchecked');
532
899
  }
533
900
  throw new Error('Unsupported interact action');
534
- })()`;
901
+ `);
535
902
  return await this.withRetry(targetWsUrl, async (conn) => {
536
903
  await this.ensureEnabled(targetWsUrl);
537
- const value = (await conn.send("Runtime.evaluate", {
904
+ return (await conn.send("Runtime.evaluate", {
538
905
  expression,
539
906
  returnByValue: true,
540
907
  awaitPromise: true
541
908
  })).result.value ?? "";
542
- if (payload.action === "click" && value === "clicked") try {
543
- await conn.waitForEvent("Page.frameNavigated", 50);
544
- try {
545
- await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
546
- } catch {}
547
- } catch {}
548
- return value;
549
909
  });
550
910
  }
551
911
  async getContent(targetWsUrl, options) {
552
912
  if (options.mode === "a11y") return {
553
913
  mode: "a11y",
554
- content: await this.getAccessibilityTree(targetWsUrl)
914
+ content: await this.getAccessibilityTree(targetWsUrl, options.selector)
555
915
  };
556
- const expression = `(() => {
557
- const options = ${JSON.stringify(options)};
916
+ const expression = buildLocatorRuntimeExpression("options", options, `
558
917
  if (options.mode === 'title') return document.title ?? '';
559
918
  if (options.mode === 'html') {
560
919
  if (options.selector) {
561
- const el = document.querySelector(options.selector);
920
+ const el = resolveLocator(options.selector);
562
921
  return el ? el.outerHTML : '';
563
922
  }
564
923
  return document.documentElement?.outerHTML ?? '';
565
924
  }
566
925
  if (options.selector) {
567
- const el = document.querySelector(options.selector);
926
+ const el = resolveLocator(options.selector);
568
927
  return el ? el.innerText ?? '' : '';
569
928
  }
570
929
  return document.body?.innerText ?? '';
571
- })()`;
930
+ `);
572
931
  return await this.withRetry(targetWsUrl, async (conn) => {
573
932
  await this.ensureEnabled(targetWsUrl);
574
933
  const content = (await conn.send("Runtime.evaluate", {
@@ -581,64 +940,88 @@ var ChromeCdpBrowserController = class {
581
940
  };
582
941
  });
583
942
  }
584
- async getAccessibilityTree(targetWsUrl) {
585
- return await this.withRetry(targetWsUrl, async (conn) => {
586
- await conn.send("Accessibility.enable");
587
- const { nodes } = await conn.send("Accessibility.getFullAXTree");
588
- const childrenMap = /* @__PURE__ */ new Map();
589
- const nodeMap = /* @__PURE__ */ new Map();
590
- for (const node of nodes) {
591
- nodeMap.set(node.nodeId, node);
592
- if (node.parentId) {
593
- const siblings = childrenMap.get(node.parentId);
594
- if (siblings) siblings.push(node.nodeId);
595
- else childrenMap.set(node.parentId, [node.nodeId]);
943
+ formatAccessibilityTree(nodes) {
944
+ const childrenMap = /* @__PURE__ */ new Map();
945
+ const nodeMap = /* @__PURE__ */ new Map();
946
+ for (const node of nodes) {
947
+ nodeMap.set(node.nodeId, node);
948
+ if (node.parentId) {
949
+ const siblings = childrenMap.get(node.parentId);
950
+ if (siblings) siblings.push(node.nodeId);
951
+ else childrenMap.set(node.parentId, [node.nodeId]);
952
+ }
953
+ }
954
+ const lines = [];
955
+ const formatNode = (nodeId, depth) => {
956
+ const node = nodeMap.get(nodeId);
957
+ if (!node) return;
958
+ const role = node.role?.value ?? "unknown";
959
+ const name = node.name?.value ?? "";
960
+ const value = node.value?.value ?? "";
961
+ if (node.ignored) {
962
+ const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
963
+ for (const childId of children) formatNode(childId, depth);
964
+ return;
965
+ }
966
+ const skip = !name && !value && (role === "generic" || role === "none" || role === "GenericContainer");
967
+ if (!skip) {
968
+ let line = `${" ".repeat(depth)}${role}`;
969
+ if (name) line += ` "${name}"`;
970
+ if (value) line += ` value="${value}"`;
971
+ if (node.properties) {
972
+ for (const prop of node.properties) if (prop.value.value === true) line += ` [${prop.name}]`;
973
+ else if (prop.name === "checked" && prop.value.value === "mixed") line += ` [indeterminate]`;
596
974
  }
975
+ lines.push(line);
597
976
  }
598
- const lines = [];
599
- const formatNode = (nodeId, depth) => {
600
- const node = nodeMap.get(nodeId);
601
- if (!node) return;
602
- const role = node.role?.value ?? "unknown";
603
- const name = node.name?.value ?? "";
604
- const value = node.value?.value ?? "";
605
- if (node.ignored) {
606
- const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
607
- for (const childId of children) formatNode(childId, depth);
608
- return;
977
+ const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
978
+ for (const childId of children) formatNode(childId, skip ? depth : depth + 1);
979
+ };
980
+ const roots = nodes.filter((node) => !node.parentId || !nodeMap.has(node.parentId));
981
+ for (const root of roots) formatNode(root.nodeId, 0);
982
+ return lines.join("\n");
983
+ }
984
+ async getAccessibilityTree(targetWsUrl, selector) {
985
+ return await this.withRetry(targetWsUrl, async (conn) => {
986
+ await this.ensureEnabled(targetWsUrl);
987
+ await conn.send("Accessibility.enable");
988
+ let nodes;
989
+ if (selector) {
990
+ const objectGroup = "agentic-browser-a11y";
991
+ const selected = await conn.send("Runtime.evaluate", {
992
+ expression: buildLocatorRuntimeExpression("selector", selector, `
993
+ return resolveLocator(selector);
994
+ `),
995
+ objectGroup
996
+ });
997
+ const objectId = selected.result.objectId;
998
+ if (!objectId || selected.result.subtype === "null") {
999
+ try {
1000
+ await conn.send("Runtime.releaseObjectGroup", { objectGroup });
1001
+ } catch {}
1002
+ return "";
609
1003
  }
610
- const skip = !name && !value && (role === "generic" || role === "none" || role === "GenericContainer");
611
- if (!skip) {
612
- let line = `${" ".repeat(depth)}${role}`;
613
- if (name) line += ` "${name}"`;
614
- if (value) line += ` value="${value}"`;
615
- if (node.properties) {
616
- for (const prop of node.properties) if (prop.value.value === true) line += ` [${prop.name}]`;
617
- else if (prop.name === "checked" && prop.value.value === "mixed") line += ` [indeterminate]`;
618
- }
619
- lines.push(line);
1004
+ try {
1005
+ nodes = (await conn.send("Accessibility.getPartialAXTree", {
1006
+ objectId,
1007
+ fetchRelatives: false
1008
+ })).nodes;
1009
+ } finally {
1010
+ try {
1011
+ await conn.send("Runtime.releaseObjectGroup", { objectGroup });
1012
+ } catch {}
620
1013
  }
621
- const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
622
- for (const childId of children) formatNode(childId, skip ? depth : depth + 1);
623
- };
624
- const roots = nodes.filter((n) => !n.parentId);
625
- for (const root of roots) formatNode(root.nodeId, 0);
626
- return lines.join("\n");
1014
+ } else nodes = (await conn.send("Accessibility.getFullAXTree")).nodes;
1015
+ return this.formatAccessibilityTree(nodes);
627
1016
  });
628
1017
  }
629
1018
  async getInteractiveElements(targetWsUrl, options) {
630
- const expression = `(() => {
631
- const options = ${JSON.stringify(options)};
1019
+ const expression = buildLocatorRuntimeExpression("options", options, `
632
1020
  const visibleOnly = options.visibleOnly !== false;
633
1021
  const limit = options.limit ?? 50;
634
1022
  const scopeSelector = options.selector;
635
1023
  const roleFilter = options.roles ? new Set(options.roles) : null;
636
-
637
- const root = scopeSelector
638
- ? document.querySelector(scopeSelector) ?? document.body
639
- : document.body;
640
-
641
- const candidates = root.querySelectorAll([
1024
+ const candidateSelector = [
642
1025
  'a[href]',
643
1026
  'button',
644
1027
  'input:not([type="hidden"])',
@@ -654,43 +1037,15 @@ var ChromeCdpBrowserController = class {
654
1037
  '[onclick]',
655
1038
  '[tabindex]',
656
1039
  '[contenteditable="true"]',
657
- '[contenteditable=""]',
658
- ].join(','));
659
-
660
- const seen = new Set();
661
-
662
- function classifyRole(el) {
663
- const tag = el.tagName.toLowerCase();
664
- const ariaRole = el.getAttribute('role');
665
- if (tag === 'a') return 'link';
666
- if (tag === 'button' || ariaRole === 'button') return 'button';
667
- if (tag === 'input') {
668
- const t = (el.type || 'text').toLowerCase();
669
- if (t === 'checkbox') return 'checkbox';
670
- if (t === 'radio') return 'radio';
671
- return 'input';
672
- }
673
- if (tag === 'select') return 'select';
674
- if (tag === 'textarea') return 'textarea';
675
- if (el.isContentEditable) return 'contenteditable';
676
- if (ariaRole === 'link') return 'link';
677
- if (ariaRole === 'checkbox' || ariaRole === 'switch') return 'checkbox';
678
- if (ariaRole === 'radio') return 'radio';
679
- return 'custom';
680
- }
681
-
682
- function getActions(role, el) {
683
- switch (role) {
684
- case 'link': case 'button': case 'custom': return ['click'];
685
- case 'input': {
686
- const t = (el.type || 'text').toLowerCase();
687
- if (t === 'submit' || t === 'reset' || t === 'button' || t === 'file') return ['click'];
688
- return ['click', 'type', 'press'];
689
- }
690
- case 'textarea': case 'contenteditable': return ['click', 'type', 'press'];
691
- case 'select': return ['click', 'select'];
692
- case 'checkbox': case 'radio': return ['click', 'toggle'];
693
- default: return ['click'];
1040
+ '[contenteditable=""]'
1041
+ ].join(',');
1042
+
1043
+ let scopeElement = null;
1044
+ if (scopeSelector) {
1045
+ try {
1046
+ scopeElement = resolveLocator(scopeSelector) ?? document.body;
1047
+ } catch {
1048
+ scopeElement = document.body;
694
1049
  }
695
1050
  }
696
1051
 
@@ -698,79 +1053,90 @@ var ChromeCdpBrowserController = class {
698
1053
  return value.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
699
1054
  }
700
1055
 
701
- function tryUniqueSelector(sel) {
702
- try { return document.querySelectorAll(sel).length === 1 ? sel : null; }
703
- catch { return null; }
1056
+ function composeLocator(path, selector) {
1057
+ if (!path.length) return selector;
1058
+ return path.join(LOCATOR_SEPARATOR) + LOCATOR_SEPARATOR + selector;
1059
+ }
1060
+
1061
+ function tryUniqueSelector(root, selector) {
1062
+ try {
1063
+ return root.querySelectorAll(selector).length === 1 ? selector : null;
1064
+ } catch {
1065
+ return null;
1066
+ }
704
1067
  }
705
1068
 
706
- function buildSelector(el) {
1069
+ function buildSelector(root, el) {
707
1070
  if (el.id) return '#' + CSS.escape(el.id);
708
1071
 
709
1072
  const name = el.getAttribute('name');
710
1073
  if (name) {
711
1074
  const tag = el.tagName.toLowerCase();
712
- const sel = tryUniqueSelector(tag + '[name="' + escapeAttr(name) + '"]');
713
- if (sel) return sel;
1075
+ const selector = tryUniqueSelector(root, tag + '[name="' + escapeAttr(name) + '"]');
1076
+ if (selector) return selector;
714
1077
  }
715
1078
 
716
1079
  const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
717
1080
  if (testId) {
718
1081
  const attr = el.hasAttribute('data-testid') ? 'data-testid' : 'data-test-id';
719
- const sel = tryUniqueSelector('[' + attr + '="' + escapeAttr(testId) + '"]');
720
- if (sel) return sel;
1082
+ const selector = tryUniqueSelector(root, '[' + attr + '="' + escapeAttr(testId) + '"]');
1083
+ if (selector) return selector;
721
1084
  }
722
1085
 
723
1086
  const ariaLabel = el.getAttribute('aria-label');
724
1087
  if (ariaLabel) {
725
1088
  const tag = el.tagName.toLowerCase();
726
- const sel = tryUniqueSelector(tag + '[aria-label="' + escapeAttr(ariaLabel) + '"]');
727
- if (sel) return sel;
1089
+ const selector = tryUniqueSelector(root, tag + '[aria-label="' + escapeAttr(ariaLabel) + '"]');
1090
+ if (selector) return selector;
728
1091
  }
729
1092
 
730
1093
  const dataCy = el.getAttribute('data-cy');
731
1094
  if (dataCy) {
732
- const sel = tryUniqueSelector('[data-cy="' + escapeAttr(dataCy) + '"]');
733
- if (sel) return sel;
1095
+ const selector = tryUniqueSelector(root, '[data-cy="' + escapeAttr(dataCy) + '"]');
1096
+ if (selector) return selector;
734
1097
  }
735
1098
 
736
1099
  const dataTest = el.getAttribute('data-test');
737
1100
  if (dataTest) {
738
- const sel = tryUniqueSelector('[data-test="' + escapeAttr(dataTest) + '"]');
739
- if (sel) return sel;
1101
+ const selector = tryUniqueSelector(root, '[data-test="' + escapeAttr(dataTest) + '"]');
1102
+ if (selector) return selector;
740
1103
  }
741
1104
 
742
1105
  const role = el.getAttribute('role');
743
1106
  if (role && ariaLabel) {
744
- const sel = tryUniqueSelector('[role="' + escapeAttr(role) + '"][aria-label="' + escapeAttr(ariaLabel) + '"]');
745
- if (sel) return sel;
1107
+ const selector = tryUniqueSelector(
1108
+ root,
1109
+ '[role="' + escapeAttr(role) + '"][aria-label="' + escapeAttr(ariaLabel) + '"]'
1110
+ );
1111
+ if (selector) return selector;
746
1112
  }
747
1113
 
748
- // Path fallback — anchor at nearest ancestor with an id for shorter selectors
749
1114
  const parts = [];
750
1115
  let current = el;
751
- while (current && current !== document.documentElement) {
1116
+ while (current && current.nodeType === 1) {
752
1117
  const tag = current.tagName.toLowerCase();
753
1118
  if (current !== el && current.id) {
754
1119
  parts.unshift('#' + CSS.escape(current.id));
755
1120
  break;
756
1121
  }
757
1122
  const parent = current.parentElement;
758
- if (!parent) { parts.unshift(tag); break; }
759
- const siblings = Array.from(parent.children).filter(
760
- c => c.tagName === current.tagName
761
- );
1123
+ if (!parent) {
1124
+ parts.unshift(tag);
1125
+ break;
1126
+ }
1127
+ const siblings = Array.from(parent.children).filter((candidate) => candidate.tagName === current.tagName);
762
1128
  if (siblings.length === 1) {
763
1129
  parts.unshift(tag);
764
1130
  } else {
765
- const idx = siblings.indexOf(current) + 1;
766
- parts.unshift(tag + ':nth-of-type(' + idx + ')');
1131
+ const index = siblings.indexOf(current) + 1;
1132
+ parts.unshift(tag + ':nth-of-type(' + index + ')');
767
1133
  }
768
1134
  current = parent;
769
1135
  }
770
1136
  return parts.join(' > ');
771
1137
  }
772
1138
 
773
- function buildFallbackSelectors(el, primarySelector) {
1139
+ function buildFallbackSelectors(root, path, el, primarySelector) {
774
1140
  const fallbacks = [];
775
1141
  const candidates = [];
776
1142
 
@@ -779,8 +1145,7 @@ var ChromeCdpBrowserController = class {
779
1145
  const name = el.getAttribute('name');
780
1146
  if (name) {
781
1147
  const tag = el.tagName.toLowerCase();
782
- const sel = tag + '[name="' + escapeAttr(name) + '"]';
783
- candidates.push(sel);
1148
+ candidates.push(tag + '[name="' + escapeAttr(name) + '"]');
784
1149
  }
785
1150
 
786
1151
  const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
@@ -806,16 +1171,68 @@ var ChromeCdpBrowserController = class {
806
1171
  candidates.push('[role="' + escapeAttr(role) + '"][aria-label="' + escapeAttr(ariaLabel) + '"]');
807
1172
  }
808
1173
 
809
- for (const sel of candidates) {
810
- if (sel === primarySelector) continue;
811
- if (tryUniqueSelector(sel)) fallbacks.push(sel);
1174
+ for (const candidate of candidates) {
1175
+ if (!tryUniqueSelector(root, candidate)) continue;
1176
+ const locator = composeLocator(path, candidate);
1177
+ if (locator === primarySelector) continue;
1178
+ fallbacks.push(locator);
812
1179
  if (fallbacks.length >= 3) break;
813
1180
  }
814
1181
  return fallbacks;
815
1182
  }
816
1183
 
1184
+ function classifyRole(el) {
1185
+ const tag = el.tagName.toLowerCase();
1186
+ const ariaRole = el.getAttribute('role');
1187
+ if (tag === 'a') return 'link';
1188
+ if (tag === 'button' || ariaRole === 'button') return 'button';
1189
+ if (tag === 'input') {
1190
+ const type = (el.type || 'text').toLowerCase();
1191
+ if (type === 'checkbox') return 'checkbox';
1192
+ if (type === 'radio') return 'radio';
1193
+ return 'input';
1194
+ }
1195
+ if (tag === 'select') return 'select';
1196
+ if (tag === 'textarea') return 'textarea';
1197
+ if (el.isContentEditable) return 'contenteditable';
1198
+ if (ariaRole === 'link') return 'link';
1199
+ if (ariaRole === 'checkbox' || ariaRole === 'switch') return 'checkbox';
1200
+ if (ariaRole === 'radio') return 'radio';
1201
+ return 'custom';
1202
+ }
1203
+
1204
+ function getActions(role, el) {
1205
+ switch (role) {
1206
+ case 'link':
1207
+ case 'button':
1208
+ case 'custom':
1209
+ return ['click'];
1210
+ case 'input': {
1211
+ const type = (el.type || 'text').toLowerCase();
1212
+ if (type === 'submit' || type === 'reset' || type === 'button' || type === 'file') {
1213
+ return ['click'];
1214
+ }
1215
+ return ['click', 'type', 'press'];
1216
+ }
1217
+ case 'textarea':
1218
+ case 'contenteditable':
1219
+ return ['click', 'type', 'press'];
1220
+ case 'select':
1221
+ return ['click', 'select'];
1222
+ case 'checkbox':
1223
+ case 'radio':
1224
+ return ['click', 'toggle'];
1225
+ default:
1226
+ return ['click'];
1227
+ }
1228
+ }
1229
+
1230
+ function viewForElement(el) {
1231
+ return el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : window;
1232
+ }
1233
+
817
1234
  function isVisible(el) {
818
- const style = window.getComputedStyle(el);
1235
+ const style = viewForElement(el).getComputedStyle(el);
819
1236
  if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
820
1237
  return false;
821
1238
  }
@@ -829,16 +1246,16 @@ var ChromeCdpBrowserController = class {
829
1246
 
830
1247
  const tag = el.tagName.toLowerCase();
831
1248
  if (tag === 'input') {
832
- const v = el.value;
833
- if (v) return v.slice(0, 80);
834
- const ph = el.getAttribute('placeholder');
835
- if (ph) return ph.slice(0, 80);
1249
+ const value = el.value;
1250
+ if (value) return value.slice(0, 80);
1251
+ const placeholder = el.getAttribute('placeholder');
1252
+ if (placeholder) return placeholder.slice(0, 80);
836
1253
  return el.type || 'text';
837
1254
  }
838
1255
 
839
1256
  const directText = Array.from(el.childNodes)
840
- .filter(n => n.nodeType === 3)
841
- .map(n => n.textContent.trim())
1257
+ .filter((node) => node.nodeType === 3)
1258
+ .map((node) => node.textContent.trim())
842
1259
  .filter(Boolean)
843
1260
  .join(' ');
844
1261
  if (directText) return directText.slice(0, 80);
@@ -861,48 +1278,95 @@ var ChromeCdpBrowserController = class {
861
1278
  return ariaDisabled !== 'true';
862
1279
  }
863
1280
 
1281
+ function rootEntries(root) {
1282
+ if (!root) return [];
1283
+ if (root.nodeType === Node.DOCUMENT_NODE) {
1284
+ if (root.body) return [root.body];
1285
+ if (root.documentElement) return [root.documentElement];
1286
+ return [];
1287
+ }
1288
+ if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
1289
+ return Array.from(root.children || []);
1290
+ }
1291
+ return [root];
1292
+ }
1293
+
864
1294
  const results = [];
865
1295
  let totalFound = 0;
866
1296
 
867
- for (const el of candidates) {
868
- if (seen.has(el)) continue;
869
- seen.add(el);
1297
+ function maybeRecordElement(root, path, el) {
1298
+ if (scopeElement && !isWithinComposedSubtree(scopeElement, el)) {
1299
+ return;
1300
+ }
1301
+
1302
+ if (!el.matches(candidateSelector)) {
1303
+ return;
1304
+ }
870
1305
 
871
1306
  const role = classifyRole(el);
872
- if (roleFilter && !roleFilter.has(role)) continue;
1307
+ if (roleFilter && !roleFilter.has(role)) return;
873
1308
 
874
- const vis = isVisible(el);
875
- if (visibleOnly && !vis) continue;
1309
+ const visible = isVisible(el);
1310
+ if (visibleOnly && !visible) return;
876
1311
 
877
- totalFound++;
878
- if (results.length >= limit) continue;
1312
+ totalFound += 1;
1313
+ if (results.length >= limit) return;
879
1314
 
880
- const primarySelector = buildSelector(el);
1315
+ const selector = composeLocator(path, buildSelector(root, el));
881
1316
  const entry = {
882
- selector: primarySelector,
1317
+ selector,
883
1318
  role,
884
1319
  tagName: el.tagName.toLowerCase(),
885
1320
  text: getText(el),
886
1321
  actions: getActions(role, el),
887
- visible: vis,
1322
+ visible,
888
1323
  enabled: isEnabled(el),
889
1324
  };
890
1325
 
891
- const fb = buildFallbackSelectors(el, primarySelector);
892
- if (fb.length) entry.fallbackSelectors = fb;
1326
+ const fallbacks = buildFallbackSelectors(root, path, el, selector);
1327
+ if (fallbacks.length) entry.fallbackSelectors = fallbacks;
893
1328
 
894
1329
  if (role === 'link' && el.href) entry.href = el.href;
895
1330
  if (role === 'input') entry.inputType = (el.type || 'text').toLowerCase();
896
- const al = el.getAttribute('aria-label');
897
- if (al) entry.ariaLabel = al;
898
- const ph = el.getAttribute('placeholder');
899
- if (ph) entry.placeholder = ph;
1331
+ const ariaLabel = el.getAttribute('aria-label');
1332
+ if (ariaLabel) entry.ariaLabel = ariaLabel;
1333
+ const placeholder = el.getAttribute('placeholder');
1334
+ if (placeholder) entry.placeholder = placeholder;
900
1335
 
901
1336
  results.push(entry);
902
1337
  }
903
1338
 
1339
+ function walkRoot(root, path) {
1340
+ const queue = rootEntries(root).slice();
1341
+ while (queue.length) {
1342
+ const current = queue.shift();
1343
+ if (!current || current.nodeType !== 1) continue;
1344
+
1345
+ maybeRecordElement(root, path, current);
1346
+
1347
+ if (current.shadowRoot) {
1348
+ walkRoot(current.shadowRoot, path.concat(buildSelector(root, current)));
1349
+ }
1350
+
1351
+ if (current.tagName && current.tagName.toLowerCase() === 'iframe') {
1352
+ try {
1353
+ if (current.contentDocument) {
1354
+ walkRoot(current.contentDocument, path.concat(buildSelector(root, current)));
1355
+ }
1356
+ } catch {
1357
+ // ignore inaccessible frames
1358
+ }
1359
+ }
1360
+
1361
+ for (const child of Array.from(current.children)) {
1362
+ queue.push(child);
1363
+ }
1364
+ }
1365
+ }
1366
+
1367
+ walkRoot(document, []);
904
1368
  return { elements: results, totalFound, truncated: totalFound > results.length };
905
- })()`;
1369
+ `);
906
1370
  const emptyResult = {
907
1371
  elements: [],
908
1372
  totalFound: 0,
@@ -1178,7 +1642,6 @@ var MockBrowserController = class {
1178
1642
  terminate(_pid) {}
1179
1643
  closeConnection(_targetWsUrl) {}
1180
1644
  };
1181
-
1182
1645
  //#endregion
1183
1646
  //#region src/session/session-store.ts
1184
1647
  var SessionStore = class {
@@ -1267,7 +1730,6 @@ var SessionStore = class {
1267
1730
  fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf8");
1268
1731
  }
1269
1732
  };
1270
-
1271
1733
  //#endregion
1272
1734
  //#region src/session/session-manager.ts
1273
1735
  var SessionManager = class {
@@ -1640,7 +2102,6 @@ var SessionManager = class {
1640
2102
  }
1641
2103
  }
1642
2104
  };
1643
-
1644
2105
  //#endregion
1645
2106
  //#region src/transport/control-api.ts
1646
2107
  var ControlApi = class {
@@ -1694,7 +2155,6 @@ var ControlApi = class {
1694
2155
  return this.sessions.cleanupSessions(input);
1695
2156
  }
1696
2157
  };
1697
-
1698
2158
  //#endregion
1699
2159
  //#region src/lib/config.ts
1700
2160
  const DEFAULT_PORT = 43111;
@@ -1720,7 +2180,6 @@ function loadConfig(env = process.env) {
1720
2180
  userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0
1721
2181
  };
1722
2182
  }
1723
-
1724
2183
  //#endregion
1725
2184
  //#region src/observability/logger.ts
1726
2185
  var Logger = class {
@@ -1752,7 +2211,6 @@ var Logger = class {
1752
2211
  this.emit("error", message, context);
1753
2212
  }
1754
2213
  };
1755
-
1756
2214
  //#endregion
1757
2215
  //#region src/observability/event-store.ts
1758
2216
  const MAX_EVENTS_PER_SESSION = 500;
@@ -1778,7 +2236,6 @@ var EventStore = class {
1778
2236
  this.events.delete(sessionId);
1779
2237
  }
1780
2238
  };
1781
-
1782
2239
  //#endregion
1783
2240
  //#region src/auth/session-token.ts
1784
2241
  var SessionTokenService = class {
@@ -1806,7 +2263,6 @@ var SessionTokenService = class {
1806
2263
  this.tokens.set(sessionId, token);
1807
2264
  }
1808
2265
  };
1809
-
1810
2266
  //#endregion
1811
2267
  //#region src/transport/ws-server.ts
1812
2268
  var AuthenticatedWsServer = class {
@@ -1841,7 +2297,6 @@ var AuthenticatedWsServer = class {
1841
2297
  this.server = void 0;
1842
2298
  }
1843
2299
  };
1844
-
1845
2300
  //#endregion
1846
2301
  //#region src/memory/memory-index.ts
1847
2302
  function normalize(value) {
@@ -1902,11 +2357,10 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain) {
1902
2357
  var MemoryIndex = class {
1903
2358
  /** Domain → insights index, rebuilt lazily when insight list changes. */
1904
2359
  domainIndex = /* @__PURE__ */ new Map();
1905
- indexedInsights = null;
1906
- indexedLength = 0;
1907
- /** Rebuild the domain index when the underlying array or its size changes. */
2360
+ indexedRefs = [];
2361
+ /** Rebuild the domain index when the insight object set changes. */
1908
2362
  ensureIndex(insights) {
1909
- if (this.indexedInsights === insights && this.indexedLength === insights.length) return;
2363
+ if (this.indexedRefs.length === insights.length && insights.every((insight, index) => this.indexedRefs[index] === insight)) return;
1910
2364
  this.domainIndex.clear();
1911
2365
  for (const insight of insights) {
1912
2366
  const domain = normalize(insight.siteDomain);
@@ -1917,8 +2371,7 @@ var MemoryIndex = class {
1917
2371
  }
1918
2372
  bucket.push(insight);
1919
2373
  }
1920
- this.indexedInsights = insights;
1921
- this.indexedLength = insights.length;
2374
+ this.indexedRefs = insights.slice();
1922
2375
  }
1923
2376
  search(insights, input) {
1924
2377
  const normalizedIntent = normalize(input.taskIntent);
@@ -1940,7 +2393,6 @@ var MemoryIndex = class {
1940
2393
  return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
1941
2394
  }
1942
2395
  };
1943
-
1944
2396
  //#endregion
1945
2397
  //#region src/memory/staleness-detector.ts
1946
2398
  const MAX_SUSPECT_STRIKES = 2;
@@ -1983,7 +2435,6 @@ function applySuccess(insight) {
1983
2435
  updatedAt: now
1984
2436
  };
1985
2437
  }
1986
-
1987
2438
  //#endregion
1988
2439
  //#region src/memory/memory-schemas.ts
1989
2440
  const InsightFreshnessSchema = z.enum([
@@ -2036,7 +2487,6 @@ const TaskInsightSchema = z.object({
2036
2487
  selectorAliases: z.array(SelectorAliasSchema).default([])
2037
2488
  });
2038
2489
  const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
2039
-
2040
2490
  //#endregion
2041
2491
  //#region src/memory/task-insight-store.ts
2042
2492
  const EMPTY_STATE = { insights: [] };
@@ -2160,7 +2610,6 @@ var TaskInsightStore = class {
2160
2610
  });
2161
2611
  }
2162
2612
  };
2163
-
2164
2613
  //#endregion
2165
2614
  //#region src/memory/memory-service.ts
2166
2615
  const SEARCH_CACHE_TTL_MS = 2e3;
@@ -2380,7 +2829,6 @@ var MemoryService = class {
2380
2829
  return [...recipe, step].slice(-8);
2381
2830
  }
2382
2831
  };
2383
-
2384
2832
  //#endregion
2385
2833
  //#region src/cli/app.ts
2386
2834
  function createAppContext(env = process.env) {
@@ -2401,7 +2849,6 @@ function createAppContext(env = process.env) {
2401
2849
  memoryService: new MemoryService(config.logDir)
2402
2850
  };
2403
2851
  }
2404
-
2405
2852
  //#endregion
2406
2853
  //#region src/cli/runtime.ts
2407
2854
  var AgenticBrowserCore = class {
@@ -2475,6 +2922,5 @@ function createMockAgenticBrowserCore(env) {
2475
2922
  function createCliRuntime() {
2476
2923
  return createAgenticBrowserCore();
2477
2924
  }
2478
-
2479
2925
  //#endregion
2480
- export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };
2926
+ export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };