@leejungkiin/awkit 1.3.8 → 1.4.2

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 (135) hide show
  1. package/bin/awk.js +630 -52
  2. package/bin/claude-generators.js +122 -0
  3. package/core/AGENTS.md +54 -0
  4. package/core/CLAUDE.md +155 -0
  5. package/core/GEMINI.md +44 -9
  6. package/core/GEMINI.md.bak +126 -199
  7. package/package.json +1 -1
  8. package/skills/ai-sprite-maker/SKILL.md +81 -0
  9. package/skills/ai-sprite-maker/scripts/animate_sprite.py +102 -0
  10. package/skills/ai-sprite-maker/scripts/process_sprites.py +140 -0
  11. package/skills/awf-session-restore/SKILL.md +12 -2
  12. package/skills/brainstorm-agent/SKILL.md +11 -8
  13. package/skills/code-review/SKILL.md +21 -33
  14. package/skills/gitnexus/gitnexus-cli/SKILL.md +82 -0
  15. package/skills/gitnexus/gitnexus-debugging/SKILL.md +89 -0
  16. package/skills/gitnexus/gitnexus-exploring/SKILL.md +78 -0
  17. package/skills/gitnexus/gitnexus-guide/SKILL.md +64 -0
  18. package/skills/gitnexus/gitnexus-impact-analysis/SKILL.md +97 -0
  19. package/skills/gitnexus/gitnexus-refactoring/SKILL.md +121 -0
  20. package/skills/lucylab-tts/SKILL.md +64 -0
  21. package/skills/lucylab-tts/resources/voices_library.json +908 -0
  22. package/skills/lucylab-tts/scripts/.env +1 -0
  23. package/skills/lucylab-tts/scripts/lucylab_tts.py +506 -0
  24. package/skills/nm-memory-sync/SKILL.md +14 -1
  25. package/skills/orchestrator/SKILL.md +5 -38
  26. package/skills/ship-to-code/SKILL.md +115 -0
  27. package/skills/short-maker/SKILL.md +150 -0
  28. package/skills/short-maker/_backup/storyboard.html +106 -0
  29. package/skills/short-maker/_backup/video_mixer.py +296 -0
  30. package/skills/short-maker/outputs/fitbite-promo/background.jpg +0 -0
  31. package/skills/short-maker/outputs/fitbite-promo/final/promo-final.mp4 +0 -0
  32. package/skills/short-maker/outputs/fitbite-promo/script.md +19 -0
  33. package/skills/short-maker/outputs/fitbite-promo/segments/scene-01.mp4 +0 -0
  34. package/skills/short-maker/outputs/fitbite-promo/segments/scene-02.mp4 +0 -0
  35. package/skills/short-maker/outputs/fitbite-promo/segments/scene-03.mp4 +0 -0
  36. package/skills/short-maker/outputs/fitbite-promo/segments/scene-04.mp4 +0 -0
  37. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-01.png +0 -0
  38. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-02.png +0 -0
  39. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-03.png +0 -0
  40. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-04.png +0 -0
  41. package/skills/short-maker/outputs/fitbite-promo/storyboard.html +133 -0
  42. package/skills/short-maker/outputs/fitbite-promo/storyboard.json +38 -0
  43. package/skills/short-maker/outputs/fitbite-promo/temp/merged_chroma.mp4 +0 -0
  44. package/skills/short-maker/outputs/fitbite-promo/temp/merged_crossfaded.mp4 +0 -0
  45. package/skills/short-maker/outputs/fitbite-promo/temp/ready_00.mp4 +0 -0
  46. package/skills/short-maker/outputs/fitbite-promo/temp/ready_01.mp4 +0 -0
  47. package/skills/short-maker/outputs/fitbite-promo/temp/ready_02.mp4 +0 -0
  48. package/skills/short-maker/outputs/fitbite-promo/temp/ready_03.mp4 +0 -0
  49. package/skills/short-maker/outputs/fitbite-promo/tts/manifest.json +31 -0
  50. package/skills/short-maker/outputs/fitbite-promo/tts/scene-01.wav +0 -0
  51. package/skills/short-maker/outputs/fitbite-promo/tts/scene-02.wav +0 -0
  52. package/skills/short-maker/outputs/fitbite-promo/tts/scene-03.wav +0 -0
  53. package/skills/short-maker/outputs/fitbite-promo/tts/scene-04.wav +0 -0
  54. package/skills/short-maker/outputs/fitbite-promo/tts_script.txt +11 -0
  55. package/skills/short-maker/scripts/google-flow-cli/.project-identity +41 -0
  56. package/skills/short-maker/scripts/google-flow-cli/.trae/rules/project_rules.md +52 -0
  57. package/skills/short-maker/scripts/google-flow-cli/CODEBASE.md +67 -0
  58. package/skills/short-maker/scripts/google-flow-cli/GoogleFlowCli.code-workspace +29 -0
  59. package/skills/short-maker/scripts/google-flow-cli/README.md +168 -0
  60. package/skills/short-maker/scripts/google-flow-cli/docs/specs/PROJECT.md +12 -0
  61. package/skills/short-maker/scripts/google-flow-cli/docs/specs/REQUIREMENTS.md +22 -0
  62. package/skills/short-maker/scripts/google-flow-cli/docs/specs/ROADMAP.md +16 -0
  63. package/skills/short-maker/scripts/google-flow-cli/docs/specs/TECH-SPEC.md +13 -0
  64. package/skills/short-maker/scripts/google-flow-cli/gflow/__init__.py +3 -0
  65. package/skills/short-maker/scripts/google-flow-cli/gflow/api/__init__.py +19 -0
  66. package/skills/short-maker/scripts/google-flow-cli/gflow/api/client.py +1921 -0
  67. package/skills/short-maker/scripts/google-flow-cli/gflow/api/models.py +64 -0
  68. package/skills/short-maker/scripts/google-flow-cli/gflow/api/rpc_ids.py +98 -0
  69. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/__init__.py +15 -0
  70. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/browser_auth.py +692 -0
  71. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/humanizer.py +417 -0
  72. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/proxy_ext.py +120 -0
  73. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/recaptcha.py +482 -0
  74. package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/__init__.py +5 -0
  75. package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/client.py +414 -0
  76. package/skills/short-maker/scripts/google-flow-cli/gflow/cli/__init__.py +1 -0
  77. package/skills/short-maker/scripts/google-flow-cli/gflow/cli/main.py +1075 -0
  78. package/skills/short-maker/scripts/google-flow-cli/pyproject.toml +36 -0
  79. package/skills/short-maker/scripts/google-flow-cli/script.txt +22 -0
  80. package/skills/short-maker/scripts/google-flow-cli/tests/__init__.py +0 -0
  81. package/skills/short-maker/scripts/google-flow-cli/tests/test_batchexecute.py +113 -0
  82. package/skills/short-maker/scripts/google-flow-cli/tests/test_client.py +190 -0
  83. package/skills/short-maker/templates/aida_script.md +40 -0
  84. package/skills/short-maker/templates/mimic_analyzer.md +29 -0
  85. package/skills/single-flow-task-execution/SKILL.md +412 -0
  86. package/skills/single-flow-task-execution/code-quality-reviewer-prompt.md +20 -0
  87. package/skills/single-flow-task-execution/implementer-prompt.md +78 -0
  88. package/skills/single-flow-task-execution/spec-reviewer-prompt.md +61 -0
  89. package/skills/skill-creator/SKILL.md +44 -0
  90. package/skills/spm-build-analysis/SKILL.md +92 -0
  91. package/skills/spm-build-analysis/references/build-optimization-sources.md +155 -0
  92. package/skills/spm-build-analysis/references/recommendation-format.md +85 -0
  93. package/skills/spm-build-analysis/references/spm-analysis-checks.md +105 -0
  94. package/skills/spm-build-analysis/scripts/check_spm_pins.py +118 -0
  95. package/skills/symphony-enforcer/SKILL.md +83 -97
  96. package/skills/symphony-orchestrator/SKILL.md +1 -1
  97. package/skills/trello-sync/SKILL.md +52 -45
  98. package/skills/verification-gate/SKILL.md +13 -2
  99. package/skills/xcode-build-benchmark/SKILL.md +88 -0
  100. package/skills/xcode-build-benchmark/references/benchmark-artifacts.md +94 -0
  101. package/skills/xcode-build-benchmark/references/benchmarking-workflow.md +67 -0
  102. package/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json +230 -0
  103. package/skills/xcode-build-benchmark/scripts/benchmark_builds.py +308 -0
  104. package/skills/xcode-build-fixer/SKILL.md +218 -0
  105. package/skills/xcode-build-fixer/references/build-settings-best-practices.md +216 -0
  106. package/skills/xcode-build-fixer/references/fix-patterns.md +290 -0
  107. package/skills/xcode-build-fixer/references/recommendation-format.md +85 -0
  108. package/skills/xcode-build-fixer/scripts/benchmark_builds.py +308 -0
  109. package/skills/xcode-build-orchestrator/SKILL.md +156 -0
  110. package/skills/xcode-build-orchestrator/references/benchmark-artifacts.md +94 -0
  111. package/skills/xcode-build-orchestrator/references/build-settings-best-practices.md +216 -0
  112. package/skills/xcode-build-orchestrator/references/orchestration-report-template.md +143 -0
  113. package/skills/xcode-build-orchestrator/references/recommendation-format.md +85 -0
  114. package/skills/xcode-build-orchestrator/scripts/benchmark_builds.py +308 -0
  115. package/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py +273 -0
  116. package/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py +533 -0
  117. package/skills/xcode-compilation-analyzer/SKILL.md +89 -0
  118. package/skills/xcode-compilation-analyzer/references/build-optimization-sources.md +155 -0
  119. package/skills/xcode-compilation-analyzer/references/code-compilation-checks.md +106 -0
  120. package/skills/xcode-compilation-analyzer/references/recommendation-format.md +85 -0
  121. package/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py +273 -0
  122. package/skills/xcode-project-analyzer/SKILL.md +76 -0
  123. package/skills/xcode-project-analyzer/references/build-optimization-sources.md +155 -0
  124. package/skills/xcode-project-analyzer/references/build-settings-best-practices.md +216 -0
  125. package/skills/xcode-project-analyzer/references/project-audit-checks.md +101 -0
  126. package/skills/xcode-project-analyzer/references/recommendation-format.md +85 -0
  127. package/templates/CODEBASE.md +26 -42
  128. package/templates/configs/trello-config.json +2 -2
  129. package/templates/workflow_dual_mode_template.md +5 -5
  130. package/workflows/_uncategorized/conductor-codex.md +125 -0
  131. package/workflows/_uncategorized/conductor.md +97 -0
  132. package/workflows/_uncategorized/ship-to-code.md +85 -0
  133. package/workflows/_uncategorized/trello-sync.md +52 -0
  134. package/workflows/context/codebase-sync.md +10 -87
  135. package/workflows/quality/visual-debug.md +66 -12
