
Felo Web Fetch
- 86 installs
- 241 repo stars
- Updated July 22, 2026
- felo-inc/felo-skills
Extract webpage content from a URL as html, markdown, or text via the Felo Web Extract API, with CSS-selector targeting and crawl options.
About
Fetches and converts a webpage URL into html, markdown, or text using the Felo Web Extract API, with options for CSS-selector targeting, readability mode, cookies, and user-agent. A developer uses it to scrape or convert page content into structured text for downstream processing.
- Target specific page elements with a CSS selector and choose fast or fine crawl mode
- Bundled Node script or packaged CLI; requires a FELO_API_KEY
Felo Web Fetch by the numbers
- 86 all-time installs (skills.sh)
- Ranked #864 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/felo-inc/felo-skills --skill felo-web-fetchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 86 |
|---|---|
| repo stars | ★ 241 |
| Last updated | July 22, 2026 |
| Repository | felo-inc/felo-skills ↗ |
What it does
Extract webpage content from a URL as html, markdown, or text via the Felo Web Extract API, with CSS-selector targeting and crawl options.
Files
Felo Web Fetch Skill
When to Use
Trigger this skill when users want to extract or convert webpage content from a URL:
- Fetch or scrape content from a webpage URL
- Convert webpage content to
html,markdown, ortext - Extract specific blocks using CSS selector
- Get article/main text from a link with readability mode
- Tune extraction behavior with crawl mode (
fast/fine) - Pass request details such as cookies, user-agent, timeout
Trigger keywords (examples):
- fetch webpage, scrape URL, fetch page content, web fetch, url to markdown
- Explicit:
/felo-web-fetch, "use felo web fetch", "extract this URL with felo" - Same intent in other languages (e.g. 网页抓取, 提取网页内容) also triggers this skill
Do NOT use this skill for:
- Real-time Q&A search summaries (use
felo-search) - Slide generation tasks (use
felo-slides) - Local file parsing in current workspace
Setup
1. Get API key
1. Visit felo.ai 2. Open Settings -> API Keys 3. Create and copy your API key
2. Configure environment variable
Linux/macOS:
export FELO_API_KEY="your-api-key-here"Windows PowerShell:
$env:FELO_API_KEY="your-api-key-here"How to Execute
Option A: Use the bundled script or packaged CLI
Script (from repo):
node felo-web-fetch/scripts/run_web_fetch.mjs --url "https://example.com/article" [options]Packaged CLI (after npm install -g felo-ai): same options, with short forms allowed:
felo web-fetch -u "https://example.com" [options]
# Short forms: -u (url), -f (format), -t (timeout, seconds), -j (json)Required parameter:
--url
Core optional parameters:
--output-format html|markdown|text--crawl-mode fast|fine--target-selector "article.main-content"--wait-for-selector ".content-ready"
Other key optional parameters:
--cookie "session_id=xxx"(repeatable)--set-cookies-json '[{"name":"sid","value":"xxx","domain":"example.com"}]'--user-agent "Mozilla/5.0 ..."--timeout 60(HTTP request timeout in seconds)--request-timeout-ms 15000(API payloadtimeoutin ms)--with-readability true--with-links-summary true--with-images-summary true--with-images-readability true--with-images true--with-links true--ignore-empty-text-image true--with-cache false--with-stypes true--json(print full JSON response)
How to write instructions (target_selector + output_format)
When the user wants a specific part of the page or a specific output format, phrase the command like this:
- Output format: "Fetch as text" / "Get markdown" / "Return html" → use
--output-format text,--output-format markdown, or--output-format html. - Target one element: "Only the main article" / "Just the content inside
#main" / "Fetch only article.main-content" → use--target-selector "article.main"or the selector they give.
Examples:
# Basic: fetch as Markdown
node felo-web-fetch/scripts/run_web_fetch.mjs --url "https://example.com" --output-format markdown
# Article-style with readability
node felo-web-fetch/scripts/run_web_fetch.mjs --url "https://example.com/article" --with-readability true --output-format markdown
# Only the element matching a CSS selector
node felo-web-fetch/scripts/run_web_fetch.mjs --url "https://example.com" --target-selector "article.main" --output-format markdown
# With cookies and custom user-agent
node felo-web-fetch/scripts/run_web_fetch.mjs --url "https://example.com/private" --cookie "session_id=abc123" --with-readability true --json
# Full JSON response
node felo-web-fetch/scripts/run_web_fetch.mjs --url "https://example.com" --output-format text --jsonOption B: Call API with curl
curl -X POST "https://openapi.felo.ai/v2/web/extract" \
-H "Authorization: Bearer $FELO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "output_format": "markdown", "with_readability": true}'API Reference (summary)
- Endpoint:
POST /v2/web/extract - Base URL:
https://openapi.felo.ai. Override withFELO_API_BASEenv if needed. - Auth:
Authorization: Bearer YOUR_API_KEY
Request body (JSON)
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| url | string | Yes | - | Webpage URL to fetch |
| crawl_mode | string | No | fast | fast or fine |
| output_format | string | No | html | html, text, markdown |
| with_readability | boolean | No | - | Use readability (main content) |
| with_links_summary | boolean | No | - | Include links summary |
| with_images_summary | boolean | No | - | Include images summary |
| target_selector | string | No | - | CSS selector for target element |
| wait_for_selector | string | No | - | Wait for selector before fetch |
| timeout | integer | No | - | Timeout in milliseconds |
| with_cache | boolean | No | true | Use cache |
| set_cookies | array | No | - | Cookie entries |
| user_agent | string | No | - | Custom user-agent |
Response
Success (200):
{
"code": 0,
"message": "success",
"data": {
"content": { ... }
}
}Fetched content is in data.content; structure depends on output_format.
Error codes
| HTTP | Code | Description |
|---|---|---|
| 400 | - | Parameter validation failed |
| 401 | INVALID_API_KEY | API key invalid or revoked |
| 500/502 | WEB_EXTRACT_FAILED | Fetch failed (server or page error) |
Output Format
- Default output is extracted content only (for direct use or piping).
- If response content is not a string, script prints JSON.
- Use
--jsonwhen user needs metadata and full response object.
Error response format:
## Web Fetch Failed
- Message: <error message>
- Suggested Action: verify URL/parameters and retryImportant Notes
- Always require URL before running.
- Validate enum values:
output_format:html,markdown,textcrawl_mode:fast,fine- Use
--target-selectorwhen users only want a specific part of the page. - Use
--request-timeout-msfor page rendering/extraction wait, and--timeoutfor local HTTP timeout. - For long articles or slow sites, consider increasing
--timeout. - API may cache results; use
--with-cache falseonly when fresh content is required.
References
Felo Web Fetch Skill for Claude Code
Extract structured webpage content from URLs with the Felo Web Extract API.
Features
- URL content extraction (required)
- Output format:
html,markdown,text - Crawl mode:
fast,fine - CSS selector extraction (
target_selector) - Advanced options: cookies, user-agent, timeout, readability and link/image summary flags
- Spinner progress indicator during fetch
Quick Start
1) Install the skill
一键安装(推荐):
npx skills add Felo-Inc/felo-skills --skill felo-web-fetch手动安装: 若上述命令不可用,从本仓库复制到 Claude Code 的 skills 目录:
# Linux/macOS
cp -r felo-web-fetch ~/.claude/skills/
# Windows (PowerShell)
Copy-Item -Recurse felo-web-fetch "$env:USERPROFILE\.claude\skills\"(Clone the repo first if needed: git clone https://github.com/Felo-Inc/felo-skills.git.)
2) Configure API key
Create API key at felo.ai -> Settings -> API Keys, then set:
# Linux/macOS
export FELO_API_KEY="your-api-key-here"# Windows PowerShell
$env:FELO_API_KEY="your-api-key-here"3) Trigger the skill
- Intent trigger: "Extract this article as markdown"
- Explicit trigger:
/felo-web-fetch https://example.com/article
Using the packaged CLI (felo web-fetch)
After npm install -g felo-ai, you can run:
felo web-fetch --url "https://example.com"All parameters (how to pass)
| Parameter | Option | Example |
|---|---|---|
| URL (required) | -u, --url | --url "https://example.com" |
| Output format | -f, --format | --format text, -f markdown, -f html |
| Target element (CSS selector) | --target-selector | --target-selector "article.main" |
| Wait for selector | --wait-for-selector | --wait-for-selector ".content" |
| Readability (main content only) | --readability | --readability |
| Crawl mode | --crawl-mode | --crawl-mode fine (default: fast) |
| Timeout (seconds) | -t, --timeout | --timeout 120, -t 90 |
| Full JSON response | -j, --json | -j or --json |
Examples with multiple options
felo web-fetch -u "https://example.com" -f text --readability
felo web-fetch --url "https://example.com" --target-selector "#content" --format markdown --timeout 90
felo web-fetch --url "https://example.com" --wait-for-selector "main" --readability -jScript Usage
The skill uses:
node felo-web-fetch/scripts/run_web_fetch.mjs --url "https://example.com"Common examples:
node felo-web-fetch/scripts/run_web_fetch.mjs \
--url "https://example.com/post" \
--output-format markdown \
--crawl-mode finenode felo-web-fetch/scripts/run_web_fetch.mjs \
--url "https://example.com" \
--target-selector "article.main" \
--output-format text \
--user-agent "Mozilla/5.0" \
--request-timeout-ms 20000node felo-web-fetch/scripts/run_web_fetch.mjs \
--url "https://example.com/private" \
--cookie "session_id=abc123" \
--with-readability true \
--jsonError Handling
- Missing key:
FELO_API_KEY not set - Invalid key:
INVALID_API_KEY - Invalid params / URL:
HTTP 400 - Upstream extraction failure:
WEB_EXTRACT_FAILED(HTTP 500/502)
Links
See SKILL.md for full agent instructions and API parameters.
#!/usr/bin/env node
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
const DEFAULT_TIMEOUT_SEC = 60;
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const SPINNER_INTERVAL_MS = 80;
const STATUS_PAD = 56;
function startSpinner(message) {
if (!process.stderr.isTTY) return null;
const start = Date.now();
let i = 0;
const id = setInterval(() => {
const elapsed = Math.floor((Date.now() - start) / 1000);
const line = `${message} ${SPINNER_FRAMES[i % SPINNER_FRAMES.length]} ${elapsed}s`;
process.stderr.write(`\r${line.padEnd(STATUS_PAD, ' ')}`);
i += 1;
}, SPINNER_INTERVAL_MS);
return id;
}
function stopSpinner(id) {
if (id != null) clearInterval(id);
if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
}
function usage() {
console.error(
[
'Usage:',
' node felo-web-fetch/scripts/run_web_fetch.mjs --url <url> [options]',
'',
'Required:',
' --url <url> Target page URL',
'',
'Options:',
' --output-format <format> html | markdown | text',
' --crawl-mode <mode> fast | fine',
' --target-selector <selector> CSS selector for target extraction',
' --wait-for-selector <selector> Wait until selector appears',
' --cookie <cookie> Add cookie entry (repeatable)',
' --set-cookies-json <json> JSON array for set_cookies',
' --user-agent <ua> Custom user-agent',
' --timeout <seconds> Request timeout in seconds (default 60)',
' --request-timeout-ms <ms> API timeout parameter in milliseconds',
' --with-readability <bool> true | false',
' --with-links-summary <bool> true | false',
' --with-images-summary <bool> true | false',
' --with-images-readability <bool> true | false',
' --with-images <bool> true | false',
' --with-links <bool> true | false',
' --ignore-empty-text-image <bool> true | false',
' --with-cache <bool> true | false',
' --with-stypes <bool> true | false',
' --json Print full JSON response',
' --help Show this help',
].join('\n')
);
}
function parseBool(v, name) {
if (typeof v !== 'string') {
throw new Error(`Missing value for ${name}`);
}
const normalized = v.trim().toLowerCase();
if (normalized === 'true') return true;
if (normalized === 'false') return false;
throw new Error(`Invalid boolean for ${name}: ${v}. Use true or false.`);
}
function parseArgs(argv) {
const out = {
url: '',
outputFormat: '',
crawlMode: '',
targetSelector: '',
waitForSelector: '',
cookies: [],
cookiesJson: '',
userAgent: '',
timeoutSec: DEFAULT_TIMEOUT_SEC,
requestTimeoutMs: null,
withReadability: null,
withLinksSummary: null,
withImagesSummary: null,
withImagesReadability: null,
withImages: null,
withLinks: null,
ignoreEmptyTextImage: null,
withCache: null,
withStypes: null,
json: false,
help: false,
};
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i];
if (a === '--help' || a === '-h') {
out.help = true;
} else if (a === '--json') {
out.json = true;
} else if (a === '--url') {
out.url = argv[i + 1] ?? '';
i += 1;
} else if (a === '--output-format') {
out.outputFormat = (argv[i + 1] ?? '').trim().toLowerCase();
i += 1;
} else if (a === '--crawl-mode') {
out.crawlMode = (argv[i + 1] ?? '').trim().toLowerCase();
i += 1;
} else if (a === '--target-selector') {
out.targetSelector = argv[i + 1] ?? '';
i += 1;
} else if (a === '--wait-for-selector') {
out.waitForSelector = argv[i + 1] ?? '';
i += 1;
} else if (a === '--cookie') {
const value = argv[i + 1] ?? '';
if (value) out.cookies.push(value);
i += 1;
} else if (a === '--set-cookies-json') {
out.cookiesJson = argv[i + 1] ?? '';
i += 1;
} else if (a === '--user-agent') {
out.userAgent = argv[i + 1] ?? '';
i += 1;
} else if (a === '--timeout') {
out.timeoutSec = Number.parseInt(argv[i + 1] ?? '', 10);
i += 1;
} else if (a === '--request-timeout-ms') {
out.requestTimeoutMs = Number.parseInt(argv[i + 1] ?? '', 10);
i += 1;
} else if (a === '--with-readability') {
out.withReadability = parseBool(argv[i + 1], '--with-readability');
i += 1;
} else if (a === '--with-links-summary') {
out.withLinksSummary = parseBool(argv[i + 1], '--with-links-summary');
i += 1;
} else if (a === '--with-images-summary') {
out.withImagesSummary = parseBool(argv[i + 1], '--with-images-summary');
i += 1;
} else if (a === '--with-images-readability') {
out.withImagesReadability = parseBool(argv[i + 1], '--with-images-readability');
i += 1;
} else if (a === '--with-images') {
out.withImages = parseBool(argv[i + 1], '--with-images');
i += 1;
} else if (a === '--with-links') {
out.withLinks = parseBool(argv[i + 1], '--with-links');
i += 1;
} else if (a === '--ignore-empty-text-image') {
out.ignoreEmptyTextImage = parseBool(argv[i + 1], '--ignore-empty-text-image');
i += 1;
} else if (a === '--with-cache') {
out.withCache = parseBool(argv[i + 1], '--with-cache');
i += 1;
} else if (a === '--with-stypes') {
out.withStypes = parseBool(argv[i + 1], '--with-stypes');
i += 1;
}
}
if (!Number.isFinite(out.timeoutSec) || out.timeoutSec <= 0) {
out.timeoutSec = DEFAULT_TIMEOUT_SEC;
}
if (out.requestTimeoutMs !== null && (!Number.isFinite(out.requestTimeoutMs) || out.requestTimeoutMs <= 0)) {
out.requestTimeoutMs = null;
}
return out;
}
function ensureInSet(value, allowed, fieldName) {
if (!value) return;
if (!allowed.includes(value)) {
throw new Error(`Invalid ${fieldName}: ${value}. Allowed values: ${allowed.join(', ')}`);
}
}
function isApiError(payload) {
if (typeof payload?.code === 'number') {
return payload.code !== 0;
}
if (typeof payload?.status === 'string') {
return payload.status.toLowerCase() === 'error';
}
return false;
}
function getMessage(payload) {
return String(payload?.message || payload?.error || payload?.msg || 'Unknown error');
}
async function fetchJson(url, init, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { ...init, signal: controller.signal });
let body = {};
try {
body = await res.json();
} catch {
body = {};
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
}
if (isApiError(body)) {
throw new Error(getMessage(body));
}
return body;
} finally {
clearTimeout(timer);
}
}
function buildPayload(args) {
const payload = {
url: args.url,
};
if (args.outputFormat) payload.output_format = args.outputFormat;
if (args.crawlMode) payload.crawl_mode = args.crawlMode;
if (args.targetSelector) payload.target_selector = args.targetSelector;
if (args.waitForSelector) payload.wait_for_selector = args.waitForSelector;
if (args.userAgent) payload.user_agent = args.userAgent;
if (args.requestTimeoutMs !== null) payload.timeout = args.requestTimeoutMs;
if (args.cookies.length) payload.set_cookies = args.cookies;
if (args.cookiesJson) {
try {
const parsed = JSON.parse(args.cookiesJson);
if (!Array.isArray(parsed)) {
throw new Error('set_cookies JSON must be an array');
}
payload.set_cookies = parsed;
} catch (err) {
throw new Error(`Invalid --set-cookies-json: ${String(err.message || err)}`);
}
}
if (args.withReadability !== null) payload.with_readability = args.withReadability;
if (args.withLinksSummary !== null) payload.with_links_summary = args.withLinksSummary;
if (args.withImagesSummary !== null) payload.with_images_summary = args.withImagesSummary;
if (args.withImagesReadability !== null) payload.with_images_readability = args.withImagesReadability;
if (args.withImages !== null) payload.with_images = args.withImages;
if (args.withLinks !== null) payload.with_links = args.withLinks;
if (args.ignoreEmptyTextImage !== null) payload.ignore_empty_text_image = args.ignoreEmptyTextImage;
if (args.withCache !== null) payload.with_cache = args.withCache;
if (args.withStypes !== null) payload.with_stypes = args.withStypes;
return payload;
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
usage();
process.exit(0);
}
if (!args.url) {
usage();
process.exit(1);
}
ensureInSet(args.outputFormat, ['html', 'markdown', 'text'], 'output-format');
ensureInSet(args.crawlMode, ['fast', 'fine'], 'crawl-mode');
const apiKey = process.env.FELO_API_KEY?.trim();
if (!apiKey) {
console.error('ERROR: FELO_API_KEY not set');
process.exit(1);
}
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
const payload = buildPayload(args);
const shortUrl = args.url.length > 45 ? args.url.slice(0, 42) + '...' : args.url;
const spinnerId = startSpinner(`Fetching ${shortUrl}`);
try {
const response = await fetchJson(
`${apiBase}/v2/web/extract`,
{
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
},
args.timeoutSec * 1000
);
if (args.json) {
console.log(JSON.stringify(response, null, 2));
return;
}
const content = response?.data?.content;
if (typeof content === 'string') {
console.log(content);
return;
}
console.log(JSON.stringify(content ?? response?.data ?? response, null, 2));
} catch (err) {
console.error(`ERROR: ${String(err?.message || err || 'Unknown error')}`);
process.exit(1);
} finally {
stopSpinner(spinnerId);
}
}
main();