aether-mcp-server 2.1.0 → 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.
@@ -14,6 +14,7 @@ const path_1 = __importDefault(require("path"));
14
14
  const os_1 = __importDefault(require("os"));
15
15
  const stealth_1 = require("./stealth");
16
16
  const element_collector_1 = require("./element-collector");
17
+ const eval_scripts_1 = require("./eval-scripts");
17
18
  class CdpClient {
18
19
  ws = null;
19
20
  messageId = 0;
@@ -32,7 +33,97 @@ class CdpClient {
32
33
  mousePosition = null;
33
34
  networkLoggingAttached = false;
34
35
  diagnosticsLoggingAttached = false;
36
+ speedMultiplier = 1.0;
37
+ documentNodeId = null;
35
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
+ }
36
127
  /**
37
128
  * Connect to existing Chrome instance on given port
38
129
  */
@@ -92,25 +183,22 @@ class CdpClient {
92
183
  this.ws.on("open", async () => {
93
184
  this.connected = true;
94
185
  this.activeTarget = target;
186
+ this.documentNodeId = null;
95
187
  console.error(`[CDP] Connected to target: ${target.title} (${target.url})`);
96
188
  try {
97
- // Enable core CDP domains
98
- await Promise.all([
99
- this.sendCommand("Page.enable"),
100
- this.sendCommand("Network.enable"),
101
- this.sendCommand("Runtime.enable"),
102
- this.sendCommand("DOM.enable"),
103
- this.sendCommand("Log.enable").catch(() => { }),
104
- this.sendCommand("Animation.enable").catch(() => { }),
105
- this.sendCommand("Page.addScriptToEvaluateOnNewDocument", {
106
- source: stealth_1.STEALTH_SCRIPT
107
- })
108
- ]);
109
- // Keep animations running. Pausing them can freeze SPA loaders and leave pages looking blank.
110
- 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(() => { });
111
200
  this.attachNetworkLogging();
112
201
  this.attachDiagnosticsLogging();
113
- console.error("[CDP] Core CDP domains enabled");
114
202
  }
115
203
  catch (e) {
116
204
  console.error("[CDP] Failed to enable core domains:", e);
@@ -176,6 +264,7 @@ class CdpClient {
176
264
  * Navigate to URL
177
265
  */
178
266
  async navigate(url) {
267
+ this.documentNodeId = null;
179
268
  await this.sendCommand("Page.navigate", { url });
180
269
  }
181
270
  async navigateAndWait(url, timeout = 10000) {
@@ -312,50 +401,28 @@ class CdpClient {
312
401
  });
313
402
  }
314
403
  /**
315
- * Wait for a selector to appear in DOM
404
+ * Wait for a selector to appear in DOM — fast CDP polling.
316
405
  */
317
406
  async waitForSelector(selector, timeout = 10000, options = {}) {
318
407
  const startTime = Date.now();
319
408
  let lastBox = null;
320
409
  let stableSince = 0;
321
410
  while (Date.now() - startTime < timeout) {
322
- const result = await this.sendCommand("Runtime.evaluate", {
323
- expression: `
324
- (function() {
325
- const el = document.querySelector(${JSON.stringify(selector)});
326
- if (!el) return { found: false };
327
- const rect = el.getBoundingClientRect();
328
- const computed = window.getComputedStyle(el);
329
- const visible = computed.display !== 'none' &&
330
- computed.visibility !== 'hidden' &&
331
- computed.opacity !== '0' &&
332
- rect.width > 0 &&
333
- rect.height > 0;
334
- return {
335
- found: true,
336
- visible,
337
- box: {
338
- x: Math.round(rect.left),
339
- y: Math.round(rect.top),
340
- width: Math.round(rect.width),
341
- height: Math.round(rect.height)
342
- }
343
- };
344
- })()
345
- `,
346
- returnByValue: true,
347
- });
348
- const state = result.result?.value;
349
- if (state?.found && (!options.visible || state.visible)) {
350
- if (!options.stable)
351
- return true;
352
- const box = state.box;
353
- const sameBox = lastBox &&
354
- lastBox.x === box.x &&
355
- lastBox.y === box.y &&
356
- lastBox.width === box.width &&
357
- lastBox.height === box.height;
358
- 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) {
359
426
  if (!stableSince)
360
427
  stableSince = Date.now();
361
428
  if (Date.now() - stableSince >= 120)
@@ -363,10 +430,13 @@ class CdpClient {
363
430
  }
364
431
  else {
365
432
  stableSince = 0;
366
- lastBox = box;
433
+ lastBox = boxKey;
367
434
  }
368
435
  }
369
- await new Promise(r => setTimeout(r, 75));
436
+ else {
437
+ return true;
438
+ }
439
+ await new Promise(r => setTimeout(r, 30));
370
440
  }
371
441
  return false;
372
442
  }
@@ -415,7 +485,7 @@ class CdpClient {
415
485
  this.removeEventListener("Network.responseReceived", resetIdle);
416
486
  resolve(); // Resolve anyway after timeout
417
487
  }, timeout);
418
- idleCheckInterval = setInterval(checkIdle, 100);
488
+ idleCheckInterval = setInterval(checkIdle, Math.max(1, 100 * this.speedMultiplier));
419
489
  this.on("Network.requestWillBeSent", resetIdle);
420
490
  this.on("Network.responseReceived", resetIdle);
421
491
  });
@@ -520,51 +590,84 @@ class CdpClient {
520
590
  if (!target) {
521
591
  throw new Error(`Target not found: ${targetId}`);
522
592
  }
593
+ this.documentNodeId = null;
523
594
  await this.attachToTarget(target);
524
595
  }
525
596
  /**
526
- * 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.
527
599
  */
528
- async getSimplifiedAccessibilityTree() {
600
+ async getRichAXTree() {
529
601
  await this.sendCommand("Accessibility.enable");
530
- const result = await this.sendCommand("Accessibility.getFullAXTree");
602
+ const result = await this.sendCommand("Accessibility.getFullAXTree", {});
531
603
  if (!result || !result.nodes)
532
- return [];
604
+ return { tree: null, nodeCount: 0 };
533
605
  const nodes = result.nodes;
534
- const interactiveNodes = [];
535
- // Map of node IDs for fast lookup
536
606
  const nodeMap = new Map();
537
- nodes.forEach((node) => nodeMap.set(node.nodeId, node));
538
- // Helper to get name from node properties
539
- const getNodeName = (node) => {
540
- if (node.name?.value)
541
- return node.name.value;
542
- const nameProp = node.properties?.find((p) => p.name === "name");
543
- return nameProp?.value?.value || "";
544
- };
545
- // Filter and simplify nodes
607
+ // First pass: build lookup
546
608
  nodes.forEach((node) => {
547
- const role = node.role?.value;
548
- const name = getNodeName(node);
549
- // Only include interactive or meaningful nodes
550
- const isInteractive = [
551
- "button", "link", "checkbox", "radio", "textbox", "searchbox",
552
- "combobox", "listbox", "menuitem", "slider", "switch", "tab"
553
- ].includes(role);
554
- const hasAction = node.properties?.some((p) => ["pressed", "expanded", "selected", "focused"].includes(p.name));
555
- if (isInteractive || (name && name.length > 0 && role !== "generic" && role !== "none")) {
556
- interactiveNodes.push({
557
- id: node.nodeId,
558
- role: role,
559
- name: name,
560
- description: node.description?.value || "",
561
- value: node.value?.value || "",
562
- disabled: node.properties?.find((p) => p.name === "disabled")?.value?.value || false,
563
- 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;
564
615
  });
565
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
+ });
632
+ });
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
+ }
566
645
  });
567
- return interactiveNodes;
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;
568
671
  }
569
672
  async getNetworkTraffic() {
570
673
  return this.networkTraffic;
@@ -901,297 +1004,61 @@ class CdpClient {
901
1004
  async takeHeapSnapshot(reportProgress, treatGlobalObjectsAsRoots, captureNumericValue) {
902
1005
  await this.sendCommand("HeapProfiler.takeHeapSnapshot", { reportProgress, treatGlobalObjectsAsRoots, captureNumericValue });
903
1006
  }
904
- // Simple multi-octave noise function (fractional Brownian motion approximation)
905
- fBm(t, octaves = 3) {
906
- let value = 0;
907
- let amplitude = 1.0;
908
- let frequency = 1.0;
909
- let maxValue = 0;
910
- for (let j = 0; j < octaves; j++) {
911
- value += Math.sin(t * frequency * Math.PI * 2 + (j * 12.34)) * amplitude;
912
- maxValue += amplitude;
913
- amplitude *= 0.5;
914
- frequency *= 2.0;
915
- }
916
- return value / maxValue;
917
- }
918
1007
  /**
919
- * 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.
920
1009
  */
921
- async click(x, y, button = "left", targetWidth) {
922
- const targetX = Math.round(Number(x));
923
- const targetY = Math.round(Number(y));
924
- await this.moveMouse(targetX, targetY, targetWidth);
925
- // Pre-click hover pause — humans don't instantly press after arriving
926
- await new Promise((r) => setTimeout(r, 80 + Math.random() * 140));
927
- // Micro-jitter at the moment of click (hand tremor)
928
- const cx = targetX + Math.round((Math.random() - 0.5) * 3);
929
- 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 };
930
1014
  await this.sendCommand("Input.dispatchMouseEvent", {
931
- type: "mousePressed", x: cx, y: cy, button, clickCount: 1, pointerType: "mouse",
1015
+ type: "mousePressed", x: rx, y: ry, button, clickCount: 1, pointerType: "mouse",
932
1016
  });
933
- // Natural hold duration before release
934
- await new Promise((r) => setTimeout(r, 60 + Math.random() * 110));
935
1017
  await this.sendCommand("Input.dispatchMouseEvent", {
936
- type: "mouseReleased", x: cx, y: cy, button, clickCount: 1, pointerType: "mouse",
1018
+ type: "mouseReleased", x: rx, y: ry, button, clickCount: 1, pointerType: "mouse",
937
1019
  });
938
1020
  }
939
1021
  /**
940
- * 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.
941
1023
  */
942
- async moveMouse(x, y, targetWidth) {
943
- const targetX = Math.round(Number(x));
944
- const targetY = Math.round(Number(y));
945
- const start = this.mousePosition ?? { x: targetX, y: targetY };
946
- const dist = Math.hypot(targetX - start.x, targetY - start.y);
947
- if (dist < 2) {
948
- this.mousePosition = { x: targetX, y: targetY };
949
- return;
950
- }
951
- // Fitts's Law: MT = a + b * log2(2D / W)
952
- const w = targetWidth ?? 30; // default target width to 30px
953
- const indexDifficulty = Math.log2(Math.max(1, (2 * dist) / w));
954
- const movementTime = 150 + 95 * indexDifficulty; // Fitts's MT in ms
955
- // Human updates motor position every 10-15ms. Compute dynamic step count.
956
- const steps = Math.max(12, Math.min(60, Math.round(movementTime / 12)));
957
- // Random cubic Bezier control points — creates an organic arc
958
- const angle = Math.atan2(targetY - start.y, targetX - start.x) + Math.PI / 2;
959
- const spread = dist * (0.25 + Math.random() * 0.35);
960
- const sign = Math.random() < 0.5 ? 1 : -1;
961
- const cp1 = {
962
- x: start.x + (targetX - start.x) * (0.1 + Math.random() * 0.2) + Math.cos(angle) * spread * sign * (0.3 + Math.random() * 0.7),
963
- y: start.y + (targetY - start.y) * (0.1 + Math.random() * 0.2) + Math.sin(angle) * spread * sign * (0.3 + Math.random() * 0.7),
964
- };
965
- const cp2 = {
966
- x: start.x + (targetX - start.x) * (0.7 + Math.random() * 0.2) + Math.cos(angle) * spread * sign * (0.05 + Math.random() * 0.35),
967
- y: start.y + (targetY - start.y) * (0.7 + Math.random() * 0.2) + Math.sin(angle) * spread * sign * (0.05 + Math.random() * 0.35),
968
- };
969
- await this.updateMouseOverlay(start.x, start.y).catch(() => { });
970
- // Unique noise seed for this movement path
971
- const seedX = Math.random() * 100;
972
- const seedY = Math.random() * 100;
973
- for (let i = 1; i <= steps; i++) {
974
- const t = i / steps;
975
- // Ease-in-out: slow start, fast middle, slow near target
976
- const e = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
977
- const u = 1 - e;
978
- const px = u * u * u * start.x + 3 * u * u * e * cp1.x + 3 * u * e * e * cp2.x + e * e * e * targetX;
979
- const py = u * u * u * start.y + 3 * u * u * e * cp1.y + 3 * u * e * e * cp2.y + e * e * e * targetY;
980
- // fractional Brownian motion noise walk (muscle tremor)
981
- const tremorAmplitude = 1.2;
982
- const noiseX = this.fBm(t * 8 + seedX, 3) * tremorAmplitude;
983
- const noiseY = this.fBm(t * 8 + seedY, 3) * tremorAmplitude;
984
- const cx = Math.round(px + noiseX);
985
- const cy = Math.round(py + noiseY);
986
- await this.sendCommand("Input.dispatchMouseEvent", {
987
- type: "mouseMoved", x: cx, y: cy, button: "none", pointerType: "mouse",
988
- });
989
- await this.updateMouseOverlay(cx, cy).catch(() => { });
990
- // Velocity profile delay: faster in the middle, slower at start/end
991
- const velocityWeight = Math.sin(t * Math.PI); // bell curve 0 -> 1 -> 0
992
- const stepDelay = 2 + (1 - velocityWeight) * 12 + Math.random() * 4;
993
- await new Promise((r) => setTimeout(r, stepDelay));
994
- }
995
- 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
+ });
996
1031
  }
997
1032
  getMousePosition() {
998
1033
  return this.mousePosition ?? { x: 300, y: 300 };
999
1034
  }
1000
- async updateMouseOverlay(x, y) {
1001
- await this.sendCommand("Runtime.evaluate", {
1002
- expression: `
1003
- (function() {
1004
- const x = ${JSON.stringify(Math.round(x))};
1005
- const y = ${JSON.stringify(Math.round(y))};
1006
- let cursor = document.getElementById('__aether_mouse_cursor');
1007
- if (!cursor) {
1008
- cursor = document.createElement('div');
1009
- cursor.id = '__aether_mouse_cursor';
1010
- cursor.setAttribute('aria-hidden', 'true');
1011
- 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>';
1012
- cursor.style.cssText = [
1013
- 'position: fixed',
1014
- 'left: 0',
1015
- 'top: 0',
1016
- 'transform: translate(-3px, -3px)',
1017
- 'transition: left 70ms linear, top 70ms linear, opacity 120ms ease',
1018
- 'z-index: 2147483647',
1019
- 'pointer-events: none',
1020
- 'opacity: 1'
1021
- ].join(';');
1022
- document.documentElement.appendChild(cursor);
1023
- }
1024
- cursor.style.left = x + 'px';
1025
- cursor.style.top = y + 'px';
1026
- cursor.style.opacity = '1';
1027
- clearTimeout(window.__aetherMouseCursorTimer);
1028
- window.__aetherMouseCursorTimer = setTimeout(() => {
1029
- const current = document.getElementById('__aether_mouse_cursor');
1030
- if (current) current.style.opacity = '0.55';
1031
- }, 900);
1032
- return true;
1033
- })()
1034
- `,
1035
- returnByValue: true,
1036
- });
1037
- }
1038
- async showScrollIndicator(x, y, deltaY) {
1039
- const isDown = deltaY > 0;
1040
- const chevron = (dy, opacity) => {
1041
- const d = isDown
1042
- ? `M10,${dy} L16,${dy + 7} L22,${dy}`
1043
- : `M10,${dy + 7} L16,${dy} L22,${dy + 7}`;
1044
- return `<path d="${d}" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="${opacity}"/>`;
1045
- };
1046
- const chevrons = isDown
1047
- ? chevron(4, 0.3) + chevron(15, 0.65) + chevron(26, 1)
1048
- : chevron(26, 0.3) + chevron(15, 0.65) + chevron(4, 1);
1049
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="36" style="display:block">${chevrons}</svg>`;
1050
- await this.sendCommand("Runtime.evaluate", {
1051
- expression: `
1052
- (function() {
1053
- var ind = document.getElementById('__aether_scroll_ind');
1054
- if (ind) ind.remove();
1055
- ind = document.createElement('div');
1056
- ind.id = '__aether_scroll_ind';
1057
- ind.innerHTML = ${JSON.stringify(svg)};
1058
- ind.style.cssText = [
1059
- 'position: fixed',
1060
- 'left: ${Math.round(x)}px',
1061
- 'top: ${Math.round(y)}px',
1062
- 'transform: translate(-50%, -50%)',
1063
- 'background: rgba(0,0,0,0.52)',
1064
- 'border-radius: 20px',
1065
- 'padding: 6px 8px',
1066
- 'z-index: 2147483647',
1067
- 'pointer-events: none',
1068
- 'opacity: 1',
1069
- 'transition: opacity 300ms ease'
1070
- ].join(';');
1071
- document.documentElement.appendChild(ind);
1072
- clearTimeout(window.__aetherScrollTimer);
1073
- window.__aetherScrollTimer = setTimeout(function() {
1074
- var cur = document.getElementById('__aether_scroll_ind');
1075
- if (cur) {
1076
- cur.style.opacity = '0';
1077
- setTimeout(function() { if (cur.parentNode) cur.parentNode.removeChild(cur); }, 320);
1078
- }
1079
- }, 500);
1080
- return true;
1081
- })()
1082
- `,
1083
- returnByValue: true,
1084
- }).catch(() => { });
1085
- }
1086
1035
  async moveMouseToSelector(selector) {
1087
- const result = await this.evaluate(`
1088
- (function() {
1089
- const el = document.querySelector(${JSON.stringify(selector)});
1090
- if (!el) return null;
1091
- el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
1092
- const r = el.getBoundingClientRect();
1093
- if (r.width === 0 || r.height === 0) return null;
1094
- return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
1095
- })()
1096
- `);
1097
- 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 {
1098
1044
  return false;
1099
- await this.moveMouse(result.x, result.y);
1100
- return true;
1045
+ }
1101
1046
  }
1102
1047
  /**
1103
- * 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.
1104
1049
  */
1105
1050
  async wheel(deltaX, deltaY, x, y) {
1106
- const origin = {
1107
- x: Number(x ?? this.mousePosition?.x ?? 0),
1108
- y: Number(y ?? this.mousePosition?.y ?? 0),
1109
- };
1110
- await this.moveMouse(origin.x, origin.y);
1111
- const dominant = Math.abs(Number(deltaY)) >= Math.abs(Number(deltaX)) ? Number(deltaY) : Number(deltaX);
1112
- if (dominant !== 0)
1113
- await this.showScrollIndicator(origin.x, origin.y, dominant);
1114
- // Break scroll into irregular chunks — humans don't scroll at perfectly uniform speed
1115
- const totalY = Number(deltaY);
1116
- const totalX = Number(deltaX);
1117
- const totalAbs = Math.max(Math.abs(totalX), Math.abs(totalY));
1118
- const steps = Math.max(1, Math.ceil(totalAbs / (300 + Math.random() * 400)));
1119
- let sentY = 0;
1120
- let sentX = 0;
1121
- for (let step = 0; step < steps; step++) {
1122
- const last = step === steps - 1;
1123
- // Random chunk size with slight ease-in (start slow, then momentum)
1124
- const fraction = last ? 1 : (0.5 + Math.random() * 0.5) / (steps - step);
1125
- const chunkY = last ? totalY - sentY : Math.round(totalY * fraction);
1126
- const chunkX = last ? totalX - sentX : Math.round(totalX * fraction);
1127
- sentY += chunkY;
1128
- sentX += chunkX;
1129
- await this.sendCommand("Input.dispatchMouseWheel", {
1130
- x: origin.x, y: origin.y, deltaX: chunkX, deltaY: chunkY,
1131
- });
1132
- if (!last) {
1133
- await new Promise((r) => setTimeout(r, 40 + Math.random() * 80));
1134
- }
1135
- }
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
+ });
1136
1057
  }
1137
1058
  async typeText(text) {
1138
- const ADJACENT_KEYS = {
1139
- 'a': 'qwsz',
1140
- 'b': 'vghn',
1141
- 'c': 'xdfv',
1142
- 'd': 'ersfxc',
1143
- 'e': 'wsdr',
1144
- 'f': 'rtgvcd',
1145
- 'g': 'tyhbvf',
1146
- 'h': 'yujnbg',
1147
- 'i': 'ujko',
1148
- 'j': 'uikmnh',
1149
- 'k': 'ijlm',
1150
- 'l': 'okp',
1151
- 'm': 'njk',
1152
- 'n': 'bhjm',
1153
- 'o': 'iklp',
1154
- 'p': 'ol',
1155
- 'q': 'wa',
1156
- 'r': 'edft',
1157
- 's': 'wedxza',
1158
- 't': 'rfgy',
1159
- 'u': 'yhji',
1160
- 'v': 'cfgb',
1161
- 'w': 'qase',
1162
- 'x': 'zsdc',
1163
- 'y': 'tghu',
1164
- 'z': 'asx',
1165
- };
1166
- for (let i = 0; i < text.length; i++) {
1167
- const ch = text[i];
1168
- const lowerCh = ch.toLowerCase();
1169
- // ~1.5% chance of typo on lowercase QWERTY letters
1170
- if (ADJACENT_KEYS[lowerCh] && Math.random() < 0.015) {
1171
- const adjList = ADJACENT_KEYS[lowerCh];
1172
- const typoCh = adjList[Math.floor(Math.random() * adjList.length)];
1173
- const resolvedTypo = ch === ch.toUpperCase() ? typoCh.toUpperCase() : typoCh;
1174
- // Type the typo first
1175
- await this.sendCommand("Input.insertText", { text: resolvedTypo });
1176
- // Natural pause for reaction time before correcting
1177
- await new Promise((r) => setTimeout(r, 120 + Math.random() * 150));
1178
- // Delete typo
1179
- await this.pressKey("Backspace");
1180
- // Short typing recovery pause
1181
- await new Promise((r) => setTimeout(r, 80 + Math.random() * 100));
1182
- }
1183
- await this.sendCommand("Input.insertText", { text: ch });
1184
- // Base inter-key delay (~55-90 WPM range)
1185
- let delay = 35 + Math.random() * 75;
1186
- // Longer pause after spaces (word boundary) and punctuation
1187
- if (ch === " ")
1188
- delay += 15 + Math.random() * 55;
1189
- if (/[.,!?;:\n]/.test(ch))
1190
- delay += 60 + Math.random() * 110;
1191
- // ~3% chance of a "thinking" pause mid-sentence
1192
- if (Math.random() < 0.03)
1193
- delay += 250 + Math.random() * 600;
1194
- 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 });
1195
1062
  }
1196
1063
  }
1197
1064
  async pressKey(key, modifiers = []) {