From c16e61ec4fd4dd35070d144b7a22dba6ffe34d90 Mon Sep 17 00:00:00 2001 From: 0xsline Date: Sun, 22 Mar 2026 01:38:14 +0800 Subject: [PATCH 1/5] feat: add GitHub Trending, Binance, and Weather (Open Meteo) adapters GitHub Trending (2 commands, browser mode): - repos: trending repositories with stars, forks, language filter - developers: trending developers with popular repos Supports --since daily/weekly/monthly and --language filter Binance (11 commands, public API via data-api.binance.vision): - top: top trading pairs by 24h volume - price: single pair 24h price stats - prices: latest prices for all pairs - ticker: 24h ticker statistics - gainers: top gaining pairs by 24h change - losers: top losing pairs by 24h change - trades: recent trades for a pair - depth: order book bid prices - asks: order book ask prices - klines: candlestick/kline data - pairs: list active trading pairs Weather / Open Meteo (11 commands, free public API, no key needed): - current: current weather for a city - forecast: daily forecast up to 16 days - hourly: hourly forecast - search: city geocoding lookup - air: air quality index (simple) - air-quality: detailed air quality (US/EU AQI, PM2.5, PM10, ozone, NO2, SO2) - sunrise: sunrise/sunset times with UV index - wind: detailed wind forecast with gusts and 80m altitude - precipitation: rain/snow forecast with probability - history: historical weather up to 92 past days - compare: side-by-side weather comparison across cities All 24 commands tested with live data. 258 existing tests pass. --- docs/adapters/browser/binance.md | 65 ++++++++++++++++++++ docs/adapters/browser/github-trending.md | 37 +++++++++++ docs/adapters/browser/weather.md | 78 ++++++++++++++++++++++++ src/clis/binance/asks.yaml | 32 ++++++++++ src/clis/binance/depth.yaml | 32 ++++++++++ src/clis/binance/gainers.yaml | 33 ++++++++++ src/clis/binance/klines.yaml | 36 +++++++++++ src/clis/binance/losers.yaml | 32 ++++++++++ src/clis/binance/pairs.yaml | 30 +++++++++ src/clis/binance/price.yaml | 30 +++++++++ src/clis/binance/prices.yaml | 25 ++++++++ src/clis/binance/ticker.yaml | 34 +++++++++++ src/clis/binance/top.yaml | 33 ++++++++++ src/clis/binance/trades.yaml | 32 ++++++++++ src/clis/github-trending/developers.yaml | 50 +++++++++++++++ src/clis/github-trending/repos.yaml | 55 +++++++++++++++++ src/clis/weather/air-quality.yaml | 53 ++++++++++++++++ src/clis/weather/air.yaml | 46 ++++++++++++++ src/clis/weather/compare.yaml | 52 ++++++++++++++++ src/clis/weather/current.yaml | 49 +++++++++++++++ src/clis/weather/forecast.yaml | 52 ++++++++++++++++ src/clis/weather/history.yaml | 51 ++++++++++++++++ src/clis/weather/hourly.yaml | 53 ++++++++++++++++ src/clis/weather/precipitation.yaml | 52 ++++++++++++++++ src/clis/weather/search.yaml | 38 ++++++++++++ src/clis/weather/sunrise.yaml | 50 +++++++++++++++ src/clis/weather/wind.yaml | 53 ++++++++++++++++ 27 files changed, 1183 insertions(+) create mode 100644 docs/adapters/browser/binance.md create mode 100644 docs/adapters/browser/github-trending.md create mode 100644 docs/adapters/browser/weather.md create mode 100644 src/clis/binance/asks.yaml create mode 100644 src/clis/binance/depth.yaml create mode 100644 src/clis/binance/gainers.yaml create mode 100644 src/clis/binance/klines.yaml create mode 100644 src/clis/binance/losers.yaml create mode 100644 src/clis/binance/pairs.yaml create mode 100644 src/clis/binance/price.yaml create mode 100644 src/clis/binance/prices.yaml create mode 100644 src/clis/binance/ticker.yaml create mode 100644 src/clis/binance/top.yaml create mode 100644 src/clis/binance/trades.yaml create mode 100644 src/clis/github-trending/developers.yaml create mode 100644 src/clis/github-trending/repos.yaml create mode 100644 src/clis/weather/air-quality.yaml create mode 100644 src/clis/weather/air.yaml create mode 100644 src/clis/weather/compare.yaml create mode 100644 src/clis/weather/current.yaml create mode 100644 src/clis/weather/forecast.yaml create mode 100644 src/clis/weather/history.yaml create mode 100644 src/clis/weather/hourly.yaml create mode 100644 src/clis/weather/precipitation.yaml create mode 100644 src/clis/weather/search.yaml create mode 100644 src/clis/weather/sunrise.yaml create mode 100644 src/clis/weather/wind.yaml diff --git a/docs/adapters/browser/binance.md b/docs/adapters/browser/binance.md new file mode 100644 index 000000000..6f243f3ea --- /dev/null +++ b/docs/adapters/browser/binance.md @@ -0,0 +1,65 @@ +# Binance + +**Mode**: :globe_with_meridians: Public API · **Domain**: `data-api.binance.vision` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli binance top` | Top trading pairs by 24h volume | +| `opencli binance price` | Quick price check for a trading pair | +| `opencli binance prices` | Latest prices for all trading pairs | +| `opencli binance ticker` | 24h ticker statistics for top pairs by volume | +| `opencli binance gainers` | Top gaining pairs by 24h price change | +| `opencli binance losers` | Top losing pairs by 24h price change | +| `opencli binance trades` | Recent trades for a trading pair | +| `opencli binance depth` | Order book bid prices for a trading pair | +| `opencli binance asks` | Order book ask prices for a trading pair | +| `opencli binance klines` | Candlestick/kline data for a trading pair | +| `opencli binance pairs` | List active trading pairs on Binance | + +## Usage Examples + +```bash +# Top trading pairs by volume +opencli binance top --limit 10 + +# Check price for a specific pair +opencli binance price --symbol BTCUSDT +opencli binance price --symbol ETHUSDT + +# All prices +opencli binance prices --limit 30 + +# 24h ticker stats +opencli binance ticker --limit 10 + +# Top gainers and losers +opencli binance gainers --limit 5 +opencli binance losers --limit 5 + +# Recent trades for BTC +opencli binance trades --symbol BTCUSDT --limit 10 + +# Order book bid and ask prices +opencli binance depth --symbol BTCUSDT --limit 5 +opencli binance asks --symbol BTCUSDT --limit 5 + +# Daily candlestick data +opencli binance klines --symbol BTCUSDT --interval 1d --limit 7 + +# Hourly candles +opencli binance klines --symbol ETHUSDT --interval 1h --limit 24 + +# List trading pairs +opencli binance pairs --limit 10 + +# JSON output +opencli binance top --limit 5 -f json +``` + +## Notes + +- All endpoints use Binance's public data API (no authentication required) +- Symbols are uppercase (e.g., BTCUSDT, ETHUSDT, BNBUSDT) +- Kline intervals: 1m, 5m, 15m, 1h, 4h, 1d, 1w, 1M diff --git a/docs/adapters/browser/github-trending.md b/docs/adapters/browser/github-trending.md new file mode 100644 index 000000000..a33b345f5 --- /dev/null +++ b/docs/adapters/browser/github-trending.md @@ -0,0 +1,37 @@ +# GitHub Trending + +**Mode**: 🔐 Browser · **Domain**: `github.com` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli github-trending repos` | Trending repositories | +| `opencli github-trending developers` | Trending developers | + +## Usage Examples + +```bash +# Daily trending repos +opencli github-trending repos --limit 10 + +# Weekly trending +opencli github-trending repos --since weekly + +# Trending Python repos +opencli github-trending repos --language python + +# Trending Rust repos this month +opencli github-trending repos --language rust --since monthly + +# Trending developers +opencli github-trending developers --limit 10 + +# JSON output +opencli github-trending repos --limit 5 -f json +``` + +## Prerequisites + +- Chrome running +- [Browser Bridge extension](/guide/browser-bridge) installed diff --git a/docs/adapters/browser/weather.md b/docs/adapters/browser/weather.md new file mode 100644 index 000000000..6303fe55c --- /dev/null +++ b/docs/adapters/browser/weather.md @@ -0,0 +1,78 @@ +# Weather (Open Meteo) + +**Mode**: Public API + Browser · **Domain**: `api.open-meteo.com` + +Free worldwide weather data powered by [Open Meteo](https://open-meteo.com). No API key required. + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli weather current` | Current weather for a city | +| `opencli weather forecast` | 7-day daily forecast | +| `opencli weather hourly` | Hourly forecast (next 24h) | +| `opencli weather search` | Search for cities (geocoding) | +| `opencli weather air-quality` | Current air quality index | +| `opencli weather sunrise` | Sunrise/sunset times and UV index | +| `opencli weather wind` | Detailed wind forecast | +| `opencli weather precipitation` | Rain and snow forecast | +| `opencli weather compare` | Compare weather across cities | +| `opencli weather history` | Historical weather data | + +## Usage Examples + +```bash +# Current weather +opencli weather current Tokyo +opencli weather current "New York" +opencli weather current London -f json + +# 7-day forecast +opencli weather forecast Paris +opencli weather forecast Berlin --days 14 + +# Hourly forecast +opencli weather hourly Sydney --hours 12 + +# Search for a city (no browser needed) +opencli weather search "San Francisco" +opencli weather search Mumbai --limit 10 + +# Air quality +opencli weather air-quality Beijing +opencli weather air-quality "Los Angeles" + +# Sunrise and sunset times +opencli weather sunrise Tokyo --days 14 + +# Wind forecast +opencli weather wind Chicago --hours 48 + +# Precipitation forecast +opencli weather precipitation London --days 10 + +# Compare multiple cities +opencli weather compare "Tokyo,London,Paris,New York" + +# Historical weather (past 30 days) +opencli weather history Berlin --days 30 + +# JSON output for scripting +opencli weather current Tokyo -f json +opencli weather forecast London -f json +``` + +## Notes + +- The `search` command uses a simple fetch pipeline (no browser needed) +- All other commands use browser mode for the 2-step geocoding lookup (city name -> coordinates -> weather data) +- City names with spaces must be quoted: `"New York"`, `"San Francisco"` +- Temperature is in Celsius, wind in km/h, precipitation in mm +- Air quality uses US AQI scale (0-500) +- Historical data supports up to 92 past days +- Forecast supports up to 16 days ahead + +## Prerequisites + +- Chrome running with [Browser Bridge extension](/guide/browser-bridge) installed +- No API key or login required - Open Meteo is completely free diff --git a/src/clis/binance/asks.yaml b/src/clis/binance/asks.yaml new file mode 100644 index 000000000..cc876cde9 --- /dev/null +++ b/src/clis/binance/asks.yaml @@ -0,0 +1,32 @@ +site: binance +name: asks +description: Order book ask prices for a trading pair +domain: data-api.binance.vision +strategy: public +browser: false + +args: + symbol: + type: str + required: true + positional: true + description: "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + limit: + type: int + default: 10 + description: Number of price levels (5, 10, 20, 50, 100) + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }} + + - select: asks + + - map: + rank: ${{ index + 1 }} + ask_price: ${{ item.0 }} + ask_qty: ${{ item.1 }} + + - limit: ${{ args.limit }} + +columns: [rank, ask_price, ask_qty] diff --git a/src/clis/binance/depth.yaml b/src/clis/binance/depth.yaml new file mode 100644 index 000000000..58ddc6841 --- /dev/null +++ b/src/clis/binance/depth.yaml @@ -0,0 +1,32 @@ +site: binance +name: depth +description: Order book bid prices for a trading pair +domain: data-api.binance.vision +strategy: public +browser: false + +args: + symbol: + type: str + required: true + positional: true + description: "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + limit: + type: int + default: 10 + description: Number of price levels (5, 10, 20, 50, 100) + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }} + + - select: bids + + - map: + rank: ${{ index + 1 }} + bid_price: ${{ item.0 }} + bid_qty: ${{ item.1 }} + + - limit: ${{ args.limit }} + +columns: [rank, bid_price, bid_qty] diff --git a/src/clis/binance/gainers.yaml b/src/clis/binance/gainers.yaml new file mode 100644 index 000000000..27e5e0087 --- /dev/null +++ b/src/clis/binance/gainers.yaml @@ -0,0 +1,33 @@ +site: binance +name: gainers +description: Top gaining trading pairs by 24h price change +domain: data-api.binance.vision +strategy: public +browser: false + +args: + limit: + type: int + default: 10 + description: Number of trading pairs + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/ticker/24hr + + - filter: item.priceChangePercent + + - sort: + by: priceChangePercent + order: desc + + - map: + rank: ${{ index + 1 }} + symbol: ${{ item.symbol }} + price: ${{ item.lastPrice }} + change_24h: ${{ item.priceChangePercent }} + volume: ${{ item.quoteVolume }} + + - limit: ${{ args.limit }} + +columns: [rank, symbol, price, change_24h, volume] diff --git a/src/clis/binance/klines.yaml b/src/clis/binance/klines.yaml new file mode 100644 index 000000000..f2cf3cb79 --- /dev/null +++ b/src/clis/binance/klines.yaml @@ -0,0 +1,36 @@ +site: binance +name: klines +description: Candlestick/kline data for a trading pair +domain: data-api.binance.vision +strategy: public +browser: false + +args: + symbol: + type: str + required: true + positional: true + description: "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + interval: + type: str + default: 1d + description: "Kline interval (1m, 5m, 15m, 1h, 4h, 1d, 1w, 1M)" + limit: + type: int + default: 10 + description: Number of klines (max 1000) + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/klines?symbol=${{ args.symbol }}&interval=${{ args.interval }}&limit=${{ args.limit }} + + - map: + open: ${{ item.1 }} + high: ${{ item.2 }} + low: ${{ item.3 }} + close: ${{ item.4 }} + volume: ${{ item.5 }} + + - limit: ${{ args.limit }} + +columns: [open, high, low, close, volume] diff --git a/src/clis/binance/losers.yaml b/src/clis/binance/losers.yaml new file mode 100644 index 000000000..a7650bd65 --- /dev/null +++ b/src/clis/binance/losers.yaml @@ -0,0 +1,32 @@ +site: binance +name: losers +description: Top losing trading pairs by 24h price change +domain: data-api.binance.vision +strategy: public +browser: false + +args: + limit: + type: int + default: 10 + description: Number of trading pairs + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/ticker/24hr + + - filter: item.priceChangePercent + + - sort: + by: priceChangePercent + + - map: + rank: ${{ index + 1 }} + symbol: ${{ item.symbol }} + price: ${{ item.lastPrice }} + change_24h: ${{ item.priceChangePercent }} + volume: ${{ item.quoteVolume }} + + - limit: ${{ args.limit }} + +columns: [rank, symbol, price, change_24h, volume] diff --git a/src/clis/binance/pairs.yaml b/src/clis/binance/pairs.yaml new file mode 100644 index 000000000..d0359b8c8 --- /dev/null +++ b/src/clis/binance/pairs.yaml @@ -0,0 +1,30 @@ +site: binance +name: pairs +description: List active trading pairs on Binance +domain: data-api.binance.vision +strategy: public +browser: false + +args: + limit: + type: int + default: 20 + description: Number of trading pairs + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/exchangeInfo + + - select: symbols + + - filter: item.status + + - map: + symbol: ${{ item.symbol }} + base: ${{ item.baseAsset }} + quote: ${{ item.quoteAsset }} + status: ${{ item.status }} + + - limit: ${{ args.limit }} + +columns: [symbol, base, quote, status] diff --git a/src/clis/binance/price.yaml b/src/clis/binance/price.yaml new file mode 100644 index 000000000..dd597baba --- /dev/null +++ b/src/clis/binance/price.yaml @@ -0,0 +1,30 @@ +site: binance +name: price +description: Quick price check for a trading pair +domain: data-api.binance.vision +strategy: public +browser: false + +args: + symbol: + type: str + required: true + positional: true + description: "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/ticker/24hr?symbol=${{ args.symbol }} + + - map: + symbol: ${{ item.symbol }} + price: ${{ item.lastPrice }} + change: ${{ item.priceChange }} + change_pct: ${{ item.priceChangePercent }} + high: ${{ item.highPrice }} + low: ${{ item.lowPrice }} + volume: ${{ item.volume }} + quote_volume: ${{ item.quoteVolume }} + trades: ${{ item.count }} + +columns: [symbol, price, change, change_pct, high, low, volume, quote_volume, trades] diff --git a/src/clis/binance/prices.yaml b/src/clis/binance/prices.yaml new file mode 100644 index 000000000..e4553dcb1 --- /dev/null +++ b/src/clis/binance/prices.yaml @@ -0,0 +1,25 @@ +site: binance +name: prices +description: Latest prices for all trading pairs +domain: data-api.binance.vision +strategy: public +browser: false + +args: + limit: + type: int + default: 20 + description: Number of prices + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/ticker/price + + - map: + rank: ${{ index + 1 }} + symbol: ${{ item.symbol }} + price: ${{ item.price }} + + - limit: ${{ args.limit }} + +columns: [rank, symbol, price] diff --git a/src/clis/binance/ticker.yaml b/src/clis/binance/ticker.yaml new file mode 100644 index 000000000..5215b21f1 --- /dev/null +++ b/src/clis/binance/ticker.yaml @@ -0,0 +1,34 @@ +site: binance +name: ticker +description: 24h ticker statistics for top trading pairs by volume +domain: data-api.binance.vision +strategy: public +browser: false + +args: + limit: + type: int + default: 20 + description: Number of tickers + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/ticker/24hr + + - sort: + by: quoteVolume + order: desc + + - map: + symbol: ${{ item.symbol }} + price: ${{ item.lastPrice }} + change_pct: ${{ item.priceChangePercent }} + high: ${{ item.highPrice }} + low: ${{ item.lowPrice }} + volume: ${{ item.volume }} + quote_vol: ${{ item.quoteVolume }} + trades: ${{ item.count }} + + - limit: ${{ args.limit }} + +columns: [symbol, price, change_pct, high, low, volume, quote_vol, trades] diff --git a/src/clis/binance/top.yaml b/src/clis/binance/top.yaml new file mode 100644 index 000000000..1be83669d --- /dev/null +++ b/src/clis/binance/top.yaml @@ -0,0 +1,33 @@ +site: binance +name: top +description: Top trading pairs by 24h volume on Binance +domain: data-api.binance.vision +strategy: public +browser: false + +args: + limit: + type: int + default: 20 + description: Number of trading pairs + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/ticker/24hr + + - sort: + by: quoteVolume + order: desc + + - map: + rank: ${{ index + 1 }} + symbol: ${{ item.symbol }} + price: ${{ item.lastPrice }} + change_24h: ${{ item.priceChangePercent }} + high: ${{ item.highPrice }} + low: ${{ item.lowPrice }} + volume: ${{ item.quoteVolume }} + + - limit: ${{ args.limit }} + +columns: [rank, symbol, price, change_24h, high, low, volume] diff --git a/src/clis/binance/trades.yaml b/src/clis/binance/trades.yaml new file mode 100644 index 000000000..958e23143 --- /dev/null +++ b/src/clis/binance/trades.yaml @@ -0,0 +1,32 @@ +site: binance +name: trades +description: Recent trades for a trading pair +domain: data-api.binance.vision +strategy: public +browser: false + +args: + symbol: + type: str + required: true + positional: true + description: "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + limit: + type: int + default: 20 + description: Number of trades (max 1000) + +pipeline: + - fetch: + url: https://data-api.binance.vision/api/v3/trades?symbol=${{ args.symbol }}&limit=${{ args.limit }} + + - map: + id: ${{ item.id }} + price: ${{ item.price }} + qty: ${{ item.qty }} + quote_qty: ${{ item.quoteQty }} + buyer_maker: ${{ item.isBuyerMaker }} + + - limit: ${{ args.limit }} + +columns: [id, price, qty, quote_qty, buyer_maker] diff --git a/src/clis/github-trending/developers.yaml b/src/clis/github-trending/developers.yaml new file mode 100644 index 000000000..6a7b8e800 --- /dev/null +++ b/src/clis/github-trending/developers.yaml @@ -0,0 +1,50 @@ +site: github-trending +name: developers +description: Trending GitHub developers +domain: github.com +browser: true + +args: + since: + type: str + default: daily + description: Time range + choices: [daily, weekly, monthly] + language: + type: str + default: "" + description: "Programming language filter (e.g. python, javascript, rust)" + limit: + type: int + default: 25 + description: Number of developers + +pipeline: + - navigate: + url: https://github.com/trending/developers/${{ args.language }}?since=${{ args.since }} + settleMs: 3000 + + - evaluate: | + (() => { + const limit = ${{ args.limit }}; + const rows = document.querySelectorAll('article.Box-row'); + return Array.from(rows).slice(0, limit).map(function(row, i) { + const nameEl = row.querySelector('h1.h3 a'); + const name = nameEl ? nameEl.textContent.trim() : ''; + const usernameEl = row.querySelector('p.f4 a'); + const username = usernameEl ? usernameEl.textContent.trim() : ''; + const repoEl = row.querySelector('h1.h4 a'); + const repo = repoEl ? repoEl.textContent.trim().replace(/\s+/g, '') : ''; + const repoDesc = row.querySelector('span.repo-snipt-description'); + const desc = repoDesc ? repoDesc.textContent.trim().substring(0, 80) : ''; + return { + rank: i + 1, + name: name || username, + username: username || name, + popular_repo: repo, + description: desc, + }; + }); + })() + +columns: [rank, name, username, popular_repo, description] diff --git a/src/clis/github-trending/repos.yaml b/src/clis/github-trending/repos.yaml new file mode 100644 index 000000000..3803ac757 --- /dev/null +++ b/src/clis/github-trending/repos.yaml @@ -0,0 +1,55 @@ +site: github-trending +name: repos +description: Trending GitHub repositories +domain: github.com +browser: true + +args: + since: + type: str + default: daily + description: Time range + choices: [daily, weekly, monthly] + language: + type: str + default: "" + description: "Programming language filter (e.g. python, javascript, rust, go)" + limit: + type: int + default: 25 + description: Number of repos + +pipeline: + - navigate: + url: https://github.com/trending/${{ args.language }}?since=${{ args.since }} + settleMs: 3000 + + - evaluate: | + (() => { + const limit = ${{ args.limit }}; + const rows = document.querySelectorAll('article.Box-row'); + return Array.from(rows).slice(0, limit).map(function(row, i) { + const nameEl = row.querySelector('h2 a'); + const name = nameEl ? nameEl.textContent.trim().replace(/\s+/g, '') : ''; + const descEl = row.querySelector('p.col-9'); + const desc = descEl ? descEl.textContent.trim().substring(0, 100) : ''; + const langEl = row.querySelector('[itemprop="programmingLanguage"]'); + const lang = langEl ? langEl.textContent.trim() : ''; + const starsEls = row.querySelectorAll('a.Link--muted'); + const stars = starsEls[0] ? starsEls[0].textContent.trim() : ''; + const forks = starsEls[1] ? starsEls[1].textContent.trim() : ''; + const todayEl = row.querySelector('span.d-inline-block.float-sm-right'); + const today = todayEl ? todayEl.textContent.trim() : ''; + return { + rank: i + 1, + repo: name, + description: desc, + language: lang, + stars: stars, + forks: forks, + today: today, + }; + }); + })() + +columns: [rank, repo, language, stars, forks, today, description] diff --git a/src/clis/weather/air-quality.yaml b/src/clis/weather/air-quality.yaml new file mode 100644 index 000000000..82efe53df --- /dev/null +++ b/src/clis/weather/air-quality.yaml @@ -0,0 +1,53 @@ +site: weather +name: air-quality +description: Current air quality index for a city +domain: air-quality-api.open-meteo.com +strategy: public +browser: true + +args: + city: + type: str + required: true + positional: true + description: City name (e.g. Tokyo, London, "New York") + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const city = ${{ args.city | json }}; + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); + const geoData = await geoRes.json(); + if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); + const loc = geoData.results[0]; + const url = 'https://air-quality-api.open-meteo.com/v1/air-quality?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '¤t=us_aqi,european_aqi,pm10,pm2_5,carbon_monoxide,nitrogen_dioxide,sulphur_dioxide,ozone'; + const res = await fetch(url); + const data = await res.json(); + const c = data.current; + const aqi = c.us_aqi; + let level = 'Good'; + if (aqi > 300) level = 'Hazardous'; + else if (aqi > 200) level = 'Very Unhealthy'; + else if (aqi > 150) level = 'Unhealthy'; + else if (aqi > 100) level = 'Unhealthy for Sensitive'; + else if (aqi > 50) level = 'Moderate'; + return [{ + city: loc.name, + country: loc.country_code || '', + us_aqi: c.us_aqi, + eu_aqi: c.european_aqi, + level: level, + pm2_5: c.pm2_5, + pm10: c.pm10, + ozone: c.ozone, + no2: c.nitrogen_dioxide, + so2: c.sulphur_dioxide, + co: c.carbon_monoxide + }]; + })() + +columns: [city, country, us_aqi, eu_aqi, level, pm2_5, pm10, ozone, no2, so2] diff --git a/src/clis/weather/air.yaml b/src/clis/weather/air.yaml new file mode 100644 index 000000000..f52e9ab5c --- /dev/null +++ b/src/clis/weather/air.yaml @@ -0,0 +1,46 @@ +site: weather +name: air +description: Air quality index for a city +domain: air-quality-api.open-meteo.com +browser: true + +args: + city: + type: str + required: true + positional: true + description: City name + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const city = ${{ args.city | json }}; + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); + const geo = await geoRes.json(); + if (!geo.results || geo.results.length === 0) throw new Error('City not found: ' + city); + const loc = geo.results[0]; + const res = await fetch('https://air-quality-api.open-meteo.com/v1/air-quality?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '¤t=us_aqi,pm2_5,pm10,carbon_monoxide,nitrogen_dioxide,ozone'); + const d = await res.json(); + const c = d.current; + var level = 'Good'; + if (c.us_aqi > 50) level = 'Moderate'; + if (c.us_aqi > 100) level = 'Unhealthy for Sensitive'; + if (c.us_aqi > 150) level = 'Unhealthy'; + if (c.us_aqi > 200) level = 'Very Unhealthy'; + if (c.us_aqi > 300) level = 'Hazardous'; + return [{ + city: loc.name, + country: loc.country || '', + aqi: c.us_aqi, + level: level, + pm25: c.pm2_5 + ' µg/m³', + pm10: c.pm10 + ' µg/m³', + ozone: c.ozone + ' µg/m³', + }]; + })() + +columns: [city, country, aqi, level, pm25, pm10, ozone] diff --git a/src/clis/weather/compare.yaml b/src/clis/weather/compare.yaml new file mode 100644 index 000000000..873bb0467 --- /dev/null +++ b/src/clis/weather/compare.yaml @@ -0,0 +1,52 @@ +site: weather +name: compare +description: Compare current weather across multiple cities +domain: api.open-meteo.com +strategy: public +browser: true + +args: + cities: + type: str + required: true + positional: true + description: "Comma-separated city names (e.g. Tokyo,London,Paris)" + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const citiesStr = ${{ args.cities | json }}; + const cityNames = citiesStr.split(',').map(s => s.trim()).filter(Boolean); + if (cityNames.length === 0) throw new Error('No cities provided'); + const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm'}; + const results = []; + for (const name of cityNames) { + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(name) + '&count=1'); + const geoData = await geoRes.json(); + if (!geoData.results || geoData.results.length === 0) { + results.push({ city: name, country: '?', condition: 'Not found', temp_c: '-', humidity_pct: '-', wind_kmh: '-', precip_mm: '-' }); + continue; + } + const loc = geoData.results[0]; + const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,precipitation,weather_code&timezone=auto'; + const res = await fetch(url); + const data = await res.json(); + const c = data.current; + results.push({ + city: loc.name, + country: loc.country_code || '', + condition: codes[c.weather_code] || ('Code ' + c.weather_code), + temp_c: c.temperature_2m, + humidity_pct: c.relative_humidity_2m, + wind_kmh: c.wind_speed_10m, + precip_mm: c.precipitation + }); + } + return results; + })() + +columns: [city, country, condition, temp_c, humidity_pct, wind_kmh, precip_mm] diff --git a/src/clis/weather/current.yaml b/src/clis/weather/current.yaml new file mode 100644 index 000000000..0a8455b8e --- /dev/null +++ b/src/clis/weather/current.yaml @@ -0,0 +1,49 @@ +site: weather +name: current +description: Current weather for a city +domain: api.open-meteo.com +strategy: public +browser: true + +args: + city: + type: str + required: true + positional: true + description: City name (e.g. Tokyo, London, "New York") + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const city = ${{ args.city | json }}; + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); + const geoData = await geoRes.json(); + if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); + const loc = geoData.results[0]; + const lat = loc.latitude; + const lon = loc.longitude; + const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + lat + '&longitude=' + lon + '¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_direction_10m,pressure_msl,precipitation,weather_code&timezone=auto'; + const res = await fetch(url); + const data = await res.json(); + const c = data.current; + const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',66:'Freezing rain',67:'Heavy freezing rain',71:'Light snow',73:'Snow',75:'Heavy snow',77:'Snow grains',80:'Light showers',81:'Showers',82:'Heavy showers',85:'Light snow showers',86:'Heavy snow showers',95:'Thunderstorm',96:'Thunderstorm w/ hail',99:'Heavy thunderstorm w/ hail'}; + return [{ + city: loc.name, + country: loc.country_code || '', + condition: codes[c.weather_code] || ('Code ' + c.weather_code), + temp_c: c.temperature_2m, + feels_like_c: c.apparent_temperature, + humidity_pct: c.relative_humidity_2m, + wind_kmh: c.wind_speed_10m, + wind_dir: c.wind_direction_10m, + pressure_hpa: c.pressure_msl, + precip_mm: c.precipitation, + timezone: data.timezone + }]; + })() + +columns: [city, country, condition, temp_c, feels_like_c, humidity_pct, wind_kmh, pressure_hpa] diff --git a/src/clis/weather/forecast.yaml b/src/clis/weather/forecast.yaml new file mode 100644 index 000000000..ed296420e --- /dev/null +++ b/src/clis/weather/forecast.yaml @@ -0,0 +1,52 @@ +site: weather +name: forecast +description: 7-day weather forecast for a city +domain: api.open-meteo.com +strategy: public +browser: true + +args: + city: + type: str + required: true + positional: true + description: City name (e.g. Tokyo, London, "New York") + days: + type: int + default: 7 + description: Number of forecast days (1-16) + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const city = ${{ args.city | json }}; + const days = ${{ args.days }}; + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); + const geoData = await geoRes.json(); + if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); + const loc = geoData.results[0]; + const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,precipitation_sum,precipitation_probability_max,wind_speed_10m_max,weather_code&forecast_days=' + days + '&timezone=auto'; + const res = await fetch(url); + const data = await res.json(); + const d = data.daily; + const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm',96:'Thunderstorm w/ hail',99:'Heavy thunderstorm w/ hail'}; + const results = []; + for (let i = 0; i < d.time.length; i++) { + results.push({ + date: d.time[i], + condition: codes[d.weather_code[i]] || ('Code ' + d.weather_code[i]), + high_c: d.temperature_2m_max[i], + low_c: d.temperature_2m_min[i], + precip_mm: d.precipitation_sum[i], + rain_pct: d.precipitation_probability_max[i], + wind_kmh: d.wind_speed_10m_max[i] + }); + } + return results; + })() + +columns: [date, condition, high_c, low_c, precip_mm, rain_pct, wind_kmh] diff --git a/src/clis/weather/history.yaml b/src/clis/weather/history.yaml new file mode 100644 index 000000000..db04ed848 --- /dev/null +++ b/src/clis/weather/history.yaml @@ -0,0 +1,51 @@ +site: weather +name: history +description: Historical weather data for a city (past days) +domain: api.open-meteo.com +strategy: public +browser: true + +args: + city: + type: str + required: true + positional: true + description: City name (e.g. Tokyo, London, "New York") + days: + type: int + default: 7 + description: Number of past days (1-92) + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const city = ${{ args.city | json }}; + const days = Math.min(${{ args.days }}, 92); + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); + const geoData = await geoRes.json(); + if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); + const loc = geoData.results[0]; + const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max,weather_code&past_days=' + days + '&forecast_days=0&timezone=auto'; + const res = await fetch(url); + const data = await res.json(); + const d = data.daily; + const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm'}; + const results = []; + for (let i = 0; i < d.time.length; i++) { + results.push({ + date: d.time[i], + condition: codes[d.weather_code[i]] || ('Code ' + d.weather_code[i]), + high_c: d.temperature_2m_max[i], + low_c: d.temperature_2m_min[i], + precip_mm: d.precipitation_sum[i], + wind_kmh: d.wind_speed_10m_max[i] + }); + } + return results; + })() + +columns: [date, condition, high_c, low_c, precip_mm, wind_kmh] diff --git a/src/clis/weather/hourly.yaml b/src/clis/weather/hourly.yaml new file mode 100644 index 000000000..b1c059d89 --- /dev/null +++ b/src/clis/weather/hourly.yaml @@ -0,0 +1,53 @@ +site: weather +name: hourly +description: Hourly weather forecast for a city (next 24h) +domain: api.open-meteo.com +strategy: public +browser: true + +args: + city: + type: str + required: true + positional: true + description: City name (e.g. Tokyo, London, "New York") + hours: + type: int + default: 24 + description: Number of hours to show + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const city = ${{ args.city | json }}; + const hours = ${{ args.hours }}; + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); + const geoData = await geoRes.json(); + if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); + const loc = geoData.results[0]; + const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&hourly=temperature_2m,apparent_temperature,precipitation_probability,precipitation,weather_code,wind_speed_10m,relative_humidity_2m&forecast_hours=' + hours + '&timezone=auto'; + const res = await fetch(url); + const data = await res.json(); + const h = data.hourly; + const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm'}; + const results = []; + for (let i = 0; i < h.time.length && i < hours; i++) { + results.push({ + time: h.time[i].replace('T', ' '), + condition: codes[h.weather_code[i]] || ('Code ' + h.weather_code[i]), + temp_c: h.temperature_2m[i], + feels_c: h.apparent_temperature[i], + humidity_pct: h.relative_humidity_2m[i], + rain_pct: h.precipitation_probability[i], + precip_mm: h.precipitation[i], + wind_kmh: h.wind_speed_10m[i] + }); + } + return results; + })() + +columns: [time, condition, temp_c, feels_c, humidity_pct, rain_pct, wind_kmh] diff --git a/src/clis/weather/precipitation.yaml b/src/clis/weather/precipitation.yaml new file mode 100644 index 000000000..af8784912 --- /dev/null +++ b/src/clis/weather/precipitation.yaml @@ -0,0 +1,52 @@ +site: weather +name: precipitation +description: Precipitation forecast for a city +domain: api.open-meteo.com +strategy: public +browser: true + +args: + city: + type: str + required: true + positional: true + description: City name (e.g. Tokyo, London, "New York") + days: + type: int + default: 7 + description: Number of forecast days + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const city = ${{ args.city | json }}; + const days = ${{ args.days }}; + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); + const geoData = await geoRes.json(); + if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); + const loc = geoData.results[0]; + const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&daily=precipitation_sum,rain_sum,snowfall_sum,precipitation_hours,precipitation_probability_max,weather_code&forecast_days=' + days + '&timezone=auto'; + const res = await fetch(url); + const data = await res.json(); + const d = data.daily; + const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm'}; + const results = []; + for (let i = 0; i < d.time.length; i++) { + results.push({ + date: d.time[i], + condition: codes[d.weather_code[i]] || ('Code ' + d.weather_code[i]), + total_mm: d.precipitation_sum[i], + rain_mm: d.rain_sum[i], + snow_cm: d.snowfall_sum[i], + hours: d.precipitation_hours[i], + probability: d.precipitation_probability_max[i] + }); + } + return results; + })() + +columns: [date, condition, total_mm, rain_mm, snow_cm, hours, probability] diff --git a/src/clis/weather/search.yaml b/src/clis/weather/search.yaml new file mode 100644 index 000000000..ace215a00 --- /dev/null +++ b/src/clis/weather/search.yaml @@ -0,0 +1,38 @@ +site: weather +name: search +description: Search for cities (geocoding) +domain: geocoding-api.open-meteo.com +strategy: public +browser: false + +args: + query: + type: str + required: true + positional: true + description: City name to search + limit: + type: int + default: 5 + description: Number of results + +pipeline: + - fetch: + url: https://geocoding-api.open-meteo.com/v1/search?name=${{ args.query }}&count=${{ args.limit }} + + - select: results + + - map: + rank: ${{ index + 1 }} + name: ${{ item.name }} + country: ${{ item.country }} + admin: ${{ item.admin1 }} + latitude: ${{ item.latitude }} + longitude: ${{ item.longitude }} + elevation: ${{ item.elevation }} + timezone: ${{ item.timezone }} + population: ${{ item.population }} + + - limit: ${{ args.limit }} + +columns: [rank, name, country, admin, latitude, longitude, elevation, timezone, population] diff --git a/src/clis/weather/sunrise.yaml b/src/clis/weather/sunrise.yaml new file mode 100644 index 000000000..3c5e197a0 --- /dev/null +++ b/src/clis/weather/sunrise.yaml @@ -0,0 +1,50 @@ +site: weather +name: sunrise +description: Sunrise and sunset times for a city +domain: api.open-meteo.com +strategy: public +browser: true + +args: + city: + type: str + required: true + positional: true + description: City name (e.g. Tokyo, London, "New York") + days: + type: int + default: 7 + description: Number of days to show + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const city = ${{ args.city | json }}; + const days = ${{ args.days }}; + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); + const geoData = await geoRes.json(); + if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); + const loc = geoData.results[0]; + const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&daily=sunrise,sunset,daylight_duration,uv_index_max&forecast_days=' + days + '&timezone=auto'; + const res = await fetch(url); + const data = await res.json(); + const d = data.daily; + const results = []; + for (let i = 0; i < d.time.length; i++) { + const dlHrs = (d.daylight_duration[i] / 3600).toFixed(1); + results.push({ + date: d.time[i], + sunrise: d.sunrise[i] ? d.sunrise[i].replace('T', ' ') : '-', + sunset: d.sunset[i] ? d.sunset[i].replace('T', ' ') : '-', + daylight_hrs: dlHrs, + uv_index: d.uv_index_max[i] + }); + } + return results; + })() + +columns: [date, sunrise, sunset, daylight_hrs, uv_index] diff --git a/src/clis/weather/wind.yaml b/src/clis/weather/wind.yaml new file mode 100644 index 000000000..5e7a30d90 --- /dev/null +++ b/src/clis/weather/wind.yaml @@ -0,0 +1,53 @@ +site: weather +name: wind +description: Wind forecast for a city +domain: api.open-meteo.com +strategy: public +browser: true + +args: + city: + type: str + required: true + positional: true + description: City name (e.g. Tokyo, London, "New York") + hours: + type: int + default: 24 + description: Number of hours to show + +pipeline: + - navigate: + url: https://open-meteo.com + settleMs: 2000 + + - evaluate: | + (async () => { + const city = ${{ args.city | json }}; + const hours = ${{ args.hours }}; + const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); + const geoData = await geoRes.json(); + if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); + const loc = geoData.results[0]; + const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&hourly=wind_speed_10m,wind_direction_10m,wind_gusts_10m,wind_speed_80m,wind_direction_80m&forecast_hours=' + hours + '&timezone=auto'; + const res = await fetch(url); + const data = await res.json(); + const h = data.hourly; + const dirs = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW']; + function degToDir(deg) { return dirs[Math.round(deg / 22.5) % 16]; } + const results = []; + for (let i = 0; i < h.time.length && i < hours; i++) { + results.push({ + time: h.time[i].replace('T', ' '), + speed_kmh: h.wind_speed_10m[i], + direction: degToDir(h.wind_direction_10m[i]), + deg: h.wind_direction_10m[i], + gusts_kmh: h.wind_gusts_10m[i], + speed_80m: h.wind_speed_80m[i], + dir_80m: degToDir(h.wind_direction_80m[i]) + }); + } + return results; + })() + +columns: [time, speed_kmh, direction, gusts_kmh, speed_80m, dir_80m] From 2eee2e5fa8279f35322a9088cac87ce5c66335e5 Mon Sep 17 00:00:00 2001 From: 0xsline Date: Sun, 22 Mar 2026 01:40:49 +0800 Subject: [PATCH 2/5] docs: add missing douban, sinablog, substack adapter documentation --- docs/adapters/browser/douban.md | 23 +++++++++++++++++++++++ docs/adapters/browser/sinablog.md | 24 ++++++++++++++++++++++++ docs/adapters/browser/substack.md | 23 +++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 docs/adapters/browser/douban.md create mode 100644 docs/adapters/browser/sinablog.md create mode 100644 docs/adapters/browser/substack.md diff --git a/docs/adapters/browser/douban.md b/docs/adapters/browser/douban.md new file mode 100644 index 000000000..d310ff67b --- /dev/null +++ b/docs/adapters/browser/douban.md @@ -0,0 +1,23 @@ +# Douban + +**Mode**: 🌐 Public · **Domain**: `douban.com` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli douban movie-hot` | Hot movies on Douban | +| `opencli douban book-hot` | Hot books on Douban | +| `opencli douban search` | Search Douban | + +## Usage Examples + +```bash +opencli douban movie-hot --limit 10 +opencli douban book-hot --limit 10 +opencli douban search --query "三体" +``` + +## Prerequisites + +None — public data, no login required. diff --git a/docs/adapters/browser/sinablog.md b/docs/adapters/browser/sinablog.md new file mode 100644 index 000000000..52b44588b --- /dev/null +++ b/docs/adapters/browser/sinablog.md @@ -0,0 +1,24 @@ +# Sina Blog + +**Mode**: 🌐 Public · **Domain**: `blog.sina.com.cn` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli sinablog hot` | Hot blog posts | +| `opencli sinablog search` | Search blog posts | +| `opencli sinablog article` | Read a blog article | +| `opencli sinablog user` | User's blog posts | + +## Usage Examples + +```bash +opencli sinablog hot --limit 10 +opencli sinablog search --query "科技" +opencli sinablog user --username someone +``` + +## Prerequisites + +None — public data, no login required. diff --git a/docs/adapters/browser/substack.md b/docs/adapters/browser/substack.md new file mode 100644 index 000000000..6573b2278 --- /dev/null +++ b/docs/adapters/browser/substack.md @@ -0,0 +1,23 @@ +# Substack + +**Mode**: 🌐 Public · **Domain**: `substack.com` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli substack search` | Search Substack publications | +| `opencli substack feed` | Publication feed | +| `opencli substack publication` | Publication info | + +## Usage Examples + +```bash +opencli substack search --query "AI" +opencli substack feed --name "platformer" +opencli substack publication --name "platformer" +``` + +## Prerequisites + +None — public data, no login required. From 2f325598548b5ff4eb3a92edb9b035fa0939cfe0 Mon Sep 17 00:00:00 2001 From: jackwener Date: Wed, 8 Apr 2026 00:51:52 +0800 Subject: [PATCH 3/5] fix(binance): sort numeric metrics and filter active pairs --- src/clis/binance/commands.test.ts | 63 +++++++++++++++++++++++++++++++ src/clis/binance/gainers.yaml | 9 ++++- src/clis/binance/losers.yaml | 9 ++++- src/clis/binance/pairs.yaml | 2 +- src/clis/binance/ticker.yaml | 13 ++++++- src/clis/binance/top.yaml | 11 +++++- 6 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 src/clis/binance/commands.test.ts diff --git a/src/clis/binance/commands.test.ts b/src/clis/binance/commands.test.ts new file mode 100644 index 000000000..a16b146b9 --- /dev/null +++ b/src/clis/binance/commands.test.ts @@ -0,0 +1,63 @@ +import fs from 'node:fs'; +import yaml from 'js-yaml'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { executePipeline } from '../../pipeline.js'; + +function loadPipeline(name: string): any[] { + const file = new URL(`./${name}.yaml`, import.meta.url); + const def = yaml.load(fs.readFileSync(file, 'utf-8')) as { pipeline: any[] }; + return def.pipeline; +} + +function mockJsonOnce(payload: unknown) { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue(payload), + })); +} + +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +describe('binance YAML adapters', () => { + it('sorts top pairs by numeric quote volume', async () => { + mockJsonOnce([ + { symbol: 'SMALL', lastPrice: '1', priceChangePercent: '1.2', highPrice: '1', lowPrice: '1', quoteVolume: '9.9' }, + { symbol: 'LARGE', lastPrice: '2', priceChangePercent: '2.3', highPrice: '2', lowPrice: '2', quoteVolume: '100.0' }, + { symbol: 'MID', lastPrice: '3', priceChangePercent: '3.4', highPrice: '3', lowPrice: '3', quoteVolume: '11.0' }, + ]); + + const result = await executePipeline(null, loadPipeline('top'), { args: { limit: 3 } }); + + expect(result.map((item: any) => item.symbol)).toEqual(['LARGE', 'MID', 'SMALL']); + expect(result.map((item: any) => item.rank)).toEqual([1, 2, 3]); + }); + + it('sorts gainers by numeric percent change', async () => { + mockJsonOnce([ + { symbol: 'TEN', lastPrice: '1', priceChangePercent: '10.0', quoteVolume: '100' }, + { symbol: 'NINE', lastPrice: '1', priceChangePercent: '9.5', quoteVolume: '100' }, + { symbol: 'HUNDRED', lastPrice: '1', priceChangePercent: '100.0', quoteVolume: '100' }, + ]); + + const result = await executePipeline(null, loadPipeline('gainers'), { args: { limit: 3 } }); + + expect(result.map((item: any) => item.symbol)).toEqual(['HUNDRED', 'TEN', 'NINE']); + }); + + it('keeps only TRADING pairs', async () => { + mockJsonOnce({ + symbols: [ + { symbol: 'BTCUSDT', baseAsset: 'BTC', quoteAsset: 'USDT', status: 'TRADING' }, + { symbol: 'OLDPAIR', baseAsset: 'OLD', quoteAsset: 'USDT', status: 'BREAK' }, + ], + }); + + const result = await executePipeline(null, loadPipeline('pairs'), { args: { limit: 10 } }); + + expect(result).toEqual([ + { symbol: 'BTCUSDT', base: 'BTC', quote: 'USDT', status: 'TRADING' }, + ]); + }); +}); diff --git a/src/clis/binance/gainers.yaml b/src/clis/binance/gainers.yaml index 27e5e0087..36e5732d8 100644 --- a/src/clis/binance/gainers.yaml +++ b/src/clis/binance/gainers.yaml @@ -17,8 +17,15 @@ pipeline: - filter: item.priceChangePercent + - map: + symbol: ${{ item.symbol }} + price: ${{ item.lastPrice }} + change_24h: ${{ item.priceChangePercent }} + volume: ${{ item.quoteVolume }} + sort_change: ${{ Number(item.priceChangePercent) }} + - sort: - by: priceChangePercent + by: sort_change order: desc - map: diff --git a/src/clis/binance/losers.yaml b/src/clis/binance/losers.yaml index a7650bd65..63c36f1da 100644 --- a/src/clis/binance/losers.yaml +++ b/src/clis/binance/losers.yaml @@ -17,8 +17,15 @@ pipeline: - filter: item.priceChangePercent + - map: + symbol: ${{ item.symbol }} + price: ${{ item.lastPrice }} + change_24h: ${{ item.priceChangePercent }} + volume: ${{ item.quoteVolume }} + sort_change: ${{ Number(item.priceChangePercent) }} + - sort: - by: priceChangePercent + by: sort_change - map: rank: ${{ index + 1 }} diff --git a/src/clis/binance/pairs.yaml b/src/clis/binance/pairs.yaml index d0359b8c8..dec32c840 100644 --- a/src/clis/binance/pairs.yaml +++ b/src/clis/binance/pairs.yaml @@ -17,7 +17,7 @@ pipeline: - select: symbols - - filter: item.status + - filter: item.status === 'TRADING' - map: symbol: ${{ item.symbol }} diff --git a/src/clis/binance/ticker.yaml b/src/clis/binance/ticker.yaml index 5215b21f1..9f2a46456 100644 --- a/src/clis/binance/ticker.yaml +++ b/src/clis/binance/ticker.yaml @@ -15,8 +15,19 @@ pipeline: - fetch: url: https://data-api.binance.vision/api/v3/ticker/24hr + - map: + symbol: ${{ item.symbol }} + price: ${{ item.lastPrice }} + change_pct: ${{ item.priceChangePercent }} + high: ${{ item.highPrice }} + low: ${{ item.lowPrice }} + volume: ${{ item.volume }} + quote_vol: ${{ item.quoteVolume }} + trades: ${{ item.count }} + sort_volume: ${{ Number(item.quoteVolume) }} + - sort: - by: quoteVolume + by: sort_volume order: desc - map: diff --git a/src/clis/binance/top.yaml b/src/clis/binance/top.yaml index 1be83669d..b4a871301 100644 --- a/src/clis/binance/top.yaml +++ b/src/clis/binance/top.yaml @@ -15,8 +15,17 @@ pipeline: - fetch: url: https://data-api.binance.vision/api/v3/ticker/24hr + - map: + symbol: ${{ item.symbol }} + price: ${{ item.lastPrice }} + change_24h: ${{ item.priceChangePercent }} + high: ${{ item.highPrice }} + low: ${{ item.lowPrice }} + volume: ${{ item.quoteVolume }} + sort_volume: ${{ Number(item.quoteVolume) }} + - sort: - by: quoteVolume + by: sort_volume order: desc - map: From 2ffc5a4167f4fb02dc9643b2e3720d50d3a250bd Mon Sep 17 00:00:00 2001 From: jackwener Date: Wed, 8 Apr 2026 00:59:50 +0800 Subject: [PATCH 4/5] chore: drop non-binance adapters from pr214 --- docs/adapters/browser/douban.md | 23 ------- docs/adapters/browser/github-trending.md | 37 ----------- docs/adapters/browser/sinablog.md | 24 -------- docs/adapters/browser/substack.md | 23 ------- docs/adapters/browser/weather.md | 78 ------------------------ src/clis/github-trending/developers.yaml | 50 --------------- src/clis/github-trending/repos.yaml | 55 ----------------- src/clis/weather/air-quality.yaml | 53 ---------------- src/clis/weather/air.yaml | 46 -------------- src/clis/weather/compare.yaml | 52 ---------------- src/clis/weather/current.yaml | 49 --------------- src/clis/weather/forecast.yaml | 52 ---------------- src/clis/weather/history.yaml | 51 ---------------- src/clis/weather/hourly.yaml | 53 ---------------- src/clis/weather/precipitation.yaml | 52 ---------------- src/clis/weather/search.yaml | 38 ------------ src/clis/weather/sunrise.yaml | 50 --------------- src/clis/weather/wind.yaml | 53 ---------------- 18 files changed, 839 deletions(-) delete mode 100644 docs/adapters/browser/douban.md delete mode 100644 docs/adapters/browser/github-trending.md delete mode 100644 docs/adapters/browser/sinablog.md delete mode 100644 docs/adapters/browser/substack.md delete mode 100644 docs/adapters/browser/weather.md delete mode 100644 src/clis/github-trending/developers.yaml delete mode 100644 src/clis/github-trending/repos.yaml delete mode 100644 src/clis/weather/air-quality.yaml delete mode 100644 src/clis/weather/air.yaml delete mode 100644 src/clis/weather/compare.yaml delete mode 100644 src/clis/weather/current.yaml delete mode 100644 src/clis/weather/forecast.yaml delete mode 100644 src/clis/weather/history.yaml delete mode 100644 src/clis/weather/hourly.yaml delete mode 100644 src/clis/weather/precipitation.yaml delete mode 100644 src/clis/weather/search.yaml delete mode 100644 src/clis/weather/sunrise.yaml delete mode 100644 src/clis/weather/wind.yaml diff --git a/docs/adapters/browser/douban.md b/docs/adapters/browser/douban.md deleted file mode 100644 index d310ff67b..000000000 --- a/docs/adapters/browser/douban.md +++ /dev/null @@ -1,23 +0,0 @@ -# Douban - -**Mode**: 🌐 Public · **Domain**: `douban.com` - -## Commands - -| Command | Description | -|---------|-------------| -| `opencli douban movie-hot` | Hot movies on Douban | -| `opencli douban book-hot` | Hot books on Douban | -| `opencli douban search` | Search Douban | - -## Usage Examples - -```bash -opencli douban movie-hot --limit 10 -opencli douban book-hot --limit 10 -opencli douban search --query "三体" -``` - -## Prerequisites - -None — public data, no login required. diff --git a/docs/adapters/browser/github-trending.md b/docs/adapters/browser/github-trending.md deleted file mode 100644 index a33b345f5..000000000 --- a/docs/adapters/browser/github-trending.md +++ /dev/null @@ -1,37 +0,0 @@ -# GitHub Trending - -**Mode**: 🔐 Browser · **Domain**: `github.com` - -## Commands - -| Command | Description | -|---------|-------------| -| `opencli github-trending repos` | Trending repositories | -| `opencli github-trending developers` | Trending developers | - -## Usage Examples - -```bash -# Daily trending repos -opencli github-trending repos --limit 10 - -# Weekly trending -opencli github-trending repos --since weekly - -# Trending Python repos -opencli github-trending repos --language python - -# Trending Rust repos this month -opencli github-trending repos --language rust --since monthly - -# Trending developers -opencli github-trending developers --limit 10 - -# JSON output -opencli github-trending repos --limit 5 -f json -``` - -## Prerequisites - -- Chrome running -- [Browser Bridge extension](/guide/browser-bridge) installed diff --git a/docs/adapters/browser/sinablog.md b/docs/adapters/browser/sinablog.md deleted file mode 100644 index 52b44588b..000000000 --- a/docs/adapters/browser/sinablog.md +++ /dev/null @@ -1,24 +0,0 @@ -# Sina Blog - -**Mode**: 🌐 Public · **Domain**: `blog.sina.com.cn` - -## Commands - -| Command | Description | -|---------|-------------| -| `opencli sinablog hot` | Hot blog posts | -| `opencli sinablog search` | Search blog posts | -| `opencli sinablog article` | Read a blog article | -| `opencli sinablog user` | User's blog posts | - -## Usage Examples - -```bash -opencli sinablog hot --limit 10 -opencli sinablog search --query "科技" -opencli sinablog user --username someone -``` - -## Prerequisites - -None — public data, no login required. diff --git a/docs/adapters/browser/substack.md b/docs/adapters/browser/substack.md deleted file mode 100644 index 6573b2278..000000000 --- a/docs/adapters/browser/substack.md +++ /dev/null @@ -1,23 +0,0 @@ -# Substack - -**Mode**: 🌐 Public · **Domain**: `substack.com` - -## Commands - -| Command | Description | -|---------|-------------| -| `opencli substack search` | Search Substack publications | -| `opencli substack feed` | Publication feed | -| `opencli substack publication` | Publication info | - -## Usage Examples - -```bash -opencli substack search --query "AI" -opencli substack feed --name "platformer" -opencli substack publication --name "platformer" -``` - -## Prerequisites - -None — public data, no login required. diff --git a/docs/adapters/browser/weather.md b/docs/adapters/browser/weather.md deleted file mode 100644 index 6303fe55c..000000000 --- a/docs/adapters/browser/weather.md +++ /dev/null @@ -1,78 +0,0 @@ -# Weather (Open Meteo) - -**Mode**: Public API + Browser · **Domain**: `api.open-meteo.com` - -Free worldwide weather data powered by [Open Meteo](https://open-meteo.com). No API key required. - -## Commands - -| Command | Description | -|---------|-------------| -| `opencli weather current` | Current weather for a city | -| `opencli weather forecast` | 7-day daily forecast | -| `opencli weather hourly` | Hourly forecast (next 24h) | -| `opencli weather search` | Search for cities (geocoding) | -| `opencli weather air-quality` | Current air quality index | -| `opencli weather sunrise` | Sunrise/sunset times and UV index | -| `opencli weather wind` | Detailed wind forecast | -| `opencli weather precipitation` | Rain and snow forecast | -| `opencli weather compare` | Compare weather across cities | -| `opencli weather history` | Historical weather data | - -## Usage Examples - -```bash -# Current weather -opencli weather current Tokyo -opencli weather current "New York" -opencli weather current London -f json - -# 7-day forecast -opencli weather forecast Paris -opencli weather forecast Berlin --days 14 - -# Hourly forecast -opencli weather hourly Sydney --hours 12 - -# Search for a city (no browser needed) -opencli weather search "San Francisco" -opencli weather search Mumbai --limit 10 - -# Air quality -opencli weather air-quality Beijing -opencli weather air-quality "Los Angeles" - -# Sunrise and sunset times -opencli weather sunrise Tokyo --days 14 - -# Wind forecast -opencli weather wind Chicago --hours 48 - -# Precipitation forecast -opencli weather precipitation London --days 10 - -# Compare multiple cities -opencli weather compare "Tokyo,London,Paris,New York" - -# Historical weather (past 30 days) -opencli weather history Berlin --days 30 - -# JSON output for scripting -opencli weather current Tokyo -f json -opencli weather forecast London -f json -``` - -## Notes - -- The `search` command uses a simple fetch pipeline (no browser needed) -- All other commands use browser mode for the 2-step geocoding lookup (city name -> coordinates -> weather data) -- City names with spaces must be quoted: `"New York"`, `"San Francisco"` -- Temperature is in Celsius, wind in km/h, precipitation in mm -- Air quality uses US AQI scale (0-500) -- Historical data supports up to 92 past days -- Forecast supports up to 16 days ahead - -## Prerequisites - -- Chrome running with [Browser Bridge extension](/guide/browser-bridge) installed -- No API key or login required - Open Meteo is completely free diff --git a/src/clis/github-trending/developers.yaml b/src/clis/github-trending/developers.yaml deleted file mode 100644 index 6a7b8e800..000000000 --- a/src/clis/github-trending/developers.yaml +++ /dev/null @@ -1,50 +0,0 @@ -site: github-trending -name: developers -description: Trending GitHub developers -domain: github.com -browser: true - -args: - since: - type: str - default: daily - description: Time range - choices: [daily, weekly, monthly] - language: - type: str - default: "" - description: "Programming language filter (e.g. python, javascript, rust)" - limit: - type: int - default: 25 - description: Number of developers - -pipeline: - - navigate: - url: https://github.com/trending/developers/${{ args.language }}?since=${{ args.since }} - settleMs: 3000 - - - evaluate: | - (() => { - const limit = ${{ args.limit }}; - const rows = document.querySelectorAll('article.Box-row'); - return Array.from(rows).slice(0, limit).map(function(row, i) { - const nameEl = row.querySelector('h1.h3 a'); - const name = nameEl ? nameEl.textContent.trim() : ''; - const usernameEl = row.querySelector('p.f4 a'); - const username = usernameEl ? usernameEl.textContent.trim() : ''; - const repoEl = row.querySelector('h1.h4 a'); - const repo = repoEl ? repoEl.textContent.trim().replace(/\s+/g, '') : ''; - const repoDesc = row.querySelector('span.repo-snipt-description'); - const desc = repoDesc ? repoDesc.textContent.trim().substring(0, 80) : ''; - return { - rank: i + 1, - name: name || username, - username: username || name, - popular_repo: repo, - description: desc, - }; - }); - })() - -columns: [rank, name, username, popular_repo, description] diff --git a/src/clis/github-trending/repos.yaml b/src/clis/github-trending/repos.yaml deleted file mode 100644 index 3803ac757..000000000 --- a/src/clis/github-trending/repos.yaml +++ /dev/null @@ -1,55 +0,0 @@ -site: github-trending -name: repos -description: Trending GitHub repositories -domain: github.com -browser: true - -args: - since: - type: str - default: daily - description: Time range - choices: [daily, weekly, monthly] - language: - type: str - default: "" - description: "Programming language filter (e.g. python, javascript, rust, go)" - limit: - type: int - default: 25 - description: Number of repos - -pipeline: - - navigate: - url: https://github.com/trending/${{ args.language }}?since=${{ args.since }} - settleMs: 3000 - - - evaluate: | - (() => { - const limit = ${{ args.limit }}; - const rows = document.querySelectorAll('article.Box-row'); - return Array.from(rows).slice(0, limit).map(function(row, i) { - const nameEl = row.querySelector('h2 a'); - const name = nameEl ? nameEl.textContent.trim().replace(/\s+/g, '') : ''; - const descEl = row.querySelector('p.col-9'); - const desc = descEl ? descEl.textContent.trim().substring(0, 100) : ''; - const langEl = row.querySelector('[itemprop="programmingLanguage"]'); - const lang = langEl ? langEl.textContent.trim() : ''; - const starsEls = row.querySelectorAll('a.Link--muted'); - const stars = starsEls[0] ? starsEls[0].textContent.trim() : ''; - const forks = starsEls[1] ? starsEls[1].textContent.trim() : ''; - const todayEl = row.querySelector('span.d-inline-block.float-sm-right'); - const today = todayEl ? todayEl.textContent.trim() : ''; - return { - rank: i + 1, - repo: name, - description: desc, - language: lang, - stars: stars, - forks: forks, - today: today, - }; - }); - })() - -columns: [rank, repo, language, stars, forks, today, description] diff --git a/src/clis/weather/air-quality.yaml b/src/clis/weather/air-quality.yaml deleted file mode 100644 index 82efe53df..000000000 --- a/src/clis/weather/air-quality.yaml +++ /dev/null @@ -1,53 +0,0 @@ -site: weather -name: air-quality -description: Current air quality index for a city -domain: air-quality-api.open-meteo.com -strategy: public -browser: true - -args: - city: - type: str - required: true - positional: true - description: City name (e.g. Tokyo, London, "New York") - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const city = ${{ args.city | json }}; - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); - const geoData = await geoRes.json(); - if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); - const loc = geoData.results[0]; - const url = 'https://air-quality-api.open-meteo.com/v1/air-quality?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '¤t=us_aqi,european_aqi,pm10,pm2_5,carbon_monoxide,nitrogen_dioxide,sulphur_dioxide,ozone'; - const res = await fetch(url); - const data = await res.json(); - const c = data.current; - const aqi = c.us_aqi; - let level = 'Good'; - if (aqi > 300) level = 'Hazardous'; - else if (aqi > 200) level = 'Very Unhealthy'; - else if (aqi > 150) level = 'Unhealthy'; - else if (aqi > 100) level = 'Unhealthy for Sensitive'; - else if (aqi > 50) level = 'Moderate'; - return [{ - city: loc.name, - country: loc.country_code || '', - us_aqi: c.us_aqi, - eu_aqi: c.european_aqi, - level: level, - pm2_5: c.pm2_5, - pm10: c.pm10, - ozone: c.ozone, - no2: c.nitrogen_dioxide, - so2: c.sulphur_dioxide, - co: c.carbon_monoxide - }]; - })() - -columns: [city, country, us_aqi, eu_aqi, level, pm2_5, pm10, ozone, no2, so2] diff --git a/src/clis/weather/air.yaml b/src/clis/weather/air.yaml deleted file mode 100644 index f52e9ab5c..000000000 --- a/src/clis/weather/air.yaml +++ /dev/null @@ -1,46 +0,0 @@ -site: weather -name: air -description: Air quality index for a city -domain: air-quality-api.open-meteo.com -browser: true - -args: - city: - type: str - required: true - positional: true - description: City name - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const city = ${{ args.city | json }}; - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); - const geo = await geoRes.json(); - if (!geo.results || geo.results.length === 0) throw new Error('City not found: ' + city); - const loc = geo.results[0]; - const res = await fetch('https://air-quality-api.open-meteo.com/v1/air-quality?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '¤t=us_aqi,pm2_5,pm10,carbon_monoxide,nitrogen_dioxide,ozone'); - const d = await res.json(); - const c = d.current; - var level = 'Good'; - if (c.us_aqi > 50) level = 'Moderate'; - if (c.us_aqi > 100) level = 'Unhealthy for Sensitive'; - if (c.us_aqi > 150) level = 'Unhealthy'; - if (c.us_aqi > 200) level = 'Very Unhealthy'; - if (c.us_aqi > 300) level = 'Hazardous'; - return [{ - city: loc.name, - country: loc.country || '', - aqi: c.us_aqi, - level: level, - pm25: c.pm2_5 + ' µg/m³', - pm10: c.pm10 + ' µg/m³', - ozone: c.ozone + ' µg/m³', - }]; - })() - -columns: [city, country, aqi, level, pm25, pm10, ozone] diff --git a/src/clis/weather/compare.yaml b/src/clis/weather/compare.yaml deleted file mode 100644 index 873bb0467..000000000 --- a/src/clis/weather/compare.yaml +++ /dev/null @@ -1,52 +0,0 @@ -site: weather -name: compare -description: Compare current weather across multiple cities -domain: api.open-meteo.com -strategy: public -browser: true - -args: - cities: - type: str - required: true - positional: true - description: "Comma-separated city names (e.g. Tokyo,London,Paris)" - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const citiesStr = ${{ args.cities | json }}; - const cityNames = citiesStr.split(',').map(s => s.trim()).filter(Boolean); - if (cityNames.length === 0) throw new Error('No cities provided'); - const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm'}; - const results = []; - for (const name of cityNames) { - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(name) + '&count=1'); - const geoData = await geoRes.json(); - if (!geoData.results || geoData.results.length === 0) { - results.push({ city: name, country: '?', condition: 'Not found', temp_c: '-', humidity_pct: '-', wind_kmh: '-', precip_mm: '-' }); - continue; - } - const loc = geoData.results[0]; - const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,precipitation,weather_code&timezone=auto'; - const res = await fetch(url); - const data = await res.json(); - const c = data.current; - results.push({ - city: loc.name, - country: loc.country_code || '', - condition: codes[c.weather_code] || ('Code ' + c.weather_code), - temp_c: c.temperature_2m, - humidity_pct: c.relative_humidity_2m, - wind_kmh: c.wind_speed_10m, - precip_mm: c.precipitation - }); - } - return results; - })() - -columns: [city, country, condition, temp_c, humidity_pct, wind_kmh, precip_mm] diff --git a/src/clis/weather/current.yaml b/src/clis/weather/current.yaml deleted file mode 100644 index 0a8455b8e..000000000 --- a/src/clis/weather/current.yaml +++ /dev/null @@ -1,49 +0,0 @@ -site: weather -name: current -description: Current weather for a city -domain: api.open-meteo.com -strategy: public -browser: true - -args: - city: - type: str - required: true - positional: true - description: City name (e.g. Tokyo, London, "New York") - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const city = ${{ args.city | json }}; - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); - const geoData = await geoRes.json(); - if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); - const loc = geoData.results[0]; - const lat = loc.latitude; - const lon = loc.longitude; - const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + lat + '&longitude=' + lon + '¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_direction_10m,pressure_msl,precipitation,weather_code&timezone=auto'; - const res = await fetch(url); - const data = await res.json(); - const c = data.current; - const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',66:'Freezing rain',67:'Heavy freezing rain',71:'Light snow',73:'Snow',75:'Heavy snow',77:'Snow grains',80:'Light showers',81:'Showers',82:'Heavy showers',85:'Light snow showers',86:'Heavy snow showers',95:'Thunderstorm',96:'Thunderstorm w/ hail',99:'Heavy thunderstorm w/ hail'}; - return [{ - city: loc.name, - country: loc.country_code || '', - condition: codes[c.weather_code] || ('Code ' + c.weather_code), - temp_c: c.temperature_2m, - feels_like_c: c.apparent_temperature, - humidity_pct: c.relative_humidity_2m, - wind_kmh: c.wind_speed_10m, - wind_dir: c.wind_direction_10m, - pressure_hpa: c.pressure_msl, - precip_mm: c.precipitation, - timezone: data.timezone - }]; - })() - -columns: [city, country, condition, temp_c, feels_like_c, humidity_pct, wind_kmh, pressure_hpa] diff --git a/src/clis/weather/forecast.yaml b/src/clis/weather/forecast.yaml deleted file mode 100644 index ed296420e..000000000 --- a/src/clis/weather/forecast.yaml +++ /dev/null @@ -1,52 +0,0 @@ -site: weather -name: forecast -description: 7-day weather forecast for a city -domain: api.open-meteo.com -strategy: public -browser: true - -args: - city: - type: str - required: true - positional: true - description: City name (e.g. Tokyo, London, "New York") - days: - type: int - default: 7 - description: Number of forecast days (1-16) - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const city = ${{ args.city | json }}; - const days = ${{ args.days }}; - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); - const geoData = await geoRes.json(); - if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); - const loc = geoData.results[0]; - const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,precipitation_sum,precipitation_probability_max,wind_speed_10m_max,weather_code&forecast_days=' + days + '&timezone=auto'; - const res = await fetch(url); - const data = await res.json(); - const d = data.daily; - const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm',96:'Thunderstorm w/ hail',99:'Heavy thunderstorm w/ hail'}; - const results = []; - for (let i = 0; i < d.time.length; i++) { - results.push({ - date: d.time[i], - condition: codes[d.weather_code[i]] || ('Code ' + d.weather_code[i]), - high_c: d.temperature_2m_max[i], - low_c: d.temperature_2m_min[i], - precip_mm: d.precipitation_sum[i], - rain_pct: d.precipitation_probability_max[i], - wind_kmh: d.wind_speed_10m_max[i] - }); - } - return results; - })() - -columns: [date, condition, high_c, low_c, precip_mm, rain_pct, wind_kmh] diff --git a/src/clis/weather/history.yaml b/src/clis/weather/history.yaml deleted file mode 100644 index db04ed848..000000000 --- a/src/clis/weather/history.yaml +++ /dev/null @@ -1,51 +0,0 @@ -site: weather -name: history -description: Historical weather data for a city (past days) -domain: api.open-meteo.com -strategy: public -browser: true - -args: - city: - type: str - required: true - positional: true - description: City name (e.g. Tokyo, London, "New York") - days: - type: int - default: 7 - description: Number of past days (1-92) - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const city = ${{ args.city | json }}; - const days = Math.min(${{ args.days }}, 92); - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); - const geoData = await geoRes.json(); - if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); - const loc = geoData.results[0]; - const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max,weather_code&past_days=' + days + '&forecast_days=0&timezone=auto'; - const res = await fetch(url); - const data = await res.json(); - const d = data.daily; - const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm'}; - const results = []; - for (let i = 0; i < d.time.length; i++) { - results.push({ - date: d.time[i], - condition: codes[d.weather_code[i]] || ('Code ' + d.weather_code[i]), - high_c: d.temperature_2m_max[i], - low_c: d.temperature_2m_min[i], - precip_mm: d.precipitation_sum[i], - wind_kmh: d.wind_speed_10m_max[i] - }); - } - return results; - })() - -columns: [date, condition, high_c, low_c, precip_mm, wind_kmh] diff --git a/src/clis/weather/hourly.yaml b/src/clis/weather/hourly.yaml deleted file mode 100644 index b1c059d89..000000000 --- a/src/clis/weather/hourly.yaml +++ /dev/null @@ -1,53 +0,0 @@ -site: weather -name: hourly -description: Hourly weather forecast for a city (next 24h) -domain: api.open-meteo.com -strategy: public -browser: true - -args: - city: - type: str - required: true - positional: true - description: City name (e.g. Tokyo, London, "New York") - hours: - type: int - default: 24 - description: Number of hours to show - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const city = ${{ args.city | json }}; - const hours = ${{ args.hours }}; - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); - const geoData = await geoRes.json(); - if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); - const loc = geoData.results[0]; - const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&hourly=temperature_2m,apparent_temperature,precipitation_probability,precipitation,weather_code,wind_speed_10m,relative_humidity_2m&forecast_hours=' + hours + '&timezone=auto'; - const res = await fetch(url); - const data = await res.json(); - const h = data.hourly; - const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm'}; - const results = []; - for (let i = 0; i < h.time.length && i < hours; i++) { - results.push({ - time: h.time[i].replace('T', ' '), - condition: codes[h.weather_code[i]] || ('Code ' + h.weather_code[i]), - temp_c: h.temperature_2m[i], - feels_c: h.apparent_temperature[i], - humidity_pct: h.relative_humidity_2m[i], - rain_pct: h.precipitation_probability[i], - precip_mm: h.precipitation[i], - wind_kmh: h.wind_speed_10m[i] - }); - } - return results; - })() - -columns: [time, condition, temp_c, feels_c, humidity_pct, rain_pct, wind_kmh] diff --git a/src/clis/weather/precipitation.yaml b/src/clis/weather/precipitation.yaml deleted file mode 100644 index af8784912..000000000 --- a/src/clis/weather/precipitation.yaml +++ /dev/null @@ -1,52 +0,0 @@ -site: weather -name: precipitation -description: Precipitation forecast for a city -domain: api.open-meteo.com -strategy: public -browser: true - -args: - city: - type: str - required: true - positional: true - description: City name (e.g. Tokyo, London, "New York") - days: - type: int - default: 7 - description: Number of forecast days - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const city = ${{ args.city | json }}; - const days = ${{ args.days }}; - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); - const geoData = await geoRes.json(); - if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); - const loc = geoData.results[0]; - const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&daily=precipitation_sum,rain_sum,snowfall_sum,precipitation_hours,precipitation_probability_max,weather_code&forecast_days=' + days + '&timezone=auto'; - const res = await fetch(url); - const data = await res.json(); - const d = data.daily; - const codes = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm'}; - const results = []; - for (let i = 0; i < d.time.length; i++) { - results.push({ - date: d.time[i], - condition: codes[d.weather_code[i]] || ('Code ' + d.weather_code[i]), - total_mm: d.precipitation_sum[i], - rain_mm: d.rain_sum[i], - snow_cm: d.snowfall_sum[i], - hours: d.precipitation_hours[i], - probability: d.precipitation_probability_max[i] - }); - } - return results; - })() - -columns: [date, condition, total_mm, rain_mm, snow_cm, hours, probability] diff --git a/src/clis/weather/search.yaml b/src/clis/weather/search.yaml deleted file mode 100644 index ace215a00..000000000 --- a/src/clis/weather/search.yaml +++ /dev/null @@ -1,38 +0,0 @@ -site: weather -name: search -description: Search for cities (geocoding) -domain: geocoding-api.open-meteo.com -strategy: public -browser: false - -args: - query: - type: str - required: true - positional: true - description: City name to search - limit: - type: int - default: 5 - description: Number of results - -pipeline: - - fetch: - url: https://geocoding-api.open-meteo.com/v1/search?name=${{ args.query }}&count=${{ args.limit }} - - - select: results - - - map: - rank: ${{ index + 1 }} - name: ${{ item.name }} - country: ${{ item.country }} - admin: ${{ item.admin1 }} - latitude: ${{ item.latitude }} - longitude: ${{ item.longitude }} - elevation: ${{ item.elevation }} - timezone: ${{ item.timezone }} - population: ${{ item.population }} - - - limit: ${{ args.limit }} - -columns: [rank, name, country, admin, latitude, longitude, elevation, timezone, population] diff --git a/src/clis/weather/sunrise.yaml b/src/clis/weather/sunrise.yaml deleted file mode 100644 index 3c5e197a0..000000000 --- a/src/clis/weather/sunrise.yaml +++ /dev/null @@ -1,50 +0,0 @@ -site: weather -name: sunrise -description: Sunrise and sunset times for a city -domain: api.open-meteo.com -strategy: public -browser: true - -args: - city: - type: str - required: true - positional: true - description: City name (e.g. Tokyo, London, "New York") - days: - type: int - default: 7 - description: Number of days to show - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const city = ${{ args.city | json }}; - const days = ${{ args.days }}; - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); - const geoData = await geoRes.json(); - if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); - const loc = geoData.results[0]; - const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&daily=sunrise,sunset,daylight_duration,uv_index_max&forecast_days=' + days + '&timezone=auto'; - const res = await fetch(url); - const data = await res.json(); - const d = data.daily; - const results = []; - for (let i = 0; i < d.time.length; i++) { - const dlHrs = (d.daylight_duration[i] / 3600).toFixed(1); - results.push({ - date: d.time[i], - sunrise: d.sunrise[i] ? d.sunrise[i].replace('T', ' ') : '-', - sunset: d.sunset[i] ? d.sunset[i].replace('T', ' ') : '-', - daylight_hrs: dlHrs, - uv_index: d.uv_index_max[i] - }); - } - return results; - })() - -columns: [date, sunrise, sunset, daylight_hrs, uv_index] diff --git a/src/clis/weather/wind.yaml b/src/clis/weather/wind.yaml deleted file mode 100644 index 5e7a30d90..000000000 --- a/src/clis/weather/wind.yaml +++ /dev/null @@ -1,53 +0,0 @@ -site: weather -name: wind -description: Wind forecast for a city -domain: api.open-meteo.com -strategy: public -browser: true - -args: - city: - type: str - required: true - positional: true - description: City name (e.g. Tokyo, London, "New York") - hours: - type: int - default: 24 - description: Number of hours to show - -pipeline: - - navigate: - url: https://open-meteo.com - settleMs: 2000 - - - evaluate: | - (async () => { - const city = ${{ args.city | json }}; - const hours = ${{ args.hours }}; - const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(city) + '&count=1'); - const geoData = await geoRes.json(); - if (!geoData.results || geoData.results.length === 0) throw new Error('City not found: ' + city); - const loc = geoData.results[0]; - const url = 'https://api.open-meteo.com/v1/forecast?latitude=' + loc.latitude + '&longitude=' + loc.longitude + '&hourly=wind_speed_10m,wind_direction_10m,wind_gusts_10m,wind_speed_80m,wind_direction_80m&forecast_hours=' + hours + '&timezone=auto'; - const res = await fetch(url); - const data = await res.json(); - const h = data.hourly; - const dirs = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW']; - function degToDir(deg) { return dirs[Math.round(deg / 22.5) % 16]; } - const results = []; - for (let i = 0; i < h.time.length && i < hours; i++) { - results.push({ - time: h.time[i].replace('T', ' '), - speed_kmh: h.wind_speed_10m[i], - direction: degToDir(h.wind_direction_10m[i]), - deg: h.wind_direction_10m[i], - gusts_kmh: h.wind_gusts_10m[i], - speed_80m: h.wind_speed_80m[i], - dir_80m: degToDir(h.wind_direction_80m[i]) - }); - } - return results; - })() - -columns: [time, speed_kmh, direction, gusts_kmh, speed_80m, dir_80m] From ce3e815db7811b0f2193d5918cbc2ecaef0ffad9 Mon Sep 17 00:00:00 2001 From: jackwener Date: Wed, 8 Apr 2026 01:07:36 +0800 Subject: [PATCH 5/5] chore: drop binance docs from pr214 --- docs/adapters/browser/binance.md | 65 -------------------------------- 1 file changed, 65 deletions(-) delete mode 100644 docs/adapters/browser/binance.md diff --git a/docs/adapters/browser/binance.md b/docs/adapters/browser/binance.md deleted file mode 100644 index 6f243f3ea..000000000 --- a/docs/adapters/browser/binance.md +++ /dev/null @@ -1,65 +0,0 @@ -# Binance - -**Mode**: :globe_with_meridians: Public API · **Domain**: `data-api.binance.vision` - -## Commands - -| Command | Description | -|---------|-------------| -| `opencli binance top` | Top trading pairs by 24h volume | -| `opencli binance price` | Quick price check for a trading pair | -| `opencli binance prices` | Latest prices for all trading pairs | -| `opencli binance ticker` | 24h ticker statistics for top pairs by volume | -| `opencli binance gainers` | Top gaining pairs by 24h price change | -| `opencli binance losers` | Top losing pairs by 24h price change | -| `opencli binance trades` | Recent trades for a trading pair | -| `opencli binance depth` | Order book bid prices for a trading pair | -| `opencli binance asks` | Order book ask prices for a trading pair | -| `opencli binance klines` | Candlestick/kline data for a trading pair | -| `opencli binance pairs` | List active trading pairs on Binance | - -## Usage Examples - -```bash -# Top trading pairs by volume -opencli binance top --limit 10 - -# Check price for a specific pair -opencli binance price --symbol BTCUSDT -opencli binance price --symbol ETHUSDT - -# All prices -opencli binance prices --limit 30 - -# 24h ticker stats -opencli binance ticker --limit 10 - -# Top gainers and losers -opencli binance gainers --limit 5 -opencli binance losers --limit 5 - -# Recent trades for BTC -opencli binance trades --symbol BTCUSDT --limit 10 - -# Order book bid and ask prices -opencli binance depth --symbol BTCUSDT --limit 5 -opencli binance asks --symbol BTCUSDT --limit 5 - -# Daily candlestick data -opencli binance klines --symbol BTCUSDT --interval 1d --limit 7 - -# Hourly candles -opencli binance klines --symbol ETHUSDT --interval 1h --limit 24 - -# List trading pairs -opencli binance pairs --limit 10 - -# JSON output -opencli binance top --limit 5 -f json -``` - -## Notes - -- All endpoints use Binance's public data API (no authentication required) -- Symbols are uppercase (e.g., BTCUSDT, ETHUSDT, BNBUSDT) -- Kline intervals: 1m, 5m, 15m, 1h, 4h, 1d, 1w, 1M