spectrawl 0.3.15 → 0.3.17

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.
package/README.md CHANGED
@@ -1,50 +1,20 @@
1
1
  # Spectrawl
2
2
 
3
- The unified web layer for AI agents. Search, browse, authenticate, and act on platforms — one tool, self-hosted, free.
3
+ The unified web layer for AI agents. Search, browse, authenticate, and act on platforms — one package, self-hosted.
4
4
 
5
- **5,000 free searches/month** with Google-quality results via Gemini Grounded Search. Better answers than Tavily. Self-hosted.
5
+ **5,000 free searches/month** via Gemini Grounded Search. Full page scraping, stealth browsing, 24 platform adapters.
6
6
 
7
7
  ## What It Does
8
8
 
9
- AI agents need to interact with the web. That means searching, browsing pages, logging into platforms, and posting content. Today you duct-tape together Playwright + Tavily + cookie managers + platform-specific scripts. Spectrawl replaces all of that.
9
+ AI agents need to interact with the web searching, browsing pages, logging into platforms, posting content. Today you wire together Playwright + a search API + cookie managers + platform-specific scripts. Spectrawl is one package that does all of it.
10
10
 
11
11
  ```
12
12
  npm install spectrawl
13
13
  ```
14
14
 
15
- ## Real Output
15
+ ## How It Works
16
16
 
17
- Here's actual output from Spectrawl vs Tavily on the same query:
18
-
19
- **Query:** `"best open source AI agent frameworks 2025"`
20
-
21
- ### Spectrawl (free)
22
- ```
23
- Time: 16.8s | Sources: 19
24
-
25
- Answer: The leading open-source AI agent frameworks for 2025 include AutoGen,
26
- CrewAI, LangChain, LangGraph, and Semantic Kernel [1, 2, 3]. AutoGen is
27
- recognized for enabling complex multi-agent conversations, while CrewAI
28
- focuses on orchestrating collaborative AI agents [1, 2]. LangChain and its
29
- component LangGraph provide robust tools for building sophisticated agent
30
- workflows and state management [1, 2, 3]. Semantic Kernel, developed by
31
- Microsoft, integrates large language models with conventional programming
32
- languages [1, 2, 3].
33
-
34
- Other prominent frameworks include LlamaIndex, Haystack, BabyAGI, AgentGPT,
35
- SuperAGI, MetaGPT, and Open Interpreter [1, 2].
36
- ```
37
- **12 frameworks named, inline citations, 19 sources**
38
-
39
- ### Tavily ($0.01/query)
40
- ```
41
- Time: 2s | Sources: 10
42
-
43
- Answer: In 2025, LangGraph and Microsoft's AutoGen + Semantic Kernel are
44
- top open-source AI agent frameworks, favored for their robust orchestration
45
- and enterprise security features.
46
- ```
47
- **3 frameworks named, no citations, 10 sources**
17
+ Spectrawl searches via Gemini Grounded Search (Google-quality results), scrapes the top pages for full content, and returns everything to your agent. Your agent's LLM reads the actual sources and forms its own answer no pre-chewed summaries.
48
18
 
49
19
  ## Quick Start
50
20
 
@@ -74,22 +44,22 @@ const basic = await web.search('query')
74
44
 
75
45
  > **Why no summary by default?** Your agent already has an LLM. If we summarize AND your agent summarizes, you're paying two LLMs for one answer. We return rich sources — your agent does the rest.
76
46
 
77
- ## vs Tavily
47
+ ## Spectrawl vs Tavily
48
+
49
+ Different tools for different needs.
78
50
 
79
51
  | | Tavily | Spectrawl |
80
52
  |---|---|---|
81
- | Speed | ~2s | ~7-17s |
82
- | Answer quality | Generic (3 items) | **Detailed** (12+ items) ✅ |
83
- | Inline citations | | **[1] [2] [3]** |
84
- | Results per query | 10 | **12-19** |
85
- | Cost | $0.01/query | **Free** (5K/mo) |
86
- | Self-hosted | No | **Yes** |
87
- | Source ranking | No | **Domain trust scoring** |
88
- | Stealth scraping | No | **Yes** |
89
- | Auth + posting | No | **24 adapters** ✅ |
90
- | Cached repeats | No | **<1ms** |
91
-
92
- Spectrawl wins on answer quality, result volume, features, and cost. Tavily wins on speed.
53
+ | Speed | ~2s | ~6-10s |
54
+ | Free tier | 1,000/month | 5,000/month |
55
+ | Returns | Snippets + AI answer | Full page content + snippets |
56
+ | Self-hosted | No | Yes |
57
+ | Stealth browsing | No | Yes (Camoufox + Playwright) |
58
+ | Platform posting | No | 24 adapters |
59
+ | Auth management | No | Cookie store + auto-refresh |
60
+ | Cached repeats | No | <1ms |
61
+
62
+ **Tavily** is fast and simple great for agents that need quick answers. **Spectrawl** returns richer data and does more (browse, auth, post) — but it's slower. Choose based on your use case.
93
63
 
94
64
  ## Search
95
65
 
package/index.d.ts CHANGED
@@ -17,6 +17,7 @@ declare module 'spectrawl' {
17
17
  defaultEngine?: string
18
18
  proxy?: { type: string; host: string; port: number; username?: string; password?: string }
19
19
  humanlike?: { minDelay?: number; maxDelay?: number; scrollBehavior?: boolean }
20
+ captcha?: { apiKey?: string; model?: string }
20
21
  }
