webscraping-ai-cli 1.1.0 → 1.2.0
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/.claude-plugin/marketplace.json +3 -3
- package/CHANGELOG.md +19 -0
- package/README.md +18 -1
- package/dist/cli.js +218 -115
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/plugins/webscraping-ai/.claude-plugin/plugin.json +2 -2
- package/plugins/webscraping-ai/skills/webscraping-ai/SKILL.md +12 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-marketplace.json",
|
|
3
3
|
"name": "webscraping-ai",
|
|
4
|
-
"description": "Official WebScraping.AI plugins for Claude Code — scrape and AI-extract data from any URL,
|
|
4
|
+
"description": "Official WebScraping.AI plugins for Claude Code — scrape and AI-extract data from any URL, get parsed Google search (SERP) results, and structured data for supported sites.",
|
|
5
5
|
"owner": {
|
|
6
6
|
"name": "WebScraping.AI",
|
|
7
7
|
"url": "https://webscraping.ai"
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
{
|
|
11
11
|
"name": "webscraping-ai",
|
|
12
12
|
"source": "./plugins/webscraping-ai",
|
|
13
|
-
"description": "Fetch HTML, text, selected elements, or AI-extracted data from any URL — including JS-rendered pages —
|
|
14
|
-
"version": "1.
|
|
13
|
+
"description": "Fetch HTML, text, selected elements, or AI-extracted data from any URL — including JS-rendered pages — parsed Google search results, and structured data for supported sites (YouTube, TikTok, X, LinkedIn, Instagram, Reddit, …), via the WebScraping.AI CLI.",
|
|
14
|
+
"version": "1.2.0",
|
|
15
15
|
"category": "web-scraping",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"web-scraping",
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to `webscraping-ai-cli` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 1.2.0 — 2026-09-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `data <url>` subcommand wrapping the new `/data` endpoint: structured JSON for a page on a supported site (e.g. YouTube, TikTok, X, LinkedIn, Instagram, Reddit; more are added server-side). Flags: `--country` (proxy country, `us` by default), `--transcript` (YouTube videos only; if the transcript fetch itself fails, the whole request fails with a 500, exit 7, not charged), `--transcript-language`, and a repeatable `--param key=value` passed through as-is for site-specific parameters. `-` reads one URL per line from stdin (stops at the first failing item, like every command); `-o` and `--pretty`/`--no-pretty` work as on `serp`. The scrape flags (`--js`, `--proxy`, `--headers`, …) don't apply and aren't registered on it. 15 credits per request.
|
|
13
|
+
- The URL is sent exactly as given and never checked against a list of sites. An unsupported URL or page type returns a 400 that is not charged (exit 3); its message lists what is supported. A blank URL exits 2 before any request.
|
|
14
|
+
- `--param` rejects, with exit 2 before any request: a missing `=` or empty key, a repeated key, `api_key`, `url`, `from_cli`, `__proto__`, and keys that have their own flag (`country`, `transcript`, `transcript_language` — the message names the flag).
|
|
15
|
+
- Bundled agent skill documents when to reach for `data` before scraping a supported site yourself.
|
|
16
|
+
- Smoke script adds a YouTube `data` call (checks `parse_status` `ok`, provider `youtube`, a non-empty `data.title`) and an example.com call that must exit 3 with a server 400 whose message contains `Unsupported URL` (~46 credits per sweep).
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Requires the `webscraping-ai` SDK release after 4.1.0 (adds `data()` and the `params` pass-through on `serp()`/`data()`).
|
|
21
|
+
- `from_cli` is now sent on `serp` and `data`, through the SDK's `params` pass-through (closes the known gap: SDK 4.1.0's `serp()` dropped it).
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `-o/--output` in stdin batch mode is truncated when the batch starts, so an empty stdin or a failing first item no longer leaves the previous run's content in the file.
|
|
26
|
+
|
|
8
27
|
## 1.1.0 — 2026-09-25
|
|
9
28
|
|
|
10
29
|
### Added
|
package/README.md
CHANGED
|
@@ -46,10 +46,21 @@ webscraping-ai selected-multiple <url> --selector a --selector b ...
|
|
|
46
46
|
webscraping-ai ask <url> --question "..." # AI question about the page
|
|
47
47
|
webscraping-ai extract <url> --fields '{"title":"...","price":"..."}'
|
|
48
48
|
webscraping-ai serp <query> [--gl us] [--hl en] [--page 1] # Parsed Google results (alias: search)
|
|
49
|
+
webscraping-ai data <url> [--country us] [--transcript] # Structured JSON for a supported site
|
|
49
50
|
webscraping-ai account # Remaining credits / quota
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
`serp` (alias `search`) is query-shaped, not URL-shaped: it takes `--engine` (`google`, the default), `--gl` (country), `--hl` (language), `--page` (1-based, 10 results per page) plus `--api-key`/`--output`/`--pretty`, and none of the scrape flags below. It prints the JSON result (`search_parameters`, `search_information`, `organic_results`, `related_searches`, `pagination`). Flat 15 credits per search; failed searches are not charged. `--page` must be an integer >= 1 (pages are 1–100; the server rejects values above 100 with a 400); anything else is a usage error (exit 2) before any request. Multi-word queries don't need quoting; `-` reads one query per line from stdin, skipping only blank lines (a leading `#` is kept as part of the query, e.g. `#coffee`). The CLI's `from_cli` analytics flag is
|
|
53
|
+
`serp` (alias `search`) is query-shaped, not URL-shaped: it takes `--engine` (`google`, the default), `--gl` (country), `--hl` (language), `--page` (1-based, 10 results per page) plus `--api-key`/`--output`/`--pretty`, and none of the scrape flags below. It prints the JSON result (`search_parameters`, `search_information`, `organic_results`, `related_searches`, `pagination`). Flat 15 credits per search; failed searches are not charged. `--page` must be an integer >= 1 (pages are 1–100; the server rejects values above 100 with a 400); anything else is a usage error (exit 2) before any request. Multi-word queries don't need quoting; `-` reads one query per line from stdin, skipping only blank lines (a leading `#` is kept as part of the query, e.g. `#coffee`). The CLI's `from_cli` analytics flag is sent on `serp` through the SDK's `params` pass-through.
|
|
54
|
+
|
|
55
|
+
`data <url>` returns structured JSON for a public page on a supported site — for example a YouTube video, TikTok profile, X post, LinkedIn company, Instagram reel or Reddit thread. Pass the page's normal URL; the site and page kind are detected server-side and echoed in `request_parameters` (`provider`, `type`), next to `parse_status` (`ok`, `parse_failed` or `not_found`) and `data` (shape depends on the site; `null` when nothing parsed). More sites are added on the server over time, so the CLI never checks the URL itself and sends it exactly as given. An unsupported URL or page type returns a 400 that is not charged (exit 3); its message lists what is supported. 15 credits per request, including `parse_failed`/`not_found` results; failed fetches are not charged. None of the scrape flags below apply; `data` has its own:
|
|
56
|
+
|
|
57
|
+
- `--country <code>`: two-letter country code of the proxy used to fetch the page, `us` by default.
|
|
58
|
+
- `--transcript`: YouTube videos only. Also fetch the video's transcript into `data.transcript`. It's null when no matching captions are available. If the transcript fetch itself fails, the whole request fails with a 500 (exit 7) and is not charged.
|
|
59
|
+
- `--transcript-language <code>`: caption language to pick, e.g. `en` or `de`. Without it, English is preferred, then the first available track. If the video has no captions in that language, `data.transcript` is null.
|
|
60
|
+
- `--param key=value` (repeatable): site-specific parameters added after this CLI version, sent as-is. A repeated key, `api_key`, `url`, `from_cli`, `__proto__`, or a key that has its own flag (`country`, `transcript`, `transcript_language` — use the flag) is a usage error (exit 2).
|
|
61
|
+
- `--api-key`, `--output`, `--pretty`/`--no-pretty`.
|
|
62
|
+
|
|
63
|
+
`-` reads one URL per line from stdin (blank and `#` lines skipped). Like every URL command, a batch **stops at the first failing item**: earlier results are already written, the remaining URLs are not requested, and the exit code is that error's. With `-o FILE` the file is truncated when the batch starts, then each result is appended. A blank URL or a bad `--param` is a usage error (exit 2) before any request.
|
|
53
64
|
|
|
54
65
|
Common flags shared by every scrape command:
|
|
55
66
|
|
|
@@ -127,6 +138,12 @@ webscraping-ai serp coffee machines --gl de --hl de --page 2
|
|
|
127
138
|
# Just the organic result links
|
|
128
139
|
webscraping-ai search "best espresso grinder" | jq -r '.organic_results[].link'
|
|
129
140
|
|
|
141
|
+
# Structured data for a YouTube video, with its transcript
|
|
142
|
+
webscraping-ai data 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' --transcript | jq '.data.title'
|
|
143
|
+
|
|
144
|
+
# Structured data for a list of profile/post URLs, one JSON result per line
|
|
145
|
+
cat social-urls.txt | webscraping-ai data - --no-pretty -o social.ndjson
|
|
146
|
+
|
|
130
147
|
# Headers from a file
|
|
131
148
|
webscraping-ai html https://example.com --headers @./headers.json
|
|
132
149
|
```
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command12 } from "commander";
|
|
5
5
|
import { readFileSync } from "fs";
|
|
6
6
|
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
7
7
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -67,6 +67,43 @@ function maskApiKey(key) {
|
|
|
67
67
|
|
|
68
68
|
// src/lib/output.ts
|
|
69
69
|
import { promises as fs2 } from "fs";
|
|
70
|
+
|
|
71
|
+
// src/lib/stdin.ts
|
|
72
|
+
import readline from "readline";
|
|
73
|
+
function urlIsStdin(url) {
|
|
74
|
+
return url === "-";
|
|
75
|
+
}
|
|
76
|
+
async function* readUrlsFromStdin() {
|
|
77
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
78
|
+
for await (const raw of rl) {
|
|
79
|
+
const line = raw.trim();
|
|
80
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
81
|
+
yield line;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function* readQueriesFromStdin(input = process.stdin) {
|
|
85
|
+
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
86
|
+
for await (const line of rl) {
|
|
87
|
+
if (line.trim() === "") continue;
|
|
88
|
+
yield line;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function* urlIterator(arg) {
|
|
92
|
+
if (urlIsStdin(arg)) {
|
|
93
|
+
yield* readUrlsFromStdin();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
yield arg;
|
|
97
|
+
}
|
|
98
|
+
async function* queryIterator(arg) {
|
|
99
|
+
if (urlIsStdin(arg)) {
|
|
100
|
+
yield* readQueriesFromStdin();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
yield arg;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/lib/output.ts
|
|
70
107
|
async function emit(value, opts = {}, write = {}) {
|
|
71
108
|
const text = stringify(value, opts);
|
|
72
109
|
if (opts.output && opts.output.trim() !== "") {
|
|
@@ -91,6 +128,13 @@ function stringify(value, opts = {}) {
|
|
|
91
128
|
const pretty = opts.pretty ?? process.stdout.isTTY ?? false;
|
|
92
129
|
return JSON.stringify(value, null, pretty ? 2 : 0);
|
|
93
130
|
}
|
|
131
|
+
async function openEmitter(opts, arg) {
|
|
132
|
+
if (urlIsStdin(arg) && opts.output && opts.output.trim() !== "") {
|
|
133
|
+
await fs2.writeFile(opts.output, "");
|
|
134
|
+
return (value) => emit(value, opts, { append: true });
|
|
135
|
+
}
|
|
136
|
+
return createEmitter(opts);
|
|
137
|
+
}
|
|
94
138
|
|
|
95
139
|
// src/lib/sdk.ts
|
|
96
140
|
import { WebScrapingAI } from "webscraping-ai";
|
|
@@ -140,7 +184,9 @@ function createClient(options) {
|
|
|
140
184
|
case "fields":
|
|
141
185
|
return (o) => target.fields({ ...o, ...tag });
|
|
142
186
|
case "serp":
|
|
143
|
-
return (o) => target.serp({ ...o, ...
|
|
187
|
+
return (o) => target.serp({ ...o, params: { ...o.params, from_cli: true } });
|
|
188
|
+
case "data":
|
|
189
|
+
return (o) => target.data({ ...o, params: { ...o.params, from_cli: true } });
|
|
144
190
|
case "account":
|
|
145
191
|
return value.bind(target);
|
|
146
192
|
default:
|
|
@@ -227,6 +273,43 @@ function buildSerpOptions(query, flags = {}) {
|
|
|
227
273
|
}
|
|
228
274
|
return out;
|
|
229
275
|
}
|
|
276
|
+
var RESERVED_DATA_PARAMS = /* @__PURE__ */ new Set(["api_key", "url", "from_cli", "__proto__"]);
|
|
277
|
+
var NAMED_DATA_PARAMS = {
|
|
278
|
+
country: "--country",
|
|
279
|
+
transcript: "--transcript",
|
|
280
|
+
transcript_language: "--transcript-language"
|
|
281
|
+
};
|
|
282
|
+
function parseDataParams(raw = []) {
|
|
283
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
284
|
+
for (const entry of raw) {
|
|
285
|
+
const eq = entry.indexOf("=");
|
|
286
|
+
if (eq <= 0) throw new Error(`--param must be key=value, got: ${entry}`);
|
|
287
|
+
const key = entry.slice(0, eq);
|
|
288
|
+
if (RESERVED_DATA_PARAMS.has(key)) {
|
|
289
|
+
throw new Error(`--param must not set "${key}"`);
|
|
290
|
+
}
|
|
291
|
+
if (Object.hasOwn(NAMED_DATA_PARAMS, key)) {
|
|
292
|
+
throw new Error(`--param must not set "${key}"; use ${NAMED_DATA_PARAMS[key]}`);
|
|
293
|
+
}
|
|
294
|
+
if (Object.hasOwn(out, key)) {
|
|
295
|
+
throw new Error(`--param "${key}" given more than once`);
|
|
296
|
+
}
|
|
297
|
+
out[key] = entry.slice(eq + 1);
|
|
298
|
+
}
|
|
299
|
+
return { ...out };
|
|
300
|
+
}
|
|
301
|
+
function buildDataOptions(url, flags = {}) {
|
|
302
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
303
|
+
throw new Error("data requires a non-empty url");
|
|
304
|
+
}
|
|
305
|
+
const out = { url };
|
|
306
|
+
if (flags.country) out.country = flags.country;
|
|
307
|
+
if (flags.transcript !== void 0) out.transcript = flags.transcript;
|
|
308
|
+
if (flags.transcriptLanguage) out.transcript_language = flags.transcriptLanguage;
|
|
309
|
+
const params = parseDataParams(flags.param);
|
|
310
|
+
if (Object.keys(params).length > 0) out.params = params;
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
230
313
|
|
|
231
314
|
// src/lib/options.ts
|
|
232
315
|
import { promises as fs3 } from "fs";
|
|
@@ -280,41 +363,6 @@ function parseJsonObject(text, label) {
|
|
|
280
363
|
return out;
|
|
281
364
|
}
|
|
282
365
|
|
|
283
|
-
// src/lib/stdin.ts
|
|
284
|
-
import readline from "readline";
|
|
285
|
-
function urlIsStdin(url) {
|
|
286
|
-
return url === "-";
|
|
287
|
-
}
|
|
288
|
-
async function* readUrlsFromStdin() {
|
|
289
|
-
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
290
|
-
for await (const raw of rl) {
|
|
291
|
-
const line = raw.trim();
|
|
292
|
-
if (line === "" || line.startsWith("#")) continue;
|
|
293
|
-
yield line;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
async function* readQueriesFromStdin(input = process.stdin) {
|
|
297
|
-
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
298
|
-
for await (const line of rl) {
|
|
299
|
-
if (line.trim() === "") continue;
|
|
300
|
-
yield line;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
async function* urlIterator(arg) {
|
|
304
|
-
if (urlIsStdin(arg)) {
|
|
305
|
-
yield* readUrlsFromStdin();
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
yield arg;
|
|
309
|
-
}
|
|
310
|
-
async function* queryIterator(arg) {
|
|
311
|
-
if (urlIsStdin(arg)) {
|
|
312
|
-
yield* readQueriesFromStdin();
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
yield arg;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
366
|
// src/commands/ask.ts
|
|
319
367
|
function askCommand() {
|
|
320
368
|
const cmd = new Command2("ask").description("Ask an AI question about a page (wraps /ai/question)").argument("<url>", "page URL (or `-` to read URLs from stdin)").requiredOption("-q, --question <text>", "the question / instructions for the model").addOption(new Option2("--format <fmt>", "response wrapping").choices(["text", "json"]));
|
|
@@ -324,7 +372,7 @@ function askCommand() {
|
|
|
324
372
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
325
373
|
const headers = await parseHeaders(opts.headers);
|
|
326
374
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
327
|
-
const write =
|
|
375
|
+
const write = await openEmitter(opts, url);
|
|
328
376
|
for await (const target of urlIterator(url)) {
|
|
329
377
|
const sdkOptions = buildQuestionOptions(target, opts.question, opts, { headers });
|
|
330
378
|
const result = await client.question(sdkOptions);
|
|
@@ -373,10 +421,117 @@ function authCommand() {
|
|
|
373
421
|
return cmd;
|
|
374
422
|
}
|
|
375
423
|
|
|
376
|
-
// src/commands/
|
|
424
|
+
// src/commands/data.ts
|
|
377
425
|
import { Command as Command4 } from "commander";
|
|
426
|
+
|
|
427
|
+
// src/lib/errors.ts
|
|
428
|
+
import {
|
|
429
|
+
APIConnectionError,
|
|
430
|
+
APIError,
|
|
431
|
+
APITimeoutError,
|
|
432
|
+
AuthenticationError,
|
|
433
|
+
BadRequestError,
|
|
434
|
+
GatewayTimeoutError,
|
|
435
|
+
PaymentRequiredError,
|
|
436
|
+
RateLimitError,
|
|
437
|
+
ServerError,
|
|
438
|
+
WebScrapingAIError
|
|
439
|
+
} from "webscraping-ai";
|
|
440
|
+
var EXIT_CODES = {
|
|
441
|
+
ok: 0,
|
|
442
|
+
generic: 1,
|
|
443
|
+
usage: 2,
|
|
444
|
+
badRequest: 3,
|
|
445
|
+
auth: 4,
|
|
446
|
+
paymentRequired: 5,
|
|
447
|
+
rateLimit: 6,
|
|
448
|
+
serverError: 7,
|
|
449
|
+
timeout: 8,
|
|
450
|
+
connection: 9
|
|
451
|
+
};
|
|
452
|
+
function exitCodeFor(err) {
|
|
453
|
+
if (err instanceof BadRequestError) return EXIT_CODES.badRequest;
|
|
454
|
+
if (err instanceof AuthenticationError) return EXIT_CODES.auth;
|
|
455
|
+
if (err instanceof PaymentRequiredError) return EXIT_CODES.paymentRequired;
|
|
456
|
+
if (err instanceof RateLimitError) return EXIT_CODES.rateLimit;
|
|
457
|
+
if (err instanceof ServerError) return EXIT_CODES.serverError;
|
|
458
|
+
if (err instanceof GatewayTimeoutError) return EXIT_CODES.timeout;
|
|
459
|
+
if (err instanceof APITimeoutError) return EXIT_CODES.timeout;
|
|
460
|
+
if (err instanceof APIConnectionError) return EXIT_CODES.connection;
|
|
461
|
+
if (err instanceof APIError) return EXIT_CODES.serverError;
|
|
462
|
+
if (err instanceof WebScrapingAIError) return EXIT_CODES.generic;
|
|
463
|
+
return EXIT_CODES.generic;
|
|
464
|
+
}
|
|
465
|
+
function formatError(err) {
|
|
466
|
+
return redactSecrets(rawMessage(err));
|
|
467
|
+
}
|
|
468
|
+
function rawMessage(err) {
|
|
469
|
+
if (err instanceof APIError) {
|
|
470
|
+
const parts = [err.message];
|
|
471
|
+
if (err.status) parts.push(`(HTTP ${err.status})`);
|
|
472
|
+
return parts.join(" ");
|
|
473
|
+
}
|
|
474
|
+
if (err instanceof Error) return err.message;
|
|
475
|
+
return String(err);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/commands/data.ts
|
|
479
|
+
function collect(value, previous = []) {
|
|
480
|
+
return [...previous, value];
|
|
481
|
+
}
|
|
482
|
+
function dataCommand() {
|
|
483
|
+
const cmd = new Command4("data").description(
|
|
484
|
+
"Structured JSON for a page on a supported site, e.g. YouTube, TikTok, X, LinkedIn, Instagram, Reddit (wraps /data, 15 credits/request; more sites are added server-side, an unsupported URL or page type gets a 400, not charged, listing what is supported)"
|
|
485
|
+
).argument("<url>", "page URL (or `-` to read URLs from stdin, one per line)").option(
|
|
486
|
+
"--country <code>",
|
|
487
|
+
"two-letter country code of the proxy used to fetch the page (default: us)"
|
|
488
|
+
).option(
|
|
489
|
+
"--transcript",
|
|
490
|
+
"YouTube videos only: also fetch the transcript into data.transcript (null without matching captions; a failed transcript fetch fails the request with a 500, not charged)"
|
|
491
|
+
).option(
|
|
492
|
+
"--transcript-language <code>",
|
|
493
|
+
"caption language to pick, e.g. en or de (default: English, then the first track)"
|
|
494
|
+
).option(
|
|
495
|
+
"--param <key=value>",
|
|
496
|
+
"extra site-specific query parameter sent as-is (repeatable)",
|
|
497
|
+
collect
|
|
498
|
+
).option("-k, --api-key <key>", "API key (overrides $WEBSCRAPING_AI_API_KEY and config file)").option("-o, --output <file>", "write result to FILE instead of stdout").option("--pretty", "pretty-print JSON output (default on TTY)").option("--no-pretty", "force single-line JSON output");
|
|
499
|
+
cmd.action(async (url, opts) => {
|
|
500
|
+
if (!urlIsStdin(url) && url.trim() === "") {
|
|
501
|
+
cmd.error("error: data requires a non-empty url", {
|
|
502
|
+
exitCode: EXIT_CODES.usage,
|
|
503
|
+
code: "wsai.emptyUrl"
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
parseDataParams(opts.param);
|
|
508
|
+
} catch (err) {
|
|
509
|
+
cmd.error(`error: ${err.message}`, {
|
|
510
|
+
exitCode: EXIT_CODES.usage,
|
|
511
|
+
code: "wsai.invalidParam"
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
const flags = {
|
|
515
|
+
country: opts.country,
|
|
516
|
+
transcript: opts.transcript,
|
|
517
|
+
transcriptLanguage: opts.transcriptLanguage,
|
|
518
|
+
param: opts.param
|
|
519
|
+
};
|
|
520
|
+
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
521
|
+
const client = createClient({ apiKey });
|
|
522
|
+
const write = await openEmitter(opts, url);
|
|
523
|
+
for await (const target of urlIterator(url)) {
|
|
524
|
+
const result = await client.data(buildDataOptions(target, flags));
|
|
525
|
+
await write(result);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
return cmd;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/commands/extract.ts
|
|
532
|
+
import { Command as Command5 } from "commander";
|
|
378
533
|
function extractCommand() {
|
|
379
|
-
const cmd = new
|
|
534
|
+
const cmd = new Command5("extract").description("Extract structured fields from a page using AI (wraps /ai/fields)").argument("<url>", "page URL (or `-` to read URLs from stdin)").requiredOption(
|
|
380
535
|
"-f, --fields <json>",
|
|
381
536
|
"fields to extract \u2014 JSON object {name: description} or @file.json"
|
|
382
537
|
);
|
|
@@ -387,7 +542,7 @@ function extractCommand() {
|
|
|
387
542
|
const headers = await parseHeaders(opts.headers);
|
|
388
543
|
const fields = await parseFields(opts.fields);
|
|
389
544
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
390
|
-
const write =
|
|
545
|
+
const write = await openEmitter(opts, url);
|
|
391
546
|
for await (const target of urlIterator(url)) {
|
|
392
547
|
const sdkOptions = buildFieldsOptions(target, fields, opts, { headers });
|
|
393
548
|
const result = await client.fields(sdkOptions);
|
|
@@ -398,9 +553,9 @@ function extractCommand() {
|
|
|
398
553
|
}
|
|
399
554
|
|
|
400
555
|
// src/commands/html.ts
|
|
401
|
-
import { Command as
|
|
556
|
+
import { Command as Command6, Option as Option3 } from "commander";
|
|
402
557
|
function htmlCommand() {
|
|
403
|
-
const cmd = new
|
|
558
|
+
const cmd = new Command6("html").description("Fetch the full HTML of a page").argument("<url>", "page URL (or `-` to read URLs from stdin)").addOption(
|
|
404
559
|
new Option3("--return-script-result", "return the result of --js-script instead of HTML")
|
|
405
560
|
).addOption(new Option3("--format <fmt>", "response wrapping").choices(["text", "json"]));
|
|
406
561
|
addCommonScrapeOptions(cmd);
|
|
@@ -408,7 +563,7 @@ function htmlCommand() {
|
|
|
408
563
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
409
564
|
const headers = await parseHeaders(opts.headers);
|
|
410
565
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
411
|
-
const write =
|
|
566
|
+
const write = await openEmitter(opts, url);
|
|
412
567
|
for await (const target of urlIterator(url)) {
|
|
413
568
|
const sdkOptions = buildHtmlOptions(
|
|
414
569
|
target,
|
|
@@ -426,9 +581,9 @@ function htmlCommand() {
|
|
|
426
581
|
}
|
|
427
582
|
|
|
428
583
|
// src/commands/selected.ts
|
|
429
|
-
import { Command as
|
|
584
|
+
import { Command as Command7, Option as Option4 } from "commander";
|
|
430
585
|
function selectedCommand() {
|
|
431
|
-
const cmd = new
|
|
586
|
+
const cmd = new Command7("selected").description("Fetch HTML of one CSS-selected page area").argument("<url>", "page URL (or `-` to read URLs from stdin)").option(
|
|
432
587
|
"-s, --selector <css>",
|
|
433
588
|
"CSS selector for the target element(s); omit for whole-page HTML"
|
|
434
589
|
).addOption(new Option4("--format <fmt>", "response wrapping").choices(["text", "json"]));
|
|
@@ -437,7 +592,7 @@ function selectedCommand() {
|
|
|
437
592
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
438
593
|
const headers = await parseHeaders(opts.headers);
|
|
439
594
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
440
|
-
const write =
|
|
595
|
+
const write = await openEmitter(opts, url);
|
|
441
596
|
for await (const target of urlIterator(url)) {
|
|
442
597
|
const sdkOptions = buildSelectedOptions(target, opts.selector, opts, { headers });
|
|
443
598
|
const result = await client.selected(sdkOptions);
|
|
@@ -448,9 +603,9 @@ function selectedCommand() {
|
|
|
448
603
|
}
|
|
449
604
|
|
|
450
605
|
// src/commands/selectedMultiple.ts
|
|
451
|
-
import { Command as
|
|
606
|
+
import { Command as Command8 } from "commander";
|
|
452
607
|
function selectedMultipleCommand() {
|
|
453
|
-
const cmd = new
|
|
608
|
+
const cmd = new Command8("selected-multiple").description("Fetch HTML of multiple CSS-selected page areas").argument("<url>", "page URL (or `-` to read URLs from stdin)").option(
|
|
454
609
|
"-s, --selector <css>",
|
|
455
610
|
"CSS selector \u2014 pass multiple times for multiple selectors",
|
|
456
611
|
collectMulti,
|
|
@@ -462,7 +617,7 @@ function selectedMultipleCommand() {
|
|
|
462
617
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
463
618
|
const headers = await parseHeaders(opts.headers);
|
|
464
619
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
465
|
-
const write =
|
|
620
|
+
const write = await openEmitter(opts, url);
|
|
466
621
|
for await (const target of urlIterator(url)) {
|
|
467
622
|
const sdkOptions = buildSelectedMultipleOptions(target, selectors, opts, { headers });
|
|
468
623
|
const result = await client.selectedMultiple(sdkOptions);
|
|
@@ -476,62 +631,9 @@ function collectMulti(value, previous) {
|
|
|
476
631
|
}
|
|
477
632
|
|
|
478
633
|
// src/commands/serp.ts
|
|
479
|
-
import { Command as
|
|
480
|
-
|
|
481
|
-
// src/lib/errors.ts
|
|
482
|
-
import {
|
|
483
|
-
APIConnectionError,
|
|
484
|
-
APIError,
|
|
485
|
-
APITimeoutError,
|
|
486
|
-
AuthenticationError,
|
|
487
|
-
BadRequestError,
|
|
488
|
-
GatewayTimeoutError,
|
|
489
|
-
PaymentRequiredError,
|
|
490
|
-
RateLimitError,
|
|
491
|
-
ServerError,
|
|
492
|
-
WebScrapingAIError
|
|
493
|
-
} from "webscraping-ai";
|
|
494
|
-
var EXIT_CODES = {
|
|
495
|
-
ok: 0,
|
|
496
|
-
generic: 1,
|
|
497
|
-
usage: 2,
|
|
498
|
-
badRequest: 3,
|
|
499
|
-
auth: 4,
|
|
500
|
-
paymentRequired: 5,
|
|
501
|
-
rateLimit: 6,
|
|
502
|
-
serverError: 7,
|
|
503
|
-
timeout: 8,
|
|
504
|
-
connection: 9
|
|
505
|
-
};
|
|
506
|
-
function exitCodeFor(err) {
|
|
507
|
-
if (err instanceof BadRequestError) return EXIT_CODES.badRequest;
|
|
508
|
-
if (err instanceof AuthenticationError) return EXIT_CODES.auth;
|
|
509
|
-
if (err instanceof PaymentRequiredError) return EXIT_CODES.paymentRequired;
|
|
510
|
-
if (err instanceof RateLimitError) return EXIT_CODES.rateLimit;
|
|
511
|
-
if (err instanceof ServerError) return EXIT_CODES.serverError;
|
|
512
|
-
if (err instanceof GatewayTimeoutError) return EXIT_CODES.timeout;
|
|
513
|
-
if (err instanceof APITimeoutError) return EXIT_CODES.timeout;
|
|
514
|
-
if (err instanceof APIConnectionError) return EXIT_CODES.connection;
|
|
515
|
-
if (err instanceof APIError) return EXIT_CODES.serverError;
|
|
516
|
-
if (err instanceof WebScrapingAIError) return EXIT_CODES.generic;
|
|
517
|
-
return EXIT_CODES.generic;
|
|
518
|
-
}
|
|
519
|
-
function formatError(err) {
|
|
520
|
-
return redactSecrets(rawMessage(err));
|
|
521
|
-
}
|
|
522
|
-
function rawMessage(err) {
|
|
523
|
-
if (err instanceof APIError) {
|
|
524
|
-
const parts = [err.message];
|
|
525
|
-
if (err.status) parts.push(`(HTTP ${err.status})`);
|
|
526
|
-
return parts.join(" ");
|
|
527
|
-
}
|
|
528
|
-
if (err instanceof Error) return err.message;
|
|
529
|
-
return String(err);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// src/commands/serp.ts
|
|
634
|
+
import { Command as Command9, Option as Option5 } from "commander";
|
|
533
635
|
function serpCommand() {
|
|
534
|
-
const cmd = new
|
|
636
|
+
const cmd = new Command9("serp").alias("search").description("Search Google and print parsed results as JSON (wraps /serp, 15 credits/search)").argument("<query...>", "search query (or `-` to read queries from stdin, one per line)").addOption(new Option5("--engine <name>", "search engine (default: google)").choices(["google"])).option("--gl <code>", "two-letter country code for search geolocation (default: us)").option("--hl <code>", "two-letter language code for the results (default: en)").option(
|
|
535
637
|
"--page <n>",
|
|
536
638
|
"results page number, integer >= 1, 10 results per page (default: 1; server rejects > 100 with a 400)"
|
|
537
639
|
).option("-k, --api-key <key>", "API key (overrides $WEBSCRAPING_AI_API_KEY and config file)").option("-o, --output <file>", "write result to FILE instead of stdout").option("--pretty", "pretty-print JSON output (default on TTY)").option("--no-pretty", "force single-line JSON output");
|
|
@@ -556,7 +658,7 @@ function serpCommand() {
|
|
|
556
658
|
const flags = { engine: opts.engine, gl: opts.gl, hl: opts.hl, page };
|
|
557
659
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
558
660
|
const client = createClient({ apiKey });
|
|
559
|
-
const write =
|
|
661
|
+
const write = await openEmitter(opts, query);
|
|
560
662
|
for await (const q of queryIterator(query)) {
|
|
561
663
|
const result = await client.serp(buildSerpOptions(q, flags));
|
|
562
664
|
await write(result);
|
|
@@ -566,7 +668,7 @@ function serpCommand() {
|
|
|
566
668
|
}
|
|
567
669
|
|
|
568
670
|
// src/commands/setupSkill.ts
|
|
569
|
-
import { Command as
|
|
671
|
+
import { Command as Command10 } from "commander";
|
|
570
672
|
|
|
571
673
|
// src/lib/skill.ts
|
|
572
674
|
import { promises as fs4 } from "fs";
|
|
@@ -683,11 +785,11 @@ async function copyDirectory(source, destination) {
|
|
|
683
785
|
|
|
684
786
|
// src/commands/setupSkill.ts
|
|
685
787
|
function setupCommand() {
|
|
686
|
-
const cmd = new
|
|
788
|
+
const cmd = new Command10("setup").description("Install the AI agent skill into your editor(s)");
|
|
687
789
|
cmd.command("skill").description("Install the WebScraping.AI skill into one or more AI coding editors").option(
|
|
688
790
|
"-e, --editor <id>",
|
|
689
791
|
`editor id (repeatable). Supported: ${EDITORS.map((e) => e.id).join(", ")}`,
|
|
690
|
-
|
|
792
|
+
collect2,
|
|
691
793
|
[]
|
|
692
794
|
).option("-a, --all", "install into every supported editor").option("-f, --force", "overwrite existing skill files").action(async (opts) => {
|
|
693
795
|
const selected = resolveSelection(opts);
|
|
@@ -712,7 +814,7 @@ function setupCommand() {
|
|
|
712
814
|
});
|
|
713
815
|
return cmd;
|
|
714
816
|
}
|
|
715
|
-
function
|
|
817
|
+
function collect2(value, previous) {
|
|
716
818
|
return [...previous, value];
|
|
717
819
|
}
|
|
718
820
|
function resolveSelection(opts) {
|
|
@@ -726,9 +828,9 @@ function resolveSelection(opts) {
|
|
|
726
828
|
}
|
|
727
829
|
|
|
728
830
|
// src/commands/text.ts
|
|
729
|
-
import { Command as
|
|
831
|
+
import { Command as Command11, Option as Option6 } from "commander";
|
|
730
832
|
function textCommand() {
|
|
731
|
-
const cmd = new
|
|
833
|
+
const cmd = new Command11("text").description("Fetch the visible text of a page").argument("<url>", "page URL (or `-` to read URLs from stdin)").addOption(
|
|
732
834
|
new Option6("--text-format <fmt>", "text response format").choices(["plain", "json", "xml"])
|
|
733
835
|
).option("--return-links", "include page links (text-format=json only)");
|
|
734
836
|
addCommonScrapeOptions(cmd);
|
|
@@ -736,7 +838,7 @@ function textCommand() {
|
|
|
736
838
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
737
839
|
const headers = await parseHeaders(opts.headers);
|
|
738
840
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
739
|
-
const write =
|
|
841
|
+
const write = await openEmitter(opts, url);
|
|
740
842
|
for await (const target of urlIterator(url)) {
|
|
741
843
|
const sdkOptions = buildTextOptions(
|
|
742
844
|
target,
|
|
@@ -762,9 +864,9 @@ function packageVersion() {
|
|
|
762
864
|
return raw.version;
|
|
763
865
|
}
|
|
764
866
|
function buildProgram() {
|
|
765
|
-
const program = new
|
|
867
|
+
const program = new Command12();
|
|
766
868
|
program.name("webscraping-ai").description(
|
|
767
|
-
"Official CLI for the WebScraping.AI API. Fetch pages, search Google, ask AI, extract
|
|
869
|
+
"Official CLI for the WebScraping.AI API. Fetch pages, search Google, get structured site data, ask AI, extract fields.\nSet $WEBSCRAPING_AI_API_KEY or run `webscraping-ai auth set <key>` first."
|
|
768
870
|
).version(packageVersion(), "-v, --version", "output the version number").showHelpAfterError();
|
|
769
871
|
program.addCommand(htmlCommand());
|
|
770
872
|
program.addCommand(textCommand());
|
|
@@ -773,6 +875,7 @@ function buildProgram() {
|
|
|
773
875
|
program.addCommand(askCommand());
|
|
774
876
|
program.addCommand(extractCommand());
|
|
775
877
|
program.addCommand(serpCommand());
|
|
878
|
+
program.addCommand(dataCommand());
|
|
776
879
|
program.addCommand(accountCommand());
|
|
777
880
|
program.addCommand(authCommand());
|
|
778
881
|
program.addCommand(setupCommand());
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/commands/account.ts","../src/lib/config.ts","../src/lib/output.ts","../src/lib/sdk.ts","../src/lib/redact.ts","../src/commands/ask.ts","../src/lib/buildRequest.ts","../src/lib/options.ts","../src/lib/stdin.ts","../src/commands/auth.ts","../src/commands/extract.ts","../src/commands/html.ts","../src/commands/selected.ts","../src/commands/selectedMultiple.ts","../src/commands/serp.ts","../src/lib/errors.ts","../src/commands/setupSkill.ts","../src/lib/skill.ts","../src/commands/text.ts"],"sourcesContent":["/**\n * `webscraping-ai` / `wsai` CLI entry point.\n *\n * Wires every subcommand into a single commander root. Errors thrown from\n * subcommand actions land in the top-level `.catch`, which maps SDK\n * exception classes to stable exit codes (see `lib/errors.ts`).\n */\n\nimport { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { accountCommand } from './commands/account.js';\nimport { askCommand } from './commands/ask.js';\nimport { authCommand } from './commands/auth.js';\nimport { extractCommand } from './commands/extract.js';\nimport { htmlCommand } from './commands/html.js';\nimport { selectedCommand } from './commands/selected.js';\nimport { selectedMultipleCommand } from './commands/selectedMultiple.js';\nimport { serpCommand } from './commands/serp.js';\nimport { setupCommand } from './commands/setupSkill.js';\nimport { textCommand } from './commands/text.js';\nimport { exitCodeFor, formatError } from './lib/errors.js';\n\nfunction packageVersion(): string {\n // Read package.json at runtime so we don't need a code-gen step. The dist\n // bundle sits at `dist/cli.js`, so package.json is one directory up.\n const here = dirname(fileURLToPath(import.meta.url));\n const pkgPath = resolve(here, '..', 'package.json');\n const raw = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string };\n return raw.version;\n}\n\nexport function buildProgram(): Command {\n const program = new Command();\n program\n .name('webscraping-ai')\n .description(\n 'Official CLI for the WebScraping.AI API. Fetch pages, search Google, ask AI, extract structured data.\\n' +\n 'Set $WEBSCRAPING_AI_API_KEY or run `webscraping-ai auth set <key>` first.',\n )\n .version(packageVersion(), '-v, --version', 'output the version number')\n .showHelpAfterError();\n\n program.addCommand(htmlCommand());\n program.addCommand(textCommand());\n program.addCommand(selectedCommand());\n program.addCommand(selectedMultipleCommand());\n program.addCommand(askCommand());\n program.addCommand(extractCommand());\n program.addCommand(serpCommand());\n program.addCommand(accountCommand());\n program.addCommand(authCommand());\n program.addCommand(setupCommand());\n\n return program;\n}\n\nasync function main(argv: string[]): Promise<void> {\n const program = buildProgram();\n try {\n await program.parseAsync(argv);\n } catch (err) {\n process.stderr.write(`Error: ${formatError(err)}\\n`);\n process.exit(exitCodeFor(err));\n }\n}\n\n// Only execute when run as a script. When imported (e.g. from tests),\n// `buildProgram` is the public surface and `main` stays dormant.\nconst entryPath = process.argv[1] ? fileURLToPath(import.meta.url) : '';\nif (entryPath && process.argv[1] === entryPath) {\n main(process.argv).catch((err: unknown) => {\n process.stderr.write(`Error: ${formatError(err)}\\n`);\n process.exit(exitCodeFor(err));\n });\n}\n","import { Command } from 'commander';\n\nimport { resolveApiKey } from '../lib/config.js';\nimport { emit } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\n\ninterface AccountFlags {\n apiKey?: string;\n output?: string;\n pretty?: boolean;\n}\n\nexport function accountCommand(): Command {\n return new Command('account')\n .description('Show account quota and usage information')\n .option('-k, --api-key <key>', 'API key (overrides $WEBSCRAPING_AI_API_KEY and config file)')\n .option('-o, --output <file>', 'write result to FILE instead of stdout')\n .option('--pretty', 'pretty-print JSON output (default on TTY)')\n .option('--no-pretty', 'force single-line JSON output')\n .action(async (opts: AccountFlags) => {\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const client = createClient({ apiKey });\n const result = await client.account();\n await emit(result, opts);\n });\n}\n","/**\n * API key resolution and persisted config.\n *\n * Resolution order (first non-empty wins):\n * 1. `--api-key` flag (handled by the caller, passed in)\n * 2. `WEBSCRAPING_AI_API_KEY` env var\n * 3. `~/.config/webscraping-ai/config.json` (or `$XDG_CONFIG_HOME/...`)\n *\n * The config file is plain JSON with `{ \"apiKey\": \"...\" }`. We chmod it to\n * 0600 so it's not world-readable.\n */\n\nimport { promises as fs } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\nconst CONFIG_FILENAME = 'config.json';\n\nexport function configDir(): string {\n const xdg = process.env.XDG_CONFIG_HOME;\n if (xdg && xdg.trim() !== '') return join(xdg, 'webscraping-ai');\n return join(homedir(), '.config', 'webscraping-ai');\n}\n\nexport function configPath(): string {\n return join(configDir(), CONFIG_FILENAME);\n}\n\nexport interface PersistedConfig {\n apiKey?: string;\n}\n\nexport async function readPersistedConfig(): Promise<PersistedConfig> {\n try {\n const text = await fs.readFile(configPath(), 'utf8');\n const parsed = JSON.parse(text) as unknown;\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as PersistedConfig;\n }\n return {};\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw err;\n }\n}\n\nexport async function writePersistedConfig(config: PersistedConfig): Promise<void> {\n const path = configPath();\n await fs.mkdir(dirname(path), { recursive: true });\n await fs.writeFile(path, JSON.stringify(config, null, 2) + '\\n', { mode: 0o600 });\n // `mode` above only applies when the file is freshly created; an existing\n // file keeps its old (possibly world-readable) perms. Enforce 0600\n // unconditionally — chmod on a just-written file is cheap and idempotent.\n // No-op on Windows, but harmless.\n await fs.chmod(path, 0o600);\n}\n\nexport async function clearPersistedConfig(): Promise<boolean> {\n try {\n await fs.unlink(configPath());\n return true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return false;\n throw err;\n }\n}\n\nexport interface ResolveApiKeyOptions {\n /** `--api-key` flag value, if the user passed one. */\n flag?: string;\n}\n\nexport async function resolveApiKey(opts: ResolveApiKeyOptions = {}): Promise<string> {\n if (opts.flag && opts.flag.trim() !== '') return opts.flag.trim();\n\n const env = process.env.WEBSCRAPING_AI_API_KEY;\n if (env && env.trim() !== '') return env.trim();\n\n const persisted = await readPersistedConfig();\n if (persisted.apiKey && persisted.apiKey.trim() !== '') return persisted.apiKey.trim();\n\n throw new Error(\n 'No API key found. Set $WEBSCRAPING_AI_API_KEY, pass --api-key, or run `webscraping-ai auth set <key>`.',\n );\n}\n\nexport function maskApiKey(key: string): string {\n if (key.length <= 8) return '*'.repeat(key.length);\n return `${key.slice(0, 4)}${'*'.repeat(key.length - 8)}${key.slice(-4)}`;\n}\n","/**\n * Output handling for command results.\n *\n * All command handlers funnel through `emit()`. The result type drives format:\n * - strings → written verbatim\n * - objects/arrays → JSON.stringified (pretty by default; `--pretty` is the\n * default for terminals, single-line for pipes/redirects)\n *\n * `--output FILE` writes to disk instead of stdout. In stdin batch mode use\n * `createEmitter()`: it truncates the file on the first result and appends\n * every later one, so each item survives instead of overwriting the last.\n */\n\nimport { promises as fs } from 'node:fs';\n\nexport interface EmitOptions {\n output?: string;\n pretty?: boolean;\n}\n\nexport interface EmitWriteOptions {\n /** Append to `--output` instead of truncating it (batch items after the first). */\n append?: boolean;\n}\n\nexport async function emit(\n value: unknown,\n opts: EmitOptions = {},\n write: EmitWriteOptions = {},\n): Promise<void> {\n const text = stringify(value, opts);\n\n if (opts.output && opts.output.trim() !== '') {\n const data = text + (text.endsWith('\\n') ? '' : '\\n');\n if (write.append) await fs.appendFile(opts.output, data);\n else await fs.writeFile(opts.output, data);\n return;\n }\n\n process.stdout.write(text);\n if (!text.endsWith('\\n')) process.stdout.write('\\n');\n}\n\n/**\n * Returns an `emit` bound to `opts` for a (possibly batched) loop: the first\n * call truncates `--output`, later calls append.\n */\nexport function createEmitter(opts: EmitOptions = {}): (value: unknown) => Promise<void> {\n let first = true;\n return async (value: unknown) => {\n const append = !first;\n first = false;\n await emit(value, opts, { append });\n };\n}\n\nexport function stringify(value: unknown, opts: EmitOptions = {}): string {\n if (typeof value === 'string') return value;\n\n const pretty = opts.pretty ?? process.stdout.isTTY ?? false;\n return JSON.stringify(value, null, pretty ? 2 : 0);\n}\n","/**\n * SDK adapter.\n *\n * Wraps the published `webscraping-ai` Node SDK. The CLI sends a\n * `from_cli=true` analytics flag on the page/AI endpoints (mirrors `from_n8n`\n * and `from_mcp_server` used by the n8n node and MCP server). It is NOT sent\n * on `serp` or `account`: the SDK builds those query strings from a fixed set\n * of keys and drops anything else. The SDK's typed\n * options don't accept arbitrary keys, so we go through a single cast point\n * here rather than scattering casts across each command.\n */\n\nimport { WebScrapingAI } from 'webscraping-ai';\n\nimport { registerSecret } from './redact.js';\nimport type {\n FieldsOptions,\n HtmlOptions,\n QuestionOptions,\n SelectedMultipleOptions,\n SelectedOptions,\n SerpOptions,\n TextOptions,\n} from 'webscraping-ai';\n\nexport interface CreateClientOptions {\n apiKey: string;\n /** Per-request timeout in ms. Maps to the SDK's `timeoutMs`. */\n requestTimeoutMs?: number;\n}\n\n/**\n * Returns a `WebScrapingAI` instance with the CLI's analytics tag baked into\n * every endpoint method. The methods preserve the SDK's public signatures.\n */\nexport function createClient(options: CreateClientOptions): WebScrapingAI {\n registerSecret(options.apiKey);\n const inner = new WebScrapingAI({\n apiKey: options.apiKey,\n timeoutMs: options.requestTimeoutMs,\n });\n\n // Wrap each endpoint method so it injects the CLI analytics tag. We splice\n // the tag in at this seam rather than at every callsite. The cast is local;\n // the SDK passes unknown query params through to the wire.\n const tag = { from_cli: true } as unknown as Record<string, never>;\n\n return new Proxy(inner, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n if (typeof value !== 'function') return value;\n switch (prop) {\n case 'html':\n return (o: HtmlOptions) => target.html({ ...o, ...tag });\n case 'text':\n return (o: TextOptions) => target.text({ ...o, ...tag });\n case 'selected':\n return (o: SelectedOptions) => target.selected({ ...o, ...tag });\n case 'selectedMultiple':\n return (o: SelectedMultipleOptions) => target.selectedMultiple({ ...o, ...tag });\n case 'question':\n return (o: QuestionOptions) => target.question({ ...o, ...tag });\n case 'fields':\n return (o: FieldsOptions) => target.fields({ ...o, ...tag });\n case 'serp':\n // Passed for parity only: SDK 4.1.0's `serp()` forwards just\n // q/engine/gl/hl/page and drops every other key, so `from_cli` is\n // not sent on /serp. Accepted limitation, like `account`.\n return (o: SerpOptions) => target.serp({ ...o, ...tag });\n case 'account':\n // `account` takes no options; the SDK builds the request itself, so\n // there's no seam here for the analytics tag — accepted limitation.\n return value.bind(target);\n default:\n return typeof value === 'function' ? value.bind(target) : value;\n }\n },\n });\n}\n","/**\n * Secret redaction for anything the CLI prints to stderr.\n *\n * API errors can echo the request URL (e.g. an HTML error page that includes\n * `?api_key=...`), so every error message goes through `redactSecrets()`\n * before it is printed. The key in use is registered by `createClient()`.\n */\n\nconst secrets = new Set<string>();\n\n/** Remember a secret so later error output never prints it. */\nexport function registerSecret(value: string | undefined): void {\n if (value && value.trim() !== '') secrets.add(value);\n}\n\n/** Forget registered secrets (tests only). */\nexport function clearSecrets(): void {\n secrets.clear();\n}\n\nconst API_KEY_PARAM = /(api_key=)[^&\\s\"'<>]*/gi;\n\nexport function redactSecrets(text: string): string {\n let out = text.replace(API_KEY_PARAM, '$1[REDACTED]');\n const candidates = new Set(secrets);\n const env = process.env.WEBSCRAPING_AI_API_KEY?.trim();\n if (env) candidates.add(env);\n for (const secret of candidates) {\n for (const form of new Set([secret, encodeURIComponent(secret)])) {\n out = out.split(form).join('[REDACTED]');\n }\n }\n return out;\n}\n","import { Command, Option } from 'commander';\n\nimport { buildQuestionOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { createEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface AskFlags extends CommonRawFlags {\n question?: string;\n}\n\nexport function askCommand(): Command {\n const cmd = new Command('ask')\n .description('Ask an AI question about a page (wraps /ai/question)')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .requiredOption('-q, --question <text>', 'the question / instructions for the model')\n .addOption(new Option('--format <fmt>', 'response wrapping').choices(['text', 'json']));\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: AskFlags) => {\n if (!opts.question) throw new Error('--question is required');\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = createEmitter(opts);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildQuestionOptions(target, opts.question, opts, { headers });\n const result = await client.question(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n","/**\n * Pure builder that maps `--flag` values into SDK option objects.\n *\n * Extracted as a separate function so unit tests can pin the mapping without\n * spinning up commander or making HTTP calls. Each command handler reads its\n * own commander options, awaits `parseHeaders` / `parseFields`, then calls\n * the matching `build*` function here.\n */\n\nimport type {\n CommonRequestOptions,\n FieldsOptions,\n HtmlOptions,\n QuestionOptions,\n SelectedMultipleOptions,\n SelectedOptions,\n SerpOptions,\n TextOptions,\n} from 'webscraping-ai';\n\nimport type { CommonRawFlags } from './options.js';\n\nexport interface ResolvedCommonOptions {\n headers?: Record<string, string>;\n}\n\nexport function buildCommonOptions(\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions = {},\n): CommonRequestOptions {\n const out: CommonRequestOptions = {};\n // `flags.js` is `false` when `--no-js`, `true` when `--js`, `undefined`\n // when neither passed. Pass through only when explicit, so the API default\n // applies otherwise.\n if (flags.js !== undefined) out.js = flags.js;\n if (flags.proxy !== undefined) out.proxy = flags.proxy;\n if (flags.country !== undefined) out.country = flags.country;\n if (flags.device !== undefined) out.device = flags.device;\n if (flags.timeout !== undefined) out.timeout = flags.timeout;\n if (flags.jsTimeout !== undefined) out.js_timeout = flags.jsTimeout;\n if (flags.waitFor !== undefined) out.wait_for = flags.waitFor;\n if (flags.jsScript !== undefined) out.js_script = flags.jsScript;\n if (flags.customProxy !== undefined) out.custom_proxy = flags.customProxy;\n if (flags.errorOn404) out.error_on_404 = true;\n if (flags.errorOnRedirect) out.error_on_redirect = true;\n if (resolved.headers) out.headers = resolved.headers;\n return out;\n}\n\nexport interface HtmlExtras {\n returnScriptResult?: boolean;\n}\n\nexport function buildHtmlOptions(\n url: string,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n extras: HtmlExtras = {},\n): HtmlOptions {\n const out: HtmlOptions = { url, ...buildCommonOptions(flags, resolved) };\n if (flags.format) out.format = flags.format;\n if (extras.returnScriptResult) out.return_script_result = true;\n return out;\n}\n\nexport interface TextExtras {\n textFormat?: 'plain' | 'json' | 'xml';\n returnLinks?: boolean;\n}\n\nexport function buildTextOptions(\n url: string,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n extras: TextExtras = {},\n): TextOptions {\n const out: TextOptions = { url, ...buildCommonOptions(flags, resolved) };\n if (extras.textFormat) out.text_format = extras.textFormat;\n if (extras.returnLinks) out.return_links = true;\n return out;\n}\n\nexport function buildSelectedOptions(\n url: string,\n selector: string | undefined,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n): SelectedOptions {\n // `selector` is optional per the OpenAPI spec: omitting it returns the\n // whole-page HTML. Only attach it when provided. (The installed SDK types\n // still mark it required, so build the object as a partial and cast.)\n const out: Partial<SelectedOptions> = { url, ...buildCommonOptions(flags, resolved) };\n if (selector !== undefined) out.selector = selector;\n if (flags.format) out.format = flags.format;\n return out as SelectedOptions;\n}\n\nexport function buildSelectedMultipleOptions(\n url: string,\n selectors: readonly string[],\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n): SelectedMultipleOptions {\n // `selectors` is optional per the OpenAPI spec: omitting it returns the\n // whole-page HTML. Only attach it when non-empty. (The installed SDK types\n // still mark it required, so build the object as a partial and cast.)\n const out: Partial<SelectedMultipleOptions> = { url, ...buildCommonOptions(flags, resolved) };\n if (selectors.length > 0) out.selectors = selectors;\n return out as SelectedMultipleOptions;\n}\n\nexport function buildQuestionOptions(\n url: string,\n question: string,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n): QuestionOptions {\n const out: QuestionOptions = { url, question, ...buildCommonOptions(flags, resolved) };\n if (flags.format) out.format = flags.format;\n return out;\n}\n\nexport function buildFieldsOptions(\n url: string,\n fields: Record<string, string>,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n): FieldsOptions {\n return { url, fields, ...buildCommonOptions(flags, resolved) };\n}\n\nexport interface SerpRawFlags {\n engine?: 'google';\n gl?: string;\n hl?: string;\n page?: number;\n}\n\n/**\n * `/serp` is query-shaped: it takes none of the common scrape options, so\n * this deliberately does not go through `buildCommonOptions`. `page` must be\n * an integer >= 1 (the server also rejects anything else with a 400, not\n * billed; checking client-side saves the round trip). Pages are 1-100: the\n * server rejects a page above 100 with a 400.\n */\nexport function buildSerpOptions(query: string, flags: SerpRawFlags = {}): SerpOptions {\n // Validate on the trimmed value but send the query as the user typed it.\n if (typeof query !== 'string' || query.trim() === '') {\n throw new Error('serp requires a non-empty query');\n }\n const out: SerpOptions = { q: query };\n if (flags.engine !== undefined) out.engine = flags.engine;\n if (flags.gl) out.gl = flags.gl;\n if (flags.hl) out.hl = flags.hl;\n if (flags.page !== undefined) {\n if (!Number.isSafeInteger(flags.page) || flags.page < 1) {\n throw new Error(`--page must be an integer >= 1, got: ${flags.page}`);\n }\n out.page = flags.page;\n }\n return out;\n}\n","/**\n * Shared commander options and the parsers/expanders behind them.\n *\n * All scrape endpoints share the same nine \"common\" flags. We define them in\n * one place and apply via `addCommonScrapeOptions(cmd)` so the surface stays\n * consistent and any change lands everywhere at once.\n */\n\nimport { promises as fs } from 'node:fs';\nimport { Option } from 'commander';\nimport type { Command } from 'commander';\n\nexport interface CommonRawFlags {\n apiKey?: string;\n output?: string;\n format?: 'text' | 'json';\n pretty?: boolean;\n js?: boolean;\n proxy?: 'datacenter' | 'residential' | 'stealth';\n country?: string;\n device?: 'desktop' | 'mobile' | 'tablet';\n timeout?: number;\n jsTimeout?: number;\n waitFor?: string;\n headers?: string;\n jsScript?: string;\n customProxy?: string;\n errorOn404?: boolean;\n errorOnRedirect?: boolean;\n}\n\n/**\n * Add the flags every scrape-endpoint command needs. Adding here keeps the\n * surface aligned across `html`, `text`, `selected`, etc.\n */\nexport function addCommonScrapeOptions(cmd: Command): Command {\n return cmd\n .option('-k, --api-key <key>', 'API key (overrides $WEBSCRAPING_AI_API_KEY and config file)')\n .option('-o, --output <file>', 'write result to FILE instead of stdout')\n .option('--pretty', 'pretty-print JSON output (default on TTY)')\n .option('--no-pretty', 'force single-line JSON output')\n .option('--js', 'execute JS via a headless browser (default: enabled)')\n .option('--no-js', 'disable JS execution (faster, cheaper)')\n .addOption(\n new Option('--proxy <type>', 'proxy pool').choices(['datacenter', 'residential', 'stealth']),\n )\n .option('--country <code>', 'two-letter proxy country code, e.g. us, gb, de')\n .addOption(\n new Option('--device <type>', 'device emulation').choices(['desktop', 'mobile', 'tablet']),\n )\n .option('--timeout <ms>', 'page-retrieval timeout in ms (max 30000)', integerParser)\n .option('--js-timeout <ms>', 'JS rendering time in ms after load (max 20000)', integerParser)\n .option('--wait-for <selector>', 'CSS selector to wait for before returning content')\n .option(\n '--headers <json>',\n 'HTTP headers to forward to the target page; JSON object or @file.json',\n )\n .option('--js-script <code>', 'custom JS to evaluate on the target page')\n .option('--custom-proxy <url>', 'custom proxy URL: http://user:pass@host:port')\n .option('--error-on-404', 'return error on 404')\n .option('--error-on-redirect', 'return error on redirect');\n}\n\n/**\n * Strict positive-integer check for flags like `serp --page`: digits only,\n * no leading zero, so `1.5`, `2abc`, `0` and `-1` are all rejected (unlike\n * `parseInt`, which would read `2abc` as 2). Values beyond\n * `Number.MAX_SAFE_INTEGER` are rejected too.\n */\nexport function isPositiveIntegerString(value: string): boolean {\n // isSafeInteger: a huge digit string becomes e.g. 1e+21 on the wire, which\n // the server rejects with a 400 (not billed); checking here saves the round trip.\n return /^[1-9]\\d*$/.test(value) && Number.isSafeInteger(Number(value));\n}\n\nexport function integerParser(value: string): number {\n const n = Number.parseInt(value, 10);\n if (!Number.isFinite(n)) throw new Error(`expected integer, got: ${value}`);\n return n;\n}\n\n/**\n * Parse a `--headers` argument. Accepts a JSON object literal or `@path` to\n * load JSON from a file. Returns `undefined` if the input is empty.\n */\nexport async function parseHeaders(\n input: string | undefined,\n): Promise<Record<string, string> | undefined> {\n if (!input || input.trim() === '') return undefined;\n const text = input.startsWith('@') ? await fs.readFile(input.slice(1), 'utf8') : input;\n return parseJsonObject(text, 'headers');\n}\n\n/**\n * Parse a `--fields` argument for `extract`. Accepts JSON object literal or\n * `@path` to a file.\n */\nexport async function parseFields(input: string): Promise<Record<string, string>> {\n const text = input.startsWith('@') ? await fs.readFile(input.slice(1), 'utf8') : input;\n const parsed = parseJsonObject(text, 'fields');\n if (!parsed || Object.keys(parsed).length === 0) {\n throw new Error('fields must be a non-empty JSON object of { fieldName: description }');\n }\n return parsed;\n}\n\nfunction parseJsonObject(text: string, label: string): Record<string, string> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n throw new Error(`${label}: invalid JSON — ${(err as Error).message}`);\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(`${label} must be a JSON object`);\n }\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {\n if (typeof v !== 'string') throw new Error(`${label}.${k}: value must be a string`);\n out[k] = v;\n }\n return out;\n}\n","/**\n * Stdin batching helpers.\n *\n * When a URL argument is `-`, the command reads one URL per line from stdin\n * and runs the operation for each. Empty lines and lines starting with `#`\n * are skipped so users can comment URL lists.\n *\n * `serp` reads queries instead (`queryIterator`): only blank lines are\n * skipped there, because `#hashtag` is a legitimate search query.\n */\n\nimport readline from 'node:readline';\n\nexport function urlIsStdin(url: string): boolean {\n return url === '-';\n}\n\nexport async function* readUrlsFromStdin(): AsyncIterable<string> {\n const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });\n for await (const raw of rl) {\n const line = raw.trim();\n if (line === '' || line.startsWith('#')) continue;\n yield line;\n }\n}\n\n/**\n * Read one search query per line. Blank / whitespace-only lines are skipped;\n * every other line is yielded verbatim (no `#` comment handling, no trim).\n */\nexport async function* readQueriesFromStdin(\n input: NodeJS.ReadableStream = process.stdin,\n): AsyncIterable<string> {\n const rl = readline.createInterface({ input, crlfDelay: Infinity });\n for await (const line of rl) {\n if (line.trim() === '') continue;\n yield line;\n }\n}\n\n/**\n * Iterate URLs from either a single CLI arg or from stdin (if the arg is `-`).\n */\nexport async function* urlIterator(arg: string): AsyncIterable<string> {\n if (urlIsStdin(arg)) {\n yield* readUrlsFromStdin();\n return;\n }\n yield arg;\n}\n\n/**\n * Iterate `serp` queries from a single CLI arg or from stdin (if it is `-`).\n */\nexport async function* queryIterator(arg: string): AsyncIterable<string> {\n if (urlIsStdin(arg)) {\n yield* readQueriesFromStdin();\n return;\n }\n yield arg;\n}\n","import { Command } from 'commander';\n\nimport {\n clearPersistedConfig,\n configPath,\n maskApiKey,\n readPersistedConfig,\n writePersistedConfig,\n} from '../lib/config.js';\n\nexport function authCommand(): Command {\n const cmd = new Command('auth').description('Manage your stored API key');\n\n cmd\n .command('set <key>')\n .description(`Save an API key to ${configPath()}`)\n .action(async (key: string) => {\n if (!key || key.trim() === '') throw new Error('API key is empty');\n await writePersistedConfig({ apiKey: key.trim() });\n process.stdout.write(`API key saved to ${configPath()}\\n`);\n });\n\n cmd\n .command('show')\n .description('Display the currently stored API key (masked)')\n .action(async () => {\n const persisted = await readPersistedConfig();\n const env = process.env.WEBSCRAPING_AI_API_KEY;\n if (env && env.trim() !== '') {\n process.stdout.write(\n `Active key (env WEBSCRAPING_AI_API_KEY): ${maskApiKey(env.trim())}\\n`,\n );\n }\n if (persisted.apiKey && persisted.apiKey.trim() !== '') {\n process.stdout.write(\n `Stored key (${configPath()}): ${maskApiKey(persisted.apiKey.trim())}\\n`,\n );\n }\n if (!env && !persisted.apiKey) {\n process.stdout.write('No API key configured. Run `webscraping-ai auth set <key>`.\\n');\n }\n });\n\n cmd\n .command('clear')\n .description('Remove the stored API key file')\n .action(async () => {\n const removed = await clearPersistedConfig();\n process.stdout.write(\n removed ? `Removed ${configPath()}\\n` : 'No stored API key to remove.\\n',\n );\n });\n\n return cmd;\n}\n","import { Command } from 'commander';\n\nimport { buildFieldsOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseFields, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { createEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface ExtractFlags extends CommonRawFlags {\n fields?: string;\n}\n\nexport function extractCommand(): Command {\n const cmd = new Command('extract')\n .description('Extract structured fields from a page using AI (wraps /ai/fields)')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .requiredOption(\n '-f, --fields <json>',\n 'fields to extract — JSON object {name: description} or @file.json',\n );\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: ExtractFlags) => {\n if (!opts.fields) throw new Error('--fields is required');\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const fields = await parseFields(opts.fields);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = createEmitter(opts);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildFieldsOptions(target, fields, opts, { headers });\n const result = await client.fields(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from 'commander';\n\nimport { buildHtmlOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { createEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface HtmlFlags extends CommonRawFlags {\n returnScriptResult?: boolean;\n}\n\nexport function htmlCommand(): Command {\n const cmd = new Command('html')\n .description('Fetch the full HTML of a page')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .addOption(\n new Option('--return-script-result', 'return the result of --js-script instead of HTML'),\n )\n .addOption(new Option('--format <fmt>', 'response wrapping').choices(['text', 'json']));\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: HtmlFlags) => {\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = createEmitter(opts);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildHtmlOptions(\n target,\n opts,\n { headers },\n {\n returnScriptResult: opts.returnScriptResult,\n },\n );\n const result = await client.html(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from 'commander';\n\nimport { buildSelectedOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { createEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface SelectedFlags extends CommonRawFlags {\n selector?: string;\n}\n\nexport function selectedCommand(): Command {\n const cmd = new Command('selected')\n .description('Fetch HTML of one CSS-selected page area')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .option(\n '-s, --selector <css>',\n 'CSS selector for the target element(s); omit for whole-page HTML',\n )\n .addOption(new Option('--format <fmt>', 'response wrapping').choices(['text', 'json']));\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: SelectedFlags) => {\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = createEmitter(opts);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildSelectedOptions(target, opts.selector, opts, { headers });\n const result = await client.selected(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n","import { Command } from 'commander';\n\nimport { buildSelectedMultipleOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { createEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface SelectedMultipleFlags extends CommonRawFlags {\n selector?: string[];\n}\n\nexport function selectedMultipleCommand(): Command {\n const cmd = new Command('selected-multiple')\n .description('Fetch HTML of multiple CSS-selected page areas')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .option(\n '-s, --selector <css>',\n 'CSS selector — pass multiple times for multiple selectors',\n collectMulti,\n [] as string[],\n );\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: SelectedMultipleFlags) => {\n // `--selector` is optional: omitting it returns whole-page HTML.\n const selectors = opts.selector ?? [];\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = createEmitter(opts);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildSelectedMultipleOptions(target, selectors, opts, { headers });\n const result = await client.selectedMultiple(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n\nfunction collectMulti(value: string, previous: string[]): string[] {\n return [...previous, value];\n}\n","import { Command, Option } from 'commander';\n\nimport { buildSerpOptions } from '../lib/buildRequest.js';\nimport type { SerpRawFlags } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { EXIT_CODES } from '../lib/errors.js';\nimport { isPositiveIntegerString } from '../lib/options.js';\nimport { createEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { queryIterator, urlIsStdin } from '../lib/stdin.js';\n\ninterface SerpFlags extends Omit<SerpRawFlags, 'page'> {\n /** Raw `--page` string; validated strictly in the action. */\n page?: string;\n apiKey?: string;\n output?: string;\n pretty?: boolean;\n}\n\n/**\n * `serp <query...>` — parsed search engine results (wraps /serp).\n *\n * Query-shaped, not URL-shaped, so none of the common scrape flags\n * (`--js`, `--proxy`, `--country`, …) apply and they're deliberately not\n * registered here. Multiple words are joined with spaces, so quoting the\n * query is optional. A lone `-` reads one query per line from stdin; only\n * blank lines are skipped (a leading `#` is part of the query, not a comment).\n *\n * Usage errors (bad `--page`, empty query) exit with code 2 before stdin is\n * read or the API key is resolved.\n */\nexport function serpCommand(): Command {\n const cmd = new Command('serp')\n .alias('search')\n .description('Search Google and print parsed results as JSON (wraps /serp, 15 credits/search)')\n .argument('<query...>', 'search query (or `-` to read queries from stdin, one per line)')\n .addOption(new Option('--engine <name>', 'search engine (default: google)').choices(['google']))\n .option('--gl <code>', 'two-letter country code for search geolocation (default: us)')\n .option('--hl <code>', 'two-letter language code for the results (default: en)')\n .option(\n '--page <n>',\n 'results page number, integer >= 1, 10 results per page (default: 1; server rejects > 100 with a 400)',\n )\n .option('-k, --api-key <key>', 'API key (overrides $WEBSCRAPING_AI_API_KEY and config file)')\n .option('-o, --output <file>', 'write result to FILE instead of stdout')\n .option('--pretty', 'pretty-print JSON output (default on TTY)')\n .option('--no-pretty', 'force single-line JSON output');\n\n cmd.action(async (words: string[], opts: SerpFlags) => {\n const query = words.join(' ');\n\n let page: number | undefined;\n if (opts.page !== undefined) {\n if (!isPositiveIntegerString(opts.page)) {\n cmd.error(`error: --page must be an integer >= 1, got: ${opts.page}`, {\n exitCode: EXIT_CODES.usage,\n code: 'wsai.invalidPage',\n });\n }\n page = Number(opts.page);\n }\n if (!urlIsStdin(query) && query.trim() === '') {\n cmd.error('error: serp requires a non-empty query', {\n exitCode: EXIT_CODES.usage,\n code: 'wsai.emptyQuery',\n });\n }\n\n const flags: SerpRawFlags = { engine: opts.engine, gl: opts.gl, hl: opts.hl, page };\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const client = createClient({ apiKey });\n const write = createEmitter(opts);\n\n for await (const q of queryIterator(query)) {\n const result = await client.serp(buildSerpOptions(q, flags));\n await write(result);\n }\n });\n\n return cmd;\n}\n","/**\n * Exit-code mapping for SDK errors.\n *\n * The SDK throws typed subclasses of `WebScrapingAIError`. Map each to a\n * stable exit code so shell scripts can branch on the failure mode without\n * parsing stderr.\n */\n\nimport {\n APIConnectionError,\n APIError,\n APITimeoutError,\n AuthenticationError,\n BadRequestError,\n GatewayTimeoutError,\n PaymentRequiredError,\n RateLimitError,\n ServerError,\n WebScrapingAIError,\n} from 'webscraping-ai';\n\nimport { redactSecrets } from './redact.js';\n\nexport const EXIT_CODES = {\n ok: 0,\n generic: 1,\n usage: 2,\n badRequest: 3,\n auth: 4,\n paymentRequired: 5,\n rateLimit: 6,\n serverError: 7,\n timeout: 8,\n connection: 9,\n} as const;\n\nexport function exitCodeFor(err: unknown): number {\n if (err instanceof BadRequestError) return EXIT_CODES.badRequest;\n if (err instanceof AuthenticationError) return EXIT_CODES.auth;\n if (err instanceof PaymentRequiredError) return EXIT_CODES.paymentRequired;\n if (err instanceof RateLimitError) return EXIT_CODES.rateLimit;\n if (err instanceof ServerError) return EXIT_CODES.serverError;\n if (err instanceof GatewayTimeoutError) return EXIT_CODES.timeout;\n if (err instanceof APITimeoutError) return EXIT_CODES.timeout;\n if (err instanceof APIConnectionError) return EXIT_CODES.connection;\n if (err instanceof APIError) return EXIT_CODES.serverError;\n if (err instanceof WebScrapingAIError) return EXIT_CODES.generic;\n return EXIT_CODES.generic;\n}\n\n/**\n * Human-readable one-liner for stderr. Always redacted: the API key and any\n * `api_key=...` pattern are replaced, since error bodies can echo the URL.\n */\nexport function formatError(err: unknown): string {\n return redactSecrets(rawMessage(err));\n}\n\nfunction rawMessage(err: unknown): string {\n if (err instanceof APIError) {\n const parts = [err.message];\n if (err.status) parts.push(`(HTTP ${err.status})`);\n return parts.join(' ');\n }\n if (err instanceof Error) return err.message;\n return String(err);\n}\n","import { Command } from 'commander';\n\nimport { EDITORS, editorById, installSkill, resolveTarget } from '../lib/skill.js';\nimport type { EditorAdapter } from '../lib/skill.js';\n\ninterface SetupSkillFlags {\n editor?: string[];\n all?: boolean;\n force?: boolean;\n}\n\nexport function setupCommand(): Command {\n const cmd = new Command('setup').description('Install the AI agent skill into your editor(s)');\n\n cmd\n .command('skill')\n .description('Install the WebScraping.AI skill into one or more AI coding editors')\n .option(\n '-e, --editor <id>',\n `editor id (repeatable). Supported: ${EDITORS.map((e) => e.id).join(', ')}`,\n collect,\n [] as string[],\n )\n .option('-a, --all', 'install into every supported editor')\n .option('-f, --force', 'overwrite existing skill files')\n .action(async (opts: SetupSkillFlags) => {\n const selected = resolveSelection(opts);\n if (selected.length === 0) {\n throw new Error(\n 'no editor selected — pass --all or --editor <id> (e.g. --editor claude-code)',\n );\n }\n\n for (const editor of selected) {\n const target = resolveTarget(editor);\n const result = await installSkill(target, { force: opts.force });\n process.stdout.write(`${editor.label}: ${result.status} → ${target.destination}\\n`);\n }\n });\n\n cmd\n .command('list', { hidden: false })\n .description('List supported editors for `setup skill`')\n .action(() => {\n for (const e of EDITORS) {\n const target = resolveTarget(e);\n process.stdout.write(`${e.id.padEnd(14)} ${e.label.padEnd(18)} ${target.destination}\\n`);\n }\n });\n\n return cmd;\n}\n\nfunction collect(value: string, previous: string[]): string[] {\n return [...previous, value];\n}\n\nfunction resolveSelection(opts: SetupSkillFlags): EditorAdapter[] {\n if (opts.all) return [...EDITORS];\n const ids = opts.editor ?? [];\n return ids.map((id) => {\n const editor = editorById(id);\n if (!editor) throw new Error(`unknown editor: ${id}`);\n return editor;\n });\n}\n","/**\n * Multi-editor skill installer.\n *\n * The canonical skill lives in this package at\n * `plugins/webscraping-ai/skills/webscraping-ai/SKILL.md`. Each supported\n * editor reads skill content from a different location and may want either\n * a single Markdown file or a whole skill directory. The `editors` registry\n * captures those differences so `setup skill` is a small driver over it.\n *\n * `mode: 'dir'` copies the entire skill directory (Claude Code-shaped\n * editors that scan a folder per skill). `mode: 'file'` writes the SKILL.md\n * contents to a single file (editors that read flat instructions, like\n * GitHub Copilot's `.github/copilot-instructions.md`).\n *\n * `kind: 'user'` writes to `$HOME`; `kind: 'project'` writes to the current\n * working directory. We prefer user-scope where the editor supports it so\n * the skill becomes globally available without polluting individual repos.\n */\n\nimport { promises as fs } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport type EditorMode = 'dir' | 'file';\nexport type EditorKind = 'user' | 'project';\n\nexport interface EditorAdapter {\n /** Stable identifier used by `--editor <name>`. */\n id: string;\n /** Human-readable label printed in summaries. */\n label: string;\n mode: EditorMode;\n kind: EditorKind;\n /**\n * Where the skill ends up. For `mode: 'dir'`, the skill directory copies\n * to `{root}/{path}`. For `mode: 'file'`, the SKILL.md contents write to\n * `{root}/{path}` (i.e. `path` ends in a filename).\n *\n * `root` is `$HOME` (user) or CWD (project).\n */\n path: string;\n}\n\n/**\n * Registry of supported editors. Order matters: it's also the order the\n * `setup skill` summary lists them in.\n */\nexport const EDITORS: readonly EditorAdapter[] = [\n // Claude Code: user-scope skills live under ~/.claude/skills/<name>/\n {\n id: 'claude-code',\n label: 'Claude Code',\n mode: 'dir',\n kind: 'user',\n path: '.claude/skills/webscraping-ai',\n },\n // Cursor: project-scope rule file. Cursor doesn't have a user-wide skill\n // dir, so this is the editor's \"install\" surface.\n {\n id: 'cursor',\n label: 'Cursor',\n mode: 'file',\n kind: 'project',\n path: '.cursor/rules/webscraping-ai.mdc',\n },\n // Windsurf: project-scope rules. Same shape as Cursor.\n {\n id: 'windsurf',\n label: 'Windsurf',\n mode: 'file',\n kind: 'project',\n path: '.windsurf/rules/webscraping-ai.md',\n },\n // Kiro: user-scope steering directory.\n { id: 'kiro', label: 'Kiro', mode: 'dir', kind: 'user', path: '.kiro/steering/webscraping-ai' },\n // OpenCode: project-scope skills (mirrors Claude shape).\n {\n id: 'opencode',\n label: 'OpenCode',\n mode: 'dir',\n kind: 'project',\n path: '.opencode/skills/webscraping-ai',\n },\n // Gemini CLI: user-scope extension dir with a GEMINI.md.\n {\n id: 'gemini',\n label: 'Gemini CLI',\n mode: 'file',\n kind: 'user',\n path: '.gemini/extensions/webscraping-ai/GEMINI.md',\n },\n // GitHub Copilot: project-scope custom instructions file. Single file.\n {\n id: 'copilot',\n label: 'GitHub Copilot',\n mode: 'file',\n kind: 'project',\n path: '.github/copilot-instructions.md',\n },\n // Augment: project-scope rules file.\n {\n id: 'augment',\n label: 'Augment',\n mode: 'file',\n kind: 'project',\n path: '.augment/rules/webscraping-ai.md',\n },\n // Factory: project-scope rules file.\n {\n id: 'factory',\n label: 'Factory',\n mode: 'file',\n kind: 'project',\n path: '.factory/rules/webscraping-ai.md',\n },\n] as const;\n\nexport function editorById(id: string): EditorAdapter | undefined {\n return EDITORS.find((e) => e.id === id);\n}\n\n/**\n * Where this package keeps the canonical skill. Resolved relative to the\n * compiled `dist/cli.js` so it works after `npm install -g`.\n */\nexport function bundledSkillDir(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n // dist/ sits at the package root, so the plugins dir is two levels up\n // from any compiled file: dist/cli.js → package root → plugins/...\n return resolve(here, '..', 'plugins', 'webscraping-ai', 'skills', 'webscraping-ai');\n}\n\nexport interface InstallTarget {\n editor: EditorAdapter;\n /** Absolute destination path (file or directory depending on `mode`). */\n destination: string;\n}\n\nexport interface ResolveOptions {\n cwd?: string;\n home?: string;\n}\n\nexport function resolveTarget(editor: EditorAdapter, opts: ResolveOptions = {}): InstallTarget {\n const root = editor.kind === 'user' ? (opts.home ?? homedir()) : (opts.cwd ?? process.cwd());\n return { editor, destination: join(root, editor.path) };\n}\n\nexport interface InstallOptions {\n /** If true, overwrite existing files/dirs. */\n force?: boolean;\n /** Override skill source dir (for tests). */\n sourceDir?: string;\n}\n\nexport type InstallStatus = 'installed' | 'skipped-exists' | 'overwritten';\n\nexport interface InstallResult {\n target: InstallTarget;\n status: InstallStatus;\n}\n\nexport async function installSkill(\n target: InstallTarget,\n opts: InstallOptions = {},\n): Promise<InstallResult> {\n const source = opts.sourceDir ?? bundledSkillDir();\n const exists = await pathExists(target.destination);\n\n if (exists && !opts.force) {\n return { target, status: 'skipped-exists' };\n }\n\n if (target.editor.mode === 'dir') {\n if (exists) await fs.rm(target.destination, { recursive: true, force: true });\n await copyDirectory(source, target.destination);\n } else {\n const skillBody = await fs.readFile(join(source, 'SKILL.md'), 'utf8');\n await fs.mkdir(dirname(target.destination), { recursive: true });\n await fs.writeFile(target.destination, skillBody);\n }\n\n return { target, status: exists ? 'overwritten' : 'installed' };\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n try {\n await fs.access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function copyDirectory(source: string, destination: string): Promise<void> {\n // `fs.cp` with `recursive` exists on Node 16.7+; we target Node 20.\n await fs.cp(source, destination, { recursive: true, force: true });\n}\n","import { Command, Option } from 'commander';\n\nimport { buildTextOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { createEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface TextFlags extends CommonRawFlags {\n textFormat?: 'plain' | 'json' | 'xml';\n returnLinks?: boolean;\n}\n\nexport function textCommand(): Command {\n const cmd = new Command('text')\n .description('Fetch the visible text of a page')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .addOption(\n new Option('--text-format <fmt>', 'text response format').choices(['plain', 'json', 'xml']),\n )\n .option('--return-links', 'include page links (text-format=json only)');\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: TextFlags) => {\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = createEmitter(opts);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildTextOptions(\n target,\n opts,\n { headers },\n {\n textFormat: opts.textFormat,\n returnLinks: opts.returnLinks,\n },\n );\n const result = await client.text(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n"],"mappings":"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","names":["Command","dirname","resolve","fileURLToPath","fs","Command","Option","fs","Command","Option","Command","Command","Command","Command","Command","Option","Command","Option","Command","Option","Command","Option","Command","Command","Command","Option","Command","Option","Command","fs","homedir","dirname","join","Command","Command","Option","Command","Option","dirname","fileURLToPath","resolve","Command"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/commands/account.ts","../src/lib/config.ts","../src/lib/output.ts","../src/lib/stdin.ts","../src/lib/sdk.ts","../src/lib/redact.ts","../src/commands/ask.ts","../src/lib/buildRequest.ts","../src/lib/options.ts","../src/commands/auth.ts","../src/commands/data.ts","../src/lib/errors.ts","../src/commands/extract.ts","../src/commands/html.ts","../src/commands/selected.ts","../src/commands/selectedMultiple.ts","../src/commands/serp.ts","../src/commands/setupSkill.ts","../src/lib/skill.ts","../src/commands/text.ts"],"sourcesContent":["/**\n * `webscraping-ai` / `wsai` CLI entry point.\n *\n * Wires every subcommand into a single commander root. Errors thrown from\n * subcommand actions land in the top-level `.catch`, which maps SDK\n * exception classes to stable exit codes (see `lib/errors.ts`).\n */\n\nimport { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { accountCommand } from './commands/account.js';\nimport { askCommand } from './commands/ask.js';\nimport { authCommand } from './commands/auth.js';\nimport { dataCommand } from './commands/data.js';\nimport { extractCommand } from './commands/extract.js';\nimport { htmlCommand } from './commands/html.js';\nimport { selectedCommand } from './commands/selected.js';\nimport { selectedMultipleCommand } from './commands/selectedMultiple.js';\nimport { serpCommand } from './commands/serp.js';\nimport { setupCommand } from './commands/setupSkill.js';\nimport { textCommand } from './commands/text.js';\nimport { exitCodeFor, formatError } from './lib/errors.js';\n\nfunction packageVersion(): string {\n // Read package.json at runtime so we don't need a code-gen step. The dist\n // bundle sits at `dist/cli.js`, so package.json is one directory up.\n const here = dirname(fileURLToPath(import.meta.url));\n const pkgPath = resolve(here, '..', 'package.json');\n const raw = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string };\n return raw.version;\n}\n\nexport function buildProgram(): Command {\n const program = new Command();\n program\n .name('webscraping-ai')\n .description(\n 'Official CLI for the WebScraping.AI API. Fetch pages, search Google, get structured site data, ask AI, extract fields.\\n' +\n 'Set $WEBSCRAPING_AI_API_KEY or run `webscraping-ai auth set <key>` first.',\n )\n .version(packageVersion(), '-v, --version', 'output the version number')\n .showHelpAfterError();\n\n program.addCommand(htmlCommand());\n program.addCommand(textCommand());\n program.addCommand(selectedCommand());\n program.addCommand(selectedMultipleCommand());\n program.addCommand(askCommand());\n program.addCommand(extractCommand());\n program.addCommand(serpCommand());\n program.addCommand(dataCommand());\n program.addCommand(accountCommand());\n program.addCommand(authCommand());\n program.addCommand(setupCommand());\n\n return program;\n}\n\nasync function main(argv: string[]): Promise<void> {\n const program = buildProgram();\n try {\n await program.parseAsync(argv);\n } catch (err) {\n process.stderr.write(`Error: ${formatError(err)}\\n`);\n process.exit(exitCodeFor(err));\n }\n}\n\n// Only execute when run as a script. When imported (e.g. from tests),\n// `buildProgram` is the public surface and `main` stays dormant.\nconst entryPath = process.argv[1] ? fileURLToPath(import.meta.url) : '';\nif (entryPath && process.argv[1] === entryPath) {\n main(process.argv).catch((err: unknown) => {\n process.stderr.write(`Error: ${formatError(err)}\\n`);\n process.exit(exitCodeFor(err));\n });\n}\n","import { Command } from 'commander';\n\nimport { resolveApiKey } from '../lib/config.js';\nimport { emit } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\n\ninterface AccountFlags {\n apiKey?: string;\n output?: string;\n pretty?: boolean;\n}\n\nexport function accountCommand(): Command {\n return new Command('account')\n .description('Show account quota and usage information')\n .option('-k, --api-key <key>', 'API key (overrides $WEBSCRAPING_AI_API_KEY and config file)')\n .option('-o, --output <file>', 'write result to FILE instead of stdout')\n .option('--pretty', 'pretty-print JSON output (default on TTY)')\n .option('--no-pretty', 'force single-line JSON output')\n .action(async (opts: AccountFlags) => {\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const client = createClient({ apiKey });\n const result = await client.account();\n await emit(result, opts);\n });\n}\n","/**\n * API key resolution and persisted config.\n *\n * Resolution order (first non-empty wins):\n * 1. `--api-key` flag (handled by the caller, passed in)\n * 2. `WEBSCRAPING_AI_API_KEY` env var\n * 3. `~/.config/webscraping-ai/config.json` (or `$XDG_CONFIG_HOME/...`)\n *\n * The config file is plain JSON with `{ \"apiKey\": \"...\" }`. We chmod it to\n * 0600 so it's not world-readable.\n */\n\nimport { promises as fs } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\nconst CONFIG_FILENAME = 'config.json';\n\nexport function configDir(): string {\n const xdg = process.env.XDG_CONFIG_HOME;\n if (xdg && xdg.trim() !== '') return join(xdg, 'webscraping-ai');\n return join(homedir(), '.config', 'webscraping-ai');\n}\n\nexport function configPath(): string {\n return join(configDir(), CONFIG_FILENAME);\n}\n\nexport interface PersistedConfig {\n apiKey?: string;\n}\n\nexport async function readPersistedConfig(): Promise<PersistedConfig> {\n try {\n const text = await fs.readFile(configPath(), 'utf8');\n const parsed = JSON.parse(text) as unknown;\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as PersistedConfig;\n }\n return {};\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw err;\n }\n}\n\nexport async function writePersistedConfig(config: PersistedConfig): Promise<void> {\n const path = configPath();\n await fs.mkdir(dirname(path), { recursive: true });\n await fs.writeFile(path, JSON.stringify(config, null, 2) + '\\n', { mode: 0o600 });\n // `mode` above only applies when the file is freshly created; an existing\n // file keeps its old (possibly world-readable) perms. Enforce 0600\n // unconditionally — chmod on a just-written file is cheap and idempotent.\n // No-op on Windows, but harmless.\n await fs.chmod(path, 0o600);\n}\n\nexport async function clearPersistedConfig(): Promise<boolean> {\n try {\n await fs.unlink(configPath());\n return true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return false;\n throw err;\n }\n}\n\nexport interface ResolveApiKeyOptions {\n /** `--api-key` flag value, if the user passed one. */\n flag?: string;\n}\n\nexport async function resolveApiKey(opts: ResolveApiKeyOptions = {}): Promise<string> {\n if (opts.flag && opts.flag.trim() !== '') return opts.flag.trim();\n\n const env = process.env.WEBSCRAPING_AI_API_KEY;\n if (env && env.trim() !== '') return env.trim();\n\n const persisted = await readPersistedConfig();\n if (persisted.apiKey && persisted.apiKey.trim() !== '') return persisted.apiKey.trim();\n\n throw new Error(\n 'No API key found. Set $WEBSCRAPING_AI_API_KEY, pass --api-key, or run `webscraping-ai auth set <key>`.',\n );\n}\n\nexport function maskApiKey(key: string): string {\n if (key.length <= 8) return '*'.repeat(key.length);\n return `${key.slice(0, 4)}${'*'.repeat(key.length - 8)}${key.slice(-4)}`;\n}\n","/**\n * Output handling for command results.\n *\n * All command handlers funnel through `emit()`. The result type drives format:\n * - strings → written verbatim\n * - objects/arrays → JSON.stringified (pretty by default; `--pretty` is the\n * default for terminals, single-line for pipes/redirects)\n *\n * `--output FILE` writes to disk instead of stdout. Commands use\n * `openEmitter()`: in stdin batch mode (`-`) it truncates the file up front\n * (so a first-item failure or empty stdin never leaves stale content) and\n * appends every result, so each item survives instead of overwriting the last.\n */\n\nimport { promises as fs } from 'node:fs';\n\nimport { urlIsStdin } from './stdin.js';\n\nexport interface EmitOptions {\n output?: string;\n pretty?: boolean;\n}\n\nexport interface EmitWriteOptions {\n /** Append to `--output` instead of truncating it (batch items after the first). */\n append?: boolean;\n}\n\nexport async function emit(\n value: unknown,\n opts: EmitOptions = {},\n write: EmitWriteOptions = {},\n): Promise<void> {\n const text = stringify(value, opts);\n\n if (opts.output && opts.output.trim() !== '') {\n const data = text + (text.endsWith('\\n') ? '' : '\\n');\n if (write.append) await fs.appendFile(opts.output, data);\n else await fs.writeFile(opts.output, data);\n return;\n }\n\n process.stdout.write(text);\n if (!text.endsWith('\\n')) process.stdout.write('\\n');\n}\n\n/**\n * Returns an `emit` bound to `opts` for a (possibly batched) loop: the first\n * call truncates `--output`, later calls append.\n */\nexport function createEmitter(opts: EmitOptions = {}): (value: unknown) => Promise<void> {\n let first = true;\n return async (value: unknown) => {\n const append = !first;\n first = false;\n await emit(value, opts, { append });\n };\n}\n\nexport function stringify(value: unknown, opts: EmitOptions = {}): string {\n if (typeof value === 'string') return value;\n\n const pretty = opts.pretty ?? process.stdout.isTTY ?? false;\n return JSON.stringify(value, null, pretty ? 2 : 0);\n}\n\n/**\n * Emitter for a command whose positional arg is `arg`. When `arg` is `-`\n * (stdin batch) and `--output` is set, the file is truncated now, before any\n * request, and every result is appended. Otherwise same as `createEmitter()`.\n */\nexport async function openEmitter(\n opts: EmitOptions,\n arg: string,\n): Promise<(value: unknown) => Promise<void>> {\n if (urlIsStdin(arg) && opts.output && opts.output.trim() !== '') {\n await fs.writeFile(opts.output, '');\n return (value: unknown) => emit(value, opts, { append: true });\n }\n return createEmitter(opts);\n}\n","/**\n * Stdin batching helpers.\n *\n * When a URL argument is `-`, the command reads one URL per line from stdin\n * and runs the operation for each. Empty lines and lines starting with `#`\n * are skipped so users can comment URL lists.\n *\n * `serp` reads queries instead (`queryIterator`): only blank lines are\n * skipped there, because `#hashtag` is a legitimate search query.\n */\n\nimport readline from 'node:readline';\n\nexport function urlIsStdin(url: string): boolean {\n return url === '-';\n}\n\nexport async function* readUrlsFromStdin(): AsyncIterable<string> {\n const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });\n for await (const raw of rl) {\n const line = raw.trim();\n if (line === '' || line.startsWith('#')) continue;\n yield line;\n }\n}\n\n/**\n * Read one search query per line. Blank / whitespace-only lines are skipped;\n * every other line is yielded verbatim (no `#` comment handling, no trim).\n */\nexport async function* readQueriesFromStdin(\n input: NodeJS.ReadableStream = process.stdin,\n): AsyncIterable<string> {\n const rl = readline.createInterface({ input, crlfDelay: Infinity });\n for await (const line of rl) {\n if (line.trim() === '') continue;\n yield line;\n }\n}\n\n/**\n * Iterate URLs from either a single CLI arg or from stdin (if the arg is `-`).\n */\nexport async function* urlIterator(arg: string): AsyncIterable<string> {\n if (urlIsStdin(arg)) {\n yield* readUrlsFromStdin();\n return;\n }\n yield arg;\n}\n\n/**\n * Iterate `serp` queries from a single CLI arg or from stdin (if it is `-`).\n */\nexport async function* queryIterator(arg: string): AsyncIterable<string> {\n if (urlIsStdin(arg)) {\n yield* readQueriesFromStdin();\n return;\n }\n yield arg;\n}\n","/**\n * SDK adapter.\n *\n * Wraps the published `webscraping-ai` Node SDK. The CLI sends a\n * `from_cli=true` analytics flag on every endpoint except `account` (mirrors\n * `from_n8n` and `from_mcp_server` used by the n8n node and MCP server). On\n * `serp` and `data` it rides in the SDK's `params` pass-through, since those\n * methods drop unknown top-level keys. `account()` takes no options, so it\n * isn't tagged. The SDK's typed\n * options don't accept arbitrary keys, so we go through a single cast point\n * here rather than scattering casts across each command.\n */\n\nimport { WebScrapingAI } from 'webscraping-ai';\n\nimport { registerSecret } from './redact.js';\nimport type {\n DataOptions,\n FieldsOptions,\n HtmlOptions,\n QuestionOptions,\n SelectedMultipleOptions,\n SelectedOptions,\n SerpOptions,\n TextOptions,\n} from 'webscraping-ai';\n\nexport interface CreateClientOptions {\n apiKey: string;\n /** Per-request timeout in ms. Maps to the SDK's `timeoutMs`. */\n requestTimeoutMs?: number;\n}\n\n/**\n * Returns a `WebScrapingAI` instance with the CLI's analytics tag baked into\n * every endpoint method. The methods preserve the SDK's public signatures.\n */\nexport function createClient(options: CreateClientOptions): WebScrapingAI {\n registerSecret(options.apiKey);\n const inner = new WebScrapingAI({\n apiKey: options.apiKey,\n timeoutMs: options.requestTimeoutMs,\n });\n\n // Wrap each endpoint method so it injects the CLI analytics tag. We splice\n // the tag in at this seam rather than at every callsite. The cast is local;\n // the SDK passes unknown query params through to the wire.\n const tag = { from_cli: true } as unknown as Record<string, never>;\n\n return new Proxy(inner, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n if (typeof value !== 'function') return value;\n switch (prop) {\n case 'html':\n return (o: HtmlOptions) => target.html({ ...o, ...tag });\n case 'text':\n return (o: TextOptions) => target.text({ ...o, ...tag });\n case 'selected':\n return (o: SelectedOptions) => target.selected({ ...o, ...tag });\n case 'selectedMultiple':\n return (o: SelectedMultipleOptions) => target.selectedMultiple({ ...o, ...tag });\n case 'question':\n return (o: QuestionOptions) => target.question({ ...o, ...tag });\n case 'fields':\n return (o: FieldsOptions) => target.fields({ ...o, ...tag });\n case 'serp':\n // `serp()` drops unknown top-level keys but sends its `params`\n // pass-through as-is, so the tag goes there (CLI tag wins).\n return (o: SerpOptions) => target.serp({ ...o, params: { ...o.params, from_cli: true } });\n case 'data':\n // `data()` drops unknown top-level keys but sends its `params`\n // pass-through as-is, so the tag goes there (CLI tag wins).\n return (o: DataOptions) => target.data({ ...o, params: { ...o.params, from_cli: true } });\n case 'account':\n // `account` takes no options; the SDK builds the request itself, so\n // there's no seam here for the analytics tag — accepted limitation.\n return value.bind(target);\n default:\n return typeof value === 'function' ? value.bind(target) : value;\n }\n },\n });\n}\n","/**\n * Secret redaction for anything the CLI prints to stderr.\n *\n * API errors can echo the request URL (e.g. an HTML error page that includes\n * `?api_key=...`), so every error message goes through `redactSecrets()`\n * before it is printed. The key in use is registered by `createClient()`.\n */\n\nconst secrets = new Set<string>();\n\n/** Remember a secret so later error output never prints it. */\nexport function registerSecret(value: string | undefined): void {\n if (value && value.trim() !== '') secrets.add(value);\n}\n\n/** Forget registered secrets (tests only). */\nexport function clearSecrets(): void {\n secrets.clear();\n}\n\nconst API_KEY_PARAM = /(api_key=)[^&\\s\"'<>]*/gi;\n\nexport function redactSecrets(text: string): string {\n let out = text.replace(API_KEY_PARAM, '$1[REDACTED]');\n const candidates = new Set(secrets);\n const env = process.env.WEBSCRAPING_AI_API_KEY?.trim();\n if (env) candidates.add(env);\n for (const secret of candidates) {\n for (const form of new Set([secret, encodeURIComponent(secret)])) {\n out = out.split(form).join('[REDACTED]');\n }\n }\n return out;\n}\n","import { Command, Option } from 'commander';\n\nimport { buildQuestionOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { openEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface AskFlags extends CommonRawFlags {\n question?: string;\n}\n\nexport function askCommand(): Command {\n const cmd = new Command('ask')\n .description('Ask an AI question about a page (wraps /ai/question)')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .requiredOption('-q, --question <text>', 'the question / instructions for the model')\n .addOption(new Option('--format <fmt>', 'response wrapping').choices(['text', 'json']));\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: AskFlags) => {\n if (!opts.question) throw new Error('--question is required');\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = await openEmitter(opts, url);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildQuestionOptions(target, opts.question, opts, { headers });\n const result = await client.question(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n","/**\n * Pure builder that maps `--flag` values into SDK option objects.\n *\n * Extracted as a separate function so unit tests can pin the mapping without\n * spinning up commander or making HTTP calls. Each command handler reads its\n * own commander options, awaits `parseHeaders` / `parseFields`, then calls\n * the matching `build*` function here.\n */\n\nimport type {\n CommonRequestOptions,\n DataOptions,\n FieldsOptions,\n HtmlOptions,\n QuestionOptions,\n SelectedMultipleOptions,\n SelectedOptions,\n SerpOptions,\n TextOptions,\n} from 'webscraping-ai';\n\nimport type { CommonRawFlags } from './options.js';\n\nexport interface ResolvedCommonOptions {\n headers?: Record<string, string>;\n}\n\nexport function buildCommonOptions(\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions = {},\n): CommonRequestOptions {\n const out: CommonRequestOptions = {};\n // `flags.js` is `false` when `--no-js`, `true` when `--js`, `undefined`\n // when neither passed. Pass through only when explicit, so the API default\n // applies otherwise.\n if (flags.js !== undefined) out.js = flags.js;\n if (flags.proxy !== undefined) out.proxy = flags.proxy;\n if (flags.country !== undefined) out.country = flags.country;\n if (flags.device !== undefined) out.device = flags.device;\n if (flags.timeout !== undefined) out.timeout = flags.timeout;\n if (flags.jsTimeout !== undefined) out.js_timeout = flags.jsTimeout;\n if (flags.waitFor !== undefined) out.wait_for = flags.waitFor;\n if (flags.jsScript !== undefined) out.js_script = flags.jsScript;\n if (flags.customProxy !== undefined) out.custom_proxy = flags.customProxy;\n if (flags.errorOn404) out.error_on_404 = true;\n if (flags.errorOnRedirect) out.error_on_redirect = true;\n if (resolved.headers) out.headers = resolved.headers;\n return out;\n}\n\nexport interface HtmlExtras {\n returnScriptResult?: boolean;\n}\n\nexport function buildHtmlOptions(\n url: string,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n extras: HtmlExtras = {},\n): HtmlOptions {\n const out: HtmlOptions = { url, ...buildCommonOptions(flags, resolved) };\n if (flags.format) out.format = flags.format;\n if (extras.returnScriptResult) out.return_script_result = true;\n return out;\n}\n\nexport interface TextExtras {\n textFormat?: 'plain' | 'json' | 'xml';\n returnLinks?: boolean;\n}\n\nexport function buildTextOptions(\n url: string,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n extras: TextExtras = {},\n): TextOptions {\n const out: TextOptions = { url, ...buildCommonOptions(flags, resolved) };\n if (extras.textFormat) out.text_format = extras.textFormat;\n if (extras.returnLinks) out.return_links = true;\n return out;\n}\n\nexport function buildSelectedOptions(\n url: string,\n selector: string | undefined,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n): SelectedOptions {\n // `selector` is optional per the OpenAPI spec: omitting it returns the\n // whole-page HTML. Only attach it when provided. (The installed SDK types\n // still mark it required, so build the object as a partial and cast.)\n const out: Partial<SelectedOptions> = { url, ...buildCommonOptions(flags, resolved) };\n if (selector !== undefined) out.selector = selector;\n if (flags.format) out.format = flags.format;\n return out as SelectedOptions;\n}\n\nexport function buildSelectedMultipleOptions(\n url: string,\n selectors: readonly string[],\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n): SelectedMultipleOptions {\n // `selectors` is optional per the OpenAPI spec: omitting it returns the\n // whole-page HTML. Only attach it when non-empty. (The installed SDK types\n // still mark it required, so build the object as a partial and cast.)\n const out: Partial<SelectedMultipleOptions> = { url, ...buildCommonOptions(flags, resolved) };\n if (selectors.length > 0) out.selectors = selectors;\n return out as SelectedMultipleOptions;\n}\n\nexport function buildQuestionOptions(\n url: string,\n question: string,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n): QuestionOptions {\n const out: QuestionOptions = { url, question, ...buildCommonOptions(flags, resolved) };\n if (flags.format) out.format = flags.format;\n return out;\n}\n\nexport function buildFieldsOptions(\n url: string,\n fields: Record<string, string>,\n flags: CommonRawFlags,\n resolved: ResolvedCommonOptions,\n): FieldsOptions {\n return { url, fields, ...buildCommonOptions(flags, resolved) };\n}\n\nexport interface SerpRawFlags {\n engine?: 'google';\n gl?: string;\n hl?: string;\n page?: number;\n}\n\n/**\n * `/serp` is query-shaped: it takes none of the common scrape options, so\n * this deliberately does not go through `buildCommonOptions`. `page` must be\n * an integer >= 1 (the server also rejects anything else with a 400, not\n * billed; checking client-side saves the round trip). Pages are 1-100: the\n * server rejects a page above 100 with a 400.\n */\nexport function buildSerpOptions(query: string, flags: SerpRawFlags = {}): SerpOptions {\n // Validate on the trimmed value but send the query as the user typed it.\n if (typeof query !== 'string' || query.trim() === '') {\n throw new Error('serp requires a non-empty query');\n }\n const out: SerpOptions = { q: query };\n if (flags.engine !== undefined) out.engine = flags.engine;\n if (flags.gl) out.gl = flags.gl;\n if (flags.hl) out.hl = flags.hl;\n if (flags.page !== undefined) {\n if (!Number.isSafeInteger(flags.page) || flags.page < 1) {\n throw new Error(`--page must be an integer >= 1, got: ${flags.page}`);\n }\n out.page = flags.page;\n }\n return out;\n}\n\nexport interface DataRawFlags {\n country?: string;\n transcript?: boolean;\n transcriptLanguage?: string;\n /** Raw repeatable `--param key=value` strings. */\n param?: readonly string[];\n}\n\n/** Keys `--param` must never set: owned by the SDK/CLI. */\nconst RESERVED_DATA_PARAMS = new Set(['api_key', 'url', 'from_cli', '__proto__']);\n\n/** `--param` keys that have their own flag: reject and point at the flag. */\nconst NAMED_DATA_PARAMS: Record<string, string> = {\n country: '--country',\n transcript: '--transcript',\n transcript_language: '--transcript-language',\n};\n\n/**\n * Parse repeatable `--param key=value` flags into a plain object. Splits on\n * the first `=`, so values may contain `=`. Throws on a missing `=`, an empty\n * or repeated key, a reserved key (`api_key`, `url`, `from_cli`,\n * `__proto__`), or a key that has its own flag (`country` → use `--country`).\n */\nexport function parseDataParams(raw: readonly string[] = []): Record<string, string> {\n const out: Record<string, string> = Object.create(null) as Record<string, string>;\n for (const entry of raw) {\n const eq = entry.indexOf('=');\n if (eq <= 0) throw new Error(`--param must be key=value, got: ${entry}`);\n const key = entry.slice(0, eq);\n if (RESERVED_DATA_PARAMS.has(key)) {\n throw new Error(`--param must not set \"${key}\"`);\n }\n if (Object.hasOwn(NAMED_DATA_PARAMS, key)) {\n throw new Error(`--param must not set \"${key}\"; use ${NAMED_DATA_PARAMS[key]}`);\n }\n if (Object.hasOwn(out, key)) {\n throw new Error(`--param \"${key}\" given more than once`);\n }\n out[key] = entry.slice(eq + 1);\n }\n return { ...out };\n}\n\n/**\n * `/data` is URL-shaped but takes none of the common scrape options, so this\n * deliberately does not go through `buildCommonOptions`. The URL is passed\n * through unmodified: there is no client-side site check (sites are added\n * server-side; the API answers an unsupported URL with a free 400).\n */\nexport function buildDataOptions(url: string, flags: DataRawFlags = {}): DataOptions {\n if (typeof url !== 'string' || url.trim() === '') {\n throw new Error('data requires a non-empty url');\n }\n const out: DataOptions = { url };\n if (flags.country) out.country = flags.country;\n if (flags.transcript !== undefined) out.transcript = flags.transcript;\n if (flags.transcriptLanguage) out.transcript_language = flags.transcriptLanguage;\n const params = parseDataParams(flags.param);\n if (Object.keys(params).length > 0) out.params = params;\n return out;\n}\n","/**\n * Shared commander options and the parsers/expanders behind them.\n *\n * All scrape endpoints share the same nine \"common\" flags. We define them in\n * one place and apply via `addCommonScrapeOptions(cmd)` so the surface stays\n * consistent and any change lands everywhere at once.\n */\n\nimport { promises as fs } from 'node:fs';\nimport { Option } from 'commander';\nimport type { Command } from 'commander';\n\nexport interface CommonRawFlags {\n apiKey?: string;\n output?: string;\n format?: 'text' | 'json';\n pretty?: boolean;\n js?: boolean;\n proxy?: 'datacenter' | 'residential' | 'stealth';\n country?: string;\n device?: 'desktop' | 'mobile' | 'tablet';\n timeout?: number;\n jsTimeout?: number;\n waitFor?: string;\n headers?: string;\n jsScript?: string;\n customProxy?: string;\n errorOn404?: boolean;\n errorOnRedirect?: boolean;\n}\n\n/**\n * Add the flags every scrape-endpoint command needs. Adding here keeps the\n * surface aligned across `html`, `text`, `selected`, etc.\n */\nexport function addCommonScrapeOptions(cmd: Command): Command {\n return cmd\n .option('-k, --api-key <key>', 'API key (overrides $WEBSCRAPING_AI_API_KEY and config file)')\n .option('-o, --output <file>', 'write result to FILE instead of stdout')\n .option('--pretty', 'pretty-print JSON output (default on TTY)')\n .option('--no-pretty', 'force single-line JSON output')\n .option('--js', 'execute JS via a headless browser (default: enabled)')\n .option('--no-js', 'disable JS execution (faster, cheaper)')\n .addOption(\n new Option('--proxy <type>', 'proxy pool').choices(['datacenter', 'residential', 'stealth']),\n )\n .option('--country <code>', 'two-letter proxy country code, e.g. us, gb, de')\n .addOption(\n new Option('--device <type>', 'device emulation').choices(['desktop', 'mobile', 'tablet']),\n )\n .option('--timeout <ms>', 'page-retrieval timeout in ms (max 30000)', integerParser)\n .option('--js-timeout <ms>', 'JS rendering time in ms after load (max 20000)', integerParser)\n .option('--wait-for <selector>', 'CSS selector to wait for before returning content')\n .option(\n '--headers <json>',\n 'HTTP headers to forward to the target page; JSON object or @file.json',\n )\n .option('--js-script <code>', 'custom JS to evaluate on the target page')\n .option('--custom-proxy <url>', 'custom proxy URL: http://user:pass@host:port')\n .option('--error-on-404', 'return error on 404')\n .option('--error-on-redirect', 'return error on redirect');\n}\n\n/**\n * Strict positive-integer check for flags like `serp --page`: digits only,\n * no leading zero, so `1.5`, `2abc`, `0` and `-1` are all rejected (unlike\n * `parseInt`, which would read `2abc` as 2). Values beyond\n * `Number.MAX_SAFE_INTEGER` are rejected too.\n */\nexport function isPositiveIntegerString(value: string): boolean {\n // isSafeInteger: a huge digit string becomes e.g. 1e+21 on the wire, which\n // the server rejects with a 400 (not billed); checking here saves the round trip.\n return /^[1-9]\\d*$/.test(value) && Number.isSafeInteger(Number(value));\n}\n\nexport function integerParser(value: string): number {\n const n = Number.parseInt(value, 10);\n if (!Number.isFinite(n)) throw new Error(`expected integer, got: ${value}`);\n return n;\n}\n\n/**\n * Parse a `--headers` argument. Accepts a JSON object literal or `@path` to\n * load JSON from a file. Returns `undefined` if the input is empty.\n */\nexport async function parseHeaders(\n input: string | undefined,\n): Promise<Record<string, string> | undefined> {\n if (!input || input.trim() === '') return undefined;\n const text = input.startsWith('@') ? await fs.readFile(input.slice(1), 'utf8') : input;\n return parseJsonObject(text, 'headers');\n}\n\n/**\n * Parse a `--fields` argument for `extract`. Accepts JSON object literal or\n * `@path` to a file.\n */\nexport async function parseFields(input: string): Promise<Record<string, string>> {\n const text = input.startsWith('@') ? await fs.readFile(input.slice(1), 'utf8') : input;\n const parsed = parseJsonObject(text, 'fields');\n if (!parsed || Object.keys(parsed).length === 0) {\n throw new Error('fields must be a non-empty JSON object of { fieldName: description }');\n }\n return parsed;\n}\n\nfunction parseJsonObject(text: string, label: string): Record<string, string> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n throw new Error(`${label}: invalid JSON — ${(err as Error).message}`);\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(`${label} must be a JSON object`);\n }\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {\n if (typeof v !== 'string') throw new Error(`${label}.${k}: value must be a string`);\n out[k] = v;\n }\n return out;\n}\n","import { Command } from 'commander';\n\nimport {\n clearPersistedConfig,\n configPath,\n maskApiKey,\n readPersistedConfig,\n writePersistedConfig,\n} from '../lib/config.js';\n\nexport function authCommand(): Command {\n const cmd = new Command('auth').description('Manage your stored API key');\n\n cmd\n .command('set <key>')\n .description(`Save an API key to ${configPath()}`)\n .action(async (key: string) => {\n if (!key || key.trim() === '') throw new Error('API key is empty');\n await writePersistedConfig({ apiKey: key.trim() });\n process.stdout.write(`API key saved to ${configPath()}\\n`);\n });\n\n cmd\n .command('show')\n .description('Display the currently stored API key (masked)')\n .action(async () => {\n const persisted = await readPersistedConfig();\n const env = process.env.WEBSCRAPING_AI_API_KEY;\n if (env && env.trim() !== '') {\n process.stdout.write(\n `Active key (env WEBSCRAPING_AI_API_KEY): ${maskApiKey(env.trim())}\\n`,\n );\n }\n if (persisted.apiKey && persisted.apiKey.trim() !== '') {\n process.stdout.write(\n `Stored key (${configPath()}): ${maskApiKey(persisted.apiKey.trim())}\\n`,\n );\n }\n if (!env && !persisted.apiKey) {\n process.stdout.write('No API key configured. Run `webscraping-ai auth set <key>`.\\n');\n }\n });\n\n cmd\n .command('clear')\n .description('Remove the stored API key file')\n .action(async () => {\n const removed = await clearPersistedConfig();\n process.stdout.write(\n removed ? `Removed ${configPath()}\\n` : 'No stored API key to remove.\\n',\n );\n });\n\n return cmd;\n}\n","import { Command } from 'commander';\n\nimport { buildDataOptions, parseDataParams } from '../lib/buildRequest.js';\nimport type { DataRawFlags } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { EXIT_CODES } from '../lib/errors.js';\nimport { openEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIsStdin, urlIterator } from '../lib/stdin.js';\n\ninterface DataFlags extends DataRawFlags {\n apiKey?: string;\n output?: string;\n pretty?: boolean;\n}\n\nfunction collect(value: string, previous: string[] = []): string[] {\n return [...previous, value];\n}\n\n/**\n * `data <url>` — structured JSON for a page on a supported site (wraps /data).\n *\n * URL-shaped, but the server picks fetching/proxy/parsing per site, so the\n * common scrape flags (`--js`, `--proxy`, `--headers`, …) don't apply and are\n * deliberately not registered. The URL is never checked against a list of\n * sites: new ones are added server-side. An unsupported URL or page type\n * returns a 400 that is not charged (exit code 3); its message lists what is\n * supported. `-` reads one URL per line from stdin (blank and `#` lines\n * skipped, and the batch stops at the first failing item, like the other URL\n * commands). `--param key=value` passes site-specific parameters through.\n *\n * Usage errors (blank url, malformed/reserved `--param`) exit with code 2\n * before stdin is read or the API key is resolved.\n */\nexport function dataCommand(): Command {\n const cmd = new Command('data')\n .description(\n 'Structured JSON for a page on a supported site, e.g. YouTube, TikTok, X, LinkedIn, ' +\n 'Instagram, Reddit (wraps /data, 15 credits/request; more sites are added server-side, ' +\n 'an unsupported URL or page type gets a 400, not charged, listing what is supported)',\n )\n .argument('<url>', 'page URL (or `-` to read URLs from stdin, one per line)')\n .option(\n '--country <code>',\n 'two-letter country code of the proxy used to fetch the page (default: us)',\n )\n .option(\n '--transcript',\n 'YouTube videos only: also fetch the transcript into data.transcript (null without matching captions; a failed transcript fetch fails the request with a 500, not charged)',\n )\n .option(\n '--transcript-language <code>',\n 'caption language to pick, e.g. en or de (default: English, then the first track)',\n )\n .option(\n '--param <key=value>',\n 'extra site-specific query parameter sent as-is (repeatable)',\n collect,\n )\n .option('-k, --api-key <key>', 'API key (overrides $WEBSCRAPING_AI_API_KEY and config file)')\n .option('-o, --output <file>', 'write result to FILE instead of stdout')\n .option('--pretty', 'pretty-print JSON output (default on TTY)')\n .option('--no-pretty', 'force single-line JSON output');\n\n cmd.action(async (url: string, opts: DataFlags) => {\n if (!urlIsStdin(url) && url.trim() === '') {\n cmd.error('error: data requires a non-empty url', {\n exitCode: EXIT_CODES.usage,\n code: 'wsai.emptyUrl',\n });\n }\n try {\n parseDataParams(opts.param);\n } catch (err) {\n cmd.error(`error: ${(err as Error).message}`, {\n exitCode: EXIT_CODES.usage,\n code: 'wsai.invalidParam',\n });\n }\n\n const flags: DataRawFlags = {\n country: opts.country,\n transcript: opts.transcript,\n transcriptLanguage: opts.transcriptLanguage,\n param: opts.param,\n };\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const client = createClient({ apiKey });\n const write = await openEmitter(opts, url);\n\n for await (const target of urlIterator(url)) {\n const result = await client.data(buildDataOptions(target, flags));\n await write(result);\n }\n });\n\n return cmd;\n}\n","/**\n * Exit-code mapping for SDK errors.\n *\n * The SDK throws typed subclasses of `WebScrapingAIError`. Map each to a\n * stable exit code so shell scripts can branch on the failure mode without\n * parsing stderr.\n */\n\nimport {\n APIConnectionError,\n APIError,\n APITimeoutError,\n AuthenticationError,\n BadRequestError,\n GatewayTimeoutError,\n PaymentRequiredError,\n RateLimitError,\n ServerError,\n WebScrapingAIError,\n} from 'webscraping-ai';\n\nimport { redactSecrets } from './redact.js';\n\nexport const EXIT_CODES = {\n ok: 0,\n generic: 1,\n usage: 2,\n badRequest: 3,\n auth: 4,\n paymentRequired: 5,\n rateLimit: 6,\n serverError: 7,\n timeout: 8,\n connection: 9,\n} as const;\n\nexport function exitCodeFor(err: unknown): number {\n if (err instanceof BadRequestError) return EXIT_CODES.badRequest;\n if (err instanceof AuthenticationError) return EXIT_CODES.auth;\n if (err instanceof PaymentRequiredError) return EXIT_CODES.paymentRequired;\n if (err instanceof RateLimitError) return EXIT_CODES.rateLimit;\n if (err instanceof ServerError) return EXIT_CODES.serverError;\n if (err instanceof GatewayTimeoutError) return EXIT_CODES.timeout;\n if (err instanceof APITimeoutError) return EXIT_CODES.timeout;\n if (err instanceof APIConnectionError) return EXIT_CODES.connection;\n if (err instanceof APIError) return EXIT_CODES.serverError;\n if (err instanceof WebScrapingAIError) return EXIT_CODES.generic;\n return EXIT_CODES.generic;\n}\n\n/**\n * Human-readable one-liner for stderr. Always redacted: the API key and any\n * `api_key=...` pattern are replaced, since error bodies can echo the URL.\n */\nexport function formatError(err: unknown): string {\n return redactSecrets(rawMessage(err));\n}\n\nfunction rawMessage(err: unknown): string {\n if (err instanceof APIError) {\n const parts = [err.message];\n if (err.status) parts.push(`(HTTP ${err.status})`);\n return parts.join(' ');\n }\n if (err instanceof Error) return err.message;\n return String(err);\n}\n","import { Command } from 'commander';\n\nimport { buildFieldsOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseFields, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { openEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface ExtractFlags extends CommonRawFlags {\n fields?: string;\n}\n\nexport function extractCommand(): Command {\n const cmd = new Command('extract')\n .description('Extract structured fields from a page using AI (wraps /ai/fields)')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .requiredOption(\n '-f, --fields <json>',\n 'fields to extract — JSON object {name: description} or @file.json',\n );\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: ExtractFlags) => {\n if (!opts.fields) throw new Error('--fields is required');\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const fields = await parseFields(opts.fields);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = await openEmitter(opts, url);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildFieldsOptions(target, fields, opts, { headers });\n const result = await client.fields(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from 'commander';\n\nimport { buildHtmlOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { openEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface HtmlFlags extends CommonRawFlags {\n returnScriptResult?: boolean;\n}\n\nexport function htmlCommand(): Command {\n const cmd = new Command('html')\n .description('Fetch the full HTML of a page')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .addOption(\n new Option('--return-script-result', 'return the result of --js-script instead of HTML'),\n )\n .addOption(new Option('--format <fmt>', 'response wrapping').choices(['text', 'json']));\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: HtmlFlags) => {\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = await openEmitter(opts, url);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildHtmlOptions(\n target,\n opts,\n { headers },\n {\n returnScriptResult: opts.returnScriptResult,\n },\n );\n const result = await client.html(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from 'commander';\n\nimport { buildSelectedOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { openEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface SelectedFlags extends CommonRawFlags {\n selector?: string;\n}\n\nexport function selectedCommand(): Command {\n const cmd = new Command('selected')\n .description('Fetch HTML of one CSS-selected page area')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .option(\n '-s, --selector <css>',\n 'CSS selector for the target element(s); omit for whole-page HTML',\n )\n .addOption(new Option('--format <fmt>', 'response wrapping').choices(['text', 'json']));\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: SelectedFlags) => {\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = await openEmitter(opts, url);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildSelectedOptions(target, opts.selector, opts, { headers });\n const result = await client.selected(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n","import { Command } from 'commander';\n\nimport { buildSelectedMultipleOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { openEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface SelectedMultipleFlags extends CommonRawFlags {\n selector?: string[];\n}\n\nexport function selectedMultipleCommand(): Command {\n const cmd = new Command('selected-multiple')\n .description('Fetch HTML of multiple CSS-selected page areas')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .option(\n '-s, --selector <css>',\n 'CSS selector — pass multiple times for multiple selectors',\n collectMulti,\n [] as string[],\n );\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: SelectedMultipleFlags) => {\n // `--selector` is optional: omitting it returns whole-page HTML.\n const selectors = opts.selector ?? [];\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = await openEmitter(opts, url);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildSelectedMultipleOptions(target, selectors, opts, { headers });\n const result = await client.selectedMultiple(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n\nfunction collectMulti(value: string, previous: string[]): string[] {\n return [...previous, value];\n}\n","import { Command, Option } from 'commander';\n\nimport { buildSerpOptions } from '../lib/buildRequest.js';\nimport type { SerpRawFlags } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { EXIT_CODES } from '../lib/errors.js';\nimport { isPositiveIntegerString } from '../lib/options.js';\nimport { openEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { queryIterator, urlIsStdin } from '../lib/stdin.js';\n\ninterface SerpFlags extends Omit<SerpRawFlags, 'page'> {\n /** Raw `--page` string; validated strictly in the action. */\n page?: string;\n apiKey?: string;\n output?: string;\n pretty?: boolean;\n}\n\n/**\n * `serp <query...>` — parsed search engine results (wraps /serp).\n *\n * Query-shaped, not URL-shaped, so none of the common scrape flags\n * (`--js`, `--proxy`, `--country`, …) apply and they're deliberately not\n * registered here. Multiple words are joined with spaces, so quoting the\n * query is optional. A lone `-` reads one query per line from stdin; only\n * blank lines are skipped (a leading `#` is part of the query, not a comment).\n *\n * Usage errors (bad `--page`, empty query) exit with code 2 before stdin is\n * read or the API key is resolved.\n */\nexport function serpCommand(): Command {\n const cmd = new Command('serp')\n .alias('search')\n .description('Search Google and print parsed results as JSON (wraps /serp, 15 credits/search)')\n .argument('<query...>', 'search query (or `-` to read queries from stdin, one per line)')\n .addOption(new Option('--engine <name>', 'search engine (default: google)').choices(['google']))\n .option('--gl <code>', 'two-letter country code for search geolocation (default: us)')\n .option('--hl <code>', 'two-letter language code for the results (default: en)')\n .option(\n '--page <n>',\n 'results page number, integer >= 1, 10 results per page (default: 1; server rejects > 100 with a 400)',\n )\n .option('-k, --api-key <key>', 'API key (overrides $WEBSCRAPING_AI_API_KEY and config file)')\n .option('-o, --output <file>', 'write result to FILE instead of stdout')\n .option('--pretty', 'pretty-print JSON output (default on TTY)')\n .option('--no-pretty', 'force single-line JSON output');\n\n cmd.action(async (words: string[], opts: SerpFlags) => {\n const query = words.join(' ');\n\n let page: number | undefined;\n if (opts.page !== undefined) {\n if (!isPositiveIntegerString(opts.page)) {\n cmd.error(`error: --page must be an integer >= 1, got: ${opts.page}`, {\n exitCode: EXIT_CODES.usage,\n code: 'wsai.invalidPage',\n });\n }\n page = Number(opts.page);\n }\n if (!urlIsStdin(query) && query.trim() === '') {\n cmd.error('error: serp requires a non-empty query', {\n exitCode: EXIT_CODES.usage,\n code: 'wsai.emptyQuery',\n });\n }\n\n const flags: SerpRawFlags = { engine: opts.engine, gl: opts.gl, hl: opts.hl, page };\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const client = createClient({ apiKey });\n const write = await openEmitter(opts, query);\n\n for await (const q of queryIterator(query)) {\n const result = await client.serp(buildSerpOptions(q, flags));\n await write(result);\n }\n });\n\n return cmd;\n}\n","import { Command } from 'commander';\n\nimport { EDITORS, editorById, installSkill, resolveTarget } from '../lib/skill.js';\nimport type { EditorAdapter } from '../lib/skill.js';\n\ninterface SetupSkillFlags {\n editor?: string[];\n all?: boolean;\n force?: boolean;\n}\n\nexport function setupCommand(): Command {\n const cmd = new Command('setup').description('Install the AI agent skill into your editor(s)');\n\n cmd\n .command('skill')\n .description('Install the WebScraping.AI skill into one or more AI coding editors')\n .option(\n '-e, --editor <id>',\n `editor id (repeatable). Supported: ${EDITORS.map((e) => e.id).join(', ')}`,\n collect,\n [] as string[],\n )\n .option('-a, --all', 'install into every supported editor')\n .option('-f, --force', 'overwrite existing skill files')\n .action(async (opts: SetupSkillFlags) => {\n const selected = resolveSelection(opts);\n if (selected.length === 0) {\n throw new Error(\n 'no editor selected — pass --all or --editor <id> (e.g. --editor claude-code)',\n );\n }\n\n for (const editor of selected) {\n const target = resolveTarget(editor);\n const result = await installSkill(target, { force: opts.force });\n process.stdout.write(`${editor.label}: ${result.status} → ${target.destination}\\n`);\n }\n });\n\n cmd\n .command('list', { hidden: false })\n .description('List supported editors for `setup skill`')\n .action(() => {\n for (const e of EDITORS) {\n const target = resolveTarget(e);\n process.stdout.write(`${e.id.padEnd(14)} ${e.label.padEnd(18)} ${target.destination}\\n`);\n }\n });\n\n return cmd;\n}\n\nfunction collect(value: string, previous: string[]): string[] {\n return [...previous, value];\n}\n\nfunction resolveSelection(opts: SetupSkillFlags): EditorAdapter[] {\n if (opts.all) return [...EDITORS];\n const ids = opts.editor ?? [];\n return ids.map((id) => {\n const editor = editorById(id);\n if (!editor) throw new Error(`unknown editor: ${id}`);\n return editor;\n });\n}\n","/**\n * Multi-editor skill installer.\n *\n * The canonical skill lives in this package at\n * `plugins/webscraping-ai/skills/webscraping-ai/SKILL.md`. Each supported\n * editor reads skill content from a different location and may want either\n * a single Markdown file or a whole skill directory. The `editors` registry\n * captures those differences so `setup skill` is a small driver over it.\n *\n * `mode: 'dir'` copies the entire skill directory (Claude Code-shaped\n * editors that scan a folder per skill). `mode: 'file'` writes the SKILL.md\n * contents to a single file (editors that read flat instructions, like\n * GitHub Copilot's `.github/copilot-instructions.md`).\n *\n * `kind: 'user'` writes to `$HOME`; `kind: 'project'` writes to the current\n * working directory. We prefer user-scope where the editor supports it so\n * the skill becomes globally available without polluting individual repos.\n */\n\nimport { promises as fs } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport type EditorMode = 'dir' | 'file';\nexport type EditorKind = 'user' | 'project';\n\nexport interface EditorAdapter {\n /** Stable identifier used by `--editor <name>`. */\n id: string;\n /** Human-readable label printed in summaries. */\n label: string;\n mode: EditorMode;\n kind: EditorKind;\n /**\n * Where the skill ends up. For `mode: 'dir'`, the skill directory copies\n * to `{root}/{path}`. For `mode: 'file'`, the SKILL.md contents write to\n * `{root}/{path}` (i.e. `path` ends in a filename).\n *\n * `root` is `$HOME` (user) or CWD (project).\n */\n path: string;\n}\n\n/**\n * Registry of supported editors. Order matters: it's also the order the\n * `setup skill` summary lists them in.\n */\nexport const EDITORS: readonly EditorAdapter[] = [\n // Claude Code: user-scope skills live under ~/.claude/skills/<name>/\n {\n id: 'claude-code',\n label: 'Claude Code',\n mode: 'dir',\n kind: 'user',\n path: '.claude/skills/webscraping-ai',\n },\n // Cursor: project-scope rule file. Cursor doesn't have a user-wide skill\n // dir, so this is the editor's \"install\" surface.\n {\n id: 'cursor',\n label: 'Cursor',\n mode: 'file',\n kind: 'project',\n path: '.cursor/rules/webscraping-ai.mdc',\n },\n // Windsurf: project-scope rules. Same shape as Cursor.\n {\n id: 'windsurf',\n label: 'Windsurf',\n mode: 'file',\n kind: 'project',\n path: '.windsurf/rules/webscraping-ai.md',\n },\n // Kiro: user-scope steering directory.\n { id: 'kiro', label: 'Kiro', mode: 'dir', kind: 'user', path: '.kiro/steering/webscraping-ai' },\n // OpenCode: project-scope skills (mirrors Claude shape).\n {\n id: 'opencode',\n label: 'OpenCode',\n mode: 'dir',\n kind: 'project',\n path: '.opencode/skills/webscraping-ai',\n },\n // Gemini CLI: user-scope extension dir with a GEMINI.md.\n {\n id: 'gemini',\n label: 'Gemini CLI',\n mode: 'file',\n kind: 'user',\n path: '.gemini/extensions/webscraping-ai/GEMINI.md',\n },\n // GitHub Copilot: project-scope custom instructions file. Single file.\n {\n id: 'copilot',\n label: 'GitHub Copilot',\n mode: 'file',\n kind: 'project',\n path: '.github/copilot-instructions.md',\n },\n // Augment: project-scope rules file.\n {\n id: 'augment',\n label: 'Augment',\n mode: 'file',\n kind: 'project',\n path: '.augment/rules/webscraping-ai.md',\n },\n // Factory: project-scope rules file.\n {\n id: 'factory',\n label: 'Factory',\n mode: 'file',\n kind: 'project',\n path: '.factory/rules/webscraping-ai.md',\n },\n] as const;\n\nexport function editorById(id: string): EditorAdapter | undefined {\n return EDITORS.find((e) => e.id === id);\n}\n\n/**\n * Where this package keeps the canonical skill. Resolved relative to the\n * compiled `dist/cli.js` so it works after `npm install -g`.\n */\nexport function bundledSkillDir(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n // dist/ sits at the package root, so the plugins dir is two levels up\n // from any compiled file: dist/cli.js → package root → plugins/...\n return resolve(here, '..', 'plugins', 'webscraping-ai', 'skills', 'webscraping-ai');\n}\n\nexport interface InstallTarget {\n editor: EditorAdapter;\n /** Absolute destination path (file or directory depending on `mode`). */\n destination: string;\n}\n\nexport interface ResolveOptions {\n cwd?: string;\n home?: string;\n}\n\nexport function resolveTarget(editor: EditorAdapter, opts: ResolveOptions = {}): InstallTarget {\n const root = editor.kind === 'user' ? (opts.home ?? homedir()) : (opts.cwd ?? process.cwd());\n return { editor, destination: join(root, editor.path) };\n}\n\nexport interface InstallOptions {\n /** If true, overwrite existing files/dirs. */\n force?: boolean;\n /** Override skill source dir (for tests). */\n sourceDir?: string;\n}\n\nexport type InstallStatus = 'installed' | 'skipped-exists' | 'overwritten';\n\nexport interface InstallResult {\n target: InstallTarget;\n status: InstallStatus;\n}\n\nexport async function installSkill(\n target: InstallTarget,\n opts: InstallOptions = {},\n): Promise<InstallResult> {\n const source = opts.sourceDir ?? bundledSkillDir();\n const exists = await pathExists(target.destination);\n\n if (exists && !opts.force) {\n return { target, status: 'skipped-exists' };\n }\n\n if (target.editor.mode === 'dir') {\n if (exists) await fs.rm(target.destination, { recursive: true, force: true });\n await copyDirectory(source, target.destination);\n } else {\n const skillBody = await fs.readFile(join(source, 'SKILL.md'), 'utf8');\n await fs.mkdir(dirname(target.destination), { recursive: true });\n await fs.writeFile(target.destination, skillBody);\n }\n\n return { target, status: exists ? 'overwritten' : 'installed' };\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n try {\n await fs.access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function copyDirectory(source: string, destination: string): Promise<void> {\n // `fs.cp` with `recursive` exists on Node 16.7+; we target Node 20.\n await fs.cp(source, destination, { recursive: true, force: true });\n}\n","import { Command, Option } from 'commander';\n\nimport { buildTextOptions } from '../lib/buildRequest.js';\nimport { resolveApiKey } from '../lib/config.js';\nimport { addCommonScrapeOptions, parseHeaders } from '../lib/options.js';\nimport type { CommonRawFlags } from '../lib/options.js';\nimport { openEmitter } from '../lib/output.js';\nimport { createClient } from '../lib/sdk.js';\nimport { urlIterator } from '../lib/stdin.js';\n\ninterface TextFlags extends CommonRawFlags {\n textFormat?: 'plain' | 'json' | 'xml';\n returnLinks?: boolean;\n}\n\nexport function textCommand(): Command {\n const cmd = new Command('text')\n .description('Fetch the visible text of a page')\n .argument('<url>', 'page URL (or `-` to read URLs from stdin)')\n .addOption(\n new Option('--text-format <fmt>', 'text response format').choices(['plain', 'json', 'xml']),\n )\n .option('--return-links', 'include page links (text-format=json only)');\n\n addCommonScrapeOptions(cmd);\n\n cmd.action(async (url: string, opts: TextFlags) => {\n const apiKey = await resolveApiKey({ flag: opts.apiKey });\n const headers = await parseHeaders(opts.headers);\n const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });\n\n const write = await openEmitter(opts, url);\n\n for await (const target of urlIterator(url)) {\n const sdkOptions = buildTextOptions(\n target,\n opts,\n { headers },\n {\n textFormat: opts.textFormat,\n returnLinks: opts.returnLinks,\n },\n );\n const result = await client.text(sdkOptions);\n await write(result);\n }\n });\n\n return cmd;\n}\n"],"mappings":"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","names":["Command","dirname","resolve","fileURLToPath","fs","fs","Command","Option","fs","Command","Option","Command","Command","Command","Command","Command","Command","Command","Option","Command","Option","Command","Option","Command","Option","Command","Command","Command","Option","Command","Option","Command","fs","homedir","dirname","join","Command","collect","Command","Option","Command","Option","dirname","fileURLToPath","resolve","Command"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webscraping-ai-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Official command-line client for the WebScraping.AI API. Includes a Claude Code skill so AI agents can use it too.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"homepage": "https://webscraping.ai",
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"commander": "^12.1.0",
|
|
56
|
-
"webscraping-ai": "^4.
|
|
56
|
+
"webscraping-ai": "^4.2.0"
|
|
57
57
|
},
|
|
58
58
|
"overrides": {
|
|
59
59
|
"esbuild": "^0.28.1"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webscraping-ai",
|
|
3
|
-
"description": "Fetch HTML, text, selected elements, or AI-extracted data from any URL — including JS-rendered pages —
|
|
4
|
-
"version": "1.
|
|
3
|
+
"description": "Fetch HTML, text, selected elements, or AI-extracted data from any URL — including JS-rendered pages — parsed Google search results, and structured data for supported sites (YouTube, TikTok, X, LinkedIn, Instagram, Reddit, …), via the WebScraping.AI CLI.",
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "WebScraping.AI",
|
|
7
7
|
"url": "https://webscraping.ai"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: webscraping-ai
|
|
3
|
-
description: Use the WebScraping.AI CLI to fetch HTML, plain text, or AI-extracted data from any URL,
|
|
3
|
+
description: Use the WebScraping.AI CLI to fetch HTML, plain text, or AI-extracted data from any URL, to get parsed Google search results for a query, or to get structured data (/data) for pages on supported sites, e.g. YouTube, TikTok, X, LinkedIn, Instagram, Reddit. Reach for this when you need real page content (including JS-rendered sites) or current search results instead of guessing or relying on stale knowledge.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# WebScraping.AI skill
|
|
@@ -26,6 +26,7 @@ Reach for `webscraping-ai` when you need to:
|
|
|
26
26
|
- Render **JavaScript-heavy** pages (SPAs, lazy-loaded content) that a plain `curl` won't see.
|
|
27
27
|
- Bypass **anti-bot protection** that's blocking `curl` / `fetch`.
|
|
28
28
|
- Get **current Google search results** for a query (to find candidate URLs, check rankings, or see what's out there) as clean JSON.
|
|
29
|
+
- Get **structured data for a social/media page** (a YouTube video, TikTok profile, X post, LinkedIn company, Instagram reel, Reddit thread, …) without scraping it yourself.
|
|
29
30
|
|
|
30
31
|
Don't use it for:
|
|
31
32
|
|
|
@@ -35,7 +36,7 @@ Don't use it for:
|
|
|
35
36
|
|
|
36
37
|
## Quick reference
|
|
37
38
|
|
|
38
|
-
The CLI exposes
|
|
39
|
+
The CLI exposes 9 subcommands, one per API endpoint:
|
|
39
40
|
|
|
40
41
|
| Command | What it does |
|
|
41
42
|
| ------------------- | ------------------------------------------------- |
|
|
@@ -46,11 +47,12 @@ The CLI exposes 8 subcommands, one per API endpoint:
|
|
|
46
47
|
| `ask` | Ask a natural-language question about a page (AI) |
|
|
47
48
|
| `extract` | Extract structured fields with descriptions (AI) |
|
|
48
49
|
| `serp` / `search` | Parsed Google search results for a query (JSON) |
|
|
50
|
+
| `data` | Structured JSON for a page on a supported site |
|
|
49
51
|
| `account` | Show remaining API credits / quota |
|
|
50
52
|
|
|
51
53
|
Authentication: set `WEBSCRAPING_AI_API_KEY`, pass `--api-key`, or run `webscraping-ai auth set <key>` once.
|
|
52
54
|
|
|
53
|
-
Every page command supports common flags: `--js/--no-js`, `--proxy {datacenter|residential|stealth}`, `--country <cc>`, `--device {desktop|mobile|tablet}`, `--timeout <ms>`, `--js-timeout <ms>`, `--wait-for <selector>`, `--headers '{"Cookie":"..."}'`, `--output <file>`. `serp` is the exception: it takes a query, not a URL, and only `--engine`, `--gl <cc>`, `--hl <lang>`, `--page <n>`, `--output`.
|
|
55
|
+
Every page command supports common flags: `--js/--no-js`, `--proxy {datacenter|residential|stealth}`, `--country <cc>`, `--device {desktop|mobile|tablet}`, `--timeout <ms>`, `--js-timeout <ms>`, `--wait-for <selector>`, `--headers '{"Cookie":"..."}'`, `--output <file>`. `serp` is the exception: it takes a query, not a URL, and only `--engine`, `--gl <cc>`, `--hl <lang>`, `--page <n>`, `--output`. `data` takes a URL but its own flag set: `--country <cc>` (same meaning as on the page commands: the proxy country, `us` by default), `--transcript`, `--transcript-language <lang>`, `--param key=value`, `--output`. The other scrape flags (`--js`, `--proxy`, `--device`, `--timeout`, `--headers`, …) don't exist on `data`.
|
|
54
56
|
|
|
55
57
|
## How to pick the right command
|
|
56
58
|
|
|
@@ -68,6 +70,8 @@ Every page command supports common flags: `--js/--no-js`, `--proxy {datacenter|r
|
|
|
68
70
|
|
|
69
71
|
**Starting from a query, not a URL** (find pages about X, check who ranks for a term, see what Google shows in a given country)? → `serp "<query>"`. Returns JSON with `organic_results` (`position`, `title`, `link`, `domain`, `snippet`), `related_searches`, and `pagination`. Use `--gl`/`--hl` for country/language and `--page 2` for the next 10 results. Don't `html` a google.com search URL and parse it yourself — `serp` is cheaper, more reliable, and already parsed. Then feed the `link`s you care about into `text`/`ask`/`extract`.
|
|
70
72
|
|
|
73
|
+
**The URL is a YouTube video/channel/playlist, TikTok video/profile, X/Twitter post/profile, LinkedIn company/job/profile, Instagram post/reel/profile or Reddit post/subreddit/user**? → try `data <url>` first. It returns `{ request_parameters: { provider, type }, parse_status, data }` with the page's fields already parsed (snake_case; `null` for what the page doesn't show). Add `--transcript` on a YouTube video to also fetch its transcript into `data.transcript` (null when no matching captions are available; `--transcript-language de` picks the caption language, otherwise English, then the first track). If the transcript fetch itself fails, the whole request fails with a 500 (exit 7) and is not charged — retry without `--transcript` if you only need the metadata. Those sites are examples, not the full list: more are added server-side, so just try the URL. An unsupported URL or page type returns a 400 that is not charged (exit 3); its message lists what is supported. Then `extract`/`ask`/`text` are the fallback.
|
|
74
|
+
|
|
71
75
|
**Need to check available credits**? → `account`.
|
|
72
76
|
|
|
73
77
|
## Example invocations
|
|
@@ -100,6 +104,9 @@ webscraping-ai serp coffee machines --gl de --hl de --page 2
|
|
|
100
104
|
webscraping-ai serp "rust async runtime comparison" --no-pretty | jq -r '.organic_results[0].link' \
|
|
101
105
|
| webscraping-ai text -
|
|
102
106
|
|
|
107
|
+
# Structured data for a YouTube video (with transcript)
|
|
108
|
+
webscraping-ai data 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' --transcript --no-pretty | jq '.data.title'
|
|
109
|
+
|
|
103
110
|
# Pipe a list of URLs through the same command
|
|
104
111
|
cat urls.txt | webscraping-ai text - --output ./articles.ndjson
|
|
105
112
|
|
|
@@ -115,6 +122,7 @@ webscraping-ai html https://geo.example.com \
|
|
|
115
122
|
- **`selected-multiple` returns nested arrays** (`Array<Array<string>>`) — known API response shape. The outer wrapper holds all matches concatenated; flatten in your script if needed.
|
|
116
123
|
- **`extract` wraps its output** under a `result` key: the parsed shape is `{ "result": { ... } }`.
|
|
117
124
|
- **`serp` costs a flat 15 credits per search** (failed searches aren't charged) and returns at most 10 organic results per page. Optional fields (`snippet`, `date`, `related_searches`, `pagination.next`, `search_information.total_results`) may be absent.
|
|
125
|
+
- **`data` costs 15 credits per request**, including `parse_status` `parse_failed` (fetched but not parsed; `data` may be `null` or partial) and `not_found`. Unsupported URLs or page types (exit 3, HTTP 400) and failed fetches aren't charged. `--transcript` can turn an otherwise successful request into a refunded 500 (exit 7) when the transcript fetch fails. The shape of `data` differs per `provider`/`type`, and `provider`/`type` values grow over time.
|
|
118
126
|
- **API key in URL.** The CLI sends the key as a query-string param. Don't log full requests in shared logs.
|
|
119
127
|
- **Exit codes** are stable per error class (auth → 4, payment → 5, rate-limit → 6, server → 7, timeout → 8, connection → 9). Script around them.
|
|
120
128
|
|
|
@@ -122,7 +130,7 @@ webscraping-ai html https://geo.example.com \
|
|
|
122
130
|
|
|
123
131
|
- Text/HTML responses print verbatim. JSON responses pretty-print on a TTY and single-line on a pipe.
|
|
124
132
|
- `--output FILE` writes the result to a file instead of stdout.
|
|
125
|
-
- For batched stdin input, each URL's response is emitted in order, separated by newlines. `-o FILE` collects all of them in
|
|
133
|
+
- For batched stdin input, each URL's response is emitted in order, separated by newlines. `-o FILE` is truncated when the batch starts and collects all of them. A batch **stops at the first failing item** (same for every command, `data -` included): the results before it are already written, the rest of the list is not processed, and the exit code is that error's. Split the list or loop per URL in the shell if you need to keep going past failures. URL lists skip `#` comment lines; `serp -` skips only blank lines, so `#hashtag` queries are searched.
|
|
126
134
|
- `serp --page` must be an integer >= 1 (server rejects > 100 with a 400); anything else exits with code 2 before any request.
|
|
127
135
|
|
|
128
136
|
## See also
|