webcrawl 1.2.1 → 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/cli.d.ts CHANGED
@@ -15,5 +15,5 @@ export interface Deps {
15
15
  /** Working directory override for `webcrawl setup` tests. */
16
16
  cwd?: string;
17
17
  }
18
- export declare const HELP = "Webcrawl Anything: search, scrape, parse and crawl any target on the internet\n\nUsage: webcrawl <command> [options]\n\nCommands\n search <query> Search the web or verticals. -c category, --sub, -n limit, --site <domain>\n scrape <url> Inspect a URL (website, SPA, PDF, Office, YouTube, GitHub, ArXiv, llms.txt)\n map <url> List a site's URLs. --filter <text>, -n limit\n crawl <url> Read several pages. -n max pages (default 5, max 25)\n extract <url> Structured JSON. --prompt <text> or --schema <file.json>\n rerank <query> Rank documents. --doc <text> (repeatable), --file <path>, or lines on stdin. -n top\n targets List all supported crawl & search targets and their requirements\n categories List search categories and vertical filters\n setup Register Webcrawl MCP server and Agent Skill (Claude, Cursor, Codex, Gemini, Windsurf)\n doctor Check local crawl engine, parsers, stealth browser, and cloud configuration\n login Save your webcrawl.app API key (--key <key>, or paste when asked)\n whoami Show execution mode (local vs cloud), key source, endpoint and credit usage\n\nOptions\n --mode <m> Execution mode: auto (default: local without key, cloud with key), local, or cloud\n --json Print the raw JSON response\n --api-key <key> Override WEBCRAWL_API_KEY\n --url <base> Override WEBCRAWL_URL (default https://webcrawl.app)\n --country, --city Where to search from (default: this machine's timezone)\n --no-cache Skip cached pages (scrape)\n --agent <name> Target agent for setup: claude, cursor, codex, gemini, windsurf, or all (repeatable)\n --schema-profile MCP schema profile for setup: compact (default) or full\n -h, --help\n\nConfig order: flags, then WEBCRAWL_API_KEY / WEBCRAWL_URL / WEBCRAWL_MODE, then ~/.config/webcrawl/config.json";
18
+ export declare const HELP = "Webcrawl Anything: search, scrape, parse and crawl any target on the internet\n\nUsage: webcrawl <command> [options]\n\nCommands\n search <query> Search the web or verticals. -c category, --sub, -n limit, --site <domain>\n scrape <url> Inspect a URL (website, SPA, PDF, Office, YouTube, GitHub, ArXiv, llms.txt)\n map <url> List a site's URLs. --filter <text>, -n limit\n crawl <url> Read several pages. -n max pages (default 5, max 25)\n extract <url> Structured JSON. --prompt <text> or --schema <file.json>\n rerank <query> Rank documents. --doc <text> (repeatable), --file <path>, or lines on stdin. -n top\n targets List all supported crawl & search targets and their requirements\n categories List search categories and vertical filters\n setup Register Webcrawl MCP server and Agent Skill (Claude, Cursor, Codex, Gemini, Windsurf)\n doctor Check local crawl engine, parsers, stealth browser, and cloud configuration\n login / signup Create an account or sign in (--email <addr> [--code <6-digit>], --web, or --key <key>)\n whoami Show execution mode (local vs cloud), account email, key source, and credit balance\n\nOptions\n --mode <m> Execution mode: auto (default: local without key, cloud with key), local, or cloud\n --email <addr> Email address for CLI account creation / OTP sign-in (assigns 1,000 free credits)\n --code <6-digit> 6-digit email verification code from `webcrawl login --email <addr>`\n --name <name> Optional display name when creating an account with `--email`\n --web Sign in or create an account in your browser via OAuth PKCE redirect\n --json Print the raw JSON response\n --api-key <key> Override WEBCRAWL_API_KEY\n --url <base> Override WEBCRAWL_URL (default https://webcrawl.app)\n --country, --city Where to search from (default: this machine's timezone)\n --no-cache Skip cached pages (scrape)\n --agent <name> Target agent for setup: claude, cursor, codex, gemini, windsurf, or all (repeatable)\n --schema-profile MCP schema profile for setup: compact (default) or full\n -h, --help\n\nConfig order: flags, then WEBCRAWL_API_KEY / WEBCRAWL_URL / WEBCRAWL_MODE, then ~/.config/webcrawl/config.json";
19
19
  export declare function main(argv: string[], io: Io, deps?: Deps): Promise<number>;
package/dist/cli.js CHANGED
@@ -1,4 +1,7 @@
1
+ import { execFile } from "node:child_process";
2
+ import crypto from "node:crypto";
1
3
  import { readFileSync } from "node:fs";
