webcrawl 1.3.5 → 1.3.7
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 +11 -0
- package/dist/cli.js +84 -8
- package/package.json +3 -3
package/dist/cli.d.ts
CHANGED
|
@@ -10,10 +10,21 @@ export interface Deps {
|
|
|
10
10
|
fetch?: typeof fetch;
|
|
11
11
|
/** Config file path override (tests). */
|
|
12
12
|
configFile?: string;
|
|
13
|
+
/** Browser opener override (tests). */
|
|
14
|
+
openUrl?: (url: string) => void;
|
|
13
15
|
/** Home directory override for `webcrawl setup` tests. */
|
|
14
16
|
homeDir?: string;
|
|
15
17
|
/** Working directory override for `webcrawl setup` tests. */
|
|
16
18
|
cwd?: string;
|
|
17
19
|
}
|
|
18
20
|
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 in your browser (default), or --email <addr> [--code <6-digit>], 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 via browser OAuth (default for login; no need to pass it)\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";
|
|
21
|
+
/** Self-contained (no network assets) page served on the loopback callback. */
|
|
22
|
+
export declare function callbackPage(opts: {
|
|
23
|
+
kind: "success" | "error" | "info";
|
|
24
|
+
title: string;
|
|
25
|
+
body: string;
|
|
26
|
+
email?: string;
|
|
27
|
+
credits?: string;
|
|
28
|
+
tryIt?: string;
|
|
29
|
+
}): string;
|
|
19
30
|
export declare function main(argv: string[], io: Io, deps?: Deps): Promise<number>;
|
package/dist/cli.js
CHANGED
|
@@ -112,7 +112,53 @@ function openBrowserUrl(url) {
|
|
|
112
112
|
// ignore
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
116
|
+
/** Self-contained (no network assets) page served on the loopback callback. */
|
|
117
|
+
export function callbackPage(opts) {
|
|
118
|
+
const tone = opts.kind === "success" ? "#157f3b" : opts.kind === "error" ? "#b42318" : "#6236e8";
|
|
119
|
+
const icon = opts.kind === "success"
|
|
120
|
+
? `<path class="mk" d="M20 33l8 8 16-17"/>`
|
|
121
|
+
: opts.kind === "error"
|
|
122
|
+
? `<path class="mk" d="M23 23l18 18M41 23L23 41"/>`
|
|
123
|
+
: `<path class="mk" d="M32 22v14M32 42v.5"/>`;
|
|
124
|
+
const chips = [opts.email ? `<span class="chip">${esc(opts.email)}</span>` : "", opts.credits ? `<span class="chip hot">${esc(opts.credits)}</span>` : ""].join("");
|
|
125
|
+
const dots = Array.from({ length: 14 }, (_, i) => `<i style="--a:${i * 25.7}deg;--d:${70 + (i % 3) * 18}px;--t:${(i % 5) * 0.06}s"></i>`).join("");
|
|
126
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(opts.title)}</title>
|
|
127
|
+
<style>
|
|
128
|
+
:root{--tone:${tone}}*{box-sizing:border-box}
|
|
129
|
+
body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;color:#1b1631;font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,sans-serif;background:#fbfaf8}
|
|
130
|
+
main{width:100%;max-width:440px;text-align:center;background:#fff;border:1px solid #e2ddd5;border-radius:16px;padding:36px 32px 28px;box-shadow:0 1px 2px #1b16310d,0 12px 32px #1b16310f;animation:rise .5s cubic-bezier(.2,.8,.2,1) both}
|
|
131
|
+
.brand{display:flex;align-items:center;justify-content:center;gap:8px;font-weight:600;letter-spacing:-.035em;color:#1b1631;margin-bottom:26px}.brand svg{width:26px;height:26px}
|
|
132
|
+
.badge{position:relative;width:96px;height:96px;margin:0 auto 18px}
|
|
133
|
+
.badge svg.ring{width:96px;height:96px;overflow:visible}
|
|
134
|
+
.ring circle{fill:color-mix(in srgb,var(--tone) 12%,transparent);stroke:var(--tone);stroke-width:2.5;stroke-dasharray:190;stroke-dashoffset:190;animation:draw .7s .1s ease-out forwards}
|
|
135
|
+
.mk{fill:none;stroke:var(--tone);stroke-width:4.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:60;stroke-dashoffset:60;animation:draw .45s .6s ease-out forwards}
|
|
136
|
+
.burst{position:absolute;inset:0;pointer-events:none}.burst i{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--tone);opacity:0;transform:rotate(var(--a)) translateY(0);animation:pop .9s calc(.7s + var(--t)) ease-out forwards}
|
|
137
|
+
h1{font-size:22px;margin:0 0 6px;letter-spacing:-.02em}p{margin:0 0 18px;color:#4a4460}
|
|
138
|
+
.chips{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-bottom:20px}
|
|
139
|
+
.chip{font-size:12.5px;padding:4px 10px;border-radius:999px;background:#f6f4f0;border:1px solid #e2ddd5;color:#4a4460}.chip.hot{background:#f1ecff;border-color:#d9ccff;color:#4f24cf}
|
|
140
|
+
.try{text-align:left;background:#f6f4f0;border:1px solid #efebe5;border-radius:10px;padding:12px 14px;margin-bottom:18px}
|
|
141
|
+
.trow{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}.trow small{margin:0!important}
|
|
142
|
+
.copy{font:600 12px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:6px 10px;border-radius:7px;border:1px solid #e2ddd5;background:#fff;color:#4a4460;cursor:pointer}.copy:hover{border-color:#6236e8;color:#4f24cf}
|
|
143
|
+
.try code{user-select:all;-webkit-user-select:all;cursor:text}
|
|
144
|
+
.try small{display:block;color:#5e5873;margin-bottom:4px;font-size:12px;text-transform:uppercase;letter-spacing:.08em}
|
|
145
|
+
.try code{font:13px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;color:#4f24cf;word-break:break-all}.try code:before{content:"$ ";color:#7d7690}
|
|
146
|
+
.foot{font-size:13px;color:#5e5873;margin:0}
|
|
147
|
+
@keyframes rise{from{opacity:0;transform:translateY(14px) scale(.98)}}
|
|
148
|
+
@keyframes draw{to{stroke-dashoffset:0}}
|
|
149
|
+
@keyframes pop{0%{opacity:1}100%{opacity:0;transform:rotate(var(--a)) translateY(calc(var(--d) * -1)) scale(.3)}}
|
|
150
|
+
@media (prefers-reduced-motion:reduce){*{animation-duration:.01s!important;animation-delay:0s!important}}
|
|
151
|
+
</style></head><body><main>
|
|
152
|
+
<div class="brand"><svg viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="7" fill="#1b1631"/><g stroke-linejoin="round" stroke-linecap="round" fill="none" stroke-width="3"><path d="M5 21.5L16 27.5L27 21.5" stroke="#8a80b8"/><path d="M5 16L16 22L27 16" stroke="#c9b8ff"/></g><path d="M16 4.5L27 10.5L16 16.5L5 10.5Z" fill="#8b6cff" stroke="#8b6cff" stroke-width="2" stroke-linejoin="round"/></svg>Webcrawl</div>
|
|
153
|
+
<div class="badge"><svg class="ring" viewBox="0 0 64 64" aria-hidden="true"><circle cx="32" cy="32" r="30"/>${icon}</svg>${opts.kind === "success" ? `<div class="burst">${dots}</div>` : ""}</div>
|
|
154
|
+
<h1>${esc(opts.title)}</h1><p>${opts.body}</p>
|
|
155
|
+
${chips ? `<div class="chips">${chips}</div>` : ""}
|
|
156
|
+
${opts.tryIt ? `<div class="try"><div class="trow"><small>Try it now</small><button type="button" class="copy" id="cp">Copy</button></div><code id="cmd">${esc(opts.tryIt)}</code></div>
|
|
157
|
+
<script>(function(){var b=document.getElementById("cp"),c=document.getElementById("cmd");if(!b||!c)return;var t=c.textContent;function done(){b.textContent="Copied";setTimeout(function(){b.textContent="Copy"},1500)}function fb(){var r=document.createRange();r.selectNodeContents(c);var s=getSelection();s.removeAllRanges();s.addRange(r);try{document.execCommand("copy");done()}catch(e){}}b.onclick=function(){if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).then(done,fb)}else{fb()}}})()</script>` : ""}
|
|
158
|
+
<p class="foot">You can close this tab and return to your terminal.</p>
|
|
159
|
+
</main></body></html>`;
|
|
160
|
+
}
|
|
161
|
+
async function runWebOAuthLogin(baseUrl, fetchImpl, io, openUrl = openBrowserUrl) {
|
|
116
162
|
const verifier = crypto.randomBytes(32).toString("base64url");
|
|
117
163
|
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
118
164
|
const state = crypto.randomBytes(16).toString("base64url");
|
|
@@ -153,7 +199,8 @@ async function runWebOAuthLogin(baseUrl, fetchImpl, io) {
|
|
|
153
199
|
"",
|
|
154
200
|
"Waiting for browser authorization...",
|
|
155
201
|
].join("\n") + "\n");
|
|
156
|
-
|
|
202
|
+
openUrl(authUrl);
|
|
203
|
+
let handled = false;
|
|
157
204
|
server.on("request", async (req, res) => {
|
|
158
205
|
try {
|
|
159
206
|
const u = new URL(req.url || "/", redirectUri);
|
|
@@ -163,14 +210,29 @@ async function runWebOAuthLogin(baseUrl, fetchImpl, io) {
|
|
|
163
210
|
}
|
|
164
211
|
const code = u.searchParams.get("code");
|
|
165
212
|
const returnedState = u.searchParams.get("state");
|
|
166
|
-
if (
|
|
213
|
+
if (handled) {
|
|
214
|
+
// Browser retry / prefetch of the same callback: the code is single-use, so never exchange it twice.
|
|
215
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
216
|
+
res.end(callbackPage({ kind: "info", title: "Already received", body: "This sign-in link was already used. Your terminal has everything it needs." }));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const oauthError = u.searchParams.get("error");
|
|
220
|
+
if (oauthError && returnedState === state) {
|
|
221
|
+
handled = true;
|
|
167
222
|
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
168
|
-
res.end("
|
|
223
|
+
res.end(callbackPage({ kind: "error", title: "Sign-in cancelled", body: "You chose not to connect the CLI. Run <b>webcrawl login</b> whenever you want to try again." }));
|
|
169
224
|
clearTimeout(timeout);
|
|
170
225
|
server.close();
|
|
171
|
-
reject(new Error(
|
|
226
|
+
reject(new Error(`OAuth authorization failed: ${oauthError}.`));
|
|
172
227
|
return;
|
|
173
228
|
}
|
|
229
|
+
if (!code || returnedState !== state) {
|
|
230
|
+
// Stray or forged request: reject it but keep waiting for the real callback.
|
|
231
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
232
|
+
res.end(callbackPage({ kind: "error", title: "Link not recognised", body: "This sign-in link does not match the one your terminal is waiting for. Run <b>webcrawl login</b> again for a fresh link." }));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
handled = true;
|
|
174
236
|
const tokenRes = await fetchImpl(`${baseUrl}/oauth/token`, {
|
|
175
237
|
method: "POST",
|
|
176
238
|
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
@@ -183,7 +245,11 @@ async function runWebOAuthLogin(baseUrl, fetchImpl, io) {
|
|
|
183
245
|
}).toString(),
|
|
184
246
|
});
|
|
185
247
|
if (!tokenRes.ok) {
|
|
186
|
-
|
|
248
|
+
const detail = (await tokenRes.json().catch(() => ({})));
|
|
249
|
+
const reason = detail.error_description || detail.error;
|
|
250
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
251
|
+
res.end(callbackPage({ kind: "error", title: "Sign-in failed", body: "Webcrawl could not finish signing you in. Your terminal has the details. Run <b>webcrawl login</b> to try again." }));
|
|
252
|
+
throw new Error(`OAuth token exchange failed (HTTP ${tokenRes.status}${reason ? `: ${reason}` : ""}). Run \`webcrawl login\` again, or use \`webcrawl login --email <you@domain.com>\`.`);
|
|
187
253
|
}
|
|
188
254
|
const tokenData = (await tokenRes.json());
|
|
189
255
|
let finalKey = tokenData.access_token;
|
|
@@ -204,7 +270,17 @@ async function runWebOAuthLogin(baseUrl, fetchImpl, io) {
|
|
|
204
270
|
}
|
|
205
271
|
const meRes = await authedClient.me().catch(() => undefined);
|
|
206
272
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
207
|
-
|
|
273
|
+
const u2 = meRes?.user;
|
|
274
|
+
res.end(callbackPage({
|
|
275
|
+
kind: "success",
|
|
276
|
+
title: "You're connected",
|
|
277
|
+
body: "Webcrawl CLI is signed in. Search, read and crawl the web from your terminal or your agent.",
|
|
278
|
+
email: u2?.email,
|
|
279
|
+
credits: u2?.creditsRemaining !== undefined && u2?.creditsLimit !== undefined
|
|
280
|
+
? `${u2.creditsRemaining.toLocaleString("en-US")} / ${u2.creditsLimit.toLocaleString("en-US")} credits`
|
|
281
|
+
: undefined,
|
|
282
|
+
tryIt: 'webcrawl search "best ramen in shibuya"',
|
|
283
|
+
}));
|
|
208
284
|
clearTimeout(timeout);
|
|
209
285
|
server.close();
|
|
210
286
|
resolve({
|
|
@@ -449,7 +525,7 @@ export async function main(argv, io, deps = {}) {
|
|
|
449
525
|
});
|
|
450
526
|
// 1. Browser OAuth PKCE redirect (default for `webcrawl login`; `--web` kept for compatibility)
|
|
451
527
|
if (v.web || (!v.email && !v.key)) {
|
|
452
|
-
const webRes = await runWebOAuthLogin(cfg.baseUrl, deps.fetch ?? globalThis.fetch, io);
|
|
528
|
+
const webRes = await runWebOAuthLogin(cfg.baseUrl, deps.fetch ?? globalThis.fetch, io, deps.openUrl);
|
|
453
529
|
const existing = readConfigFile(file);
|
|
454
530
|
writeConfigFile({ ...existing, apiKey: webRes.apiKey, ...(v.url ? { baseUrl: v.url } : {}) }, file);
|
|
455
531
|
const payload = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcrawl",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
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.3.
|
|
131
|
-
"@webcrawl/curl-impersonate": "^1.3.
|
|
130
|
+
"@webcrawl/client": "^1.3.7",
|
|
131
|
+
"@webcrawl/curl-impersonate": "^1.3.7",
|
|
132
132
|
"ai": "^7.0.150",
|
|
133
133
|
"impit": "^0.14.7",
|
|
134
134
|
"mammoth": "^1.13.0",
|