@webcrawl/client 1.2.0 → 1.2.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.
package/dist/client.d.ts CHANGED
@@ -14,6 +14,7 @@ export declare class WebcrawlClient {
14
14
  private readonly apiKey?;
15
15
  private readonly timeoutMs;
16
16
  private readonly fetchImpl;
17
+ private readonly hasCustomFetch;
17
18
  private readonly userAgent;
18
19
  private readonly timezone?;
19
20
  constructor(opts?: ClientOptions);
@@ -31,5 +32,49 @@ export declare class WebcrawlClient {
31
32
  categories(): Promise<ApiResponse>;
32
33
  targets(): Promise<ApiResponse>;
33
34
  usage(): Promise<ApiResponse>;
35
+ /** Send a 6-digit OTP verification code to an email address to sign up or log in (provisions 1,000 free monthly credits). */
36
+ sendOtp(email: string): Promise<{
37
+ success: boolean;
38
+ email: string;
39
+ expiresInSeconds: number;
40
+ transport?: string;
41
+ devCode?: string;
42
+ error?: string;
43
+ }>;
44
+ /** Verify a 6-digit OTP code, creating the user account (with 1,000 monthly credits) if new, and return a `wc_live_...` API key. */
45
+ verifyOtp(params: {
46
+ email: string;
47
+ code: string;
48
+ name?: string;
49
+ }): Promise<{
50
+ success: boolean;
51
+ isNewUser?: boolean;
52
+ apiKey?: string;
53
+ sessionToken?: string;
54
+ user?: {
55
+ id: string;
56
+ email: string;
57
+ name?: string | null;
58
+ plan: string;
59
+ creditsLimit: number;
60
+ creditsUsed: number;
61
+ creditsRemaining: number;
62
+ };
63
+ error?: string;
64
+ }>;
65
+ /** Fetch authenticated user profile & remaining credit balance. */
66
+ me(): Promise<{
67
+ success: boolean;
68
+ authenticated: boolean;
69
+ user?: {
70
+ id: string;
71
+ email: string;
72
+ name?: string | null;
73
+ plan: string;
74
+ creditsLimit: number;
75
+ creditsUsed: number;
76
+ creditsRemaining: number;
77
+ };
78
+ }>;
34
79
  }
35
80
  export declare function createClient(opts?: ClientOptions): WebcrawlClient;
package/dist/client.js CHANGED
@@ -24,6 +24,7 @@ export class WebcrawlClient {
24
24
  apiKey;
25
25
  timeoutMs;
26
26
  fetchImpl;
27
+ hasCustomFetch;
27
28
  userAgent;
28
29
  timezone;
29
30
  constructor(opts = {}) {
@@ -31,6 +32,7 @@ export class WebcrawlClient {
31
32
  this.apiKey = opts.apiKey;
32
33
  this.timeoutMs = opts.timeoutMs ?? 60_000;
33
34
  this.fetchImpl = opts.fetch ?? globalThis.fetch;
35
+ this.hasCustomFetch = Boolean(opts.fetch);
34
36
  this.userAgent = opts.userAgent ?? "webcrawl-client/0.1";
35
37
  this.timezone = opts.timezone ?? localTimezone();
36
38
  }
@@ -79,13 +81,66 @@ export class WebcrawlClient {
79
81
  }
80
82
  return json;
81
83
  }
82
- search(p) {
84
+ async search(p) {
83
85
  const { sites, scrapeContent, ...rest } = p;
84
- return this.request("POST", "/v1/search", {
85
- ...compact(rest),
86
+ const rawCat = (p.category || "").trim().toLowerCase();
87
+ const rawSub = (p.subcategory || "").trim().toLowerCase();
88
+ const isAcademic = rawCat === "academic" ||
89
+ rawCat === "paper" ||
90
+ rawCat === "papers" ||
91
+ rawCat === "research" ||
92
+ rawCat === "arxiv" ||
93
+ rawCat === "scholar" ||
94
+ rawCat === "scholarly" ||
95
+ rawSub === "arxiv" ||
96
+ rawSub === "papers" ||
97
+ rawSub === "ml" ||
98
+ rawSub === "pubmed" ||
99
+ rawSub === "math";
100
+ const proxyPromise = this.request("POST", "/v1/search", {
101
+ ...compact({ ...rest, ...(isAcademic ? { category: "academic" } : {}) }),
86
102
  ...(sites?.length ? { filters: { sites } } : {}),
87
103
  ...(scrapeContent ? { scrapeOptions: { formats: ["markdown"] } } : {}),
88
104
  });
105
+ if (!isAcademic || this.hasCustomFetch) {
106
+ return proxyPromise;
107
+ }
108
+ const limit = p.limit ?? 5;
109
+ if (!this.apiKey) {
110
+ const scholarlyItems = await fetchAcademicPapersForClient(this.fetchImpl, p.query, limit);
111
+ return {
112
+ success: true,
113
+ category: "academic",
114
+ results: scholarlyItems.slice(0, limit),
115
+ };
116
+ }
117
+ const [proxyRes, scholarlyItems] = await Promise.all([
118
+ proxyPromise.catch(() => ({ success: true, results: [] })),
119
+ fetchAcademicPapersForClient(this.fetchImpl, p.query, limit),
120
+ ]);
121
+ if (!scholarlyItems.length) {
122
+ return proxyRes;
123
+ }
124
+ const existing = Array.isArray(proxyRes.results) ? proxyRes.results : [];
125
+ const merged = [];
126
+ const seen = new Set();
127
+ for (const item of [...scholarlyItems, ...existing]) {
128
+ const url = typeof item.url === "string" ? item.url.toLowerCase().replace(/v\d+$/, "") : "";
129
+ if (!url || seen.has(url)) {
130
+ continue;
131
+ }
132
+ seen.add(url);
133
+ merged.push(item);
134
+ if (merged.length >= limit) {
135
+ break;
136
+ }
137
+ }
138
+ return {
139
+ ...proxyRes,
140
+ success: true,
141
+ category: "academic",
142
+ results: merged,
143
+ };
89
144
  }
90
145
  scrape(p) {
91
146
  return this.request("POST", "/v1/scrape", compact({ url: p.url, formats: p.formats, noCache: p.noCache }));
@@ -118,13 +173,248 @@ export class WebcrawlClient {
118
173
  categories() {
119
174
  return this.request("GET", "/v1/search/categories");
120
175
  }
121
- targets() {
122
- return this.request("GET", "/v1/targets");
176
+ async targets() {
177
+ try {
178
+ return await this.request("GET", "/v1/targets");
179
+ }
180
+ catch (e) {
181
+ if (!this.hasCustomFetch && e instanceof WebcrawlError && (e.status === 404 || e.status === 405)) {
182
+ return { success: true, count: BUILTIN_CLIENT_TARGETS.length, targets: BUILTIN_CLIENT_TARGETS };
183
+ }
184
+ throw e;
185
+ }
123
186
  }
124
187
  usage() {
125
188
  return this.request("GET", "/v1/account/usage");
126
189
  }
190
+ /** Send a 6-digit OTP verification code to an email address to sign up or log in (provisions 1,000 free monthly credits). */
191
+ sendOtp(email) {
192
+ return this.request("POST", "/v1/auth/otp/send", { email: email.trim() });
193
+ }
194
+ /** Verify a 6-digit OTP code, creating the user account (with 1,000 monthly credits) if new, and return a `wc_live_...` API key. */
195
+ async verifyOtp(params) {
196
+ const res = await this.request("POST", "/v1/auth/otp/verify", {
197
+ email: params.email.trim(),
198
+ code: params.code.trim(),
199
+ ...(params.name ? { name: params.name.trim() } : {}),
200
+ });
201
+ if (res.success && !res.apiKey && res.sessionToken) {
202
+ try {
203
+ const sessionClient = new WebcrawlClient({
204
+ baseUrl: this.baseUrl,
205
+ apiKey: res.sessionToken,
206
+ fetch: this.fetchImpl,
207
+ userAgent: this.userAgent,
208
+ });
209
+ const keyRes = await sessionClient.request("POST", "/v1/account/keys", { name: "CLI API Key" });
210
+ if (keyRes.apiKey) {
211
+ res.apiKey = keyRes.apiKey;
212
+ }
213
+ }
214
+ catch {
215
+ // Fallback to sessionToken if key creation fails
216
+ res.apiKey = res.sessionToken;
217
+ }
218
+ }
219
+ return res;
220
+ }
221
+ /** Fetch authenticated user profile & remaining credit balance. */
222
+ me() {
223
+ return this.request("GET", "/v1/auth/me");
224
+ }
225
+ }
226
+ const decodeXmlEntities = (s) => s
227
+ .replace(/&amp;/g, "&")
228
+ .replace(/&lt;/g, "<")
229
+ .replace(/&gt;/g, ">")
230
+ .replace(/&quot;/g, '"')
231
+ .replace(/&#39;/g, "'");
232
+ async function fetchAcademicPapersForClient(fetchImpl, rawQuery, limit) {
233
+ const clean = rawQuery
234
+ .replace(/\([^)]*\)|-?site:\S+/g, " ")
235
+ .replace(/["'`]+/g, " ")
236
+ .replace(/\s+/g, " ")
237
+ .trim();
238
+ if (!clean) {
239
+ return [];
240
+ }
241
+ const arxivIdMatch = clean.match(/\b(\d{4}\.\d{4,5}(?:v\d+)?)\b/i);
242
+ const stop = new Set(["the", "and", "for", "with", "from", "paper", "papers", "arxiv", "research"]);
243
+ const words = clean
244
+ .split(/\s+/)
245
+ .map((w) => w.replace(/[^a-zA-Z0-9.-]/g, ""))
246
+ .filter((w) => w.length >= 2 && !stop.has(w.toLowerCase()))
247
+ .slice(0, 6);
248
+ const searchExpr = arxivIdMatch
249
+ ? `id:${encodeURIComponent(arxivIdMatch[1])}`
250
+ : words.length <= 1
251
+ ? `all:${encodeURIComponent(words[0] || clean)}`
252
+ : words.map((w) => `all:${encodeURIComponent(w)}`).join("+AND+");
253
+ const arxivPromise = (async () => {
254
+ try {
255
+ const url = `https://export.arxiv.org/api/query?search_query=${searchExpr}&start=0&max_results=${Math.max(limit, 6)}`;
256
+ const res = await fetchImpl(url, {
257
+ headers: { "user-agent": "webcrawl-anything/1.2 (https://webcrawl.app)" },
258
+ signal: AbortSignal.timeout(4000),
259
+ });
260
+ if (!res.ok) {
261
+ return [];
262
+ }
263
+ const xml = await res.text();
264
+ const entries = xml.match(/<entry>([\s\S]*?)<\/entry>/gi) || [];
265
+ const out = [];
266
+ for (const entry of entries) {
267
+ if (out.length >= limit) {
268
+ break;
269
+ }
270
+ const title = decodeXmlEntities((entry.match(/<title>([\s\S]*?)<\/title>/i)?.[1] || "").replace(/\s+/g, " ").trim());
271
+ const rawIdUrl = (entry.match(/<id>([\s\S]*?)<\/id>/i)?.[1] || "").trim();
272
+ const idUrl = rawIdUrl.replace(/^http:\/\/arxiv\.org\//i, "https://arxiv.org/");
273
+ const summary = decodeXmlEntities((entry.match(/<summary>([\s\S]*?)<\/summary>/i)?.[1] || "").replace(/\s+/g, " ").trim());
274
+ const published = (entry.match(/<published>([\s\S]*?)<\/published>/i)?.[1] || "").trim();
275
+ const authors = Array.from(entry.matchAll(/<author>[\s\S]*?<name>([\s\S]*?)<\/name>[\s\S]*?<\/author>/gi))
276
+ .map((m) => decodeXmlEntities(m[1].trim()))
277
+ .filter(Boolean)
278
+ .slice(0, 4);
279
+ const pdfUrl = (entry.match(/<link[^>]+title=["']pdf["'][^>]+href=["']([^"']+)["']/i)?.[1] ||
280
+ idUrl.replace("/abs/", "/pdf/")).replace(/^http:\/\//i, "https://");
281
+ if (title && idUrl) {
282
+ const metaPrefix = [
283
+ authors.length ? `Authors: ${authors.join(", ")}` : "",
284
+ pdfUrl ? `PDF: ${pdfUrl}` : "",
285
+ ]
286
+ .filter(Boolean)
287
+ .join(" · ");
288
+ out.push({
289
+ title: `[arXiv] ${title}`,
290
+ url: idUrl,
291
+ description: (metaPrefix ? `${metaPrefix} — ${summary}` : summary).slice(0, 460),
292
+ publishedDate: published || undefined,
293
+ sources: ["arxiv"],
294
+ });
295
+ }
296
+ }
297
+ return out;
298
+ }
299
+ catch {
300
+ return [];
301
+ }
302
+ })();
303
+ const openAlexPromise = (async () => {
304
+ try {
305
+ const url = `https://api.openalex.org/works?search=${encodeURIComponent(clean)}&per-page=${Math.min(limit, 6)}`;
306
+ const res = await fetchImpl(url, {
307
+ headers: { "user-agent": "webcrawl-anything/1.2 (mailto:support@webcrawl.app)" },
308
+ signal: AbortSignal.timeout(3200),
309
+ });
310
+ if (!res.ok) {
311
+ return [];
312
+ }
313
+ const data = (await res.json());
314
+ const out = [];
315
+ for (const w of data.results || []) {
316
+ if (out.length >= limit) {
317
+ break;
318
+ }
319
+ const title = (w.title || "").replace(/\s+/g, " ").trim();
320
+ const targetUrl = w.primary_location?.landing_page_url || w.open_access?.oa_url || w.doi || "";
321
+ if (!title || !targetUrl) {
322
+ continue;
323
+ }
324
+ const authors = (w.authorships || [])
325
+ .map((a) => a.author?.display_name)
326
+ .filter((n) => Boolean(n))
327
+ .slice(0, 4)
328
+ .join(", ");
329
+ const venue = w.primary_location?.source?.display_name;
330
+ const pdfUrl = w.primary_location?.pdf_url || w.open_access?.oa_url;
331
+ const desc = [
332
+ authors ? `Authors: ${authors}` : "",
333
+ venue ? `Venue: ${venue}` : "",
334
+ typeof w.cited_by_count === "number" ? `Citations: ${w.cited_by_count}` : "",
335
+ pdfUrl ? `PDF: ${pdfUrl}` : "",
336
+ ]
337
+ .filter(Boolean)
338
+ .join(" · ");
339
+ out.push({
340
+ title: `[Paper] ${title}`,
341
+ url: targetUrl,
342
+ description: desc || title,
343
+ publishedDate: w.publication_date,
344
+ sources: ["arxiv"],
345
+ });
346
+ }
347
+ return out;
348
+ }
349
+ catch {
350
+ return [];
351
+ }
352
+ })();
353
+ const [arxivItems, openAlexItems] = await Promise.all([arxivPromise, openAlexPromise]);
354
+ return [...arxivItems, ...openAlexItems];
127
355
  }
356
+ const BUILTIN_CLIENT_TARGETS = [
357
+ {
358
+ id: "llms-txt",
359
+ name: "llms.txt & AI Documentation Indexes",
360
+ returns: "Structured sections, doc links, summaries, and full markdown manifest for LLM context ingestion",
361
+ requirements: "Built-in zero-key markdown manifest parser",
362
+ },
363
+ {
364
+ id: "github",
365
+ name: "GitHub Repositories, Issues & Releases",
366
+ returns: "Repository metadata, stars/forks, topics, language, issue/PR threads, release notes, and README markdown",
367
+ requirements: "Built-in zero-key HTML/JSON inspector via impit TLS fetch",
368
+ },
369
+ {
370
+ id: "arxiv",
371
+ name: "ArXiv & Academic Research Papers",
372
+ returns: "Paper title, authors, abstract, primary subject, DOI, submission date, and direct PDF link",
373
+ requirements: "Built-in ArXiv + OpenAlex paper search (-c academic) and abstract/PDF parser",
374
+ },
375
+ {
376
+ id: "youtube-media",
377
+ name: "YouTube Videos, Audio & Media Files",
378
+ returns: "Timestamped transcript Markdown, video title/channel/duration metadata, and spoken audio transcription",
379
+ requirements: "Built-in zero-key YouTube multi-client caption extractor",
380
+ },
381
+ {
382
+ id: "pdf-office",
383
+ name: "PDFs & Office Documents (DOCX, XLSX, PPTX)",
384
+ returns: "Page-numbered Markdown, tables, native text extraction, automatic OCR on scanned pages",
385
+ requirements: "Built-in (@firecrawl/pdf-inspector, pdf-parse, mammoth, officeparser, tesseract.js)",
386
+ },
387
+ {
388
+ id: "cve",
389
+ name: "Security Advisories & CVEs (NVD / OSV / CISA KEV)",
390
+ returns: "Structured CVERecord with CVSS v3.1/v4.0 score, severity, CWEs, affected/fixed versions, and CISA KEV status",
391
+ requirements: "Built-in live NVD 2.0 + OSV.dev + CISA KEV enrichment (-c security --sub cve)",
392
+ },
393
+ {
394
+ id: "shopping",
395
+ name: "Shopping & Multi-Retailer Product Offers",
396
+ returns: "AggregatedProduct & ProductOffer records with price, currency, stock availability, retailer, rating, and reviews",
397
+ requirements: "Zero-key local HTML/JSON-LD product extractor (-c shopping)",
398
+ },
399
+ {
400
+ id: "travel",
401
+ name: "Flights & Hotels",
402
+ returns: "Structured FlightOffer & HotelListing items with price, currency, airline/stops or star/guest rating",
403
+ requirements: "Zero-key local search + schema.org extractor (-c flights / -c hotels)",
404
+ },
405
+ {
406
+ id: "real-estate",
407
+ name: "Real Estate & Local Business Listings",
408
+ returns: "PropertyListing (bedrooms, bathrooms, parking, suburb) & BusinessListing (phone, address, rating, hours)",
409
+ requirements: "Zero-key local extractor (-c real-estate --sub buy|rent|sold / -c business)",
410
+ },
411
+ {
412
+ id: "web",
413
+ name: "Websites, Docs & Single-Page Apps (SPAs)",
414
+ returns: "Clean Markdown, title, metadata, canonical links, content hash, and evidence source tier",
415
+ requirements: "Zero-key local (impit TLS impersonation + local Chrome) or webcrawl.app cloud stealth browsers",
416
+ },
417
+ ];
128
418
  export function createClient(opts = {}) {
129
419
  return new WebcrawlClient(opts);
130
420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webcrawl/client",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Zero-dependency TypeScript & Node.js SDK for Webcrawl Anything (search, scrape, parse, map, crawl, extract, rerank, targets)",
5
5
  "type": "module",
6
6
  "license": "MIT",