4
+ import { createServer } from "node:http";
2
5
  import { createInterface } from "node:readline";
3
6
  import { parseArgs } from "node:util";
4
7
  import { WebcrawlClient, WebcrawlError, configPath, describeError, readConfigFile, resolveConfig, writeConfigFile, } from "@webcrawl/client";
@@ -19,11 +22,15 @@ Commands
19
22
  categories List search categories and vertical filters
20
23
  setup Register Webcrawl MCP server and Agent Skill (Claude, Cursor, Codex, Gemini, Windsurf)
21
24
  doctor Check local crawl engine, parsers, stealth browser, and cloud configuration
22
- login Save your webcrawl.app API key (--key <key>, or paste when asked)
23
- whoami Show execution mode (local vs cloud), key source, endpoint and credit usage
25
+ login / signup Create an account or sign in (--email <addr> [--code <6-digit>], --web, or --key <key>)
26
+ whoami Show execution mode (local vs cloud), account email, key source, and credit balance
24
27
 
25
28
  Options
26
29
  --mode <m> Execution mode: auto (default: local without key, cloud with key), local, or cloud
30
+ --email <addr> Email address for CLI account creation / OTP sign-in (assigns 1,000 free credits)
31
+ --code <6-digit> 6-digit email verification code from \`webcrawl login --email <addr>\`
32
+ --name <name> Optional display name when creating an account with \`--email\`
33
+ --web Sign in or create an account in your browser via OAuth PKCE redirect
27
34
  --json Print the raw JSON response
28
35
  --api-key <key> Override WEBCRAWL_API_KEY
29
36
  --url <base> Override WEBCRAWL_URL (default https://webcrawl.app)
@@ -52,6 +59,11 @@ const OPTIONS = {
52
59
  doc: { type: "string", multiple: true },
53
60
  file: { type: "string" },
54
61
  key: { type: "string" },
62
+ email: { type: "string" },
63
+ code: { type: "string" },
64
+ name: { type: "string" },
65
+ web: { type: "boolean" },
66
+ "no-wait": { type: "boolean" },
55
67
  "no-cache": { type: "boolean" },
56
68
  content: { type: "boolean" },
57
69
  agent: { type: "string", multiple: true },
@@ -69,18 +81,155 @@ function int(v, name) {
69
81
  }
70
82
  return n;
71
83
  }
72
- async function readKeyInteractively(io) {
84
+ async function promptLine(io, promptText) {
73
85
  if (!io.isTTY) {
74
86
  return (await io.readStdin()).trim();
75
87
  }
76
88
  const rl = createInterface({ input: process.stdin, output: process.stderr });
77
89
  return new Promise((resolve) => {
78
- rl.question("Paste your API key (wc_live_…): ", (a) => {
90
+ rl.question(promptText, (a) => {
79
91
  rl.close();
80
92
  resolve(a.trim());
81
93
  });
82
94
  });
83
95
  }
96
+ async function readKeyInteractively(io) {
97
+ return promptLine(io, "Paste your API key (wc_live_…): ");
98
+ }
99
+ function openBrowserUrl(url) {
100
+ const cmd = process.platform === "darwin"
101
+ ? "open"
102
+ : process.platform === "win32"
103
+ ? "cmd"
104
+ : "xdg-open";
105
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
106
+ try {
107
+ execFile(cmd, args, () => {
108
+ // ignore browser launch errors in headless environments
109
+ });
110
+ }
111
+ catch {
112
+ // ignore
113
+ }
114
+ }
115
+ async function runWebOAuthLogin(baseUrl, fetchImpl, io) {
116
+ const verifier = crypto.randomBytes(32).toString("base64url");
117
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
118
+ const state = crypto.randomBytes(16).toString("base64url");
119
+ return new Promise((resolve, reject) => {
120
+ const server = createServer();
121
+ const timeout = setTimeout(() => {
122
+ server.close();
123
+ reject(new Error("Browser sign-in timed out after 180 seconds."));
124
+ }, 180_000);
125
+ server.listen(0, "127.0.0.1", async () => {
126
+ try {
127
+ const addr = server.address();
128
+ const port = typeof addr === "object" && addr ? addr.port : 0;
129
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
130
+ const regRes = await fetchImpl(`${baseUrl}/oauth/register`, {
131
+ method: "POST",
132
+ headers: { "content-type": "application/json", accept: "application/json" },
133
+ body: JSON.stringify({
134
+ client_name: "Webcrawl Anything CLI",
135
+ redirect_uris: [redirectUri],
136
+ }),
137
+ });
138
+ if (!regRes.ok) {
139
+ throw new Error(`Could not register CLI OAuth client (HTTP ${regRes.status}). Try \`webcrawl login --email <you@domain.com>\`.`);
140
+ }
141
+ const { client_id: clientId } = (await regRes.json());
142
+ const authUrl = `${baseUrl}/oauth/authorize?${new URLSearchParams({
143
+ response_type: "code",
144
+ client_id: clientId,
145
+ redirect_uri: redirectUri,
146
+ code_challenge: challenge,
147
+ code_challenge_method: "S256",
148
+ state,
149
+ }).toString()}`;
150
+ io.out([
151
+ "Open this URL in your browser to create your webcrawl.app account or sign in (1,000 free monthly credits):",
152
+ ` ${authUrl}`,
153
+ "",
154
+ "Waiting for browser authorization...",
155
+ ].join("\n") + "\n");
156
+ openBrowserUrl(authUrl);
157
+ server.on("request", async (req, res) => {
158
+ try {
159
+ const u = new URL(req.url || "/", redirectUri);
160
+ if (u.pathname !== "/callback") {
161
+ res.writeHead(404).end("Not found");
162
+ return;
163
+ }
164
+ const code = u.searchParams.get("code");
165
+ const returnedState = u.searchParams.get("state");
166
+ if (!code || returnedState !== state) {
167
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
168
+ res.end("<h3>Authorization failed or state mismatch. You can close this tab.</h3>");
169
+ clearTimeout(timeout);
170
+ server.close();
171
+ reject(new Error("OAuth callback missing code or state mismatch."));
172
+ return;
173
+ }
174
+ const tokenRes = await fetchImpl(`${baseUrl}/oauth/token`, {
175
+ method: "POST",
176
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
177
+ body: new URLSearchParams({
178
+ grant_type: "authorization_code",
179
+ code,
180
+ client_id: clientId,
181
+ redirect_uri: redirectUri,
182
+ code_verifier: verifier,
183
+ }).toString(),
184
+ });
185
+ if (!tokenRes.ok) {
186
+ throw new Error(`OAuth token exchange failed (HTTP ${tokenRes.status}).`);
187
+ }
188
+ const tokenData = (await tokenRes.json());
189
+ let finalKey = tokenData.access_token;
190
+ const authedClient = new WebcrawlClient({
191
+ baseUrl,
192
+ apiKey: finalKey,
193
+ fetch: fetchImpl,
194
+ userAgent: "webcrawl-cli/1.2",
195
+ });
196
+ try {
197
+ const keyRes = await authedClient.request("POST", "/v1/account/keys", { name: "CLI API Key" });
198
+ if (keyRes.apiKey) {
199
+ finalKey = keyRes.apiKey;
200
+ }
201
+ }
202
+ catch {
203
+ // keep OAuth access_token (`wc_mcp_...`) which is also accepted on all /v1/* endpoints
204
+ }
205
+ const meRes = await authedClient.me().catch(() => undefined);
206
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
207
+ res.end("<html><body style='font-family:system-ui;padding:2rem'><h2>✓ Webcrawl CLI authenticated!</h2><p>Your account and 1,000 monthly credits are now linked. You can close this tab and return to your terminal or agent.</p></body></html>");
208
+ clearTimeout(timeout);
209
+ server.close();
210
+ resolve({
211
+ apiKey: finalKey,
212
+ email: meRes?.user?.email,
213
+ creditsRemaining: meRes?.user?.creditsRemaining,
214
+ creditsLimit: meRes?.user?.creditsLimit,
215
+ plan: meRes?.user?.plan,
216
+ });
217
+ }
218
+ catch (err) {
219
+ clearTimeout(timeout);
220
+ server.close();
221
+ reject(err);
222
+ }
223
+ });
224
+ }
225
+ catch (err) {
226
+ clearTimeout(timeout);
227
+ server.close();
228
+ reject(err);
229
+ }
230
+ });
231
+ });
232
+ }
84
233
  async function resolveBackendClient(cfg, deps) {
85
234
  // When a custom fetch mock is injected (e.g. unit tests) or effectiveMode is cloud, use WebcrawlClient
86
235
  if (cfg.effectiveMode === "cloud" || deps.fetch) {
@@ -142,6 +291,38 @@ export async function main(argv, io, deps = {}) {
142
291
  io.err(`[${credits}]\n`);
143
292
  }
144
293
  };
294
+ const METERED_COMMANDS = new Set(["search", "scrape", "map", "crawl", "extract", "rerank"]);
295
+ if (METERED_COMMANDS.has(cmd) && cfg.effectiveMode === "cloud" && !cfg.apiKey && !deps.fetch) {
296
+ const authHelp = [
297
+ "webcrawl: Cloud mode requires a logged-in webcrawl.app account so credits can be attributed to your user (1,000 free monthly credits on signup).",
298
+ "",
299
+ "Create an account or sign in from the CLI:",
300
+ " 1. Email OTP (agent & terminal friendly):",
301
+ " webcrawl login --email <you@domain.com>",
302
+ " webcrawl login --email <you@domain.com> --code <6-digit-code>",
303
+ " 2. Browser OAuth redirect:",
304
+ " webcrawl login --web",
305
+ " 3. Or run in zero-key local mode:",
306
+ ` webcrawl --mode local ${cmd} ${rest.join(" ")}`,
307
+ ].join("\n");
308
+ if (v.json) {
309
+ io.out(`${JSON.stringify({
310
+ success: false,
311
+ error: "authentication_required",
312
+ message: "Cloud mode requires a logged-in webcrawl.app account (1,000 free monthly credits on signup).",
313
+ loginCommands: {
314
+ sendOtp: "webcrawl login --email <you@domain.com>",
315
+ verifyOtp: "webcrawl login --email <you@domain.com> --code <6-digit-code>",
316
+ browserRedirect: "webcrawl login --web",
317
+ signupUrl: `${cfg.baseUrl}/?signup=cli`,
318
+ },
319
+ }, null, 2)}\n`);
320
+ }
321
+ else {
322
+ io.err(`${authHelp}\n`);
323
+ }
324
+ return 1;
325
+ }
145
326
  try {
146
327
  switch (cmd) {
147
328
  case "search": {
@@ -258,10 +439,93 @@ export async function main(argv, io, deps = {}) {
258
439
  emit({ success: true, ...report }, prettyLines);
259
440
  return 0;
260
441
  }
261
- case "login": {
442
+ case "login":
443
+ case "signup": {
444
+ const authClient = new WebcrawlClient({
445
+ baseUrl: cfg.baseUrl,
446
+ fetch: deps.fetch,
447
+ userAgent: "webcrawl-cli/1.2",
448
+ });
449
+ // 1. Browser OAuth PKCE redirect (`webcrawl login --web`)
450
+ if (v.web) {
451
+ const webRes = await runWebOAuthLogin(cfg.baseUrl, deps.fetch ?? globalThis.fetch, io);
452
+ const existing = readConfigFile(file);
453
+ writeConfigFile({ ...existing, apiKey: webRes.apiKey, ...(v.url ? { baseUrl: v.url } : {}) }, file);
454
+ const payload = {
455
+ success: true,
456
+ method: "web_oauth",
457
+ email: webRes.email,
458
+ plan: webRes.plan ?? "free",
459
+ creditsRemaining: webRes.creditsRemaining ?? 1000,
460
+ creditsLimit: webRes.creditsLimit ?? 1000,
461
+ configFile: file,
462
+ };
463
+ emit(payload, [
464
+ `✓ Authenticated${webRes.email ? ` as ${webRes.email}` : ""} (${payload.plan} plan · ${payload.creditsRemaining} credits remaining of ${payload.creditsLimit}).`,
465
+ `Saved API key to ${file}.`,
466
+ ].join("\n"));
467
+ return 0;
468
+ }
469
+ // 2. Email OTP account creation / sign-in (`webcrawl login --email <addr> [--code <6-digit>]`)
470
+ if (v.email) {
471
+ const email = v.email.trim();
472
+ let code = v.code?.trim();
473
+ if (!code) {
474
+ const otpRes = await authClient.sendOtp(email);
475
+ if (!otpRes.success) {
476
+ throw new WebcrawlError("bad_request", otpRes.error || "Could not send verification code.");
477
+ }
478
+ if (otpRes.devCode && !io.isTTY) {
479
+ code = otpRes.devCode;
480
+ }
481
+ else if (io.isTTY && !v["no-wait"]) {
482
+ io.err(`Sent a 6-digit verification code to ${email} (expires in 10 minutes).\n`);
483
+ code = await promptLine(io, `Enter the 6-digit code sent to ${email}: `);
484
+ }
485
+ else {
486
+ const stepPayload = {
487
+ success: true,
488
+ step: "otp_sent",
489
+ email: otpRes.email,
490
+ expiresInSeconds: otpRes.expiresInSeconds,
491
+ nextCommand: `webcrawl login --email ${email} --code <6-digit-code>`,
492
+ browserFallbackUrl: `${cfg.baseUrl}/?signup=cli&email=${encodeURIComponent(email)}`,
493
+ };
494
+ emit(stepPayload, [
495
+ `Verification code sent to ${otpRes.email} (expires in ${Math.round(otpRes.expiresInSeconds / 60)} minutes).`,
496
+ `Ask the user for the 6-digit code from their email, then complete sign-in with:`,
497
+ ` webcrawl login --email ${email} --code <6-digit-code>`,
498
+ `Or redirect the user to sign in on the web: ${stepPayload.browserFallbackUrl}`,
499
+ ].join("\n"));
500
+ return 0;
501
+ }
502
+ }
503
+ const verified = await authClient.verifyOtp({ email, code, name: v.name });
504
+ if (!verified.success || !verified.apiKey) {
505
+ throw new WebcrawlError("auth", verified.error || "Could not verify the 6-digit code.");
506
+ }
507
+ const existing = readConfigFile(file);
508
+ writeConfigFile({ ...existing, apiKey: verified.apiKey, ...(v.url ? { baseUrl: v.url } : {}) }, file);
509
+ const u = verified.user;
510
+ const payload = {
511
+ success: true,
512
+ isNewUser: verified.isNewUser ?? false,
513
+ email: u?.email ?? email,
514
+ plan: u?.plan ?? "free",
515
+ creditsRemaining: u?.creditsRemaining ?? 1000,
516
+ creditsLimit: u?.creditsLimit ?? 1000,
517
+ configFile: file,
518
+ };
519
+ emit(payload, [
520
+ `✓ ${verified.isNewUser ? "Created webcrawl.app account" : "Signed in"} as ${payload.email} (${payload.plan} plan · ${payload.creditsRemaining} credits remaining of ${payload.creditsLimit}).`,
521
+ `Saved API key (${verified.apiKey.slice(0, 8)}…${verified.apiKey.slice(-4)}) to ${file}.`,
522
+ ].join("\n"));
523
+ return 0;
524
+ }
525
+ // 3. Direct API key (`webcrawl login --key wc_live_...` or interactive paste)
262
526
  const key = (v.key ?? (await readKeyInteractively(io))).trim();
263
527
  if (!key) {
264
- throw new UsageError("No key provided.");
528
+ throw new UsageError("No key provided. Pass --key <wc_live_...>, --email <you@domain.com>, or --web.");
265
529
  }
266
530
  const existing = readConfigFile(file);
267
531
  writeConfigFile({ ...existing, apiKey: key, ...(v.url ? { baseUrl: v.url } : {}) }, file);
@@ -273,7 +537,7 @@ export async function main(argv, io, deps = {}) {
273
537
  const lines = [
274
538
  `endpoint: ${cfg.baseUrl} (${cfg.source.baseUrl})`,
275
539
  `mode: ${cfg.effectiveMode} (${cfg.source.mode})`,
276
- `key: ${masked}${cfg.apiKey ? ` (${cfg.source.apiKey})` : ". Run `webcrawl login` or set WEBCRAWL_API_KEY (local mode works with zero key)."}`,
540
+ `key: ${masked}${cfg.apiKey ? ` (${cfg.source.apiKey})` : ". Run `webcrawl login` (`--email <you@domain.com>` or `--web`) or set WEBCRAWL_API_KEY (local mode works with zero key)."}`,
277
541
  ];
278
542
  if (cfg.apiKey) {
279
543
  const u = await client.usage();
@@ -281,6 +545,10 @@ export async function main(argv, io, deps = {}) {
281
545
  io.out(`${JSON.stringify({ endpoint: cfg.baseUrl, mode: cfg.effectiveMode, keySource: cfg.source.apiKey, usage: u }, null, 2)}\n`);
282
546
  return 0;
283
547
  }
548
+ const userObj = (u.user ?? {});
549
+ if (typeof userObj.email === "string") {
550
+ lines.push(`account: ${userObj.email} (${userObj.plan ?? "free"} plan)`);
551
+ }
284
552
  const credits = (u.credits ?? u);
285
553
  if (typeof credits.remaining === "number") {
286
554
  lines.push(`credits: ${credits.remaining} remaining of ${credits.limit ?? "?"}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcrawl",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Webcrawl Anything CLI — 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",
@@ -53,7 +53,7 @@
53
53
  "test": "node --conditions=development --import tsx --test test/*.test.ts"
54
54
  },
55
55
  "dependencies": {
56
- "@webcrawl/client": "^1.2.0"
56
+ "@webcrawl/client": "^1.2.2"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=20"