webcli
by ashark-ai-05
README.md
<p align="center">
<h1 align="center">webcli</h1>
<p align="center">
<strong>Secure, agent-driven web data extraction.</strong><br>
Turn any website into structured data. No browser required.
</p>
<p align="center">
<a href="#quick-start">Quick Start</a> •
<a href="#built-in-adapters">Adapters</a> •
<a href="#mcp-server">MCP Server</a> •
<a href="#architecture">Architecture</a> •
<a href="#security">Security</a>
</p>
</p>
<p align="center">
<img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen?style=flat-square" alt="Node.js 20+">
<img src="https://img.shields.io/badge/tests-87%20passing-brightgreen?style=flat-square" alt="87 tests passing">
<img src="https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square" alt="Apache 2.0">
<img src="https://img.shields.io/badge/MCP-compatible-purple?style=flat-square" alt="MCP compatible">
</p>
---
<p align="center">
<img src="assets/demo-overview.gif" alt="webcli demo - searching HackerNews, fetching crypto prices, and querying Bluesky" width="700">
</p>
## What is webcli?
webcli extracts structured data from websites using an **API-first approach** -- it tries public APIs, RSS feeds, and structured HTML before ever touching a browser. Built for AI agents that need to read the web.
**How it compares to opencli:**
| | opencli | webcli |
|---|---|---|
| **Browser** | Reuses your Chrome session | No browser needed for most sites |
| **Anti-detection** | Patches navigator.webdriver, fakes fingerprints | Honest automation, no evasion |
| **Extension** | God-mode Chrome extension (debugger + cookies + all URLs) | No extension at all |
| **Security** | Arbitrary JS in your browser context | Sandboxed V8 isolates, domain-locked fetch |
| **Agent interface** | CLI only | CLI + MCP server |
| **Auth** | Steals your cookies | You provide API keys explicitly |
## Quick Start
```bash
# Clone and install
git clone https://github.com/ashark-ai-05/webcli.git
cd webcli
npm install
# List available adapters
npx tsx src/main.ts list
# Search Hacker News
npx tsx src/main.ts run hackernews search -a query=AI -a limit=5
# Get live crypto prices
npx tsx src/main.ts run coingecko prices -a coins=bitcoin,ethereum,solana
# Search Bluesky users
npx tsx src/main.ts run bluesky search -a query=typescript -a limit=5
```
## Built-in Adapters
### Hacker News
<img src="assets/demo-search.gif" alt="Searching Hacker News" width="700">
```bash
# Search stories
webcli run hackernews search -a query=rust -a limit=10
# Output as JSON (for piping to other tools)
webcli run hackernews search -a query=rust -f json
# Output as CSV (for spreadsheets)
webcli run hackernews search -a query=rust -f csv
```
### CoinGecko
<img src="assets/demo-crypto.gif" alt="Live cryptocurrency prices" width="700">
```bash
# Live prices
webcli run coingecko prices -a coins=bitcoin,ethereum,solana
# Trending coins
webcli run coingecko trending -a limit=10
```
### Bluesky
```bash
# Search users
webcli run bluesky search -a query=developer -a limit=5
# Get a profile
webcli run bluesky profile -a handle=bsky.app
```
## MCP Server
webcli runs as an [MCP](https://modelcontextprotocol.io) server so AI agents (Claude, Cursor, etc.) can use it as a tool.
```bash
# Start MCP server
npx tsx src/main.ts --mcp
```
Add to your Claude Code MCP config:
```json
{
"mcpServers": {
"webcli": {
"command": "npx",
"args": ["tsx", "/path/to/webcli/src/main.ts", "--mcp"]
}
}
}
```
**Available MCP tools:**
| Tool | Description |
|------|-------------|
| `webcli_run` | Run any adapter -- extract data from a website |
| `webcli_list` | List all available adapters |
| `webcli_schema` | Get the data schema of an adapter (field names, types, roles) |
An agent can discover what data a site provides (`webcli_schema`), then extract it (`webcli_run`) -- all without any hardcoded knowledge of the site.
## YAML Adapter DSL
Every data source is defined as a declarative YAML adapter. Here's the CoinGecko prices adapter:
```yaml
site: coingecko
name: prices
description: Live cryptocurrency prices
version: 1
source:
type: api
base_url: https://api.coingecko.com/api/v3
auth: none
rate_limit: { requests: 30, per: 60 }
args:
coins: { type: string, default: "bitcoin,ethereum" }
currency: { type: string, default: usd }
schema:
type: price_feed
entity: cryptocurrency
fields:
id: { role: id }
price: { role: value, unit: currency }
change_24h: { role: delta }
market_cap: { role: metric }
update_frequency: "~60s"
pipeline:
- fetch:
url: /simple/price
params:
ids: "${{ args.coins }}"
vs_currencies: "${{ args.currency }}"
include_market_cap: true
include_24hr_change: true
- transform: |
Object.entries(data).map(([id, v]) => ({
id, price: v[args.currency],
change_24h: v[args.currency + '_24h_change'].toFixed(2) + '%',
market_cap: v[args.currency + '_market_cap']
}))
columns: [id, price, change_24h, market_cap]
```
### Pipeline Steps
| Step | Purpose | Example |
|------|---------|---------|
| `fetch` | HTTP request | `fetch: { url: /api/data, params: { q: "${{ args.query }}" } }` |
| `select` | Extract nested key | `select: response.data.items` |
| `transform` | JS in sandboxed V8 | `transform: "data.map(x => ({ ...x, rank: x.score * 2 }))"` |
| `map` | Reshape each item | `map: { title: "${{ item.name }}", score: "${{ item.points }}" }` |
| `filter` | Keep matching items | `filter: "item.score > 10"` |
| `sort` | Order results | `sort: { field: score, order: desc }` |
| `limit` | Cap result count | `limit: "${{ args.limit }}"` |
| `dedupe` | Remove duplicates | `dedupe: id` |
| `flatten` | Flatten nested arrays | `flatten: tags` |
## Architecture
```
┌─────────┐ ┌────────────┐
│ CLI │ │ MCP Server │
└────┬────┘ └─────┬──────┘
└───────┬──────┘
v
┌────────────────────────┐
│ Core Engine │
│ Pipeline Executor │
│ Adapter Registry │
│ Rate Limiter │
└───────────┬────────────┘
v
┌─────────────────────────────────┐
│ Extractor Cascade │
│ HTTP/API > RSS > HTML > Browser│
└─────────────────────────────────┘
v
┌─────────────────────────────────┐
│ Discovery Engine │
│ Schema Inference │
│ Known Site Registry (8 sites) │
│ Field Role Detection │
└─────────────────────────────────┘
v
┌─────────────────────────────────┐
│ Data Layer │
│ Snapshot Store + Diff Engine │
│ Subscription Manager │
│ Adaptive Interval Scheduler │
└─────────────────────────────────┘
```
### Extractor Cascade
webcli tries the lightest extraction method first:
1. **HTTP/API** -- Direct JSON API calls. No browser. Fastest.
2. **RSS/Atom** -- Feed parsing. Perfect for news sites.
3. **HTML** -- JSON-LD, structured data, DOM pattern detection.
4. **Browser** -- Playwright (sandboxed). Last resort for SPAs.
90% of useful data is accessible without a browser.
### Discovery Engine
Point webcli at any URL and it figures out the data model:
- **Known Site Registry** -- 8 major sites with pre-mapped APIs (CoinGecko, HN, Bluesky, Reddit, StackOverflow, GitHub, X, Wikipedia)
- **Schema Inference** -- Detects field roles (id, title, url, timestamp, score, price, author) from field names and values
- **Entity Typing** -- Classifies data as articles, posts, prices, users, etc.
## Security
webcli was designed as a secure alternative to tools like opencli. Every layer has explicit security boundaries:
### Sandboxed Expressions
The `${{ }}` template engine and `transform` step run in **isolated V8 contexts** via `vm.createContext`:
```
Available: args, item, index, data, Math, Date, JSON, String, Array, Object
BLOCKED: fetch, require, import, process, globalThis, eval, Function,
setTimeout, Buffer, fs, child_process
```
Memory limit: 64MB. Time limit: 5s for transforms, 1s for expressions.
### Domain-Locked Fetch
The `fetch` pipeline step only makes requests to URLs matching the adapter's declared `source.base_url`. A CoinGecko adapter cannot make requests to Twitter's API.
### Adapter Validation
Every adapter is validated before execution:
- Max 20 pipeline steps
- Only known step types allowed
- Source type and auth method must be from allowed lists
- No shell commands anywhere in the pipeline
### Rate Limiting
Global and per-site token-bucket rate limiting prevents accidental DoS:
```yaml
rate_limits:
global: { max_requests_per_minute: 120 }
per_site:
default: { max_requests_per_minute: 30 }
overrides:
api.coingecko.com: 50
```
### Honest Automation
No anti-detection. No fingerprint faking. No `navigator.webdriver` patching. webcli identifies itself honestly:
```
User-Agent: webcli/0.1.0 (+https://github.com/webcli)
```
If a site blocks us, we escalate gracefully (try browser, then auth, then report blocked) -- we don't start an evasion arms race.
## Output Formats
```bash
# Pretty table (default)
webcli run hackernews search -a query=AI
# JSON (for piping to jq, agents, scripts)
webcli run hackernews search -a query=AI -f json
# CSV (for spreadsheets, pandas)
webcli run hackernews search -a query=AI -f csv
```
## Data Layer
### Snapshots & Diffs
webcli can track changes over time:
```typescript
// Snapshot store saves results to ~/.webcli/data/{site}/{name}/
// Diff engine compares snapshots to detect added/removed/changed items
{
"added": [{ "id": "solana", "price": 142.50 }],
"removed": [],
"changed": [{
"id": "bitcoin",
"fields": { "price": { "old": 71000, "new": 71250 } }
}],
"unchanged_count": 1
}
```
### Adaptive Polling
Subscriptions automatically adjust their polling frequency:
- **High volatility** (>50% data changed) -- halve the interval
- **Moderate activity** (10-50% changed) -- keep current interval
- **No changes** -- increase interval by 50%
Respects `min_interval` and `max_interval` bounds, plus `update_frequency` hints from the adapter schema.
## Development
```bash
# Run tests
npm test
# Watch mode
npm run test:watch
# Type check
npm run lint
# Run the CLI in dev mode
npm run dev -- list
```
### Project Structure
```
webcli/
src/
core/ # Pipeline engine, types, adapter system
extractors/ # HTTP, RSS, HTML, cascade
discovery/ # Schema inference, known sites
data/ # Snapshots, diffs, subscriptions
scheduler/ # Adaptive intervals
interfaces/ # CLI (commander) + MCP server
utils/ # Config, logging
adapters/ # Built-in YAML adapters
tests/e2e/ # End-to-end tests (hit live APIs)
```
## Writing Custom Adapters
Create a YAML file in `~/.webcli/adapters/{site}/{name}.yaml`:
```yaml
site: mysite
name: feed
description: My custom feed
version: 1
created_by: manual
source:
type: api
base_url: https://api.mysite.com
auth: none
args:
limit: { type: number, default: 10 }
schema:
type: feed
entity: article
fields:
title: { role: title }
url: { role: url }
pipeline:
- fetch: { url: /posts, params: { limit: "${{ args.limit }}" } }
- select: data
- map:
title: "${{ item.title }}"
url: "${{ item.url }}"
date: "${{ item.published_at }}"
- limit: "${{ args.limit }}"
columns: [title, url, date]
```
Validate it:
```bash
webcli validate ~/.webcli/adapters/mysite/feed.yaml
```
Then use it:
```bash
webcli run mysite feed -a limit=5
```
## License
Apache-2.0
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues