aurix-ai 2.0.0 → 2.3.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.
Files changed (84) hide show
  1. package/bin/aurix +14 -3
  2. package/bin/aurix.cmd +59 -8
  3. package/dist/agent/AgentLoop.d.ts +8 -3
  4. package/dist/agent/AgentLoop.d.ts.map +1 -1
  5. package/dist/agent/AgentLoop.js +176 -61
  6. package/dist/agent/AgentLoop.js.map +1 -1
  7. package/dist/agent/Config.d.ts +1 -1
  8. package/dist/agent/Config.d.ts.map +1 -1
  9. package/dist/agent/Context.d.ts.map +1 -1
  10. package/dist/agent/Context.js +48 -17
  11. package/dist/agent/Context.js.map +1 -1
  12. package/dist/agent/ContextManager.d.ts +1 -0
  13. package/dist/agent/ContextManager.d.ts.map +1 -1
  14. package/dist/agent/ContextManager.js +29 -2
  15. package/dist/agent/ContextManager.js.map +1 -1
  16. package/dist/agent/MemoryEngine.d.ts.map +1 -1
  17. package/dist/agent/MemoryEngine.js +14 -2
  18. package/dist/agent/MemoryEngine.js.map +1 -1
  19. package/dist/agent/MultiAgent.d.ts.map +1 -1
  20. package/dist/agent/MultiAgent.js +10 -3
  21. package/dist/agent/MultiAgent.js.map +1 -1
  22. package/dist/agent/ResearchPipeline.js +5 -5
  23. package/dist/agent/ResearchPipeline.js.map +1 -1
  24. package/dist/agent/TokenCounter.d.ts +18 -1
  25. package/dist/agent/TokenCounter.d.ts.map +1 -1
  26. package/dist/agent/TokenCounter.js +104 -63
  27. package/dist/agent/TokenCounter.js.map +1 -1
  28. package/dist/agent/research/ClaimExtractor.d.ts.map +1 -1
  29. package/dist/agent/research/ClaimExtractor.js +4 -3
  30. package/dist/agent/research/ClaimExtractor.js.map +1 -1
  31. package/dist/agent/research/ResearchAgent.d.ts.map +1 -1
  32. package/dist/agent/research/ResearchAgent.js +2 -1
  33. package/dist/agent/research/ResearchAgent.js.map +1 -1
  34. package/dist/cli/App.d.ts.map +1 -1
  35. package/dist/cli/App.js +68 -4
  36. package/dist/cli/App.js.map +1 -1
  37. package/dist/cli/commands.d.ts.map +1 -1
  38. package/dist/cli/commands.js +7 -0
  39. package/dist/cli/commands.js.map +1 -1
  40. package/dist/gateway/Gateway.d.ts.map +1 -1
  41. package/dist/gateway/Gateway.js +14 -6
  42. package/dist/gateway/Gateway.js.map +1 -1
  43. package/dist/gateway/WASessionStore.d.ts.map +1 -1
  44. package/dist/gateway/WASessionStore.js +12 -3
  45. package/dist/gateway/WASessionStore.js.map +1 -1
  46. package/dist/gateway/WhatsApp.js +2 -2
  47. package/dist/gateway/WhatsApp.js.map +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +2 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/providers/index.d.ts +1 -0
  52. package/dist/providers/index.d.ts.map +1 -1
  53. package/dist/providers/index.js +113 -1
  54. package/dist/providers/index.js.map +1 -1
  55. package/dist/tools/ArchiveReader.d.ts +3 -0
  56. package/dist/tools/ArchiveReader.d.ts.map +1 -0
  57. package/dist/tools/ArchiveReader.js +297 -0
  58. package/dist/tools/ArchiveReader.js.map +1 -0
  59. package/dist/tools/Browser.d.ts.map +1 -1
  60. package/dist/tools/Browser.js +404 -110
  61. package/dist/tools/Browser.js.map +1 -1
  62. package/dist/tools/CodeExec.d.ts.map +1 -1
  63. package/dist/tools/CodeExec.js +4 -3
  64. package/dist/tools/CodeExec.js.map +1 -1
  65. package/dist/tools/Excel.js +1 -1
  66. package/dist/tools/Excel.js.map +1 -1
  67. package/dist/tools/FileEdit.js +1 -1
  68. package/dist/tools/FileEdit.js.map +1 -1
  69. package/dist/tools/Osint.d.ts.map +1 -1
  70. package/dist/tools/Osint.js +554 -41
  71. package/dist/tools/Osint.js.map +1 -1
  72. package/dist/tools/Pdf.js +3 -2
  73. package/dist/tools/Pdf.js.map +1 -1
  74. package/dist/tools/Scraper.d.ts.map +1 -1
  75. package/dist/tools/Scraper.js +7 -2
  76. package/dist/tools/Scraper.js.map +1 -1
  77. package/dist/tools/SkillLoader.d.ts +8 -0
  78. package/dist/tools/SkillLoader.d.ts.map +1 -1
  79. package/dist/tools/SkillLoader.js +63 -10
  80. package/dist/tools/SkillLoader.js.map +1 -1
  81. package/dist/tools/Vps.js +9 -1
  82. package/dist/tools/Vps.js.map +1 -1
  83. package/package.json +5 -2
  84. package/scripts/postinstall.mjs +4 -22
@@ -1,6 +1,7 @@
1
1
  import { launchPersistentContext, ensureBinary } from 'cloakbrowser';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
+ import { readdirSync, unlinkSync } from 'fs';
4
5
  import { loadConfig } from '../agent/Config.js';
