thunder-agent 0.2.2__tar.gz → 0.4.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 (32) hide show
  1. thunder_agent-0.4.0/PKG-INFO +206 -0
  2. thunder_agent-0.4.0/README.md +178 -0
  3. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/pyproject.toml +1 -1
  4. thunder_agent-0.4.0/tests/test_stealth.py +60 -0
  5. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder/__init__.py +1 -1
  6. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder/body.py +17 -1
  7. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder/captcha.py +73 -0
  8. thunder_agent-0.4.0/thunder/fingerprint.py +297 -0
  9. thunder_agent-0.4.0/thunder/ghost.py +135 -0
  10. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder/human.py +20 -1
  11. thunder_agent-0.4.0/thunder/proxy.py +175 -0
  12. thunder_agent-0.4.0/thunder/warmup.py +43 -0
  13. thunder_agent-0.4.0/thunder_agent.egg-info/PKG-INFO +206 -0
  14. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder_agent.egg-info/SOURCES.txt +3 -0
  15. thunder_agent-0.2.2/PKG-INFO +0 -143
  16. thunder_agent-0.2.2/README.md +0 -115
  17. thunder_agent-0.2.2/thunder/fingerprint.py +0 -181
  18. thunder_agent-0.2.2/thunder/ghost.py +0 -72
  19. thunder_agent-0.2.2/thunder_agent.egg-info/PKG-INFO +0 -143
  20. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/LICENSE +0 -0
  21. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/setup.cfg +0 -0
  22. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/tests/test_body.py +0 -0
  23. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/tests/test_core.py +0 -0
  24. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder/cli.py +0 -0
  25. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder/demo.py +0 -0
  26. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder/mcp_server.py +0 -0
  27. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder/mind.py +0 -0
  28. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder/nodriver_body.py +0 -0
  29. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder_agent.egg-info/dependency_links.txt +0 -0
  30. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder_agent.egg-info/entry_points.txt +0 -0
  31. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder_agent.egg-info/requires.txt +0 -0
  32. {thunder_agent-0.2.2 → thunder_agent-0.4.0}/thunder_agent.egg-info/top_level.txt +0 -0
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: thunder-agent
3
+ Version: 0.4.0
4
+ Summary: Thunder — the human engine for AI agents: stealth browser body, human hands, TLS ghost, CAPTCHA bridge, and a stewmind thought layer.
5
+ Author-email: Emmanuel Ene Rejoice <emmanuelerogian723@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/emmanuelerogian723-alt/thunder
8
+ Project-URL: Powered by, https://huggingface.co/Erog0291/stewmind-1.7b
9
+ Keywords: ai-agents,browser-automation,stealth,mcp,anti-detection,human-like,scraping,stewmind
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: curl_cffi>=0.7
19
+ Requires-Dist: playwright>=1.40
20
+ Provides-Extra: mind
21
+ Requires-Dist: transformers>=4.44; extra == "mind"
22
+ Requires-Dist: torch; extra == "mind"
23
+ Provides-Extra: solver
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8; extra == "dev"
26
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ <div align="center">
30
+
31
+ <img src="site/assets/thunder-logo.png" width="220" alt="Thunder logo"/>
32
+
33
+ # ⚡ Thunder
34
+
35
+ **The human engine for AI agents.**
36
+
37
+ Give any software a human body: a real browser with a real fingerprint,
38
+ a mouse that trembles like a hand, keystrokes at human cadence, and a
39
+ TLS handshake indistinguishable from Chrome's.
40
+
41
+ *A bot that walks like a person.*
42
+
43
+ [![PyPI](https://img.shields.io/pypi/v/thunder-agent?color=%23ffd447&label=pypi%3A%20thunder-agent)](https://pypi.org/project/thunder-agent/)
44
+ [![Python](https://img.shields.io/badge/python-3.10%2B-3ddc84)](https://pypi.org/project/thunder-agent/)
45
+ [![License: MIT](https://img.shields.io/badge/license-MIT-4c8dff)](LICENSE)
46
+ [![Tests](https://img.shields.io/badge/stealth%20suite-passing-3ddc84)](tests/)
47
+
48
+ **[Install](#-install-in-30-seconds)** · **[Watch the proof](https://emmanuelerogian723-alt.github.io/thunder/)** · **[MCP for any software](#-mcp-give-any-software-a-human-body)**
49
+
50
+ </div>
51
+
52
+ ---
53
+
54
+ ## Why Thunder exists
55
+
56
+ Every anti-bot system on Earth looks for the same three tells:
57
+
58
+ 1. *Protocol tells* — a Python scraper's TLS/HTTP-2 handshake screams "bot" before a byte of JS runs.
59
+ 2. *Fingerprint tells* — `navigator.webdriver`, headless quirks, software-rendered WebGL, empty plugin arrays.
60
+ 3. *Behavioral tells* — perfectly straight mouse paths, instant typing, zero reading pauses.
61
+
62
+ Thunder is a body for your AI that has **none of them**. Not patched over — *absent*, the way a real human's browser is.
63
+
64
+ ```python
65
+ pip install thunder-agent
66
+ ```
67
+
68
+ ```python
69
+ import thunder
70
+
71
+ body = thunder.Body(profile="ada") # a persistent human named Ada
72
+ await body.warmup() # age the session like a returning user
73
+ await body.go("https://protected-site.com")
74
+ await body.type("#email", "ada@example.com") # typed like a person. Typos included.
75
+ await body.look("proof.png") # screenshot evidence
76
+ await body.rest() # close, keep the identity
77
+ ```
78
+
79
+ Ada comes back tomorrow with the same cookies, same screen, same GPU string, same timezone. The site sees a returning regular.
80
+
81
+ ---
82
+
83
+ ## 📦 Install in 30 seconds
84
+
85
+ ```bash
86
+ pip install thunder-agent
87
+ ```
88
+
89
+ Works on Linux, macOS, Windows. Real-Chrome engine auto-detected; falls back to bundled Chromium.
90
+
91
+ ```bash
92
+ thunder demo # watch it browse like a human, right now
93
+ thunder ghost https://nowsecure.nl # TLS-level fetch that passes the gate
94
+ ```
95
+
96
+ ---
97
+
98
+ ## ⚡ The five layers
99
+
100
+ | Layer | What it does | The tell it kills |
101
+ |---|---|---|
102
+ | **Body** | Stealth browser: real Google Chrome, persistent profiles | headless shells, disposable identities |
103
+ | **Skin** | One consistent identity: UA, platform, GPU, screen, timezone, locale | fingerprint mismatches, `SwiftShader` WebGL |
104
+ | **Hand** | Bézier mouse with *hand tremor*, human keystroke cadence, typos it fixes | straight-line paths, machine-perfect timing |
105
+ | **Ghost** | Pure HTTP with Chrome's exact TLS/JA3 fingerprint + full header orchestration + human-style retries | Python-scrape handshake = instant 403 |
106
+ | **Pool** | Thunder's own proxy pool: auto-fetched, Ghost-validated egress, sticky per profile | shared/flagged datacenter IPs |
107
+ | **Warmup** | Organic light browsing before your target | cold first-hit sessions |
108
+
109
+ Verified live: `bot.sannysoft.com` under Thunder — **WebDriver missing (passed) · WebDriver Advanced passed · Chrome present (passed) · PluginArray passed** — *4 passed, 0 failed* with headed real Chrome. Ghost pulls a **200** from a Cloudflare gate that 403s plain Python, same minute.
110
+
111
+ ---
112
+
113
+ ## 🔌 MCP: give ANY software a human body
114
+
115
+ **Zero-install, zero-account — paste this URL into Claude** (Settings → Connectors → Add custom connector, or claude.ai/mcp):
116
+
117
+ ```
118
+ https://thunder-mcp.onrender.com/mcp
119
+ ```
120
+
121
+ That's it. No signup, no keys. Thunder-hosted, always on. You get `thunder_search`, `thunder_read`, `thunder_ghost`, `thunder_feel`, `thunder_solve`, `thunder_open` — search, clean extraction and bot-gate-safe fetches through Chrome's exact TLS handshake, from inside Claude.
122
+
123
+ Want the *full* human body (stealth Chrome, mouse tremor, human typing) under MCP? Run it locally — same 10-tool MCP server:
124
+
125
+ Thunder ships an MCP server (10 tools). Paste one config and Claude, Cursor, Cline, Windsurf — anything that speaks MCP — browses as a human:
126
+
127
+ ```json
128
+ { "mcpServers": { "thunder": {
129
+ "command": "pipx", "args": ["run", "thunder-agent", "thunder-mcp"] } } }
130
+ ```
131
+
132
+ ```json
133
+ { "mcpServers": { "thunder": {
134
+ "command": "uvx", "args": ["--from", "thunder-agent", "thunder-mcp"] } } }
135
+ ```
136
+
137
+ Tools: `thunder_open` · `thunder_read` · `thunder_search` · `thunder_click` · `thunder_type` · `thunder_scroll` · `thunder_ghost` · `thunder_solve` · `thunder_feel` · `thunder_close` — each one moves like a person, never like a script.
138
+
139
+ ---
140
+
141
+ ## 🚫 The blocker table — what used to stop bots, and Thunder's answer
142
+
143
+ | Blocker | Thunder's counter |
144
+ |---|---|
145
+ | Datacenter IP suspicion | `ProxyManager`: sticky per-profile residential proxies, health-checked, auto-rotation — `THUNDER_PROXY` env or `ProxyManager.from_env()` |
146
+ | No GPU / software WebGL | Full WebGL1 **and** WebGL2 spoof: vendor + renderer read as a real consumer GPU, on every API path detectors use |
147
+ | Headless leaks | Headless-evasion pack: `outerWidth/Height`, `colorDepth`, broken-image dimensions, full `window.chrome` shape, battery API |
148
+ | Advanced CAPTCHAs | Keyless `soft_solve` (human-hand click inside challenges) + `ocr_solve` (local OCR for image/math) + CapSolver/2Captcha bridge for the hardest |
149
+ | Behavioral ML | Hand tremor + micro-corrections + warmup sessions + thinking pauses — entropy that reads as a person, because it moves like one |
150
+
151
+ ## 🔒 CAPTCHAs without a subscription
152
+
153
+ ```python
154
+ from thunder.captcha import soft_solve, ocr_solve
155
+ result = await soft_solve(body.page) # human-hand click, no keys needed
156
+ ```
157
+
158
+ Non-interactive challenges often pass on a warmed-up session. For Turnstile/reCAPTCHA Enterprise, wire your own CapSolver or 2Captcha key — Thunder senses the challenge type and routes it.
159
+
160
+ ---
161
+
162
+ ## 🧠 Pair with any mind
163
+
164
+ Thunder is the body; bring any brain. Works with stewmind, Ollama, LM Studio, vLLM, OpenAI-compatible APIs, or fully offline transformers.
165
+
166
+ ```python
167
+ from thunder.mind import Mind
168
+ mind = Mind(provider="ollama", model="llama3")
169
+ plan = await mind.think("find the pricing page and extract the tiers", body)
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 🧪 Tested, not promised
175
+
176
+ ```bash
177
+ git clone https://github.com/emmanuelerogian723-alt/thunder
178
+ cd thunder && pip install -e ".[dev]" && pytest
179
+ ```
180
+
181
+ Stealth regression suite: webdriver erased + scan-invisible · genuine PluginArray · headless pack · WebGL skin-consistency · proxy rotation. Each one fails loudly if a future commit breaks the human illusion.
182
+
183
+ ---
184
+
185
+ ## ⚖️ The honest limits (why people trust Thunder)
186
+
187
+ - No tool on Earth is "undetectable." Anyone selling that is lying to you.
188
+ - Cloud IPs still look like clouds — pair Thunder with a residential proxy for the hardest targets.
189
+ - Google-scale behavioral ML scores the whole session. Thunder's warmup + hand give you a real chance; it's not a magic key.
190
+ - **Use Thunder only where you're authorized.** Your accounts, your tests, your sites. That's the license, and it's the line.
191
+
192
+ MIT licensed. Built in the open — every claim on this page has a script in `tests/` or a screenshot in `proofs/`.
193
+
194
+ ---
195
+
196
+ <div align="center">
197
+
198
+ **Watch Thunder beat the bot-detection gauntlet, live:**
199
+
200
+ **https://emmanuelerogian723-alt.github.io/thunder/**
201
+
202
+ ⭐ If this saved you a month of anti-detect work, star the repo — it helps others find it.
203
+
204
+ *Thunder — built so software can finally act human.* 🌩️
205
+
206
+ </div>
@@ -0,0 +1,178 @@
1
+ <div align="center">
2
+
3
+ <img src="site/assets/thunder-logo.png" width="220" alt="Thunder logo"/>
4
+
5
+ # ⚡ Thunder
6
+
7
+ **The human engine for AI agents.**
8
+
9
+ Give any software a human body: a real browser with a real fingerprint,
10
+ a mouse that trembles like a hand, keystrokes at human cadence, and a
11
+ TLS handshake indistinguishable from Chrome's.
12
+
13
+ *A bot that walks like a person.*
14
+
15
+ [![PyPI](https://img.shields.io/pypi/v/thunder-agent?color=%23ffd447&label=pypi%3A%20thunder-agent)](https://pypi.org/project/thunder-agent/)
16
+ [![Python](https://img.shields.io/badge/python-3.10%2B-3ddc84)](https://pypi.org/project/thunder-agent/)
17
+ [![License: MIT](https://img.shields.io/badge/license-MIT-4c8dff)](LICENSE)
18
+ [![Tests](https://img.shields.io/badge/stealth%20suite-passing-3ddc84)](tests/)
19
+
20
+ **[Install](#-install-in-30-seconds)** · **[Watch the proof](https://emmanuelerogian723-alt.github.io/thunder/)** · **[MCP for any software](#-mcp-give-any-software-a-human-body)**
21
+
22
+ </div>
23
+
24
+ ---
25
+
26
+ ## Why Thunder exists
27
+
28
+ Every anti-bot system on Earth looks for the same three tells:
29
+
30
+ 1. *Protocol tells* — a Python scraper's TLS/HTTP-2 handshake screams "bot" before a byte of JS runs.
31
+ 2. *Fingerprint tells* — `navigator.webdriver`, headless quirks, software-rendered WebGL, empty plugin arrays.
32
+ 3. *Behavioral tells* — perfectly straight mouse paths, instant typing, zero reading pauses.
33
+
34
+ Thunder is a body for your AI that has **none of them**. Not patched over — *absent*, the way a real human's browser is.
35
+
36
+ ```python
37
+ pip install thunder-agent
38
+ ```
39
+
40
+ ```python
41
+ import thunder
42
+
43
+ body = thunder.Body(profile="ada") # a persistent human named Ada
44
+ await body.warmup() # age the session like a returning user
45
+ await body.go("https://protected-site.com")
46
+ await body.type("#email", "ada@example.com") # typed like a person. Typos included.
47
+ await body.look("proof.png") # screenshot evidence
48
+ await body.rest() # close, keep the identity
49
+ ```
50
+
51
+ Ada comes back tomorrow with the same cookies, same screen, same GPU string, same timezone. The site sees a returning regular.
52
+
53
+ ---
54
+
55
+ ## 📦 Install in 30 seconds
56
+
57
+ ```bash
58
+ pip install thunder-agent
59
+ ```
60
+
61
+ Works on Linux, macOS, Windows. Real-Chrome engine auto-detected; falls back to bundled Chromium.
62
+
63
+ ```bash
64
+ thunder demo # watch it browse like a human, right now
65
+ thunder ghost https://nowsecure.nl # TLS-level fetch that passes the gate
66
+ ```
67
+
68
+ ---
69
+
70
+ ## ⚡ The five layers
71
+
72
+ | Layer | What it does | The tell it kills |
73
+ |---|---|---|
74
+ | **Body** | Stealth browser: real Google Chrome, persistent profiles | headless shells, disposable identities |
75
+ | **Skin** | One consistent identity: UA, platform, GPU, screen, timezone, locale | fingerprint mismatches, `SwiftShader` WebGL |
76
+ | **Hand** | Bézier mouse with *hand tremor*, human keystroke cadence, typos it fixes | straight-line paths, machine-perfect timing |
77
+ | **Ghost** | Pure HTTP with Chrome's exact TLS/JA3 fingerprint + full header orchestration + human-style retries | Python-scrape handshake = instant 403 |
78
+ | **Pool** | Thunder's own proxy pool: auto-fetched, Ghost-validated egress, sticky per profile | shared/flagged datacenter IPs |
79
+ | **Warmup** | Organic light browsing before your target | cold first-hit sessions |
80
+
81
+ Verified live: `bot.sannysoft.com` under Thunder — **WebDriver missing (passed) · WebDriver Advanced passed · Chrome present (passed) · PluginArray passed** — *4 passed, 0 failed* with headed real Chrome. Ghost pulls a **200** from a Cloudflare gate that 403s plain Python, same minute.
82
+
83
+ ---
84
+
85
+ ## 🔌 MCP: give ANY software a human body
86
+
87
+ **Zero-install, zero-account — paste this URL into Claude** (Settings → Connectors → Add custom connector, or claude.ai/mcp):
88
+
89
+ ```
90
+ https://thunder-mcp.onrender.com/mcp
91
+ ```
92
+
93
+ That's it. No signup, no keys. Thunder-hosted, always on. You get `thunder_search`, `thunder_read`, `thunder_ghost`, `thunder_feel`, `thunder_solve`, `thunder_open` — search, clean extraction and bot-gate-safe fetches through Chrome's exact TLS handshake, from inside Claude.
94
+
95
+ Want the *full* human body (stealth Chrome, mouse tremor, human typing) under MCP? Run it locally — same 10-tool MCP server:
96
+
97
+ Thunder ships an MCP server (10 tools). Paste one config and Claude, Cursor, Cline, Windsurf — anything that speaks MCP — browses as a human:
98
+
99
+ ```json
100
+ { "mcpServers": { "thunder": {
101
+ "command": "pipx", "args": ["run", "thunder-agent", "thunder-mcp"] } } }
102
+ ```
103
+
104
+ ```json
105
+ { "mcpServers": { "thunder": {
106
+ "command": "uvx", "args": ["--from", "thunder-agent", "thunder-mcp"] } } }
107
+ ```
108
+
109
+ Tools: `thunder_open` · `thunder_read` · `thunder_search` · `thunder_click` · `thunder_type` · `thunder_scroll` · `thunder_ghost` · `thunder_solve` · `thunder_feel` · `thunder_close` — each one moves like a person, never like a script.
110
+
111
+ ---
112
+
113
+ ## 🚫 The blocker table — what used to stop bots, and Thunder's answer
114
+
115
+ | Blocker | Thunder's counter |
116
+ |---|---|
117
+ | Datacenter IP suspicion | `ProxyManager`: sticky per-profile residential proxies, health-checked, auto-rotation — `THUNDER_PROXY` env or `ProxyManager.from_env()` |
118
+ | No GPU / software WebGL | Full WebGL1 **and** WebGL2 spoof: vendor + renderer read as a real consumer GPU, on every API path detectors use |
119
+ | Headless leaks | Headless-evasion pack: `outerWidth/Height`, `colorDepth`, broken-image dimensions, full `window.chrome` shape, battery API |
120
+ | Advanced CAPTCHAs | Keyless `soft_solve` (human-hand click inside challenges) + `ocr_solve` (local OCR for image/math) + CapSolver/2Captcha bridge for the hardest |
121
+ | Behavioral ML | Hand tremor + micro-corrections + warmup sessions + thinking pauses — entropy that reads as a person, because it moves like one |
122
+
123
+ ## 🔒 CAPTCHAs without a subscription
124
+
125
+ ```python
126
+ from thunder.captcha import soft_solve, ocr_solve
127
+ result = await soft_solve(body.page) # human-hand click, no keys needed
128
+ ```
129
+
130
+ Non-interactive challenges often pass on a warmed-up session. For Turnstile/reCAPTCHA Enterprise, wire your own CapSolver or 2Captcha key — Thunder senses the challenge type and routes it.
131
+
132
+ ---
133
+
134
+ ## 🧠 Pair with any mind
135
+
136
+ Thunder is the body; bring any brain. Works with stewmind, Ollama, LM Studio, vLLM, OpenAI-compatible APIs, or fully offline transformers.
137
+
138
+ ```python
139
+ from thunder.mind import Mind
140
+ mind = Mind(provider="ollama", model="llama3")
141
+ plan = await mind.think("find the pricing page and extract the tiers", body)
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 🧪 Tested, not promised
147
+
148
+ ```bash
149
+ git clone https://github.com/emmanuelerogian723-alt/thunder
150
+ cd thunder && pip install -e ".[dev]" && pytest
151
+ ```
152
+
153
+ Stealth regression suite: webdriver erased + scan-invisible · genuine PluginArray · headless pack · WebGL skin-consistency · proxy rotation. Each one fails loudly if a future commit breaks the human illusion.
154
+
155
+ ---
156
+
157
+ ## ⚖️ The honest limits (why people trust Thunder)
158
+
159
+ - No tool on Earth is "undetectable." Anyone selling that is lying to you.
160
+ - Cloud IPs still look like clouds — pair Thunder with a residential proxy for the hardest targets.
161
+ - Google-scale behavioral ML scores the whole session. Thunder's warmup + hand give you a real chance; it's not a magic key.
162
+ - **Use Thunder only where you're authorized.** Your accounts, your tests, your sites. That's the license, and it's the line.
163
+
164
+ MIT licensed. Built in the open — every claim on this page has a script in `tests/` or a screenshot in `proofs/`.
165
+
166
+ ---
167
+
168
+ <div align="center">
169
+
170
+ **Watch Thunder beat the bot-detection gauntlet, live:**
171
+
172
+ **https://emmanuelerogian723-alt.github.io/thunder/**
173
+
174
+ ⭐ If this saved you a month of anti-detect work, star the repo — it helps others find it.
175
+
176
+ *Thunder — built so software can finally act human.* 🌩️
177
+
178
+ </div>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "thunder-agent"
7
- version = "0.2.2"
7
+ version = "0.4.0"
8
8
  description = "Thunder — the human engine for AI agents: stealth browser body, human hands, TLS ghost, CAPTCHA bridge, and a stewmind thought layer."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,60 @@
1
+ """Stealth regression: every known tell must stay covered. (sync, no pytest-asyncio)"""
2
+ import asyncio
3
+ import pytest
4
+ from thunder.body import Body
5
+ from thunder.proxy import _normalize, ProxyManager
6
+
7
+ @pytest.fixture(scope="module")
8
+ def body():
9
+ b = Body(profile="stealth-test", headless=True)
10
+ asyncio.run(b.wake("https://example.com"))
11
+ yield b
12
+ asyncio.run(b.rest())
13
+
14
+ def test_webdriver_erased(body):
15
+ r = asyncio.run(body.page.evaluate(
16
+ "(() => ({w: navigator.webdriver, scan: (() => { let f=null;"
17
+ " for (const k in navigator) if (/web/i.test(k)&&/driver/i.test(k)) f=k;"
18
+ " return f; })()}))()"))
19
+ assert r["w"] in (None, False) and r["scan"] is None
20
+
21
+ def test_plugins(body):
22
+ r = asyncio.run(body.page.evaluate(
23
+ "(() => ({pa: navigator.plugins instanceof PluginArray,"
24
+ " l: navigator.plugins.length}))()"))
25
+ assert r["pa"] is True and r["l"] == 5
26
+
27
+ def test_headless_pack(body):
28
+ r = asyncio.run(body.page.evaluate(
29
+ "(() => ({ow: window.outerWidth, oh: window.outerHeight,"
30
+ " cd: screen.colorDepth, ch: !!window.chrome,"
31
+ " bt: typeof navigator.getBattery}))()"))
32
+ assert r["ow"] > 0 and r["oh"] > 0 and r["cd"] == 24 and r["ch"] is True
33
+ assert r["bt"] == "function"
34
+
35
+ def test_webgl(body):
36
+ r = asyncio.run(body.page.evaluate(
37
+ """(() => { const c=document.createElement('canvas');
38
+ const gl=c.getContext('webgl2')||c.getContext('webgl');
39
+ const ext=gl.getExtension('WEBGL_debug_renderer_info');
40
+ return {v: gl.getParameter(ext.UNMASKED_VENDOR_WEBGL),
41
+ r: gl.getParameter(ext.UNMASKED_RENDERER_WEBGL),
42
+ d: gl.getParameter(37446)}; })()"""))
43
+ assert r["v"] == body.skin.webgl_vendor
44
+ assert r["d"] == r["r"]
45
+ assert "SwiftShader" not in r["r"]
46
+
47
+ def test_proxy_normalize():
48
+ assert _normalize("host:1234")["server"] == "http://host:1234"
49
+ p = _normalize("user:pass@1.2.3.4:8080")
50
+ assert p["server"] == "http://1.2.3.4:8080" and p["username"] == "user"
51
+ assert _normalize("socks5://h:1")["server"] == "socks5://h:1"
52
+
53
+ def test_proxy_sticky_rotation():
54
+ pm = ProxyManager([{"server": "http://a:1"}, {"server": "http://b:2"}])
55
+ a = pm.pick("ada"); b = pm.pick("ada")
56
+ assert a == b
57
+ c = pm.pick("bob")
58
+ assert c["server"] != a["server"]
59
+ pm.mark_bad(a, cooldown=9999)
60
+ assert pm.pick("ada")["server"] == c["server"]
@@ -24,5 +24,5 @@ from thunder.ghost import Ghost
24
24
  from thunder.mind import StewMind
25
25
  from thunder.fingerprint import Skin, random_skin
26
26
 
27
- __version__ = "0.2.2"
27
+ __version__ = "0.4.0"
28
28
  __all__ = ["Body", "Ghost", "StewMind", "Skin", "random_skin", "NodriverBody", "__version__"]
@@ -42,10 +42,12 @@ class Body:
42
42
  def __init__(self, profile: str = "default", skin: Optional[Skin] = None,
43
43
  engine: str = "playwright-stealth", headless: bool = True,
44
44
  record_video: bool = False, viewport: Optional[dict] = None,
45
- locale: str = "en-US", timezone: str = "Africa/Lagos"):
45
+ locale: str = "en-US", timezone: str = "Africa/Lagos",
46
+ proxy=None):
46
47
  self.profile = profile
47
48
  self.engine = engine
48
49
  self.headless = headless
50
+ self.proxy = proxy
49
51
  self.viewport = viewport
50
52
  self.record = record_video
51
53
  self.locale = locale
@@ -93,6 +95,13 @@ class Body:
93
95
  # nodriver is a stronger, separate engine; supported via
94
96
  # thunder.nodriver_engine when installed (AGPL-3).
95
97
  raise ValueError("use thunder.nodriver_engine.NodriverBody for the nodriver engine")
98
+ if self.proxy:
99
+ p = self.proxy
100
+ if isinstance(p, str):
101
+ from .proxy import _normalize
102
+ p = _normalize(p)
103
+ if p:
104
+ launch_kw["proxy"] = p
96
105
  launch_kw.update({
97
106
  "user_agent": self.skin.user_agent, # header + JS always match the Skin
98
107
  "viewport": {"width": self.skin.window_w, "height": self.skin.window_h},
@@ -132,6 +141,13 @@ class Body:
132
141
  except Exception:
133
142
  pass
134
143
 
144
+ async def warmup(self, visits: int = 2) -> dict:
145
+ """Age the session with organic light browsing so arriving at
146
+ your target reads like a returning human, not a cold first hit.
147
+ Big targets score the WHOLE session — this is the counter."""
148
+ from .warmup import warmup
149
+ return await warmup(self, visits=visits)
150
+
135
151
  async def rest(self):
136
152
  """Close the body gracefully (context persists to disk)."""
137
153
  try:
@@ -151,3 +151,76 @@ async def describe_challenge(page, mind=None) -> str:
151
151
  if mind is None:
152
152
  return prompt # caller routes screenshot + prompt to a vision model
153
153
  return prompt
154
+
155
+
156
+ # ── KEYLESS FALLBACKS ─────────────────────────────────────────────
157
+ # Thunder avoids most challenges with skin + hand. These two give you
158
+ # a real shot at the rest WITHOUT any solver subscription.
159
+
160
+ async def soft_solve(page, wait_after: float = 8.0) -> Dict[str, Any]:
161
+ """Human-hand click inside the challenge — no solver key needed.
162
+
163
+ Non-interactive Turnstile and 'just click the box' reCAPTCHA often
164
+ pass when the click comes from a human-shaped hand on a warmed-up
165
+ session instead of an instant synthetic one.
166
+ """
167
+ frames = page.frames
168
+ target = None
169
+ for f in frames:
170
+ if any(k in (f.url or "") for k in ("turnstile", "recaptcha", "hcaptcha")):
171
+ target = f
172
+ break
173
+ if target is None:
174
+ return {"solved": False, "error": "no challenge frame found"}
175
+ try:
176
+ box = await target.locator("body").bounding_box()
177
+ if not box:
178
+ return {"solved": False, "error": "no clickable area"}
179
+ from .human import Hand
180
+ hand = Hand(page)
181
+ # human approach: drift in from nearby, pause, click
182
+ await hand.idle(1.5)
183
+ await hand.click((box["x"] + box["width"] * 0.5 + 15,
184
+ box["y"] + box["height"] * 0.5))
185
+ await page.wait_for_timeout(int(wait_after * 1000))
186
+ token = await page.evaluate("""(() => {
187
+ const el = document.querySelector(
188
+ '[name=cf-turnstile-response],textarea#g-recaptcha-response,[name=h-captcha-response]');
189
+ return el ? el.value : window.turnbridge_token || null; })()""")
190
+ return {"solved": bool(token), "token": (token or "")[:24] + "..."}
191
+ except Exception as e:
192
+ return {"solved": False, "error": str(e)[:120]}
193
+
194
+
195
+ async def ocr_solve(page, input_selector: str = "input",
196
+ img_selector: str = "img") -> Dict[str, Any]:
197
+ """Local OCR for simple image/math CAPTCHAs — still keyless.
198
+
199
+ Uses tesseract when installed (apt install tesseract-ocr).
200
+ Reads the challenge image, types the answer with the human hand.
201
+ """
202
+ try:
203
+ import pytesseract # optional; pip install pytesseract
204
+ except ImportError:
205
+ return {"solved": False,
206
+ "error": "install pytesseract + tesseract-ocr for local OCR"}
207
+ import base64
208
+ img = page.locator(img_selector).first
209
+ data = await img.screenshot()
210
+ import io
211
+ from PIL import Image
212
+ text_img = Image.open(io.BytesIO(data)).convert("L")
213
+ answer = pytesseract.image_to_string(
214
+ text_img, config="--psm 7 -c tessedit_char_whitelist=0123456789+-*=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
215
+ ).strip()
216
+ if not answer:
217
+ return {"solved": False, "error": "OCR read nothing"}
218
+ # simple arithmetic like "7 + 4 ="
219
+ import re
220
+ m = re.search(r"(\d+)\s*([+\-x*])\s*(\d+)", answer)
221
+ if m:
222
+ a, op, c = int(m.group(1)), m.group(2), int(m.group(3))
223
+ answer = str(a + c if op == "+" else a - c if op == "-" else a * c)
224
+ from .human import Hand
225
+ await Hand(page).type(input_selector, answer)
226
+ return {"solved": True, "answer": answer}