webforai 3.0.0 → 4.0.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/README.md +25 -5
- package/dist/bin.js +1277 -479
- package/dist/cjs/detect-client-shell.js +78 -0
- package/dist/cjs/extract-meta-refresh.js +71 -0
- package/dist/cjs/extractors/lib/sanitize.js +4 -0
- package/dist/cjs/html-to-mdast.js +2 -1
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/link-replacer.js +19 -13
- package/dist/cjs/loaders/cf-puppeteer.js +14 -7
- package/dist/cjs/loaders/fetch.js +14 -2
- package/dist/cjs/loaders/playwright.js +32 -30
- package/dist/cjs/loaders/puppeteer.js +14 -7
- package/dist/cjs/md-splitter.js +24 -11
- package/dist/cjs/mdast-handlers/custom-code-handler.js +18 -7
- package/dist/cjs/mdast-handlers/custom-div-handler.js +12 -33
- package/dist/cjs/mdast-handlers/definition-list-handler.js +17 -6
- package/dist/cjs/mdast-to-markdown.js +2 -5
- package/dist/cjs/normalize/index.js +11 -2
- package/dist/cjs/platform/client.js +73 -16
- package/dist/cjs/platform/index.js +4 -0
- package/dist/cjs/platform/types.js +7 -1
- package/dist/detect-client-shell.js +53 -0
- package/dist/extract-meta-refresh.js +47 -0
- package/dist/extractors/lib/sanitize.js +4 -0
- package/dist/html-to-mdast.js +2 -1
- package/dist/index.js +14 -0
- package/dist/link-replacer.js +19 -13
- package/dist/loaders/cf-puppeteer.js +15 -8
- package/dist/loaders/fetch.js +14 -2
- package/dist/loaders/playwright.js +32 -30
- package/dist/loaders/puppeteer.js +15 -8
- package/dist/md-splitter.js +24 -11
- package/dist/mdast-handlers/custom-code-handler.js +17 -7
- package/dist/mdast-handlers/custom-div-handler.js +12 -33
- package/dist/mdast-handlers/definition-list-handler.js +17 -6
- package/dist/mdast-to-markdown.js +2 -5
- package/dist/normalize/index.js +12 -2
- package/dist/platform/client.js +73 -16
- package/dist/platform/index.js +4 -0
- package/dist/platform/types.js +5 -1
- package/dist/types/cli/bin.js.map +1 -1
- package/dist/types/cli/commands/jobs/files.d.ts +28 -0
- package/dist/types/cli/commands/jobs/files.js.map +1 -0
- package/dist/types/cli/commands/jobs/llms-txt.d.ts +33 -0
- package/dist/types/cli/commands/jobs/llms-txt.js.map +1 -0
- package/dist/types/cli/commands/jobs/options.d.ts +58 -0
- package/dist/types/cli/commands/jobs/options.js.map +1 -0
- package/dist/types/cli/commands/jobs/run.d.ts +44 -0
- package/dist/types/cli/commands/jobs/run.js.map +1 -0
- package/dist/types/cli/commands/skill/content.js.map +1 -1
- package/dist/types/cli/commands/webforai/interactive.d.ts +1 -1
- package/dist/types/cli/commands/webforai/interactive.js.map +1 -1
- package/dist/types/cli/commands/webforai/options.d.ts +9 -2
- package/dist/types/cli/commands/webforai/options.js.map +1 -1
- package/dist/types/cli/commands/webforai/run.js.map +1 -1
- package/dist/types/cli/utils.d.ts +8 -1
- package/dist/types/cli/utils.js.map +1 -1
- package/dist/types/detect-client-shell.d.ts +24 -0
- package/dist/types/detect-client-shell.js.map +1 -0
- package/dist/types/extract-meta-refresh.d.ts +29 -0
- package/dist/types/extract-meta-refresh.js.map +1 -0
- package/dist/types/extractors/lib/sanitize.js.map +1 -1
- package/dist/types/html-to-mdast.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/link-replacer.d.ts +3 -1
- package/dist/types/link-replacer.js.map +1 -1
- package/dist/types/loaders/cf-puppeteer.js.map +1 -1
- package/dist/types/loaders/fetch.d.ts +4 -0
- package/dist/types/loaders/fetch.js.map +1 -1
- package/dist/types/loaders/playwright.js.map +1 -1
- package/dist/types/loaders/puppeteer.js.map +1 -1
- package/dist/types/md-splitter.d.ts +1 -1
- package/dist/types/md-splitter.js.map +1 -1
- package/dist/types/mdast-handlers/custom-code-handler.d.ts +2 -0
- package/dist/types/mdast-handlers/custom-code-handler.js.map +1 -1
- package/dist/types/mdast-handlers/custom-div-handler.js.map +1 -1
- package/dist/types/mdast-handlers/definition-list-handler.d.ts +0 -11
- package/dist/types/mdast-handlers/definition-list-handler.js.map +1 -1
- package/dist/types/mdast-to-markdown.js.map +1 -1
- package/dist/types/normalize/index.js.map +1 -1
- package/dist/types/platform/client.d.ts +2 -1
- package/dist/types/platform/client.js.map +1 -1
- package/dist/types/platform/error.d.ts +5 -3
- package/dist/types/platform/error.js.map +1 -1
- package/dist/types/platform/fetch.d.ts +2 -0
- package/dist/types/platform/fetch.js.map +1 -1
- package/dist/types/platform/index.d.ts +1 -1
- package/dist/types/platform/index.js.map +1 -1
- package/dist/types/platform/types.d.ts +35 -6
- package/dist/types/platform/types.js.map +1 -1
- package/package.json +16 -5
package/dist/bin.js
CHANGED
|
@@ -6,24 +6,35 @@ import { CommanderError, program } from "commander";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "webforai",
|
|
9
|
-
version: "
|
|
10
|
-
description: "
|
|
9
|
+
version: "4.0.0",
|
|
10
|
+
description: "Convert web pages and local HTML to clean, LLM-ready Markdown: library, CLI (npx webforai) and hosted-API client.",
|
|
11
11
|
author: "inaridiy",
|
|
12
12
|
license: "Apache-2.0",
|
|
13
13
|
keywords: [
|
|
14
|
-
"
|
|
14
|
+
"html-to-markdown",
|
|
15
|
+
"markdown",
|
|
16
|
+
"llm",
|
|
15
17
|
"ai",
|
|
18
|
+
"scraper",
|
|
19
|
+
"crawler",
|
|
20
|
+
"readability",
|
|
21
|
+
"web-scraping",
|
|
22
|
+
"cli",
|
|
23
|
+
"rag",
|
|
16
24
|
"html",
|
|
17
25
|
"html2md",
|
|
18
|
-
"markdown",
|
|
19
26
|
"mdast",
|
|
20
27
|
"hast"
|
|
21
28
|
],
|
|
22
29
|
repository: {
|
|
23
30
|
type: "git",
|
|
24
|
-
url: "https://github.com/inaridiy/webforai.git"
|
|
31
|
+
url: "https://github.com/inaridiy/webforai.git",
|
|
32
|
+
directory: "packages/webforai"
|
|
25
33
|
},
|
|
26
34
|
homepage: "https://webforai.dev/",
|
|
35
|
+
bugs: {
|
|
36
|
+
url: "https://github.com/inaridiy/webforai/issues"
|
|
37
|
+
},
|
|
27
38
|
scripts: {
|
|
28
39
|
"copy:package.cjs.json": "pnpm ncp ./package.cjs.json ./dist/cjs/package.json && pnpm ncp ./package.cjs.json ./dist/types/package.json ",
|
|
29
40
|
clean: "rimraf dist",
|
|
@@ -154,163 +165,6 @@ var package_default = {
|
|
|
154
165
|
}
|
|
155
166
|
};
|
|
156
167
|
|
|
157
|
-
// src/cli/commands/skill/index.ts
|
|
158
|
-
import { spawn } from "node:child_process";
|
|
159
|
-
import fs from "node:fs/promises";
|
|
160
|
-
import os from "node:os";
|
|
161
|
-
import path from "node:path";
|
|
162
|
-
|
|
163
|
-
// src/cli/commands/skill/content.ts
|
|
164
|
-
var renderSkillMarkdown = (version) => `---
|
|
165
|
-
name: webforai
|
|
166
|
-
description: Convert web pages or local HTML files to clean, LLM-ready Markdown with the webforai CLI. Use when a task needs page content as Markdown - reading documentation or articles, scraping, summarising URLs, building text corpora - including JavaScript-heavy or bot-protected pages via the hosted platform.
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
# webforai CLI${version ? ` (v${version})` : ""}
|
|
170
|
-
|
|
171
|
-
Convert a URL or local HTML file to Markdown. Non-interactive by default: the Markdown goes
|
|
172
|
-
to stdout, every log goes to stderr, so output can be piped or captured directly.
|
|
173
|
-
|
|
174
|
-
## Basic usage
|
|
175
|
-
|
|
176
|
-
\`\`\`bash
|
|
177
|
-
npx webforai https://example.com/article # Markdown to stdout
|
|
178
|
-
npx webforai https://example.com/article -o out.md # write to a file instead
|
|
179
|
-
npx webforai ./page.html # convert a local HTML file
|
|
180
|
-
npx webforai https://example.com --json # machine-readable envelope (see below)
|
|
181
|
-
\`\`\`
|
|
182
|
-
|
|
183
|
-
## Flags
|
|
184
|
-
|
|
185
|
-
- \`-o, --output <path>\` \u2014 write Markdown to a file instead of stdout.
|
|
186
|
-
- \`-l, --loader <fetch|playwright|platform>\` \u2014 how HTML is acquired. Default: \`fetch\` for
|
|
187
|
-
URLs, automatic \`local\` for file paths. \`playwright\` renders JavaScript locally (needs
|
|
188
|
-
\`npx playwright install chromium\`). \`platform\` uses the hosted API (below).
|
|
189
|
-
- \`-m, --mode <default|ai>\` \u2014 \`ai\` strips links/tables/images down to plain text
|
|
190
|
-
(local conversion only).
|
|
191
|
-
- \`--extractor <auto|takumi|minimal|none>\` \u2014 main-content extraction preset
|
|
192
|
-
(\`none\` converts the whole page).
|
|
193
|
-
- \`--frontmatter\` \u2014 prepend YAML front matter (title, author, canonical URL, ...).
|
|
194
|
-
- \`--json\` \u2014 print a JSON envelope to stdout instead of raw Markdown.
|
|
195
|
-
- \`--engine <fetch|browser|proxy-fetch|proxy-browser>\` \u2014 platform acquisition engine
|
|
196
|
-
(implies \`-l platform\`). \`browser\`/\`proxy-browser\` render JavaScript; \`proxy-*\` engines
|
|
197
|
-
egress through rotating proxies for bot-protected sites.
|
|
198
|
-
- \`--region <auto|us|eu|uk|jp|asia>\` \u2014 platform proxy egress region (implies \`-l platform\`).
|
|
199
|
-
- \`--screenshot\` \u2014 platform browser engines only; the envelope carries an expiring
|
|
200
|
-
\`screenshotUrl\`.
|
|
201
|
-
- \`--api-key <key>\` / \`--platform-url <url>\` \u2014 platform credentials and host; usually set
|
|
202
|
-
via env vars instead.
|
|
203
|
-
- \`-i, --interactive\` \u2014 guided prompt flow (for humans; do not use from an agent).
|
|
204
|
-
- \`-d, --debug\` \u2014 verbose logs on stderr.
|
|
205
|
-
|
|
206
|
-
## JSON envelope (\`--json\`)
|
|
207
|
-
|
|
208
|
-
\`\`\`json
|
|
209
|
-
{
|
|
210
|
-
"source": "https://example.com",
|
|
211
|
-
"loader": "fetch",
|
|
212
|
-
"url": "https://example.com",
|
|
213
|
-
"markdown": "# ...",
|
|
214
|
-
"metadata": { "title": "..." },
|
|
215
|
-
"engine": "browser",
|
|
216
|
-
"region": "auto",
|
|
217
|
-
"credits": 5,
|
|
218
|
-
"screenshotUrl": "https://...",
|
|
219
|
-
"output": "out.md"
|
|
220
|
-
}
|
|
221
|
-
\`\`\`
|
|
222
|
-
|
|
223
|
-
\`engine\`/\`region\`/\`credits\`/\`screenshotUrl\` appear only for the platform loader; \`output\`
|
|
224
|
-
only when \`-o\` also wrote a file.
|
|
225
|
-
|
|
226
|
-
## Hosted platform
|
|
227
|
-
|
|
228
|
-
For JavaScript-heavy or bot-protected pages, the hosted platform fetches server-side:
|
|
229
|
-
|
|
230
|
-
\`\`\`bash
|
|
231
|
-
export WEBFORAI_API_KEY=wfa_... # from https://platform.webforai.dev/dashboard
|
|
232
|
-
npx webforai https://example.com --engine browser --json
|
|
233
|
-
\`\`\`
|
|
234
|
-
|
|
235
|
-
\`WEBFORAI_PLATFORM_URL\` overrides the host for self-hosted deployments. Requests are metered
|
|
236
|
-
in credits (fetch 1, proxy-fetch 2, browser/proxy-browser 5); failures are never billed.
|
|
237
|
-
|
|
238
|
-
## Exit codes
|
|
239
|
-
|
|
240
|
-
- \`0\` \u2014 success.
|
|
241
|
-
- \`1\` \u2014 runtime failure (network, conversion, platform error; message on stderr).
|
|
242
|
-
- \`2\` \u2014 usage error (unknown flag or invalid value; message on stderr).
|
|
243
|
-
|
|
244
|
-
## Errors worth handling
|
|
245
|
-
|
|
246
|
-
- \`payment_required\` on stderr \u2192 platform credits exhausted.
|
|
247
|
-
- \`rate_limited\` \u2192 back off (a \`retryAfter\` seconds hint is included when known).
|
|
248
|
-
- Playwright loader errors explain the exact \`npx playwright install\` command to run.
|
|
249
|
-
`;
|
|
250
|
-
|
|
251
|
-
// src/cli/commands/skill/index.ts
|
|
252
|
-
var buildSkillsAddArgs = (source, flags) => {
|
|
253
|
-
const args = ["-y", "skills", "add", source];
|
|
254
|
-
if (flags.global) {
|
|
255
|
-
args.push("--global");
|
|
256
|
-
}
|
|
257
|
-
if (flags.agent) {
|
|
258
|
-
args.push("--agent", flags.agent);
|
|
259
|
-
}
|
|
260
|
-
if (flags.yes) {
|
|
261
|
-
args.push("-y");
|
|
262
|
-
}
|
|
263
|
-
if (flags.copy) {
|
|
264
|
-
args.push("--copy");
|
|
265
|
-
}
|
|
266
|
-
if (flags.all) {
|
|
267
|
-
args.push("--all");
|
|
268
|
-
}
|
|
269
|
-
return args;
|
|
270
|
-
};
|
|
271
|
-
var runSkillsAdd = (args) => new Promise((resolve, reject) => {
|
|
272
|
-
const child = spawn("npx", args, { stdio: ["inherit", "inherit", "inherit"] });
|
|
273
|
-
child.on("error", reject);
|
|
274
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
275
|
-
});
|
|
276
|
-
var skillCommand = async (flags) => {
|
|
277
|
-
const markdown = renderSkillMarkdown(package_default.version);
|
|
278
|
-
if (!(flags.install || flags.dir)) {
|
|
279
|
-
process.stdout.write(markdown);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (flags.dir) {
|
|
283
|
-
const target = path.join(flags.dir, "webforai", "SKILL.md");
|
|
284
|
-
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
285
|
-
await fs.writeFile(target, markdown);
|
|
286
|
-
console.error(`[webforai] Agent Skill written to ${target}`);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
const stage = await fs.mkdtemp(path.join(os.tmpdir(), "webforai-skill-"));
|
|
290
|
-
try {
|
|
291
|
-
await fs.mkdir(path.join(stage, "webforai"), { recursive: true });
|
|
292
|
-
await fs.writeFile(path.join(stage, "webforai", "SKILL.md"), markdown);
|
|
293
|
-
const args = buildSkillsAddArgs(stage, flags);
|
|
294
|
-
console.error(`[webforai] running: npx ${args.join(" ")}`);
|
|
295
|
-
const code = await runSkillsAdd(args).catch((error) => {
|
|
296
|
-
console.error(
|
|
297
|
-
`[webforai] could not launch the skills CLI (${error instanceof Error ? error.message : String(error)}).`
|
|
298
|
-
);
|
|
299
|
-
console.error("[webforai] fallback: `webforai skill --dir .claude/skills` writes the file directly.");
|
|
300
|
-
return 1;
|
|
301
|
-
});
|
|
302
|
-
if (code !== 0) {
|
|
303
|
-
process.exitCode = 1;
|
|
304
|
-
}
|
|
305
|
-
} finally {
|
|
306
|
-
await fs.rm(stage, { recursive: true, force: true });
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// src/cli/commands/webforai/interactive.ts
|
|
311
|
-
import { intro, log, outro, select as select7, spinner, text as text5 } from "@clack/prompts";
|
|
312
|
-
import pc2 from "picocolors";
|
|
313
|
-
|
|
314
168
|
// src/platform/error.ts
|
|
315
169
|
var PlatformApiError = class extends Error {
|
|
316
170
|
constructor(code, message, status, retryAfter) {
|
|
@@ -336,7 +190,9 @@ var resolveFetch = (custom) => {
|
|
|
336
190
|
|
|
337
191
|
// src/platform/types.ts
|
|
338
192
|
var ENGINES = ["fetch", "browser", "proxy-fetch", "proxy-browser"];
|
|
339
|
-
var
|
|
193
|
+
var REQUESTED_ENGINES = [...ENGINES, "auto"];
|
|
194
|
+
var REGIONS = ["auto", "jp"];
|
|
195
|
+
var CRAWL_SITEMAP_MODES = ["skip", "include", "only"];
|
|
340
196
|
var isStoredPageStub = (item) => item.status === "ok" && "resultUrl" in item;
|
|
341
197
|
|
|
342
198
|
// src/platform/client.ts
|
|
@@ -365,16 +221,30 @@ var parseRetryAfter = (body, response) => {
|
|
|
365
221
|
const header = Number(response.headers.get("retry-after"));
|
|
366
222
|
return Number.isFinite(header) && header > 0 ? header : void 0;
|
|
367
223
|
};
|
|
368
|
-
var
|
|
224
|
+
var dashboardHint = (status, dashboardUrl) => {
|
|
225
|
+
if (status === 401) {
|
|
226
|
+
return `create or check API keys at ${dashboardUrl}`;
|
|
227
|
+
}
|
|
228
|
+
if (status === 402) {
|
|
229
|
+
return `manage billing, credits and the spend cap at ${dashboardUrl}`;
|
|
230
|
+
}
|
|
231
|
+
return void 0;
|
|
232
|
+
};
|
|
233
|
+
var toApiError = (response, body, dashboardUrl) => {
|
|
369
234
|
const envelope = body;
|
|
370
235
|
const code = typeof envelope?.error?.code === "string" ? envelope.error.code : "invalid_response";
|
|
371
|
-
|
|
236
|
+
let message = typeof envelope?.error?.message === "string" ? envelope.error.message : `unexpected response (HTTP ${response.status})`;
|
|
237
|
+
const hint = code === "invalid_response" ? void 0 : dashboardHint(response.status, dashboardUrl);
|
|
238
|
+
if (hint && !message.includes("/dashboard")) {
|
|
239
|
+
message = `${message} (${hint})`;
|
|
240
|
+
}
|
|
372
241
|
return new PlatformApiError(code, message, response.status, parseRetryAfter(body, response));
|
|
373
242
|
};
|
|
374
243
|
var createPlatformClient = (options = {}) => {
|
|
375
244
|
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
245
|
+
const dashboardUrl = `${baseUrl}/dashboard`;
|
|
376
246
|
const fetchImpl = resolveFetch(options.fetch);
|
|
377
|
-
const request = async (
|
|
247
|
+
const request = async (path6, init) => {
|
|
378
248
|
const headers = { accept: "application/json" };
|
|
379
249
|
if (init.body !== void 0) {
|
|
380
250
|
headers["content-type"] = "application/json";
|
|
@@ -383,27 +253,28 @@ var createPlatformClient = (options = {}) => {
|
|
|
383
253
|
if (!options.apiKey) {
|
|
384
254
|
throw new PlatformApiError(
|
|
385
255
|
"missing_api_key",
|
|
386
|
-
|
|
256
|
+
`This endpoint needs an API key: pass \`apiKey\` to createPlatformClient (create one at ${dashboardUrl}; 1,000 free credits/month).`,
|
|
387
257
|
0
|
|
388
258
|
);
|
|
389
259
|
}
|
|
390
260
|
headers.authorization = `Bearer ${options.apiKey}`;
|
|
391
261
|
}
|
|
392
|
-
const response = await fetchImpl(`${baseUrl}${
|
|
262
|
+
const response = await fetchImpl(`${baseUrl}${path6}`, {
|
|
393
263
|
method: init.method,
|
|
394
264
|
headers,
|
|
395
|
-
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
265
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
|
|
266
|
+
...init.signal ? { signal: init.signal } : {}
|
|
396
267
|
});
|
|
397
268
|
const body = await response.json().catch(() => void 0);
|
|
398
269
|
if (!response.ok) {
|
|
399
|
-
throw toApiError(response, body);
|
|
270
|
+
throw toApiError(response, body, dashboardUrl);
|
|
400
271
|
}
|
|
401
272
|
if (body === void 0) {
|
|
402
273
|
throw new PlatformApiError("invalid_response", "response body is not JSON", response.status);
|
|
403
274
|
}
|
|
404
275
|
return body;
|
|
405
276
|
};
|
|
406
|
-
const getJob = (jobId) => request(`/v1/jobs/${encodeURIComponent(jobId)}`, { method: "GET", auth: true });
|
|
277
|
+
const getJob = (jobId, signal) => request(`/v1/jobs/${encodeURIComponent(jobId)}`, { method: "GET", auth: true, signal });
|
|
407
278
|
const getJobResults = (jobId, opts) => {
|
|
408
279
|
const query = opts?.cursor ? `?cursor=${encodeURIComponent(opts.cursor)}` : "";
|
|
409
280
|
return request(`/v1/jobs/${encodeURIComponent(jobId)}/results${query}`, { method: "GET", auth: true });
|
|
@@ -419,6 +290,17 @@ var createPlatformClient = (options = {}) => {
|
|
|
419
290
|
}
|
|
420
291
|
return await response.json();
|
|
421
292
|
};
|
|
293
|
+
const getJobOrBackOff = async (jobId, signal, interval) => {
|
|
294
|
+
try {
|
|
295
|
+
return await getJob(jobId, signal);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (!(error instanceof PlatformApiError && error.status === 429)) {
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
await sleep(error.retryAfter !== void 0 ? error.retryAfter * 1e3 : interval, signal);
|
|
301
|
+
return void 0;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
422
304
|
return {
|
|
423
305
|
scrape: (opts) => request("/v1/scrape", { method: "POST", body: opts, auth: true }),
|
|
424
306
|
scrapeAsync: (opts) => request("/v1/scrape", { method: "POST", body: { ...opts, async: true }, auth: true }),
|
|
@@ -429,17 +311,48 @@ var createPlatformClient = (options = {}) => {
|
|
|
429
311
|
waitForJob: async (jobId, opts = {}) => {
|
|
430
312
|
const interval = opts.pollIntervalMs ?? 2e3;
|
|
431
313
|
const timeout = opts.timeoutMs ?? 10 * 60 * 1e3;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
314
|
+
if (!Number.isFinite(interval) || interval < 0 || interval > 2147483647) {
|
|
315
|
+
throw new RangeError("pollIntervalMs must be between 0 and 2147483647");
|
|
316
|
+
}
|
|
317
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
|
|
318
|
+
throw new RangeError("timeoutMs must be greater than 0 and at most 2147483647");
|
|
319
|
+
}
|
|
320
|
+
opts.signal?.throwIfAborted();
|
|
321
|
+
const controller = new AbortController();
|
|
322
|
+
const { signal } = controller;
|
|
323
|
+
const onAbort = () => controller.abort(opts.signal?.reason);
|
|
324
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
325
|
+
const timer = setTimeout(
|
|
326
|
+
() => controller.abort(new PlatformApiError("poll_timeout", `job ${jobId} did not finish within ${timeout}ms`, 0)),
|
|
327
|
+
timeout
|
|
328
|
+
);
|
|
329
|
+
let rejectOnAbort = () => {
|
|
330
|
+
};
|
|
331
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
332
|
+
rejectOnAbort = () => reject(signal.reason);
|
|
333
|
+
signal.addEventListener("abort", rejectOnAbort, { once: true });
|
|
334
|
+
});
|
|
335
|
+
const poll = async () => {
|
|
336
|
+
for (; ; ) {
|
|
337
|
+
signal.throwIfAborted();
|
|
338
|
+
const status = await getJobOrBackOff(jobId, signal, interval);
|
|
339
|
+
signal.throwIfAborted();
|
|
340
|
+
if (status === void 0) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
opts.onStatus?.(status);
|
|
344
|
+
if (TERMINAL_STATES.has(status.status)) {
|
|
345
|
+
return status;
|
|
346
|
+
}
|
|
347
|
+
await sleep(interval, signal);
|
|
441
348
|
}
|
|
442
|
-
|
|
349
|
+
};
|
|
350
|
+
try {
|
|
351
|
+
return await Promise.race([poll(), aborted]);
|
|
352
|
+
} finally {
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
355
|
+
signal.removeEventListener("abort", rejectOnAbort);
|
|
443
356
|
}
|
|
444
357
|
},
|
|
445
358
|
jobResults: async function* (jobId) {
|
|
@@ -456,6 +369,9 @@ var createPlatformClient = (options = {}) => {
|
|
|
456
369
|
};
|
|
457
370
|
};
|
|
458
371
|
|
|
372
|
+
// src/cli/commands/jobs/options.ts
|
|
373
|
+
import fs3 from "node:fs";
|
|
374
|
+
|
|
459
375
|
// src/cli/constants.ts
|
|
460
376
|
var DEFAULT_PATH = "https://example.com";
|
|
461
377
|
var LOADERS = ["fetch", "playwright", "platform"];
|
|
@@ -464,6 +380,850 @@ var EXTRACTORS = ["auto", "takumi", "minimal", "none"];
|
|
|
464
380
|
var API_KEY_ENV = "WEBFORAI_API_KEY";
|
|
465
381
|
var PLATFORM_URL_ENV = "WEBFORAI_PLATFORM_URL";
|
|
466
382
|
|
|
383
|
+
// src/cli/commands/webforai/options.ts
|
|
384
|
+
import fs2 from "node:fs";
|
|
385
|
+
|
|
386
|
+
// src/cli/utils.ts
|
|
387
|
+
import fs from "node:fs";
|
|
388
|
+
import path from "node:path";
|
|
389
|
+
var isUrl = (maybeUrl) => {
|
|
390
|
+
try {
|
|
391
|
+
new URL(maybeUrl);
|
|
392
|
+
return true;
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
function changeFileExtension(filePath, newExtension) {
|
|
398
|
+
const parsedPath = filePath.split("/");
|
|
399
|
+
const fileName = parsedPath[parsedPath.length - 1];
|
|
400
|
+
const formattedNewExtension = newExtension.startsWith(".") ? newExtension : `.${newExtension}`;
|
|
401
|
+
if (fileName.startsWith(".")) {
|
|
402
|
+
const parts = fileName.split(".");
|
|
403
|
+
if (parts.length === 2) {
|
|
404
|
+
return parsedPath.slice(0, -1).concat(`${fileName}${formattedNewExtension}`).join("/");
|
|
405
|
+
}
|
|
406
|
+
parts[parts.length - 1] = newExtension.replace(/^\./, "");
|
|
407
|
+
return parsedPath.slice(0, -1).concat(parts.join(".")).join("/");
|
|
408
|
+
}
|
|
409
|
+
const lastDotIndex = fileName.lastIndexOf(".");
|
|
410
|
+
const baseName = lastDotIndex !== -1 ? fileName.slice(0, lastDotIndex) : fileName;
|
|
411
|
+
const newFileName = `${baseName}${formattedNewExtension}`;
|
|
412
|
+
parsedPath[parsedPath.length - 1] = newFileName;
|
|
413
|
+
return parsedPath.join("/");
|
|
414
|
+
}
|
|
415
|
+
function urlToFilename(url) {
|
|
416
|
+
try {
|
|
417
|
+
const urlObj = new URL(url);
|
|
418
|
+
const domainParts = urlObj.hostname.split(".").reverse().reduce((acc, part, index) => {
|
|
419
|
+
if (index === 0) {
|
|
420
|
+
return acc;
|
|
421
|
+
}
|
|
422
|
+
if (acc.length >= 2) {
|
|
423
|
+
return acc;
|
|
424
|
+
}
|
|
425
|
+
if (part === "www") {
|
|
426
|
+
return acc;
|
|
427
|
+
}
|
|
428
|
+
return [part, ...acc];
|
|
429
|
+
}, []);
|
|
430
|
+
const domainString = domainParts.reverse().join("-");
|
|
431
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
432
|
+
const relevantPathParts = pathParts.slice(-2);
|
|
433
|
+
const pathString = relevantPathParts.map((part) => decodeURIComponent(part)).join("-");
|
|
434
|
+
let filename = [domainString, pathString].filter(Boolean).join("-");
|
|
435
|
+
filename = filename.toLowerCase().replace(/[<>:"/\\|?*\x00-\x1F]/g, "").replace(/[\s.]+/g, "-").replace(/^-+|-+$/g, "");
|
|
436
|
+
return filename || "output";
|
|
437
|
+
} catch {
|
|
438
|
+
return "output";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
var sourcePathToOutputPath = (sourcePath) => {
|
|
442
|
+
return isUrl(sourcePath) ? `${urlToFilename(sourcePath)}.md` : changeFileExtension(sourcePath, "md");
|
|
443
|
+
};
|
|
444
|
+
function getNextAvailableFilePath(filePath, exists = fs.existsSync) {
|
|
445
|
+
const parsedPath = path.parse(filePath);
|
|
446
|
+
const directory = parsedPath.dir;
|
|
447
|
+
const fullName = parsedPath.base;
|
|
448
|
+
const [firstPart, ...restParts] = fullName.split(".");
|
|
449
|
+
const restName = restParts.length > 0 ? `.${restParts.join(".")}` : "";
|
|
450
|
+
const baseName = firstPart.replace(/_\d+$/, "");
|
|
451
|
+
const match = firstPart.match(/_(\d+)$/);
|
|
452
|
+
let counter = match ? Number.parseInt(match[1], 10) + 1 : 1;
|
|
453
|
+
let nextFilePath = filePath;
|
|
454
|
+
while (exists(nextFilePath)) {
|
|
455
|
+
const newName = `${baseName}_${counter}${restName}`;
|
|
456
|
+
nextFilePath = path.join(directory, newName);
|
|
457
|
+
counter++;
|
|
458
|
+
}
|
|
459
|
+
return nextFilePath;
|
|
460
|
+
}
|
|
461
|
+
var platformDashboardUrl = (platformUrl) => `${(platformUrl || DEFAULT_BASE_URL).replace(/\/+$/u, "")}/dashboard`;
|
|
462
|
+
|
|
463
|
+
// src/cli/commands/webforai/options.ts
|
|
464
|
+
var UsageError = class extends Error {
|
|
465
|
+
};
|
|
466
|
+
var oneOf = (name, value, allowed) => {
|
|
467
|
+
if (!allowed.includes(value)) {
|
|
468
|
+
throw new UsageError(`invalid --${name} "${value}" (expected: ${allowed.join(" | ")})`);
|
|
469
|
+
}
|
|
470
|
+
return value;
|
|
471
|
+
};
|
|
472
|
+
var impliesPlatform = (flags) => flags.engine !== void 0 || flags.region !== void 0;
|
|
473
|
+
var resolveLoader = (sourceIsUrl, flags) => {
|
|
474
|
+
if (flags.loader) {
|
|
475
|
+
const loader = oneOf("loader", flags.loader, LOADERS);
|
|
476
|
+
if (impliesPlatform(flags) && loader !== "platform") {
|
|
477
|
+
throw new UsageError(`--engine/--region require --loader platform (received --loader ${loader})`);
|
|
478
|
+
}
|
|
479
|
+
return loader;
|
|
480
|
+
}
|
|
481
|
+
if (impliesPlatform(flags)) {
|
|
482
|
+
return "platform";
|
|
483
|
+
}
|
|
484
|
+
return sourceIsUrl ? "fetch" : "local";
|
|
485
|
+
};
|
|
486
|
+
var assertUsableLocalSource = (source, loader, flags) => {
|
|
487
|
+
if (loader !== "local" && flags.loader) {
|
|
488
|
+
throw new UsageError(`--loader ${loader} needs a URL source, but "${source}" is a local path`);
|
|
489
|
+
}
|
|
490
|
+
if (impliesPlatform(flags)) {
|
|
491
|
+
throw new UsageError("--engine/--region need a URL source (the platform cannot read local files)");
|
|
492
|
+
}
|
|
493
|
+
if (!fs2.existsSync(source)) {
|
|
494
|
+
throw new UsageError(`source not found: "${source}" (URLs must start with http:// or https://)`);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
var applyPlatformFields = (resolved, flags, env) => {
|
|
498
|
+
resolved.engine = flags.engine ? oneOf("engine", flags.engine, REQUESTED_ENGINES) : void 0;
|
|
499
|
+
resolved.region = flags.region ? oneOf("region", flags.region, REGIONS) : void 0;
|
|
500
|
+
resolved.apiKey = flags.apiKey ?? env[API_KEY_ENV];
|
|
501
|
+
resolved.platformUrl = flags.platformUrl ?? env[PLATFORM_URL_ENV];
|
|
502
|
+
if (!resolved.apiKey) {
|
|
503
|
+
throw missingApiKeyError(resolved.platformUrl);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
var missingApiKeyError = (platformUrl) => new UsageError(
|
|
507
|
+
`the platform loader needs an API key: pass --api-key or set ${API_KEY_ENV} (create one at ${platformDashboardUrl(
|
|
508
|
+
platformUrl
|
|
509
|
+
)}; 1,000 free credits/month, no card)`
|
|
510
|
+
);
|
|
511
|
+
var resolveRunOptions = (source, flags, env = process.env) => {
|
|
512
|
+
const sourceIsUrl = isUrl(source);
|
|
513
|
+
const loader = resolveLoader(sourceIsUrl, flags);
|
|
514
|
+
if (!sourceIsUrl) {
|
|
515
|
+
assertUsableLocalSource(source, loader, flags);
|
|
516
|
+
}
|
|
517
|
+
const resolved = {
|
|
518
|
+
source,
|
|
519
|
+
loader: sourceIsUrl ? loader : "local",
|
|
520
|
+
mode: flags.mode ? oneOf("mode", flags.mode, MODES) : "default",
|
|
521
|
+
extractor: flags.extractor ? oneOf("extractor", flags.extractor, EXTRACTORS) : "auto",
|
|
522
|
+
frontmatter: flags.frontmatter ?? false,
|
|
523
|
+
output: flags.output,
|
|
524
|
+
json: flags.json ?? false,
|
|
525
|
+
debug: flags.debug ?? false,
|
|
526
|
+
screenshot: flags.screenshot ?? false,
|
|
527
|
+
respectRobotsTxt: flags.respectRobots ?? false
|
|
528
|
+
};
|
|
529
|
+
if (resolved.loader === "platform") {
|
|
530
|
+
applyPlatformFields(resolved, flags, env);
|
|
531
|
+
} else if (flags.screenshot) {
|
|
532
|
+
throw new UsageError("--screenshot is only available with the platform loader (browser engines)");
|
|
533
|
+
} else if (flags.respectRobots) {
|
|
534
|
+
throw new UsageError("--respect-robots is only available with the platform loader");
|
|
535
|
+
}
|
|
536
|
+
return resolved;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/cli/commands/jobs/options.ts
|
|
540
|
+
var MAX_BATCH_URLS = 100;
|
|
541
|
+
var MAX_CRAWL_DEPTH = 5;
|
|
542
|
+
var MAX_CRAWL_LIMIT = 500;
|
|
543
|
+
var DEFAULT_TIMEOUT_SECONDS = 30 * 60;
|
|
544
|
+
var isHttpUrl = (value) => {
|
|
545
|
+
try {
|
|
546
|
+
const { protocol } = new URL(value);
|
|
547
|
+
return protocol === "http:" || protocol === "https:";
|
|
548
|
+
} catch {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
var oneOf2 = (name, value, allowed) => {
|
|
553
|
+
if (!allowed.includes(value)) {
|
|
554
|
+
throw new UsageError(`invalid --${name} "${value}" (expected: ${allowed.join(" | ")})`);
|
|
555
|
+
}
|
|
556
|
+
return value;
|
|
557
|
+
};
|
|
558
|
+
var integerIn = (name, value, min, max) => {
|
|
559
|
+
if (value === void 0) {
|
|
560
|
+
return void 0;
|
|
561
|
+
}
|
|
562
|
+
const parsed = Number(value);
|
|
563
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
564
|
+
throw new UsageError(`invalid --${name} "${value}" (expected an integer from ${min} to ${max})`);
|
|
565
|
+
}
|
|
566
|
+
return parsed;
|
|
567
|
+
};
|
|
568
|
+
var patterns = (name, values) => {
|
|
569
|
+
if (!values || values.length === 0) {
|
|
570
|
+
return void 0;
|
|
571
|
+
}
|
|
572
|
+
for (const value of values) {
|
|
573
|
+
try {
|
|
574
|
+
new RegExp(value);
|
|
575
|
+
} catch {
|
|
576
|
+
throw new UsageError(`invalid --${name} pattern "${value}" (expected a regular expression)`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return values;
|
|
580
|
+
};
|
|
581
|
+
var parseUrlList = (text6) => text6.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#"));
|
|
582
|
+
var readUrlFile = (file, readFile) => {
|
|
583
|
+
try {
|
|
584
|
+
return parseUrlList(readFile(file));
|
|
585
|
+
} catch (error) {
|
|
586
|
+
throw new UsageError(`cannot read --file "${file}": ${error instanceof Error ? error.message : String(error)}`);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
var defaultReadFile = (file) => fs3.readFileSync(file === "-" ? 0 : file, "utf-8");
|
|
590
|
+
var resolveJobOptions = (kind, sources, flags, env = process.env, readFile = defaultReadFile) => {
|
|
591
|
+
const fromFile = kind === "batch" && flags.file ? readUrlFile(flags.file, readFile) : [];
|
|
592
|
+
const urls = [.../* @__PURE__ */ new Set([...sources, ...fromFile])];
|
|
593
|
+
if (kind === "crawl" && urls.length !== 1) {
|
|
594
|
+
throw new UsageError("crawl takes exactly one seed URL");
|
|
595
|
+
}
|
|
596
|
+
if (urls.length === 0) {
|
|
597
|
+
throw new UsageError("batch needs at least one URL (as arguments or via --file <path|->)");
|
|
598
|
+
}
|
|
599
|
+
if (urls.length > MAX_BATCH_URLS) {
|
|
600
|
+
throw new UsageError(
|
|
601
|
+
`batch takes at most ${MAX_BATCH_URLS} URLs per job (received ${urls.length}); split the list`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
const invalid = urls.find((url) => !isHttpUrl(url));
|
|
605
|
+
if (invalid !== void 0) {
|
|
606
|
+
throw new UsageError(`not an http(s) URL: "${invalid}"`);
|
|
607
|
+
}
|
|
608
|
+
if (!flags.output) {
|
|
609
|
+
throw new UsageError(`${kind} writes one Markdown file per page: pass -o, --output <dir>`);
|
|
610
|
+
}
|
|
611
|
+
const apiKey = flags.apiKey ?? env[API_KEY_ENV];
|
|
612
|
+
const platformUrl = flags.platformUrl ?? env[PLATFORM_URL_ENV];
|
|
613
|
+
if (!apiKey) {
|
|
614
|
+
throw missingApiKeyError(platformUrl);
|
|
615
|
+
}
|
|
616
|
+
const timeoutSeconds = integerIn("timeout", flags.timeout, 1, 24 * 60 * 60) ?? DEFAULT_TIMEOUT_SECONDS;
|
|
617
|
+
return {
|
|
618
|
+
kind,
|
|
619
|
+
urls,
|
|
620
|
+
outputDir: flags.output,
|
|
621
|
+
engine: flags.engine ? oneOf2("engine", flags.engine, REQUESTED_ENGINES) : void 0,
|
|
622
|
+
region: flags.region ? oneOf2("region", flags.region, REGIONS) : void 0,
|
|
623
|
+
extractor: flags.extractor ? oneOf2("extractor", flags.extractor, EXTRACTORS) : "auto",
|
|
624
|
+
frontmatter: flags.frontmatter ?? false,
|
|
625
|
+
respectRobotsTxt: flags.respectRobots,
|
|
626
|
+
apiKey,
|
|
627
|
+
platformUrl,
|
|
628
|
+
json: flags.json ?? false,
|
|
629
|
+
debug: flags.debug ?? false,
|
|
630
|
+
timeoutMs: timeoutSeconds * 1e3,
|
|
631
|
+
maxDepth: integerIn("max-depth", flags.maxDepth, 0, MAX_CRAWL_DEPTH),
|
|
632
|
+
limit: integerIn("limit", flags.limit, 1, MAX_CRAWL_LIMIT),
|
|
633
|
+
includePaths: patterns("include", flags.include),
|
|
634
|
+
excludePaths: patterns("exclude", flags.exclude),
|
|
635
|
+
sitemap: flags.sitemap ? oneOf2("sitemap", flags.sitemap, CRAWL_SITEMAP_MODES) : void 0,
|
|
636
|
+
llmsTxt: flags.llmsTxt ?? false
|
|
637
|
+
};
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// src/cli/commands/jobs/run.ts
|
|
641
|
+
import fs4 from "node:fs/promises";
|
|
642
|
+
import path3 from "node:path";
|
|
643
|
+
|
|
644
|
+
// src/cli/commands/jobs/files.ts
|
|
645
|
+
import { createHash } from "node:crypto";
|
|
646
|
+
import path2 from "node:path";
|
|
647
|
+
var MAX_SEGMENT_LENGTH = 80;
|
|
648
|
+
var MAX_DEPTH = 12;
|
|
649
|
+
var PAGE_EXTENSION = /\.(?:html?|php|aspx?|jsp|md)$/iu;
|
|
650
|
+
var shortHash = (value) => createHash("sha256").update(value).digest("hex").slice(0, 8);
|
|
651
|
+
var decodeSegment = (segment) => {
|
|
652
|
+
try {
|
|
653
|
+
return decodeURIComponent(segment);
|
|
654
|
+
} catch {
|
|
655
|
+
return segment;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
var sanitizeSegment = (segment) => {
|
|
659
|
+
const cleaned = decodeSegment(segment).normalize("NFC").replace(/[<>:"/\\|?*\u0000-\u001f\u007f]+/gu, "-").replace(/\s+/gu, "-").replace(/-{2,}/gu, "-").replace(/^[-.]+|[-.]+$/gu, "");
|
|
660
|
+
if (cleaned.length <= MAX_SEGMENT_LENGTH) {
|
|
661
|
+
return cleaned;
|
|
662
|
+
}
|
|
663
|
+
return `${cleaned.slice(0, MAX_SEGMENT_LENGTH - 9)}-${shortHash(cleaned)}`;
|
|
664
|
+
};
|
|
665
|
+
var pageRelativePath = (pageUrl, options = {}) => {
|
|
666
|
+
const url = new URL(pageUrl);
|
|
667
|
+
const rawSegments = url.pathname.split("/");
|
|
668
|
+
const trailingSlash = url.pathname.endsWith("/");
|
|
669
|
+
const segments = rawSegments.map(sanitizeSegment).filter(Boolean).slice(0, MAX_DEPTH);
|
|
670
|
+
let leaf = trailingSlash || segments.length === 0 ? "index" : segments.pop() ?? "index";
|
|
671
|
+
leaf = leaf.replace(PAGE_EXTENSION, "") || "index";
|
|
672
|
+
if (url.search.length > 1) {
|
|
673
|
+
leaf = `${leaf}_q-${shortHash(url.search)}`;
|
|
674
|
+
}
|
|
675
|
+
const directories = options.includeHost ? [sanitizeSegment(url.host) || "host", ...segments] : segments;
|
|
676
|
+
return path2.posix.join(...directories, `${leaf}.md`);
|
|
677
|
+
};
|
|
678
|
+
var planOutputFiles = (urls, options = {}) => {
|
|
679
|
+
const taken = /* @__PURE__ */ new Set();
|
|
680
|
+
const sorted = [...new Set(urls)].sort();
|
|
681
|
+
return sorted.map((url) => {
|
|
682
|
+
const candidate = pageRelativePath(url, options);
|
|
683
|
+
const relativePath = getNextAvailableFilePath(candidate, (name) => taken.has(name.toLowerCase()));
|
|
684
|
+
taken.add(relativePath.toLowerCase());
|
|
685
|
+
return { url, relativePath };
|
|
686
|
+
});
|
|
687
|
+
};
|
|
688
|
+
var resolveInside = (outputDir, relativePath) => {
|
|
689
|
+
const root = path2.resolve(outputDir);
|
|
690
|
+
const target = path2.resolve(root, ...relativePath.split("/"));
|
|
691
|
+
if (target !== root && !target.startsWith(`${root}${path2.sep}`)) {
|
|
692
|
+
throw new Error(`refusing to write outside the output directory: ${relativePath}`);
|
|
693
|
+
}
|
|
694
|
+
return target;
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
// src/cli/commands/jobs/llms-txt.ts
|
|
698
|
+
var str = (value) => typeof value === "string" && value.trim() !== "" ? value.trim().replace(/\s+/gu, " ") : void 0;
|
|
699
|
+
var firstHeading = (markdown) => {
|
|
700
|
+
const match = /^#{1,2}[ \t]+(.+?)[ \t#]*$/mu.exec(markdown);
|
|
701
|
+
return match ? str(match[1]) : void 0;
|
|
702
|
+
};
|
|
703
|
+
var escapeLinkText = (text6) => text6.replace(/([\\[\]])/gu, "\\$1");
|
|
704
|
+
var pageTitle = (page) => str(page.metadata.title) ?? firstHeading(page.markdown) ?? (new URL(page.url).pathname || page.url);
|
|
705
|
+
var normalizeUrl = (value) => {
|
|
706
|
+
const url = new URL(value);
|
|
707
|
+
url.hash = "";
|
|
708
|
+
return url.href;
|
|
709
|
+
};
|
|
710
|
+
var findSeed = (site) => {
|
|
711
|
+
const seed = normalizeUrl(site.seedUrl);
|
|
712
|
+
return site.pages.find((page) => normalizeUrl(page.url) === seed);
|
|
713
|
+
};
|
|
714
|
+
var siteTitle = (site, seed) => str(seed?.metadata.siteName) ?? str(site.pages.find((page) => str(page.metadata.siteName))?.metadata.siteName) ?? str(seed?.metadata.title) ?? new URL(site.seedUrl).host;
|
|
715
|
+
var sectionOf = (pageUrl) => {
|
|
716
|
+
const { pathname } = new URL(pageUrl);
|
|
717
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
718
|
+
const first = segments[0];
|
|
719
|
+
if (!first || segments.length === 1 && !pathname.endsWith("/")) {
|
|
720
|
+
return "";
|
|
721
|
+
}
|
|
722
|
+
let decoded = first;
|
|
723
|
+
try {
|
|
724
|
+
decoded = decodeURIComponent(first);
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
const words = decoded.replace(/[-_]+/gu, " ").trim();
|
|
728
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
729
|
+
};
|
|
730
|
+
var orderPages = (pages) => {
|
|
731
|
+
const groups = /* @__PURE__ */ new Map();
|
|
732
|
+
for (const page of [...pages].sort((a, b) => a.url < b.url ? -1 : a.url > b.url ? 1 : 0)) {
|
|
733
|
+
const section = sectionOf(page.url);
|
|
734
|
+
groups.set(section, [...groups.get(section) ?? [], page]);
|
|
735
|
+
}
|
|
736
|
+
return [...groups.entries()].sort(([a], [b]) => a === "" ? -1 : b === "" ? 1 : a.localeCompare(b)).map(([section, grouped]) => ({ section, pages: grouped }));
|
|
737
|
+
};
|
|
738
|
+
var renderLlmsTxt = (site) => {
|
|
739
|
+
const seed = findSeed(site);
|
|
740
|
+
const lines = [`# ${siteTitle(site, seed)}`, ""];
|
|
741
|
+
const summary = str(seed?.metadata.description);
|
|
742
|
+
if (summary) {
|
|
743
|
+
lines.push(`> ${summary}`, "");
|
|
744
|
+
}
|
|
745
|
+
for (const { section, pages } of orderPages(site.pages)) {
|
|
746
|
+
lines.push(`## ${section || "Pages"}`, "");
|
|
747
|
+
for (const page of pages) {
|
|
748
|
+
const description = str(page.metadata.description);
|
|
749
|
+
const link = `- [${escapeLinkText(pageTitle(page))}](${page.url})`;
|
|
750
|
+
lines.push(description ? `${link}: ${description}` : link);
|
|
751
|
+
}
|
|
752
|
+
lines.push("");
|
|
753
|
+
}
|
|
754
|
+
return `${lines.join("\n").trimEnd()}
|
|
755
|
+
`;
|
|
756
|
+
};
|
|
757
|
+
var renderLlmsFullTxt = (site) => {
|
|
758
|
+
const seed = findSeed(site);
|
|
759
|
+
const parts = [`# ${siteTitle(site, seed)}`];
|
|
760
|
+
for (const { pages } of orderPages(site.pages)) {
|
|
761
|
+
for (const page of pages) {
|
|
762
|
+
const body = page.markdown.trim();
|
|
763
|
+
const heading2 = /^#[ \t]/u.test(body) ? "" : `# ${pageTitle(page)}
|
|
764
|
+
|
|
765
|
+
`;
|
|
766
|
+
parts.push(`${heading2}Source: ${page.url}
|
|
767
|
+
|
|
768
|
+
${body}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return `${parts.join("\n\n---\n\n")}
|
|
772
|
+
`;
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// src/cli/commands/jobs/run.ts
|
|
776
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
777
|
+
var MAX_RATE_LIMIT_RETRIES = 5;
|
|
778
|
+
var MAX_LISTED_FAILURES = 20;
|
|
779
|
+
var defaultIo = () => ({
|
|
780
|
+
stdout: (text6) => process.stdout.write(text6),
|
|
781
|
+
stderr: (text6) => process.stderr.write(text6),
|
|
782
|
+
isTTY: Boolean(process.stderr.isTTY),
|
|
783
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
784
|
+
fetch: globalThis.fetch.bind(globalThis)
|
|
785
|
+
});
|
|
786
|
+
var JobIncompleteError = class extends Error {
|
|
787
|
+
};
|
|
788
|
+
var progressLine = (kind, status) => {
|
|
789
|
+
const total = status.total > 0 ? `/${status.total}` : "";
|
|
790
|
+
const failed = status.failed > 0 ? ` \xB7 ${status.failed} failed` : "";
|
|
791
|
+
return `${kind} ${status.jobId} \xB7 ${status.status} \xB7 ${status.completed}${total} pages${failed} \xB7 ${status.credits} credits`;
|
|
792
|
+
};
|
|
793
|
+
var withRateLimitRetry = async (io, log2, call) => {
|
|
794
|
+
for (let attempt = 1; ; attempt++) {
|
|
795
|
+
try {
|
|
796
|
+
return await call();
|
|
797
|
+
} catch (error) {
|
|
798
|
+
if (!(error instanceof PlatformApiError) || error.code !== "rate_limited" || attempt > MAX_RATE_LIMIT_RETRIES) {
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
const seconds = Math.min(error.retryAfter ?? 2 ** attempt, 120);
|
|
802
|
+
log2(`rate limited; retrying in ${seconds}s`);
|
|
803
|
+
await io.sleep(seconds * 1e3);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
var submit = (client, job) => {
|
|
808
|
+
const common = {
|
|
809
|
+
engine: job.engine,
|
|
810
|
+
region: job.region,
|
|
811
|
+
respectRobotsTxt: job.respectRobotsTxt,
|
|
812
|
+
convert: { extractor: job.extractor, frontmatter: job.frontmatter || void 0 }
|
|
813
|
+
};
|
|
814
|
+
if (job.kind === "batch") {
|
|
815
|
+
return client.batch({ urls: job.urls, ...common });
|
|
816
|
+
}
|
|
817
|
+
return client.crawl({
|
|
818
|
+
url: job.urls[0],
|
|
819
|
+
maxDepth: job.maxDepth,
|
|
820
|
+
limit: job.limit,
|
|
821
|
+
includePaths: job.includePaths,
|
|
822
|
+
excludePaths: job.excludePaths,
|
|
823
|
+
sitemap: job.sitemap,
|
|
824
|
+
...common
|
|
825
|
+
});
|
|
826
|
+
};
|
|
827
|
+
var resolveItem = async (io, item) => {
|
|
828
|
+
if (!isStoredPageStub(item)) {
|
|
829
|
+
return item;
|
|
830
|
+
}
|
|
831
|
+
const response = await io.fetch(item.resultUrl);
|
|
832
|
+
if (!response.ok) {
|
|
833
|
+
throw new PlatformApiError(
|
|
834
|
+
"artifact_fetch_failed",
|
|
835
|
+
`stored result for ${item.url} could not be downloaded (HTTP ${response.status})`,
|
|
836
|
+
response.status
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
return await response.json();
|
|
840
|
+
};
|
|
841
|
+
var createReporter = (io, job) => {
|
|
842
|
+
let liveLine = false;
|
|
843
|
+
const clearLive = () => {
|
|
844
|
+
if (liveLine) {
|
|
845
|
+
io.stderr("\r\x1B[2K");
|
|
846
|
+
liveLine = false;
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
const log2 = (line) => {
|
|
850
|
+
clearLive();
|
|
851
|
+
io.stderr(`[webforai] ${line}
|
|
852
|
+
`);
|
|
853
|
+
};
|
|
854
|
+
const status = (current) => {
|
|
855
|
+
if (io.isTTY) {
|
|
856
|
+
io.stderr(`\r\x1B[2K[webforai] ${progressLine(job.kind, current)}`);
|
|
857
|
+
liveLine = true;
|
|
858
|
+
} else if (job.debug) {
|
|
859
|
+
log2(progressLine(job.kind, current));
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
return { log: log2, status, clearLive };
|
|
863
|
+
};
|
|
864
|
+
var waitWithProgress = async (client, job, jobId, io, reporter) => {
|
|
865
|
+
const deadline = Date.now() + job.timeoutMs;
|
|
866
|
+
try {
|
|
867
|
+
return await withRateLimitRetry(
|
|
868
|
+
io,
|
|
869
|
+
reporter.log,
|
|
870
|
+
() => client.waitForJob(jobId, {
|
|
871
|
+
pollIntervalMs: POLL_INTERVAL_MS,
|
|
872
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
873
|
+
onStatus: reporter.status
|
|
874
|
+
})
|
|
875
|
+
);
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (error instanceof PlatformApiError && error.code === "poll_timeout") {
|
|
878
|
+
throw new PlatformApiError(
|
|
879
|
+
"poll_timeout",
|
|
880
|
+
`${error.message}; the job keeps running on the platform and its results stay available for 7 days (GET /v1/jobs/${jobId}/results)`,
|
|
881
|
+
0
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
throw error;
|
|
885
|
+
} finally {
|
|
886
|
+
reporter.clearLive();
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
var collectResults = async (client, jobId, io, reporter) => {
|
|
890
|
+
const successes = [];
|
|
891
|
+
const failures = [];
|
|
892
|
+
let cursor;
|
|
893
|
+
do {
|
|
894
|
+
const page = await withRateLimitRetry(io, reporter.log, () => client.getJobResults(jobId, { cursor }));
|
|
895
|
+
for (const item of page.results) {
|
|
896
|
+
const result = await resolveItem(io, item);
|
|
897
|
+
if (result.status === "ok") {
|
|
898
|
+
successes.push(result);
|
|
899
|
+
} else {
|
|
900
|
+
failures.push(result);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
cursor = page.cursor;
|
|
904
|
+
} while (cursor);
|
|
905
|
+
return { successes, failures };
|
|
906
|
+
};
|
|
907
|
+
var writePages = async (job, successes) => {
|
|
908
|
+
const byUrl = new Map(successes.map((page) => [page.url, page]));
|
|
909
|
+
const written = [];
|
|
910
|
+
await fs4.mkdir(job.outputDir, { recursive: true });
|
|
911
|
+
for (const { url, relativePath } of planOutputFiles([...byUrl.keys()], { includeHost: job.kind === "batch" })) {
|
|
912
|
+
const page = byUrl.get(url);
|
|
913
|
+
const target = resolveInside(job.outputDir, relativePath);
|
|
914
|
+
await fs4.mkdir(path3.dirname(target), { recursive: true });
|
|
915
|
+
await fs4.writeFile(target, page.markdown.endsWith("\n") ? page.markdown : `${page.markdown}
|
|
916
|
+
`);
|
|
917
|
+
written.push({
|
|
918
|
+
url,
|
|
919
|
+
file: path3.join(job.outputDir, relativePath),
|
|
920
|
+
title: typeof page.metadata.title === "string" ? page.metadata.title : void 0,
|
|
921
|
+
engine: page.engine,
|
|
922
|
+
credits: page.credits,
|
|
923
|
+
warning: page.warning
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
return written;
|
|
927
|
+
};
|
|
928
|
+
var writeLlmsFiles = async (job, successes, envelope) => {
|
|
929
|
+
const pages = [...new Map(successes.map((page) => [page.url, page])).values()];
|
|
930
|
+
const site = { seedUrl: job.urls[0], pages };
|
|
931
|
+
envelope.llmsTxt = path3.join(job.outputDir, "llms.txt");
|
|
932
|
+
envelope.llmsFullTxt = path3.join(job.outputDir, "llms-full.txt");
|
|
933
|
+
await fs4.writeFile(envelope.llmsTxt, renderLlmsTxt(site));
|
|
934
|
+
await fs4.writeFile(envelope.llmsFullTxt, renderLlmsFullTxt(site));
|
|
935
|
+
};
|
|
936
|
+
var report = (job, envelope, io, log2) => {
|
|
937
|
+
if (job.json) {
|
|
938
|
+
io.stdout(`${JSON.stringify(envelope, null, 2)}
|
|
939
|
+
`);
|
|
940
|
+
} else {
|
|
941
|
+
for (const page of envelope.pages) {
|
|
942
|
+
io.stdout(`${page.file}
|
|
943
|
+
`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const failed = envelope.failures.length;
|
|
947
|
+
log2(
|
|
948
|
+
`${job.kind} ${envelope.jobId} ${envelope.status}: ${envelope.pages.length} page(s) written to ${job.outputDir}${failed > 0 ? `, ${failed} failed` : ""} \xB7 ${envelope.credits} credits`
|
|
949
|
+
);
|
|
950
|
+
if (envelope.llmsTxt) {
|
|
951
|
+
log2(`llms.txt and llms-full.txt written to ${job.outputDir}`);
|
|
952
|
+
}
|
|
953
|
+
for (const failure of envelope.failures.slice(0, MAX_LISTED_FAILURES)) {
|
|
954
|
+
log2(` failed ${failure.url} \u2014 ${failure.code}: ${failure.message}`);
|
|
955
|
+
}
|
|
956
|
+
if (failed > MAX_LISTED_FAILURES) {
|
|
957
|
+
log2(` \u2026 and ${failed - MAX_LISTED_FAILURES} more (see --json)`);
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
var runJobCommand = async (job, io = defaultIo()) => {
|
|
961
|
+
const client = createPlatformClient({ apiKey: job.apiKey, baseUrl: job.platformUrl, fetch: io.fetch });
|
|
962
|
+
const reporter = createReporter(io, job);
|
|
963
|
+
const { jobId } = await withRateLimitRetry(io, reporter.log, () => submit(client, job));
|
|
964
|
+
reporter.log(
|
|
965
|
+
`${job.kind} job ${jobId} submitted (${job.kind === "crawl" ? job.urls[0] : `${job.urls.length} URLs`})`
|
|
966
|
+
);
|
|
967
|
+
const final = await waitWithProgress(client, job, jobId, io, reporter);
|
|
968
|
+
const { successes, failures } = await collectResults(client, jobId, io, reporter);
|
|
969
|
+
const envelope = {
|
|
970
|
+
jobId,
|
|
971
|
+
type: job.kind,
|
|
972
|
+
status: final.status,
|
|
973
|
+
outputDir: job.outputDir,
|
|
974
|
+
credits: final.credits,
|
|
975
|
+
pages: await writePages(job, successes),
|
|
976
|
+
failures: failures.map(({ url, error }) => ({ url, code: error.code, message: error.message })),
|
|
977
|
+
error: final.error
|
|
978
|
+
};
|
|
979
|
+
if (job.llmsTxt && successes.length > 0) {
|
|
980
|
+
await writeLlmsFiles(job, successes, envelope);
|
|
981
|
+
}
|
|
982
|
+
report(job, envelope, io, reporter.log);
|
|
983
|
+
if (final.status === "failed") {
|
|
984
|
+
throw new JobIncompleteError(`${job.kind} job ${jobId} failed${final.error ? `: ${final.error}` : ""}`);
|
|
985
|
+
}
|
|
986
|
+
if (envelope.pages.length === 0) {
|
|
987
|
+
throw new JobIncompleteError(`${job.kind} job ${jobId} converted no pages`);
|
|
988
|
+
}
|
|
989
|
+
return envelope;
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
// src/cli/commands/skill/index.ts
|
|
993
|
+
import { spawn } from "node:child_process";
|
|
994
|
+
import fs5 from "node:fs/promises";
|
|
995
|
+
import os from "node:os";
|
|
996
|
+
import path4 from "node:path";
|
|
997
|
+
|
|
998
|
+
// src/cli/commands/skill/content.ts
|
|
999
|
+
var renderSkillMarkdown = (version) => `---
|
|
1000
|
+
name: webforai
|
|
1001
|
+
description: Convert web pages or local HTML files to clean, LLM-ready Markdown with the webforai CLI. Use when a task needs page content as Markdown - reading documentation or articles, scraping, summarising URLs, crawling a docs site into files or llms.txt, building text corpora - including JavaScript-heavy or bot-protected pages via the hosted platform.
|
|
1002
|
+
---
|
|
1003
|
+
|
|
1004
|
+
# webforai CLI${version ? ` (v${version})` : ""}
|
|
1005
|
+
|
|
1006
|
+
Convert a URL or local HTML file to Markdown. Non-interactive by default: the Markdown goes
|
|
1007
|
+
to stdout, every log goes to stderr, so output can be piped or captured directly.
|
|
1008
|
+
|
|
1009
|
+
## Basic usage
|
|
1010
|
+
|
|
1011
|
+
\`\`\`bash
|
|
1012
|
+
npx webforai https://example.com/article # Markdown to stdout
|
|
1013
|
+
npx webforai https://example.com/article -o out.md # write to a file instead
|
|
1014
|
+
npx webforai ./page.html # convert a local HTML file
|
|
1015
|
+
npx webforai https://example.com --json # machine-readable envelope (see below)
|
|
1016
|
+
\`\`\`
|
|
1017
|
+
|
|
1018
|
+
## Which command
|
|
1019
|
+
|
|
1020
|
+
- One page, or a handful you will read right away \u2192 \`npx webforai <url>\` (free, local;
|
|
1021
|
+
add \`--engine auto\` when the page needs JavaScript or blocks plain fetches).
|
|
1022
|
+
- A known list of URLs (2\u2013100) to keep as files \u2192 \`npx webforai batch\` (hosted platform).
|
|
1023
|
+
- A whole site or docs section \u2192 \`npx webforai crawl\` (hosted platform); add \`--llms-txt\`
|
|
1024
|
+
for a single \`llms-full.txt\` you can read in one go.
|
|
1025
|
+
|
|
1026
|
+
## Flags
|
|
1027
|
+
|
|
1028
|
+
- \`-o, --output <path>\` \u2014 write Markdown to a file instead of stdout.
|
|
1029
|
+
- \`-l, --loader <fetch|playwright|platform>\` \u2014 how HTML is acquired. Default: \`fetch\` for
|
|
1030
|
+
URLs, automatic \`local\` for file paths. \`playwright\` renders JavaScript locally (needs
|
|
1031
|
+
\`npx playwright install chromium\`). \`platform\` uses the hosted API (below).
|
|
1032
|
+
- \`-m, --mode <default|ai>\` \u2014 \`ai\` strips links/tables/images down to plain text
|
|
1033
|
+
(local conversion only).
|
|
1034
|
+
- \`--extractor <auto|takumi|minimal|none>\` \u2014 main-content extraction preset
|
|
1035
|
+
(\`none\` converts the whole page).
|
|
1036
|
+
- \`--frontmatter\` \u2014 prepend YAML front matter (title, author, canonical URL, ...).
|
|
1037
|
+
- \`--json\` \u2014 print a JSON envelope to stdout instead of raw Markdown.
|
|
1038
|
+
- \`--engine <auto|fetch|browser|proxy-fetch|proxy-browser>\` \u2014 platform acquisition engine
|
|
1039
|
+
(implies \`-l platform\`). \`auto\` (the platform default) starts with a plain fetch and
|
|
1040
|
+
escalates to browser rendering when the page is a client-rendered shell;
|
|
1041
|
+
\`browser\`/\`proxy-browser\` always render JavaScript; \`proxy-*\` engines egress through
|
|
1042
|
+
rotating proxies for bot-protected sites (paid plan only).
|
|
1043
|
+
- \`--region <auto|jp>\` \u2014 platform proxy egress region (implies \`-l platform\`); \`jp\` runs
|
|
1044
|
+
on the proxy tier, so it needs a paid plan too.
|
|
1045
|
+
- \`--screenshot\` \u2014 platform browser engines only; the envelope carries an expiring
|
|
1046
|
+
\`screenshotUrl\`.
|
|
1047
|
+
- \`--respect-robots\` \u2014 platform loader only; honor the target site's robots.txt rules
|
|
1048
|
+
(off by default). A disallowed URL fails with \`robots_disallowed\` and is not billed.
|
|
1049
|
+
- \`--api-key <key>\` / \`--platform-url <url>\` \u2014 platform credentials and host; usually set
|
|
1050
|
+
via env vars instead.
|
|
1051
|
+
- \`-i, --interactive\` \u2014 guided prompt flow (for humans; do not use from an agent).
|
|
1052
|
+
- \`-d, --debug\` \u2014 verbose logs on stderr.
|
|
1053
|
+
|
|
1054
|
+
## JSON envelope (\`--json\`)
|
|
1055
|
+
|
|
1056
|
+
\`\`\`json
|
|
1057
|
+
{
|
|
1058
|
+
"source": "https://example.com",
|
|
1059
|
+
"loader": "fetch",
|
|
1060
|
+
"url": "https://example.com",
|
|
1061
|
+
"markdown": "# ...",
|
|
1062
|
+
"metadata": { "title": "..." },
|
|
1063
|
+
"engine": "browser",
|
|
1064
|
+
"region": "auto",
|
|
1065
|
+
"credits": 5,
|
|
1066
|
+
"screenshotUrl": "https://...",
|
|
1067
|
+
"output": "out.md"
|
|
1068
|
+
}
|
|
1069
|
+
\`\`\`
|
|
1070
|
+
|
|
1071
|
+
\`engine\`/\`region\`/\`credits\`/\`screenshotUrl\` appear only for the platform loader; \`output\`
|
|
1072
|
+
only when \`-o\` also wrote a file. A \`warning\` field appears when the result is probably
|
|
1073
|
+
degraded \u2014 most commonly a client-rendered shell fetched without JavaScript; rerun with
|
|
1074
|
+
\`--engine auto\` (or \`-l playwright\`) when you see one.
|
|
1075
|
+
|
|
1076
|
+
## Hosted platform
|
|
1077
|
+
|
|
1078
|
+
For JavaScript-heavy or bot-protected pages, the hosted platform fetches server-side:
|
|
1079
|
+
|
|
1080
|
+
\`\`\`bash
|
|
1081
|
+
export WEBFORAI_API_KEY=wfa_... # from https://platform.webforai.dev/dashboard
|
|
1082
|
+
npx webforai https://example.com --engine auto --json
|
|
1083
|
+
\`\`\`
|
|
1084
|
+
|
|
1085
|
+
\`WEBFORAI_PLATFORM_URL\` overrides the host for self-hosted deployments. Accounts get 1,000
|
|
1086
|
+
free credits per month; failures are never billed.
|
|
1087
|
+
|
|
1088
|
+
| operation | credits |
|
|
1089
|
+
| --- | --- |
|
|
1090
|
+
| \`fetch\` | 1 |
|
|
1091
|
+
| \`browser\` | 2 |
|
|
1092
|
+
| \`proxy-fetch\` (paid plan only) | 2 |
|
|
1093
|
+
| \`proxy-browser\` (paid plan only) | 3 |
|
|
1094
|
+
| \`--screenshot\` | +1 |
|
|
1095
|
+
| image rehosting (API \`rehostImages\`) | +1 per started 5 images |
|
|
1096
|
+
|
|
1097
|
+
\`auto\` bills the engine that actually ran (1, or 2 when it needed a browser). The proxy
|
|
1098
|
+
engines \u2014 and \`--region jp\`, which runs on them \u2014 need an active subscription; on the free
|
|
1099
|
+
allowance they fail with \`payment_required\`. Prefer \`--engine auto\` unless a site blocks
|
|
1100
|
+
it. Batch and crawl jobs bill per converted page.
|
|
1101
|
+
|
|
1102
|
+
## Crawl and batch (hosted platform)
|
|
1103
|
+
|
|
1104
|
+
\`\`\`bash
|
|
1105
|
+
# a docs site \u2192 one .md per page, mirroring URL paths, plus llms.txt / llms-full.txt
|
|
1106
|
+
npx webforai crawl https://docs.example.com -o docs --limit 100 --include '^/docs' --llms-txt
|
|
1107
|
+
|
|
1108
|
+
# a list of URLs (arguments, or one per line via --file; '-' reads stdin)
|
|
1109
|
+
npx webforai batch https://a.example/post https://b.example/news -o pages
|
|
1110
|
+
npx webforai batch --file urls.txt -o pages --json
|
|
1111
|
+
\`\`\`
|
|
1112
|
+
|
|
1113
|
+
Both submit an async job, wait for it, then write every converted page under \`-o <dir>\`
|
|
1114
|
+
(\`/docs/intro\` \u2192 \`docs/intro.md\`; batch prefixes the host: \`a.example/post.md\`). stdout
|
|
1115
|
+
lists the written files, one per line; \`--json\` prints \`{ jobId, status, credits, pages:
|
|
1116
|
+
[{ url, file, title }], failures: [{ url, code, message }], llmsTxt?, llmsFullTxt? }\`
|
|
1117
|
+
instead. Progress and the summary go to stderr. Exit code 1 when the job failed or no page
|
|
1118
|
+
converted; individual failed pages are listed but do not fail the command.
|
|
1119
|
+
|
|
1120
|
+
- \`crawl <url>\` \u2014 same-origin only. \`--max-depth <0-5>\` (default 2), \`--limit <1-500>\`
|
|
1121
|
+
(default 50), \`--include <regex>\` / \`--exclude <regex>\` on the pathname (repeatable).
|
|
1122
|
+
\`--sitemap <skip|include|only>\`: \`include\` adds the site's sitemap URLs to the links it
|
|
1123
|
+
follows, \`only\` crawls just the seed plus sitemap URLs (best for blogs and large docs with
|
|
1124
|
+
a good sitemap). Honors robots.txt by default; \`--no-respect-robots\` turns that off.
|
|
1125
|
+
- \`--llms-txt\` (crawl) \u2014 also write \`llms.txt\` (site title, summary, \`- [title](url):
|
|
1126
|
+
description\` links grouped by top-level path) and \`llms-full.txt\` (every page's Markdown
|
|
1127
|
+
concatenated) per llmstxt.org. Read \`llms-full.txt\` when you need the whole site in context.
|
|
1128
|
+
- \`batch [urls...]\` \u2014 up to 100 URLs per job; \`-f, --file <path|->\` adds URLs from a file.
|
|
1129
|
+
\`--respect-robots\` opts in to robots.txt checks.
|
|
1130
|
+
- Shared: \`--engine\`, \`--region\`, \`--extractor\`, \`--frontmatter\`, \`--json\`,
|
|
1131
|
+
\`--timeout <seconds>\` (default 1800; the job keeps running after a timeout and results stay
|
|
1132
|
+
available for 7 days), \`--api-key\`, \`--platform-url\`, \`-d\`.
|
|
1133
|
+
|
|
1134
|
+
## Exit codes
|
|
1135
|
+
|
|
1136
|
+
- \`0\` \u2014 success.
|
|
1137
|
+
- \`1\` \u2014 runtime failure (network, conversion, platform error; message on stderr).
|
|
1138
|
+
- \`2\` \u2014 usage error (unknown flag or invalid value; message on stderr).
|
|
1139
|
+
|
|
1140
|
+
## Errors worth handling
|
|
1141
|
+
|
|
1142
|
+
Platform errors print as \`error: <code>: <message>\` on stderr (exit 1). What to do:
|
|
1143
|
+
|
|
1144
|
+
- \`invalid_api_key\` (401) \u2192 the key is missing, revoked or mistyped. Do not retry; ask the
|
|
1145
|
+
user for a key from the dashboard named in the message.
|
|
1146
|
+
- \`payment_required\` (402) \u2192 the free monthly allowance is used up, or a proxy engine /
|
|
1147
|
+
\`--region jp\` was requested without a subscription. Do not retry; fall back to
|
|
1148
|
+
\`--engine auto\` (or the local loaders) for proxy requests, otherwise tell the user.
|
|
1149
|
+
- \`spend_cap_reached\` (402) \u2192 the account's monthly spend cap (default $50) is reached; the
|
|
1150
|
+
user can raise it on the dashboard. Do not retry.
|
|
1151
|
+
- \`rate_limited\` (429) \u2192 per-account limit: 60 requests/minute (free), 600 (paid). Wait the
|
|
1152
|
+
\`retryAfter\` seconds shown, then retry; for many URLs use one \`batch\` job instead of a
|
|
1153
|
+
loop of single requests. \`crawl\`/\`batch\` retry this automatically.
|
|
1154
|
+
- \`too_many_jobs\` (429) \u2192 3 (free) / 20 (paid) batch/crawl jobs already queued or running.
|
|
1155
|
+
Wait for one to finish, then resubmit.
|
|
1156
|
+
- \`robots_disallowed\` (403) \u2192 robots.txt disallows the URL (\`--respect-robots\`, or a crawl
|
|
1157
|
+
by default). Not billed; respect it unless the user explicitly decides otherwise.
|
|
1158
|
+
- \`invalid_request\` (400) \u2192 a bad flag value or URL; fix the input.
|
|
1159
|
+
- \`engine_unavailable\` / \`scheduling_unknown\` / \`result_unavailable\` (503) \u2192 transient;
|
|
1160
|
+
retry later.
|
|
1161
|
+
- Playwright loader errors explain the exact \`npx playwright install\` command to run.
|
|
1162
|
+
`;
|
|
1163
|
+
|
|
1164
|
+
// src/cli/commands/skill/index.ts
|
|
1165
|
+
var buildSkillsAddArgs = (source, flags) => {
|
|
1166
|
+
const args = ["-y", "skills", "add", source];
|
|
1167
|
+
if (flags.global) {
|
|
1168
|
+
args.push("--global");
|
|
1169
|
+
}
|
|
1170
|
+
if (flags.agent) {
|
|
1171
|
+
args.push("--agent", flags.agent);
|
|
1172
|
+
}
|
|
1173
|
+
if (flags.yes) {
|
|
1174
|
+
args.push("-y");
|
|
1175
|
+
}
|
|
1176
|
+
if (flags.copy) {
|
|
1177
|
+
args.push("--copy");
|
|
1178
|
+
}
|
|
1179
|
+
if (flags.all) {
|
|
1180
|
+
args.push("--all");
|
|
1181
|
+
}
|
|
1182
|
+
return args;
|
|
1183
|
+
};
|
|
1184
|
+
var runSkillsAdd = (args) => new Promise((resolve, reject) => {
|
|
1185
|
+
const child = spawn("npx", args, { stdio: ["inherit", "inherit", "inherit"] });
|
|
1186
|
+
child.on("error", reject);
|
|
1187
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
1188
|
+
});
|
|
1189
|
+
var skillCommand = async (flags) => {
|
|
1190
|
+
const markdown = renderSkillMarkdown(package_default.version);
|
|
1191
|
+
if (!(flags.install || flags.dir)) {
|
|
1192
|
+
process.stdout.write(markdown);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (flags.dir) {
|
|
1196
|
+
const target = path4.join(flags.dir, "webforai", "SKILL.md");
|
|
1197
|
+
await fs5.mkdir(path4.dirname(target), { recursive: true });
|
|
1198
|
+
await fs5.writeFile(target, markdown);
|
|
1199
|
+
console.error(`[webforai] Agent Skill written to ${target}`);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const stage = await fs5.mkdtemp(path4.join(os.tmpdir(), "webforai-skill-"));
|
|
1203
|
+
try {
|
|
1204
|
+
await fs5.mkdir(path4.join(stage, "webforai"), { recursive: true });
|
|
1205
|
+
await fs5.writeFile(path4.join(stage, "webforai", "SKILL.md"), markdown);
|
|
1206
|
+
const args = buildSkillsAddArgs(stage, flags);
|
|
1207
|
+
console.error(`[webforai] running: npx ${args.join(" ")}`);
|
|
1208
|
+
const code = await runSkillsAdd(args).catch((error) => {
|
|
1209
|
+
console.error(
|
|
1210
|
+
`[webforai] could not launch the skills CLI (${error instanceof Error ? error.message : String(error)}).`
|
|
1211
|
+
);
|
|
1212
|
+
console.error("[webforai] fallback: `webforai skill --dir .claude/skills` writes the file directly.");
|
|
1213
|
+
return 1;
|
|
1214
|
+
});
|
|
1215
|
+
if (code !== 0) {
|
|
1216
|
+
process.exitCode = 1;
|
|
1217
|
+
}
|
|
1218
|
+
} finally {
|
|
1219
|
+
await fs5.rm(stage, { recursive: true, force: true });
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
// src/cli/commands/webforai/interactive.ts
|
|
1224
|
+
import { intro, log, outro, select as select7, spinner, text as text5 } from "@clack/prompts";
|
|
1225
|
+
import pc2 from "picocolors";
|
|
1226
|
+
|
|
467
1227
|
// src/cli/helpers/assertContinue.ts
|
|
468
1228
|
import { cancel, isCancel } from "@clack/prompts";
|
|
469
1229
|
function assertContinue(message, cancelMessage = "Canceled.") {
|
|
@@ -474,89 +1234,8 @@ function assertContinue(message, cancelMessage = "Canceled.") {
|
|
|
474
1234
|
}
|
|
475
1235
|
|
|
476
1236
|
// src/cli/helpers/inputOutputPath.ts
|
|
477
|
-
import
|
|
1237
|
+
import fs6 from "node:fs";
|
|
478
1238
|
import { confirm, text } from "@clack/prompts";
|
|
479
|
-
|
|
480
|
-
// src/cli/utils.ts
|
|
481
|
-
import fs2 from "node:fs";
|
|
482
|
-
import path2 from "node:path";
|
|
483
|
-
var isUrl = (maybeUrl) => {
|
|
484
|
-
try {
|
|
485
|
-
new URL(maybeUrl);
|
|
486
|
-
return true;
|
|
487
|
-
} catch {
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
function changeFileExtension(filePath, newExtension) {
|
|
492
|
-
const parsedPath = filePath.split("/");
|
|
493
|
-
const fileName = parsedPath[parsedPath.length - 1];
|
|
494
|
-
const formattedNewExtension = newExtension.startsWith(".") ? newExtension : `.${newExtension}`;
|
|
495
|
-
if (fileName.startsWith(".")) {
|
|
496
|
-
const parts = fileName.split(".");
|
|
497
|
-
if (parts.length === 2) {
|
|
498
|
-
return parsedPath.slice(0, -1).concat(`${fileName}${formattedNewExtension}`).join("/");
|
|
499
|
-
}
|
|
500
|
-
parts[parts.length - 1] = newExtension.replace(/^\./, "");
|
|
501
|
-
return parsedPath.slice(0, -1).concat(parts.join(".")).join("/");
|
|
502
|
-
}
|
|
503
|
-
const lastDotIndex = fileName.lastIndexOf(".");
|
|
504
|
-
const baseName = lastDotIndex !== -1 ? fileName.slice(0, lastDotIndex) : fileName;
|
|
505
|
-
const newFileName = `${baseName}${formattedNewExtension}`;
|
|
506
|
-
parsedPath[parsedPath.length - 1] = newFileName;
|
|
507
|
-
return parsedPath.join("/");
|
|
508
|
-
}
|
|
509
|
-
function urlToFilename(url) {
|
|
510
|
-
try {
|
|
511
|
-
const urlObj = new URL(url);
|
|
512
|
-
const domainParts = urlObj.hostname.split(".").reverse().reduce((acc, part, index) => {
|
|
513
|
-
if (index === 0) {
|
|
514
|
-
return acc;
|
|
515
|
-
}
|
|
516
|
-
if (acc.length >= 2) {
|
|
517
|
-
return acc;
|
|
518
|
-
}
|
|
519
|
-
if (part === "www") {
|
|
520
|
-
return acc;
|
|
521
|
-
}
|
|
522
|
-
return [part, ...acc];
|
|
523
|
-
}, []);
|
|
524
|
-
const domainString = domainParts.reverse().join("-");
|
|
525
|
-
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
526
|
-
const relevantPathParts = pathParts.slice(-2);
|
|
527
|
-
const pathString = relevantPathParts.map((part) => decodeURIComponent(part)).join("-");
|
|
528
|
-
let filename = [domainString, pathString].filter(Boolean).join("-");
|
|
529
|
-
filename = filename.toLowerCase().replace(/[<>:"/\\|?*\x00-\x1F]/g, "").replace(/[\s.]+/g, "-").replace(/^-+|-+$/g, "");
|
|
530
|
-
return filename || "output";
|
|
531
|
-
} catch {
|
|
532
|
-
return "output";
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
var sourcePathToOutputPath = (sourcePath) => {
|
|
536
|
-
return isUrl(sourcePath) ? `${urlToFilename(sourcePath)}.md` : changeFileExtension(sourcePath, "md");
|
|
537
|
-
};
|
|
538
|
-
function getNextAvailableFilePath(filePath) {
|
|
539
|
-
const parsedPath = path2.parse(filePath);
|
|
540
|
-
const directory = parsedPath.dir;
|
|
541
|
-
const fullName = parsedPath.base;
|
|
542
|
-
const [firstPart, ...restParts] = fullName.split(".");
|
|
543
|
-
const restName = restParts.length > 0 ? `.${restParts.join(".")}` : "";
|
|
544
|
-
const baseName = firstPart.replace(/_\d+$/, "");
|
|
545
|
-
let counter = 1;
|
|
546
|
-
let nextFilePath = filePath;
|
|
547
|
-
while (fs2.existsSync(nextFilePath)) {
|
|
548
|
-
const match = firstPart.match(/_(\d+)$/);
|
|
549
|
-
if (match) {
|
|
550
|
-
counter = Number.parseInt(match[1], 10) + 1;
|
|
551
|
-
}
|
|
552
|
-
const newName = `${baseName}_${counter}${restName}`;
|
|
553
|
-
nextFilePath = path2.join(directory, newName);
|
|
554
|
-
counter++;
|
|
555
|
-
}
|
|
556
|
-
return nextFilePath;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// src/cli/helpers/inputOutputPath.ts
|
|
560
1239
|
var inputOutputPath = async (sourcePath) => {
|
|
561
1240
|
const outputPath = await text({
|
|
562
1241
|
message: "Enter the output file path:",
|
|
@@ -566,16 +1245,16 @@ var inputOutputPath = async (sourcePath) => {
|
|
|
566
1245
|
if (value.trim() === "") {
|
|
567
1246
|
return "Output path is required";
|
|
568
1247
|
}
|
|
569
|
-
if (
|
|
1248
|
+
if (fs6.existsSync(value) && fs6.statSync(value).isDirectory()) {
|
|
570
1249
|
return "No directory can be specified.";
|
|
571
1250
|
}
|
|
572
|
-
if (!
|
|
1251
|
+
if (!fs6.existsSync(value) && value.endsWith("/")) {
|
|
573
1252
|
return "No directory can be specified.";
|
|
574
1253
|
}
|
|
575
1254
|
}
|
|
576
1255
|
});
|
|
577
1256
|
assertContinue(outputPath);
|
|
578
|
-
if (!
|
|
1257
|
+
if (!fs6.existsSync(outputPath)) {
|
|
579
1258
|
return outputPath;
|
|
580
1259
|
}
|
|
581
1260
|
const isOutputFileOverwrite = await confirm({
|
|
@@ -594,10 +1273,10 @@ var inputOutputPath = async (sourcePath) => {
|
|
|
594
1273
|
if (value.trim() === "") {
|
|
595
1274
|
return "Output path is required";
|
|
596
1275
|
}
|
|
597
|
-
if (
|
|
1276
|
+
if (fs6.existsSync(value)) {
|
|
598
1277
|
return "The file already exists";
|
|
599
1278
|
}
|
|
600
|
-
if (!
|
|
1279
|
+
if (!fs6.existsSync(value) && value.endsWith("/")) {
|
|
601
1280
|
return "No directory can be specified.";
|
|
602
1281
|
}
|
|
603
1282
|
}
|
|
@@ -607,7 +1286,7 @@ var inputOutputPath = async (sourcePath) => {
|
|
|
607
1286
|
};
|
|
608
1287
|
|
|
609
1288
|
// src/cli/helpers/inputSourcePath.ts
|
|
610
|
-
import
|
|
1289
|
+
import fs7 from "node:fs";
|
|
611
1290
|
import { text as text2 } from "@clack/prompts";
|
|
612
1291
|
var inputSourcePath = async () => {
|
|
613
1292
|
const result = await text2({
|
|
@@ -619,10 +1298,10 @@ var inputSourcePath = async () => {
|
|
|
619
1298
|
return "Source is required";
|
|
620
1299
|
}
|
|
621
1300
|
if (!isUrl(value)) {
|
|
622
|
-
if (!
|
|
1301
|
+
if (!fs7.existsSync(value)) {
|
|
623
1302
|
return "It appears that you are specifying a local file, but the file cannot be found. hint: when specifying a url, start with http or https.";
|
|
624
1303
|
}
|
|
625
|
-
if (
|
|
1304
|
+
if (fs7.statSync(value).isDirectory()) {
|
|
626
1305
|
return "You are specifying a local file, but you cannot specify a directory. hint: when specifying a url, start with http or https.";
|
|
627
1306
|
}
|
|
628
1307
|
}
|
|
@@ -662,8 +1341,8 @@ var selectLoader = async () => {
|
|
|
662
1341
|
};
|
|
663
1342
|
|
|
664
1343
|
// src/cli/commands/webforai/run.ts
|
|
665
|
-
import
|
|
666
|
-
import
|
|
1344
|
+
import fs9 from "node:fs/promises";
|
|
1345
|
+
import path5 from "node:path";
|
|
667
1346
|
|
|
668
1347
|
// src/html-to-markdown.ts
|
|
669
1348
|
import { fromHtml as fromHtml2 } from "hast-util-from-html";
|
|
@@ -1494,7 +2173,11 @@ var countAnchors = (element3) => {
|
|
|
1494
2173
|
}
|
|
1495
2174
|
return count;
|
|
1496
2175
|
};
|
|
2176
|
+
var LITERAL_TEXT_TAGS = /* @__PURE__ */ new Set(["code", "kbd", "samp", "var", "pre"]);
|
|
1497
2177
|
var isMidArticleWidget = (element3) => {
|
|
2178
|
+
if (LITERAL_TEXT_TAGS.has(element3.tagName)) {
|
|
2179
|
+
return false;
|
|
2180
|
+
}
|
|
1498
2181
|
if (element3.tagName === "form" || element3.tagName === "fieldset") {
|
|
1499
2182
|
return true;
|
|
1500
2183
|
}
|
|
@@ -2684,14 +3367,22 @@ var detectLanguage = (code) => {
|
|
|
2684
3367
|
};
|
|
2685
3368
|
|
|
2686
3369
|
// src/mdast-handlers/custom-code-handler.ts
|
|
2687
|
-
var LANGUAGE_MATCH_REGEX = [
|
|
3370
|
+
var LANGUAGE_MATCH_REGEX = [/^language-(\S+)$/, /^highlight-source-(\S+)$/, /^CodeBlock--language-(\S+)$/];
|
|
3371
|
+
var LANGUAGE_ATTRIBUTES = ["dataLanguage", "dataLang"];
|
|
3372
|
+
var attributeLanguage = (element3) => {
|
|
3373
|
+
for (const name of LANGUAGE_ATTRIBUTES) {
|
|
3374
|
+
const value = element3.properties?.[name];
|
|
3375
|
+
if (typeof value === "string" && /^[\w+#.-]+$/.test(value.trim()) && value.trim() !== "plaintext") {
|
|
3376
|
+
return value.trim();
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
return void 0;
|
|
3380
|
+
};
|
|
3381
|
+
var codeLanguage = (...elements) => elements.flatMap(classList).map((className) => LANGUAGE_MATCH_REGEX.map((regex) => className.match(regex)).find(Boolean)?.[1]).find(Boolean) ?? elements.map(attributeLanguage).find(Boolean);
|
|
2688
3382
|
var customCodeHandler = (state, node) => {
|
|
2689
|
-
const
|
|
2690
|
-
const codeValue = trimTrailingLines(toText(node))
|
|
2691
|
-
const classLang =
|
|
2692
|
-
const match = LANGUAGE_MATCH_REGEX.map((regex) => className.match(regex)).find((match2) => match2);
|
|
2693
|
-
return match?.[1];
|
|
2694
|
-
}).find((className) => className);
|
|
3383
|
+
const code = node.children.find((child) => isElement(child) && child.tagName === "code");
|
|
3384
|
+
const codeValue = trimTrailingLines(toText(node));
|
|
3385
|
+
const classLang = codeLanguage(...isElement(code) ? [code, node] : [node]);
|
|
2695
3386
|
const lang = classLang || detectLanguage(codeValue) || null;
|
|
2696
3387
|
const result = { type: "code", lang, meta: null, value: codeValue };
|
|
2697
3388
|
state.patch(node, result);
|
|
@@ -2706,42 +3397,19 @@ import { toText as toText2 } from "hast-util-to-text";
|
|
|
2706
3397
|
import { trimTrailingLines as trimTrailingLines2 } from "trim-trailing-lines";
|
|
2707
3398
|
var CODE_BLOCK_REGEX = /highlight-source|language-|codegroup|codeblock|code-block/i;
|
|
2708
3399
|
var CODE_FILENAME_SELECTORS = "[class*='fileName'],[class*='fileName'],[class*='title'],[class*='Title']";
|
|
2709
|
-
var LANGUAGE_MATCH_REGEX2 = [/language-(\w+)/, /highlight-source-(\w+)/, /CodeBlock--language-(\w+)/];
|
|
2710
|
-
var findRecursive = (array, condition, maxDepth = 3) => {
|
|
2711
|
-
if (maxDepth <= 0) {
|
|
2712
|
-
return null;
|
|
2713
|
-
}
|
|
2714
|
-
for (const value of array) {
|
|
2715
|
-
const result = condition(value);
|
|
2716
|
-
if (Array.isArray(result)) {
|
|
2717
|
-
return findRecursive(result, condition, maxDepth - 1);
|
|
2718
|
-
}
|
|
2719
|
-
if (result) {
|
|
2720
|
-
return value;
|
|
2721
|
-
}
|
|
2722
|
-
}
|
|
2723
|
-
return null;
|
|
2724
|
-
};
|
|
2725
3400
|
var customDivHandler = (state, node) => {
|
|
2726
|
-
const classNames =
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
return child.children.filter((child2) => child2.type === "element");
|
|
2735
|
-
});
|
|
2736
|
-
if (codeBlock && classNames.some((className) => CODE_BLOCK_REGEX.test(className))) {
|
|
2737
|
-
const codeBlockClassNames = codeBlock.type === "element" ? codeBlock.properties.className ?? [] : [];
|
|
2738
|
-
const codeValue = trimTrailingLines2(toText2(codeBlock)).trim();
|
|
3401
|
+
const classNames = classList(node);
|
|
3402
|
+
if (!classNames.some((className) => CODE_BLOCK_REGEX.test(className))) {
|
|
3403
|
+
return defaultHandlers2.div(state, node);
|
|
3404
|
+
}
|
|
3405
|
+
const codeBlocks = collectElements(node, (element3) => element3.tagName === "pre");
|
|
3406
|
+
const codeBlock = codeBlocks.length === 1 ? codeBlocks[0] : void 0;
|
|
3407
|
+
if (codeBlock) {
|
|
3408
|
+
const codeValue = trimTrailingLines2(toText2(codeBlock));
|
|
2739
3409
|
const filenameElement = select5(CODE_FILENAME_SELECTORS, node);
|
|
2740
3410
|
const fileLang = filenameElement ? hastToString2(filenameElement).match(/\.(\w+)$/)?.[1] : null;
|
|
2741
|
-
const
|
|
2742
|
-
|
|
2743
|
-
return match?.[1];
|
|
2744
|
-
}).find((className) => className);
|
|
3411
|
+
const code = codeBlock.children.find((child) => isElement(child) && child.tagName === "code");
|
|
3412
|
+
const classLang = codeLanguage(...isElement(code) ? [code, codeBlock, node] : [codeBlock, node]);
|
|
2745
3413
|
const lang = fileLang || classLang || detectLanguage(codeValue) || null;
|
|
2746
3414
|
const result = { type: "code", lang, meta: null, value: codeValue };
|
|
2747
3415
|
state.patch(node, result);
|
|
@@ -2837,6 +3505,20 @@ var hoistBoundaryWhitespace = (nodes) => {
|
|
|
2837
3505
|
}
|
|
2838
3506
|
return result;
|
|
2839
3507
|
};
|
|
3508
|
+
var termsAndDefinitions = (parent) => {
|
|
3509
|
+
const found = [];
|
|
3510
|
+
for (const child of parent.children) {
|
|
3511
|
+
if (child.type !== "element") {
|
|
3512
|
+
continue;
|
|
3513
|
+
}
|
|
3514
|
+
if (child.tagName === "dt" || child.tagName === "dd") {
|
|
3515
|
+
found.push(child);
|
|
3516
|
+
} else if (child.tagName === "div") {
|
|
3517
|
+
found.push(...termsAndDefinitions(child));
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
return found;
|
|
3521
|
+
};
|
|
2840
3522
|
var definitionListHandler = (state, node) => {
|
|
2841
3523
|
const items = [];
|
|
2842
3524
|
let current;
|
|
@@ -2851,11 +3533,7 @@ var definitionListHandler = (state, node) => {
|
|
|
2851
3533
|
items.push({ type: "listItem", spread: false, children: [heading2, ...current.definitions] });
|
|
2852
3534
|
current = void 0;
|
|
2853
3535
|
};
|
|
2854
|
-
for (const
|
|
2855
|
-
if (child.type !== "element") {
|
|
2856
|
-
continue;
|
|
2857
|
-
}
|
|
2858
|
-
const element3 = child;
|
|
3536
|
+
for (const element3 of termsAndDefinitions(node)) {
|
|
2859
3537
|
if (element3.tagName === "dt") {
|
|
2860
3538
|
if (current && current.definitions.length > 0) {
|
|
2861
3539
|
flush();
|
|
@@ -2874,7 +3552,8 @@ var definitionListHandler = (state, node) => {
|
|
|
2874
3552
|
}
|
|
2875
3553
|
flush();
|
|
2876
3554
|
if (items.length === 0) {
|
|
2877
|
-
|
|
3555
|
+
const fallback = state.toFlow(state.all(node));
|
|
3556
|
+
return fallback.length > 0 ? fallback : void 0;
|
|
2878
3557
|
}
|
|
2879
3558
|
const result = { type: "list", ordered: false, spread: false, children: items };
|
|
2880
3559
|
state.patch(node, result);
|
|
@@ -2987,14 +3666,16 @@ var katexDuplicates = (node) => {
|
|
|
2987
3666
|
if (!(classes.includes("katex") || classes.includes("katex-display"))) {
|
|
2988
3667
|
return [];
|
|
2989
3668
|
}
|
|
2990
|
-
const hasMathml = node.children.some(
|
|
3669
|
+
const hasMathml = node.children.some(
|
|
3670
|
+
(child) => isElement(child) && classList(child).includes("katex-mathml") && hasMathSource(child)
|
|
3671
|
+
);
|
|
2991
3672
|
if (!hasMathml) {
|
|
2992
3673
|
return [];
|
|
2993
3674
|
}
|
|
2994
3675
|
return node.children.filter((child) => isElement(child) && classList(child).includes("katex-html"));
|
|
2995
3676
|
};
|
|
2996
3677
|
var mediawikiDuplicates = (node) => {
|
|
2997
|
-
if (!classList(node).includes("mwe-math-element")) {
|
|
3678
|
+
if (!classList(node).includes("mwe-math-element") || !hasMathSource(node)) {
|
|
2998
3679
|
return [];
|
|
2999
3680
|
}
|
|
3000
3681
|
return node.children.filter(
|
|
@@ -3005,8 +3686,15 @@ var mathjaxDuplicates = (node) => {
|
|
|
3005
3686
|
if (node.tagName !== "mjx-container") {
|
|
3006
3687
|
return [];
|
|
3007
3688
|
}
|
|
3689
|
+
const hasMathml = node.children.some(
|
|
3690
|
+
(child) => isElement(child) && child.tagName === "mjx-assistive-mml" && hasMathSource(child)
|
|
3691
|
+
);
|
|
3692
|
+
if (!hasMathml) {
|
|
3693
|
+
return [];
|
|
3694
|
+
}
|
|
3008
3695
|
return node.children.filter((child) => isElement(child) && child.tagName !== "mjx-assistive-mml");
|
|
3009
3696
|
};
|
|
3697
|
+
var hasMathSource = (node) => Boolean(findElement(node, (element3) => element3.tagName === "math" && element3.children.length > 0));
|
|
3010
3698
|
var dedupeRenderedMath = (tree) => {
|
|
3011
3699
|
const doomed = /* @__PURE__ */ new Set();
|
|
3012
3700
|
walk(tree, (node) => {
|
|
@@ -3068,8 +3756,8 @@ var getLangFromHast = (node) => {
|
|
|
3068
3756
|
}
|
|
3069
3757
|
return void 0;
|
|
3070
3758
|
};
|
|
3071
|
-
var getLangFromStr = (
|
|
3072
|
-
const match =
|
|
3759
|
+
var getLangFromStr = (str2) => {
|
|
3760
|
+
const match = str2.match(/lang=["']([^"']+)["']/);
|
|
3073
3761
|
if (match) {
|
|
3074
3762
|
return match[1];
|
|
3075
3763
|
}
|
|
@@ -3096,7 +3784,7 @@ var htmlToMdast = (htmlOrHast, options) => {
|
|
|
3096
3784
|
const [lang, hast] = typeof htmlOrHast === "string" ? [defaultLang || getLangFromStr(htmlOrHast), fromHtml(htmlOrHast, { fragment: true })] : [defaultLang || getLangFromHast(htmlOrHast), htmlOrHast];
|
|
3097
3785
|
const isOwned = typeof htmlOrHast === "string" || (options?.owned ?? false);
|
|
3098
3786
|
const url = defaultUrl || getUrlFromHast(hast);
|
|
3099
|
-
const extractedHast = pipeExtractors({ hast, lang, url, owned:
|
|
3787
|
+
const extractedHast = pipeExtractors({ hast: isOwned ? hast : cloneHast(hast), lang, url, owned: true }, extractors);
|
|
3100
3788
|
normalizeHast(extractedHast, options?.normalize);
|
|
3101
3789
|
const mdast = toMdast(extractedHast, {
|
|
3102
3790
|
handlers: {
|
|
@@ -3121,19 +3809,25 @@ import { mathToMarkdown } from "mdast-util-math";
|
|
|
3121
3809
|
import { toMarkdown } from "mdast-util-to-markdown";
|
|
3122
3810
|
|
|
3123
3811
|
// src/link-replacer.ts
|
|
3124
|
-
var
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3812
|
+
var absoluteUrl = (url, base) => {
|
|
3813
|
+
if (/^(?:[a-z][a-z\d+.-]*:|#)/i.test(url)) {
|
|
3814
|
+
return url;
|
|
3815
|
+
}
|
|
3816
|
+
try {
|
|
3817
|
+
return new URL(url, base).href;
|
|
3818
|
+
} catch {
|
|
3819
|
+
return url;
|
|
3820
|
+
}
|
|
3821
|
+
};
|
|
3822
|
+
var linkReplacer = (node, base) => {
|
|
3823
|
+
let result = node;
|
|
3824
|
+
if (node.type === "link" || node.type === "image" || node.type === "definition") {
|
|
3825
|
+
result = { ...node, url: absoluteUrl(node.url, base) };
|
|
3826
|
+
}
|
|
3827
|
+
if ("children" in result) {
|
|
3828
|
+
return { ...result, children: result.children.map((child) => linkReplacer(child, base)) };
|
|
3829
|
+
}
|
|
3830
|
+
return result;
|
|
3137
3831
|
};
|
|
3138
3832
|
|
|
3139
3833
|
// src/utils/mdast-utils.ts
|
|
@@ -3151,11 +3845,8 @@ var DEFAULT_MDAST_TO_MARKDOWN_OPTIONS = {
|
|
|
3151
3845
|
};
|
|
3152
3846
|
var mdastToMarkdown = (mdast, options) => {
|
|
3153
3847
|
const { baseUrl, ...toMarkdownOptions } = { ...DEFAULT_MDAST_TO_MARKDOWN_OPTIONS, ...options };
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
markdown = linkReplacer(markdown, baseUrl);
|
|
3157
|
-
}
|
|
3158
|
-
return markdown;
|
|
3848
|
+
const tree = warpRoot(mdast);
|
|
3849
|
+
return toMarkdown(baseUrl ? linkReplacer(tree, baseUrl) : tree, toMarkdownOptions);
|
|
3159
3850
|
};
|
|
3160
3851
|
|
|
3161
3852
|
// src/metadata/index.ts
|
|
@@ -3269,7 +3960,7 @@ var documentTitle = (tree) => {
|
|
|
3269
3960
|
}
|
|
3270
3961
|
return title.children.map((child) => child.type === "text" ? child.value : "").join("");
|
|
3271
3962
|
};
|
|
3272
|
-
var
|
|
3963
|
+
var firstHeading2 = (tree) => {
|
|
3273
3964
|
const heading2 = findElement(tree, (element3) => element3.tagName === "h1");
|
|
3274
3965
|
if (!heading2) {
|
|
3275
3966
|
return void 0;
|
|
@@ -3319,7 +4010,7 @@ var extractMetadata = (tree) => {
|
|
|
3319
4010
|
}
|
|
3320
4011
|
return void 0;
|
|
3321
4012
|
};
|
|
3322
|
-
const title = asTitle(get("og:title", "twitter:title")) ?? asTitle(asString(article?.headline)) ?? asTitle(asString(article?.name)) ?? asTitle(documentTitle(tree)) ?? asTitle(
|
|
4013
|
+
const title = asTitle(get("og:title", "twitter:title")) ?? asTitle(asString(article?.headline)) ?? asTitle(asString(article?.name)) ?? asTitle(documentTitle(tree)) ?? asTitle(firstHeading2(tree));
|
|
3323
4014
|
return {
|
|
3324
4015
|
title,
|
|
3325
4016
|
description: clean(get("og:description", "twitter:description", "description")) ?? clean(asString(article?.description)),
|
|
@@ -3390,8 +4081,101 @@ ${body}`;
|
|
|
3390
4081
|
};
|
|
3391
4082
|
var normalizeForCompare = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
3392
4083
|
|
|
4084
|
+
// src/detect-client-shell.ts
|
|
4085
|
+
var SHELL_SUSPICION_THRESHOLD = 300;
|
|
4086
|
+
var SHELL_EMPTY_THRESHOLD = 50;
|
|
4087
|
+
var BODY_PATTERN = /<body[^>]*>([\s\S]*?)<\/body>/i;
|
|
4088
|
+
var HEAD_PATTERN = /<head[^>]*>[\s\S]*?<\/head>/gi;
|
|
4089
|
+
var NOSCRIPT_PATTERN = /<noscript[^>]*>([\s\S]*?)<\/noscript>/gi;
|
|
4090
|
+
var INVISIBLE_ELEMENT_PATTERN = /<(script|style|template|svg|noscript)[^>]*>[\s\S]*?<\/\1>/gi;
|
|
4091
|
+
var COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
|
|
4092
|
+
var TAG_PATTERN = /<[^>]+>/g;
|
|
4093
|
+
var MOUNT_POINT_PATTERN = /<[a-z][^>]*\sid=["'](?:root|app|__next|___gatsby|__nuxt|q-app|svelte|react-root)["']|<app-root[\s>]|\sdata-reactroot[\s>=]/i;
|
|
4094
|
+
var CHALLENGE_PATTERN = /cf-chl|challenge-platform|cf_chl_opt|just a moment|attention required|checking your browser|_incapsula_|px-captcha|datadome/i;
|
|
4095
|
+
var ENTITIES = {
|
|
4096
|
+
" ": " ",
|
|
4097
|
+
"&": "&",
|
|
4098
|
+
"<": "<",
|
|
4099
|
+
">": ">",
|
|
4100
|
+
""": '"',
|
|
4101
|
+
"'": "'"
|
|
4102
|
+
};
|
|
4103
|
+
var visibleTextOf = (markup) => markup.replace(COMMENT_PATTERN, " ").replace(INVISIBLE_ELEMENT_PATTERN, " ").replace(TAG_PATTERN, " ").replace(/&(?:nbsp|amp|lt|gt|quot|#39);/g, (entity) => ENTITIES[entity] ?? " ").replace(/\s+/g, " ").trim();
|
|
4104
|
+
var noscriptMentionsJavascript = (html) => {
|
|
4105
|
+
for (const match of html.matchAll(NOSCRIPT_PATTERN)) {
|
|
4106
|
+
if (/javascript/i.test(match[1] ?? "")) {
|
|
4107
|
+
return true;
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
return false;
|
|
4111
|
+
};
|
|
4112
|
+
var detectClientShell = (html) => {
|
|
4113
|
+
const body = BODY_PATTERN.exec(html)?.[1] ?? html.replace(HEAD_PATTERN, " ");
|
|
4114
|
+
const visibleTextLength = visibleTextOf(body).length;
|
|
4115
|
+
if (visibleTextLength >= SHELL_SUSPICION_THRESHOLD) {
|
|
4116
|
+
return { isShell: false, visibleTextLength };
|
|
4117
|
+
}
|
|
4118
|
+
if (CHALLENGE_PATTERN.test(html)) {
|
|
4119
|
+
return { isShell: true, reason: "anti-bot-challenge", visibleTextLength };
|
|
4120
|
+
}
|
|
4121
|
+
if (MOUNT_POINT_PATTERN.test(body)) {
|
|
4122
|
+
return { isShell: true, reason: "spa-shell", visibleTextLength };
|
|
4123
|
+
}
|
|
4124
|
+
if (noscriptMentionsJavascript(html)) {
|
|
4125
|
+
return { isShell: true, reason: "noscript-only", visibleTextLength };
|
|
4126
|
+
}
|
|
4127
|
+
if (visibleTextLength < SHELL_EMPTY_THRESHOLD) {
|
|
4128
|
+
return { isShell: true, reason: "empty-body", visibleTextLength };
|
|
4129
|
+
}
|
|
4130
|
+
return { isShell: false, visibleTextLength };
|
|
4131
|
+
};
|
|
4132
|
+
|
|
4133
|
+
// src/extract-meta-refresh.ts
|
|
4134
|
+
var MAX_META_REFRESH_DELAY_SECONDS = 10;
|
|
4135
|
+
var META_TAG_PATTERN = /<meta\b[^>]*>/gi;
|
|
4136
|
+
var HTTP_EQUIV_REFRESH_PATTERN = /\bhttp-equiv\s*=\s*("\s*refresh\s*"|'\s*refresh\s*'|refresh\b)/i;
|
|
4137
|
+
var CONTENT_ATTR_PATTERN = /\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
|
|
4138
|
+
var CONTENT_VALUE_PATTERN = /^\s*(\d+(?:\.\d+)?)\s*[;,]\s*url\s*=\s*(?:"([^"]*)"|'([^']*)'|(.+?))\s*$/i;
|
|
4139
|
+
var resolveTarget = (raw, baseUrl) => {
|
|
4140
|
+
try {
|
|
4141
|
+
return baseUrl === void 0 ? new URL(raw) : new URL(raw, baseUrl);
|
|
4142
|
+
} catch {
|
|
4143
|
+
return void 0;
|
|
4144
|
+
}
|
|
4145
|
+
};
|
|
4146
|
+
var extractMetaRefresh = (html, baseUrl) => {
|
|
4147
|
+
for (const [tag] of html.matchAll(META_TAG_PATTERN)) {
|
|
4148
|
+
if (!HTTP_EQUIV_REFRESH_PATTERN.test(tag)) {
|
|
4149
|
+
continue;
|
|
4150
|
+
}
|
|
4151
|
+
const contentMatch = CONTENT_ATTR_PATTERN.exec(tag);
|
|
4152
|
+
const content = contentMatch?.[1] ?? contentMatch?.[2] ?? contentMatch?.[3];
|
|
4153
|
+
if (!content) {
|
|
4154
|
+
continue;
|
|
4155
|
+
}
|
|
4156
|
+
const value = CONTENT_VALUE_PATTERN.exec(content);
|
|
4157
|
+
if (!value) {
|
|
4158
|
+
continue;
|
|
4159
|
+
}
|
|
4160
|
+
const delaySeconds = Number.parseFloat(value[1] ?? "");
|
|
4161
|
+
const raw = (value[2] ?? value[3] ?? value[4] ?? "").trim();
|
|
4162
|
+
if (!raw || delaySeconds > MAX_META_REFRESH_DELAY_SECONDS) {
|
|
4163
|
+
continue;
|
|
4164
|
+
}
|
|
4165
|
+
const target = resolveTarget(raw, baseUrl);
|
|
4166
|
+
if (!target || target.protocol !== "http:" && target.protocol !== "https:") {
|
|
4167
|
+
continue;
|
|
4168
|
+
}
|
|
4169
|
+
if (baseUrl !== void 0 && resolveTarget(baseUrl, void 0)?.href === target.href) {
|
|
4170
|
+
continue;
|
|
4171
|
+
}
|
|
4172
|
+
return { url: target.href, delaySeconds };
|
|
4173
|
+
}
|
|
4174
|
+
return void 0;
|
|
4175
|
+
};
|
|
4176
|
+
|
|
3393
4177
|
// src/cli/commands/webforai/loadHtml.ts
|
|
3394
|
-
import
|
|
4178
|
+
import fs8 from "node:fs/promises";
|
|
3395
4179
|
import { fileURLToPath } from "node:url";
|
|
3396
4180
|
import boxen from "boxen";
|
|
3397
4181
|
import pc from "picocolors";
|
|
@@ -3399,9 +4183,20 @@ import { chromium as chromium2 } from "playwright-core";
|
|
|
3399
4183
|
|
|
3400
4184
|
// src/loaders/fetch.ts
|
|
3401
4185
|
var USER_AGENT = "mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/125.0.0.0 safari/537.36";
|
|
4186
|
+
var MAX_META_REFRESH_HOPS = 3;
|
|
3402
4187
|
var loadHtml = async (url, userAgent = USER_AGENT) => {
|
|
3403
|
-
|
|
3404
|
-
|
|
4188
|
+
let current = url;
|
|
4189
|
+
let html = "";
|
|
4190
|
+
for (let hop = 0; hop <= MAX_META_REFRESH_HOPS; hop++) {
|
|
4191
|
+
const response = await fetch(current, { headers: { "User-Agent": userAgent } });
|
|
4192
|
+
html = await response.text();
|
|
4193
|
+
const refresh = extractMetaRefresh(html, response.url || current);
|
|
4194
|
+
if (!refresh) {
|
|
4195
|
+
break;
|
|
4196
|
+
}
|
|
4197
|
+
current = refresh.url;
|
|
4198
|
+
}
|
|
4199
|
+
return html;
|
|
3405
4200
|
};
|
|
3406
4201
|
|
|
3407
4202
|
// src/loaders/playwright.ts
|
|
@@ -3419,40 +4214,42 @@ var SUPER_BYPASS_DEVICE = {
|
|
|
3419
4214
|
var loadHtml2 = async (url, options) => {
|
|
3420
4215
|
const { browser, waitUntil, timeout, superBypassMode } = options ?? {};
|
|
3421
4216
|
const _browser = browser ?? await chromium.launch({ headless: true });
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
await
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
await
|
|
3433
|
-
|
|
3434
|
-
|
|
4217
|
+
let context;
|
|
4218
|
+
try {
|
|
4219
|
+
context = await _browser.newContext(superBypassMode ? SUPER_BYPASS_DEVICE : devices["Desktop Chrome"]);
|
|
4220
|
+
if (superBypassMode) {
|
|
4221
|
+
await context.addInitScript(() => {
|
|
4222
|
+
Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
|
|
4223
|
+
Object.defineProperty(navigator, "languages", { get: () => ["en-US", "en"] });
|
|
4224
|
+
Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5] });
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
4227
|
+
const page = await context.newPage();
|
|
4228
|
+
if (superBypassMode) {
|
|
4229
|
+
await page.route("**/*.js", (route) => {
|
|
4230
|
+
if (route.request().url().includes("captcha-delivery")) {
|
|
4231
|
+
return route.abort();
|
|
4232
|
+
}
|
|
4233
|
+
return route.continue();
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
await page.goto(url, { waitUntil: waitUntil ?? "load", timeout });
|
|
4237
|
+
await page.evaluate(() => {
|
|
4238
|
+
const elements = document.querySelectorAll("*");
|
|
4239
|
+
for (const element3 of elements) {
|
|
4240
|
+
const rect = element3.getBoundingClientRect();
|
|
4241
|
+
element3.setAttribute("data-rwidth", rect.width.toString());
|
|
4242
|
+
element3.setAttribute("data-rheight", rect.height.toString());
|
|
3435
4243
|
}
|
|
3436
|
-
return route.continue();
|
|
3437
4244
|
});
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
element3.setAttribute("data-rwidth", rect.width.toString());
|
|
3445
|
-
element3.setAttribute("data-rheight", rect.height.toString());
|
|
4245
|
+
return await page.content();
|
|
4246
|
+
} finally {
|
|
4247
|
+
if (browser) {
|
|
4248
|
+
await context?.close();
|
|
4249
|
+
} else {
|
|
4250
|
+
await _browser.close();
|
|
3446
4251
|
}
|
|
3447
|
-
});
|
|
3448
|
-
const html = await page.content();
|
|
3449
|
-
await page.close();
|
|
3450
|
-
if (browser) {
|
|
3451
|
-
await context.close();
|
|
3452
|
-
} else {
|
|
3453
|
-
await _browser.close();
|
|
3454
4252
|
}
|
|
3455
|
-
return html;
|
|
3456
4253
|
};
|
|
3457
4254
|
|
|
3458
4255
|
// src/cli/commands/webforai/loadHtml.ts
|
|
@@ -3462,23 +4259,23 @@ var debugLog = (enabled, message) => {
|
|
|
3462
4259
|
}
|
|
3463
4260
|
};
|
|
3464
4261
|
var checkPlaywrightAvailable = async () => {
|
|
3465
|
-
const
|
|
4262
|
+
const path6 = chromium2.executablePath();
|
|
3466
4263
|
try {
|
|
3467
|
-
await
|
|
4264
|
+
await fs8.access(path6);
|
|
3468
4265
|
return true;
|
|
3469
4266
|
} catch {
|
|
3470
4267
|
return false;
|
|
3471
4268
|
}
|
|
3472
4269
|
};
|
|
3473
4270
|
var getPlaywrightVersion = async () => {
|
|
3474
|
-
const
|
|
3475
|
-
const pwPackageJson = await
|
|
4271
|
+
const path6 = await import.meta.resolve("playwright-core/package.json");
|
|
4272
|
+
const pwPackageJson = await fs8.readFile(fileURLToPath(path6), "utf-8").then((res) => JSON.parse(res.toString())).catch(() => null);
|
|
3476
4273
|
return pwPackageJson?.version;
|
|
3477
4274
|
};
|
|
3478
4275
|
var loadHtml3 = async (sourcePath, loader, options) => {
|
|
3479
4276
|
if (loader === "local") {
|
|
3480
4277
|
debugLog(options.debug, `Loading HTML from local file: ${sourcePath}`);
|
|
3481
|
-
const content = await
|
|
4278
|
+
const content = await fs8.readFile(sourcePath, "utf-8");
|
|
3482
4279
|
debugLog(options.debug, `HTML loaded: ${content.slice(0, 100)}`);
|
|
3483
4280
|
return content;
|
|
3484
4281
|
}
|
|
@@ -3545,12 +4342,13 @@ var convertViaPlatform = async (run) => {
|
|
|
3545
4342
|
if (run.mode !== "default") {
|
|
3546
4343
|
console.error("[webforai] --mode is applied by local conversion only and is ignored by the platform loader");
|
|
3547
4344
|
}
|
|
3548
|
-
debugLog2(run.debug, `platform scrape: ${run.source} (engine=${run.engine ?? "
|
|
4345
|
+
debugLog2(run.debug, `platform scrape: ${run.source} (engine=${run.engine ?? "auto"})`);
|
|
3549
4346
|
const result = await platform.scrape({
|
|
3550
4347
|
url: run.source,
|
|
3551
4348
|
engine: run.engine,
|
|
3552
4349
|
region: run.region,
|
|
3553
4350
|
screenshot: run.screenshot ? true : void 0,
|
|
4351
|
+
respectRobotsTxt: run.respectRobotsTxt ? true : void 0,
|
|
3554
4352
|
convert: { extractor: run.extractor, frontmatter: run.frontmatter }
|
|
3555
4353
|
});
|
|
3556
4354
|
return {
|
|
@@ -3562,7 +4360,8 @@ var convertViaPlatform = async (run) => {
|
|
|
3562
4360
|
markdown: result.markdown,
|
|
3563
4361
|
metadata: result.metadata,
|
|
3564
4362
|
credits: result.credits,
|
|
3565
|
-
screenshotUrl: result.screenshotUrl
|
|
4363
|
+
screenshotUrl: result.screenshotUrl,
|
|
4364
|
+
warning: result.warning
|
|
3566
4365
|
};
|
|
3567
4366
|
};
|
|
3568
4367
|
var convertLocally = async (run) => {
|
|
@@ -3575,18 +4374,21 @@ var convertLocally = async (run) => {
|
|
|
3575
4374
|
extractors: extractorOptions(run.extractor),
|
|
3576
4375
|
...run.mode === "ai" ? AI_MODE_OPTIONS : {}
|
|
3577
4376
|
});
|
|
4377
|
+
const shell = run.loader === "fetch" ? detectClientShell(html) : void 0;
|
|
4378
|
+
const warning = shell?.isShell ? "This page looks like it renders client-side; the fetched HTML has no readable content. Try --engine auto (hosted platform, renders JavaScript) or --loader playwright." : void 0;
|
|
3578
4379
|
return {
|
|
3579
4380
|
source: run.source,
|
|
3580
4381
|
loader: run.loader,
|
|
3581
4382
|
url: sourceUrl,
|
|
3582
4383
|
markdown,
|
|
3583
|
-
metadata
|
|
4384
|
+
metadata,
|
|
4385
|
+
warning
|
|
3584
4386
|
};
|
|
3585
4387
|
};
|
|
3586
4388
|
var executeRun = (run) => run.loader === "platform" ? convertViaPlatform(run) : convertLocally(run);
|
|
3587
4389
|
var writeOutputFile = async (outputPath, markdown) => {
|
|
3588
|
-
await
|
|
3589
|
-
await
|
|
4390
|
+
await fs9.mkdir(path5.dirname(path5.resolve(outputPath)), { recursive: true });
|
|
4391
|
+
await fs9.writeFile(outputPath, markdown);
|
|
3590
4392
|
};
|
|
3591
4393
|
var runCommand = async (run) => {
|
|
3592
4394
|
const envelope = await executeRun(run);
|
|
@@ -3604,6 +4406,9 @@ var runCommand = async (run) => {
|
|
|
3604
4406
|
process.stdout.write(envelope.markdown.endsWith("\n") ? envelope.markdown : `${envelope.markdown}
|
|
3605
4407
|
`);
|
|
3606
4408
|
}
|
|
4409
|
+
if (envelope.warning) {
|
|
4410
|
+
console.error(`[webforai] warning: ${envelope.warning}`);
|
|
4411
|
+
}
|
|
3607
4412
|
if (envelope.screenshotUrl) {
|
|
3608
4413
|
console.error(`[webforai] screenshot (expires ~24h): ${envelope.screenshotUrl}`);
|
|
3609
4414
|
}
|
|
@@ -3614,20 +4419,21 @@ var runCommand = async (run) => {
|
|
|
3614
4419
|
|
|
3615
4420
|
// src/cli/commands/webforai/interactive.ts
|
|
3616
4421
|
var ENGINE_HINTS = {
|
|
4422
|
+
auto: "Cheapest first, renders when the page needs JavaScript \xB7 1\u20132 credits",
|
|
3617
4423
|
fetch: "Plain fetch \xB7 1 credit",
|
|
3618
|
-
browser: "Browser rendering, screenshots \xB7
|
|
3619
|
-
"proxy-fetch": "Rotating-proxy egress \xB7 2 credits",
|
|
3620
|
-
"proxy-browser": "Browser rendering behind the proxy \xB7
|
|
4424
|
+
browser: "Browser rendering, screenshots \xB7 2 credits",
|
|
4425
|
+
"proxy-fetch": "Rotating-proxy egress \xB7 2 credits \xB7 paid plan",
|
|
4426
|
+
"proxy-browser": "Browser rendering behind the proxy \xB7 3 credits \xB7 paid plan"
|
|
3621
4427
|
};
|
|
3622
4428
|
var promptPlatformOptions = async (flags, env) => {
|
|
3623
4429
|
const engine = flags.engine ?? await select7({
|
|
3624
4430
|
message: "Select platform engine:",
|
|
3625
|
-
initialValue: "
|
|
3626
|
-
options:
|
|
4431
|
+
initialValue: "auto",
|
|
4432
|
+
options: REQUESTED_ENGINES.map((value) => ({ value, label: value, hint: ENGINE_HINTS[value] }))
|
|
3627
4433
|
});
|
|
3628
4434
|
assertContinue(engine);
|
|
3629
4435
|
const region = flags.region ?? await select7({
|
|
3630
|
-
message: "Select egress region (proxy engines only):",
|
|
4436
|
+
message: "Select egress region (proxy engines only; jp needs a paid plan):",
|
|
3631
4437
|
initialValue: "auto",
|
|
3632
4438
|
options: REGIONS.map((value) => ({ value, label: value }))
|
|
3633
4439
|
});
|
|
@@ -3647,26 +4453,18 @@ var promptPlatformOptions = async (flags, env) => {
|
|
|
3647
4453
|
var interactiveCommand = async (initialSource, flags, env = process.env) => {
|
|
3648
4454
|
intro(pc2.bold(pc2.green(`webforai CLI version ${package_default.version}`)));
|
|
3649
4455
|
const source = initialSource ?? await inputSourcePath();
|
|
3650
|
-
const loader = isUrl(source) ? flags.loader ?? await selectLoader() : "local";
|
|
3651
|
-
const
|
|
3652
|
-
|
|
3653
|
-
loader
|
|
3654
|
-
mode: "default",
|
|
3655
|
-
extractor: flags.extractor ?? "auto",
|
|
3656
|
-
frontmatter: flags.frontmatter ?? false,
|
|
3657
|
-
json: false,
|
|
3658
|
-
debug: flags.debug ?? false,
|
|
3659
|
-
screenshot: flags.screenshot ?? false
|
|
4456
|
+
const loader = isUrl(source) ? flags.loader ?? (flags.engine !== void 0 || flags.region !== void 0 ? "platform" : await selectLoader()) : "local";
|
|
4457
|
+
const selectedFlags = {
|
|
4458
|
+
...flags,
|
|
4459
|
+
loader: loader === "local" ? flags.loader : loader
|
|
3660
4460
|
};
|
|
3661
4461
|
if (loader === "platform") {
|
|
3662
4462
|
const platform = await promptPlatformOptions(flags, env);
|
|
3663
|
-
|
|
3664
|
-
run.region = platform.region;
|
|
3665
|
-
run.apiKey = platform.apiKey;
|
|
3666
|
-
run.platformUrl = flags.platformUrl;
|
|
4463
|
+
Object.assign(selectedFlags, platform);
|
|
3667
4464
|
} else {
|
|
3668
|
-
|
|
4465
|
+
selectedFlags.mode = flags.mode ?? await selectExtractMode();
|
|
3669
4466
|
}
|
|
4467
|
+
const run = resolveRunOptions(source, selectedFlags, env);
|
|
3670
4468
|
const outputPath = flags.output ?? await inputOutputPath(source);
|
|
3671
4469
|
const s = spinner();
|
|
3672
4470
|
s.start(loader === "platform" ? "Converting via the platform..." : "Loading and converting...");
|
|
@@ -3689,73 +4487,6 @@ var interactiveCommand = async (initialSource, flags, env = process.env) => {
|
|
|
3689
4487
|
outro(pc2.green(`${pc2.bold("Done!")} Markdown saved to ${outputPath}`));
|
|
3690
4488
|
};
|
|
3691
4489
|
|
|
3692
|
-
// src/cli/commands/webforai/options.ts
|
|
3693
|
-
import fs7 from "node:fs";
|
|
3694
|
-
var UsageError = class extends Error {
|
|
3695
|
-
};
|
|
3696
|
-
var oneOf = (name, value, allowed) => {
|
|
3697
|
-
if (!allowed.includes(value)) {
|
|
3698
|
-
throw new UsageError(`invalid --${name} "${value}" (expected: ${allowed.join(" | ")})`);
|
|
3699
|
-
}
|
|
3700
|
-
return value;
|
|
3701
|
-
};
|
|
3702
|
-
var impliesPlatform = (flags) => flags.engine !== void 0 || flags.region !== void 0;
|
|
3703
|
-
var resolveLoader = (sourceIsUrl, flags) => {
|
|
3704
|
-
if (flags.loader) {
|
|
3705
|
-
return oneOf("loader", flags.loader, LOADERS);
|
|
3706
|
-
}
|
|
3707
|
-
if (impliesPlatform(flags)) {
|
|
3708
|
-
return "platform";
|
|
3709
|
-
}
|
|
3710
|
-
return sourceIsUrl ? "fetch" : "local";
|
|
3711
|
-
};
|
|
3712
|
-
var assertUsableLocalSource = (source, loader, flags) => {
|
|
3713
|
-
if (loader !== "local" && flags.loader) {
|
|
3714
|
-
throw new UsageError(`--loader ${loader} needs a URL source, but "${source}" is a local path`);
|
|
3715
|
-
}
|
|
3716
|
-
if (impliesPlatform(flags)) {
|
|
3717
|
-
throw new UsageError("--engine/--region need a URL source (the platform cannot read local files)");
|
|
3718
|
-
}
|
|
3719
|
-
if (!fs7.existsSync(source)) {
|
|
3720
|
-
throw new UsageError(`source not found: "${source}" (URLs must start with http:// or https://)`);
|
|
3721
|
-
}
|
|
3722
|
-
};
|
|
3723
|
-
var applyPlatformFields = (resolved, flags, env) => {
|
|
3724
|
-
resolved.engine = flags.engine ? oneOf("engine", flags.engine, ENGINES) : void 0;
|
|
3725
|
-
resolved.region = flags.region ? oneOf("region", flags.region, REGIONS) : void 0;
|
|
3726
|
-
resolved.apiKey = flags.apiKey ?? env[API_KEY_ENV];
|
|
3727
|
-
resolved.platformUrl = flags.platformUrl ?? env[PLATFORM_URL_ENV];
|
|
3728
|
-
if (!resolved.apiKey) {
|
|
3729
|
-
throw new UsageError(
|
|
3730
|
-
`the platform loader needs an API key: pass --api-key or set ${API_KEY_ENV} (create one at https://platform.webforai.dev/dashboard)`
|
|
3731
|
-
);
|
|
3732
|
-
}
|
|
3733
|
-
};
|
|
3734
|
-
var resolveRunOptions = (source, flags, env = process.env) => {
|
|
3735
|
-
const sourceIsUrl = isUrl(source);
|
|
3736
|
-
const loader = resolveLoader(sourceIsUrl, flags);
|
|
3737
|
-
if (!sourceIsUrl) {
|
|
3738
|
-
assertUsableLocalSource(source, loader, flags);
|
|
3739
|
-
}
|
|
3740
|
-
const resolved = {
|
|
3741
|
-
source,
|
|
3742
|
-
loader: sourceIsUrl ? loader : "local",
|
|
3743
|
-
mode: flags.mode ? oneOf("mode", flags.mode, MODES) : "default",
|
|
3744
|
-
extractor: flags.extractor ? oneOf("extractor", flags.extractor, EXTRACTORS) : "auto",
|
|
3745
|
-
frontmatter: flags.frontmatter ?? false,
|
|
3746
|
-
output: flags.output,
|
|
3747
|
-
json: flags.json ?? false,
|
|
3748
|
-
debug: flags.debug ?? false,
|
|
3749
|
-
screenshot: flags.screenshot ?? false
|
|
3750
|
-
};
|
|
3751
|
-
if (resolved.loader === "platform") {
|
|
3752
|
-
applyPlatformFields(resolved, flags, env);
|
|
3753
|
-
} else if (flags.screenshot) {
|
|
3754
|
-
throw new UsageError("--screenshot is only available with the platform loader (browser engines)");
|
|
3755
|
-
}
|
|
3756
|
-
return resolved;
|
|
3757
|
-
};
|
|
3758
|
-
|
|
3759
4490
|
// src/cli/bin.ts
|
|
3760
4491
|
var HELP_EPILOGUE = `
|
|
3761
4492
|
Examples:
|
|
@@ -3764,7 +4495,9 @@ Examples:
|
|
|
3764
4495
|
$ webforai ./page.html --extractor none convert a local file, no extraction
|
|
3765
4496
|
$ webforai https://example.com --json | jq .markdown
|
|
3766
4497
|
$ webforai https://spa.example.com -l playwright render JS in local Chromium
|
|
3767
|
-
$ webforai https://example.com --engine
|
|
4498
|
+
$ webforai https://example.com --engine auto hosted platform, renders JS when needed
|
|
4499
|
+
$ webforai crawl https://docs.example.com -o docs --llms-txt crawl a site (platform) + llms.txt
|
|
4500
|
+
$ webforai batch --file urls.txt -o pages many URLs, one .md per page (platform)
|
|
3768
4501
|
$ webforai -i guided interactive mode
|
|
3769
4502
|
$ webforai skill --install install the Agent Skill for this CLI
|
|
3770
4503
|
|
|
@@ -3777,9 +4510,23 @@ Exit codes:
|
|
|
3777
4510
|
|
|
3778
4511
|
Output contract:
|
|
3779
4512
|
stdout carries only the Markdown (or the --json envelope); all logs go to stderr.
|
|
4513
|
+
|
|
4514
|
+
Docs:
|
|
4515
|
+
https://webforai.dev/cli this CLI
|
|
4516
|
+
https://webforai.dev/platform the hosted platform (--engine / --region)
|
|
4517
|
+
${DEFAULT_BASE_URL}/dashboard API keys, usage and billing
|
|
3780
4518
|
`;
|
|
3781
|
-
|
|
3782
|
-
|
|
4519
|
+
var DASHBOARD_ERRORS = /* @__PURE__ */ new Set(["invalid_api_key", "payment_required", "spend_cap_reached"]);
|
|
4520
|
+
var activePlatformUrl;
|
|
4521
|
+
var rememberPlatformUrl = (flags) => {
|
|
4522
|
+
activePlatformUrl = flags.platformUrl ?? process.env[PLATFORM_URL_ENV];
|
|
4523
|
+
};
|
|
4524
|
+
program.name("webforai").enablePositionalOptions().exitOverride().description("Convert web pages and local HTML to clean, LLM-ready Markdown.").version(package_default.version, "-v, --version", "output the current version").addHelpText("after", HELP_EPILOGUE).showSuggestionAfterError();
|
|
4525
|
+
program.argument("[source]", "URL or local HTML file to convert").option("-o, --output <path>", "write Markdown to a file instead of stdout").option("-l, --loader <loader>", `how HTML is acquired (${LOADERS.join(" | ")}; local files are automatic)`).option("-m, --mode <mode>", `conversion mode (${MODES.join(" | ")}); ai strips links/tables/images`).option("--extractor <extractor>", `main-content extraction preset (${EXTRACTORS.join(" | ")})`, void 0).option("--frontmatter", "prepend YAML front matter built from the page metadata").option("--json", "print a JSON envelope {source, loader, url, markdown, metadata, ...} to stdout").option(
|
|
4526
|
+
"--engine <engine>",
|
|
4527
|
+
"platform engine (auto | fetch | browser | proxy-fetch | proxy-browser; proxy-* need a paid plan); implies -l platform"
|
|
4528
|
+
).option("--region <region>", "platform proxy egress region (auto | jp; jp needs a paid plan); implies -l platform").option("--screenshot", "platform browser engines: also capture a screenshot (expiring URL)").option("--respect-robots", "platform loader: honor the target site's robots.txt rules (off by default)").option("--api-key <key>", `platform API key (defaults to $${API_KEY_ENV})`).option("--platform-url <url>", `platform base URL (defaults to $${PLATFORM_URL_ENV} or the hosted instance)`).option("-i, --interactive", "guided prompt flow (the pre-v3 default)").option("-d, --debug", "verbose logs on stderr").action(async (source, flags) => {
|
|
4529
|
+
rememberPlatformUrl(flags);
|
|
3783
4530
|
if (flags.interactive) {
|
|
3784
4531
|
await interactiveCommand(source, flags);
|
|
3785
4532
|
return;
|
|
@@ -3789,6 +4536,46 @@ program.argument("[source]", "URL or local HTML file to convert").option("-o, --
|
|
|
3789
4536
|
}
|
|
3790
4537
|
await runCommand(resolveRunOptions(source, flags));
|
|
3791
4538
|
});
|
|
4539
|
+
var collect = (value, previous = []) => [...previous, value];
|
|
4540
|
+
var addPlatformJobOptions = (command) => command.requiredOption("-o, --output <dir>", "directory for the Markdown files (one per page; created if missing)").option(
|
|
4541
|
+
"--engine <engine>",
|
|
4542
|
+
"platform engine (auto | fetch | browser | proxy-fetch | proxy-browser; proxy-* need a paid plan)"
|
|
4543
|
+
).option("--region <region>", "proxy egress region (auto | jp); jp runs on the paid proxy tier").option("--extractor <extractor>", `main-content extraction preset (${EXTRACTORS.join(" | ")})`).option("--frontmatter", "prepend YAML front matter built from each page's metadata").option("--json", "print a JSON envelope {jobId, pages[], failures[], ...} to stdout instead of file paths").option("--timeout <seconds>", "stop waiting after this long (the job keeps running); default 1800").option("--api-key <key>", `platform API key (defaults to $${API_KEY_ENV})`).option("--platform-url <url>", `platform base URL (defaults to $${PLATFORM_URL_ENV} or the hosted instance)`).option("-d, --debug", "verbose logs on stderr");
|
|
4544
|
+
var JOB_OUTPUT_HELP = `
|
|
4545
|
+
Output:
|
|
4546
|
+
One .md per converted page under --output, mirroring URL paths (/docs/intro \u2192 docs/intro.md).
|
|
4547
|
+
stdout lists the written files (or the --json envelope); progress and the summary go to stderr.
|
|
4548
|
+
Exit 1 when the job failed or converted no pages; failed pages are listed and never billed.
|
|
4549
|
+
`;
|
|
4550
|
+
addPlatformJobOptions(
|
|
4551
|
+
program.command("crawl").description("crawl a site on the hosted platform and write one Markdown file per page").argument("<url>", "seed URL; only same-origin links are followed")
|
|
4552
|
+
).option("--max-depth <n>", "link depth from the seed (0-5, default 2)").option("--limit <n>", "maximum pages (1-500, default 50)").option("--include <regex>", "only crawl pathnames matching this regex (repeatable)", collect).option("--exclude <regex>", "skip pathnames matching this regex (repeatable)", collect).option(
|
|
4553
|
+
"--sitemap <mode>",
|
|
4554
|
+
"use the site's sitemaps: skip (default) | include (add their URLs) | only (no link following)"
|
|
4555
|
+
).option("--respect-robots", "honor robots.txt (the default for crawls)").option("--no-respect-robots", "do not read robots.txt").option("--llms-txt", "also write llms.txt (page index) and llms-full.txt (all pages) per llmstxt.org").addHelpText(
|
|
4556
|
+
"after",
|
|
4557
|
+
`${JOB_OUTPUT_HELP}
|
|
4558
|
+
Examples:
|
|
4559
|
+
$ webforai crawl https://docs.example.com -o docs --limit 100 --include '^/docs' --llms-txt
|
|
4560
|
+
$ webforai crawl https://blog.example.com -o blog --sitemap only --limit 500
|
|
4561
|
+
`
|
|
4562
|
+
).action(async (url, flags) => {
|
|
4563
|
+
rememberPlatformUrl(flags);
|
|
4564
|
+
await runJobCommand(resolveJobOptions("crawl", [url], flags));
|
|
4565
|
+
});
|
|
4566
|
+
addPlatformJobOptions(
|
|
4567
|
+
program.command("batch").description("convert many URLs on the hosted platform, one Markdown file per page").argument("[urls...]", "URLs to convert (up to 100 per job)")
|
|
4568
|
+
).option("-f, --file <path>", "read URLs from a file, one per line ('-' for stdin; # comments allowed)").option("--respect-robots", "honor each site's robots.txt (off by default for batches)").addHelpText(
|
|
4569
|
+
"after",
|
|
4570
|
+
`${JOB_OUTPUT_HELP}
|
|
4571
|
+
Examples:
|
|
4572
|
+
$ webforai batch https://a.example/post https://b.example/news -o pages
|
|
4573
|
+
$ cat urls.txt | webforai batch --file - -o pages --json
|
|
4574
|
+
`
|
|
4575
|
+
).action(async (urls, flags) => {
|
|
4576
|
+
rememberPlatformUrl(flags);
|
|
4577
|
+
await runJobCommand(resolveJobOptions("batch", urls, flags));
|
|
4578
|
+
});
|
|
3792
4579
|
program.command("skill").description("print the Agent Skill (SKILL.md) for this CLI, or install it via the skills CLI").option("--install", "install through `npx skills add` (interactive agent picker; flags below forward to it)").option("-g, --global", "forward to skills add: install user-level instead of project-level").option("-a, --agent <agents>", "forward to skills add: agents to install to ('*' for all)").option("-y, --yes", "forward to skills add: skip confirmation prompts").option("--copy", "forward to skills add: copy files instead of symlinking").option("--all", "forward to skills add: all skills, all agents, no prompts").option("--dir <path>", "offline fallback: write <path>/webforai/SKILL.md directly, without the skills CLI").addHelpText("after", "\nAlso installable straight from the repository: npx skills add inaridiy/webforai\n").action(async (flags) => {
|
|
3793
4580
|
await skillCommand(flags);
|
|
3794
4581
|
});
|
|
@@ -3800,8 +4587,19 @@ var fail = (error) => {
|
|
|
3800
4587
|
console.error(`error: ${error.message}`);
|
|
3801
4588
|
process.exit(2);
|
|
3802
4589
|
}
|
|
4590
|
+
if (error instanceof PlatformApiError) {
|
|
4591
|
+
const retry = error.retryAfter === void 0 ? "" : ` (retryAfter: ${error.retryAfter}s)`;
|
|
4592
|
+
console.error(`error: ${error.code}: ${error.message}${retry}`);
|
|
4593
|
+
if (DASHBOARD_ERRORS.has(error.code) && !error.message.includes("/dashboard")) {
|
|
4594
|
+
console.error(`hint: manage API keys and credits at ${platformDashboardUrl(activePlatformUrl)}`);
|
|
4595
|
+
}
|
|
4596
|
+
process.exit(1);
|
|
4597
|
+
}
|
|
4598
|
+
if (error instanceof JobIncompleteError) {
|
|
4599
|
+
console.error(`error: ${error.message}`);
|
|
4600
|
+
process.exit(1);
|
|
4601
|
+
}
|
|
3803
4602
|
console.error(error instanceof Error ? `error: ${error.message}` : `error: ${String(error)}`);
|
|
3804
4603
|
process.exit(1);
|
|
3805
4604
|
};
|
|
3806
|
-
program.exitOverride();
|
|
3807
4605
|
program.parseAsync().catch(fail);
|