aether-mcp-server 2.0.2 → 2.1.1

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.
@@ -13,6 +13,8 @@ const fs_1 = require("fs");
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const os_1 = __importDefault(require("os"));
15
15
  const stealth_1 = require("./stealth");
16
+ const element_collector_1 = require("./element-collector");
17
+ const eval_scripts_1 = require("./eval-scripts");
16
18
  class CdpClient {
17
19
  ws = null;
18
20
  messageId = 0;
@@ -31,7 +33,97 @@ class CdpClient {
31
33
  mousePosition = null;
32
34
  networkLoggingAttached = false;
33
35
  diagnosticsLoggingAttached = false;
36
+ speedMultiplier = 1.0;
37
+ documentNodeId = null;
34
38
  constructor() { }
39
+ /** Set speed multiplier. 0 = instant, 1 = normal, 2 = slow (2x delays). */
40
+ setSpeed(m) {
41
+ this.speedMultiplier = Math.max(0, m);
42
+ }
43
+ /** Get current speed multiplier. */
44
+ getSpeedMultiplier() {
45
+ return this.speedMultiplier;
46
+ }
47
+ // ─── CDP DOM-level utilities (3-5x faster than Runtime.evaluate) ───
48
+ /** Invalidate cached document node ID. Call after navigation or tab switch. */
49
+ invalidateDocumentNodeId() {
50
+ this.documentNodeId = null;
51
+ }
52
+ /** Get the document root nodeId (cached per page). */
53
+ async getDocumentNodeId() {
54
+ try {
55
+ if (this.documentNodeId !== null)
56
+ return this.documentNodeId;
57
+ const result = await this.sendCommand("DOM.getDocument", { depth: -1, pierce: true });
58
+ const nodeId = result.root.nodeId;
59
+ this.documentNodeId = nodeId;
60
+ return nodeId;
61
+ }
62
+ catch {
63
+ return 0; // sentinel — caller should handle
64
+ }
65
+ }
66
+ /** Run DOM.querySelector via CDP (no JS injection). Returns nodeId or null. */
67
+ async querySelectorNodeId(selector) {
68
+ try {
69
+ const docId = await this.getDocumentNodeId();
70
+ if (!docId)
71
+ return null;
72
+ const result = await this.sendCommand("DOM.querySelector", { nodeId: docId, selector });
73
+ return result.nodeId ?? null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ /** Get bounding box from the layout engine (no JS injection). Returns null if hidden. */
80
+ async getBoxModel(nodeId) {
81
+ try {
82
+ const result = await this.sendCommand("DOM.getBoxModel", { nodeId });
83
+ if (!result || !result.model)
84
+ return null;
85
+ // quad[0..7] = x1,y1, x2,y2, x3,y3, x4,y4 (corners of content box)
86
+ // top-left = [quad[0], quad[1]], bottom-right = [quad[4], quad[5]]
87
+ const q = result.model.content;
88
+ return {
89
+ x: q[0],
90
+ y: q[1],
91
+ width: q[4] - q[0],
92
+ height: q[5] - q[1],
93
+ };
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ /** Focus an element via CDP DOM.focus. */
100
+ async focusNode(nodeId) {
101
+ try {
102
+ await this.sendCommand("DOM.focus", { nodeId });
103
+ }
104
+ catch {
105
+ // ignore
106
+ }
107
+ }
108
+ /** Get the center coordinates of an element matched by selector. */
109
+ async getElementCenter(selector) {
110
+ try {
111
+ const nodeId = await this.querySelectorNodeId(selector);
112
+ if (!nodeId)
113
+ return null;
114
+ const box = await this.getBoxModel(nodeId);
115
+ if (!box)
116
+ return null;
117
+ return {
118
+ x: box.x + box.width / 2,
119
+ y: box.y + box.height / 2,
120
+ width: box.width,
121
+ };
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
35
127
  /**
36
128
  * Connect to existing Chrome instance on given port
37
129
  */
@@ -91,25 +183,22 @@ class CdpClient {
91
183
  this.ws.on("open", async () => {
92
184
  this.connected = true;
93
185
  this.activeTarget = target;
186
+ this.documentNodeId = null;
94
187
  console.error(`[CDP] Connected to target: ${target.title} (${target.url})`);
95
188
  try {
96
- // Enable core CDP domains
97
- await Promise.all([
98
- this.sendCommand("Page.enable"),
99
- this.sendCommand("Network.enable"),
100
- this.sendCommand("Runtime.enable"),
101
- this.sendCommand("DOM.enable"),
102
- this.sendCommand("Log.enable").catch(() => { }),
103
- this.sendCommand("Animation.enable").catch(() => { }),
104
- this.sendCommand("Page.addScriptToEvaluateOnNewDocument", {
105
- source: stealth_1.STEALTH_SCRIPT
106
- })
107
- ]);
108
- // Keep animations running. Pausing them can freeze SPA loaders and leave pages looking blank.
109
- this.sendCommand("Animation.setPlaybackRate", { playbackRate: 1 }).catch(() => { });
189
+ // Enable minimal CDP domains on connect. Everything else is
190
+ // lazily enabled by the caller when needed.
191
+ await this.sendCommand("Runtime.enable").catch(() => { });
192
+ // Inject locator bootstrap once so element resolution works.
193
+ await this.sendCommand("Page.addScriptToEvaluateOnNewDocument", {
194
+ source: eval_scripts_1.LOCATOR_BOOTSTRAP_SCRIPT
195
+ });
196
+ // Stealth script — makes the browser harder to detect as automated.
197
+ await this.sendCommand("Page.addScriptToEvaluateOnNewDocument", {
198
+ source: stealth_1.STEALTH_SCRIPT
199
+ }).catch(() => { });
110
200
  this.attachNetworkLogging();
111
201
  this.attachDiagnosticsLogging();
112
- console.error("[CDP] Core CDP domains enabled");
113
202
  }
114
203
  catch (e) {
115
204
  console.error("[CDP] Failed to enable core domains:", e);
@@ -175,6 +264,7 @@ class CdpClient {
175
264
  * Navigate to URL
176
265
  */
177
266
  async navigate(url) {
267
+ this.documentNodeId = null;
178
268
  await this.sendCommand("Page.navigate", { url });
179
269
  }
180
270
  async navigateAndWait(url, timeout = 10000) {
@@ -208,7 +298,8 @@ class CdpClient {
208
298
  expression: `
209
299
  (function() {
210
300
  const withSoM = ${JSON.stringify(withSoM)};
211
-
301
+ ${element_collector_1.SHARED_DOM_HELPERS}
302
+
212
303
  // Remove existing overlays
213
304
  const oldContainer = document.getElementById('aether-som-container');
214
305
  if (oldContainer) oldContainer.remove();
@@ -245,11 +336,8 @@ class CdpClient {
245
336
  let text = el.innerText || el.textContent || '';
246
337
  text = text.trim().substring(0, 100);
247
338
 
248
- // Get selector
249
- let selector = '';
250
- if (el.id) selector = '#' + CSS.escape(el.id);
251
- else if (el.className && typeof el.className === 'string') selector = '.' + el.className.split(' ')[0];
252
- else selector = el.tagName.toLowerCase();
339
+ // Get a stable selector (shared with the locator engine)
340
+ const selector = aetherStableSelector(el);
253
341
 
254
342
  if (withSoM && container) {
255
343
  const id = String(validIndex);
@@ -285,7 +373,7 @@ class CdpClient {
285
373
  attributes: {
286
374
  type: el.getAttribute('type') || '',
287
375
  href: el.getAttribute('href') || '',
288
- role: el.getAttribute('role') || '',
376
+ role: aetherImplicitRole(el),
289
377
  'aria-label': el.getAttribute('aria-label') || ''
290
378
  }
291
379
  };
@@ -313,50 +401,28 @@ class CdpClient {
313
401
  });
314
402
  }
315
403
  /**
316
- * Wait for a selector to appear in DOM
404
+ * Wait for a selector to appear in DOM — fast CDP polling.
317
405
  */
318
406
  async waitForSelector(selector, timeout = 10000, options = {}) {
319
407
  const startTime = Date.now();
320
408
  let lastBox = null;
321
409
  let stableSince = 0;
322
410
  while (Date.now() - startTime < timeout) {
323
- const result = await this.sendCommand("Runtime.evaluate", {
324
- expression: `
325
- (function() {
326
- const el = document.querySelector(${JSON.stringify(selector)});
327
- if (!el) return { found: false };
328
- const rect = el.getBoundingClientRect();
329
- const computed = window.getComputedStyle(el);
330
- const visible = computed.display !== 'none' &&
331
- computed.visibility !== 'hidden' &&
332
- computed.opacity !== '0' &&
333
- rect.width > 0 &&
334
- rect.height > 0;
335
- return {
336
- found: true,
337
- visible,
338
- box: {
339
- x: Math.round(rect.left),
340
- y: Math.round(rect.top),
341
- width: Math.round(rect.width),
342
- height: Math.round(rect.height)
343
- }
344
- };
345
- })()
346
- `,
347
- returnByValue: true,
348
- });
349
- const state = result.result?.value;
350
- if (state?.found && (!options.visible || state.visible)) {
351
- if (!options.stable)
352
- return true;
353
- const box = state.box;
354
- const sameBox = lastBox &&
355
- lastBox.x === box.x &&
356
- lastBox.y === box.y &&
357
- lastBox.width === box.width &&
358
- lastBox.height === box.height;
359
- if (sameBox) {
411
+ const nodeId = await this.querySelectorNodeId(selector).catch(() => null);
412
+ if (!nodeId) {
413
+ await new Promise(r => setTimeout(r, 30));
414
+ continue;
415
+ }
416
+ if (!options.visible && !options.stable)
417
+ return true;
418
+ const box = await this.getBoxModel(nodeId);
419
+ if (!box || box.width === 0 || box.height === 0) {
420
+ await new Promise(r => setTimeout(r, 30));
421
+ continue;
422
+ }
423
+ if (options.stable) {
424
+ const boxKey = `${Math.round(box.x)},${Math.round(box.y)},${Math.round(box.width)},${Math.round(box.height)}`;
425
+ if (lastBox === boxKey) {
360
426
  if (!stableSince)
361
427
  stableSince = Date.now();
362
428
  if (Date.now() - stableSince >= 120)
@@ -364,10 +430,13 @@ class CdpClient {
364
430
  }
365
431
  else {
366
432
  stableSince = 0;
367
- lastBox = box;
433
+ lastBox = boxKey;
368
434
  }
369
435
  }
370
- await new Promise(r => setTimeout(r, 75));
436
+ else {
437
+ return true;
438
+ }
439
+ await new Promise(r => setTimeout(r, 30));
371
440
  }
372
441
  return false;
373
442
  }
@@ -416,7 +485,7 @@ class CdpClient {
416
485
  this.removeEventListener("Network.responseReceived", resetIdle);
417
486
  resolve(); // Resolve anyway after timeout
418
487
  }, timeout);
419
- idleCheckInterval = setInterval(checkIdle, 100);
488
+ idleCheckInterval = setInterval(checkIdle, Math.max(1, 100 * this.speedMultiplier));
420
489
  this.on("Network.requestWillBeSent", resetIdle);
421
490
  this.on("Network.responseReceived", resetIdle);
422
491
  });
@@ -521,51 +590,84 @@ class CdpClient {
521
590
  if (!target) {
522
591
  throw new Error(`Target not found: ${targetId}`);
523
592
  }
593
+ this.documentNodeId = null;
524
594
  await this.attachToTarget(target);
525
595
  }
526
596
  /**
527
- * Get a simplified version of the Accessibility Tree for AI agents.
597
+ * Get a rich Accessibility Tree preserving tree structure, node IDs, and selectors.
598
+ * AI agents can use this instead of screenshots for most decisions.
528
599
  */
529
- async getSimplifiedAccessibilityTree() {
600
+ async getRichAXTree() {
530
601
  await this.sendCommand("Accessibility.enable");
531
- const result = await this.sendCommand("Accessibility.getFullAXTree");
602
+ const result = await this.sendCommand("Accessibility.getFullAXTree", {});
532
603
  if (!result || !result.nodes)
533
- return [];
604
+ return { tree: null, nodeCount: 0 };
534
605
  const nodes = result.nodes;
535
- const interactiveNodes = [];
536
- // Map of node IDs for fast lookup
537
606
  const nodeMap = new Map();
538
- nodes.forEach((node) => nodeMap.set(node.nodeId, node));
539
- // Helper to get name from node properties
540
- const getNodeName = (node) => {
541
- if (node.name?.value)
542
- return node.name.value;
543
- const nameProp = node.properties?.find((p) => p.name === "name");
544
- return nameProp?.value?.value || "";
545
- };
546
- // Filter and simplify nodes
607
+ // First pass: build lookup
547
608
  nodes.forEach((node) => {
548
- const role = node.role?.value;
549
- const name = getNodeName(node);
550
- // Only include interactive or meaningful nodes
551
- const isInteractive = [
552
- "button", "link", "checkbox", "radio", "textbox", "searchbox",
553
- "combobox", "listbox", "menuitem", "slider", "switch", "tab"
554
- ].includes(role);
555
- const hasAction = node.properties?.some((p) => ["pressed", "expanded", "selected", "focused"].includes(p.name));
556
- if (isInteractive || (name && name.length > 0 && role !== "generic" && role !== "none")) {
557
- interactiveNodes.push({
558
- id: node.nodeId,
559
- role: role,
560
- name: name,
561
- description: node.description?.value || "",
562
- value: node.value?.value || "",
563
- disabled: node.properties?.find((p) => p.name === "disabled")?.value?.value || false,
564
- focused: node.properties?.find((p) => p.name === "focused")?.value?.value || false,
609
+ const name = node.name?.value || '';
610
+ const role = node.role?.value || 'unknown';
611
+ const properties = {};
612
+ if (node.properties) {
613
+ node.properties.forEach((p) => {
614
+ properties[p.name] = p.value?.value ?? p.value;
565
615
  });
566
616
  }
617
+ nodeMap.set(node.nodeId, {
618
+ axId: node.nodeId,
619
+ backendDOMNodeId: node.backendDOMNodeId,
620
+ role,
621
+ name,
622
+ description: node.description?.value || '',
623
+ value: node.value?.value || '',
624
+ disabled: !!properties.disabled,
625
+ focused: !!properties.focused,
626
+ checked: properties.checked,
627
+ selected: !!properties.selected,
628
+ expanded: !!properties.expanded,
629
+ pressed: !!properties.pressed,
630
+ children: [],
631
+ });
567
632
  });
568
- return interactiveNodes;
633
+ // Second pass: build tree by following parentIds
634
+ const roots = [];
635
+ nodes.forEach((node) => {
636
+ const enriched = nodeMap.get(node.nodeId);
637
+ if (!enriched)
638
+ return;
639
+ if (node.parentId && nodeMap.has(node.parentId)) {
640
+ nodeMap.get(node.parentId).children.push(enriched);
641
+ }
642
+ else {
643
+ roots.push(enriched);
644
+ }
645
+ });
646
+ // Prune: remove nodes with no name AND no meaningful role AND no children
647
+ function prune(node) {
648
+ node.children = node.children.filter(prune);
649
+ const hasContent = node.name || node.value ||
650
+ ['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'listbox', 'heading', 'alert', 'dialog', 'navigation', 'main', 'banner', 'contentinfo', 'complementary', 'form', 'search', 'table', 'list', 'listitem', 'menu', 'menuitem', 'tab', 'tablist', 'tabpanel', 'tree', 'treeitem', 'slider', 'switch', 'progressbar', 'img', 'image'].includes(node.role);
651
+ return hasContent || node.children.length > 0;
652
+ }
653
+ roots.forEach(prune);
654
+ // Count total nodes after pruning
655
+ let totalNodes = 0;
656
+ function count(n) { totalNodes++; n.children.forEach(count); }
657
+ roots.forEach(count);
658
+ return { tree: roots, nodeCount: totalNodes };
659
+ }
660
+ /**
661
+ * Backward-compat: flat list of accessible nodes.
662
+ * Calls getRichAXTree() and flattens the tree structure.
663
+ */
664
+ async getSimplifiedAccessibilityTree() {
665
+ const result = await this.getRichAXTree();
666
+ // Flatten for backward compat
667
+ const flat = [];
668
+ function flatten(n) { flat.push(n); n.children.forEach(flatten); }
669
+ (result.tree || []).forEach(flatten);
670
+ return flat;
569
671
  }
570
672
  async getNetworkTraffic() {
571
673
  return this.networkTraffic;
@@ -902,297 +1004,61 @@ class CdpClient {
902
1004
  async takeHeapSnapshot(reportProgress, treatGlobalObjectsAsRoots, captureNumericValue) {
903
1005
  await this.sendCommand("HeapProfiler.takeHeapSnapshot", { reportProgress, treatGlobalObjectsAsRoots, captureNumericValue });
904
1006
  }
905
- // Simple multi-octave noise function (fractional Brownian motion approximation)
906
- fBm(t, octaves = 3) {
907
- let value = 0;
908
- let amplitude = 1.0;
909
- let frequency = 1.0;
910
- let maxValue = 0;
911
- for (let j = 0; j < octaves; j++) {
912
- value += Math.sin(t * frequency * Math.PI * 2 + (j * 12.34)) * amplitude;
913
- maxValue += amplitude;
914
- amplitude *= 0.5;
915
- frequency *= 2.0;
916
- }
917
- return value / maxValue;
918
- }
919
1007
  /**
920
- * Click at coordinates with human-like timing, micro-jitter, and optional target width.
1008
+ * Click at coordinates fast path. Direct CDP dispatch, no Bezier/tremor/jitter.
921
1009
  */
922
- async click(x, y, button = "left", targetWidth) {
923
- const targetX = Math.round(Number(x));
924
- const targetY = Math.round(Number(y));
925
- await this.moveMouse(targetX, targetY, targetWidth);
926
- // Pre-click hover pause — humans don't instantly press after arriving
927
- await new Promise((r) => setTimeout(r, 80 + Math.random() * 140));
928
- // Micro-jitter at the moment of click (hand tremor)
929
- const cx = targetX + Math.round((Math.random() - 0.5) * 3);
930
- const cy = targetY + Math.round((Math.random() - 0.5) * 3);
1010
+ async click(x, y, button = "left", _targetWidth) {
1011
+ const rx = Math.round(Number(x));
1012
+ const ry = Math.round(Number(y));
1013
+ this.mousePosition = { x: rx, y: ry };
931
1014
  await this.sendCommand("Input.dispatchMouseEvent", {
932
- type: "mousePressed", x: cx, y: cy, button, clickCount: 1, pointerType: "mouse",
1015
+ type: "mousePressed", x: rx, y: ry, button, clickCount: 1, pointerType: "mouse",
933
1016
  });
934
- // Natural hold duration before release
935
- await new Promise((r) => setTimeout(r, 60 + Math.random() * 110));
936
1017
  await this.sendCommand("Input.dispatchMouseEvent", {
937
- type: "mouseReleased", x: cx, y: cy, button, clickCount: 1, pointerType: "mouse",
1018
+ type: "mouseReleased", x: rx, y: ry, button, clickCount: 1, pointerType: "mouse",
938
1019
  });
939
1020
  }
940
1021
  /**
941
- * Move mouse along a cubic Bezier arc with Fitts's Law duration and fractional Brownian motion tremors.
1022
+ * Move mouse to coordinates fast path. Single CDP dispatch, no Bezier/tremor interpolation.
942
1023
  */
943
- async moveMouse(x, y, targetWidth) {
944
- const targetX = Math.round(Number(x));
945
- const targetY = Math.round(Number(y));
946
- const start = this.mousePosition ?? { x: targetX, y: targetY };
947
- const dist = Math.hypot(targetX - start.x, targetY - start.y);
948
- if (dist < 2) {
949
- this.mousePosition = { x: targetX, y: targetY };
950
- return;
951
- }
952
- // Fitts's Law: MT = a + b * log2(2D / W)
953
- const w = targetWidth ?? 30; // default target width to 30px
954
- const indexDifficulty = Math.log2(Math.max(1, (2 * dist) / w));
955
- const movementTime = 150 + 95 * indexDifficulty; // Fitts's MT in ms
956
- // Human updates motor position every 10-15ms. Compute dynamic step count.
957
- const steps = Math.max(12, Math.min(60, Math.round(movementTime / 12)));
958
- // Random cubic Bezier control points — creates an organic arc
959
- const angle = Math.atan2(targetY - start.y, targetX - start.x) + Math.PI / 2;
960
- const spread = dist * (0.25 + Math.random() * 0.35);
961
- const sign = Math.random() < 0.5 ? 1 : -1;
962
- const cp1 = {
963
- x: start.x + (targetX - start.x) * (0.1 + Math.random() * 0.2) + Math.cos(angle) * spread * sign * (0.3 + Math.random() * 0.7),
964
- y: start.y + (targetY - start.y) * (0.1 + Math.random() * 0.2) + Math.sin(angle) * spread * sign * (0.3 + Math.random() * 0.7),
965
- };
966
- const cp2 = {
967
- x: start.x + (targetX - start.x) * (0.7 + Math.random() * 0.2) + Math.cos(angle) * spread * sign * (0.05 + Math.random() * 0.35),
968
- y: start.y + (targetY - start.y) * (0.7 + Math.random() * 0.2) + Math.sin(angle) * spread * sign * (0.05 + Math.random() * 0.35),
969
- };
970
- await this.updateMouseOverlay(start.x, start.y).catch(() => { });
971
- // Unique noise seed for this movement path
972
- const seedX = Math.random() * 100;
973
- const seedY = Math.random() * 100;
974
- for (let i = 1; i <= steps; i++) {
975
- const t = i / steps;
976
- // Ease-in-out: slow start, fast middle, slow near target
977
- const e = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
978
- const u = 1 - e;
979
- const px = u * u * u * start.x + 3 * u * u * e * cp1.x + 3 * u * e * e * cp2.x + e * e * e * targetX;
980
- const py = u * u * u * start.y + 3 * u * u * e * cp1.y + 3 * u * e * e * cp2.y + e * e * e * targetY;
981
- // fractional Brownian motion noise walk (muscle tremor)
982
- const tremorAmplitude = 1.2;
983
- const noiseX = this.fBm(t * 8 + seedX, 3) * tremorAmplitude;
984
- const noiseY = this.fBm(t * 8 + seedY, 3) * tremorAmplitude;
985
- const cx = Math.round(px + noiseX);
986
- const cy = Math.round(py + noiseY);
987
- await this.sendCommand("Input.dispatchMouseEvent", {
988
- type: "mouseMoved", x: cx, y: cy, button: "none", pointerType: "mouse",
989
- });
990
- await this.updateMouseOverlay(cx, cy).catch(() => { });
991
- // Velocity profile delay: faster in the middle, slower at start/end
992
- const velocityWeight = Math.sin(t * Math.PI); // bell curve 0 -> 1 -> 0
993
- const stepDelay = 2 + (1 - velocityWeight) * 12 + Math.random() * 4;
994
- await new Promise((r) => setTimeout(r, stepDelay));
995
- }
996
- this.mousePosition = { x: targetX, y: targetY };
1024
+ async moveMouse(x, y, _targetWidth) {
1025
+ const rx = Math.round(Number(x));
1026
+ const ry = Math.round(Number(y));
1027
+ this.mousePosition = { x: rx, y: ry };
1028
+ await this.sendCommand("Input.dispatchMouseEvent", {
1029
+ type: "mouseMoved", x: rx, y: ry, button: "none", pointerType: "mouse",
1030
+ });
997
1031
  }
998
1032
  getMousePosition() {
999
1033
  return this.mousePosition ?? { x: 300, y: 300 };
1000
1034
  }
1001
- async updateMouseOverlay(x, y) {
1002
- await this.sendCommand("Runtime.evaluate", {
1003
- expression: `
1004
- (function() {
1005
- const x = ${JSON.stringify(Math.round(x))};
1006
- const y = ${JSON.stringify(Math.round(y))};
1007
- let cursor = document.getElementById('__aether_mouse_cursor');
1008
- if (!cursor) {
1009
- cursor = document.createElement('div');
1010
- cursor.id = '__aether_mouse_cursor';
1011
- cursor.setAttribute('aria-hidden', 'true');
1012
- cursor.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style="display:block"><path d="M20.5056 10.7754C21.1225 10.5355 21.431 10.4155 21.5176 10.2459C21.5926 10.099 21.5903 9.92446 21.5115 9.77954C21.4205 9.61226 21.109 9.50044 20.486 9.2768L4.59629 3.5728C4.0866 3.38983 3.83175 3.29835 3.66514 3.35605C3.52029 3.40621 3.40645 3.52004 3.35629 3.6649C3.29859 3.8315 3.39008 4.08635 3.57304 4.59605L9.277 20.4858C9.50064 21.1088 9.61246 21.4203 9.77973 21.5113C9.92465 21.5901 10.0991 21.5924 10.2461 21.5174C10.4157 21.4308 10.5356 21.1223 10.7756 20.5054L13.3724 13.8278C13.4194 13.707 13.4429 13.6466 13.4792 13.5957C13.5114 13.5506 13.5508 13.5112 13.5959 13.479C13.6468 13.4427 13.7072 13.4192 13.828 13.3722L20.5056 10.7754Z" stroke="#111111" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M20.5056 10.7754C21.1225 10.5355 21.431 10.4155 21.5176 10.2459C21.5926 10.099 21.5903 9.92446 21.5115 9.77954C21.4205 9.61226 21.109 9.50044 20.486 9.2768L4.59629 3.5728C4.0866 3.38983 3.83175 3.29835 3.66514 3.35605C3.52029 3.40621 3.40645 3.52004 3.35629 3.6649C3.29859 3.8315 3.39008 4.08635 3.57304 4.59605L9.277 20.4858C9.50064 21.1088 9.61246 21.4203 9.77973 21.5113C9.92465 21.5901 10.0991 21.5924 10.2461 21.5174C10.4157 21.4308 10.5356 21.1223 10.7756 20.5054L13.3724 13.8278C13.4194 13.707 13.4429 13.6466 13.4792 13.5957C13.5114 13.5506 13.5508 13.5112 13.5959 13.479C13.6468 13.4427 13.7072 13.4192 13.828 13.3722L20.5056 10.7754Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
1013
- cursor.style.cssText = [
1014
- 'position: fixed',
1015
- 'left: 0',
1016
- 'top: 0',
1017
- 'transform: translate(-3px, -3px)',
1018
- 'transition: left 70ms linear, top 70ms linear, opacity 120ms ease',
1019
- 'z-index: 2147483647',
1020
- 'pointer-events: none',
1021
- 'opacity: 1'
1022
- ].join(';');
1023
- document.documentElement.appendChild(cursor);
1024
- }
1025
- cursor.style.left = x + 'px';
1026
- cursor.style.top = y + 'px';
1027
- cursor.style.opacity = '1';
1028
- clearTimeout(window.__aetherMouseCursorTimer);
1029
- window.__aetherMouseCursorTimer = setTimeout(() => {
1030
- const current = document.getElementById('__aether_mouse_cursor');
1031
- if (current) current.style.opacity = '0.55';
1032
- }, 900);
1033
- return true;
1034
- })()
1035
- `,
1036
- returnByValue: true,
1037
- });
1038
- }
1039
- async showScrollIndicator(x, y, deltaY) {
1040
- const isDown = deltaY > 0;
1041
- const chevron = (dy, opacity) => {
1042
- const d = isDown
1043
- ? `M10,${dy} L16,${dy + 7} L22,${dy}`
1044
- : `M10,${dy + 7} L16,${dy} L22,${dy + 7}`;
1045
- return `<path d="${d}" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="${opacity}"/>`;
1046
- };
1047
- const chevrons = isDown
1048
- ? chevron(4, 0.3) + chevron(15, 0.65) + chevron(26, 1)
1049
- : chevron(26, 0.3) + chevron(15, 0.65) + chevron(4, 1);
1050
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="36" style="display:block">${chevrons}</svg>`;
1051
- await this.sendCommand("Runtime.evaluate", {
1052
- expression: `
1053
- (function() {
1054
- var ind = document.getElementById('__aether_scroll_ind');
1055
- if (ind) ind.remove();
1056
- ind = document.createElement('div');
1057
- ind.id = '__aether_scroll_ind';
1058
- ind.innerHTML = ${JSON.stringify(svg)};
1059
- ind.style.cssText = [
1060
- 'position: fixed',
1061
- 'left: ${Math.round(x)}px',
1062
- 'top: ${Math.round(y)}px',
1063
- 'transform: translate(-50%, -50%)',
1064
- 'background: rgba(0,0,0,0.52)',
1065
- 'border-radius: 20px',
1066
- 'padding: 6px 8px',
1067
- 'z-index: 2147483647',
1068
- 'pointer-events: none',
1069
- 'opacity: 1',
1070
- 'transition: opacity 300ms ease'
1071
- ].join(';');
1072
- document.documentElement.appendChild(ind);
1073
- clearTimeout(window.__aetherScrollTimer);
1074
- window.__aetherScrollTimer = setTimeout(function() {
1075
- var cur = document.getElementById('__aether_scroll_ind');
1076
- if (cur) {
1077
- cur.style.opacity = '0';
1078
- setTimeout(function() { if (cur.parentNode) cur.parentNode.removeChild(cur); }, 320);
1079
- }
1080
- }, 500);
1081
- return true;
1082
- })()
1083
- `,
1084
- returnByValue: true,
1085
- }).catch(() => { });
1086
- }
1087
1035
  async moveMouseToSelector(selector) {
1088
- const result = await this.evaluate(`
1089
- (function() {
1090
- const el = document.querySelector(${JSON.stringify(selector)});
1091
- if (!el) return null;
1092
- el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
1093
- const r = el.getBoundingClientRect();
1094
- if (r.width === 0 || r.height === 0) return null;
1095
- return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
1096
- })()
1097
- `);
1098
- if (!result)
1036
+ try {
1037
+ const center = await this.getElementCenter(selector);
1038
+ if (!center)
1039
+ return false;
1040
+ await this.moveMouse(center.x, center.y);
1041
+ return true;
1042
+ }
1043
+ catch {
1099
1044
  return false;
1100
- await this.moveMouse(result.x, result.y);
1101
- return true;
1045
+ }
1102
1046
  }
1103
1047
  /**
1104
- * Scroll from the current or supplied pointer location using wheel events.
1048
+ * Scroll from the current or supplied pointer location fast path. Single wheel event.
1105
1049
  */
1106
1050
  async wheel(deltaX, deltaY, x, y) {
1107
- const origin = {
1108
- x: Number(x ?? this.mousePosition?.x ?? 0),
1109
- y: Number(y ?? this.mousePosition?.y ?? 0),
1110
- };
1111
- await this.moveMouse(origin.x, origin.y);
1112
- const dominant = Math.abs(Number(deltaY)) >= Math.abs(Number(deltaX)) ? Number(deltaY) : Number(deltaX);
1113
- if (dominant !== 0)
1114
- await this.showScrollIndicator(origin.x, origin.y, dominant);
1115
- // Break scroll into irregular chunks — humans don't scroll at perfectly uniform speed
1116
- const totalY = Number(deltaY);
1117
- const totalX = Number(deltaX);
1118
- const totalAbs = Math.max(Math.abs(totalX), Math.abs(totalY));
1119
- const steps = Math.max(1, Math.ceil(totalAbs / (300 + Math.random() * 400)));
1120
- let sentY = 0;
1121
- let sentX = 0;
1122
- for (let step = 0; step < steps; step++) {
1123
- const last = step === steps - 1;
1124
- // Random chunk size with slight ease-in (start slow, then momentum)
1125
- const fraction = last ? 1 : (0.5 + Math.random() * 0.5) / (steps - step);
1126
- const chunkY = last ? totalY - sentY : Math.round(totalY * fraction);
1127
- const chunkX = last ? totalX - sentX : Math.round(totalX * fraction);
1128
- sentY += chunkY;
1129
- sentX += chunkX;
1130
- await this.sendCommand("Input.dispatchMouseWheel", {
1131
- x: origin.x, y: origin.y, deltaX: chunkX, deltaY: chunkY,
1132
- });
1133
- if (!last) {
1134
- await new Promise((r) => setTimeout(r, 40 + Math.random() * 80));
1135
- }
1136
- }
1051
+ const ox = Math.round(Number(x ?? this.mousePosition?.x ?? 0));
1052
+ const oy = Math.round(Number(y ?? this.mousePosition?.y ?? 0));
1053
+ this.mousePosition = { x: ox, y: oy };
1054
+ await this.sendCommand("Input.dispatchMouseWheel", {
1055
+ x: ox, y: oy, deltaX: Math.round(Number(deltaX)), deltaY: Math.round(Number(deltaY)),
1056
+ });
1137
1057
  }
1138
1058
  async typeText(text) {
1139
- const ADJACENT_KEYS = {
1140
- 'a': 'qwsz',
1141
- 'b': 'vghn',
1142
- 'c': 'xdfv',
1143
- 'd': 'ersfxc',
1144
- 'e': 'wsdr',
1145
- 'f': 'rtgvcd',
1146
- 'g': 'tyhbvf',
1147
- 'h': 'yujnbg',
1148
- 'i': 'ujko',
1149
- 'j': 'uikmnh',
1150
- 'k': 'ijlm',
1151
- 'l': 'okp',
1152
- 'm': 'njk',
1153
- 'n': 'bhjm',
1154
- 'o': 'iklp',
1155
- 'p': 'ol',
1156
- 'q': 'wa',
1157
- 'r': 'edft',
1158
- 's': 'wedxza',
1159
- 't': 'rfgy',
1160
- 'u': 'yhji',
1161
- 'v': 'cfgb',
1162
- 'w': 'qase',
1163
- 'x': 'zsdc',
1164
- 'y': 'tghu',
1165
- 'z': 'asx',
1166
- };
1167
- for (let i = 0; i < text.length; i++) {
1168
- const ch = text[i];
1169
- const lowerCh = ch.toLowerCase();
1170
- // ~1.5% chance of typo on lowercase QWERTY letters
1171
- if (ADJACENT_KEYS[lowerCh] && Math.random() < 0.015) {
1172
- const adjList = ADJACENT_KEYS[lowerCh];
1173
- const typoCh = adjList[Math.floor(Math.random() * adjList.length)];
1174
- const resolvedTypo = ch === ch.toUpperCase() ? typoCh.toUpperCase() : typoCh;
1175
- // Type the typo first
1176
- await this.sendCommand("Input.insertText", { text: resolvedTypo });
1177
- // Natural pause for reaction time before correcting
1178
- await new Promise((r) => setTimeout(r, 120 + Math.random() * 150));
1179
- // Delete typo
1180
- await this.pressKey("Backspace");
1181
- // Short typing recovery pause
1182
- await new Promise((r) => setTimeout(r, 80 + Math.random() * 100));
1183
- }
1184
- await this.sendCommand("Input.insertText", { text: ch });
1185
- // Base inter-key delay (~55-90 WPM range)
1186
- let delay = 35 + Math.random() * 75;
1187
- // Longer pause after spaces (word boundary) and punctuation
1188
- if (ch === " ")
1189
- delay += 15 + Math.random() * 55;
1190
- if (/[.,!?;:\n]/.test(ch))
1191
- delay += 60 + Math.random() * 110;
1192
- // ~3% chance of a "thinking" pause mid-sentence
1193
- if (Math.random() < 0.03)
1194
- delay += 250 + Math.random() * 600;
1195
- await new Promise((r) => setTimeout(r, delay));
1059
+ // Fast path — insert entire text in one CDP call, no per-character delays or typo simulation.
1060
+ if (text) {
1061
+ await this.sendCommand("Input.insertText", { text });
1196
1062
  }
1197
1063
  }
1198
1064
  async pressKey(key, modifiers = []) {