webscraping-ai-cli 1.0.2 → 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 +14 -4
- package/CHANGELOG.md +40 -0
- package/README.md +56 -30
- package/dist/cli.js +399 -122
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/plugins/webscraping-ai/.claude-plugin/plugin.json +13 -3
- package/plugins/webscraping-ai/skills/webscraping-ai/SKILL.md +34 -13
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,24 +67,100 @@ function maskApiKey(key) {
|
|
|
67
67
|
|
|
68
68
|
// src/lib/output.ts
|
|
69
69
|
import { promises as fs2 } from "fs";
|
|
70
|
-
|
|
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
|
|
107
|
+
async function emit(value, opts = {}, write = {}) {
|
|
71
108
|
const text = stringify(value, opts);
|
|
72
109
|
if (opts.output && opts.output.trim() !== "") {
|
|
73
|
-
|
|
110
|
+
const data = text + (text.endsWith("\n") ? "" : "\n");
|
|
111
|
+
if (write.append) await fs2.appendFile(opts.output, data);
|
|
112
|
+
else await fs2.writeFile(opts.output, data);
|
|
74
113
|
return;
|
|
75
114
|
}
|
|
76
115
|
process.stdout.write(text);
|
|
77
116
|
if (!text.endsWith("\n")) process.stdout.write("\n");
|
|
78
117
|
}
|
|
118
|
+
function createEmitter(opts = {}) {
|
|
119
|
+
let first = true;
|
|
120
|
+
return async (value) => {
|
|
121
|
+
const append = !first;
|
|
122
|
+
first = false;
|
|
123
|
+
await emit(value, opts, { append });
|
|
124
|
+
};
|
|
125
|
+
}
|
|
79
126
|
function stringify(value, opts = {}) {
|
|
80
127
|
if (typeof value === "string") return value;
|
|
81
128
|
const pretty = opts.pretty ?? process.stdout.isTTY ?? false;
|
|
82
129
|
return JSON.stringify(value, null, pretty ? 2 : 0);
|
|
83
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
|
+
}
|
|
84
138
|
|
|
85
139
|
// src/lib/sdk.ts
|
|
86
140
|
import { WebScrapingAI } from "webscraping-ai";
|
|
141
|
+
|
|
142
|
+
// src/lib/redact.ts
|
|
143
|
+
var secrets = /* @__PURE__ */ new Set();
|
|
144
|
+
function registerSecret(value) {
|
|
145
|
+
if (value && value.trim() !== "") secrets.add(value);
|
|
146
|
+
}
|
|
147
|
+
var API_KEY_PARAM = /(api_key=)[^&\s"'<>]*/gi;
|
|
148
|
+
function redactSecrets(text) {
|
|
149
|
+
let out = text.replace(API_KEY_PARAM, "$1[REDACTED]");
|
|
150
|
+
const candidates = new Set(secrets);
|
|
151
|
+
const env = process.env.WEBSCRAPING_AI_API_KEY?.trim();
|
|
152
|
+
if (env) candidates.add(env);
|
|
153
|
+
for (const secret of candidates) {
|
|
154
|
+
for (const form of /* @__PURE__ */ new Set([secret, encodeURIComponent(secret)])) {
|
|
155
|
+
out = out.split(form).join("[REDACTED]");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/lib/sdk.ts
|
|
87
162
|
function createClient(options) {
|
|
163
|
+
registerSecret(options.apiKey);
|
|
88
164
|
const inner = new WebScrapingAI({
|
|
89
165
|
apiKey: options.apiKey,
|
|
90
166
|
timeoutMs: options.requestTimeoutMs
|
|
@@ -107,6 +183,10 @@ function createClient(options) {
|
|
|
107
183
|
return (o) => target.question({ ...o, ...tag });
|
|
108
184
|
case "fields":
|
|
109
185
|
return (o) => target.fields({ ...o, ...tag });
|
|
186
|
+
case "serp":
|
|
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 } });
|
|
110
190
|
case "account":
|
|
111
191
|
return value.bind(target);
|
|
112
192
|
default:
|
|
@@ -177,19 +257,66 @@ function buildQuestionOptions(url, question, flags, resolved) {
|
|
|
177
257
|
function buildFieldsOptions(url, fields, flags, resolved) {
|
|
178
258
|
return { url, fields, ...buildCommonOptions(flags, resolved) };
|
|
179
259
|
}
|
|
260
|
+
function buildSerpOptions(query, flags = {}) {
|
|
261
|
+
if (typeof query !== "string" || query.trim() === "") {
|
|
262
|
+
throw new Error("serp requires a non-empty query");
|
|
263
|
+
}
|
|
264
|
+
const out = { q: query };
|
|
265
|
+
if (flags.engine !== void 0) out.engine = flags.engine;
|
|
266
|
+
if (flags.gl) out.gl = flags.gl;
|
|
267
|
+
if (flags.hl) out.hl = flags.hl;
|
|
268
|
+
if (flags.page !== void 0) {
|
|
269
|
+
if (!Number.isSafeInteger(flags.page) || flags.page < 1) {
|
|
270
|
+
throw new Error(`--page must be an integer >= 1, got: ${flags.page}`);
|
|
271
|
+
}
|
|
272
|
+
out.page = flags.page;
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
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
|
+
}
|
|
180
313
|
|
|
181
314
|
// src/lib/options.ts
|
|
182
315
|
import { promises as fs3 } from "fs";
|
|
183
316
|
import { Option } from "commander";
|
|
184
317
|
function addCommonScrapeOptions(cmd) {
|
|
185
|
-
return cmd.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)").addOption(
|
|
186
|
-
new Option("--
|
|
187
|
-
).option("--js", "execute JS via a headless browser (default: enabled)").option("--no-js", "disable JS execution (faster, cheaper)").addOption(
|
|
188
|
-
new Option("--proxy <type>", "proxy pool").choices([
|
|
189
|
-
"datacenter",
|
|
190
|
-
"residential",
|
|
191
|
-
"stealth"
|
|
192
|
-
])
|
|
318
|
+
return cmd.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").option("--js", "execute JS via a headless browser (default: enabled)").option("--no-js", "disable JS execution (faster, cheaper)").addOption(
|
|
319
|
+
new Option("--proxy <type>", "proxy pool").choices(["datacenter", "residential", "stealth"])
|
|
193
320
|
).option("--country <code>", "two-letter proxy country code, e.g. us, gb, de").addOption(
|
|
194
321
|
new Option("--device <type>", "device emulation").choices(["desktop", "mobile", "tablet"])
|
|
195
322
|
).option("--timeout <ms>", "page-retrieval timeout in ms (max 30000)", integerParser).option("--js-timeout <ms>", "JS rendering time in ms after load (max 20000)", integerParser).option("--wait-for <selector>", "CSS selector to wait for before returning content").option(
|
|
@@ -197,6 +324,9 @@ function addCommonScrapeOptions(cmd) {
|
|
|
197
324
|
"HTTP headers to forward to the target page; JSON object or @file.json"
|
|
198
325
|
).option("--js-script <code>", "custom JS to evaluate on the target page").option("--custom-proxy <url>", "custom proxy URL: http://user:pass@host:port").option("--error-on-404", "return error on 404").option("--error-on-redirect", "return error on redirect");
|
|
199
326
|
}
|
|
327
|
+
function isPositiveIntegerString(value) {
|
|
328
|
+
return /^[1-9]\d*$/.test(value) && Number.isSafeInteger(Number(value));
|
|
329
|
+
}
|
|
200
330
|
function integerParser(value) {
|
|
201
331
|
const n = Number.parseInt(value, 10);
|
|
202
332
|
if (!Number.isFinite(n)) throw new Error(`expected integer, got: ${value}`);
|
|
@@ -233,27 +363,6 @@ function parseJsonObject(text, label) {
|
|
|
233
363
|
return out;
|
|
234
364
|
}
|
|
235
365
|
|
|
236
|
-
// src/lib/stdin.ts
|
|
237
|
-
import readline from "readline";
|
|
238
|
-
function urlIsStdin(url) {
|
|
239
|
-
return url === "-";
|
|
240
|
-
}
|
|
241
|
-
async function* readUrlsFromStdin() {
|
|
242
|
-
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
243
|
-
for await (const raw of rl) {
|
|
244
|
-
const line = raw.trim();
|
|
245
|
-
if (line === "" || line.startsWith("#")) continue;
|
|
246
|
-
yield line;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
async function* urlIterator(arg) {
|
|
250
|
-
if (urlIsStdin(arg)) {
|
|
251
|
-
yield* readUrlsFromStdin();
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
yield arg;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
366
|
// src/commands/ask.ts
|
|
258
367
|
function askCommand() {
|
|
259
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"]));
|
|
@@ -263,10 +372,11 @@ function askCommand() {
|
|
|
263
372
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
264
373
|
const headers = await parseHeaders(opts.headers);
|
|
265
374
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
375
|
+
const write = await openEmitter(opts, url);
|
|
266
376
|
for await (const target of urlIterator(url)) {
|
|
267
377
|
const sdkOptions = buildQuestionOptions(target, opts.question, opts, { headers });
|
|
268
378
|
const result = await client.question(sdkOptions);
|
|
269
|
-
await
|
|
379
|
+
await write(result);
|
|
270
380
|
}
|
|
271
381
|
});
|
|
272
382
|
return cmd;
|
|
@@ -286,8 +396,10 @@ function authCommand() {
|
|
|
286
396
|
const persisted = await readPersistedConfig();
|
|
287
397
|
const env = process.env.WEBSCRAPING_AI_API_KEY;
|
|
288
398
|
if (env && env.trim() !== "") {
|
|
289
|
-
process.stdout.write(
|
|
290
|
-
`)
|
|
399
|
+
process.stdout.write(
|
|
400
|
+
`Active key (env WEBSCRAPING_AI_API_KEY): ${maskApiKey(env.trim())}
|
|
401
|
+
`
|
|
402
|
+
);
|
|
291
403
|
}
|
|
292
404
|
if (persisted.apiKey && persisted.apiKey.trim() !== "") {
|
|
293
405
|
process.stdout.write(
|
|
@@ -301,16 +413,125 @@ function authCommand() {
|
|
|
301
413
|
});
|
|
302
414
|
cmd.command("clear").description("Remove the stored API key file").action(async () => {
|
|
303
415
|
const removed = await clearPersistedConfig();
|
|
304
|
-
process.stdout.write(
|
|
305
|
-
|
|
416
|
+
process.stdout.write(
|
|
417
|
+
removed ? `Removed ${configPath()}
|
|
418
|
+
` : "No stored API key to remove.\n"
|
|
419
|
+
);
|
|
306
420
|
});
|
|
307
421
|
return cmd;
|
|
308
422
|
}
|
|
309
423
|
|
|
310
|
-
// src/commands/
|
|
424
|
+
// src/commands/data.ts
|
|
311
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";
|
|
312
533
|
function extractCommand() {
|
|
313
|
-
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(
|
|
314
535
|
"-f, --fields <json>",
|
|
315
536
|
"fields to extract \u2014 JSON object {name: description} or @file.json"
|
|
316
537
|
);
|
|
@@ -321,39 +542,48 @@ function extractCommand() {
|
|
|
321
542
|
const headers = await parseHeaders(opts.headers);
|
|
322
543
|
const fields = await parseFields(opts.fields);
|
|
323
544
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
545
|
+
const write = await openEmitter(opts, url);
|
|
324
546
|
for await (const target of urlIterator(url)) {
|
|
325
547
|
const sdkOptions = buildFieldsOptions(target, fields, opts, { headers });
|
|
326
548
|
const result = await client.fields(sdkOptions);
|
|
327
|
-
await
|
|
549
|
+
await write(result);
|
|
328
550
|
}
|
|
329
551
|
});
|
|
330
552
|
return cmd;
|
|
331
553
|
}
|
|
332
554
|
|
|
333
555
|
// src/commands/html.ts
|
|
334
|
-
import { Command as
|
|
556
|
+
import { Command as Command6, Option as Option3 } from "commander";
|
|
335
557
|
function htmlCommand() {
|
|
336
|
-
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(
|
|
559
|
+
new Option3("--return-script-result", "return the result of --js-script instead of HTML")
|
|
560
|
+
).addOption(new Option3("--format <fmt>", "response wrapping").choices(["text", "json"]));
|
|
337
561
|
addCommonScrapeOptions(cmd);
|
|
338
562
|
cmd.action(async (url, opts) => {
|
|
339
563
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
340
564
|
const headers = await parseHeaders(opts.headers);
|
|
341
565
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
566
|
+
const write = await openEmitter(opts, url);
|
|
342
567
|
for await (const target of urlIterator(url)) {
|
|
343
|
-
const sdkOptions = buildHtmlOptions(
|
|
344
|
-
|
|
345
|
-
|
|
568
|
+
const sdkOptions = buildHtmlOptions(
|
|
569
|
+
target,
|
|
570
|
+
opts,
|
|
571
|
+
{ headers },
|
|
572
|
+
{
|
|
573
|
+
returnScriptResult: opts.returnScriptResult
|
|
574
|
+
}
|
|
575
|
+
);
|
|
346
576
|
const result = await client.html(sdkOptions);
|
|
347
|
-
await
|
|
577
|
+
await write(result);
|
|
348
578
|
}
|
|
349
579
|
});
|
|
350
580
|
return cmd;
|
|
351
581
|
}
|
|
352
582
|
|
|
353
583
|
// src/commands/selected.ts
|
|
354
|
-
import { Command as
|
|
584
|
+
import { Command as Command7, Option as Option4 } from "commander";
|
|
355
585
|
function selectedCommand() {
|
|
356
|
-
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(
|
|
357
587
|
"-s, --selector <css>",
|
|
358
588
|
"CSS selector for the target element(s); omit for whole-page HTML"
|
|
359
589
|
).addOption(new Option4("--format <fmt>", "response wrapping").choices(["text", "json"]));
|
|
@@ -362,19 +592,20 @@ function selectedCommand() {
|
|
|
362
592
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
363
593
|
const headers = await parseHeaders(opts.headers);
|
|
364
594
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
595
|
+
const write = await openEmitter(opts, url);
|
|
365
596
|
for await (const target of urlIterator(url)) {
|
|
366
597
|
const sdkOptions = buildSelectedOptions(target, opts.selector, opts, { headers });
|
|
367
598
|
const result = await client.selected(sdkOptions);
|
|
368
|
-
await
|
|
599
|
+
await write(result);
|
|
369
600
|
}
|
|
370
601
|
});
|
|
371
602
|
return cmd;
|
|
372
603
|
}
|
|
373
604
|
|
|
374
605
|
// src/commands/selectedMultiple.ts
|
|
375
|
-
import { Command as
|
|
606
|
+
import { Command as Command8 } from "commander";
|
|
376
607
|
function selectedMultipleCommand() {
|
|
377
|
-
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(
|
|
378
609
|
"-s, --selector <css>",
|
|
379
610
|
"CSS selector \u2014 pass multiple times for multiple selectors",
|
|
380
611
|
collectMulti,
|
|
@@ -386,10 +617,11 @@ function selectedMultipleCommand() {
|
|
|
386
617
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
387
618
|
const headers = await parseHeaders(opts.headers);
|
|
388
619
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
620
|
+
const write = await openEmitter(opts, url);
|
|
389
621
|
for await (const target of urlIterator(url)) {
|
|
390
622
|
const sdkOptions = buildSelectedMultipleOptions(target, selectors, opts, { headers });
|
|
391
623
|
const result = await client.selectedMultiple(sdkOptions);
|
|
392
|
-
await
|
|
624
|
+
await write(result);
|
|
393
625
|
}
|
|
394
626
|
});
|
|
395
627
|
return cmd;
|
|
@@ -398,8 +630,45 @@ function collectMulti(value, previous) {
|
|
|
398
630
|
return [...previous, value];
|
|
399
631
|
}
|
|
400
632
|
|
|
633
|
+
// src/commands/serp.ts
|
|
634
|
+
import { Command as Command9, Option as Option5 } from "commander";
|
|
635
|
+
function serpCommand() {
|
|
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(
|
|
637
|
+
"--page <n>",
|
|
638
|
+
"results page number, integer >= 1, 10 results per page (default: 1; server rejects > 100 with a 400)"
|
|
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");
|
|
640
|
+
cmd.action(async (words, opts) => {
|
|
641
|
+
const query = words.join(" ");
|
|
642
|
+
let page;
|
|
643
|
+
if (opts.page !== void 0) {
|
|
644
|
+
if (!isPositiveIntegerString(opts.page)) {
|
|
645
|
+
cmd.error(`error: --page must be an integer >= 1, got: ${opts.page}`, {
|
|
646
|
+
exitCode: EXIT_CODES.usage,
|
|
647
|
+
code: "wsai.invalidPage"
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
page = Number(opts.page);
|
|
651
|
+
}
|
|
652
|
+
if (!urlIsStdin(query) && query.trim() === "") {
|
|
653
|
+
cmd.error("error: serp requires a non-empty query", {
|
|
654
|
+
exitCode: EXIT_CODES.usage,
|
|
655
|
+
code: "wsai.emptyQuery"
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
const flags = { engine: opts.engine, gl: opts.gl, hl: opts.hl, page };
|
|
659
|
+
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
660
|
+
const client = createClient({ apiKey });
|
|
661
|
+
const write = await openEmitter(opts, query);
|
|
662
|
+
for await (const q of queryIterator(query)) {
|
|
663
|
+
const result = await client.serp(buildSerpOptions(q, flags));
|
|
664
|
+
await write(result);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
return cmd;
|
|
668
|
+
}
|
|
669
|
+
|
|
401
670
|
// src/commands/setupSkill.ts
|
|
402
|
-
import { Command as
|
|
671
|
+
import { Command as Command10 } from "commander";
|
|
403
672
|
|
|
404
673
|
// src/lib/skill.ts
|
|
405
674
|
import { promises as fs4 } from "fs";
|
|
@@ -408,24 +677,72 @@ import { dirname as dirname2, join as join2, resolve } from "path";
|
|
|
408
677
|
import { fileURLToPath } from "url";
|
|
409
678
|
var EDITORS = [
|
|
410
679
|
// Claude Code: user-scope skills live under ~/.claude/skills/<name>/
|
|
411
|
-
{
|
|
680
|
+
{
|
|
681
|
+
id: "claude-code",
|
|
682
|
+
label: "Claude Code",
|
|
683
|
+
mode: "dir",
|
|
684
|
+
kind: "user",
|
|
685
|
+
path: ".claude/skills/webscraping-ai"
|
|
686
|
+
},
|
|
412
687
|
// Cursor: project-scope rule file. Cursor doesn't have a user-wide skill
|
|
413
688
|
// dir, so this is the editor's "install" surface.
|
|
414
|
-
{
|
|
689
|
+
{
|
|
690
|
+
id: "cursor",
|
|
691
|
+
label: "Cursor",
|
|
692
|
+
mode: "file",
|
|
693
|
+
kind: "project",
|
|
694
|
+
path: ".cursor/rules/webscraping-ai.mdc"
|
|
695
|
+
},
|
|
415
696
|
// Windsurf: project-scope rules. Same shape as Cursor.
|
|
416
|
-
{
|
|
697
|
+
{
|
|
698
|
+
id: "windsurf",
|
|
699
|
+
label: "Windsurf",
|
|
700
|
+
mode: "file",
|
|
701
|
+
kind: "project",
|
|
702
|
+
path: ".windsurf/rules/webscraping-ai.md"
|
|
703
|
+
},
|
|
417
704
|
// Kiro: user-scope steering directory.
|
|
418
705
|
{ id: "kiro", label: "Kiro", mode: "dir", kind: "user", path: ".kiro/steering/webscraping-ai" },
|
|
419
706
|
// OpenCode: project-scope skills (mirrors Claude shape).
|
|
420
|
-
{
|
|
707
|
+
{
|
|
708
|
+
id: "opencode",
|
|
709
|
+
label: "OpenCode",
|
|
710
|
+
mode: "dir",
|
|
711
|
+
kind: "project",
|
|
712
|
+
path: ".opencode/skills/webscraping-ai"
|
|
713
|
+
},
|
|
421
714
|
// Gemini CLI: user-scope extension dir with a GEMINI.md.
|
|
422
|
-
{
|
|
715
|
+
{
|
|
716
|
+
id: "gemini",
|
|
717
|
+
label: "Gemini CLI",
|
|
718
|
+
mode: "file",
|
|
719
|
+
kind: "user",
|
|
720
|
+
path: ".gemini/extensions/webscraping-ai/GEMINI.md"
|
|
721
|
+
},
|
|
423
722
|
// GitHub Copilot: project-scope custom instructions file. Single file.
|
|
424
|
-
{
|
|
723
|
+
{
|
|
724
|
+
id: "copilot",
|
|
725
|
+
label: "GitHub Copilot",
|
|
726
|
+
mode: "file",
|
|
727
|
+
kind: "project",
|
|
728
|
+
path: ".github/copilot-instructions.md"
|
|
729
|
+
},
|
|
425
730
|
// Augment: project-scope rules file.
|
|
426
|
-
{
|
|
731
|
+
{
|
|
732
|
+
id: "augment",
|
|
733
|
+
label: "Augment",
|
|
734
|
+
mode: "file",
|
|
735
|
+
kind: "project",
|
|
736
|
+
path: ".augment/rules/webscraping-ai.md"
|
|
737
|
+
},
|
|
427
738
|
// Factory: project-scope rules file.
|
|
428
|
-
{
|
|
739
|
+
{
|
|
740
|
+
id: "factory",
|
|
741
|
+
label: "Factory",
|
|
742
|
+
mode: "file",
|
|
743
|
+
kind: "project",
|
|
744
|
+
path: ".factory/rules/webscraping-ai.md"
|
|
745
|
+
}
|
|
429
746
|
];
|
|
430
747
|
function editorById(id) {
|
|
431
748
|
return EDITORS.find((e) => e.id === id);
|
|
@@ -468,11 +785,11 @@ async function copyDirectory(source, destination) {
|
|
|
468
785
|
|
|
469
786
|
// src/commands/setupSkill.ts
|
|
470
787
|
function setupCommand() {
|
|
471
|
-
const cmd = new
|
|
788
|
+
const cmd = new Command10("setup").description("Install the AI agent skill into your editor(s)");
|
|
472
789
|
cmd.command("skill").description("Install the WebScraping.AI skill into one or more AI coding editors").option(
|
|
473
790
|
"-e, --editor <id>",
|
|
474
791
|
`editor id (repeatable). Supported: ${EDITORS.map((e) => e.id).join(", ")}`,
|
|
475
|
-
|
|
792
|
+
collect2,
|
|
476
793
|
[]
|
|
477
794
|
).option("-a, --all", "install into every supported editor").option("-f, --force", "overwrite existing skill files").action(async (opts) => {
|
|
478
795
|
const selected = resolveSelection(opts);
|
|
@@ -497,7 +814,7 @@ function setupCommand() {
|
|
|
497
814
|
});
|
|
498
815
|
return cmd;
|
|
499
816
|
}
|
|
500
|
-
function
|
|
817
|
+
function collect2(value, previous) {
|
|
501
818
|
return [...previous, value];
|
|
502
819
|
}
|
|
503
820
|
function resolveSelection(opts) {
|
|
@@ -511,76 +828,34 @@ function resolveSelection(opts) {
|
|
|
511
828
|
}
|
|
512
829
|
|
|
513
830
|
// src/commands/text.ts
|
|
514
|
-
import { Command as
|
|
831
|
+
import { Command as Command11, Option as Option6 } from "commander";
|
|
515
832
|
function textCommand() {
|
|
516
|
-
const cmd = new
|
|
517
|
-
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(
|
|
834
|
+
new Option6("--text-format <fmt>", "text response format").choices(["plain", "json", "xml"])
|
|
518
835
|
).option("--return-links", "include page links (text-format=json only)");
|
|
519
836
|
addCommonScrapeOptions(cmd);
|
|
520
837
|
cmd.action(async (url, opts) => {
|
|
521
838
|
const apiKey = await resolveApiKey({ flag: opts.apiKey });
|
|
522
839
|
const headers = await parseHeaders(opts.headers);
|
|
523
840
|
const client = createClient({ apiKey, requestTimeoutMs: opts.timeout });
|
|
841
|
+
const write = await openEmitter(opts, url);
|
|
524
842
|
for await (const target of urlIterator(url)) {
|
|
525
|
-
const sdkOptions = buildTextOptions(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
843
|
+
const sdkOptions = buildTextOptions(
|
|
844
|
+
target,
|
|
845
|
+
opts,
|
|
846
|
+
{ headers },
|
|
847
|
+
{
|
|
848
|
+
textFormat: opts.textFormat,
|
|
849
|
+
returnLinks: opts.returnLinks
|
|
850
|
+
}
|
|
851
|
+
);
|
|
529
852
|
const result = await client.text(sdkOptions);
|
|
530
|
-
await
|
|
853
|
+
await write(result);
|
|
531
854
|
}
|
|
532
855
|
});
|
|
533
856
|
return cmd;
|
|
534
857
|
}
|
|
535
858
|
|
|
536
|
-
// src/lib/errors.ts
|
|
537
|
-
import {
|
|
538
|
-
APIConnectionError,
|
|
539
|
-
APIError,
|
|
540
|
-
APITimeoutError,
|
|
541
|
-
AuthenticationError,
|
|
542
|
-
BadRequestError,
|
|
543
|
-
GatewayTimeoutError,
|
|
544
|
-
PaymentRequiredError,
|
|
545
|
-
RateLimitError,
|
|
546
|
-
ServerError,
|
|
547
|
-
WebScrapingAIError
|
|
548
|
-
} from "webscraping-ai";
|
|
549
|
-
var EXIT_CODES = {
|
|
550
|
-
ok: 0,
|
|
551
|
-
generic: 1,
|
|
552
|
-
usage: 2,
|
|
553
|
-
badRequest: 3,
|
|
554
|
-
auth: 4,
|
|
555
|
-
paymentRequired: 5,
|
|
556
|
-
rateLimit: 6,
|
|
557
|
-
serverError: 7,
|
|
558
|
-
timeout: 8,
|
|
559
|
-
connection: 9
|
|
560
|
-
};
|
|
561
|
-
function exitCodeFor(err) {
|
|
562
|
-
if (err instanceof BadRequestError) return EXIT_CODES.badRequest;
|
|
563
|
-
if (err instanceof AuthenticationError) return EXIT_CODES.auth;
|
|
564
|
-
if (err instanceof PaymentRequiredError) return EXIT_CODES.paymentRequired;
|
|
565
|
-
if (err instanceof RateLimitError) return EXIT_CODES.rateLimit;
|
|
566
|
-
if (err instanceof ServerError) return EXIT_CODES.serverError;
|
|
567
|
-
if (err instanceof GatewayTimeoutError) return EXIT_CODES.timeout;
|
|
568
|
-
if (err instanceof APITimeoutError) return EXIT_CODES.timeout;
|
|
569
|
-
if (err instanceof APIConnectionError) return EXIT_CODES.connection;
|
|
570
|
-
if (err instanceof APIError) return EXIT_CODES.serverError;
|
|
571
|
-
if (err instanceof WebScrapingAIError) return EXIT_CODES.generic;
|
|
572
|
-
return EXIT_CODES.generic;
|
|
573
|
-
}
|
|
574
|
-
function formatError(err) {
|
|
575
|
-
if (err instanceof APIError) {
|
|
576
|
-
const parts = [err.message];
|
|
577
|
-
if (err.status) parts.push(`(HTTP ${err.status})`);
|
|
578
|
-
return parts.join(" ");
|
|
579
|
-
}
|
|
580
|
-
if (err instanceof Error) return err.message;
|
|
581
|
-
return String(err);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
859
|
// src/cli.ts
|
|
585
860
|
function packageVersion() {
|
|
586
861
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
@@ -589,9 +864,9 @@ function packageVersion() {
|
|
|
589
864
|
return raw.version;
|
|
590
865
|
}
|
|
591
866
|
function buildProgram() {
|
|
592
|
-
const program = new
|
|
867
|
+
const program = new Command12();
|
|
593
868
|
program.name("webscraping-ai").description(
|
|
594
|
-
"Official CLI for the WebScraping.AI API. Fetch pages, 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."
|
|
595
870
|
).version(packageVersion(), "-v, --version", "output the version number").showHelpAfterError();
|
|
596
871
|
program.addCommand(htmlCommand());
|
|
597
872
|
program.addCommand(textCommand());
|
|
@@ -599,6 +874,8 @@ function buildProgram() {
|
|
|
599
874
|
program.addCommand(selectedMultipleCommand());
|
|
600
875
|
program.addCommand(askCommand());
|
|
601
876
|
program.addCommand(extractCommand());
|
|
877
|
+
program.addCommand(serpCommand());
|
|
878
|
+
program.addCommand(dataCommand());
|
|
602
879
|
program.addCommand(accountCommand());
|
|
603
880
|
program.addCommand(authCommand());
|
|
604
881
|
program.addCommand(setupCommand());
|