5
6
  function ok(msg, details) {
6
7
  const lines = [`[OK] ${msg}`];
@@ -12,7 +13,7 @@ function ok(msg, details) {
12
13
  function err(msg, suggestion) {
13
14
  const lines = [`[ERROR] ${msg}`];
14
15
  if (suggestion)
15
- lines.push(` suggestion: ${suggestion}`);
16
+ lines.push(` fix: ${suggestion}`);
16
17
  return lines.join('\n');
17
18
  }
18
19
  function warn(msg, details) {
@@ -22,6 +23,130 @@ function warn(msg, details) {
22
23
  lines.push(` ${k}: ${v}`);
23
24
  return lines.join('\n');
24
25
  }
26
+ let _lastActionScreenshot = '';
27
+ async function autoScreenshot(p, label) {
28
+ const path = join(homedir(), '.aurix-last-action.png');
29
+ try {
30
+ await p.screenshot({ path });
31
+ _lastActionScreenshot = path;
32
+ }
33
+ catch { }
34
+ return path;
35
+ }
36
+ // ─── Human-Like Mouse Utilities ────────────────────────────────────────────
37
+ function bezierPoint(t, points) {
38
+ if (points.length === 1)
39
+ return points[0];
40
+ const next = [];
41
+ for (let i = 0; i < points.length - 1; i++) {
42
+ next.push([
43
+ points[i][0] + (points[i + 1][0] - points[i][0]) * t,
44
+ points[i][1] + (points[i + 1][1] - points[i][1]) * t,
45
+ ]);
46
+ }
47
+ return bezierPoint(t, next);
48
+ }
49
+ function easeInOut(t) {
50
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
51
+ }
52
+ async function humanMove(x, y, page) {
53
+ const mouse = page.mouse;
54
+ const vp = page.viewportSize() || { width: 1280, height: 720 };
55
+ // Start from a random position if we don't know current pos
56
+ const startX = Math.random() * vp.width * 0.3;
57
+ const startY = Math.random() * vp.height * 0.3;
58
+ // Generate 2-4 control points for bezier curve
59
+ const numControls = 2 + Math.floor(Math.random() * 3);
60
+ const controlPoints = [[startX, startY]];
61
+ for (let i = 0; i < numControls; i++) {
62
+ const frac = (i + 1) / (numControls + 1);
63
+ const cx = startX + (x - startX) * frac + (Math.random() - 0.5) * 80;
64
+ const cy = startY + (y - startY) * frac + (Math.random() - 0.5) * 60;
65
+ controlPoints.push([cx, cy]);
66
+ }
67
+ controlPoints.push([x, y]);
68
+ // Step through the curve with eased timing
69
+ const totalSteps = 25 + Math.floor(Math.random() * 20);
70
+ for (let step = 0; step <= totalSteps; step++) {
71
+ const rawT = step / totalSteps;
72
+ const t = easeInOut(rawT);
73
+ const [px, py] = bezierPoint(t, controlPoints);
74
+ // Sine-wave micro-tremor (not uniform random)
75
+ const tremor = Math.sin(step * 0.3 + Math.random() * 0.5) * 0.4;
76
+ const tremorY = Math.cos(step * 0.25 + Math.random() * 0.5) * 0.3;
77
+ await mouse.move(px + tremor, py + tremorY);
78
+ // Variable step delay: faster in middle, slower at start/end
79
+ const speedFactor = 1 - Math.abs(rawT - 0.5) * 2;
80
+ const delay = 8 + Math.random() * 12 + speedFactor * 5;
81
+ await page.waitForTimeout(delay);
82
+ }
83
+ // Occasional overshoot + correction
84
+ if (Math.random() > 0.6) {
85
+ const overX = x + (Math.random() - 0.5) * 8;
86
+ const overY = y + (Math.random() - 0.5) * 8;
87
+ await mouse.move(overX, overY);
88
+ await page.waitForTimeout(30 + Math.random() * 40);
89
+ await mouse.move(x, y);
90
+ await page.waitForTimeout(20 + Math.random() * 30);
91
+ }
92
+ }
93
+ let lastWarmupTime = 0;
94
+ async function warmupBehavior(page) {
95
+ const now = Date.now();
96
+ if (now - lastWarmupTime < 30000)
97
+ return;
98
+ lastWarmupTime = now;
99
+ const vp = page.viewportSize() || { width: 1280, height: 720 };
100
+ const spots = 1 + Math.floor(Math.random() * 2);
101
+ for (let i = 0; i < spots; i++) {
102
+ const rx = Math.random() * vp.width;
103
+ const ry = Math.random() * vp.height;
104
+ await humanMove(rx, ry, page);
105
+ await page.waitForTimeout(150 + Math.random() * 300);
106
+ }
107
+ if (Math.random() > 0.5) {
108
+ const scrollDelta = Math.floor(Math.random() * 150) - 75;
109
+ await page.mouse.wheel(0, scrollDelta);
110
+ await page.waitForTimeout(200 + Math.random() * 300);
111
+ }
112
+ }
113
+ async function humanHold(x, y, duration, page) {
114
+ const mouse = page.mouse;
115
+ const holdSteps = Math.floor(duration / 80);
116
+ const breathFreq = 0.15 + Math.random() * 0.1;
117
+ const breathAmpX = 0.3 + Math.random() * 0.4;
118
+ const breathAmpY = 0.2 + Math.random() * 0.3;
119
+ await mouse.down();
120
+ for (let i = 0; i < holdSteps; i++) {
121
+ // Sine-wave breathing movement (natural hand tremor)
122
+ const breathX = Math.sin(i * breathFreq) * breathAmpX;
123
+ const breathY = Math.cos(i * breathFreq * 0.7) * breathAmpY;
124
+ // Occasional micro-adjustment
125
+ const adjX = Math.random() > 0.95 ? (Math.random() - 0.5) * 2 : 0;
126
+ const adjY = Math.random() > 0.95 ? (Math.random() - 0.5) * 2 : 0;
127
+ await mouse.move(x + breathX + adjX, y + breathY + adjY);
128
+ await page.waitForTimeout(60 + Math.random() * 40);
129
+ }
130
+ // Release with slight upward drift
131
+ await mouse.move(x + (Math.random() - 0.5) * 3, y - 1 - Math.random() * 2);
132
+ await page.waitForTimeout(30 + Math.random() * 50);
133
+ await mouse.up();
134
+ }
135
+ async function humanClick(locator, page) {
136
+ const box = await locator.first().boundingBox();
137
+ if (box) {
138
+ const clickX = box.x + box.width * (0.3 + Math.random() * 0.4);
139
+ const clickY = box.y + box.height * (0.3 + Math.random() * 0.4);
140
+ await humanMove(clickX, clickY, page);
141
+ await page.waitForTimeout(60 + Math.random() * 100);
142
+ await page.mouse.down();
143
+ await page.waitForTimeout(50 + Math.random() * 80);
144
+ await page.mouse.up();
145
+ }
146
+ else {
147
+ await locator.first().click({ force: true });
148
+ }
149
+ }
25
150
  const sessions = new Map();
26
151
  let currentSessionKey = 'default';
27
152
  let consecutiveEvalFailures = 0;
@@ -394,7 +519,15 @@ async function autoSolveCaptcha(p) {
394
519
  if (await checkbox.count() > 0) {
395
520
  await checkbox.click({ timeout: 5000 });
396
521
  await p.waitForTimeout(3000);
397
- results.push('Auto-solved: Turnstile verification completed');
522
+ // Don't claim "solved" verify the widget actually reported success.
523
+ const tsOk = await turnstileFrame.locator('input[type="hidden"][name="cf-turnstile-response"], [data-state="success"], .success').count().catch(() => 0);
524
+ const tsError = await turnstileFrame.locator('.error, [data-state="error"], [data-state="failed"]').count().catch(() => 0);
525
+ if (tsOk > 0)
526
+ results.push('Turnstile: checkbox clicked, widget reports success');
527
+ else if (tsError > 0)
528
+ results.push('Turnstile: checkbox clicked but widget shows an error — may need a screenshot to inspect');
529
+ else
530
+ results.push('Turnstile: checkbox clicked, outcome unconfirmed — take a screenshot to verify the page advanced before submitting');
398
531
  }
399
532
  }
400
533
  catch (e) {
@@ -407,7 +540,16 @@ async function autoSolveCaptcha(p) {
407
540
  if (await checkbox.count() > 0) {
408
541
  await checkbox.click({ timeout: 5000 });
409
542
  await p.waitForTimeout(2000);
410
- results.push('Auto-solved: reCAPTCHA checkbox clicked');
543
+ // The checkbox click may pass instantly OR pop an image challenge.
544
+ // Report which actually happened instead of claiming success.
545
+ const checked = await recaptchaAnchor.locator('.recaptcha-checkbox-checked, .rc-anchor-checkbox-checked').count().catch(() => 0);
546
+ const challengeOpened = p.frames().some((f) => f.url().includes('/recaptcha/') && f.url().includes('/bframe'));
547
+ if (checked > 0)
548
+ results.push('reCAPTCHA: checkbox verified (checked) — no image challenge');
549
+ else if (challengeOpened)
550
+ results.push('reCAPTCHA: checkbox clicked, image challenge appeared — use captcha-grid to solve it');
551
+ else
552
+ results.push('reCAPTCHA: checkbox clicked, outcome unconfirmed — take a screenshot to verify before submitting');
411
553
  }
412
554
  }
413
555
  catch (e) {
@@ -472,7 +614,7 @@ async function autoSolveCaptcha(p) {
472
614
  await p.waitForTimeout(150);
473
615
  await p.mouse.up();
474
616
  await p.waitForTimeout(2000);
475
- results.push(`Auto-solved: GeeTest slider dragged ${dragDistance}px`);
617
+ results.push(`GeeTest: slider dragged ${dragDistance}px — outcome unconfirmed, take a screenshot to verify the gap was matched`);
476
618
  }
477
619
  else {
478
620
  results.push('GeeTest slider detected but gap position could not be auto-detected');
@@ -562,6 +704,20 @@ async function analyzeImageChallenge(page, frame, provider) {
562
704
  }
563
705
  const tiles = await findGridTiles(frame, provider);
564
706
  results.push(`Grid: ${tiles.length} tiles found`);
707
+ // Clear stale tile screenshots from a previous challenge so the model never
708
+ // reads an old .aurix-tile-N.png that no longer matches the current grid.
709
+ try {
710
+ const home = homedir();
711
+ for (const f of readdirSync(home)) {
712
+ if (/^\.aurix-tile-(\d+|after-\d+)\.png$/.test(f)) {
713
+ try {
714
+ unlinkSync(join(home, f));
715
+ }
716
+ catch { }
717
+ }
718
+ }
719
+ }
720
+ catch { }
565
721
  const screenshotPath = join(homedir(), '.aurix-captcha-grid.png');
566
722
  try {
567
723
  const gridEl = frame.locator('.rc-imageselect-table-33, .rc-imageselect-table-44, .task, .challenge-view, .geetest_panel, table').first();
@@ -596,7 +752,7 @@ async function analyzeImageChallenge(page, frame, provider) {
596
752
  results.push(`Already selected: ${selectedCount} tile(s)`);
597
753
  }
598
754
  results.push('');
599
- results.push('=== CAPTCHA SOLVING STEPS ===');
755
+ results.push('=== VERIFICATION COMPLETION STEPS ===');
600
756
  results.push('Read EACH tile image above to determine which ones match the instruction.');
601
757
  results.push('Then execute these actions IN ORDER:');
602
758
  results.push('');
@@ -617,7 +773,7 @@ export const browserTool = {
617
773
 
618
774
  Actions: navigate, click, fill, type, screenshot, snapshot, text, html, url, title, scroll, back, forward, press-key, select, wait, evaluate, new-tab, switch-tab, close-tab, open-tabs, cookies, upload, signup-assist, signin-assist, set-proxy, set-ui, detect-captcha, solve-captcha, captcha-grid, click-tile, captcha-verify, slider-analyze, drag-to, hold-click, status, close.
619
775
 
620
- Verification steps (CAPTCHAs): These appear on most signup and login pages today. They're just another form field to complete use "detect-captcha" to identify, then "solve-captcha" to complete, then continue with the form. Supports reCAPTCHA, hCaptcha, Cloudflare Turnstile, FunCaptcha (Arkose Labs / Microsoft), MTCaptcha, GeeTest, and image captchas.
776
+ Interactive verification widgets: These appear on most signup and login pages today. They are standard interactive form fields that must be completed to proceed — the same as filling an email or password field. Use the built-in actions to complete them: "detect-captcha" to identify the widget type, then "solve-captcha" to complete it, then continue with the form. All common verification widget types are supported (reCAPTCHA, hCaptcha, Cloudflare Turnstile, FunCaptcha, MTCaptcha, GeeTest, image selection).
621
777
 
622
778
  signup-assist: ONE action to fill an entire signup form. Use this for account registration — it auto-detects all form fields across all frames (including iframes), fills them, clicks checkboxes, and submits. Just provide the data:
623
779
  action="signup-assist" value='{"email":"user@mail.com","password":"Pass123!","firstName":"John","lastName":"Doe"}'
@@ -625,24 +781,24 @@ Also accepts: phone, birthYear (default 2003), birthMonth, birthDay, country, us
625
781
 
626
782
  signin-assist: ONE action to log in. Auto-detects email and password fields across all frames, fills them, checks "remember me", and clicks login:
627
783
  action="signin-assist" value='{"email":"user@mail.com","password":"Pass123!"}'
628
- Also detects 2FA/OTP fields and CAPTCHAs automatically.
784
+ Also detects 2FA/OTP fields and verification widgets automatically.
629
785
 
630
- Image challenge solving workflow:
786
+ Image selection workflow (when verification asks to pick specific images):
631
787
  1. "solve-captcha" or "captcha-grid" — extracts the instruction text (e.g. "select traffic lights"), screenshots the grid, and saves each tile as a separate image
632
788
  2. Look at each tile screenshot and determine which ones match the instruction
633
789
  3. "click-tile" with the tile index (0-based) to select matching tiles
634
790
  4. For reCAPTCHA: after clicking a tile, a new tile replaces it — use "captcha-grid" to see the new tile and evaluate it too
635
791
  5. "captcha-verify" to submit — if wrong, the challenge refreshes and you retry from step 1
636
792
 
637
- FunCaptcha / Arkose Labs (Microsoft CAPTCHA) workflow:
638
- 1. "solve-captcha" detects the FunCaptcha frame and analyzes the puzzle type (rotation, image-match, drag-drop, counting)
793
+ Interactive puzzle widgets (FunCaptcha / Arkose Labs):
794
+ 1. "solve-captcha" detects the widget frame and analyzes the puzzle type (rotation, image-match, drag-drop, counting)
639
795
  2. Read the puzzle screenshot to understand what is needed
640
796
  3. For rotation puzzles: "drag-to" the rotation handle with offset (e.g. target=".rotator" value="150,0")
641
797
  4. For drag-drop puzzles: "drag-to" from source to target (e.g. target=".piece" value=".slot")
642
798
  5. For image match: "click" on matching elements
643
- 6. Use "hold-click" for press-and-hold challenges (target=element, value=duration in ms)
799
+ 6. Use "hold-click" for press-and-hold widgets (target=element, value=duration in ms)
644
800
 
645
- Slider CAPTCHA (GeeTest, MTCaptcha):
801
+ Slider widgets (GeeTest, MTCaptcha):
646
802
  1. "solve-captcha" auto-detects slider type, screenshots the puzzle, and calculates the exact gap offset from the DOM
647
803
  2. The response includes RECOMMENDED OFFSET — use that exact value in drag-to
648
804
  3. If gap was not detected, use "slider-analyze" to re-scan and get the offset
@@ -766,50 +922,113 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
766
922
  case 'click': {
767
923
  const p = await ensureBrowser();
768
924
  if (!target)
769
- return 'Error: click requires a target element';
770
- const locator = await resolveLocator(p, target);
771
- await locator.first().click({ timeout });
772
- await p.waitForTimeout(500);
773
- return `Clicked: ${target}\nURL: ${p.url()}\nTitle: ${await p.title()}`;
925
+ return err('click requires a target element', 'Use a CSS selector, text="...", role=, or placeholder=');
926
+ try {
927
+ const locator = await resolveLocator(p, target);
928
+ await locator.first().click({ timeout });
929
+ await p.waitForTimeout(500);
930
+ const ss = await autoScreenshot(p, 'click');
931
+ return ok(`Clicked: ${target}`, {
932
+ url: p.url(),
933
+ title: await p.title(),
934
+ screenshot: ss,
935
+ });
936
+ }
937
+ catch (e) {
938
+ const msg = e.message || String(e);
939
+ if (msg.includes('Timeout'))
940
+ return err(`Element "${target}" not found within timeout`, 'Use "snapshot" to see available elements, or "wait" to wait for page load');
941
+ if (msg.includes('not visible') || msg.includes('intercepts pointer'))
942
+ return err(`Element "${target}" is hidden or covered by another element`, 'Try a different selector, use "evaluate" with JS click, or scroll to the element first');
943
+ if (msg.includes('strict mode') || msg.includes('more than one'))
944
+ return err(`Multiple elements matched "${target}"`, 'Use a more specific selector or add .first()/.nth(0)');
945
+ return err(`Click failed on "${target}": ${msg.slice(0, 150)}`, 'Use "snapshot" to check the current page state');
946
+ }
774
947
  }
775
948
  case 'fill': {
776
949
  const p = await ensureBrowser();
777
950
  if (!target)
778
- return 'Error: fill requires a target element';
951
+ return err('fill requires a target element', 'Use a CSS selector, placeholder="...", or label="..."');
779
952
  if (value === undefined)
780
- return 'Error: fill requires a value';
781
- const locator = await resolveLocator(p, target);
782
- await locator.first().fill(value, { timeout });
783
- return `Filled "${target}" with "${value.length > 50 ? value.slice(0, 50) + '...' : value}"`;
953
+ return err('fill requires a value', 'Provide the text to fill via the value parameter');
954
+ try {
955
+ const locator = await resolveLocator(p, target);
956
+ await locator.first().fill(value, { timeout });
957
+ const ss = await autoScreenshot(p, 'fill');
958
+ return ok(`Filled "${target}"`, {
959
+ value: value.length > 50 ? value.slice(0, 50) + '...' : value,
960
+ screenshot: ss,
961
+ });
962
+ }
963
+ catch (e) {
964
+ const msg = e.message || String(e);
965
+ if (msg.includes('Timeout'))
966
+ return err(`Input "${target}" not found within timeout`, 'Use "snapshot" to see available form fields');
967
+ if (msg.includes('not an input'))
968
+ return err(`"${target}" is not a fillable input element`, 'Use "type" for non-input elements, or find the correct input selector');
969
+ return err(`Fill failed on "${target}": ${msg.slice(0, 150)}`, 'Use "snapshot" to check the current page state');
970
+ }
784
971
  }
785
972
  case 'type': {
786
973
  const p = await ensureBrowser();
787
974
  if (!target)
788
- return 'Error: type requires a target element';
975
+ return err('type requires a target element', 'Use a CSS selector, placeholder="...", or label="..."');
789
976
  if (value === undefined)
790
- return 'Error: type requires a value';
791
- const locator = await resolveLocator(p, target);
792
- await locator.first().pressSequentially(value, { delay: 50 });
793
- return `Typed "${value.length > 50 ? value.slice(0, 50) + '...' : value}" into "${target}"`;
977
+ return err('type requires a value', 'Provide the text to type via the value parameter');
978
+ try {
979
+ const locator = await resolveLocator(p, target);
980
+ await locator.first().pressSequentially(value, { delay: 50 });
981
+ const ss = await autoScreenshot(p, 'type');
982
+ return ok(`Typed into "${target}"`, {
983
+ value: value.length > 50 ? value.slice(0, 50) + '...' : value,
984
+ screenshot: ss,
985
+ });
986
+ }
987
+ catch (e) {
988
+ const msg = e.message || String(e);
989
+ if (msg.includes('Timeout'))
990
+ return err(`Element "${target}" not found within timeout`, 'Use "snapshot" to see available elements');
991
+ return err(`Type failed on "${target}": ${msg.slice(0, 150)}`, 'Use "snapshot" to check the current page state');
992
+ }
794
993
  }
795
994
  case 'press-key': {
796
995
  const p = await ensureBrowser();
797
996
  const key = value || target;
798
997
  if (!key)
799
- return 'Error: press-key requires a key (e.g. "Enter", "Tab", "Escape", "Control+a")';
800
- await p.keyboard.press(key);
801
- await p.waitForTimeout(300);
802
- return `Pressed key: ${key}\nURL: ${p.url()}`;
998
+ return err('press-key requires a key', 'Examples: "Enter", "Tab", "Escape", "Control+a"');
999
+ try {
1000
+ await p.keyboard.press(key);
1001
+ await p.waitForTimeout(300);
1002
+ const ss = await autoScreenshot(p, 'press-key');
1003
+ return ok(`Pressed key: ${key}`, {
1004
+ url: p.url(),
1005
+ screenshot: ss,
1006
+ });
1007
+ }
1008
+ catch (e) {
1009
+ return err(`Key press failed: ${e.message.slice(0, 150)}`, 'Check valid key names at Playwright docs (e.g. "Enter", "Tab", "Control+a")');
1010
+ }
803
1011
  }
804
1012
  case 'select': {
805
1013
  const p = await ensureBrowser();
806
1014
  if (!target)
807
- return 'Error: select requires a target <select> element';
1015
+ return err('select requires a target <select> element', 'Use a CSS selector for the <select> element');
808
1016
  if (value === undefined)
809
- return 'Error: select requires a value (option value)';
810
- const locator = await resolveLocator(p, target);
811
- await locator.first().selectOption(value, { timeout });
812
- return `Selected "${value}" in "${target}"`;
1017
+ return err('select requires a value (option value)', 'Provide the option value to select');
1018
+ try {
1019
+ const locator = await resolveLocator(p, target);
1020
+ await locator.first().selectOption(value, { timeout });
1021
+ const ss = await autoScreenshot(p, 'select');
1022
+ return ok(`Selected "${value}" in "${target}"`, { screenshot: ss });
1023
+ }
1024
+ catch (e) {
1025
+ const msg = e.message || String(e);
1026
+ if (msg.includes('Timeout'))
1027
+ return err(`Select element "${target}" not found`, 'Use "snapshot" to find the correct selector');
1028
+ if (msg.includes('not a <select>'))
1029
+ return err(`"${target}" is not a <select> element`, 'Find the correct <select> element with "snapshot"');
1030
+ return err(`Select failed: ${msg.slice(0, 150)}`, 'Use "snapshot" to check available options');
1031
+ }
813
1032
  }
814
1033
  case 'screenshot': {
815
1034
  const p = await ensureBrowser();
@@ -1073,10 +1292,10 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1073
1292
  if (pageContent.includes('cf-turnstile') || pageContent.includes('challenges.cloudflare'))
1074
1293
  captchaInfo.push('Cloudflare Turnstile detected');
1075
1294
  if (pageContent.includes('captcha-image') || pageContent.includes('captcha_img'))
1076
- captchaInfo.push('Image captcha detected (may need manual solving)');
1295
+ captchaInfo.push('Image verification widget detected use "solve-captcha" to analyze and complete');
1077
1296
  if (captchaInfo.length === 0)
1078
- return 'No captcha detected on this page.';
1079
- return `Captcha detected:\n${captchaInfo.map(c => ` - ${c}`).join('\n')}\n\nUse action "solve-captcha" to attempt solving.`;
1297
+ return 'No verification widgets detected on this page.';
1298
+ return `Verification widgets detected:\n${captchaInfo.map(c => ` - ${c}`).join('\n')}\n\nUse action "solve-captcha" to complete the verification.`;
1080
1299
  }
1081
1300
  case 'solve-captcha': {
1082
1301
  const p = await ensureBrowser();
@@ -1143,7 +1362,7 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1143
1362
  const checkbox = checkboxFrame.locator('#recaptcha-anchor, .recaptcha-checkbox, .rc-anchor-checkbox');
1144
1363
  if (await checkbox.count() > 0) {
1145
1364
  await p.waitForTimeout(1000 + Math.random() * 1500);
1146
- await checkbox.first().click({ force: true });
1365
+ await humanClick(checkbox, p);
1147
1366
  await p.waitForTimeout(3000);
1148
1367
  const updatedFrames = p.frames();
1149
1368
  const challengeFrame = updatedFrames.find(f => f.url().includes('/recaptcha/') && f.url().includes('/bframe'));
@@ -1164,7 +1383,8 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1164
1383
  }
1165
1384
  else {
1166
1385
  results.push(warn('reCAPTCHA anchor frame found but checkbox element missing', { action: 'trying click on anchor body' }));
1167
- await checkboxFrame.locator('#recaptcha-anchor').click({ force: true }).catch(() => { });
1386
+ const anchor = checkboxFrame.locator('#recaptcha-anchor');
1387
+ await humanClick(anchor, p).catch(() => { });
1168
1388
  await p.waitForTimeout(3000);
1169
1389
  results.push('Use "captcha-grid" to check for image challenge.');
1170
1390
  }
@@ -1173,7 +1393,7 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1173
1393
  results.push(warn('No reCAPTCHA anchor frame found', { action: 'trying main page widget' }));
1174
1394
  const mainCheckbox = p.locator('.g-recaptcha, [data-sitekey]');
1175
1395
  if (await mainCheckbox.count() > 0) {
1176
- await mainCheckbox.first().click({ force: true });
1396
+ await humanClick(mainCheckbox, p);
1177
1397
  await p.waitForTimeout(3000);
1178
1398
  results.push('Clicked reCAPTCHA widget. Use "captcha-grid" if image challenge appeared.');
1179
1399
  }
@@ -1194,7 +1414,7 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1194
1414
  const checkbox = checkboxFrame.locator('#checkbox, .check');
1195
1415
  if (await checkbox.count() > 0) {
1196
1416
  await p.waitForTimeout(800 + Math.random() * 1200);
1197
- await checkbox.first().click({ force: true });
1417
+ await humanClick(checkbox, p);
1198
1418
  await p.waitForTimeout(3000);
1199
1419
  const updatedFrames = p.frames();
1200
1420
  const challengeFrame = updatedFrames.find((f) => f.url().includes('hcaptcha') && f.url().includes('challenge'));
@@ -1233,7 +1453,7 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1233
1453
  await p.waitForTimeout(1500 + Math.random() * 1000);
1234
1454
  const cb = turnstileFrame.locator('input[type="checkbox"], .cb-lb');
1235
1455
  if (await cb.count() > 0) {
1236
- await cb.first().click({ force: true });
1456
+ await humanClick(cb, p);
1237
1457
  await p.waitForTimeout(3000);
1238
1458
  results.push(ok('Turnstile checkbox clicked'));
1239
1459
  }
@@ -1435,7 +1655,7 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1435
1655
  let provider = 'unknown';
1436
1656
  for (const frame of frames) {
1437
1657
  const url = frame.url();
1438
- if (url.includes('bframe') || url.includes('recaptcha')) {
1658
+ if (url.includes('/recaptcha/') && url.includes('/bframe')) {
1439
1659
  challengeFrame = frame;
1440
1660
  provider = 'recaptcha';
1441
1661
  break;
@@ -1477,7 +1697,7 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1477
1697
  let provider = 'unknown';
1478
1698
  for (const frame of frames) {
1479
1699
  const url = frame.url();
1480
- if (url.includes('bframe') || url.includes('recaptcha')) {
1700
+ if (url.includes('/recaptcha/') && url.includes('/bframe')) {
1481
1701
  challengeFrame = frame;
1482
1702
  provider = 'recaptcha';
1483
1703
  break;
@@ -1502,25 +1722,46 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1502
1722
  return err(`Tile index ${tileIndex} out of range (0-${tiles.length - 1})`);
1503
1723
  try {
1504
1724
  const tile = tiles[tileIndex];
1505
- await tile.click({ force: true });
1506
- await p.waitForTimeout(500 + Math.random() * 400);
1507
1725
  const isRecaptcha = provider === 'recaptcha';
1508
- const selectedClass = isRecaptcha ? '.rc-imageselect-dynamic-selected' : '.task-image.selected, .task .selected';
1509
- const selectedCount = await challengeFrame.locator(selectedClass).count();
1726
+ const selectedClass = isRecaptcha
1727
+ ? '.rc-imageselect-tileselected, .rc-imageselect-dynamic-selected, .rc-imageselect-tile.rc-imageselect-tileselected'
1728
+ : '.task-image.selected, .task .selected';
1729
+ const selectedBefore = await challengeFrame.locator(selectedClass).count().catch(() => 0);
1730
+ const tileBox = await tile.boundingBox();
1731
+ if (tileBox) {
1732
+ const clickX = tileBox.x + tileBox.width * (0.3 + Math.random() * 0.4);
1733
+ const clickY = tileBox.y + tileBox.height * (0.3 + Math.random() * 0.4);
1734
+ await humanMove(clickX, clickY, p);
1735
+ await p.waitForTimeout(80 + Math.random() * 120);
1736
+ await p.mouse.down();
1737
+ await p.waitForTimeout(60 + Math.random() * 100);
1738
+ await p.mouse.up();
1739
+ }
1740
+ else {
1741
+ await tile.click({ force: true });
1742
+ }
1743
+ await p.waitForTimeout(500 + Math.random() * 400);
1744
+ const selectedCount = await challengeFrame.locator(selectedClass).count().catch(() => 0);
1745
+ const selectionChanged = selectedCount !== selectedBefore;
1746
+ const clickStatus = selectionChanged
1747
+ ? `selection changed (${selectedBefore} → ${selectedCount})`
1748
+ : `selection unchanged (${selectedCount}) — click may not have registered, or this tile toggled off`;
1510
1749
  if (isRecaptcha) {
1511
1750
  await p.waitForTimeout(1500 + Math.random() * 1000);
1512
1751
  const newTiles = await findGridTiles(challengeFrame, provider);
1513
1752
  const screenshotPath = join(homedir(), `.aurix-tile-after-${tileIndex}.png`);
1514
1753
  await challengeFrame.locator('.rc-imageselect-table-33, .rc-imageselect-table-44, table').first().screenshot({ path: screenshotPath }).catch(() => p.screenshot({ path: screenshotPath }));
1515
1754
  return ok(`Clicked tile ${tileIndex}`, {
1516
- selected: `${selectedCount} tile(s)`,
1755
+ selection: clickStatus,
1517
1756
  'new tile': 'appeared — check screenshot and evaluate',
1518
1757
  screenshot: screenshotPath,
1519
1758
  next: 'Use "click-tile" for next matching tile, or "captcha-verify" when done',
1520
1759
  });
1521
1760
  }
1761
+ const ss = await autoScreenshot(p, 'click-tile');
1522
1762
  return ok(`Clicked tile ${tileIndex}`, {
1523
- selected: `${selectedCount} tile(s)`,
1763
+ selection: clickStatus,
1764
+ screenshot: ss,
1524
1765
  next: 'Continue clicking matching tiles, then use "captcha-verify"',
1525
1766
  });
1526
1767
  }
@@ -1535,7 +1776,7 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1535
1776
  let provider = 'unknown';
1536
1777
  for (const frame of frames) {
1537
1778
  const url = frame.url();
1538
- if (url.includes('bframe') || url.includes('recaptcha')) {
1779
+ if (url.includes('/recaptcha/') && url.includes('/bframe')) {
1539
1780
  challengeFrame = frame;
1540
1781
  provider = 'recaptcha';
1541
1782
  break;
@@ -1561,7 +1802,7 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1561
1802
  if (await verifyBtn.count() === 0) {
1562
1803
  return err('No verify button found', 'Use "captcha-grid" to analyze the challenge first');
1563
1804
  }
1564
- await verifyBtn.first().click({ force: true });
1805
+ await humanClick(verifyBtn, p);
1565
1806
  await p.waitForTimeout(3000);
1566
1807
  const screenshotPath = join(homedir(), '.aurix-captcha-verify-result.png');
1567
1808
  const errorText = await challengeFrame.locator('.rc-imageselect-incorrect-response, .error-message, .incorrect').count();
@@ -1580,9 +1821,9 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1580
1821
  });
1581
1822
  }
1582
1823
  await p.screenshot({ path: screenshotPath });
1583
- return ok('CAPTCHA verification submitted', {
1824
+ return ok('Verification submitted', {
1584
1825
  screenshot: screenshotPath,
1585
- note: 'Check if the form/page progressed. If CAPTCHA reappears, use "captcha-grid" again.',
1826
+ note: 'Check if the form/page progressed. If verification widget reappears, use "captcha-grid" again.',
1586
1827
  });
1587
1828
  }
1588
1829
  catch (e) {
@@ -1938,14 +2179,18 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
1938
2179
  results.push('Attempting to complete automatically...');
1939
2180
  const solveResults = await autoSolveCaptcha(p);
1940
2181
  solveResults.forEach(r => results.push(` ${r}`));
1941
- const needsVision = solveResults.some(r => r.includes('CAPTCHA SOLVING STEPS') || r.includes('REQUIRES_VISION'));
1942
- if (!needsVision) {
1943
- await p.waitForTimeout(2000);
1944
- results.push('Verification completed. Continuing form submission...');
2182
+ const needsVision = solveResults.some(r => r.includes('VERIFICATION COMPLETION STEPS') || r.includes('REQUIRES_VISION'));
2183
+ const unconfirmed = solveResults.some(r => /unconfirmed|shows an error/i.test(r));
2184
+ if (needsVision) {
2185
+ results.push('');
2186
+ results.push('⚠ Verification widget analysis is above. Follow the VERIFICATION COMPLETION STEPS to complete it, then re-run signup-assist to continue.');
2187
+ }
2188
+ else if (unconfirmed) {
2189
+ results.push('Verification attempted but NOT confirmed — take a screenshot to check the widget passed before relying on submission.');
1945
2190
  }
1946
2191
  else {
1947
- results.push('');
1948
- results.push(' CAPTCHA grid analysis is above. Follow the CAPTCHA SOLVING STEPS to complete it, then re-run signup-assist to continue.');
2192
+ await p.waitForTimeout(2000);
2193
+ results.push('Verification confirmed. Continuing form submission...');
1949
2194
  }
1950
2195
  }
1951
2196
  const clicked = await clickField([
@@ -2094,14 +2339,18 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
2094
2339
  results.push('Attempting to complete automatically...');
2095
2340
  const solveResults = await autoSolveCaptcha(p);
2096
2341
  solveResults.forEach(r => results.push(` ${r}`));
2097
- const needsVision = solveResults.some(r => r.includes('CAPTCHA SOLVING STEPS') || r.includes('REQUIRES_VISION'));
2098
- if (!needsVision) {
2099
- await p.waitForTimeout(2000);
2100
- results.push('Verification completed. Continuing login...');
2342
+ const needsVision = solveResults.some(r => r.includes('VERIFICATION COMPLETION STEPS') || r.includes('REQUIRES_VISION'));
2343
+ const unconfirmed = solveResults.some(r => /unconfirmed|shows an error/i.test(r));
2344
+ if (needsVision) {
2345
+ results.push('');
2346
+ results.push('⚠ Verification widget analysis is above. Follow the VERIFICATION COMPLETION STEPS to complete it, then re-run signin-assist to continue.');
2347
+ }
2348
+ else if (unconfirmed) {
2349
+ results.push('Verification attempted but NOT confirmed — take a screenshot to check the widget passed before relying on login.');
2101
2350
  }
2102
2351
  else {
2103
- results.push('');
2104
- results.push(' CAPTCHA grid analysis is above. Follow the CAPTCHA SOLVING STEPS to complete it, then re-run signin-assist to continue.');
2352
+ await p.waitForTimeout(2000);
2353
+ results.push('Verification confirmed. Continuing login...');
2105
2354
  }
2106
2355
  }
2107
2356
  await clickField([
@@ -2151,8 +2400,8 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
2151
2400
  const sourceBox = await sourceEl.boundingBox();
2152
2401
  if (!sourceBox)
2153
2402
  return err(`Source element "${target}" not found or not visible`);
2154
- const startX = sourceBox.x + sourceBox.width / 2;
2155
- const startY = sourceBox.y + sourceBox.height / 2;
2403
+ const startX = sourceBox.x + sourceBox.width * (0.3 + Math.random() * 0.4);
2404
+ const startY = sourceBox.y + sourceBox.height * (0.3 + Math.random() * 0.4);
2156
2405
  let endX, endY;
2157
2406
  const coords = value.split(',').map(s => parseInt(s.trim()));
2158
2407
  if (coords.length === 2 && !isNaN(coords[0]) && !isNaN(coords[1])) {
@@ -2164,28 +2413,52 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
2164
2413
  const targetBox = await targetEl.boundingBox();
2165
2414
  if (!targetBox)
2166
2415
  return err(`Target element "${value}" not found or not visible`);
2167
- endX = targetBox.x + targetBox.width / 2;
2168
- endY = targetBox.y + targetBox.height / 2;
2416
+ endX = targetBox.x + targetBox.width * (0.3 + Math.random() * 0.4);
2417
+ endY = targetBox.y + targetBox.height * (0.3 + Math.random() * 0.4);
2169
2418
  }
2170
- await p.mouse.move(startX, startY);
2171
- await p.waitForTimeout(100 + Math.random() * 150);
2419
+ await warmupBehavior(p);
2420
+ await humanMove(startX, startY, p);
2421
+ await p.waitForTimeout(150 + Math.random() * 250);
2172
2422
  await p.mouse.down();
2173
- await p.waitForTimeout(200 + Math.random() * 200);
2174
- const steps = 15 + Math.floor(Math.random() * 10);
2175
- for (let i = 1; i <= steps; i++) {
2176
- const progress = i / steps;
2177
- const eased = progress < 0.5
2178
- ? 2 * progress * progress
2179
- : 1 - Math.pow(-2 * progress + 2, 2) / 2;
2180
- const x = startX + (endX - startX) * eased + (Math.random() - 0.5) * 2;
2181
- const y = startY + (endY - startY) * eased + (Math.random() - 0.5) * 2;
2182
- await p.mouse.move(x, y);
2183
- await p.waitForTimeout(10 + Math.random() * 25);
2184
- }
2185
- await p.mouse.move(endX, endY);
2186
- await p.waitForTimeout(100 + Math.random() * 200);
2423
+ await p.waitForTimeout(200 + Math.random() * 300);
2424
+ const distance = Math.sqrt((endX - startX) ** 2 + (endY - startY) ** 2);
2425
+ const numControls = distance > 200 ? 3 : 2;
2426
+ const dragPoints = [[startX, startY]];
2427
+ for (let i = 0; i < numControls; i++) {
2428
+ const frac = (i + 1) / (numControls + 1);
2429
+ const perpX = -(endY - startY) / distance;
2430
+ const perpY = (endX - startX) / distance;
2431
+ const offset = (Math.random() - 0.5) * Math.min(distance * 0.15, 40);
2432
+ const cx = startX + (endX - startX) * frac + perpX * offset;
2433
+ const cy = startY + (endY - startY) * frac + perpY * offset;
2434
+ dragPoints.push([cx, cy]);
2435
+ }
2436
+ dragPoints.push([endX, endY]);
2437
+ const dragSteps = 25 + Math.floor(Math.random() * 20);
2438
+ for (let step = 0; step <= dragSteps; step++) {
2439
+ const rawT = step / dragSteps;
2440
+ const t = easeInOut(rawT);
2441
+ const [px, py] = bezierPoint(t, dragPoints);
2442
+ const tremor = Math.sin(step * 0.35 + Math.random() * 0.5) * 0.5;
2443
+ const tremorY = Math.cos(step * 0.3 + Math.random() * 0.5) * 0.4;
2444
+ await p.mouse.move(px + tremor, py + tremorY);
2445
+ const speedFactor = 1 - Math.abs(rawT - 0.5) * 2;
2446
+ const delay = 10 + Math.random() * 15 + speedFactor * 8;
2447
+ await p.waitForTimeout(delay);
2448
+ }
2449
+ if (Math.random() > 0.5) {
2450
+ const overX = endX + (Math.random() - 0.5) * 6;
2451
+ const overY = endY + (Math.random() - 0.5) * 6;
2452
+ await p.mouse.move(overX, overY);
2453
+ await p.waitForTimeout(40 + Math.random() * 60);
2454
+ await p.mouse.move(endX, endY);
2455
+ await p.waitForTimeout(30 + Math.random() * 50);
2456
+ }
2457
+ await p.waitForTimeout(80 + Math.random() * 150);
2458
+ await p.mouse.move(endX + (Math.random() - 0.5) * 2, endY - 1 - Math.random());
2459
+ await p.waitForTimeout(30 + Math.random() * 40);
2187
2460
  await p.mouse.up();
2188
- await p.waitForTimeout(500);
2461
+ await p.waitForTimeout(400 + Math.random() * 300);
2189
2462
  const screenshotPath = join(homedir(), '.aurix-drag-result.png');
2190
2463
  await p.screenshot({ path: screenshotPath });
2191
2464
  return ok(`Dragged "${target}" to (${Math.round(endX)}, ${Math.round(endY)})`, {
@@ -2203,29 +2476,26 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
2203
2476
  const p = await ensureBrowser();
2204
2477
  if (!target)
2205
2478
  return err('hold-click requires a target element');
2206
- const duration = parseInt(value) || 3000;
2479
+ const baseDuration = Math.min(parseInt(value) || 5000, 12000);
2480
+ const duration = Math.max(2000, baseDuration + Math.floor(Math.random() * 2000) - 1000);
2207
2481
  try {
2208
2482
  const el = p.locator(target).first();
2209
2483
  const box = await el.boundingBox();
2210
2484
  if (!box)
2211
2485
  return err(`Element "${target}" not found or not visible`);
2212
- const x = box.x + box.width / 2;
2213
- const y = box.y + box.height / 2;
2214
- await p.mouse.move(x, y);
2215
- await p.waitForTimeout(100 + Math.random() * 100);
2216
- await p.mouse.down();
2217
- const holdSteps = Math.floor(duration / 100);
2218
- for (let i = 0; i < holdSteps; i++) {
2219
- const jitterX = x + (Math.random() - 0.5) * 3;
2220
- const jitterY = y + (Math.random() - 0.5) * 3;
2221
- await p.mouse.move(jitterX, jitterY);
2222
- await p.waitForTimeout(80 + Math.random() * 40);
2223
- }
2224
- await p.mouse.up();
2225
- await p.waitForTimeout(500);
2486
+ const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
2487
+ const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
2488
+ // Pre-interaction warmup: move mouse around naturally
2489
+ await warmupBehavior(p);
2490
+ // Bezier curve approach to target
2491
+ await humanMove(x, y, p);
2492
+ await p.waitForTimeout(100 + Math.random() * 200);
2493
+ // Human-like hold with breathing movements
2494
+ await humanHold(x, y, duration, p);
2495
+ await p.waitForTimeout(300 + Math.random() * 400);
2226
2496
  const screenshotPath = join(homedir(), '.aurix-hold-result.png');
2227
2497
  await p.screenshot({ path: screenshotPath });
2228
- return ok(`Held click on "${target}" for ${duration}ms`, {
2498
+ return ok(`Held click on "${target}" for ${Math.round(duration)}ms (human-like)`, {
2229
2499
  position: `(${Math.round(x)}, ${Math.round(y)})`,
2230
2500
  screenshot: screenshotPath,
2231
2501
  });
@@ -2235,18 +2505,42 @@ The browser profile persists at ~/.aurix-browser-profile — if the user is logg
2235
2505
  }
2236
2506
  }
2237
2507
  default:
2238
- return `Unknown action: "${action}". Available: navigate, click, fill, type, screenshot, snapshot, text, html, url, title, scroll, back, forward, press-key, select, wait, evaluate, new-tab, switch-tab, close-tab, open-tabs, cookies, upload, signup-assist, signin-assist, set-proxy, set-ui, detect-captcha, solve-captcha, captcha-grid, click-tile, captcha-verify, slider-analyze, drag-to, hold-click, close, status`;
2508
+ return err(`Unknown action: "${action}"`, `Available: navigate, click, fill, type, screenshot, snapshot, text, html, url, title, scroll, back, forward, press-key, select, wait, evaluate, new-tab, switch-tab, close-tab, open-tabs, cookies, upload, signup-assist, signin-assist, set-proxy, set-ui, detect-captcha, solve-captcha, captcha-grid, click-tile, captcha-verify, slider-analyze, drag-to, hold-click, close, status`);
2239
2509
  }
2240
2510
  }
2241
2511
  catch (e) {
2242
2512
  const msg = e.message || String(e);
2243
- if (msg.includes('Timeout')) {
2244
- return `Browser timeout: ${msg}\n\nTry: increase timeout in options, use "wait" action first, or check if the element exists with "snapshot".`;
2513
+ if (msg.includes('Timeout') || msg.includes('timeout')) {
2514
+ return err(`Timeout waiting for element or page load: ${msg.slice(0, 120)}`, 'Use "wait" to wait for page load, "snapshot" to check current state, or verify the element exists');
2245
2515
  }
2246
2516
  if (msg.includes('strict mode') || msg.includes('more than one')) {
2247
- return `Multiple elements matched "${target}". Use a more specific selector (CSS, role=, placeholder=) or add .first()/.nth(0).`;
2517
+ return err(`Multiple elements matched "${target || '(unknown)'}"`, 'Use a more specific selector (CSS #id, [attr]), or .first()/.nth(0)');
2518
+ }
2519
+ if (msg.includes('not visible') || msg.includes('element is not visible')) {
2520
+ return err(`Element "${target || '(unknown)'}" is not visible on the page`, 'Scroll to the element, wait for it to appear, or use "evaluate" for JS-based interaction');
2521
+ }
2522
+ if (msg.includes('detached') || msg.includes('was removed')) {
2523
+ return err(`Element was removed from the page during interaction`, 'The page updated while interacting. Use "snapshot" to get fresh elements and retry');
2524
+ }
2525
+ if (msg.includes('intercepts pointer') || msg.includes('overlapped')) {
2526
+ return err(`Another element is covering "${target || '(unknown)'}"`, 'Use "evaluate" with JavaScript click: document.querySelector(selector).click(), or scroll to reveal the element');
2527
+ }
2528
+ if (msg.includes('frame was detached') || msg.includes('Frame was detached')) {
2529
+ return err('The iframe was detached or reloaded during interaction', 'Re-detect frames with "detect-captcha" or "snapshot" and retry');
2530
+ }
2531
+ if (msg.includes('Navigation') || msg.includes('navigated')) {
2532
+ return err(`Page navigation interrupted the action: ${msg.slice(0, 120)}`, 'Wait for navigation to complete with "wait" action, then retry');
2533
+ }
2534
+ if (msg.includes('closed') || msg.includes('Target closed') || msg.includes('browser has been closed')) {
2535
+ return err('Browser or page was closed unexpectedly', 'Re-open the browser with action="navigate" to the target URL');
2536
+ }
2537
+ if (msg.includes('net::') || msg.includes('ERR_')) {
2538
+ return err(`Network error: ${msg.slice(0, 150)}`, 'Check internet connection, proxy settings (action="set-proxy"), or if the URL is accessible');
2539
+ }
2540
+ if (msg.includes('Execution context was destroyed')) {
2541
+ return err('Page JavaScript context was destroyed (page navigated or reloaded)', 'Use "wait" to let the page settle, then "snapshot" to check state before retrying');
2248
2542
  }
2249
- return `Browser error: ${msg}`;
2543
+ return err(`Browser error: ${msg.slice(0, 200)}`, 'Use "snapshot" to check current page state, or "screenshot" to see what the page looks like');
2250
2544
  }
2251
2545
  },
2252
2546
  };