@leejungkiin/awkit 1.4.0 → 1.4.3

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 (119) hide show
  1. package/bin/awk.js +458 -7
  2. package/bin/claude-generators.js +122 -0
  3. package/core/AGENTS.md +16 -0
  4. package/core/CLAUDE.md +155 -0
  5. package/core/GEMINI.md +44 -9
  6. package/package.json +1 -1
  7. package/skills/ai-sprite-maker/SKILL.md +81 -0
  8. package/skills/ai-sprite-maker/scripts/animate_sprite.py +102 -0
  9. package/skills/ai-sprite-maker/scripts/process_sprites.py +140 -0
  10. package/skills/code-review/SKILL.md +21 -33
  11. package/skills/lucylab-tts/SKILL.md +64 -0
  12. package/skills/lucylab-tts/resources/voices_library.json +908 -0
  13. package/skills/lucylab-tts/scripts/.env +1 -0
  14. package/skills/lucylab-tts/scripts/lucylab_tts.py +506 -0
  15. package/skills/orchestrator/SKILL.md +5 -0
  16. package/skills/short-maker/SKILL.md +150 -0
  17. package/skills/short-maker/_backup/storyboard.html +106 -0
  18. package/skills/short-maker/_backup/video_mixer.py +296 -0
  19. package/skills/short-maker/outputs/fitbite-promo/background.jpg +0 -0
  20. package/skills/short-maker/outputs/fitbite-promo/final/promo-final.mp4 +0 -0
  21. package/skills/short-maker/outputs/fitbite-promo/script.md +19 -0
  22. package/skills/short-maker/outputs/fitbite-promo/segments/scene-01.mp4 +0 -0
  23. package/skills/short-maker/outputs/fitbite-promo/segments/scene-02.mp4 +0 -0
  24. package/skills/short-maker/outputs/fitbite-promo/segments/scene-03.mp4 +0 -0
  25. package/skills/short-maker/outputs/fitbite-promo/segments/scene-04.mp4 +0 -0
  26. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-01.png +0 -0
  27. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-02.png +0 -0
  28. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-03.png +0 -0
  29. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-04.png +0 -0
  30. package/skills/short-maker/outputs/fitbite-promo/storyboard.html +133 -0
  31. package/skills/short-maker/outputs/fitbite-promo/storyboard.json +38 -0
  32. package/skills/short-maker/outputs/fitbite-promo/temp/merged_chroma.mp4 +0 -0
  33. package/skills/short-maker/outputs/fitbite-promo/temp/merged_crossfaded.mp4 +0 -0
  34. package/skills/short-maker/outputs/fitbite-promo/temp/ready_00.mp4 +0 -0
  35. package/skills/short-maker/outputs/fitbite-promo/temp/ready_01.mp4 +0 -0
  36. package/skills/short-maker/outputs/fitbite-promo/temp/ready_02.mp4 +0 -0
  37. package/skills/short-maker/outputs/fitbite-promo/temp/ready_03.mp4 +0 -0
  38. package/skills/short-maker/outputs/fitbite-promo/tts/manifest.json +31 -0
  39. package/skills/short-maker/outputs/fitbite-promo/tts/scene-01.wav +0 -0
  40. package/skills/short-maker/outputs/fitbite-promo/tts/scene-02.wav +0 -0
  41. package/skills/short-maker/outputs/fitbite-promo/tts/scene-03.wav +0 -0
  42. package/skills/short-maker/outputs/fitbite-promo/tts/scene-04.wav +0 -0
  43. package/skills/short-maker/outputs/fitbite-promo/tts_script.txt +11 -0
  44. package/skills/short-maker/scripts/google-flow-cli/.project-identity +41 -0
  45. package/skills/short-maker/scripts/google-flow-cli/.trae/rules/project_rules.md +52 -0
  46. package/skills/short-maker/scripts/google-flow-cli/CODEBASE.md +67 -0
  47. package/skills/short-maker/scripts/google-flow-cli/GoogleFlowCli.code-workspace +29 -0
  48. package/skills/short-maker/scripts/google-flow-cli/README.md +168 -0
  49. package/skills/short-maker/scripts/google-flow-cli/docs/specs/PROJECT.md +12 -0
  50. package/skills/short-maker/scripts/google-flow-cli/docs/specs/REQUIREMENTS.md +22 -0
  51. package/skills/short-maker/scripts/google-flow-cli/docs/specs/ROADMAP.md +16 -0
  52. package/skills/short-maker/scripts/google-flow-cli/docs/specs/TECH-SPEC.md +13 -0
  53. package/skills/short-maker/scripts/google-flow-cli/gflow/__init__.py +3 -0
  54. package/skills/short-maker/scripts/google-flow-cli/gflow/api/__init__.py +19 -0
  55. package/skills/short-maker/scripts/google-flow-cli/gflow/api/client.py +1921 -0
  56. package/skills/short-maker/scripts/google-flow-cli/gflow/api/models.py +64 -0
  57. package/skills/short-maker/scripts/google-flow-cli/gflow/api/rpc_ids.py +98 -0
  58. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/__init__.py +15 -0
  59. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/browser_auth.py +692 -0
  60. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/humanizer.py +417 -0
  61. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/proxy_ext.py +120 -0
  62. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/recaptcha.py +482 -0
  63. package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/__init__.py +5 -0
  64. package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/client.py +414 -0
  65. package/skills/short-maker/scripts/google-flow-cli/gflow/cli/__init__.py +1 -0
  66. package/skills/short-maker/scripts/google-flow-cli/gflow/cli/main.py +1075 -0
  67. package/skills/short-maker/scripts/google-flow-cli/pyproject.toml +36 -0
  68. package/skills/short-maker/scripts/google-flow-cli/script.txt +22 -0
  69. package/skills/short-maker/scripts/google-flow-cli/tests/__init__.py +0 -0
  70. package/skills/short-maker/scripts/google-flow-cli/tests/test_batchexecute.py +113 -0
  71. package/skills/short-maker/scripts/google-flow-cli/tests/test_client.py +190 -0
  72. package/skills/short-maker/templates/aida_script.md +40 -0
  73. package/skills/short-maker/templates/mimic_analyzer.md +29 -0
  74. package/skills/single-flow-task-execution/SKILL.md +9 -6
  75. package/skills/skill-creator/SKILL.md +44 -0
  76. package/skills/spm-build-analysis/SKILL.md +92 -0
  77. package/skills/spm-build-analysis/references/build-optimization-sources.md +155 -0
  78. package/skills/spm-build-analysis/references/recommendation-format.md +85 -0
  79. package/skills/spm-build-analysis/references/spm-analysis-checks.md +105 -0
  80. package/skills/spm-build-analysis/scripts/check_spm_pins.py +118 -0
  81. package/skills/symphony-enforcer/SKILL.md +51 -83
  82. package/skills/symphony-orchestrator/SKILL.md +1 -1
  83. package/skills/trello-sync/SKILL.md +27 -28
  84. package/skills/verification-gate/SKILL.md +13 -2
  85. package/skills/xcode-build-benchmark/SKILL.md +88 -0
  86. package/skills/xcode-build-benchmark/references/benchmark-artifacts.md +94 -0
  87. package/skills/xcode-build-benchmark/references/benchmarking-workflow.md +67 -0
  88. package/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json +230 -0
  89. package/skills/xcode-build-benchmark/scripts/benchmark_builds.py +308 -0
  90. package/skills/xcode-build-fixer/SKILL.md +218 -0
  91. package/skills/xcode-build-fixer/references/build-settings-best-practices.md +216 -0
  92. package/skills/xcode-build-fixer/references/fix-patterns.md +290 -0
  93. package/skills/xcode-build-fixer/references/recommendation-format.md +85 -0
  94. package/skills/xcode-build-fixer/scripts/benchmark_builds.py +308 -0
  95. package/skills/xcode-build-orchestrator/SKILL.md +156 -0
  96. package/skills/xcode-build-orchestrator/references/benchmark-artifacts.md +94 -0
  97. package/skills/xcode-build-orchestrator/references/build-settings-best-practices.md +216 -0
  98. package/skills/xcode-build-orchestrator/references/orchestration-report-template.md +143 -0
  99. package/skills/xcode-build-orchestrator/references/recommendation-format.md +85 -0
  100. package/skills/xcode-build-orchestrator/scripts/benchmark_builds.py +308 -0
  101. package/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py +273 -0
  102. package/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py +533 -0
  103. package/skills/xcode-compilation-analyzer/SKILL.md +89 -0
  104. package/skills/xcode-compilation-analyzer/references/build-optimization-sources.md +155 -0
  105. package/skills/xcode-compilation-analyzer/references/code-compilation-checks.md +106 -0
  106. package/skills/xcode-compilation-analyzer/references/recommendation-format.md +85 -0
  107. package/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py +273 -0
  108. package/skills/xcode-project-analyzer/SKILL.md +76 -0
  109. package/skills/xcode-project-analyzer/references/build-optimization-sources.md +155 -0
  110. package/skills/xcode-project-analyzer/references/build-settings-best-practices.md +216 -0
  111. package/skills/xcode-project-analyzer/references/project-audit-checks.md +101 -0
  112. package/skills/xcode-project-analyzer/references/recommendation-format.md +85 -0
  113. package/templates/project-identity/android.json +0 -10
  114. package/templates/project-identity/backend-nestjs.json +0 -10
  115. package/templates/project-identity/expo.json +0 -10
  116. package/templates/project-identity/ios.json +0 -10
  117. package/templates/project-identity/web-nextjs.json +0 -10
  118. package/workflows/_uncategorized/ship-to-code.md +85 -0
  119. package/workflows/context/codebase-sync.md +10 -87