21
22
  auth?: {
22
23
  refreshInterval?: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrawl",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "The unified web layer for AI agents. Search (6 engines), stealth browse (Camoufox + Playwright), auth (cookies, multi-account), act (24 adapters, 30+ platforms), proxy rotation. Self-hosted, free.",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,162 @@
1
+ const https = require('https')
2
+ const fs = require('fs')
3
+
4
+ /**
5
+ * CAPTCHA solver using Gemini Vision.
6
+ * Free tier: 1,500 req/day (gemini-2.0-flash).
7
+ *
8
+ * Handles: image CAPTCHAs, text CAPTCHAs, simple challenges.
9
+ * Does NOT handle: reCAPTCHA v2/v3, hCaptcha, Cloudflare Turnstile
10
+ * (those require token solving services like 2captcha).
11
+ *
12
+ * Strategy: Playwright stealth bypasses most CAPTCHAs.
13
+ * This is the fallback when a visual CAPTCHA appears.
14
+ */
15
+
16
+ class CaptchaSolver {
17
+ constructor(config = {}) {
18
+ this.apiKey = config.apiKey || process.env.GEMINI_API_KEY
19
+ this.model = config.model || 'gemini-2.0-flash'
20
+ }
21
+
22
+ /**
23
+ * Detect if a page has a CAPTCHA challenge.
24
+ * Returns { hasCaptcha, type, selector } or null.
25
+ */
26
+ async detect(page) {
27
+ return page.evaluate(() => {
28
+ // Check for common CAPTCHA indicators
29
+ const indicators = [
30
+ // reCAPTCHA
31
+ { selector: '.g-recaptcha, #recaptcha, [data-sitekey]', type: 'recaptcha' },
32
+ // hCaptcha
33
+ { selector: '.h-captcha, [data-hcaptcha-sitekey]', type: 'hcaptcha' },
34
+ // Cloudflare Turnstile
35
+ { selector: '.cf-turnstile, [data-turnstile-sitekey]', type: 'turnstile' },
36
+ // Image CAPTCHA (solvable with vision)
37
+ { selector: 'img[src*="captcha"], img[alt*="captcha"], .captcha-image', type: 'image' },
38
+ // Text/math CAPTCHA
39
+ { selector: '[class*="captcha"] input, #captcha-input', type: 'text' },
40
+ ]
41
+
42
+ for (const { selector, type } of indicators) {
43
+ const el = document.querySelector(selector)
44
+ if (el) return { hasCaptcha: true, type, selector }
45
+ }
46
+
47
+ // Check page text for CAPTCHA mentions
48
+ const bodyText = document.body?.innerText?.toLowerCase() || ''
49
+ if (bodyText.includes('verify you are human') || bodyText.includes('complete the captcha')) {
50
+ return { hasCaptcha: true, type: 'unknown', selector: null }
51
+ }
52
+
53
+ return { hasCaptcha: false, type: null, selector: null }
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Attempt to solve a visual CAPTCHA using Gemini Vision.
59
+ * Takes a screenshot of the CAPTCHA element, sends to Gemini, returns answer.
60
+ */
61
+ async solveImage(page, captchaSelector) {
62
+ if (!this.apiKey) {
63
+ throw new Error('GEMINI_API_KEY required for CAPTCHA solving')
64
+ }
65
+
66
+ // Screenshot the CAPTCHA element
67
+ const element = await page.$(captchaSelector)
68
+ if (!element) throw new Error(`CAPTCHA element not found: ${captchaSelector}`)
69
+
70
+ const screenshot = await element.screenshot({ type: 'png' })
71
+ const base64 = screenshot.toString('base64')
72
+
73
+ // Ask Gemini to solve it
74
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`
75
+ const body = JSON.stringify({
76
+ contents: [{
77
+ parts: [
78
+ { text: 'This is a CAPTCHA image. What text, numbers, or answer does it show? Reply with ONLY the answer, nothing else.' },
79
+ { inline_data: { mime_type: 'image/png', data: base64 } }
80
+ ]
81
+ }],
82
+ generationConfig: { temperature: 0, maxOutputTokens: 100 }
83
+ })
84
+
85
+ const data = await this._post(url, body)
86
+ const answer = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim()
87
+ return answer || null
88
+ }
89
+
90
+ /**
91
+ * Full solve flow: detect → solve → fill → submit.
92
+ * Returns true if solved, false if unsolvable type.
93
+ */
94
+ async trySolve(page) {
95
+ const detection = await this.detect(page)
96
+ if (!detection?.hasCaptcha) return { solved: false, reason: 'no captcha detected' }
97
+
98
+ // Token-based CAPTCHAs — can't solve with vision
99
+ if (['recaptcha', 'hcaptcha', 'turnstile'].includes(detection.type)) {
100
+ return { solved: false, reason: `${detection.type} requires token solving (2captcha/CapSolver)` }
101
+ }
102
+
103
+ // Image CAPTCHA — solve with Gemini Vision
104
+ if (detection.type === 'image') {
105
+ try {
106
+ const answer = await this.solveImage(page, detection.selector)
107
+ if (!answer) return { solved: false, reason: 'gemini could not read captcha' }
108
+
109
+ // Find the input field near the CAPTCHA
110
+ const inputSelector = await page.evaluate((captchaSelector) => {
111
+ const captcha = document.querySelector(captchaSelector)
112
+ // Look for nearby input
113
+ const parent = captcha?.closest('form') || captcha?.parentElement
114
+ const input = parent?.querySelector('input[type="text"], input:not([type])')
115
+ if (input) {
116
+ input.id = input.id || '__spectrawl_captcha_input'
117
+ return '#' + input.id
118
+ }
119
+ return null
120
+ }, detection.selector)
121
+
122
+ if (inputSelector) {
123
+ await page.fill(inputSelector, answer)
124
+ return { solved: true, answer, type: 'image' }
125
+ }
126
+ return { solved: false, reason: 'could not find captcha input field', answer }
127
+ } catch (e) {
128
+ return { solved: false, reason: e.message }
129
+ }
130
+ }
131
+
132
+ return { solved: false, reason: `unsupported captcha type: ${detection.type}` }
133
+ }
134
+
135
+ _post(url, body) {
136
+ return new Promise((resolve, reject) => {
137
+ const urlObj = new URL(url)
138
+ const req = https.request({
139
+ hostname: urlObj.hostname,
140
+ path: urlObj.pathname + urlObj.search,
141
+ method: 'POST',
142
+ headers: {
143
+ 'Content-Type': 'application/json',
144
+ 'Content-Length': Buffer.byteLength(body)
145
+ }
146
+ }, res => {
147
+ let data = ''
148
+ res.on('data', c => data += c)
149
+ res.on('end', () => {
150
+ try { resolve(JSON.parse(data)) }
151
+ catch (e) { reject(new Error('Invalid Gemini response')) }
152
+ })
153
+ })
154
+ req.on('error', reject)
155
+ req.setTimeout(15000, () => { req.destroy(); reject(new Error('Gemini vision timeout')) })
156
+ req.write(body)
157
+ req.end()
158
+ })
159
+ }
160
+ }
161
+
162
+ module.exports = { CaptchaSolver }
@@ -12,6 +12,7 @@ const os = require('os')
12
12
  const path = require('path')
13
13
  const { CamoufoxClient } = require('./camoufox')
14
14
  const { getCamoufoxPath, isInstalled } = require('./install-stealth')
15
+ const { CaptchaSolver } = require('./captcha-solver')
15
16
 
16
17
  class BrowseEngine {
17
18
  constructor(config = {}, cache) {
@@ -23,6 +24,9 @@ class BrowseEngine {
23
24
  this.remoteCamoufox = config.camoufox?.url ? new CamoufoxClient(config.camoufox) : null
24
25
  this._remoteCamoufoxAvailable = null
25
26
 
27
+ // CAPTCHA solver (Gemini Vision fallback)
28
+ this.captchaSolver = new CaptchaSolver(config.captcha || {})
29
+
26
30
  // Which engine we're using
27
31
  this._engine = null
28
32
  }
@@ -114,15 +114,60 @@ async function install() {
114
114
  // Download
115
115
  await download(url, zipPath)
116
116
 
117
- // Extract
117
+ // Extract — try multiple methods (large zip64 files break some tools)
118
118
  console.log(' Extracting...')
119
119
  fs.mkdirSync(extractDir, { recursive: true })
120
120
 
121
- try {
122
- execSync(`unzip -o "${zipPath}" -d "${extractDir}"`, { stdio: 'pipe' })
123
- } catch (e) {
124
- // Try with built-in tools on systems without unzip
125
- execSync(`python3 -c "import zipfile; zipfile.ZipFile('${zipPath}').extractall('${extractDir}')"`, { stdio: 'pipe' })
121
+ const extractMethods = [
122
+ // 1. unzip (most common on Linux/Mac)
123
+ () => execSync(`unzip -o "${zipPath}" -d "${extractDir}"`, { stdio: 'pipe' }),
124
+ // 2. 7z (handles zip64 reliably)
125
+ () => execSync(`7z x -o"${extractDir}" -y "${zipPath}"`, { stdio: 'pipe' }),
126
+ // 3. bsdtar (available on many systems, handles zip64)
127
+ () => execSync(`bsdtar -xf "${zipPath}" -C "${extractDir}"`, { stdio: 'pipe' }),
128
+ // 4. Node.js built-in (no external deps, handles zip64)
129
+ () => {
130
+ const { execSync: es } = require('child_process')
131
+ es(`node -e "
132
+ const fs = require('fs');
133
+ const zlib = require('zlib');
134
+ const { execFileSync } = require('child_process');
135
+ // Use jar if available (JDK)
136
+ execFileSync('jar', ['xf', '${zipPath}'], { cwd: '${extractDir}', stdio: 'pipe' });
137
+ "`, { stdio: 'pipe' })
138
+ },
139
+ // 5. Python with explicit zip64 support
140
+ () => execSync(`python3 -c "
141
+ import zipfile, sys
142
+ try:
143
+ z = zipfile.ZipFile('${zipPath}', allowZip64=True)
144
+ z.extractall('${extractDir}')
145
+ z.close()
146
+ except Exception as e:
147
+ print(f'Python extract failed: {e}', file=sys.stderr)
148
+ sys.exit(1)
149
+ "`, { stdio: 'pipe' }),
150
+ ]
151
+
152
+ let extracted = false
153
+ for (const method of extractMethods) {
154
+ try {
155
+ method()
156
+ extracted = true
157
+ break
158
+ } catch (e) {
159
+ continue
160
+ }
161
+ }
162
+
163
+ if (!extracted) {
164
+ fs.unlinkSync(zipPath)
165
+ throw new Error(
166
+ 'Could not extract Camoufox archive. Install one of: unzip, 7z, or bsdtar.\n' +
167
+ ' Ubuntu/Debian: sudo apt-get install unzip\n' +
168
+ ' macOS: brew install p7zip\n' +
169
+ ' Alpine: apk add unzip'
170
+ )
126
171
  }
127
172
 
128
173
  // Clean up zip