webcrawl 1.4.1 → 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.
@@ -219,10 +219,105 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
219
219
  }
220
220
  collected.push(...jina);
221
221
  }
222
+ // HTML SERPs (DDG, Brave) are frequently challenge/429 pages. When they
223
+ // yield nothing, fall back to keyless APIs that still work without a key.
224
+ if (collected.length === 0 && (!req.provider || req.provider === "auto")) {
225
+ const extras = await fetchKeylessSearchFallbacks(localizedQuery, perEngineLimit);
226
+ collected.push(...extras);
227
+ }
222
228
  return collected;
223
229
  }
224
230
  /** Below this many unique URLs from the free engines, the Jina fallback runs. */
225
231
  export const JINA_FALLBACK_MIN_URLS = 3;
232
+ /** True when a scraped SERP is a bot-check / captcha rather than results. */
233
+ export function isSerpChallengeHtml(html, status = 200) {
234
+ if (status === 403 || status === 429 || status === 503)
235
+ return true;
236
+ const lower = String(html ?? "").slice(0, 8000).toLowerCase();
237
+ return (lower.includes("bots use duckduckgo") ||
238
+ lower.includes("anomaly-modal") ||
239
+ lower.includes("please complete the following challenge") ||
240
+ lower.includes("schedule a captcha") ||
241
+ lower.includes("flagged as being suspicious") ||
242
+ lower.includes("enable javascript to function"));
243
+ }
244
+ export const EMPTY_SEARCH_ERROR = "Keyless search engines returned no results (DuckDuckGo and Brave often serve a bot challenge). " +
245
+ "Tried Wikipedia, Hacker News, and Bing RSS as well. " +
246
+ "Set BRAVE_SEARCH_API_KEY, SERPER_API_KEY, or JINA_API_KEY, or retry later.";
247
+ /** Parse Bing's `format=rss` web-search feed into candidate docs. */
248
+ export function parseBingWebRss(xml, limit) {
249
+ const items = [];
250
+ const itemRe = /<item>([\s\S]*?)<\/item>/gi;
251
+ let match;
252
+ while ((match = itemRe.exec(xml)) !== null && items.length < limit) {
253
+ const block = match[1];
254
+ const title = decodeHtmlEntities((block.match(/<title>([\s\S]*?)<\/title>/i)?.[1] || "")
255
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
256
+ .trim());
257
+ const link = (block.match(/<link>([\s\S]*?)<\/link>/i)?.[1] || "")
258
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
259
+ .trim();
260
+ const description = decodeHtmlEntities((block.match(/<description>([\s\S]*?)<\/description>/i)?.[1] || "")
261
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
262
+ .replace(/<[^>]+>/g, " ")
263
+ .replace(/\s+/g, " ")
264
+ .trim());
265
+ if (!title || !link.startsWith("http"))
266
+ continue;
267
+ items.push({
268
+ title,
269
+ url: link,
270
+ description: description || title,
271
+ engine: "bing-rss",
272
+ engineRank: items.length + 1,
273
+ });
274
+ }
275
+ return items;
276
+ }
277
+ async function fetchBingWebRss(query, limit) {
278
+ try {
279
+ const rssUrl = `https://www.bing.com/search?format=rss&q=${encodeURIComponent(query)}`;
280
+ const res = await fetch(rssUrl, {
281
+ headers: {
282
+ Accept: "application/rss+xml, application/xml, text/xml;q=0.9, */*;q=0.8",
283
+ "User-Agent": "Mozilla/5.0 (compatible; WebCrawl/1.4; +https://webcrawl.app)",
284
+ },
285
+ signal: AbortSignal.timeout(4000),
286
+ });
287
+ if (!res.ok)
288
+ return [];
289
+ const xml = await res.text();
290
+ if (!xml.includes("<item>"))
291
+ return [];
292
+ return parseBingWebRss(xml, limit);
293
+ }
294
+ catch {
295
+ return [];
296
+ }
297
+ }
298
+ /**
299
+ * Last-resort keyless engines used when DDG/Brave HTML SERPs come back empty.
300
+ * Wikipedia OpenSearch, HN Algolia, and Bing RSS do not require a JS challenge.
301
+ */
302
+ export async function fetchKeylessSearchFallbacks(query, limit) {
303
+ const per = Math.max(limit, 5);
304
+ const [hn, wiki, bing] = await Promise.all([
305
+ fetchHackerNewsAlgolia(query, per),
306
+ fetchWikipediaOpenSearch(query, per),
307
+ fetchBingWebRss(query, per),
308
+ ]);
309
+ const seen = new Set();
310
+ const out = [];
311
+ for (const item of [...hn, ...wiki, ...bing]) {
312
+ if (!item.url || seen.has(item.url))
313
+ continue;
314
+ seen.add(item.url);
315
+ out.push(item);
316
+ if (out.length >= Math.max(limit + 4, 8))
317
+ break;
318
+ }
319
+ return out;
320
+ }
226
321
  export async function fetchJinaSearch(query, limit, req) {
227
322
  const key = process.env.JINA_API_KEY;
228
323
  if (!key) {
@@ -435,7 +530,7 @@ export async function fetchBraveNativeSerp(query, limit, userLocation) {
435
530
  : "";
436
531
  const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}&source=web${countryParam}`;
437
532
  const res = await fetchWithTlsImpersonation(url, 2200);
438
- if (res.status !== 200 || !res.html) {
533
+ if (res.status !== 200 || !res.html || isSerpChallengeHtml(res.html, res.status)) {
439
534
  return [];
440
535
  }
441
536
  const html = res.html;
@@ -545,7 +640,7 @@ async function fetchDuckDuckGoLiteOnly(query, limit, userLocation) {
545
640
  const kl = ddgRegion(countryCode, undefined);
546
641
  const klParam = kl ? `&kl=${encodeURIComponent(kl)}` : "";
547
642
  const liteRes = await fetchWithTlsImpersonation(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}${klParam}`, 2200);
548
- if (liteRes && liteRes.html) {
643
+ if (liteRes && liteRes.html && !isSerpChallengeHtml(liteRes.html, liteRes.status)) {
549
644
  const html = liteRes.html;
550
645
  const linkMatches = Array.from(html.matchAll(/<a\s+[^>]*href=["']([^"']+)["'][^>]*class=["']result-link["'][^>]*>([\s\S]*?)<\/a>|<a\s+[^>]*class=["']result-link["'][^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi));
551
646
  const snippetMatches = Array.from(html.matchAll(/<td\s+[^>]*class=["']result-snippet["'][^>]*>([\s\S]*?)<\/td>/gi));
package/dist/format.js CHANGED
@@ -49,9 +49,16 @@ export function formatSearch(res) {
49
49
  })
50
50
  .join("\n")}`);
51
51
  }
52
- return parts.length ? parts.join("\n\n") : "No results.";
52
+ if (parts.length)
53
+ return parts.join("\n\n");
54
+ if (res.success === false && str(res.error))
55
+ return `Error: ${str(res.error)}`;
56
+ return "No results.";
53
57
  }
54
58
  export function formatScrape(res) {
59
+ if (res.success === false && str(res.error)) {
60
+ return `Error: ${str(res.error)}`;
61
+ }
55
62
  const d = isRec(res.data) ? res.data : res;
56
63
  return str(d.markdown) || str(d.content) || "(no content returned)";
57
64
  }
package/dist/setup.js CHANGED
@@ -147,18 +147,22 @@ export function runDoctor(env = process.env, effectiveMode = "local", apiKeySour
147
147
  });
148
148
  // 3. Local Chrome / Stealth Browser availability
149
149
  const chromePaths = [
150
+ env.WEBCRAWL_CHROME_PATH,
151
+ env.INTERACT_CHROME_PATH,
150
152
  env.CHROME_PATH,
151
153
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
152
154
  "/usr/bin/google-chrome",
155
+ "/usr/bin/google-chrome-stable",
153
156
  "/usr/bin/chromium",
154
157
  "/usr/bin/chromium-browser",
158
+ "/snap/bin/chromium",
155
159
  ].filter(Boolean);
156
160
  const foundChrome = chromePaths.find((p) => existsSync(p));
157
161
  checks.push({
158
162
  name: "Local stealth browser",
159
163
  status: foundChrome ? "ok" : "optional",
160
164
  detail: foundChrome
161
- ? `Found Chrome at ${foundChrome}`
165
+ ? `Found Chrome at ${foundChrome} (used to render JS pages)`
162
166
  : "No local Chrome binary detected; static/TLS fetch works out of the box, or run `webcrawl login` for cloud stealth browsers",
163
167
  });
164
168
  // 4. Search providers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcrawl",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Webcrawl Anything CLI & Core Engine — crawl, search, and extract structured evidence from any website, SPA, PDF, Office file, YouTube video, GitHub repo, ArXiv paper, or CVE",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -127,8 +127,8 @@
127
127
  "@ai-sdk/gateway": "^4.0.123",
128
128
  "@firecrawl/pdf-inspector": "^1.25.2",
129
129
  "@vercel/sandbox": "^1.0.0",
130
- "@webcrawl/client": "^1.4.1",
131
- "@webcrawl/curl-impersonate": "^1.4.1",
130
+ "@webcrawl/client": "^1.4.3",
131
+ "@webcrawl/curl-impersonate": "^1.4.3",
132
132
  "ai": "^7.0.150",
133
133
  "impit": "^0.14.7",
134
134
  "mammoth": "^1.13.0",