@@ -0,0 +1,417 @@
1
+ """
2
+ Human-like browser interaction via Chrome DevTools Protocol (CDP).
3
+
4
+ Adapted from a Playwright-based humanizer to work over raw CDP WebSocket.
5
+ Provides Bezier-curved mouse movement, gradual scrolling with reading pace,
6
+ and realistic timing — all via Input.dispatchMouseEvent / Input.dispatchKeyEvent.
7
+
8
+ reCAPTCHA Enterprise v3 tracks real DOM-level input events, not just JS
9
+ dispatched events. CDP Input.dispatch* fires at the browser engine level,
10
+ identical to actual hardware input, so reCAPTCHA scores them highly.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import math
16
+ import random
17
+ import time
18
+ import logging
19
+ from typing import Tuple, List, Optional, Callable
20
+
21
+ logger = logging.getLogger("gflow.humanizer")
22
+
23
+ # ── Types ──
24
+ Point = Tuple[float, float]
25
+
26
+
27
+ # ═══════════════════════════════════════════════════════════════════
28
+ # Timing — session state machine + calibrated delay distributions
29
+ # ═══════════════════════════════════════════════════════════════════
30
+
31
+ class HumanTiming:
32
+ """Delay distributions calibrated against real human interaction data."""
33
+
34
+ def __init__(self, speed_multiplier: float = 1.0):
35
+ self.speed_multiplier = speed_multiplier
36
+
37
+ def _scale(self, ms: float) -> float:
38
+ return max(10, ms * self.speed_multiplier)
39
+
40
+ def _gaussian(self, mean: float, stddev: float, min_val: float = 0) -> float:
41
+ return max(min_val, random.gauss(mean, stddev))
42
+
43
+ # ── Specific delay types (return seconds) ──
44
+
45
+ def pre_click_delay(self) -> float:
46
+ return self._scale(self._gaussian(120, 40, 50)) / 1000
47
+
48
+ def click_hold_duration(self) -> float:
49
+ return self._scale(self._gaussian(80, 20, 40)) / 1000
50
+
51
+ def post_click_delay(self) -> float:
52
+ return self._scale(self._gaussian(200, 80, 80)) / 1000
53
+
54
+ def between_actions_delay(self) -> float:
55
+ return self._scale(self._gaussian(400, 150, 150)) / 1000
56
+
57
+ def scroll_tick_delay(self) -> float:
58
+ return self._scale(self._gaussian(55, 15, 20)) / 1000
59
+
60
+ def scroll_reading_pause(self) -> float:
61
+ return self._scale(self._gaussian(1200, 400, 300)) / 1000
62
+
63
+
64
+ # ═══════════════════════════════════════════════════════════════════
65
+ # Bezier math — same algorithms as the Playwright humanizer
66
+ # ═══════════════════════════════════════════════════════════════════
67
+
68
+ def _bezier_point(t: float, points: List[Point]) -> Point:
69
+ """De Casteljau evaluation of an arbitrary-degree Bezier curve."""
70
+ pts = list(points)
71
+ n = len(pts)
72
+ for r in range(1, n):
73
+ for i in range(n - r):
74
+ pts[i] = (
75
+ (1 - t) * pts[i][0] + t * pts[i + 1][0],
76
+ (1 - t) * pts[i][1] + t * pts[i + 1][1],
77
+ )
78
+ return pts[0]
79
+
80
+
81
+ def _distance(a: Point, b: Point) -> float:
82
+ return math.hypot(b[0] - a[0], b[1] - a[1])
83
+
84
+
85
+ def _fitts_time(distance: float, target_width: float = 50) -> float:
86
+ """Fitts's Law: movement time in ms."""
87
+ a, b = 50, 150
88
+ w = max(target_width, 1)
89
+ return a + b * math.log2(distance / w + 1)
90
+
91
+
92
+ def _generate_control_points(start: Point, end: Point, num_points: int = 2) -> List[Point]:
93
+ dx = end[0] - start[0]
94
+ dy = end[1] - start[1]
95
+ dist = math.hypot(dx, dy)
96
+ angle = math.atan2(dy, dx)
97
+ spread = dist * random.uniform(0.15, 0.4)
98
+
99
+ controls = []
100
+ for i in range(num_points):
101
+ t = (i + 1) / (num_points + 1) + random.uniform(-0.1, 0.1)
102
+ t = max(0.1, min(0.9, t))
103
+ offset = random.gauss(0, spread * 0.4)
104
+ cx = start[0] + dx * t + math.cos(angle + math.pi / 2) * offset
105
+ cy = start[1] + dy * t + math.sin(angle + math.pi / 2) * offset
106
+ controls.append((cx, cy))
107
+ return controls
108
+
109
+
110
+ def _generate_path(start: Point, end: Point, steps: int = 50) -> List[Point]:
111
+ num_controls = random.choice([2, 2, 3])
112
+ controls = _generate_control_points(start, end, num_controls)
113
+ all_points = [start] + controls + [end]
114
+ return [_bezier_point(i / steps, all_points) for i in range(steps + 1)]
115
+
116
+
117
+ def _add_jitter(path: List[Point], intensity: float = 1.2) -> List[Point]:
118
+ jittered = []
119
+ for i, (x, y) in enumerate(path):
120
+ edge_factor = 0.3 if (i < 5 or i > len(path) - 5) else 1.0
121
+ jx = x + random.gauss(0, intensity * edge_factor)
122
+ jy = y + random.gauss(0, intensity * edge_factor)
123
+ jittered.append((jx, jy))
124
+ jittered[0] = path[0]
125
+ jittered[-1] = path[-1]
126
+ return jittered
127
+
128
+
129
+ def _generate_step_delays(num_steps: int, total_time_ms: float) -> List[float]:
130
+ """Sine-based easing: slow at start/end, fast in middle."""
131
+ raw = [math.sin(math.pi * i / num_steps) + 0.3 for i in range(num_steps)]
132
+ total_raw = sum(raw)
133
+ delays = [(r / total_raw) * total_time_ms for r in raw]
134
+ return [max(3, d + random.gauss(0, d * 0.15)) for d in delays]
135
+
136
+
137
+ # ═══════════════════════════════════════════════════════════════════
138
+ # CDP Humanizer — ties math to actual CDP Input.dispatch* commands
139
+ # ═══════════════════════════════════════════════════════════════════
140
+
141
+ class CDPHumanizer:
142
+ """
143
+ Human-like mouse/scroll controller that sends real input events
144
+ through Chrome DevTools Protocol.
145
+
146
+ Usage:
147
+ humanizer = CDPHumanizer(cdp_send_fn)
148
+ humanizer.move_mouse(500, 300)
149
+ humanizer.click(500, 300)
150
+ humanizer.scroll_down(400)
151
+ humanizer.full_warmup() # Run a complete warm-up sequence
152
+ """
153
+
154
+ def __init__(self, cdp_send: Callable, timing: Optional[HumanTiming] = None):
155
+ """
156
+ Args:
157
+ cdp_send: Function that sends CDP commands. Signature:
158
+ cdp_send(method: str, params: dict) -> dict
159
+ timing: HumanTiming instance (created with defaults if None)
160
+ """
161
+ self._cdp = cdp_send
162
+ self.timing = timing or HumanTiming()
163
+ # Start mouse at a random realistic position
164
+ self.mouse_x = random.uniform(300, 700)
165
+ self.mouse_y = random.uniform(200, 400)
166
+
167
+ # ── Low-level CDP dispatchers ──
168
+
169
+ def _dispatch_mouse(self, event_type: str, x: float, y: float,
170
+ button: str = "none", click_count: int = 0) -> None:
171
+ """Send Input.dispatchMouseEvent via CDP."""
172
+ self._cdp("Input.dispatchMouseEvent", {
173
+ "type": event_type,
174
+ "x": round(x, 2),
175
+ "y": round(y, 2),
176
+ "button": button,
177
+ "clickCount": click_count,
178
+ "timestamp": time.time(),
179
+ })
180
+
181
+ def _dispatch_mouse_wheel(self, x: float, y: float,
182
+ delta_x: float = 0, delta_y: float = 0) -> None:
183
+ """Send a mouse wheel event via CDP."""
184
+ self._cdp("Input.dispatchMouseEvent", {
185
+ "type": "mouseWheel",
186
+ "x": round(x, 2),
187
+ "y": round(y, 2),
188
+ "deltaX": round(delta_x),
189
+ "deltaY": round(delta_y),
190
+ "timestamp": time.time(),
191
+ })
192
+
193
+ # ── Mouse movement ──
194
+
195
+ def move_mouse(self, target_x: float, target_y: float,
196
+ target_width: float = 50) -> None:
197
+ """
198
+ Move mouse to (target_x, target_y) along a Bezier curve
199
+ with Fitts's Law timing and micro-jitter.
200
+ """
201
+ start = (self.mouse_x, self.mouse_y)
202
+ end = (target_x, target_y)
203
+ dist = _distance(start, end)
204
+
205
+ if dist < 2:
206
+ return # Already there
207
+
208
+ # Calculate movement time via Fitts's Law
209
+ total_time_ms = _fitts_time(dist, target_width) * self.timing.speed_multiplier
210
+
211
+ # More steps for longer distances
212
+ steps = max(15, min(80, int(dist / 8)))
213
+
214
+ # Generate curved path with jitter
215
+ path = _generate_path(start, end, steps)
216
+ path = _add_jitter(path, intensity=random.uniform(0.8, 1.5))
217
+
218
+ # Generate per-step timing (slow-fast-slow)
219
+ delays = _generate_step_delays(len(path) - 1, total_time_ms)
220
+
221
+ # Execute movement
222
+ for i in range(1, len(path)):
223
+ px, py = path[i]
224
+ self._dispatch_mouse("mouseMoved", px, py)
225
+ time.sleep(delays[i - 1] / 1000)
226
+
227
+ self.mouse_x = target_x
228
+ self.mouse_y = target_y
229
+
230
+ def click(self, x: float, y: float, target_width: float = 50) -> None:
231
+ """Move to position and click with human timing."""
232
+ self.move_mouse(x, y, target_width)
233
+
234
+ # Overshoot and correct (~20% chance)
235
+ if random.random() < 0.20:
236
+ overshoot_dist = random.uniform(5, 15)
237
+ angle = random.uniform(0, 2 * math.pi)
238
+ ox = x + math.cos(angle) * overshoot_dist
239
+ oy = y + math.sin(angle) * overshoot_dist
240
+
241
+ self._dispatch_mouse("mouseMoved", ox, oy)
242
+ time.sleep(random.uniform(0.05, 0.12))
243
+
244
+ # Correct back
245
+ correction_path = _generate_path((ox, oy), (x, y), random.randint(5, 10))
246
+ for px, py in correction_path[1:]:
247
+ self._dispatch_mouse("mouseMoved", px, py)
248
+ time.sleep(random.uniform(0.005, 0.015))
249
+
250
+ # Pre-click pause
251
+ time.sleep(self.timing.pre_click_delay())
252
+
253
+ # Mouse down
254
+ self._dispatch_mouse("mousePressed", x, y, button="left", click_count=1)
255
+ time.sleep(self.timing.click_hold_duration())
256
+
257
+ # Mouse up
258
+ self._dispatch_mouse("mouseReleased", x, y, button="left", click_count=1)
259
+
260
+ # Post-click wait
261
+ time.sleep(self.timing.post_click_delay())
262
+
263
+ self.mouse_x = x
264
+ self.mouse_y = y
265
+
266
+ # ── Scrolling ──
267
+
268
+ def scroll_down(self, pixels: int) -> None:
269
+ """Scroll down gradually in bursts, like a real human."""
270
+ scrolled = 0
271
+ target = pixels + random.randint(-30, 30)
272
+ mx = self.mouse_x
273
+ my = self.mouse_y
274
+
275
+ while scrolled < target:
276
+ tick_amount = random.randint(50, 150)
277
+ tick_amount = min(tick_amount, target - scrolled + random.randint(0, 30))
278
+
279
+ burst_ticks = random.randint(2, 5)
280
+ for _ in range(burst_ticks):
281
+ if scrolled >= target:
282
+ break
283
+ self._dispatch_mouse_wheel(mx, my, delta_y=tick_amount)
284
+ scrolled += tick_amount
285
+ time.sleep(self.timing.scroll_tick_delay())
286
+
287
+ # Reading pause ~30% of the time
288
+ if random.random() < 0.30:
289
+ time.sleep(self.timing.scroll_reading_pause())
290
+ else:
291
+ time.sleep(random.uniform(0.1, 0.4) * self.timing.speed_multiplier)
292
+
293
+ # Overshoot and correct (~25% chance)
294
+ if random.random() < 0.25:
295
+ overshoot = random.randint(50, 150)
296
+ self._dispatch_mouse_wheel(mx, my, delta_y=overshoot)
297
+ time.sleep(random.uniform(0.2, 0.5))
298
+ # Scroll back
299
+ ticks = random.randint(2, 3)
300
+ per_tick = overshoot // ticks
301
+ for _ in range(ticks):
302
+ self._dispatch_mouse_wheel(mx, my, delta_y=-per_tick)
303
+ time.sleep(self.timing.scroll_tick_delay())
304
+
305
+ def scroll_up(self, pixels: int) -> None:
306
+ """Scroll up gradually."""
307
+ scrolled = 0
308
+ target = pixels + random.randint(-20, 20)
309
+ mx = self.mouse_x
310
+ my = self.mouse_y
311
+
312
+ while scrolled < target:
313
+ tick_amount = random.randint(50, 120)
314
+ tick_amount = min(tick_amount, target - scrolled + random.randint(0, 20))
315
+
316
+ burst_ticks = random.randint(2, 4)
317
+ for _ in range(burst_ticks):
318
+ if scrolled >= target:
319
+ break
320
+ self._dispatch_mouse_wheel(mx, my, delta_y=-tick_amount)
321
+ scrolled += tick_amount
322
+ time.sleep(self.timing.scroll_tick_delay())
323
+
324
+ if random.random() < 0.25:
325
+ time.sleep(self.timing.scroll_reading_pause())
326
+ else:
327
+ time.sleep(random.uniform(0.1, 0.3) * self.timing.speed_multiplier)
328
+
329
+ # ── Idle fidgeting ──
330
+
331
+ def idle_movement(self, duration: float = 3.0) -> None:
332
+ """Small random mouse movements simulating fidgeting while reading."""
333
+ end_time = time.time() + duration
334
+ while time.time() < end_time:
335
+ dx = random.gauss(0, 40)
336
+ dy = random.gauss(0, 30)
337
+ target_x = max(50, min(1400, self.mouse_x + dx))
338
+ target_y = max(50, min(800, self.mouse_y + dy))
339
+
340
+ # Small movement (fewer steps for idle)
341
+ path = _generate_path(
342
+ (self.mouse_x, self.mouse_y),
343
+ (target_x, target_y),
344
+ random.randint(5, 12),
345
+ )
346
+ for px, py in path[1:]:
347
+ self._dispatch_mouse("mouseMoved", px, py)
348
+ time.sleep(random.uniform(0.01, 0.03))
349
+
350
+ self.mouse_x = target_x
351
+ self.mouse_y = target_y
352
+ time.sleep(random.uniform(0.8, 2.5))
353
+
354
+ # ── Full warm-up sequence ──
355
+
356
+ def full_warmup(self, duration: float = 12.0) -> None:
357
+ """
358
+ Run a complete warm-up that convincingly mimics a real user
359
+ browsing the Flow page for several seconds.
360
+
361
+ Sequence:
362
+ 1. Idle mouse movements (user landed on page, looking around)
363
+ 2. Scroll down to explore content
364
+ 3. Move mouse to various page elements
365
+ 4. Click on something benign
366
+ 5. Scroll back up
367
+ 6. More idle movement
368
+
369
+ This builds enough behavioral signal for reCAPTCHA to assign
370
+ a good trust score.
371
+ """
372
+ logger.info("Running human-like warm-up (%ds)...", int(duration))
373
+ start = time.time()
374
+
375
+ # Phase 1: Initial idle — user just loaded the page, eyes scanning
376
+ self.idle_movement(duration=min(2.5, duration * 0.2))
377
+
378
+ if time.time() - start > duration:
379
+ return
380
+
381
+ # Phase 2: Scroll down to explore
382
+ self.scroll_down(random.randint(200, 500))
383
+ time.sleep(random.uniform(0.5, 1.5))
384
+
385
+ if time.time() - start > duration:
386
+ return
387
+
388
+ # Phase 3: Move mouse to a few random positions (reading content)
389
+ for _ in range(random.randint(3, 5)):
390
+ target_x = random.uniform(200, 900)
391
+ target_y = random.uniform(150, 600)
392
+ self.move_mouse(target_x, target_y)
393
+ time.sleep(random.uniform(0.3, 1.0))
394
+ if time.time() - start > duration:
395
+ return
396
+
397
+ # Phase 4: Click somewhere benign (body area, not a link)
398
+ click_x = random.uniform(400, 800)
399
+ click_y = random.uniform(300, 500)
400
+ self.click(click_x, click_y)
401
+
402
+ if time.time() - start > duration:
403
+ return
404
+
405
+ # Phase 5: Scroll back up
406
+ self.scroll_up(random.randint(100, 300))
407
+ time.sleep(random.uniform(0.3, 0.8))
408
+
409
+ if time.time() - start > duration:
410
+ return
411
+
412
+ # Phase 6: More idle movement
413
+ remaining = max(0.5, duration - (time.time() - start))
414
+ self.idle_movement(duration=min(remaining, 3.0))
415
+
416
+ elapsed = time.time() - start
417
+ logger.info("Human warm-up complete (%.1fs)", elapsed)
@@ -0,0 +1,120 @@
1
+ """
2
+ Generate a tiny Chrome extension that handles proxy authentication.
3
+
4
+ Chrome's --proxy-server flag doesn't support user:pass authentication.
5
+ This module creates a minimal Manifest V3 extension that:
6
+ 1. Sets Chrome to use the configured proxy via chrome.proxy API
7
+ 2. Handles the 407 auth challenge automatically via onAuthRequired
8
+
9
+ The extension is written to ~/.gflow/proxy-ext/ and loaded via
10
+ --load-extension when Chrome launches.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+ from pathlib import Path
18
+
19
+ logger = logging.getLogger("gflow.auth.proxy")
20
+
21
+ EXT_DIR = Path.home() / ".gflow" / "proxy-ext"
22
+
23
+
24
+ def create_proxy_extension(host: str, port: int, username: str, password: str,
25
+ scheme: str = "http") -> str:
26
+ """Create a Chrome proxy auth extension and return its path.
27
+
28
+ Args:
29
+ host: Proxy hostname
30
+ port: Proxy port
31
+ username: Proxy username
32
+ password: Proxy password
33
+ scheme: Proxy scheme (http or socks5)
34
+
35
+ Returns:
36
+ Path to the extension directory (for --load-extension)
37
+ """
38
+ EXT_DIR.mkdir(parents=True, exist_ok=True)
39
+
40
+ # Manifest V3
41
+ manifest = {
42
+ "version": "1.0.0",
43
+ "manifest_version": 3,
44
+ "name": "Proxy Auth",
45
+ "permissions": ["proxy", "webRequest", "webRequestAuthProvider"],
46
+ "host_permissions": ["<all_urls>"],
47
+ "background": {
48
+ "service_worker": "background.js"
49
+ }
50
+ }
51
+
52
+ # Background service worker
53
+ background_js = f"""
54
+ // Set proxy configuration
55
+ chrome.proxy.settings.set({{
56
+ value: {{
57
+ mode: "fixed_servers",
58
+ rules: {{
59
+ singleProxy: {{
60
+ scheme: "{scheme}",
61
+ host: "{host}",
62
+ port: {port}
63
+ }},
64
+ bypassList: ["localhost", "127.0.0.1"]
65
+ }}
66
+ }},
67
+ scope: "regular"
68
+ }});
69
+
70
+ // Handle proxy authentication
71
+ chrome.webRequest.onAuthRequired.addListener(
72
+ function(details) {{
73
+ return {{
74
+ authCredentials: {{
75
+ username: "{username}",
76
+ password: "{password}"
77
+ }}
78
+ }};
79
+ }},
80
+ {{ urls: ["<all_urls>"] }},
81
+ ["blocking"]
82
+ );
83
+ """
84
+
85
+ (EXT_DIR / "manifest.json").write_text(json.dumps(manifest, indent=2))
86
+ (EXT_DIR / "background.js").write_text(background_js)
87
+
88
+ logger.info("Proxy extension created at %s (-> %s:%d)", EXT_DIR, host, port)
89
+ return str(EXT_DIR)
90
+
91
+
92
+ def get_chrome_proxy_args() -> list[str]:
93
+ """Return Chrome CLI args to route all traffic through the residential proxy.
94
+
95
+ Returns empty list if no proxy is configured.
96
+ Uses the first proxy from ~/.gflow/proxies.txt (sticky session).
97
+ """
98
+ try:
99
+ from gflow.api.client import get_active_proxy, parse_proxy_url
100
+ except ImportError:
101
+ return []
102
+
103
+ proxy_url = get_active_proxy()
104
+ if not proxy_url:
105
+ return []
106
+
107
+ info = parse_proxy_url(proxy_url)
108
+ if not info["host"]:
109
+ return []
110
+
111
+ # Create proxy auth extension
112
+ ext_path = create_proxy_extension(
113
+ host=info["host"],
114
+ port=info["port"],
115
+ username=info["username"],
116
+ password=info["password"],
117
+ scheme=info["scheme"],
118
+ )
119
+
120
+ return [f"--load-extension={ext_path}"]