webcrawl 0.0.0-stage → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -2
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +16 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +308 -0
- package/dist/format.d.ts +9 -0
- package/dist/format.js +113 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/setup.d.ts +38 -0
- package/dist/setup.js +184 -0
- package/package.json +59 -4
package/README.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
-
#
|
|
1
|
+
# webcrawl — Webcrawl Anything CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Crawl, search, and extract structured evidence from any website, SPA, PDF, Office file, YouTube video, GitHub repo, ArXiv paper, shopping offer, travel fare, or CVE advisory — from a single CLI and MCP server.**
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
One command registers the `@webcrawl/mcp` server and installs the `webcrawl-anything` Agent Skill (`SKILL.md`) for Claude Code, Cursor, and Codex:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx webcrawl setup
|
|
11
|
+
npx webcrawl doctor
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## CLI Commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# List all supported targets and their requirements
|
|
18
|
+
npx webcrawl targets
|
|
19
|
+
|
|
20
|
+
# Scrape any website, GitHub repo, ArXiv paper, PDF, or YouTube video
|
|
21
|
+
npx webcrawl scrape https://github.com/torvalds/linux
|
|
22
|
+
npx webcrawl scrape https://arxiv.org/abs/1706.03762
|
|
23
|
+
|
|
24
|
+
# Search the web or structured verticals
|
|
25
|
+
npx webcrawl search "react 19 compiler" -n 5
|
|
26
|
+
npx webcrawl search "sony wh-1000xm5" -c shopping
|
|
27
|
+
npx webcrawl search "boutique hotels in shibuya" -c hotels
|
|
28
|
+
npx webcrawl search "CVE-2024-3094 xz utils" -c security --sub cve
|
|
29
|
+
|
|
30
|
+
# Discover URLs and crawl multi-page documentation
|
|
31
|
+
npx webcrawl map https://docs.stripe.com --filter webhook -n 50
|
|
32
|
+
npx webcrawl crawl https://docs.stripe.com/webhooks -n 5
|
|
33
|
+
|
|
34
|
+
# Connect to webcrawl.app cloud for managed stealth browsers & global cache
|
|
35
|
+
npx webcrawl login
|
|
36
|
+
npx webcrawl whoami
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Documentation & Source
|
|
40
|
+
|
|
41
|
+
- **GitHub Repository:** [brainfish-ai/webcrawl-anything](https://github.com/brainfish-ai/webcrawl-anything)
|
|
42
|
+
- **Cloud Platform:** [https://webcrawl.app](https://webcrawl.app)
|
|
43
|
+
- **License:** MIT
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from "./cli.js";
|
|
3
|
+
async function readStdin() {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
for await (const c of process.stdin) {
|
|
6
|
+
chunks.push(c);
|
|
7
|
+
}
|
|
8
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
9
|
+
}
|
|
10
|
+
const code = await main(process.argv.slice(2), {
|
|
11
|
+
out: (s) => process.stdout.write(s),
|
|
12
|
+
err: (s) => process.stderr.write(s),
|
|
13
|
+
readStdin,
|
|
14
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
15
|
+
}, {});
|
|
16
|
+
process.exitCode = code;
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Io {
|
|
2
|
+
out: (s: string) => void;
|
|
3
|
+
err: (s: string) => void;
|
|
4
|
+
/** Whole stdin as text (only called when needed). */
|
|
5
|
+
readStdin: () => Promise<string>;
|
|
6
|
+
isTTY: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface Deps {
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
/** Config file path override (tests). */
|
|
12
|
+
configFile?: string;
|
|
13
|
+
/** Home directory override for `webcrawl setup` tests. */
|
|
14
|
+
homeDir?: string;
|
|
15
|
+
/** Working directory override for `webcrawl setup` tests. */
|
|
16
|
+
cwd?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const HELP = "Webcrawl Anything: search, scrape, parse and crawl any target on the internet\n\nUsage: webcrawl <command> [options]\n\nCommands\n search <query> Search the web or verticals. -c category, --sub, -n limit, --site <domain>\n scrape <url> Inspect a URL (website, SPA, PDF, Office, YouTube, GitHub, ArXiv, llms.txt)\n map <url> List a site's URLs. --filter <text>, -n limit\n crawl <url> Read several pages. -n max pages (default 5, max 25)\n extract <url> Structured JSON. --prompt <text> or --schema <file.json>\n rerank <query> Rank documents. --doc <text> (repeatable), --file <path>, or lines on stdin. -n top\n targets List all supported crawl & search targets and their requirements\n categories List search categories and vertical filters\n setup Register Webcrawl MCP server and Agent Skill (Claude, Cursor, Codex, Gemini, Windsurf)\n doctor Check local crawl engine, parsers, stealth browser, and cloud configuration\n login Save your webcrawl.app API key (--key <key>, or paste when asked)\n whoami Show execution mode (local vs cloud), key source, endpoint and credit usage\n\nOptions\n --mode <m> Execution mode: auto (default: local without key, cloud with key), local, or cloud\n --json Print the raw JSON response\n --api-key <key> Override WEBCRAWL_API_KEY\n --url <base> Override WEBCRAWL_URL (default https://webcrawl.app)\n --country, --city Where to search from (default: this machine's timezone)\n --no-cache Skip cached pages (scrape)\n --agent <name> Target agent for setup: claude, cursor, codex, gemini, windsurf, or all (repeatable)\n --schema-profile MCP schema profile for setup: compact (default) or full\n -h, --help\n\nConfig order: flags, then WEBCRAWL_API_KEY / WEBCRAWL_URL / WEBCRAWL_MODE, then ~/.config/webcrawl/config.json";
|
|
19
|
+
export declare function main(argv: string[], io: Io, deps?: Deps): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { WebcrawlClient, WebcrawlError, configPath, describeError, readConfigFile, resolveConfig, writeConfigFile, } from "@webcrawl/client";
|
|
5
|
+
import { formatCrawl, formatCredits, formatMap, formatRerank, formatScrape, formatSearch, formatTargets, } from "./format.js";
|
|
6
|
+
import { SUPPORTED_AGENTS, runDoctor, runSetup, } from "./setup.js";
|
|
7
|
+
export const HELP = `Webcrawl Anything: search, scrape, parse and crawl any target on the internet
|
|
8
|
+
|
|
9
|
+
Usage: webcrawl <command> [options]
|
|
10
|
+
|
|
11
|
+
Commands
|
|
12
|
+
search <query> Search the web or verticals. -c category, --sub, -n limit, --site <domain>
|
|
13
|
+
scrape <url> Inspect a URL (website, SPA, PDF, Office, YouTube, GitHub, ArXiv, llms.txt)
|
|
14
|
+
map <url> List a site's URLs. --filter <text>, -n limit
|
|
15
|
+
crawl <url> Read several pages. -n max pages (default 5, max 25)
|
|
16
|
+
extract <url> Structured JSON. --prompt <text> or --schema <file.json>
|
|
17
|
+
rerank <query> Rank documents. --doc <text> (repeatable), --file <path>, or lines on stdin. -n top
|
|
18
|
+
targets List all supported crawl & search targets and their requirements
|
|
19
|
+
categories List search categories and vertical filters
|
|
20
|
+
setup Register Webcrawl MCP server and Agent Skill (Claude, Cursor, Codex, Gemini, Windsurf)
|
|
21
|
+
doctor Check local crawl engine, parsers, stealth browser, and cloud configuration
|
|
22
|
+
login Save your webcrawl.app API key (--key <key>, or paste when asked)
|
|
23
|
+
whoami Show execution mode (local vs cloud), key source, endpoint and credit usage
|
|
24
|
+
|
|
25
|
+
Options
|
|
26
|
+
--mode <m> Execution mode: auto (default: local without key, cloud with key), local, or cloud
|
|
27
|
+
--json Print the raw JSON response
|
|
28
|
+
--api-key <key> Override WEBCRAWL_API_KEY
|
|
29
|
+
--url <base> Override WEBCRAWL_URL (default https://webcrawl.app)
|
|
30
|
+
--country, --city Where to search from (default: this machine's timezone)
|
|
31
|
+
--no-cache Skip cached pages (scrape)
|
|
32
|
+
--agent <name> Target agent for setup: claude, cursor, codex, gemini, windsurf, or all (repeatable)
|
|
33
|
+
--schema-profile MCP schema profile for setup: compact (default) or full
|
|
34
|
+
-h, --help
|
|
35
|
+
|
|
36
|
+
Config order: flags, then WEBCRAWL_API_KEY / WEBCRAWL_URL / WEBCRAWL_MODE, then ~/.config/webcrawl/config.json`;
|
|
37
|
+
const OPTIONS = {
|
|
38
|
+
json: { type: "boolean" },
|
|
39
|
+
help: { type: "boolean", short: "h" },
|
|
40
|
+
"api-key": { type: "string" },
|
|
41
|
+
url: { type: "string" },
|
|
42
|
+
mode: { type: "string" },
|
|
43
|
+
category: { type: "string", short: "c" },
|
|
44
|
+
sub: { type: "string" },
|
|
45
|
+
limit: { type: "string", short: "n" },
|
|
46
|
+
site: { type: "string", multiple: true },
|
|
47
|
+
country: { type: "string" },
|
|
48
|
+
city: { type: "string" },
|
|
49
|
+
filter: { type: "string" },
|
|
50
|
+
prompt: { type: "string" },
|
|
51
|
+
schema: { type: "string" },
|
|
52
|
+
doc: { type: "string", multiple: true },
|
|
53
|
+
file: { type: "string" },
|
|
54
|
+
key: { type: "string" },
|
|
55
|
+
"no-cache": { type: "boolean" },
|
|
56
|
+
content: { type: "boolean" },
|
|
57
|
+
agent: { type: "string", multiple: true },
|
|
58
|
+
"schema-profile": { type: "string" },
|
|
59
|
+
};
|
|
60
|
+
class UsageError extends Error {
|
|
61
|
+
}
|
|
62
|
+
function int(v, name) {
|
|
63
|
+
if (v === undefined) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const n = Number(v);
|
|
67
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
68
|
+
throw new UsageError(`${name} must be a positive whole number`);
|
|
69
|
+
}
|
|
70
|
+
return n;
|
|
71
|
+
}
|
|
72
|
+
async function readKeyInteractively(io) {
|
|
73
|
+
if (!io.isTTY) {
|
|
74
|
+
return (await io.readStdin()).trim();
|
|
75
|
+
}
|
|
76
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
rl.question("Paste your API key (wc_live_…): ", (a) => {
|
|
79
|
+
rl.close();
|
|
80
|
+
resolve(a.trim());
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function resolveBackendClient(cfg, deps) {
|
|
85
|
+
// When a custom fetch mock is injected (e.g. unit tests) or effectiveMode is cloud, use WebcrawlClient
|
|
86
|
+
if (cfg.effectiveMode === "cloud" || deps.fetch) {
|
|
87
|
+
return new WebcrawlClient({
|
|
88
|
+
apiKey: cfg.apiKey,
|
|
89
|
+
baseUrl: cfg.baseUrl,
|
|
90
|
+
fetch: deps.fetch,
|
|
91
|
+
userAgent: "webcrawl-cli/0.1",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const localEngineSpec = ["..", "..", "..", "src", "local-engine.js"].join("/");
|
|
96
|
+
const localMod = (await import(localEngineSpec));
|
|
97
|
+
return localMod.createLocalEngineClient();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return new WebcrawlClient({
|
|
101
|
+
apiKey: cfg.apiKey,
|
|
102
|
+
baseUrl: cfg.baseUrl,
|
|
103
|
+
fetch: deps.fetch,
|
|
104
|
+
userAgent: "webcrawl-cli/0.1",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function main(argv, io, deps = {}) {
|
|
109
|
+
const env = deps.env ?? process.env;
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true });
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
io.err(`${e instanceof Error ? e.message : String(e)}\nRun \`webcrawl --help\`.\n`);
|
|
116
|
+
return 2;
|
|
117
|
+
}
|
|
118
|
+
const { values: v, positionals } = parsed;
|
|
119
|
+
const [cmd, ...rest] = positionals;
|
|
120
|
+
if (v.help || !cmd || cmd === "help") {
|
|
121
|
+
io.out(`${HELP}\n`);
|
|
122
|
+
return cmd || v.help ? 0 : 2;
|
|
123
|
+
}
|
|
124
|
+
const file = deps.configFile ?? configPath(env);
|
|
125
|
+
const cfg = resolveConfig({ apiKey: v["api-key"], baseUrl: v.url, mode: v.mode }, env, readConfigFile(file));
|
|
126
|
+
const client = await resolveBackendClient(cfg, deps);
|
|
127
|
+
const arg = rest.join(" ").trim();
|
|
128
|
+
const need = (what) => {
|
|
129
|
+
if (!arg) {
|
|
130
|
+
throw new UsageError(`Missing ${what}. Usage: webcrawl ${cmd} <${what}>`);
|
|
131
|
+
}
|
|
132
|
+
return arg;
|
|
133
|
+
};
|
|
134
|
+
const emit = (res, pretty) => {
|
|
135
|
+
if (v.json) {
|
|
136
|
+
io.out(`${JSON.stringify(res, null, 2)}\n`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
io.out(`${pretty}\n`);
|
|
140
|
+
const credits = formatCredits(res);
|
|
141
|
+
if (credits) {
|
|
142
|
+
io.err(`[${credits}]\n`);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
switch (cmd) {
|
|
147
|
+
case "search": {
|
|
148
|
+
const res = await client.search({
|
|
149
|
+
query: need("query"),
|
|
150
|
+
category: v.category,
|
|
151
|
+
subcategory: v.sub,
|
|
152
|
+
limit: int(v.limit, "--limit"),
|
|
153
|
+
sites: v.site,
|
|
154
|
+
country: v.country,
|
|
155
|
+
city: v.city,
|
|
156
|
+
scrapeContent: v.content,
|
|
157
|
+
});
|
|
158
|
+
emit(res, formatSearch(res));
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
case "scrape": {
|
|
162
|
+
const res = await client.scrape({ url: need("url"), formats: ["markdown"], noCache: v["no-cache"] });
|
|
163
|
+
emit(res, formatScrape(res));
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
case "map": {
|
|
167
|
+
const res = await client.map({ url: need("url"), search: v.filter, limit: int(v.limit, "--limit") });
|
|
168
|
+
emit(res, formatMap(res));
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
case "crawl": {
|
|
172
|
+
const res = await client.crawl({ url: need("url"), maxPages: int(v.limit, "--limit") });
|
|
173
|
+
emit(res, formatCrawl(res));
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
case "extract": {
|
|
177
|
+
let schema;
|
|
178
|
+
if (v.schema) {
|
|
179
|
+
try {
|
|
180
|
+
schema = JSON.parse(readFileSync(v.schema, "utf8"));
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
throw new UsageError(`Could not read schema ${v.schema}: ${e instanceof Error ? e.message : String(e)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const res = await client.extract({ url: need("url"), prompt: v.prompt, schema });
|
|
187
|
+
emit(res, JSON.stringify(res.data ?? res, null, 2));
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
case "rerank": {
|
|
191
|
+
const query = need("query");
|
|
192
|
+
let docs = v.doc ?? [];
|
|
193
|
+
if (v.file) {
|
|
194
|
+
docs = docs.concat(readFileSync(v.file, "utf8").split(/\r?\n/).filter(Boolean));
|
|
195
|
+
}
|
|
196
|
+
if (!docs.length && !io.isTTY) {
|
|
197
|
+
docs = (await io.readStdin()).split(/\r?\n/).filter(Boolean);
|
|
198
|
+
}
|
|
199
|
+
if (!docs.length) {
|
|
200
|
+
throw new UsageError("No documents. Pass --doc, --file, or pipe lines on stdin.");
|
|
201
|
+
}
|
|
202
|
+
const res = await client.rerank({ query, documents: docs, topK: int(v.limit, "--limit") });
|
|
203
|
+
emit(res, formatRerank(res));
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
case "categories": {
|
|
207
|
+
const res = await client.categories();
|
|
208
|
+
emit(res, JSON.stringify(res, null, 2));
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
case "targets": {
|
|
212
|
+
const res = await client.targets();
|
|
213
|
+
emit(res, formatTargets(res));
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
case "setup": {
|
|
217
|
+
const rawAgents = v.agent ?? [];
|
|
218
|
+
let agents;
|
|
219
|
+
if (rawAgents.includes("all")) {
|
|
220
|
+
agents = SUPPORTED_AGENTS.map((a) => a.id);
|
|
221
|
+
}
|
|
222
|
+
else if (rawAgents.length) {
|
|
223
|
+
const valid = new Set(SUPPORTED_AGENTS.map((a) => a.id));
|
|
224
|
+
for (const a of rawAgents) {
|
|
225
|
+
if (!valid.has(a)) {
|
|
226
|
+
throw new UsageError(`Unknown agent "${a}". Supported: ${SUPPORTED_AGENTS.map((x) => x.id).join(", ")}, all`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
agents = rawAgents;
|
|
230
|
+
}
|
|
231
|
+
const schemaProfile = v["schema-profile"] === "full" ? "full" : "compact";
|
|
232
|
+
const setupRes = runSetup({
|
|
233
|
+
agents,
|
|
234
|
+
schemaProfile,
|
|
235
|
+
apiKey: cfg.apiKey,
|
|
236
|
+
baseUrl: cfg.source.baseUrl !== "default" ? cfg.baseUrl : undefined,
|
|
237
|
+
homeDir: deps.homeDir,
|
|
238
|
+
cwd: deps.cwd,
|
|
239
|
+
});
|
|
240
|
+
const prettyLines = [
|
|
241
|
+
`Webcrawl Anything agent setup complete (MCP schema profile: ${setupRes.schemaProfile}):`,
|
|
242
|
+
...setupRes.configuredAgents.map((a) => ` ✓ ${a.name}\n MCP: ${a.mcpConfigPath}${a.backupPath ? ` (backup: ${a.backupPath})` : ""}\n Skill: ${a.skillPath}`),
|
|
243
|
+
"",
|
|
244
|
+
"Restart your agent to load the `webcrawl` MCP server and `webcrawl-anything` skill.",
|
|
245
|
+
].join("\n");
|
|
246
|
+
emit({ success: true, ...setupRes }, prettyLines);
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
case "doctor": {
|
|
250
|
+
const report = runDoctor(env, cfg.effectiveMode, cfg.source.apiKey);
|
|
251
|
+
const prettyLines = [
|
|
252
|
+
`Webcrawl Anything Doctor (mode: ${report.mode})`,
|
|
253
|
+
...report.checks.map((c) => {
|
|
254
|
+
const badge = c.status === "ok" ? "✓" : c.status === "optional" ? "○" : "!";
|
|
255
|
+
return ` ${badge} ${c.name.padEnd(26)} ${c.detail}`;
|
|
256
|
+
}),
|
|
257
|
+
].join("\n");
|
|
258
|
+
emit({ success: true, ...report }, prettyLines);
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
case "login": {
|
|
262
|
+
const key = (v.key ?? (await readKeyInteractively(io))).trim();
|
|
263
|
+
if (!key) {
|
|
264
|
+
throw new UsageError("No key provided.");
|
|
265
|
+
}
|
|
266
|
+
const existing = readConfigFile(file);
|
|
267
|
+
writeConfigFile({ ...existing, apiKey: key, ...(v.url ? { baseUrl: v.url } : {}) }, file);
|
|
268
|
+
io.out(`Saved your key to ${file}.\n`);
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
case "whoami": {
|
|
272
|
+
const masked = cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}…${cfg.apiKey.slice(-4)}` : "none";
|
|
273
|
+
const lines = [
|
|
274
|
+
`endpoint: ${cfg.baseUrl} (${cfg.source.baseUrl})`,
|
|
275
|
+
`mode: ${cfg.effectiveMode} (${cfg.source.mode})`,
|
|
276
|
+
`key: ${masked}${cfg.apiKey ? ` (${cfg.source.apiKey})` : ". Run `webcrawl login` or set WEBCRAWL_API_KEY (local mode works with zero key)."}`,
|
|
277
|
+
];
|
|
278
|
+
if (cfg.apiKey) {
|
|
279
|
+
const u = await client.usage();
|
|
280
|
+
if (v.json) {
|
|
281
|
+
io.out(`${JSON.stringify({ endpoint: cfg.baseUrl, mode: cfg.effectiveMode, keySource: cfg.source.apiKey, usage: u }, null, 2)}\n`);
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
const credits = (u.credits ?? u);
|
|
285
|
+
if (typeof credits.remaining === "number") {
|
|
286
|
+
lines.push(`credits: ${credits.remaining} remaining of ${credits.limit ?? "?"}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
io.out(`${lines.join("\n")}\n`);
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
default:
|
|
293
|
+
throw new UsageError(`Unknown command "${cmd}".`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
if (e instanceof UsageError) {
|
|
298
|
+
io.err(`${e.message}\nRun \`webcrawl --help\`.\n`);
|
|
299
|
+
return 2;
|
|
300
|
+
}
|
|
301
|
+
if (e instanceof WebcrawlError) {
|
|
302
|
+
io.err(`webcrawl: ${describeError(e, cfg.baseUrl)}\n`);
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
io.err(`webcrawl: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type Rec = Record<string, unknown>;
|
|
2
|
+
export declare function formatSearch(res: Rec): string;
|
|
3
|
+
export declare function formatScrape(res: Rec): string;
|
|
4
|
+
export declare function formatMap(res: Rec): string;
|
|
5
|
+
export declare function formatCrawl(res: Rec): string;
|
|
6
|
+
export declare function formatRerank(res: Rec): string;
|
|
7
|
+
export declare function formatCredits(res: Rec): string;
|
|
8
|
+
export declare function formatTargets(res: Rec): string;
|
|
9
|
+
export {};
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const isRec = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2
|
+
const str = (v) => (typeof v === "string" ? v : typeof v === "number" ? String(v) : "");
|
|
3
|
+
const first = (o, keys) => {
|
|
4
|
+
for (const k of keys) {
|
|
5
|
+
const v = str(o[k]);
|
|
6
|
+
if (v) {
|
|
7
|
+
return v;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return "";
|
|
11
|
+
};
|
|
12
|
+
const VERTICALS = ["products", "offers", "listings", "flights", "hotels", "businesses", "cves"];
|
|
13
|
+
/** CVE records have no title/price; map them onto the shared row shape. */
|
|
14
|
+
const cveRow = (o) => ({
|
|
15
|
+
title: `${str(o.id)} ${str(o.title) || str(o.summary)}`.trim(),
|
|
16
|
+
price: [str(o.cvssScore), str(o.severity), o.kev === true ? "KEV" : ""].filter(Boolean).join(" "),
|
|
17
|
+
source: str(o.source),
|
|
18
|
+
url: str(o.url),
|
|
19
|
+
});
|
|
20
|
+
const clip = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
|
|
21
|
+
function rows(items) {
|
|
22
|
+
const data = items.filter(isRec).map((o) => ({
|
|
23
|
+
title: clip(first(o, ["title", "name", "address", "airline", "productName"]) || "(untitled)", 52),
|
|
24
|
+
price: first(o, ["priceText", "price", "pricePerNight", "minPrice"]),
|
|
25
|
+
src: clip(first(o, ["provider", "portal", "vendor", "domain", "source"]), 24),
|
|
26
|
+
url: first(o, ["url", "link"]),
|
|
27
|
+
}));
|
|
28
|
+
const w = (k) => Math.max(...data.map((d) => d[k].length), 1);
|
|
29
|
+
const [wt, wp, ws] = [w("title"), w("price"), w("src")];
|
|
30
|
+
return data
|
|
31
|
+
.map((d, i) => `${String(i + 1).padStart(2)} ${d.title.padEnd(wt)} ${d.price.padEnd(wp)} ${d.src.padEnd(ws)}${d.url ? `\n ${d.url}` : ""}`)
|
|
32
|
+
.join("\n");
|
|
33
|
+
}
|
|
34
|
+
export function formatSearch(res) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const key of VERTICALS) {
|
|
37
|
+
const arr = res[key];
|
|
38
|
+
if (Array.isArray(arr) && arr.length) {
|
|
39
|
+
parts.push(`${key}\n${rows(key === "cves" ? arr.filter(isRec).map(cveRow) : arr)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const results = Array.isArray(res.results) ? res.results : Array.isArray(res.data) ? res.data : [];
|
|
43
|
+
if (results.length) {
|
|
44
|
+
parts.push(`results\n${results
|
|
45
|
+
.filter(isRec)
|
|
46
|
+
.map((r, i) => {
|
|
47
|
+
const desc = first(r, ["description", "snippet"]).replace(/\s+/g, " ").slice(0, 160);
|
|
48
|
+
return `${String(i + 1).padStart(2)} ${first(r, ["title"]) || "(untitled)"}\n ${first(r, ["url"])}${desc ? `\n ${desc}` : ""}`;
|
|
49
|
+
})
|
|
50
|
+
.join("\n")}`);
|
|
51
|
+
}
|
|
52
|
+
return parts.length ? parts.join("\n\n") : "No results.";
|
|
53
|
+
}
|
|
54
|
+
export function formatScrape(res) {
|
|
55
|
+
const d = isRec(res.data) ? res.data : res;
|
|
56
|
+
return str(d.markdown) || str(d.content) || "(no content returned)";
|
|
57
|
+
}
|
|
58
|
+
export function formatMap(res) {
|
|
59
|
+
const links = Array.isArray(res.links) ? res.links : Array.isArray(res.urls) ? res.urls : [];
|
|
60
|
+
return links.map((l) => (isRec(l) ? str(l.url) : str(l))).filter(Boolean).join("\n") || "No URLs found.";
|
|
61
|
+
}
|
|
62
|
+
export function formatCrawl(res) {
|
|
63
|
+
const pages = Array.isArray(res.data) ? res.data : Array.isArray(res.pages) ? res.pages : [];
|
|
64
|
+
if (!pages.length) {
|
|
65
|
+
return "No pages crawled.";
|
|
66
|
+
}
|
|
67
|
+
return pages
|
|
68
|
+
.filter(isRec)
|
|
69
|
+
.map((p) => {
|
|
70
|
+
const url = str(p.url) || (isRec(p.metadata) ? str(p.metadata.sourceURL) || str(p.metadata.url) : "");
|
|
71
|
+
return `# ${str(p.title) || url || "page"}\n${url}\n\n${str(p.markdown)}`;
|
|
72
|
+
})
|
|
73
|
+
.join("\n\n---\n\n");
|
|
74
|
+
}
|
|
75
|
+
export function formatRerank(res) {
|
|
76
|
+
const items = Array.isArray(res.results) ? res.results : Array.isArray(res.data) ? res.data : [];
|
|
77
|
+
if (!items.length) {
|
|
78
|
+
return JSON.stringify(res, null, 2);
|
|
79
|
+
}
|
|
80
|
+
return items
|
|
81
|
+
.map((r, i) => {
|
|
82
|
+
const o = isRec(r) ? r : {};
|
|
83
|
+
const score = typeof o.score === "number" ? o.score.toFixed(3) : "";
|
|
84
|
+
return `${String(i + 1).padStart(2)} ${score.padEnd(6)} ${clip(first(o, ["description", "markdown", "title"]).replace(/\s+/g, " "), 100)}`;
|
|
85
|
+
})
|
|
86
|
+
.join("\n");
|
|
87
|
+
}
|
|
88
|
+
export function formatCredits(res) {
|
|
89
|
+
const c = res.credits;
|
|
90
|
+
if (!isRec(c)) {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
const bits = [
|
|
94
|
+
typeof c.charged === "number" ? `charged ${c.charged}` : "",
|
|
95
|
+
typeof c.remaining === "number" ? `${c.remaining} remaining` : "",
|
|
96
|
+
].filter(Boolean);
|
|
97
|
+
return bits.join(", ");
|
|
98
|
+
}
|
|
99
|
+
export function formatTargets(res) {
|
|
100
|
+
const list = Array.isArray(res.targets) ? res.targets.filter(isRec) : [];
|
|
101
|
+
if (!list.length) {
|
|
102
|
+
return "No targets registered.";
|
|
103
|
+
}
|
|
104
|
+
return list
|
|
105
|
+
.map((t) => {
|
|
106
|
+
const id = str(t.id).padEnd(16);
|
|
107
|
+
const name = str(t.name);
|
|
108
|
+
const returns = str(t.returns);
|
|
109
|
+
const reqs = str(t.requirements);
|
|
110
|
+
return `${id} ${name}\n Returns: ${returns}\n Requires: ${reqs}`;
|
|
111
|
+
})
|
|
112
|
+
.join("\n\n");
|
|
113
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type SupportedAgentId = "claude" | "cursor" | "codex" | "gemini" | "windsurf";
|
|
2
|
+
export interface AgentTarget {
|
|
3
|
+
id: SupportedAgentId;
|
|
4
|
+
name: string;
|
|
5
|
+
mcpConfigPath: (home: string, cwd: string) => string;
|
|
6
|
+
skillPath: (home: string, cwd: string) => string;
|
|
7
|
+
}
|
|
8
|
+
export declare const SUPPORTED_AGENTS: AgentTarget[];
|
|
9
|
+
export declare const WEBCRAWL_SKILL_MD = "---\nname: webcrawl-anything\ndescription: Crawl, search, and extract structured evidence from any website, SPA, PDF, Office file, YouTube video, GitHub repo, ArXiv paper, shopping offer, travel fare, or CVE advisory.\n---\n\n# Webcrawl Anything \u2014 Agent Investigation Workflow\n\nUse **Webcrawl Anything** (`webcrawl` CLI or `webcrawl` MCP tools) whenever you need live web search, anti-bot page scraping, document/video parsing, or structured vertical extraction with provenance.\n\n## 1. Choose the Right Tool or Target\n\n1. **Structured Search (`web_search` / `webcrawl search`)**:\n - Pass `category` when the query belongs to a vertical:\n - `shopping`: Multi-retailer product prices, stock availability, ratings, shipping.\n - `flights`: IATA routes, fares, airlines, stops, cabin class.\n - `hotels`: Nightly rates, star ratings, guest reviews, amenities.\n - `real-estate`: Property listings (`subcategory: \"buy\" | \"rent\" | \"sold\"`).\n - `business`: Local businesses, clinics, phone numbers, hours, ratings.\n - `security`: CVE / GHSA records with CVSS scores, CWEs, affected/fixed versions, and CISA KEV status.\n - `academic`: ArXiv and research papers.\n - `tech` / `news` / `general`: General and developer search.\n2. **Page & Developer Target Inspection (`web_scrape` / `webcrawl scrape <url>`)**:\n - Works on any website, client-rendered SPA, GitHub repository/issue/release, ArXiv abstract page, or `llms.txt` manifest.\n3. **Binary Documents & Media (`web_parse` / `webcrawl scrape <pdf-or-youtube-url>`)**:\n - Reads PDFs (native text + automatic OCR on scanned pages), Word (`.docx`), Excel (`.xlsx`), PowerPoint (`.pptx`), and YouTube URLs (timestamped transcripts).\n4. **Site Discovery & Multi-Page Crawl (`web_map` & `web_crawl`)**:\n - Always run `web_map` first on unfamiliar domains to inspect available URLs before running `web_crawl` (capped at 25 pages).\n\n## 2. Verify Evidence & Provenance\n\n- Check the returned `evidence` block (`targetId`, `tier`, `contentHash`, `matchedSignals`, `limitations`) before stating claims as fact.\n- If `limitations` notes that a page was a client-rendered shell or blocked by a challenge in static mode, escalate to stealth browser or `webcrawl.app` cloud (`webcrawl login`).\n";
|
|
10
|
+
export interface SetupOptions {
|
|
11
|
+
agents?: SupportedAgentId[];
|
|
12
|
+
schemaProfile?: "compact" | "full";
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
homeDir?: string;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SetupResult {
|
|
19
|
+
configuredAgents: Array<{
|
|
20
|
+
id: SupportedAgentId;
|
|
21
|
+
name: string;
|
|
22
|
+
mcpConfigPath: string;
|
|
23
|
+
backupPath?: string;
|
|
24
|
+
skillPath: string;
|
|
25
|
+
}>;
|
|
26
|
+
schemaProfile: "compact" | "full";
|
|
27
|
+
}
|
|
28
|
+
export declare function runSetup(opts?: SetupOptions): SetupResult;
|
|
29
|
+
export interface DoctorCheck {
|
|
30
|
+
name: string;
|
|
31
|
+
status: "ok" | "optional" | "warn";
|
|
32
|
+
detail: string;
|
|
33
|
+
}
|
|
34
|
+
export interface DoctorReport {
|
|
35
|
+
mode: "local" | "cloud";
|
|
36
|
+
checks: DoctorCheck[];
|
|
37
|
+
}
|
|
38
|
+
export declare function runDoctor(env?: NodeJS.ProcessEnv, effectiveMode?: "local" | "cloud", apiKeySource?: string): DoctorReport;
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `webcrawl setup` and `webcrawl doctor` — REA-style one-command agent onboarding.
|
|
3
|
+
*
|
|
4
|
+
* Registers the `@webcrawl/mcp` server into Claude Code, Cursor, Codex, Windsurf,
|
|
5
|
+
* and Gemini CLI config files (with timestamped backup) and installs the matching
|
|
6
|
+
* `webcrawl-anything` Agent Skill (`SKILL.md`).
|
|
7
|
+
*/
|
|
8
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
export const SUPPORTED_AGENTS = [
|
|
12
|
+
{
|
|
13
|
+
id: "claude",
|
|
14
|
+
name: "Claude Code",
|
|
15
|
+
mcpConfigPath: (home) => join(home, ".claude.json"),
|
|
16
|
+
skillPath: (home) => join(home, ".claude", "skills", "webcrawl-anything", "SKILL.md"),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "cursor",
|
|
20
|
+
name: "Cursor",
|
|
21
|
+
mcpConfigPath: (home) => join(home, ".cursor", "mcp.json"),
|
|
22
|
+
skillPath: (home) => join(home, ".cursor", "skills", "webcrawl-anything", "SKILL.md"),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "codex",
|
|
26
|
+
name: "Codex / OpenAI Agents",
|
|
27
|
+
mcpConfigPath: (home) => join(home, ".codex", "mcp.json"),
|
|
28
|
+
skillPath: (_home, cwd) => join(cwd, ".agents", "skills", "webcrawl-anything", "SKILL.md"),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "gemini",
|
|
32
|
+
name: "Gemini CLI",
|
|
33
|
+
mcpConfigPath: (home) => join(home, ".gemini", "settings.json"),
|
|
34
|
+
skillPath: (home) => join(home, ".gemini", "skills", "webcrawl-anything", "SKILL.md"),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "windsurf",
|
|
38
|
+
name: "Windsurf",
|
|
39
|
+
mcpConfigPath: (home) => join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
40
|
+
skillPath: (home) => join(home, ".codeium", "windsurf", "skills", "webcrawl-anything", "SKILL.md"),
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
export const WEBCRAWL_SKILL_MD = `---
|
|
44
|
+
name: webcrawl-anything
|
|
45
|
+
description: Crawl, search, and extract structured evidence from any website, SPA, PDF, Office file, YouTube video, GitHub repo, ArXiv paper, shopping offer, travel fare, or CVE advisory.
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
# Webcrawl Anything — Agent Investigation Workflow
|
|
49
|
+
|
|
50
|
+
Use **Webcrawl Anything** (\`webcrawl\` CLI or \`webcrawl\` MCP tools) whenever you need live web search, anti-bot page scraping, document/video parsing, or structured vertical extraction with provenance.
|
|
51
|
+
|
|
52
|
+
## 1. Choose the Right Tool or Target
|
|
53
|
+
|
|
54
|
+
1. **Structured Search (\`web_search\` / \`webcrawl search\`)**:
|
|
55
|
+
- Pass \`category\` when the query belongs to a vertical:
|
|
56
|
+
- \`shopping\`: Multi-retailer product prices, stock availability, ratings, shipping.
|
|
57
|
+
- \`flights\`: IATA routes, fares, airlines, stops, cabin class.
|
|
58
|
+
- \`hotels\`: Nightly rates, star ratings, guest reviews, amenities.
|
|
59
|
+
- \`real-estate\`: Property listings (\`subcategory: "buy" | "rent" | "sold"\`).
|
|
60
|
+
- \`business\`: Local businesses, clinics, phone numbers, hours, ratings.
|
|
61
|
+
- \`security\`: CVE / GHSA records with CVSS scores, CWEs, affected/fixed versions, and CISA KEV status.
|
|
62
|
+
- \`academic\`: ArXiv and research papers.
|
|
63
|
+
- \`tech\` / \`news\` / \`general\`: General and developer search.
|
|
64
|
+
2. **Page & Developer Target Inspection (\`web_scrape\` / \`webcrawl scrape <url>\`)**:
|
|
65
|
+
- Works on any website, client-rendered SPA, GitHub repository/issue/release, ArXiv abstract page, or \`llms.txt\` manifest.
|
|
66
|
+
3. **Binary Documents & Media (\`web_parse\` / \`webcrawl scrape <pdf-or-youtube-url>\`)**:
|
|
67
|
+
- Reads PDFs (native text + automatic OCR on scanned pages), Word (\`.docx\`), Excel (\`.xlsx\`), PowerPoint (\`.pptx\`), and YouTube URLs (timestamped transcripts).
|
|
68
|
+
4. **Site Discovery & Multi-Page Crawl (\`web_map\` & \`web_crawl\`)**:
|
|
69
|
+
- Always run \`web_map\` first on unfamiliar domains to inspect available URLs before running \`web_crawl\` (capped at 25 pages).
|
|
70
|
+
|
|
71
|
+
## 2. Verify Evidence & Provenance
|
|
72
|
+
|
|
73
|
+
- Check the returned \`evidence\` block (\`targetId\`, \`tier\`, \`contentHash\`, \`matchedSignals\`, \`limitations\`) before stating claims as fact.
|
|
74
|
+
- If \`limitations\` notes that a page was a client-rendered shell or blocked by a challenge in static mode, escalate to stealth browser or \`webcrawl.app\` cloud (\`webcrawl login\`).
|
|
75
|
+
`;
|
|
76
|
+
export function runSetup(opts = {}) {
|
|
77
|
+
const home = opts.homeDir ?? homedir();
|
|
78
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
79
|
+
const schemaProfile = opts.schemaProfile ?? "compact";
|
|
80
|
+
const selectedIds = opts.agents?.length ? new Set(opts.agents) : new Set(["claude", "cursor", "codex"]);
|
|
81
|
+
const targets = SUPPORTED_AGENTS.filter((a) => selectedIds.has(a.id));
|
|
82
|
+
const configuredAgents = [];
|
|
83
|
+
for (const agent of targets) {
|
|
84
|
+
const mcpPath = agent.mcpConfigPath(home, cwd);
|
|
85
|
+
let existing = {};
|
|
86
|
+
let backupPath;
|
|
87
|
+
if (existsSync(mcpPath)) {
|
|
88
|
+
try {
|
|
89
|
+
existing = JSON.parse(readFileSync(mcpPath, "utf8"));
|
|
90
|
+
backupPath = `${mcpPath}.bak`;
|
|
91
|
+
copyFileSync(mcpPath, backupPath);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
existing = {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const mcpServers = existing.mcpServers && typeof existing.mcpServers === "object" && !Array.isArray(existing.mcpServers)
|
|
98
|
+
? { ...existing.mcpServers }
|
|
99
|
+
: {};
|
|
100
|
+
const envVars = {
|
|
101
|
+
WEBCRAWL_MCP_SCHEMA_PROFILE: schemaProfile,
|
|
102
|
+
};
|
|
103
|
+
if (opts.apiKey)
|
|
104
|
+
envVars.WEBCRAWL_API_KEY = opts.apiKey;
|
|
105
|
+
if (opts.baseUrl)
|
|
106
|
+
envVars.WEBCRAWL_URL = opts.baseUrl;
|
|
107
|
+
mcpServers.webcrawl = {
|
|
108
|
+
command: "npx",
|
|
109
|
+
args: ["-y", "@webcrawl/mcp@latest"],
|
|
110
|
+
env: envVars,
|
|
111
|
+
};
|
|
112
|
+
const updated = { ...existing, mcpServers };
|
|
113
|
+
mkdirSync(dirname(mcpPath), { recursive: true });
|
|
114
|
+
writeFileSync(mcpPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
|
|
115
|
+
const skillPath = agent.skillPath(home, cwd);
|
|
116
|
+
mkdirSync(dirname(skillPath), { recursive: true });
|
|
117
|
+
writeFileSync(skillPath, WEBCRAWL_SKILL_MD, "utf8");
|
|
118
|
+
configuredAgents.push({
|
|
119
|
+
id: agent.id,
|
|
120
|
+
name: agent.name,
|
|
121
|
+
mcpConfigPath: mcpPath,
|
|
122
|
+
backupPath,
|
|
123
|
+
skillPath,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return { configuredAgents, schemaProfile };
|
|
127
|
+
}
|
|
128
|
+
export function runDoctor(env = process.env, effectiveMode = "local", apiKeySource = "none") {
|
|
129
|
+
const checks = [];
|
|
130
|
+
// 1. Node runtime
|
|
131
|
+
const nodeVer = process.versions.node;
|
|
132
|
+
const major = Number(nodeVer.split(".")[0]);
|
|
133
|
+
checks.push({
|
|
134
|
+
name: "Node.js runtime",
|
|
135
|
+
status: major >= 20 ? "ok" : "warn",
|
|
136
|
+
detail: `v${nodeVer} (requires >=20)`,
|
|
137
|
+
});
|
|
138
|
+
// 2. Execution mode & Cloud API key
|
|
139
|
+
checks.push({
|
|
140
|
+
name: "Execution mode",
|
|
141
|
+
status: "ok",
|
|
142
|
+
detail: effectiveMode === "cloud"
|
|
143
|
+
? `cloud (https://webcrawl.app via ${apiKeySource} key)`
|
|
144
|
+
: "local zero-key engine (impit TLS + built-in parsers & vertical extractors)",
|
|
145
|
+
});
|
|
146
|
+
// 3. Local Chrome / Stealth Browser availability
|
|
147
|
+
const chromePaths = [
|
|
148
|
+
env.CHROME_PATH,
|
|
149
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
150
|
+
"/usr/bin/google-chrome",
|
|
151
|
+
"/usr/bin/chromium",
|
|
152
|
+
"/usr/bin/chromium-browser",
|
|
153
|
+
].filter(Boolean);
|
|
154
|
+
const foundChrome = chromePaths.find((p) => existsSync(p));
|
|
155
|
+
checks.push({
|
|
156
|
+
name: "Local stealth browser",
|
|
157
|
+
status: foundChrome ? "ok" : "optional",
|
|
158
|
+
detail: foundChrome
|
|
159
|
+
? `Found Chrome at ${foundChrome}`
|
|
160
|
+
: "No local Chrome binary detected; static/TLS fetch works out of the box, or run `webcrawl login` for cloud stealth browsers",
|
|
161
|
+
});
|
|
162
|
+
// 4. Search providers
|
|
163
|
+
const serper = Boolean(env.SERPER_API_KEY);
|
|
164
|
+
const brave = Boolean(env.BRAVE_SEARCH_API_KEY);
|
|
165
|
+
const jina = Boolean(env.JINA_API_KEY);
|
|
166
|
+
const configuredProviders = [
|
|
167
|
+
"DuckDuckGo/Bing (built-in zero-key)",
|
|
168
|
+
...(serper ? ["Serper"] : []),
|
|
169
|
+
...(brave ? ["Brave"] : []),
|
|
170
|
+
...(jina ? ["Jina"] : []),
|
|
171
|
+
];
|
|
172
|
+
checks.push({
|
|
173
|
+
name: "Search providers",
|
|
174
|
+
status: "ok",
|
|
175
|
+
detail: configuredProviders.join(", "),
|
|
176
|
+
});
|
|
177
|
+
// 5. Document & Media parsers
|
|
178
|
+
checks.push({
|
|
179
|
+
name: "Document & Media parsers",
|
|
180
|
+
status: "ok",
|
|
181
|
+
detail: "PDF (@firecrawl/pdf-inspector), Office (mammoth/officeparser), YouTube captions, NVD/OSV CVEs ready",
|
|
182
|
+
});
|
|
183
|
+
return { mode: effectiveMode, checks };
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcrawl",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Webcrawl Anything CLI — crawl, search, and extract structured evidence from any website, SPA, PDF, Office file, YouTube video, GitHub repo, ArXiv paper, or CVE",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Brainfish AI <support@webcrawl.app> (https://webcrawl.app)",
|
|
8
|
+
"homepage": "https://webcrawl.app",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/brainfish-ai/webcrawl-anything.git",
|
|
12
|
+
"directory": "packages/cli"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/brainfish-ai/webcrawl-anything/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"webcrawl",
|
|
19
|
+
"web-crawler",
|
|
20
|
+
"web-scraper",
|
|
21
|
+
"search-engine",
|
|
22
|
+
"mcp",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"cursor",
|
|
25
|
+
"codex",
|
|
26
|
+
"pdf-parser",
|
|
27
|
+
"youtube-transcript",
|
|
28
|
+
"cve",
|
|
29
|
+
"ai-agents"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"bin": {
|
|
35
|
+
"webcrawl": "./dist/bin.js"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"main": "./dist/index.js",
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"development": "./src/index.ts",
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"default": "./dist/index.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -p tsconfig.build.json",
|
|
52
|
+
"prepack": "npm run build",
|
|
53
|
+
"test": "node --conditions=development --import tsx --test test/*.test.ts"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@webcrawl/client": "^1.2.0"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20"
|
|
60
|
+
}
|
|
61
|
+
}
|