@@ -0,0 +1,482 @@
1
+ """
2
+ reCAPTCHA Enterprise token provider for Google Flow.
3
+
4
+ Flow requires a reCAPTCHA Enterprise v3 token with every generation request.
5
+ reCAPTCHA Enterprise v3 uses deep behavioral scoring — it tracks mouse movements,
6
+ browsing history, interaction patterns, and more. No automated/headless browser
7
+ can pass this scoring, regardless of how well it's disguised.
8
+
9
+ This module connects to the Chrome browser that was kept alive after `gflow auth`.
10
+ Since that browser has genuine user interaction (the user logged in manually),
11
+ reCAPTCHA Enterprise gives it a good score and accepts the tokens.
12
+
13
+ The connection is via Chrome DevTools Protocol (CDP) over WebSocket to the
14
+ --remote-debugging-port that was set during `gflow auth`.
15
+
16
+ Site key (from Flow network traffic):
17
+ 6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ import threading
25
+ import time
26
+ import urllib.request
27
+ import urllib.error
28
+
29
+ logger = logging.getLogger("gflow.recaptcha")
30
+
31
+ # reCAPTCHA Enterprise site key for labs.google
32
+ RECAPTCHA_SITE_KEY = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
33
+
34
+ # Flow URL to load for reCAPTCHA context
35
+ FLOW_URL = "https://labs.google/fx/tools/flow"
36
+
37
+
38
+ class RecaptchaProvider:
39
+ """
40
+ Provides fresh reCAPTCHA Enterprise tokens by connecting to the
41
+ Chrome browser that was kept alive from `gflow auth`.
42
+
43
+ This avoids launching any new browser — it reuses the authenticated
44
+ session where the user already interacted, which is the only way
45
+ to get valid reCAPTCHA Enterprise v3 tokens.
46
+ """
47
+
48
+ def __init__(self, cookies: str = "", debug: bool = False):
49
+ self.debug = debug
50
+ self._cookies = cookies
51
+ self._ws = None
52
+ self._msg_id: int = 0
53
+ self._ready = False
54
+ # Threading lock prevents concurrent reconnection storms
55
+ # (inspired by notebooklm-py's asyncio.Lock pattern)
56
+ self._lock = threading.Lock()
57
+ self._connecting = False
58
+
59
+ def get_token(self, action: str = "IMAGE_GENERATION") -> str:
60
+ """
61
+ Get a fresh reCAPTCHA Enterprise token.
62
+
63
+ Thread-safe: concurrent callers share a single connection/reconnection
64
+ cycle via a lock, preventing reCAPTCHA refresh storms (inspired by
65
+ notebooklm-py's concurrency-safe auth refresh pattern).
66
+
67
+ Args:
68
+ action: The reCAPTCHA action string. Flow uses:
69
+ - "IMAGE_GENERATION" for image generation
70
+ - "VIDEO_GENERATION" for video generation
71
+
72
+ Returns:
73
+ A reCAPTCHA token string
74
+
75
+ Raises:
76
+ RecaptchaError if token cannot be obtained
77
+ """
78
+ if not self._ready:
79
+ with self._lock:
80
+ # Double-check after acquiring lock (another thread may have connected)
81
+ if not self._ready:
82
+ self._connect()
83
+
84
+ try:
85
+ return self._execute_recaptcha(action)
86
+ except RecaptchaError:
87
+ # Token execution failed — try reconnecting once before giving up
88
+ # (the CDP WebSocket may have dropped)
89
+ with self._lock:
90
+ logger.info("reCAPTCHA token failed, attempting reconnect...")
91
+ self._ready = False
92
+ self._close_ws()
93
+ self._connect()
94
+ return self._execute_recaptcha(action)
95
+
96
+ def _connect(self) -> None:
97
+ """Connect to the existing auth Chrome browser via CDP, launching one if needed."""
98
+ from gflow.auth.browser_auth import get_saved_cdp_port
99
+
100
+ port = get_saved_cdp_port()
101
+ ws_url = None
102
+
103
+ if port:
104
+ if self.debug:
105
+ logger.info("Checking saved CDP port %d...", port)
106
+ ws_url = self._find_flow_tab(port) or self._find_any_tab(port)
107
+ if not ws_url:
108
+ if self.debug:
109
+ logger.info("CDP port %d is dead or has no tabs. Falling back to auto-launch...", port)
110
+ port = None
111
+
112
+ if not port:
113
+ logger.info("No valid Chrome session found — auto-launching auth browser...")
114
+ port = self._auto_launch_chrome()
115
+ if not port:
116
+ raise RecaptchaError(
117
+ "No Chrome browser session found and auto-launch failed.\n"
118
+ "Run 'gflow auth' first — it opens a Chrome window that stays\n"
119
+ "open for reCAPTCHA. Don't close it until you're done generating."
120
+ )
121
+ ws_url = self._find_flow_tab(port) or self._find_any_tab(port)
122
+ if not ws_url:
123
+ raise RecaptchaError(
124
+ "Chrome is running but has no usable tabs.\n"
125
+ "Run 'gflow auth' again to set up a fresh session."
126
+ )
127
+
128
+ if self.debug:
129
+ logger.info("Connecting to auth Chrome on port %d...", port)
130
+
131
+ self._connect_ws(ws_url)
132
+ # Ensure we navigate to the flow page if we grabbed a generic tab
133
+ self._cdp_send("Page.enable")
134
+ self._cdp_navigate(FLOW_URL)
135
+
136
+ # Wait for reCAPTCHA to be available
137
+ self._wait_for_recaptcha()
138
+
139
+ # Warm up reCAPTCHA to build trust score
140
+ self._warm_up()
141
+
142
+ self._ready = True
143
+
144
+ if self.debug:
145
+ logger.info("Connected to auth Chrome, reCAPTCHA ready")
146
+
147
+ def _auto_launch_chrome(self) -> int | None:
148
+ """Auto-launch Chrome with CDP for reCAPTCHA, reusing saved cookies."""
149
+ import platform
150
+ import subprocess
151
+ from gflow.auth.browser_auth import (
152
+ _get_chrome_path, _find_free_port, save_cdp_port,
153
+ _wait_for_cdp_page, ENV_DIR, kill_auth_browser
154
+ )
155
+
156
+ try:
157
+ # First, forcefully kill any leftover Chrome process that might be
158
+ # holding the profile lock but not responding.
159
+ kill_auth_browser()
160
+ except Exception as e:
161
+ if self.debug:
162
+ logger.warning("Failed to kill auth browser before launch: %s", e)
163
+
164
+ try:
165
+ chrome_path = _get_chrome_path()
166
+ except Exception:
167
+ return None
168
+
169
+ cdp_port = _find_free_port()
170
+ profile_dir = str(ENV_DIR / "chrome-profile")
171
+
172
+ args = [
173
+ chrome_path,
174
+ f"--remote-debugging-port={cdp_port}",
175
+ "--remote-allow-origins=*",
176
+ f"--user-data-dir={profile_dir}",
177
+ "--no-first-run",
178
+ "--no-default-browser-check",
179
+ # Run visible (not headless or offscreen — reCAPTCHA detects
180
+ # hidden/offscreen windows and tanks the trust score)
181
+ "--window-size=800,600",
182
+ ]
183
+
184
+ # Route Chrome through residential proxy if configured
185
+ try:
186
+ from gflow.auth.proxy_ext import get_chrome_proxy_args
187
+ proxy_args = get_chrome_proxy_args()
188
+ if proxy_args:
189
+ args.extend(proxy_args)
190
+ logger.info("Chrome using residential proxy")
191
+ except Exception:
192
+ pass
193
+
194
+ args.append(FLOW_URL)
195
+
196
+ if self.debug:
197
+ logger.info("Auto-launching Chrome on CDP port %d", cdp_port)
198
+
199
+ creation_flags = 0
200
+ if platform.system() == "Windows":
201
+ creation_flags = (
202
+ subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
203
+ )
204
+
205
+ try:
206
+ subprocess.Popen(
207
+ args,
208
+ stdout=subprocess.DEVNULL,
209
+ stderr=subprocess.DEVNULL,
210
+ creationflags=creation_flags if platform.system() == "Windows" else 0,
211
+ start_new_session=(platform.system() != "Windows"),
212
+ )
213
+ except Exception as e:
214
+ logger.warning("Failed to launch Chrome: %s", e)
215
+ return None
216
+
217
+ # Wait for CDP to become available
218
+ try:
219
+ _wait_for_cdp_page(cdp_port, timeout=30)
220
+ except Exception:
221
+ logger.warning("Chrome launched but CDP not available")
222
+ return None
223
+
224
+ save_cdp_port(cdp_port)
225
+
226
+ # Give the page time to fully load reCAPTCHA scripts
227
+ import time as _time
228
+ _time.sleep(5)
229
+
230
+ return cdp_port
231
+
232
+ def _find_flow_tab(self, port: int) -> str | None:
233
+ """Find a tab that's already on labs.google/fx."""
234
+ try:
235
+ url = f"http://127.0.0.1:{port}/json/list"
236
+ resp = urllib.request.urlopen(url, timeout=5)
237
+ targets = json.loads(resp.read().decode())
238
+
239
+ for target in targets:
240
+ if target.get("type") == "page":
241
+ page_url = target.get("url", "")
242
+ if "labs.google/fx" in page_url:
243
+ return target.get("webSocketDebuggerUrl", "")
244
+ except Exception as e:
245
+ if self.debug:
246
+ logger.warning("Failed to list tabs: %s", e)
247
+ return None
248
+
249
+ def _find_any_tab(self, port: int) -> str | None:
250
+ """Find any page tab we can use, or create one if none exist (macOS closed-window case)."""
251
+ try:
252
+ url = f"http://127.0.0.1:{port}/json/list"
253
+ resp = urllib.request.urlopen(url, timeout=5)
254
+ targets = json.loads(resp.read().decode())
255
+
256
+ for target in targets:
257
+ if target.get("type") == "page" and target.get("webSocketDebuggerUrl"):
258
+ return target.get("webSocketDebuggerUrl", "")
259
+
260
+ # Chrome is running but has no windows/tabs (common on macOS). Create one.
261
+ new_url = f"http://127.0.0.1:{port}/json/new"
262
+ resp = urllib.request.urlopen(new_url, timeout=5)
263
+ target = json.loads(resp.read().decode())
264
+ if target.get("type") == "page" and target.get("webSocketDebuggerUrl"):
265
+ return target.get("webSocketDebuggerUrl", "")
266
+
267
+ except Exception:
268
+ pass
269
+ return None
270
+
271
+ def _connect_ws(self, ws_url: str) -> None:
272
+ """Connect to a CDP WebSocket endpoint."""
273
+ import websocket
274
+
275
+ try:
276
+ self._ws = websocket.create_connection(ws_url, timeout=30)
277
+ except Exception as e:
278
+ raise RecaptchaError(
279
+ f"Failed to connect to Chrome: {e}\n"
280
+ "The auth browser may have been closed. Run 'gflow auth' again."
281
+ )
282
+
283
+ def _cdp_send(self, method: str, params: dict | None = None) -> dict:
284
+ """Send a CDP command and wait for the result."""
285
+ self._msg_id += 1
286
+ msg = {"id": self._msg_id, "method": method}
287
+ if params:
288
+ msg["params"] = params
289
+
290
+ self._ws.send(json.dumps(msg))
291
+
292
+ deadline = time.time() + 30
293
+ while time.time() < deadline:
294
+ try:
295
+ self._ws.settimeout(5)
296
+ raw = self._ws.recv()
297
+ data = json.loads(raw)
298
+ if data.get("id") == self._msg_id:
299
+ if "error" in data:
300
+ raise RecaptchaError(f"CDP error: {data['error']}")
301
+ return data.get("result", {})
302
+ except RecaptchaError:
303
+ raise
304
+ except Exception as e:
305
+ if "timed out" in str(e).lower() or "timeout" in str(e).lower():
306
+ continue
307
+ raise
308
+
309
+ raise RecaptchaError("CDP command timed out")
310
+
311
+ def _cdp_navigate(self, url: str) -> None:
312
+ """Navigate to a URL and wait for it to load."""
313
+ self._cdp_send("Page.navigate", {"url": url})
314
+
315
+ deadline = time.time() + 30
316
+ while time.time() < deadline:
317
+ try:
318
+ self._ws.settimeout(2)
319
+ raw = self._ws.recv()
320
+ data = json.loads(raw)
321
+ method = data.get("method", "")
322
+ if method in ("Page.loadEventFired", "Page.frameStoppedLoading"):
323
+ return
324
+ except Exception:
325
+ continue
326
+
327
+ if self.debug:
328
+ logger.info("Navigation wait timed out, continuing...")
329
+
330
+ def _cdp_evaluate(self, expression: str):
331
+ """Evaluate JS expression in the page and return the result."""
332
+ result = self._cdp_send("Runtime.evaluate", {
333
+ "expression": expression,
334
+ "awaitPromise": True,
335
+ "returnByValue": True,
336
+ })
337
+ inner = result.get("result", {})
338
+ if inner.get("subtype") == "error":
339
+ raise RecaptchaError(f"JS error: {inner.get('description', 'unknown')}")
340
+ return inner.get("value")
341
+
342
+ def _wait_for_recaptcha(self, timeout: int = 30) -> None:
343
+ """Wait for reCAPTCHA Enterprise to load on the page."""
344
+ deadline = time.time() + timeout
345
+
346
+ while time.time() < deadline:
347
+ try:
348
+ ready = self._cdp_evaluate(
349
+ "typeof grecaptcha !== 'undefined' && "
350
+ "typeof grecaptcha.enterprise !== 'undefined' && "
351
+ "typeof grecaptcha.enterprise.execute === 'function'"
352
+ )
353
+ if ready:
354
+ if self.debug:
355
+ logger.info("reCAPTCHA Enterprise loaded")
356
+ return
357
+ except RecaptchaError:
358
+ pass
359
+ time.sleep(1)
360
+
361
+ raise RecaptchaError(
362
+ "reCAPTCHA Enterprise did not load within 30s.\n"
363
+ "The auth browser may have navigated away from Flow.\n"
364
+ "Run 'gflow auth' again."
365
+ )
366
+
367
+ def _warm_up(self) -> None:
368
+ """Warm up reCAPTCHA with realistic human-like browser interaction.
369
+
370
+ Uses CDP Input.dispatchMouseEvent / dispatchMouseWheel to send
371
+ real browser-level input events (Bezier-curved mouse movement,
372
+ gradual scrolling, idle fidgeting). These register identically
373
+ to hardware input in reCAPTCHA's behavioral scoring model.
374
+
375
+ Then pre-executes reCAPTCHA tokens to further build trust.
376
+ """
377
+ logger.info("Warming up reCAPTCHA (building trust score)...")
378
+
379
+ # ── Phase 1: Human-like interaction via CDP Input.dispatch* ──
380
+ try:
381
+ from gflow.auth.humanizer import CDPHumanizer
382
+
383
+ humanizer = CDPHumanizer(
384
+ cdp_send=lambda method, params: self._cdp_send(method, params),
385
+ )
386
+ # Run full warm-up sequence: idle movement, scrolling, clicking,
387
+ # mouse exploration — ~12 seconds of convincing human behavior
388
+ humanizer.full_warmup(duration=12.0)
389
+ except Exception as e:
390
+ logger.warning("Humanizer warm-up failed (%s), falling back to basic warm-up", e)
391
+ # Fallback: basic JS-dispatched events (less effective but better than nothing)
392
+ try:
393
+ self._cdp_evaluate("""
394
+ (function() {
395
+ window.scrollTo(0, 200);
396
+ window.scrollTo(0, 0);
397
+ document.dispatchEvent(new MouseEvent('mousemove', {clientX: 100, clientY: 200}));
398
+ document.dispatchEvent(new MouseEvent('mousemove', {clientX: 300, clientY: 400}));
399
+ document.dispatchEvent(new MouseEvent('mousemove', {clientX: 500, clientY: 300}));
400
+ window.dispatchEvent(new Event('focus'));
401
+ return true;
402
+ })()
403
+ """)
404
+ except Exception:
405
+ pass
406
+ time.sleep(2)
407
+
408
+ # ── Phase 2: Pre-execute reCAPTCHA tokens to build trust ──
409
+ # Adaptive backoff between warm-up tokens (inspired by notebooklm-py's
410
+ # rate-limit-aware retry pattern). Each successful token slightly
411
+ # reduces the delay; failures increase it.
412
+ warmup_delay = 1.5
413
+ for i in range(3):
414
+ try:
415
+ token = self._cdp_evaluate(
416
+ f"grecaptcha.enterprise.execute('{RECAPTCHA_SITE_KEY}', {{action: 'WARM_UP'}})"
417
+ )
418
+ if token and isinstance(token, str) and len(token) > 100:
419
+ if self.debug:
420
+ logger.info(" Warm-up %d/%d OK (%d chars)", i + 1, 3, len(token))
421
+ # Success — can reduce delay slightly for next iteration
422
+ warmup_delay = max(1.0, warmup_delay * 0.8)
423
+ else:
424
+ warmup_delay = min(5.0, warmup_delay * 1.5)
425
+ time.sleep(warmup_delay)
426
+ except Exception as e:
427
+ logger.warning(" Warm-up %d/%d failed: %s", i + 1, 3, e)
428
+ warmup_delay = min(5.0, warmup_delay * 2.0)
429
+ time.sleep(warmup_delay)
430
+
431
+ logger.info("reCAPTCHA warm-up complete")
432
+
433
+ def _execute_recaptcha(self, action: str = "IMAGE_GENERATION") -> str:
434
+ """Execute reCAPTCHA and get a token."""
435
+ try:
436
+ token = self._cdp_evaluate(
437
+ f"grecaptcha.enterprise.execute('{RECAPTCHA_SITE_KEY}', {{action: '{action}'}})"
438
+ )
439
+
440
+ if not token or not isinstance(token, str):
441
+ raise RecaptchaError(f"reCAPTCHA returned invalid token: {token}")
442
+
443
+ if len(token) < 100:
444
+ raise RecaptchaError(f"reCAPTCHA token too short ({len(token)} chars)")
445
+
446
+ if self.debug:
447
+ logger.info("Got reCAPTCHA token: %s... (%d chars)", token[:30], len(token))
448
+
449
+ return token
450
+
451
+ except RecaptchaError:
452
+ raise
453
+ except Exception as e:
454
+ raise RecaptchaError(f"Failed to execute reCAPTCHA: {e}")
455
+
456
+ def _close_ws(self) -> None:
457
+ """Close the WebSocket connection only (internal helper)."""
458
+ if self._ws:
459
+ try:
460
+ self._ws.close()
461
+ except Exception:
462
+ pass
463
+ self._ws = None
464
+
465
+ def close(self) -> None:
466
+ """Close the WebSocket connection (does NOT close Chrome)."""
467
+ self._close_ws()
468
+ self._ready = False
469
+
470
+ def __del__(self):
471
+ self.close()
472
+
473
+ def __enter__(self):
474
+ return self
475
+
476
+ def __exit__(self, *args):
477
+ self.close()
478
+
479
+
480
+ class RecaptchaError(Exception):
481
+ """Raised when reCAPTCHA token cannot be obtained."""
482
+ pass
@@ -0,0 +1,5 @@
1
+ """Google BatchExecute protocol implementation."""
2
+
3
+ from .client import BatchExecuteClient, RPC, Response, BatchExecuteError
4
+
5
+ __all__ = ["BatchExecuteClient", "RPC", "Response", "BatchExecuteError"]