web3-agent-kit 0.8.0__tar.gz → 0.9.0__tar.gz

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 (69) hide show
  1. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/PKG-INFO +1 -1
  2. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/pyproject.toml +1 -1
  3. web3_agent_kit-0.9.0/src/airdrop/executor/__init__.py +33 -0
  4. web3_agent_kit-0.9.0/src/airdrop/executor/browser.py +435 -0
  5. web3_agent_kit-0.9.0/src/airdrop/executor/cli.py +402 -0
  6. web3_agent_kit-0.9.0/src/airdrop/executor/gleam_exec.py +664 -0
  7. web3_agent_kit-0.9.0/src/airdrop/executor/social_exec.py +789 -0
  8. web3_agent_kit-0.9.0/src/airdrop/executor/zealy_exec.py +535 -0
  9. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/__init__.py +3 -3
  10. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_api.py +1 -1
  11. web3_agent_kit-0.9.0/tests/test_executor.py +250 -0
  12. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/PKG-INFO +1 -1
  13. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/SOURCES.txt +7 -0
  14. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/LICENSE +0 -0
  15. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/README.md +0 -0
  16. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/setup.cfg +0 -0
  17. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/setup.py +0 -0
  18. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/__init__.py +0 -0
  19. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/agent.py +0 -0
  20. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/__init__.py +0 -0
  21. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/base.py +0 -0
  22. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/galxe.py +0 -0
  23. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/gleam.py +0 -0
  24. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/multi_wallet.py +0 -0
  25. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/social.py +0 -0
  26. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/tracker.py +0 -0
  27. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/zealy.py +0 -0
  28. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/models.py +0 -0
  29. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/__init__.py +0 -0
  30. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/approval.py +0 -0
  31. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/bridge.py +0 -0
  32. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/dca.py +0 -0
  33. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/gas.py +0 -0
  34. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/portfolio.py +0 -0
  35. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/swap.py +0 -0
  36. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/wallet.py +0 -0
  37. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/watcher.py +0 -0
  38. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/yield_opt.py +0 -0
  39. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/approval_manager.py +0 -0
  40. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/bridge.py +0 -0
  41. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/chain.py +0 -0
  42. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/dca_bot.py +0 -0
  43. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/defi/__init__.py +0 -0
  44. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/gas_optimizer.py +0 -0
  45. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/llm.py +0 -0
  46. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/multi_wallet.py +0 -0
  47. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/plugins/__init__.py +0 -0
  48. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/plugins/examples/gas_tracker.py +0 -0
  49. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/portfolio.py +0 -0
  50. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/sniper.py +0 -0
  51. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/utils/__init__.py +0 -0
  52. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/wallet.py +0 -0
  53. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/wallet_watcher.py +0 -0
  54. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/yield_optimizer.py +0 -0
  55. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_airdrop.py +0 -0
  56. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_bridge.py +0 -0
  57. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_core.py +0 -0
  58. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_dca_bot.py +0 -0
  59. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_defi.py +0 -0
  60. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_llm.py +0 -0
  61. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_multi_wallet.py +0 -0
  62. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_new_features.py +0 -0
  63. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_plugins.py +0 -0
  64. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_portfolio.py +0 -0
  65. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_sniper.py +0 -0
  66. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_yield_optimizer.py +0 -0
  67. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/dependency_links.txt +0 -0
  68. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/requires.txt +0 -0
  69. {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: web3-agent-kit
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Open-source framework for building autonomous AI agents that interact with blockchain networks
5
5
  Author-email: Maulana <khasbim240803@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "web3-agent-kit"
7
- version = "0.8.0"
7
+ version = "0.9.0"
8
8
  description = "Open-source framework for building autonomous AI agents that interact with blockchain networks"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,33 @@
1
+ """Executor layer — browser automation for airdrop task completion.
2
+
3
+ Provides real browser-based automation for Gleam.io, Zealy, and social
4
+ platform tasks using Playwright with anti-detect capabilities.
5
+ """
6
+
7
+ from .browser import BrowserManager, BrowserConfig
8
+ from .gleam_exec import GleamExecutor, GleamResult, GleamTaskEntry
9
+ from .social_exec import (
10
+ TwitterExecutor,
11
+ DiscordExecutor,
12
+ TelegramExecutor,
13
+ SocialExecutorConfig,
14
+ )
15
+ from .zealy_exec import ZealyExecutor, ZealyResult
16
+
17
+ __all__ = [
18
+ # Browser
19
+ "BrowserManager",
20
+ "BrowserConfig",
21
+ # Gleam
22
+ "GleamExecutor",
23
+ "GleamResult",
24
+ "GleamTaskEntry",
25
+ # Social
26
+ "TwitterExecutor",
27
+ "DiscordExecutor",
28
+ "TelegramExecutor",
29
+ "SocialExecutorConfig",
30
+ # Zealy
31
+ "ZealyExecutor",
32
+ "ZealyResult",
33
+ ]
@@ -0,0 +1,435 @@
1
+ """Anti-detect browser management using Playwright.
2
+
3
+ Provides stealth browsing with viewport randomization, user-agent rotation,
4
+ timezone spoofing, cookie persistence, and proxy support.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import os
12
+ import random
13
+ import time
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any, Optional
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Graceful import — playwright is optional
21
+ try:
22
+ from playwright.sync_api import (
23
+ Browser,
24
+ BrowserContext,
25
+ Page,
26
+ Playwright,
27
+ sync_playwright,
28
+ )
29
+
30
+ HAS_PLAYWRIGHT = True
31
+ except ImportError:
32
+ HAS_PLAYWRIGHT = False
33
+ # Type stubs for when playwright isn't installed
34
+ Browser = Any # type: ignore[misc,assignment]
35
+ BrowserContext = Any # type: ignore[misc,assignment]
36
+ Page = Any # type: ignore[misc,assignment]
37
+ Playwright = Any # type: ignore[misc,assignment]
38
+
39
+ SESSIONS_DIR = Path.home() / ".web3-agent-kit" / "sessions"
40
+
41
+ USER_AGENTS = [
42
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
43
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
44
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
45
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
46
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
47
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
48
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
49
+ ]
50
+
51
+ TIMEZONES = [
52
+ "America/New_York",
53
+ "America/Chicago",
54
+ "America/Los_Angeles",
55
+ "Europe/London",
56
+ "Europe/Berlin",
57
+ "Asia/Singapore",
58
+ "Asia/Tokyo",
59
+ "Australia/Sydney",
60
+ ]
61
+
62
+ VIEWPORTS = [
63
+ {"width": 1920, "height": 1080},
64
+ {"width": 1366, "height": 768},
65
+ {"width": 1536, "height": 864},
66
+ {"width": 1440, "height": 900},
67
+ {"width": 1280, "height": 720},
68
+ {"width": 2560, "height": 1440},
69
+ ]
70
+
71
+
72
+ @dataclass
73
+ class BrowserConfig:
74
+ """Configuration for the anti-detect browser."""
75
+
76
+ headless: bool = True
77
+ proxy: Optional[str] = None # http://host:port or socks5://host:port
78
+ user_agent: Optional[str] = None # None = random
79
+ timezone: Optional[str] = None # None = random
80
+ viewport: Optional[dict[str, int]] = None # None = random
81
+ session_name: str = "default"
82
+ sessions_dir: Path = SESSIONS_DIR
83
+ screenshot_dir: Optional[Path] = None
84
+ max_retries: int = 3
85
+ retry_delay: float = 2.0
86
+ slow_mo: int = 0 # ms delay between actions
87
+ extra_args: list[str] = field(default_factory=list)
88
+
89
+
90
+ class BrowserManager:
91
+ """Anti-detect browser manager using Playwright.
92
+
93
+ Manages browser lifecycle with stealth features: viewport randomization,
94
+ user-agent rotation, timezone spoofing, cookie persistence, and proxy support.
95
+
96
+ Example::
97
+
98
+ config = BrowserConfig(headless=True, proxy="socks5://proxy:1080")
99
+ manager = BrowserManager(config)
100
+
101
+ manager.launch()
102
+ page = manager.new_page()
103
+ page.goto("https://gleam.io/contest/abc123")
104
+ manager.screenshot(page, "contest.png")
105
+ manager.close()
106
+ """
107
+
108
+ def __init__(self, config: Optional[BrowserConfig] = None):
109
+ if not HAS_PLAYWRIGHT:
110
+ raise ImportError(
111
+ "playwright is required for browser automation. "
112
+ "Install with: pip install playwright && playwright install chromium"
113
+ )
114
+ self.config = config or BrowserConfig()
115
+ self._playwright: Optional[Playwright] = None
116
+ self._browser: Optional[Browser] = None
117
+ self._context: Optional[BrowserContext] = None
118
+ self._pages: list[Page] = []
119
+ self._session_dir = self.config.sessions_dir / self.config.session_name
120
+ self._session_dir.mkdir(parents=True, exist_ok=True)
121
+
122
+ def launch(self) -> None:
123
+ """Launch the browser with stealth configuration.
124
+
125
+ Sets up anti-detect measures including randomized viewport,
126
+ user-agent, timezone, and proxy if configured.
127
+ """
128
+ self._playwright = sync_playwright().start()
129
+
130
+ # Build launch args with stealth flags
131
+ launch_args = [
132
+ "--disable-blink-features=AutomationControlled",
133
+ "--disable-features=IsolateOrigins,site-per-process",
134
+ "--disable-site-isolation-trials",
135
+ "--no-sandbox",
136
+ "--disable-setuid-sandbox",
137
+ "--disable-dev-shm-usage",
138
+ "--disable-accelerated-2d-canvas",
139
+ "--no-first-run",
140
+ "--no-zygote",
141
+ "--disable-gpu",
142
+ ]
143
+ launch_args.extend(self.config.extra_args)
144
+
145
+ # Proxy configuration
146
+ proxy_settings = None
147
+ if self.config.proxy:
148
+ proxy_settings = {"server": self.config.proxy}
149
+
150
+ self._browser = self._playwright.chromium.launch(
151
+ headless=self.config.headless,
152
+ args=launch_args,
153
+ proxy=proxy_settings,
154
+ slow_mo=self.config.slow_mo,
155
+ )
156
+
157
+ # Create context with stealth settings
158
+ self._context = self._create_stealth_context()
159
+
160
+ # Load saved cookies if they exist
161
+ self._load_cookies()
162
+
163
+ logger.info(
164
+ f"Browser launched (headless={self.config.headless}, "
165
+ f"proxy={'yes' if self.config.proxy else 'no'})"
166
+ )
167
+
168
+ def new_page(self) -> Page:
169
+ """Create a new browser page.
170
+
171
+ Returns:
172
+ A new Playwright Page object.
173
+
174
+ Raises:
175
+ RuntimeError: If browser is not launched.
176
+ """
177
+ if not self._context:
178
+ raise RuntimeError("Browser not launched. Call launch() first.")
179
+
180
+ page = self._context.new_page()
181
+ self._pages.append(page)
182
+
183
+ # Inject stealth scripts
184
+ self._inject_stealth_scripts(page)
185
+
186
+ logger.debug("New page created")
187
+ return page
188
+
189
+ def close(self) -> None:
190
+ """Close all pages, context, browser, and playwright.
191
+
192
+ Saves cookies before closing.
193
+ """
194
+ try:
195
+ self._save_cookies()
196
+ except Exception as e:
197
+ logger.warning(f"Failed to save cookies on close: {e}")
198
+
199
+ for page in self._pages:
200
+ try:
201
+ page.close()
202
+ except Exception:
203
+ pass
204
+ self._pages.clear()
205
+
206
+ if self._context:
207
+ try:
208
+ self._context.close()
209
+ except Exception:
210
+ pass
211
+ self._context = None
212
+
213
+ if self._browser:
214
+ try:
215
+ self._browser.close()
216
+ except Exception:
217
+ pass
218
+ self._browser = None
219
+
220
+ if self._playwright:
221
+ try:
222
+ self._playwright.stop()
223
+ except Exception:
224
+ pass
225
+ self._playwright = None
226
+
227
+ logger.info("Browser closed")
228
+
229
+ def screenshot(self, page: Page, name: str) -> Optional[str]:
230
+ """Take a screenshot of the current page.
231
+
232
+ Args:
233
+ page: The Playwright Page to screenshot.
234
+ name: Filename for the screenshot.
235
+
236
+ Returns:
237
+ Path to saved screenshot, or None if failed.
238
+ """
239
+ try:
240
+ if self.config.screenshot_dir:
241
+ ss_dir = self.config.screenshot_dir
242
+ else:
243
+ ss_dir = self._session_dir / "screenshots"
244
+ ss_dir.mkdir(parents=True, exist_ok=True)
245
+
246
+ path = ss_dir / name
247
+ page.screenshot(path=str(path), full_page=True)
248
+ logger.debug(f"Screenshot saved: {path}")
249
+ return str(path)
250
+ except Exception as e:
251
+ logger.warning(f"Screenshot failed: {e}")
252
+ return None
253
+
254
+ def save_cookies(self) -> None:
255
+ """Explicitly save current cookies to disk."""
256
+ self._save_cookies()
257
+
258
+ def load_cookies(self) -> None:
259
+ """Explicitly load cookies from disk."""
260
+ self._load_cookies()
261
+
262
+ def get_session_path(self, platform: str = "") -> Path:
263
+ """Get the session directory path for a platform.
264
+
265
+ Args:
266
+ platform: Platform name (e.g., 'twitter', 'discord').
267
+
268
+ Returns:
269
+ Path to the session directory.
270
+ """
271
+ if platform:
272
+ p = self._session_dir / platform
273
+ p.mkdir(parents=True, exist_ok=True)
274
+ return p
275
+ return self._session_dir
276
+
277
+ def navigate_with_retry(self, page: Page, url: str, timeout: int = 30000) -> bool:
278
+ """Navigate to a URL with retry on failure.
279
+
280
+ Args:
281
+ page: The Playwright Page.
282
+ url: URL to navigate to.
283
+ timeout: Navigation timeout in milliseconds.
284
+
285
+ Returns:
286
+ True if navigation succeeded.
287
+ """
288
+ for attempt in range(self.config.max_retries):
289
+ try:
290
+ page.goto(url, timeout=timeout, wait_until="domcontentloaded")
291
+ return True
292
+ except Exception as e:
293
+ logger.warning(f"Navigation failed (attempt {attempt + 1}): {e}")
294
+ if attempt < self.config.max_retries - 1:
295
+ time.sleep(self.config.retry_delay * (attempt + 1))
296
+ # Try to recover from crash
297
+ try:
298
+ if page.is_closed():
299
+ page = self.new_page()
300
+ except Exception:
301
+ pass
302
+ return False
303
+
304
+ def wait_for_element(
305
+ self,
306
+ page: Page,
307
+ selector: str,
308
+ timeout: int = 10000,
309
+ ) -> bool:
310
+ """Wait for an element to appear on the page.
311
+
312
+ Args:
313
+ page: The Playwright Page.
314
+ selector: CSS selector to wait for.
315
+ timeout: Max wait time in milliseconds.
316
+
317
+ Returns:
318
+ True if element appeared.
319
+ """
320
+ try:
321
+ page.wait_for_selector(selector, timeout=timeout)
322
+ return True
323
+ except Exception:
324
+ return False
325
+
326
+ def click_with_retry(self, page: Page, selector: str, timeout: int = 10000) -> bool:
327
+ """Click an element with retry.
328
+
329
+ Args:
330
+ page: The Playwright Page.
331
+ selector: CSS selector to click.
332
+ timeout: Max wait time in milliseconds.
333
+
334
+ Returns:
335
+ True if click succeeded.
336
+ """
337
+ for attempt in range(self.config.max_retries):
338
+ try:
339
+ page.click(selector, timeout=timeout)
340
+ return True
341
+ except Exception as e:
342
+ logger.debug(f"Click failed (attempt {attempt + 1}): {e}")
343
+ if attempt < self.config.max_retries - 1:
344
+ time.sleep(1)
345
+ return False
346
+
347
+ @property
348
+ def is_launched(self) -> bool:
349
+ """Check if the browser is currently launched."""
350
+ return self._browser is not None and self._context is not None
351
+
352
+ def __enter__(self):
353
+ self.launch()
354
+ return self
355
+
356
+ def __exit__(self, *args):
357
+ self.close()
358
+
359
+ # --- Private helpers ---
360
+
361
+ def _create_stealth_context(self) -> BrowserContext:
362
+ """Create a browser context with stealth settings."""
363
+ ua = self.config.user_agent or random.choice(USER_AGENTS)
364
+ tz = self.config.timezone or random.choice(TIMEZONES)
365
+ vp = self.config.viewport or random.choice(VIEWPORTS)
366
+
367
+ context = self._browser.new_context( # type: ignore[union-attr]
368
+ user_agent=ua,
369
+ viewport=vp,
370
+ locale="en-US",
371
+ timezone_id=tz,
372
+ # Reduce detection signals
373
+ java_script_enabled=True,
374
+ has_touch=False,
375
+ is_mobile=False,
376
+ color_scheme="light",
377
+ )
378
+
379
+ # Override navigator.webdriver
380
+ context.add_init_script("""
381
+ Object.defineProperty(navigator, 'webdriver', {
382
+ get: () => undefined
383
+ });
384
+ Object.defineProperty(navigator, 'languages', {
385
+ get: () => ['en-US', 'en']
386
+ });
387
+ Object.defineProperty(navigator, 'plugins', {
388
+ get: () => [1, 2, 3, 4, 5]
389
+ });
390
+ window.chrome = { runtime: {} };
391
+ """)
392
+
393
+ return context
394
+
395
+ def _inject_stealth_scripts(self, page: Page) -> None:
396
+ """Inject additional stealth scripts into a page."""
397
+ try:
398
+ page.add_init_script("""
399
+ // Fake notification permission
400
+ const originalQuery = window.navigator.permissions.query;
401
+ window.navigator.permissions.query = (parameters) =>
402
+ parameters.name === 'notifications'
403
+ ? Promise.resolve({ state: Notification.permission })
404
+ : originalQuery(parameters);
405
+ """)
406
+ except Exception:
407
+ pass
408
+
409
+ def _save_cookies(self) -> None:
410
+ """Save context cookies to disk."""
411
+ if not self._context:
412
+ return
413
+ try:
414
+ cookies = self._context.cookies()
415
+ cookie_file = self._session_dir / "cookies.json"
416
+ cookie_file.parent.mkdir(parents=True, exist_ok=True)
417
+ with open(cookie_file, "w") as f:
418
+ json.dump(cookies, f, indent=2)
419
+ logger.debug(f"Saved {len(cookies)} cookies to {cookie_file}")
420
+ except Exception as e:
421
+ logger.warning(f"Cookie save failed: {e}")
422
+
423
+ def _load_cookies(self) -> None:
424
+ """Load cookies from disk into context."""
425
+ cookie_file = self._session_dir / "cookies.json"
426
+ if not cookie_file.exists():
427
+ return
428
+ try:
429
+ with open(cookie_file) as f:
430
+ cookies = json.load(f)
431
+ if cookies and self._context:
432
+ self._context.add_cookies(cookies)
433
+ logger.debug(f"Loaded {len(cookies)} cookies from {cookie_file}")
434
+ except Exception as e:
435
+ logger.warning(f"Cookie load failed: {e}")