h17-webpilot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1852 @@
1
+ // Element Handle Registry
2
+
3
+ const handles = new Map(); // handleId -> { ref: WeakRef<Element>, lastAccessed }
4
+ let handleCounter = 0;
5
+ const frameworkRuntime = {
6
+ handles: {
7
+ ttlMs: 15 * 60 * 1000,
8
+ cleanupIntervalMs: 60 * 1000,
9
+ },
10
+ debug: {
11
+ cursor: true,
12
+ },
13
+ };
14
+ let handleCleanupTimer = null;
15
+ let lastFrameworkConfigJson = "";
16
+
17
+ function storeHandle(el) {
18
+ const id = `el_${++handleCounter}`;
19
+ handles.set(id, { ref: new WeakRef(el), lastAccessed: Date.now() });
20
+ return id;
21
+ }
22
+
23
+ function getHandle(id) {
24
+ const entry = handles.get(id);
25
+ if (!entry) throw new Error(`Handle ${id} not found`);
26
+ const el = entry.ref.deref();
27
+ if (!el) {
28
+ handles.delete(id);
29
+ throw new Error(`Handle ${id} was garbage collected`);
30
+ }
31
+ entry.lastAccessed = Date.now();
32
+ return el;
33
+ }
34
+
35
+ function resolveElement(params) {
36
+ if (params.handleId) return getHandle(params.handleId);
37
+ if (params.selector) {
38
+ const el = document.querySelector(params.selector);
39
+ if (!el) throw new Error(`Element not found: ${params.selector}`);
40
+ return el;
41
+ }
42
+ throw new Error("No handleId or selector provided");
43
+ }
44
+
45
+ function cleanupStaleHandles() {
46
+ const cutoff = Date.now() - frameworkRuntime.handles.ttlMs;
47
+ for (const [id, entry] of handles) {
48
+ if (!entry.ref.deref() || entry.lastAccessed < cutoff) {
49
+ handles.delete(id);
50
+ }
51
+ }
52
+ }
53
+
54
+ function restartHandleCleanupTimer() {
55
+ if (handleCleanupTimer) clearInterval(handleCleanupTimer);
56
+ handleCleanupTimer = setInterval(
57
+ cleanupStaleHandles,
58
+ frameworkRuntime.handles.cleanupIntervalMs,
59
+ );
60
+ }
61
+
62
+ // Cursor State & Bezier Math
63
+
64
+ let cursorX = 0;
65
+ let cursorY = 0;
66
+
67
+ // Restore cursor position from service worker (survives page reloads)
68
+ chrome.runtime.sendMessage(
69
+ { action: "cursor.getPosition", params: {} },
70
+ (response) => {
71
+ if (response && response.result) {
72
+ cursorX = response.result.x || 0;
73
+ cursorY = response.result.y || 0;
74
+ }
75
+ },
76
+ );
77
+
78
+ // Save cursor position to service worker after moves
79
+ function saveCursorPosition() {
80
+ chrome.runtime.sendMessage(
81
+ { action: "cursor.reportPosition", params: { x: cursorX, y: cursorY } },
82
+ () => {},
83
+ );
84
+ }
85
+
86
+ // Debug mode — defaults are configurable from framework config.
87
+ let debugMode = frameworkRuntime.debug.cursor;
88
+
89
+ function applyFrameworkConfig(rawConfig) {
90
+ if (!rawConfig || typeof rawConfig !== "object") return;
91
+
92
+ const nextJson = JSON.stringify(rawConfig);
93
+ if (nextJson === lastFrameworkConfigJson) return;
94
+ lastFrameworkConfigJson = nextJson;
95
+
96
+ if (rawConfig.handles && typeof rawConfig.handles === "object") {
97
+ const ttlMs = Number(rawConfig.handles.ttlMs);
98
+ const cleanupIntervalMs = Number(rawConfig.handles.cleanupIntervalMs);
99
+
100
+ if (Number.isFinite(ttlMs) && ttlMs >= 1000) {
101
+ frameworkRuntime.handles.ttlMs = ttlMs;
102
+ }
103
+ if (Number.isFinite(cleanupIntervalMs) && cleanupIntervalMs >= 1000) {
104
+ frameworkRuntime.handles.cleanupIntervalMs = cleanupIntervalMs;
105
+ }
106
+ restartHandleCleanupTimer();
107
+ }
108
+
109
+ if (
110
+ rawConfig.debug &&
111
+ typeof rawConfig.debug === "object" &&
112
+ typeof rawConfig.debug.cursor === "boolean"
113
+ ) {
114
+ debugMode = rawConfig.debug.cursor;
115
+ if (!debugMode) clearTrail();
116
+ }
117
+ }
118
+
119
+ restartHandleCleanupTimer();
120
+
121
+ // Cubic bezier point at t (0-1)
122
+ function bezierPoint(t, p0, p1, p2, p3) {
123
+ const u = 1 - t;
124
+ return (
125
+ u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3
126
+ );
127
+ }
128
+
129
+ // Ease-in-out curve: slow start, fast middle, slow end (like a real hand)
130
+ function easeInOut(t) {
131
+ if (t < 0.5) return 2 * t * t;
132
+ return -1 + (4 - 2 * t) * t;
133
+ }
134
+
135
+ // Generate bezier path from (x0,y0) to (x1,y1) with human-like control points
136
+ function generateBezierPath(x0, y0, x1, y1, steps) {
137
+ const dist = Math.hypot(x1 - x0, y1 - y0);
138
+ if (dist < 2) return [{ x: x1, y: y1 }];
139
+
140
+ // Control points offset perpendicular to the line
141
+ // More spread = more arc curvature
142
+ const spread = Math.min(dist * 0.35, 120);
143
+ const angle = Math.atan2(y1 - y0, x1 - x0);
144
+ const perpAngle = angle + Math.PI / 2;
145
+
146
+ // Asymmetric control points — real hands don't make symmetric arcs
147
+ const bias1 = (Math.random() - 0.5) * 2; // -1 to 1
148
+ const bias2 = (Math.random() - 0.5) * 2;
149
+ const cp1x =
150
+ x0 +
151
+ (x1 - x0) * (0.2 + Math.random() * 0.15) +
152
+ Math.cos(perpAngle) * bias1 * spread;
153
+ const cp1y =
154
+ y0 +
155
+ (y1 - y0) * (0.2 + Math.random() * 0.15) +
156
+ Math.sin(perpAngle) * bias1 * spread;
157
+ const cp2x =
158
+ x0 +
159
+ (x1 - x0) * (0.65 + Math.random() * 0.15) +
160
+ Math.cos(perpAngle) * bias2 * spread * 0.6;
161
+ const cp2y =
162
+ y0 +
163
+ (y1 - y0) * (0.65 + Math.random() * 0.15) +
164
+ Math.sin(perpAngle) * bias2 * spread * 0.6;
165
+
166
+ const numSteps = steps || Math.max(15, Math.min(Math.floor(dist / 4), 100));
167
+ const points = [];
168
+
169
+ for (let i = 1; i <= numSteps; i++) {
170
+ // Apply ease-in-out to t — cursor accelerates then decelerates
171
+ const linearT = i / numSteps;
172
+ const t = easeInOut(linearT);
173
+
174
+ let px = bezierPoint(t, x0, cp1x, cp2x, x1);
175
+ let py = bezierPoint(t, y0, cp1y, cp2y, y1);
176
+
177
+ // Micro-jitter (hand tremor) — strongest in the middle of the movement
178
+ // Fades near start and end where hand is steadier
179
+ const jitterStrength =
180
+ Math.sin(linearT * Math.PI) * Math.min(dist * 0.003, 1.5);
181
+ px += (Math.random() - 0.5) * jitterStrength * 2;
182
+ py += (Math.random() - 0.5) * jitterStrength * 2;
183
+
184
+ points.push({ x: px, y: py });
185
+ }
186
+
187
+ // Ensure final point is exactly on target
188
+ points[points.length - 1] = { x: x1, y: y1 };
189
+
190
+ return points;
191
+ }
192
+
193
+ // Visual Cursor Dot + Debug Trail
194
+
195
+ let cursorDot = null;
196
+ let cursorFadeTimer = null;
197
+ let trailContainer = null;
198
+
199
+ function ensureCursorDot() {
200
+ if (cursorDot && document.body?.contains(cursorDot)) return cursorDot;
201
+ cursorDot = document.createElement("div");
202
+ cursorDot.id = "__bridge_cursor";
203
+ cursorDot.style.cssText = [
204
+ "position:fixed",
205
+ "z-index:2147483647",
206
+ "width:12px",
207
+ "height:12px",
208
+ "border-radius:50%",
209
+ "background:rgba(66,133,244,0.8)",
210
+ "box-shadow:0 0 8px rgba(66,133,244,0.5)",
211
+ "pointer-events:none",
212
+ "transform:translate(-50%,-50%)",
213
+ "transition:opacity 0.5s",
214
+ "opacity:0",
215
+ ].join(";");
216
+ (document.body || document.documentElement).appendChild(cursorDot);
217
+ return cursorDot;
218
+ }
219
+
220
+ function ensureTrailContainer() {
221
+ if (trailContainer && document.body?.contains(trailContainer))
222
+ return trailContainer;
223
+ trailContainer = document.createElement("div");
224
+ trailContainer.id = "__bridge_trail";
225
+ trailContainer.style.cssText =
226
+ "position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";
227
+ (document.body || document.documentElement).appendChild(trailContainer);
228
+ return trailContainer;
229
+ }
230
+
231
+ function clearTrail() {
232
+ if (trailContainer) trailContainer.innerHTML = "";
233
+ }
234
+
235
+ function drawTrailDot(x, y, progress) {
236
+ const container = ensureTrailContainer();
237
+ const dot = document.createElement("div");
238
+ // Color shifts from green (start) → yellow (mid) → red (end)
239
+ const r = Math.floor(progress * 255);
240
+ const g = Math.floor((1 - progress) * 200);
241
+ const size = 3 + (1 - Math.abs(progress - 0.5) * 2) * 3; // larger in the middle
242
+ dot.style.cssText = [
243
+ "position:fixed",
244
+ "border-radius:50%",
245
+ "pointer-events:none",
246
+ `width:${size}px`,
247
+ `height:${size}px`,
248
+ `left:${x - size / 2}px`,
249
+ `top:${y - size / 2}px`,
250
+ `background:rgba(${r},${g},80,0.7)`,
251
+ "transition:opacity 2s",
252
+ ].join(";");
253
+ container.appendChild(dot);
254
+ // Fade trail dots after 3s
255
+ setTimeout(() => {
256
+ dot.style.opacity = "0";
257
+ }, 3000);
258
+ setTimeout(() => {
259
+ dot.remove();
260
+ }, 5000);
261
+ }
262
+
263
+ function moveCursorDot(x, y) {
264
+ const dot = ensureCursorDot();
265
+ dot.style.left = x + "px";
266
+ dot.style.top = y + "px";
267
+ dot.style.opacity = "1";
268
+ clearTimeout(cursorFadeTimer);
269
+ cursorFadeTimer = setTimeout(() => {
270
+ dot.style.opacity = "0";
271
+ }, 2000);
272
+ }
273
+
274
+ // Dispatch mouse move along path with variable timing (ease-in-out speed)
275
+ function dispatchMousePath(points) {
276
+ clearTrail();
277
+
278
+ return new Promise((resolve) => {
279
+ let i = 0;
280
+ const total = points.length;
281
+
282
+ function step() {
283
+ if (i >= total) {
284
+ resolve();
285
+ return;
286
+ }
287
+
288
+ const p = points[i];
289
+ const progress = i / total;
290
+ cursorX = p.x;
291
+ cursorY = p.y;
292
+ moveCursorDot(p.x, p.y);
293
+ drawTrailDot(p.x, p.y, progress);
294
+
295
+ const target = document.elementFromPoint(p.x, p.y) || document.body;
296
+ target.dispatchEvent(
297
+ new MouseEvent("mousemove", {
298
+ clientX: p.x,
299
+ clientY: p.y,
300
+ bubbles: true,
301
+ cancelable: true,
302
+ view: window,
303
+ }),
304
+ );
305
+
306
+ i++;
307
+
308
+ // Variable frame timing — occasionally skip a frame for micro-stutter
309
+ // Real hands don't move at perfectly uniform frame rate
310
+ if (Math.random() < 0.08 && i < total - 2) {
311
+ // Double-frame pause (~32ms instead of ~16ms) — simulates hand hesitation
312
+ requestAnimationFrame(() => requestAnimationFrame(step));
313
+ } else {
314
+ requestAnimationFrame(step);
315
+ }
316
+ }
317
+ requestAnimationFrame(step);
318
+ });
319
+ }
320
+
321
+ // Overshoot: move past target then correct back
322
+ function generateOvershootPath(x0, y0, x1, y1) {
323
+ const dist = Math.hypot(x1 - x0, y1 - y0);
324
+ if (dist < 100) return generateBezierPath(x0, y0, x1, y1);
325
+
326
+ // Overshoot proportional to distance, with randomization
327
+ const overshoot = Math.min(20, dist * 0.06) * (0.4 + Math.random() * 0.6);
328
+ const angle = Math.atan2(y1 - y0, x1 - x0);
329
+ // Slight perpendicular offset on the overshoot too
330
+ const perpOffset = (Math.random() - 0.5) * overshoot * 0.5;
331
+ const perpAngle = angle + Math.PI / 2;
332
+ const overX =
333
+ x1 + Math.cos(angle) * overshoot + Math.cos(perpAngle) * perpOffset;
334
+ const overY =
335
+ y1 + Math.sin(angle) * overshoot + Math.sin(perpAngle) * perpOffset;
336
+
337
+ const pathToOver = generateBezierPath(x0, y0, overX, overY);
338
+ const pathBack = generateBezierPath(overX, overY, x1, y1, 10);
339
+
340
+ return [...pathToOver, ...pathBack];
341
+ }
342
+
343
+ // Action: dom.markElement / dom.unmarkElement (for service-worker eval)
344
+
345
+ function actionMarkElement(params) {
346
+ const el = getHandle(params.handleId);
347
+ el.setAttribute("data-bridge-eval", params.markerId);
348
+ return { marked: true };
349
+ }
350
+
351
+ function actionUnmarkElement(params) {
352
+ const el = document.querySelector(`[data-bridge-eval="${params.markerId}"]`);
353
+ if (el) el.removeAttribute("data-bridge-eval");
354
+ return { unmarked: true };
355
+ }
356
+
357
+ // Register an element marked by evaluateHandle in MAIN world
358
+ function actionRegisterMarkedElement(params) {
359
+ const el = document.querySelector(`[data-bridge-handle="${params.marker}"]`);
360
+ if (!el) return null;
361
+ el.removeAttribute("data-bridge-handle");
362
+ return storeHandle(el);
363
+ }
364
+
365
+ // NOTE: dom.evaluate and dom.elementEvaluate are handled by the service worker
366
+ // using chrome.scripting.executeScript (MAIN world) to bypass MV3 CSP.
367
+ // They never reach the content script.
368
+
369
+ // Action: dom.querySelector / querySelectorAll / querySelectorWithin
370
+
371
+ function actionQuerySelector(params) {
372
+ const el = document.querySelector(params.selector);
373
+ return el ? storeHandle(el) : null;
374
+ }
375
+
376
+ function actionQuerySelectorAll(params) {
377
+ const els = document.querySelectorAll(params.selector);
378
+ return Array.from(els).map(storeHandle);
379
+ }
380
+
381
+ function actionQuerySelectorWithin(params) {
382
+ const parent = getHandle(params.parentHandleId);
383
+ const el = parent.querySelector(params.selector);
384
+ return el ? storeHandle(el) : null;
385
+ }
386
+
387
+ function actionQuerySelectorAllWithin(params) {
388
+ const parent = getHandle(params.parentHandleId);
389
+ return Array.from(parent.querySelectorAll(params.selector)).map(storeHandle);
390
+ }
391
+
392
+ // Action: dom.queryAllInfo — single-call querySelectorAll + handles + element info
393
+ function actionQueryAllInfo(params) {
394
+ const els = document.querySelectorAll(params.selector);
395
+ return Array.from(els).map(el => {
396
+ const handleId = storeHandle(el);
397
+ return {
398
+ handleId,
399
+ tag: el.tagName.toLowerCase(),
400
+ id: el.id || null,
401
+ cls: [...el.classList].slice(0, 3).join(' ') || null,
402
+ text: (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60) || null,
403
+ label: el.getAttribute('aria-label') || el.getAttribute('name') || el.getAttribute('placeholder') || (el.labels && el.labels[0] ? el.labels[0].textContent.trim() : null),
404
+ };
405
+ });
406
+ }
407
+
408
+ // Action: dom.boundingBox
409
+
410
+ function actionBoundingBox(params) {
411
+ const el = resolveElement(params);
412
+ const rect = el.getBoundingClientRect();
413
+ if (rect.width === 0 && rect.height === 0) return null;
414
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
415
+ }
416
+
417
+ // Action: dom.mouseMoveTo
418
+
419
+ async function actionMouseMoveTo(params) {
420
+ const el = resolveElement(params);
421
+ const rect = el.getBoundingClientRect();
422
+ // Target: random point within center 60% of element
423
+ const padX = rect.width * 0.2;
424
+ const padY = rect.height * 0.2;
425
+ const targetX = rect.x + padX + Math.random() * (rect.width - padX * 2);
426
+ const targetY = rect.y + padY + Math.random() * (rect.height - padY * 2);
427
+
428
+ let startX = cursorX;
429
+ let startY = cursorY;
430
+
431
+ // If cursor is already on or very near the target, drift away first so
432
+ // the movement path is always visible (avoids teleport-click appearance).
433
+ const dist = Math.hypot(targetX - startX, targetY - startY);
434
+ if (dist < 80) {
435
+ const driftAngle = Math.random() * Math.PI * 2;
436
+ const driftDist = 80 + Math.random() * 120;
437
+ const driftX = Math.max(0, Math.min(window.innerWidth, startX + Math.cos(driftAngle) * driftDist));
438
+ const driftY = Math.max(0, Math.min(window.innerHeight, startY + Math.sin(driftAngle) * driftDist));
439
+ await dispatchMousePath(generateBezierPath(startX, startY, driftX, driftY));
440
+ startX = cursorX;
441
+ startY = cursorY;
442
+ }
443
+
444
+ const dist2 = Math.hypot(targetX - startX, targetY - startY);
445
+ const points =
446
+ dist2 > 200
447
+ ? generateOvershootPath(startX, startY, targetX, targetY)
448
+ : generateBezierPath(startX, startY, targetX, targetY);
449
+
450
+ await dispatchMousePath(points);
451
+ saveCursorPosition();
452
+ return { x: cursorX, y: cursorY };
453
+ }
454
+
455
+ // Action: dom.click (with mousedown/mouseup/click dispatch)
456
+
457
+ function actionClick(params) {
458
+ const el = resolveElement(params);
459
+ const rect = el.getBoundingClientRect();
460
+ const x = cursorX || rect.x + rect.width / 2;
461
+ const y = cursorY || rect.y + rect.height / 2;
462
+ const clickCount = params.clickCount || 1;
463
+
464
+ for (let i = 1; i <= clickCount; i++) {
465
+ const opts = {
466
+ clientX: x,
467
+ clientY: y,
468
+ bubbles: true,
469
+ cancelable: true,
470
+ view: window,
471
+ button: 0,
472
+ detail: i,
473
+ };
474
+ // Dispatch on the element physically under the cursor.
475
+ // If nothing is at the cursor coordinates, abort — element is not truly visible.
476
+ const atPoint = document.elementFromPoint(x, y);
477
+ if (!atPoint) return;
478
+ atPoint.dispatchEvent(new MouseEvent("mousedown", opts));
479
+ if (i === 1) el.focus();
480
+ atPoint.dispatchEvent(new MouseEvent("mouseup", opts));
481
+ atPoint.dispatchEvent(new MouseEvent("click", opts));
482
+
483
+ // detail:2 = double-click (select word), detail:3 = triple-click (select all)
484
+ if (i === 2) atPoint.dispatchEvent(new MouseEvent("dblclick", opts));
485
+ }
486
+
487
+ // Triple-click: select all text in input/textarea
488
+ if (
489
+ clickCount >= 3 &&
490
+ (el.tagName === "INPUT" || el.tagName === "TEXTAREA")
491
+ ) {
492
+ el.setSelectionRange(0, el.value.length);
493
+ }
494
+
495
+ return { clicked: true };
496
+ }
497
+
498
+ // Action: dom.type
499
+
500
+ function actionType(params) {
501
+ const { text, handleId, selector } = params;
502
+ let target;
503
+ if (handleId) target = getHandle(handleId);
504
+ else if (selector) target = document.querySelector(selector);
505
+ else target = document.activeElement;
506
+
507
+ if (!target) throw new Error("No target for typing");
508
+ if (target !== document.activeElement) target.focus();
509
+
510
+ for (const char of text) {
511
+ const charCode = char.charCodeAt(0);
512
+ const shared = {
513
+ key: char,
514
+ code: `Key${char.toUpperCase()}`,
515
+ keyCode: charCode,
516
+ charCode,
517
+ which: charCode,
518
+ bubbles: true,
519
+ cancelable: true,
520
+ view: window,
521
+ };
522
+ target.dispatchEvent(new KeyboardEvent("keydown", shared));
523
+ target.dispatchEvent(new KeyboardEvent("keypress", shared));
524
+
525
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
526
+ const start = target.selectionStart || 0;
527
+ const end = target.selectionEnd || 0;
528
+ const newValue =
529
+ target.value.slice(0, start) + char + target.value.slice(end);
530
+ // Use native setter to trigger React's change detection
531
+ const proto =
532
+ target.tagName === "TEXTAREA"
533
+ ? HTMLTextAreaElement.prototype
534
+ : HTMLInputElement.prototype;
535
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
536
+ if (nativeSetter) {
537
+ nativeSetter.call(target, newValue);
538
+ } else {
539
+ target.value = newValue;
540
+ }
541
+ target.selectionStart = target.selectionEnd = start + 1;
542
+ } else if (target.isContentEditable) {
543
+ document.execCommand("insertText", false, char);
544
+ }
545
+
546
+ target.dispatchEvent(
547
+ new InputEvent("input", {
548
+ data: char,
549
+ inputType: "insertText",
550
+ bubbles: true,
551
+ cancelable: true,
552
+ }),
553
+ );
554
+ target.dispatchEvent(new KeyboardEvent("keyup", shared));
555
+ }
556
+
557
+ return { typed: true };
558
+ }
559
+
560
+ // Keyboard: key mapping, modifier tracking, functional keys
561
+
562
+ // Map Puppeteer key names → { key, code, keyCode }
563
+ const KEY_MAP = {
564
+ // Modifiers
565
+ Meta: { key: "Meta", code: "MetaLeft", keyCode: 91 },
566
+ Control: { key: "Control", code: "ControlLeft", keyCode: 17 },
567
+ Shift: { key: "Shift", code: "ShiftLeft", keyCode: 16 },
568
+ Alt: { key: "Alt", code: "AltLeft", keyCode: 18 },
569
+ // Action keys
570
+ Enter: { key: "Enter", code: "Enter", keyCode: 13 },
571
+ Tab: { key: "Tab", code: "Tab", keyCode: 9 },
572
+ Escape: { key: "Escape", code: "Escape", keyCode: 27 },
573
+ Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
574
+ Delete: { key: "Delete", code: "Delete", keyCode: 46 },
575
+ Space: { key: " ", code: "Space", keyCode: 32 },
576
+ " ": { key: " ", code: "Space", keyCode: 32 },
577
+ // Arrow keys
578
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
579
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
580
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
581
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
582
+ // Navigation
583
+ Home: { key: "Home", code: "Home", keyCode: 36 },
584
+ End: { key: "End", code: "End", keyCode: 35 },
585
+ PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
586
+ PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 },
587
+ };
588
+
589
+ // Resolve Puppeteer key name (e.g. "KeyA", "Backspace", "Meta") to event properties
590
+ function resolveKey(rawKey) {
591
+ if (KEY_MAP[rawKey]) return KEY_MAP[rawKey];
592
+
593
+ // "KeyA" → key: "a", code: "KeyA"
594
+ const keyMatch = rawKey.match(/^Key([A-Z])$/);
595
+ if (keyMatch) {
596
+ const letter = keyMatch[1].toLowerCase();
597
+ return {
598
+ key: letter,
599
+ code: rawKey,
600
+ keyCode: letter.toUpperCase().charCodeAt(0),
601
+ };
602
+ }
603
+
604
+ // "Digit5" → key: "5", code: "Digit5"
605
+ const digitMatch = rawKey.match(/^Digit(\d)$/);
606
+ if (digitMatch) {
607
+ return {
608
+ key: digitMatch[1],
609
+ code: rawKey,
610
+ keyCode: digitMatch[1].charCodeAt(0),
611
+ };
612
+ }
613
+
614
+ // Single character
615
+ if (rawKey.length === 1) {
616
+ const upper = rawKey.toUpperCase();
617
+ return { key: rawKey, code: `Key${upper}`, keyCode: upper.charCodeAt(0) };
618
+ }
619
+
620
+ // Fallback
621
+ return { key: rawKey, code: rawKey, keyCode: 0 };
622
+ }
623
+
624
+ // Track active modifiers for combo detection (Ctrl+A, etc.)
625
+ const activeModifiers = {
626
+ meta: false,
627
+ control: false,
628
+ shift: false,
629
+ alt: false,
630
+ };
631
+
632
+ function isModifier(key) {
633
+ return (
634
+ key === "Meta" || key === "Control" || key === "Shift" || key === "Alt"
635
+ );
636
+ }
637
+
638
+ function buildEventProps(resolved) {
639
+ return {
640
+ key: resolved.key,
641
+ code: resolved.code,
642
+ keyCode: resolved.keyCode,
643
+ charCode: resolved.keyCode,
644
+ which: resolved.keyCode,
645
+ metaKey: activeModifiers.meta,
646
+ ctrlKey: activeModifiers.control,
647
+ shiftKey: activeModifiers.shift,
648
+ altKey: activeModifiers.alt,
649
+ bubbles: true,
650
+ cancelable: true,
651
+ view: window,
652
+ };
653
+ }
654
+
655
+ // Handle functional side-effects of key presses (select-all, backspace, delete, enter)
656
+ function applyKeyEffect(target, resolved) {
657
+ const key = resolved.key;
658
+ const isInput = target.tagName === "INPUT" || target.tagName === "TEXTAREA";
659
+ const isSelect = target.tagName === "SELECT";
660
+ const selectAll = activeModifiers.meta || activeModifiers.control;
661
+
662
+ if (selectAll && (key === "a" || key === "A")) {
663
+ // Ctrl+A / Cmd+A → select all
664
+ if (isInput) {
665
+ target.setSelectionRange(0, target.value.length);
666
+ } else if (target.isContentEditable) {
667
+ const range = document.createRange();
668
+ range.selectNodeContents(target);
669
+ const sel = window.getSelection();
670
+ sel.removeAllRanges();
671
+ sel.addRange(range);
672
+ }
673
+ return;
674
+ }
675
+
676
+ if (isSelect && (key === "ArrowDown" || key === "ArrowUp")) {
677
+ const dir = key === "ArrowDown" ? 1 : -1;
678
+ const current = target.selectedIndex >= 0 ? target.selectedIndex : 0;
679
+ const next = Math.max(
680
+ 0,
681
+ Math.min(target.options.length - 1, current + dir),
682
+ );
683
+ if (next !== target.selectedIndex) {
684
+ target.selectedIndex = next;
685
+ target.dispatchEvent(new Event("input", { bubbles: true }));
686
+ target.dispatchEvent(new Event("change", { bubbles: true }));
687
+ }
688
+ return;
689
+ }
690
+
691
+ if (isSelect && key === "Enter") {
692
+ target.dispatchEvent(new Event("change", { bubbles: true }));
693
+ return;
694
+ }
695
+
696
+ if (key === "Backspace" && isInput) {
697
+ const start = target.selectionStart ?? 0;
698
+ const end = target.selectionEnd ?? 0;
699
+ const val = target.value;
700
+ let newValue, newCursor;
701
+ if (start !== end) {
702
+ // Delete selection
703
+ newValue = val.slice(0, start) + val.slice(end);
704
+ newCursor = start;
705
+ } else if (start > 0) {
706
+ // Delete char before cursor
707
+ newValue = val.slice(0, start - 1) + val.slice(end);
708
+ newCursor = start - 1;
709
+ } else {
710
+ return; // nothing to delete
711
+ }
712
+ const proto =
713
+ target.tagName === "TEXTAREA"
714
+ ? HTMLTextAreaElement.prototype
715
+ : HTMLInputElement.prototype;
716
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
717
+ if (nativeSetter) nativeSetter.call(target, newValue);
718
+ else target.value = newValue;
719
+ target.selectionStart = target.selectionEnd = newCursor;
720
+ target.dispatchEvent(
721
+ new InputEvent("input", {
722
+ inputType: "deleteContentBackward",
723
+ bubbles: true,
724
+ }),
725
+ );
726
+ return;
727
+ }
728
+
729
+ if (key === "Delete" && isInput) {
730
+ const start = target.selectionStart ?? 0;
731
+ const end = target.selectionEnd ?? 0;
732
+ const val = target.value;
733
+ let newValue;
734
+ if (start !== end) {
735
+ newValue = val.slice(0, start) + val.slice(end);
736
+ } else if (end < val.length) {
737
+ newValue = val.slice(0, start) + val.slice(end + 1);
738
+ } else {
739
+ return;
740
+ }
741
+ const proto =
742
+ target.tagName === "TEXTAREA"
743
+ ? HTMLTextAreaElement.prototype
744
+ : HTMLInputElement.prototype;
745
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
746
+ if (nativeSetter) nativeSetter.call(target, newValue);
747
+ else target.value = newValue;
748
+ target.selectionStart = target.selectionEnd = start;
749
+ target.dispatchEvent(
750
+ new InputEvent("input", {
751
+ inputType: "deleteContentForward",
752
+ bubbles: true,
753
+ }),
754
+ );
755
+ return;
756
+ }
757
+ }
758
+
759
+ function actionKeyPress(params) {
760
+ const target = document.activeElement || document.body;
761
+ const resolved = resolveKey(params.key);
762
+ const props = buildEventProps(resolved);
763
+ target.dispatchEvent(new KeyboardEvent("keydown", props));
764
+ applyKeyEffect(target, resolved);
765
+ target.dispatchEvent(new KeyboardEvent("keyup", props));
766
+ return { pressed: true };
767
+ }
768
+
769
+ function actionKeyDown(params) {
770
+ const target = document.activeElement || document.body;
771
+ const resolved = resolveKey(params.key);
772
+ // Track modifier state
773
+ if (params.key === "Meta") activeModifiers.meta = true;
774
+ if (params.key === "Control") activeModifiers.control = true;
775
+ if (params.key === "Shift") activeModifiers.shift = true;
776
+ if (params.key === "Alt") activeModifiers.alt = true;
777
+ const props = buildEventProps(resolved);
778
+ target.dispatchEvent(new KeyboardEvent("keydown", props));
779
+ return { down: true };
780
+ }
781
+
782
+ function actionKeyUp(params) {
783
+ const target = document.activeElement || document.body;
784
+ const resolved = resolveKey(params.key);
785
+ const props = buildEventProps(resolved);
786
+ target.dispatchEvent(new KeyboardEvent("keyup", props));
787
+ // Clear modifier state
788
+ if (params.key === "Meta") activeModifiers.meta = false;
789
+ if (params.key === "Control") activeModifiers.control = false;
790
+ if (params.key === "Shift") activeModifiers.shift = false;
791
+ if (params.key === "Alt") activeModifiers.alt = false;
792
+ return { up: true };
793
+ }
794
+
795
+ // Action: dom.scroll
796
+
797
+ function actionScroll(params) {
798
+ const {
799
+ selector,
800
+ amount = 400,
801
+ direction = "down",
802
+ behavior = "smooth",
803
+ } = params;
804
+ const top = direction === "down" ? amount : direction === "up" ? -amount : 0;
805
+ const left =
806
+ direction === "right" ? amount : direction === "left" ? -amount : 0;
807
+
808
+ // Support handleId, selector, or fallback to window
809
+ let el = null;
810
+ if (params.handleId) {
811
+ el = resolveElement(params);
812
+ } else if (selector) {
813
+ el = document.querySelector(selector);
814
+ }
815
+ const target = (el && el.scrollHeight > el.clientHeight + 10) ? el : window;
816
+ const before = target === window ? window.scrollY : target.scrollTop;
817
+ target.scrollBy({ top, left, behavior });
818
+ // Check actual scroll after a tick (smooth may not be instant)
819
+ const after = target === window ? window.scrollY : target.scrollTop;
820
+ return { scrolled: true, before, after, target: target === window ? "window" : "element" };
821
+ }
822
+
823
+ // Action: dom.focus
824
+
825
+ function actionFocus(params) {
826
+ const el = resolveElement(params);
827
+ el.focus();
828
+ return { focused: true };
829
+ }
830
+
831
+ // Action: dom.setValue
832
+
833
+ function actionSetValue(params) {
834
+ const el = resolveElement(params);
835
+
836
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
837
+ const nativeSetter = Object.getOwnPropertyDescriptor(
838
+ el instanceof HTMLInputElement
839
+ ? window.HTMLInputElement.prototype
840
+ : window.HTMLTextAreaElement.prototype,
841
+ "value",
842
+ )?.set;
843
+ if (nativeSetter) nativeSetter.call(el, params.value);
844
+ else el.value = params.value;
845
+ } else if (el instanceof HTMLSelectElement) {
846
+ // For select elements, assigning .value selects the matching option by value.
847
+ el.value = String(params.value);
848
+ if (el.value !== String(params.value)) {
849
+ const idx = Array.from(el.options).findIndex(
850
+ (opt) =>
851
+ opt.value === String(params.value) ||
852
+ opt.textContent?.trim() === String(params.value),
853
+ );
854
+ if (idx >= 0) el.selectedIndex = idx;
855
+ }
856
+ } else if (el.isContentEditable) {
857
+ el.textContent = String(params.value);
858
+ } else {
859
+ el.value = params.value;
860
+ }
861
+
862
+ el.dispatchEvent(new Event("input", { bubbles: true }));
863
+ el.dispatchEvent(new Event("change", { bubbles: true }));
864
+ return { set: true };
865
+ }
866
+
867
+ // Action: dom.getAttribute / dom.getProperty
868
+
869
+ function actionGetAttribute(params) {
870
+ const el = resolveElement(params);
871
+ return el.getAttribute(params.name);
872
+ }
873
+
874
+ function actionGetProperty(params) {
875
+ const el = resolveElement(params);
876
+ return el[params.name];
877
+ }
878
+
879
+ // Action: dom.waitForSelector (async)
880
+
881
+ function actionWaitForSelector(params, sendResponse) {
882
+ const { selector, timeout = 5000 } = params;
883
+ const start = Date.now();
884
+
885
+ // Check immediately
886
+ const existing = document.querySelector(selector);
887
+ if (existing) {
888
+ sendResponse({ result: storeHandle(existing) });
889
+ return;
890
+ }
891
+
892
+ // Observe mutations
893
+ const observer = new MutationObserver(() => {
894
+ const el = document.querySelector(selector);
895
+ if (el) {
896
+ observer.disconnect();
897
+ clearTimeout(timer);
898
+ sendResponse({ result: storeHandle(el) });
899
+ }
900
+ });
901
+
902
+ observer.observe(document.documentElement, {
903
+ childList: true,
904
+ subtree: true,
905
+ attributes: true,
906
+ });
907
+
908
+ const timer = setTimeout(() => {
909
+ observer.disconnect();
910
+ sendResponse({ result: null });
911
+ }, timeout);
912
+ }
913
+
914
+ // Avoid Check — custom element filtering for human.* commands
915
+
916
+ function checkAvoid(el, avoid) {
917
+ if (!avoid) return { avoided: false };
918
+
919
+ // Check CSS selectors (el.matches or el.closest)
920
+ for (const sel of avoid.selectors || []) {
921
+ try {
922
+ if (el.matches(sel) || el.closest(sel)) {
923
+ return { avoided: true, rule: `selector:${sel}` };
924
+ }
925
+ } catch {}
926
+ }
927
+
928
+ // Check class names
929
+ for (const cls of avoid.classes || []) {
930
+ if (el.classList.contains(cls)) {
931
+ return { avoided: true, rule: `class:${cls}` };
932
+ }
933
+ // Also check ancestors
934
+ if (el.closest(`.${CSS.escape(cls)}`)) {
935
+ return { avoided: true, rule: `class:${cls}` };
936
+ }
937
+ }
938
+
939
+ // Check IDs
940
+ for (const id of avoid.ids || []) {
941
+ if (el.id === id) {
942
+ return { avoided: true, rule: `id:${id}` };
943
+ }
944
+ if (el.closest(`#${CSS.escape(id)}`)) {
945
+ return { avoided: true, rule: `id:${id}` };
946
+ }
947
+ }
948
+
949
+ // Check attributes
950
+ for (const [attr, val] of Object.entries(avoid.attributes || {})) {
951
+ if (val === "*") {
952
+ if (el.hasAttribute(attr)) return { avoided: true, rule: `attr:${attr}` };
953
+ } else {
954
+ if (el.getAttribute(attr) === val)
955
+ return { avoided: true, rule: `attr:${attr}=${val}` };
956
+ }
957
+ }
958
+
959
+ return { avoided: false };
960
+ }
961
+
962
+ // Honeypot / Ghost Element Detection (built-in, always runs)
963
+
964
+ function checkHoneypot(el) {
965
+ // SVG elements — not clickable targets
966
+ const isSvg =
967
+ el.tagName === "svg" || el.tagName === "SVG" || el.closest("svg");
968
+ if (isSvg) return { safe: false, reason: "svg-element" };
969
+
970
+ const hasDisplayContents =
971
+ el.getAttribute("data-display-contents") === "true" ||
972
+ getComputedStyle(el).display === "contents";
973
+
974
+ // Aria-hidden
975
+ if (el.getAttribute("aria-hidden") === "true")
976
+ return { safe: false, reason: "aria-hidden" };
977
+
978
+ // No offsetParent (hidden), unless display:contents
979
+ if (!hasDisplayContents && el.offsetParent === null)
980
+ return { safe: false, reason: "no-offsetParent" };
981
+
982
+ // Honeypot class patterns
983
+ const classStr = Array.from(el.classList).join(" ");
984
+ if (
985
+ /\b(ghost|sr-only|visually-hidden|trap|honey|offscreen|off-screen)\b/i.test(
986
+ classStr,
987
+ )
988
+ )
989
+ return { safe: false, reason: "honeypot-class", detail: classStr };
990
+
991
+ // CSS checks
992
+ const computed = getComputedStyle(el);
993
+ if (parseFloat(computed.opacity) === 0)
994
+ return { safe: false, reason: "opacity-zero" };
995
+ if (computed.visibility === "hidden")
996
+ return { safe: false, reason: "visibility-hidden" };
997
+
998
+ // Sub-pixel elements (tracking pixels, invisible traps)
999
+ const rect = el.getBoundingClientRect();
1000
+ if (rect.width < 5 || rect.height < 5)
1001
+ return {
1002
+ safe: false,
1003
+ reason: "sub-pixel",
1004
+ detail: `${rect.width.toFixed(1)}x${rect.height.toFixed(1)}`,
1005
+ };
1006
+
1007
+ return { safe: true };
1008
+ }
1009
+
1010
+ // Shared helper: scroll element into comfortable view before interacting
1011
+ async function ensureElementVisible(el, params) {
1012
+ let rect = el.getBoundingClientRect();
1013
+ const vh = window.innerHeight;
1014
+ const vw = window.innerWidth;
1015
+ const fullyOffScreen =
1016
+ rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw;
1017
+ const partiallyVisible =
1018
+ !fullyOffScreen &&
1019
+ (rect.top < 0 ||
1020
+ rect.bottom > vh ||
1021
+ rect.top > vh * 0.85 ||
1022
+ rect.bottom < vh * 0.15);
1023
+
1024
+ if (fullyOffScreen || partiallyVisible) {
1025
+ // Smooth scroll into comfortable view first
1026
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
1027
+ await new Promise((r) =>
1028
+ setTimeout(r, 400 + Math.floor(Math.random() * 300)),
1029
+ );
1030
+ rect = el.getBoundingClientRect();
1031
+
1032
+ // If still fully off-screen after scrollIntoView, use multi-step humanScroll
1033
+ if (rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw) {
1034
+ const maxSteps = 20;
1035
+ for (let step = 0; step < maxSteps; step++) {
1036
+ rect = el.getBoundingClientRect();
1037
+ if (rect.top > vh * 0.15 && rect.bottom < vh * 0.85) break;
1038
+
1039
+ const direction = rect.top > vh ? "down" : "up";
1040
+ await actionHumanScroll({ ...params, direction });
1041
+ await new Promise((r) =>
1042
+ setTimeout(r, 600 + Math.floor(Math.random() * 800)),
1043
+ );
1044
+ }
1045
+ }
1046
+ rect = el.getBoundingClientRect();
1047
+ if (rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw) {
1048
+ return { visible: false, rect };
1049
+ }
1050
+ }
1051
+ return { visible: true, rect };
1052
+ }
1053
+
1054
+ // Action: human.click — safe click with honeypot detection + bezier movement
1055
+
1056
+ async function actionHumanClick(params) {
1057
+ const el = resolveElement(params);
1058
+ const config = params.config || {};
1059
+ const minDelay =
1060
+ config.thinkDelayMin !== undefined ? config.thinkDelayMin : 150;
1061
+ const maxDelay =
1062
+ config.thinkDelayMax !== undefined ? config.thinkDelayMax : 400;
1063
+ const maxShift = config.maxShiftPx !== undefined ? config.maxShiftPx : 50;
1064
+
1065
+ // Check custom avoid rules
1066
+ const avoidResult = checkAvoid(el, params.avoid);
1067
+ if (avoidResult.avoided)
1068
+ return { clicked: false, reason: "avoided", rule: avoidResult.rule };
1069
+
1070
+ // Built-in honeypot detection
1071
+ const honeypot = checkHoneypot(el);
1072
+ if (!honeypot.safe)
1073
+ return { clicked: false, reason: honeypot.reason, detail: honeypot.detail };
1074
+
1075
+ // Bounding box validation
1076
+ let rect = el.getBoundingClientRect();
1077
+ if (rect.width === 0 && rect.height === 0)
1078
+ return { clicked: false, reason: "no-bounding-box" };
1079
+
1080
+ // Scroll element into comfortable view
1081
+ const scrollResult = await ensureElementVisible(el, params);
1082
+ if (!scrollResult.visible)
1083
+ return {
1084
+ clicked: false,
1085
+ reason: "off-screen",
1086
+ detail: "could not scroll into view",
1087
+ };
1088
+ rect = scrollResult.rect;
1089
+
1090
+ // Bezier mouse move to element
1091
+ await actionMouseMoveTo(params);
1092
+
1093
+ // Human think-time delay
1094
+ const thinkTime =
1095
+ minDelay + Math.floor(Math.random() * (maxDelay - minDelay + 1));
1096
+ await new Promise((r) => setTimeout(r, thinkTime));
1097
+
1098
+ // Element shift detection — did it move during think time?
1099
+ const rectAfter = el.getBoundingClientRect();
1100
+ if (rectAfter.width === 0 && rectAfter.height === 0)
1101
+ return { clicked: false, reason: "element-disappeared" };
1102
+
1103
+ const shiftX = Math.abs(rectAfter.x - rect.x);
1104
+ const shiftY = Math.abs(rectAfter.y - rect.y);
1105
+ if (shiftX > maxShift || shiftY > maxShift)
1106
+ return {
1107
+ clicked: false,
1108
+ reason: "element-shifted",
1109
+ detail: `${shiftX.toFixed(0)}x${shiftY.toFixed(0)}px`,
1110
+ };
1111
+
1112
+ // Click dispatch (mousedown → mouseup → click)
1113
+ actionClick(params);
1114
+ return { clicked: true };
1115
+ }
1116
+
1117
+ // Action: human.type — per-character typing with human-like timing
1118
+
1119
+ async function actionHumanType(params) {
1120
+ const { text, handleId, selector } = params;
1121
+ const config = params.config || {};
1122
+ const baseMin = config.baseDelayMin !== undefined ? config.baseDelayMin : 80;
1123
+ const baseMax = config.baseDelayMax !== undefined ? config.baseDelayMax : 180;
1124
+ const variance = config.variance !== undefined ? config.variance : 25;
1125
+ const pauseChance =
1126
+ config.pauseChance !== undefined ? config.pauseChance : 0.12;
1127
+ const pauseMin = config.pauseMin !== undefined ? config.pauseMin : 150;
1128
+ const pauseMax = config.pauseMax !== undefined ? config.pauseMax : 400;
1129
+
1130
+ // Resolve target
1131
+ let target;
1132
+ if (handleId) target = getHandle(handleId);
1133
+ else if (selector) target = document.querySelector(selector);
1134
+ else target = document.activeElement;
1135
+ if (!target) throw new Error("No target for typing");
1136
+
1137
+ // Check avoid rules on target element
1138
+ const avoidResult = checkAvoid(target, params.avoid);
1139
+ if (avoidResult.avoided)
1140
+ return { typed: false, reason: "avoided", rule: avoidResult.rule };
1141
+
1142
+ if (target !== document.activeElement) target.focus();
1143
+
1144
+ // Tokenize: split text into regular chars and {SpecialKey} tokens
1145
+ const tokens = [];
1146
+ let pos = 0;
1147
+ while (pos < text.length) {
1148
+ if (text[pos] === "{") {
1149
+ const end = text.indexOf("}", pos);
1150
+ if (end > pos + 1) {
1151
+ tokens.push({ type: "key", value: text.slice(pos + 1, end) });
1152
+ pos = end + 1;
1153
+ continue;
1154
+ }
1155
+ }
1156
+ tokens.push({ type: "char", value: text[pos] });
1157
+ pos++;
1158
+ }
1159
+
1160
+ for (let i = 0; i < tokens.length; i++) {
1161
+ const token = tokens[i];
1162
+
1163
+ if (token.type === "key") {
1164
+ // Special key — dispatch via resolveKey/buildEventProps like actionKeyPress
1165
+ const resolved = resolveKey(token.value);
1166
+ const props = buildEventProps(resolved);
1167
+ target.dispatchEvent(new KeyboardEvent("keydown", props));
1168
+ applyKeyEffect(target, resolved);
1169
+ target.dispatchEvent(new KeyboardEvent("keyup", props));
1170
+ } else {
1171
+ // Regular character
1172
+ const char = token.value;
1173
+ const charCode = char.charCodeAt(0);
1174
+ const shared = {
1175
+ key: char,
1176
+ code: `Key${char.toUpperCase()}`,
1177
+ keyCode: charCode,
1178
+ charCode,
1179
+ which: charCode,
1180
+ bubbles: true,
1181
+ cancelable: true,
1182
+ view: window,
1183
+ };
1184
+
1185
+ target.dispatchEvent(new KeyboardEvent("keydown", shared));
1186
+ target.dispatchEvent(new KeyboardEvent("keypress", shared));
1187
+
1188
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
1189
+ const start = target.selectionStart || 0;
1190
+ const end = target.selectionEnd || 0;
1191
+ const newValue =
1192
+ target.value.slice(0, start) + char + target.value.slice(end);
1193
+ const proto =
1194
+ target.tagName === "TEXTAREA"
1195
+ ? HTMLTextAreaElement.prototype
1196
+ : HTMLInputElement.prototype;
1197
+ const nativeSetter = Object.getOwnPropertyDescriptor(
1198
+ proto,
1199
+ "value",
1200
+ )?.set;
1201
+ if (nativeSetter) nativeSetter.call(target, newValue);
1202
+ else target.value = newValue;
1203
+ target.selectionStart = target.selectionEnd = start + 1;
1204
+ } else if (target.isContentEditable) {
1205
+ document.execCommand("insertText", false, char);
1206
+ }
1207
+
1208
+ target.dispatchEvent(
1209
+ new InputEvent("input", {
1210
+ data: char,
1211
+ inputType: "insertText",
1212
+ bubbles: true,
1213
+ cancelable: true,
1214
+ }),
1215
+ );
1216
+ target.dispatchEvent(new KeyboardEvent("keyup", shared));
1217
+ }
1218
+
1219
+ // Human delay between tokens
1220
+ const baseDelay =
1221
+ baseMin + Math.floor(Math.random() * (baseMax - baseMin + 1));
1222
+ const micro = Math.floor(Math.random() * (variance * 2 + 1)) - variance;
1223
+ const charDelay = Math.max(50, baseDelay + micro);
1224
+ await new Promise((r) => setTimeout(r, charDelay));
1225
+
1226
+ // Thinking pause (random chance, not on last token)
1227
+ if (Math.random() < pauseChance && i < tokens.length - 1) {
1228
+ const pause =
1229
+ pauseMin + Math.floor(Math.random() * (pauseMax - pauseMin + 1));
1230
+ await new Promise((r) => setTimeout(r, pause));
1231
+ }
1232
+ }
1233
+
1234
+ return { typed: true };
1235
+ }
1236
+
1237
+ // Action: human.scroll — natural scrolling with optional back-scroll
1238
+
1239
+ async function actionHumanScroll(params) {
1240
+ const config = params.config || {};
1241
+ const flickMin = config.flickMin !== undefined ? config.flickMin : 150;
1242
+ const flickMax = config.flickMax !== undefined ? config.flickMax : 350;
1243
+ const backScrollChance =
1244
+ config.backScrollChance !== undefined ? config.backScrollChance : 0.1;
1245
+ const backScrollMin =
1246
+ config.backScrollMin !== undefined ? config.backScrollMin : 15;
1247
+ const backScrollMax =
1248
+ config.backScrollMax !== undefined ? config.backScrollMax : 60;
1249
+
1250
+ const { selector, direction = "down", amount } = params;
1251
+
1252
+ // Determine total amount: use param if provided, else randomized default from config
1253
+ const defaultMin = config.amountMin !== undefined ? config.amountMin : 250;
1254
+ const defaultMax = config.amountMax !== undefined ? config.amountMax : 550;
1255
+
1256
+ const totalAmount =
1257
+ amount !== undefined
1258
+ ? amount
1259
+ : defaultMin + Math.floor(Math.random() * (defaultMax - defaultMin + 1));
1260
+ let remaining = totalAmount;
1261
+
1262
+ let el = null;
1263
+ if (params.handleId) {
1264
+ try { el = resolveElement(params); } catch {}
1265
+ } else if (selector) {
1266
+ el = document.querySelector(selector);
1267
+ }
1268
+ const isTargetScrollable = el && el.scrollHeight > el.clientHeight + 10;
1269
+ const target = isTargetScrollable ? el : window;
1270
+
1271
+ while (remaining > 0) {
1272
+ // Determine this flick's amount
1273
+ const flickAmount = Math.min(
1274
+ remaining,
1275
+ flickMin + Math.floor(Math.random() * (flickMax - flickMin + 1)),
1276
+ );
1277
+
1278
+ const top =
1279
+ direction === "down"
1280
+ ? flickAmount
1281
+ : direction === "up"
1282
+ ? -flickAmount
1283
+ : 0;
1284
+ const left =
1285
+ direction === "right"
1286
+ ? flickAmount
1287
+ : direction === "left"
1288
+ ? -flickAmount
1289
+ : 0;
1290
+
1291
+ target.scrollBy({ top, left, behavior: "smooth" });
1292
+ remaining -= flickAmount;
1293
+
1294
+ // Back-scroll for realism (per-flick chance)
1295
+ if (Math.random() < backScrollChance) {
1296
+ await new Promise((r) =>
1297
+ setTimeout(r, 200 + Math.floor(Math.random() * 100)),
1298
+ );
1299
+ const backAmount =
1300
+ backScrollMin +
1301
+ Math.floor(Math.random() * (backScrollMax - backScrollMin + 1));
1302
+ const backTop =
1303
+ direction === "down"
1304
+ ? -backAmount
1305
+ : direction === "up"
1306
+ ? backAmount
1307
+ : 0;
1308
+ const backLeft =
1309
+ direction === "right"
1310
+ ? -backAmount
1311
+ : direction === "left"
1312
+ ? backAmount
1313
+ : 0;
1314
+ target.scrollBy({ top: backTop, left: backLeft, behavior: "smooth" });
1315
+ }
1316
+
1317
+ // Pause between flicks (unless it was the last one)
1318
+ if (remaining > 0) {
1319
+ const flickPause = 150 + Math.floor(Math.random() * 250);
1320
+ await new Promise((r) => setTimeout(r, flickPause));
1321
+ }
1322
+ }
1323
+
1324
+ // Final settling pause for smooth scrolling to finish
1325
+ await new Promise((r) => setTimeout(r, 500));
1326
+
1327
+ return { scrolled: true, amount: totalAmount };
1328
+ }
1329
+
1330
+ // Action: dom.batchQuery — perform multiple selector checks in one go
1331
+ function actionBatchQuery(params) {
1332
+ const { selectors = [] } = params;
1333
+ const results = {};
1334
+ for (const selector of selectors) {
1335
+ const el = document.querySelector(selector);
1336
+ results[selector] = !!el;
1337
+ }
1338
+ return results;
1339
+ }
1340
+
1341
+ // Action: human.clearInput — focus + select-all + delete with human timing
1342
+
1343
+ async function actionHumanClearInput(params) {
1344
+ // Use human.click to focus (gets honeypot + avoid checks)
1345
+ const clickResult = await actionHumanClick(params);
1346
+ if (!clickResult.clicked) return clickResult;
1347
+
1348
+ const el = resolveElement(params);
1349
+
1350
+ // Triple-click with human timing to select all
1351
+ await new Promise((r) => setTimeout(r, 40 + Math.floor(Math.random() * 50)));
1352
+ actionClick({ ...params, clickCount: 1 });
1353
+ await new Promise((r) => setTimeout(r, 50 + Math.floor(Math.random() * 60)));
1354
+ actionClick({ ...params, clickCount: 2 });
1355
+ await new Promise((r) => setTimeout(r, 45 + Math.floor(Math.random() * 55)));
1356
+ actionClick({ ...params, clickCount: 3 });
1357
+
1358
+ // Pause, then delete
1359
+ await new Promise((r) =>
1360
+ setTimeout(r, 120 + Math.floor(Math.random() * 120)),
1361
+ );
1362
+ actionKeyPress({ key: "Backspace" });
1363
+ await new Promise((r) =>
1364
+ setTimeout(r, 180 + Math.floor(Math.random() * 140)),
1365
+ );
1366
+
1367
+ return { cleared: true };
1368
+ }
1369
+
1370
+ // Message Handler
1371
+
1372
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1373
+ const action = message?.action;
1374
+ const incomingParams = message?.params || {};
1375
+ applyFrameworkConfig(incomingParams.__frameworkConfig);
1376
+ const { __frameworkConfig, ...params } = incomingParams;
1377
+
1378
+ try {
1379
+ switch (action) {
1380
+ case "dom.markElement":
1381
+ sendResponse({ result: actionMarkElement(params) });
1382
+ return;
1383
+ case "dom.unmarkElement":
1384
+ sendResponse({ result: actionUnmarkElement(params) });
1385
+ return;
1386
+ case "dom.registerMarkedElement":
1387
+ sendResponse({ result: actionRegisterMarkedElement(params) });
1388
+ return;
1389
+ case "dom.querySelector":
1390
+ sendResponse({ result: actionQuerySelector(params) });
1391
+ return;
1392
+ case "dom.querySelectorAll":
1393
+ sendResponse({ result: actionQuerySelectorAll(params) });
1394
+ return;
1395
+ case "dom.querySelectorWithin":
1396
+ sendResponse({ result: actionQuerySelectorWithin(params) });
1397
+ return;
1398
+ case "dom.querySelectorAllWithin":
1399
+ sendResponse({ result: actionQuerySelectorAllWithin(params) });
1400
+ return;
1401
+ case "dom.queryAllInfo":
1402
+ sendResponse({ result: actionQueryAllInfo(params) });
1403
+ return;
1404
+ case "dom.batchQuery":
1405
+ sendResponse({ result: actionBatchQuery(params) });
1406
+ return;
1407
+ case "dom.boundingBox":
1408
+ sendResponse({ result: actionBoundingBox(params) });
1409
+ return;
1410
+ case "dom.mouseMoveTo":
1411
+ actionMouseMoveTo(params)
1412
+ .then((r) => sendResponse({ result: r }))
1413
+ .catch((e) => sendResponse({ error: e.message }));
1414
+ return true; // async
1415
+ case "dom.click":
1416
+ actionHumanClick(params)
1417
+ .then((r) => sendResponse({ result: r }))
1418
+ .catch((e) => sendResponse({ error: e.message }));
1419
+ return true; // async
1420
+ case "dom.type":
1421
+ sendResponse({ result: actionType(params) });
1422
+ return;
1423
+ case "dom.keyPress":
1424
+ sendResponse({ result: actionKeyPress(params) });
1425
+ return;
1426
+ case "dom.keyDown":
1427
+ sendResponse({ result: actionKeyDown(params) });
1428
+ return;
1429
+ case "dom.keyUp":
1430
+ sendResponse({ result: actionKeyUp(params) });
1431
+ return;
1432
+ case "dom.scroll":
1433
+ sendResponse({ result: actionScroll(params) });
1434
+ return;
1435
+ case "dom.focus":
1436
+ sendResponse({ result: actionFocus(params) });
1437
+ return;
1438
+ case "dom.setValue":
1439
+ sendResponse({ result: actionSetValue(params) });
1440
+ return;
1441
+ case "dom.getAttribute":
1442
+ sendResponse({ result: actionGetAttribute(params) });
1443
+ return;
1444
+ case "dom.getProperty":
1445
+ sendResponse({ result: actionGetProperty(params) });
1446
+ return;
1447
+ case "dom.getHTML":
1448
+ // CSP-safe HTML retrieval from ISOLATED world
1449
+ sendResponse({
1450
+ result: {
1451
+ html: document.documentElement?.outerHTML || "",
1452
+ title: document.title || "",
1453
+ url: location?.href || ""
1454
+ }
1455
+ });
1456
+ return;
1457
+ case "dom.elementHTML": {
1458
+ // CSP-safe: get outerHTML/innerHTML of a specific handle
1459
+ const ehEl = resolveElement(params);
1460
+ sendResponse({
1461
+ result: {
1462
+ outer: ehEl.outerHTML.slice(0, params.limit || 5000),
1463
+ inner: ehEl.innerHTML.slice(0, params.limit || 5000),
1464
+ tag: ehEl.tagName.toLowerCase(),
1465
+ }
1466
+ });
1467
+ return;
1468
+ }
1469
+ case "dom.findScrollable": {
1470
+ // Find all scrollable containers on the page
1471
+ const scrollables = [];
1472
+ const all = document.querySelectorAll('*');
1473
+ for (const el of all) {
1474
+ if (el.scrollHeight > el.clientHeight + 20 && el !== document.documentElement && el !== document.body) {
1475
+ const style = getComputedStyle(el);
1476
+ const oy = style.overflowY;
1477
+ const ox = style.overflow;
1478
+ if (oy === 'visible' && ox === 'visible') continue; // skip non-scrollable
1479
+ const hid = storeHandle(el);
1480
+ scrollables.push({
1481
+ handleId: hid,
1482
+ tag: el.tagName.toLowerCase(),
1483
+ id: el.id || null,
1484
+ cls: [...el.classList].slice(0, 3).join(' ') || null,
1485
+ overflowY: oy,
1486
+ overflow: ox,
1487
+ scrollHeight: el.scrollHeight,
1488
+ clientHeight: el.clientHeight,
1489
+ children: el.children.length,
1490
+ text: (el.textContent || '').trim().slice(0, 80),
1491
+ });
1492
+ }
1493
+ }
1494
+ }
1495
+ sendResponse({ result: scrollables });
1496
+ return;
1497
+ }
1498
+ case "dom.waitForSelector":
1499
+ actionWaitForSelector(params, sendResponse);
1500
+ return true; // async
1501
+ case "dom.evaluate":
1502
+ case "dom.evaluateViaScript": {
1503
+ // Inject inline <script> to run in MAIN world (works with 'unsafe-inline' CSP)
1504
+ const callId =
1505
+ "_hb_" + Date.now() + "_" + Math.floor(Math.random() * 1e6);
1506
+ const resultEl = document.createElement("div");
1507
+ resultEl.id = callId;
1508
+ resultEl.style.display = "none";
1509
+ document.documentElement.appendChild(resultEl);
1510
+
1511
+ const argsJson = JSON.stringify(params.args || []);
1512
+ let markerSetup = "";
1513
+ if (params.markerId) {
1514
+ markerSetup = `var __el = document.querySelector('[data-bridge-eval="${params.markerId}"]');
1515
+ if (__el) __el.removeAttribute('data-bridge-eval');`;
1516
+ }
1517
+
1518
+ const script = document.createElement("script");
1519
+ script.textContent = `(function(){
1520
+ try {
1521
+ ${markerSetup}
1522
+ var __fn = (${params.fn});
1523
+ var __args = ${argsJson};
1524
+ ${params.markerId ? "if (__el) __args.unshift(__el);" : ""}
1525
+ var __r = __fn.apply(null, __args);
1526
+ if (__r && typeof __r.then === 'function') {
1527
+ __r.then(function(v) {
1528
+ document.getElementById('${callId}').setAttribute('data-result', JSON.stringify({v:v}));
1529
+ }).catch(function(e) {
1530
+ document.getElementById('${callId}').setAttribute('data-error', e.message || String(e));
1531
+ });
1532
+ } else {
1533
+ document.getElementById('${callId}').setAttribute('data-result', JSON.stringify({v:__r}));
1534
+ }
1535
+ } catch(e) {
1536
+ document.getElementById('${callId}').setAttribute('data-error', e.message || String(e));
1537
+ }
1538
+ })();`;
1539
+ document.documentElement.appendChild(script);
1540
+ script.remove();
1541
+
1542
+ // Check result — sync scripts already set data-result before we get here.
1543
+ // Async scripts: use MutationObserver for instant notification (no 50ms polling).
1544
+ const pollForResult = (resolve) => {
1545
+ const el = document.getElementById(callId);
1546
+ if (!el) {
1547
+ resolve({ error: "Result element removed" });
1548
+ return;
1549
+ }
1550
+
1551
+ const harvest = () => {
1552
+ const resultAttr = el.getAttribute("data-result");
1553
+ if (resultAttr !== null) {
1554
+ el.remove();
1555
+ try {
1556
+ resolve({ result: JSON.parse(resultAttr).v });
1557
+ } catch {
1558
+ resolve({ result: resultAttr });
1559
+ }
1560
+ return true;
1561
+ }
1562
+ const errorAttr = el.getAttribute("data-error");
1563
+ if (errorAttr !== null) {
1564
+ el.remove();
1565
+ resolve({ error: errorAttr });
1566
+ return true;
1567
+ }
1568
+ return false;
1569
+ };
1570
+
1571
+ // Instant path: synchronous scripts already wrote the result
1572
+ if (harvest()) return;
1573
+
1574
+ // Async path: observe attribute changes on the result element
1575
+ const observer = new MutationObserver(() => {
1576
+ if (harvest()) {
1577
+ observer.disconnect();
1578
+ clearTimeout(timer);
1579
+ }
1580
+ });
1581
+ observer.observe(el, {
1582
+ attributes: true,
1583
+ attributeFilter: ["data-result", "data-error"],
1584
+ });
1585
+
1586
+ const timer = setTimeout(() => {
1587
+ observer.disconnect();
1588
+ el.remove();
1589
+ resolve({ error: "Evaluate timed out" });
1590
+ }, 5000);
1591
+ };
1592
+
1593
+ new Promise(pollForResult).then((res) => {
1594
+ if (res.error) sendResponse({ error: res.error });
1595
+ else sendResponse({ result: res.result });
1596
+ });
1597
+ return true; // async
1598
+ }
1599
+ case "dom.elementEvaluate": {
1600
+ // Route through dom.evaluateViaScript with markerId
1601
+ // First mark the element, then run evaluate
1602
+ const el = getHandle(params.handleId);
1603
+ const markerId =
1604
+ "_hbm_" + Date.now() + "_" + Math.floor(Math.random() * 1e6);
1605
+ el.setAttribute("data-bridge-eval", markerId);
1606
+
1607
+ const callId =
1608
+ "_hb_" + Date.now() + "_" + Math.floor(Math.random() * 1e6);
1609
+ const resultEl = document.createElement("div");
1610
+ resultEl.id = callId;
1611
+ resultEl.style.display = "none";
1612
+ document.documentElement.appendChild(resultEl);
1613
+
1614
+ const argsJson = JSON.stringify(params.args || []);
1615
+ const script = document.createElement("script");
1616
+ script.textContent = `(function(){
1617
+ try {
1618
+ var __el = document.querySelector('[data-bridge-eval="${markerId}"]');
1619
+ if (__el) __el.removeAttribute('data-bridge-eval');
1620
+ var __fn = (${params.fn});
1621
+ var __args = ${argsJson};
1622
+ var __r = __fn.apply(null, [__el].concat(__args));
1623
+ if (__r && typeof __r.then === 'function') {
1624
+ __r.then(function(v) {
1625
+ document.getElementById('${callId}').setAttribute('data-result', JSON.stringify({v:v}));
1626
+ }).catch(function(e) {
1627
+ document.getElementById('${callId}').setAttribute('data-error', e.message || String(e));
1628
+ });
1629
+ } else {
1630
+ document.getElementById('${callId}').setAttribute('data-result', JSON.stringify({v:__r}));
1631
+ }
1632
+ } catch(e) {
1633
+ document.getElementById('${callId}').setAttribute('data-error', e.message || String(e));
1634
+ }
1635
+ })();`;
1636
+ document.documentElement.appendChild(script);
1637
+ script.remove();
1638
+
1639
+ const pollForResult2 = (resolve) => {
1640
+ const el = document.getElementById(callId);
1641
+ if (!el) {
1642
+ resolve({ error: "Result element removed" });
1643
+ return;
1644
+ }
1645
+
1646
+ const harvest = () => {
1647
+ const resultAttr = el.getAttribute("data-result");
1648
+ if (resultAttr !== null) {
1649
+ el.remove();
1650
+ try {
1651
+ resolve({ result: JSON.parse(resultAttr).v });
1652
+ } catch {
1653
+ resolve({ result: resultAttr });
1654
+ }
1655
+ return true;
1656
+ }
1657
+ const errorAttr = el.getAttribute("data-error");
1658
+ if (errorAttr !== null) {
1659
+ el.remove();
1660
+ resolve({ error: errorAttr });
1661
+ return true;
1662
+ }
1663
+ return false;
1664
+ };
1665
+
1666
+ if (harvest()) return;
1667
+
1668
+ const observer = new MutationObserver(() => {
1669
+ if (harvest()) {
1670
+ observer.disconnect();
1671
+ clearTimeout(timer);
1672
+ }
1673
+ });
1674
+ observer.observe(el, {
1675
+ attributes: true,
1676
+ attributeFilter: ["data-result", "data-error"],
1677
+ });
1678
+
1679
+ const timer = setTimeout(() => {
1680
+ observer.disconnect();
1681
+ el.remove();
1682
+ resolve({ error: "Evaluate timed out" });
1683
+ }, 5000);
1684
+ };
1685
+
1686
+ new Promise(pollForResult2).then((res) => {
1687
+ if (res.error) sendResponse({ error: res.error });
1688
+ else sendResponse({ result: res.result });
1689
+ });
1690
+ return true; // async
1691
+ }
1692
+ case "dom.evaluateIsolated": {
1693
+ // CSP-safe evaluation in ISOLATED world (content script context)
1694
+ // Limited to DOM access, cannot access page JS globals
1695
+ try {
1696
+ const fn = new Function("return (" + params.fn + ")")();
1697
+ const result = fn.apply(null, params.args || []);
1698
+ sendResponse({ result });
1699
+ } catch (e) {
1700
+ sendResponse({ error: e.message });
1701
+ }
1702
+ return;
1703
+ }
1704
+ case "dom.discoverElements": {
1705
+ // CSP-safe element discovery — no eval needed
1706
+ const results = [];
1707
+ const seen = new Set();
1708
+ const vw = window.innerWidth;
1709
+ const vh = window.innerHeight;
1710
+
1711
+ // Helper: find a safe CSS class (no special chars like : [ ] ( ) )
1712
+ function safeClass(el) {
1713
+ for (const cls of el.classList) {
1714
+ if (/^[a-zA-Z_-][a-zA-Z0-9_-]*$/.test(cls)) return cls;
1715
+ }
1716
+ return null;
1717
+ }
1718
+
1719
+ // Links
1720
+ for (const el of document.querySelectorAll("a[href]")) {
1721
+ const href = el.href || "";
1722
+ const text = (el.textContent || "").trim().slice(0, 80);
1723
+ if (!text || seen.has(href + text)) continue;
1724
+ seen.add(href + text);
1725
+ const rect = el.getBoundingClientRect();
1726
+ const cs = getComputedStyle(el);
1727
+ if (
1728
+ rect.width <= 0 ||
1729
+ rect.height <= 0 ||
1730
+ cs.display === "none" ||
1731
+ cs.visibility === "hidden"
1732
+ )
1733
+ continue;
1734
+ const sc = safeClass(el);
1735
+ const rawHref = el.getAttribute("href") || "";
1736
+ const selector = el.id
1737
+ ? `a#${el.id}`
1738
+ : sc
1739
+ ? `a.${sc}`
1740
+ : rawHref
1741
+ ? `a[href="${rawHref.replace(/"/g, '\\"')}"]`
1742
+ : "a";
1743
+ results.push({
1744
+ type: "link",
1745
+ tag: "a",
1746
+ text,
1747
+ href,
1748
+ selector,
1749
+ handleId: storeHandle(el),
1750
+ rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
1751
+ });
1752
+ }
1753
+
1754
+ // Buttons
1755
+ for (const el of document.querySelectorAll('button, [role="button"]')) {
1756
+ const text = (el.textContent || "").trim().slice(0, 80);
1757
+ const rect = el.getBoundingClientRect();
1758
+ const cs = getComputedStyle(el);
1759
+ if (rect.width <= 0 || rect.height <= 0 || cs.display === "none")
1760
+ continue;
1761
+ const tag = el.tagName.toLowerCase();
1762
+ const sc = safeClass(el);
1763
+ const selector = el.id ? `#${el.id}` : sc ? `${tag}.${sc}` : tag;
1764
+ results.push({
1765
+ type: "button",
1766
+ tag,
1767
+ text,
1768
+ selector,
1769
+ handleId: storeHandle(el),
1770
+ rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
1771
+ });
1772
+ }
1773
+
1774
+ // Inputs / textareas
1775
+ for (const el of document.querySelectorAll(
1776
+ 'input, textarea, [contenteditable="true"]',
1777
+ )) {
1778
+ const rect = el.getBoundingClientRect();
1779
+ const cs = getComputedStyle(el);
1780
+ if (rect.width <= 0 || rect.height <= 0 || cs.display === "none")
1781
+ continue;
1782
+ const inputType = el.type || el.tagName.toLowerCase();
1783
+ if (
1784
+ ["hidden", "submit", "button", "image", "reset"].includes(inputType)
1785
+ )
1786
+ continue;
1787
+ const tag = el.tagName.toLowerCase();
1788
+ const selector = el.id
1789
+ ? `#${el.id}`
1790
+ : el.name
1791
+ ? `${tag}[name="${el.name}"]`
1792
+ : el.placeholder
1793
+ ? `${tag}[placeholder="${el.placeholder}"]`
1794
+ : tag;
1795
+ results.push({
1796
+ type: "input",
1797
+ tag,
1798
+ inputType,
1799
+ name: el.name || "",
1800
+ placeholder: el.placeholder || "",
1801
+ selector,
1802
+ handleId: storeHandle(el),
1803
+ rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
1804
+ });
1805
+ }
1806
+
1807
+ sendResponse({
1808
+ result: {
1809
+ elements: results,
1810
+ cursor: { x: cursorX, y: cursorY },
1811
+ viewport: { width: vw, height: vh },
1812
+ scrollY: window.scrollY,
1813
+ },
1814
+ });
1815
+ return;
1816
+ }
1817
+ case "dom.setDebug":
1818
+ debugMode = !!params.enabled;
1819
+ frameworkRuntime.debug.cursor = debugMode;
1820
+ if (!debugMode) clearTrail();
1821
+ sendResponse({ result: { debug: debugMode } });
1822
+ return;
1823
+
1824
+ // Human commands — safe, human-like actions with timing + detection
1825
+ case "human.click":
1826
+ actionHumanClick(params)
1827
+ .then((r) => sendResponse({ result: r }))
1828
+ .catch((e) => sendResponse({ error: e.message }));
1829
+ return true; // async
1830
+ case "human.type":
1831
+ actionHumanType(params)
1832
+ .then((r) => sendResponse({ result: r }))
1833
+ .catch((e) => sendResponse({ error: e.message }));
1834
+ return true; // async
1835
+ case "human.scroll":
1836
+ actionHumanScroll(params)
1837
+ .then((r) => sendResponse({ result: r }))
1838
+ .catch((e) => sendResponse({ error: e.message }));
1839
+ return true; // async
1840
+ case "human.clearInput":
1841
+ actionHumanClearInput(params)
1842
+ .then((r) => sendResponse({ result: r }))
1843
+ .catch((e) => sendResponse({ error: e.message }));
1844
+ return true; // async
1845
+
1846
+ default:
1847
+ sendResponse({ error: `Unknown action: ${action}` });
1848
+ }
1849
+ } catch (err) {
1850
+ sendResponse({ error: err.message });
1851
+ }
1852
+ });