rss-ai
v0.1.12
Published
A powerful CLI RSS/Atom feed reader with OPML support, smart caching, full-text search, content extraction, and interactive TUI
Maintainers
Readme
RSS CLI
A powerful CLI RSS/Atom feed reader with OPML support, smart caching, full-text search, content extraction, broken link repair, and an interactive TUI. Built with Bun, Drizzle ORM, and SQLite.
Installation
# Run directly with bunx (no install required, requires Bun)
bunx rss-ai --help
# Or install globally
bun install -g rss-ai
rss-ai --helpThe published npm package requires Bun 1.3.3 or newer. To produce a local standalone executable, clone the repository
and run bun run build as shown under Development; releases do not currently publish prebuilt platform binaries.
All examples below use rss as shorthand for bunx rss-ai.
Quick Start
# Add a feed (database auto-initializes on first use)
rss feed add https://hnrss.org/frontpage
# Fetch all feeds
rss fetch all
# List unread articles
rss article list --unread
# Read an article
rss article show 1
# Launch interactive TUI
rss tuiFeatures
- RSS & Atom feed support with lenient parsing
- OPML import/export (1.0, 1.1, 2.0)
- Smart caching with ETag, Last-Modified, and content hashing
- Full-text search via SQLite FTS5
- Content extraction from original article links
- Broken link repair (HTTP/HTTPS, www toggle, archive.org fallback)
- Flexible date filtering (ISO, relative, natural language, ranges)
- JSON output on every command for scripting and agent workflows
- Interactive TUI with keyboard navigation and markdown rendering
- Auto-migration database on every boot via Drizzle ORM
- Per-process, per-domain rate limiting to avoid server hammering
- Concurrent fetching with configurable parallelism
Global Options
| Flag | Description |
|------|-------------|
| -d, --database <path> | Path to SQLite database (default: ~/.config/rss-ai/rss.db) |
| -j, --json | Output as JSON |
| -q, --quiet | Suppress non-essential output |
| -v, --verbose | Show full error stack traces |
Commands
Feed Management
rss feed add <url> # Add a new feed
-t, --title <title> # Custom title
-c, --category <category> # Assign category
--no-fetch # Skip initial fetch
rss feed list # List all feeds
-c, --category <category> # Filter by category
--active / --inactive / --errors # Filter by status
rss feed remove <id> # Remove feed and its articles (prompts in a TTY)
-f, --force # Required for scripts, JSON, and non-interactive use
rss feed update <id> # Update feed properties
-t, --title / -c, --category # New title or category
--activate / --deactivate # Toggle active status
rss feed categories # List all categories
rss feed cleanup --force # Remove feeds with errors
-e, --errors <n> # Min error count (default: 1)
--dry-run # Preview only
--force # Required for deletionFetching
rss fetch all # Fetch all active feeds
-l, --limit <n> # Max feeds to fetch
-f, --force # Ignore cache
-c, --concurrency <n> # Parallel fetches (1-20, default: 5)
--timeout <ms> # Request timeout (default: 30000)
rss fetch feed <id> # Fetch a single feed
-f, --force # Ignore cache
rss fetch content [ids...] # Fetch full article content
-l, --limit <n> # Max articles (default: 50)
--fix-broken # Try to repair broken URLs
--flag-broken # Mark definitively missing (404/410) URLs in DB
--no-skip-broken # Re-check known broken articles
--allow-private-network # Allow private/LAN Article destinations
-c, --concurrency <n> # Parallel fetches (1-20, default: 5)Article content fetching blocks loopback, private, link-local, reserved, and cloud metadata destinations by default,
including redirects and DNS answers that mix public and non-public addresses. Use --allow-private-network only for
trusted feeds that intentionally link to services on your LAN; cloud metadata destinations remain blocked. This policy
applies to publisher-controlled Article URLs, not to feed subscription URLs or the explicit rss article check command.
Because Bun's process-level proxy routing can override a pinned transport address, protected Article fetching fails closed
when the process starts with proxy environment variables. Restart with those variables unset to fetch Article content.
Articles
rss article list # List articles with filtering
-f, --feed <id> # Filter by feed
-u, --feed-url <url> # Filter by feed URL
-c, --category <category> # Filter by category
--unread / --read / --starred # Filter by status
--broken # Filter broken links
--author <name> # Filter by author
-s, --search <term> # Full-text search
--today / --yesterday # Date presets
--this-week / --this-month # Date presets
--from <date> / --to <date> # Date range
-l, --limit <n> / -o, --offset <n> # Pagination
--sort <field> --asc/--desc # Sorting
rss article show <id> # Show article details
-w, --web # Open in browser
rss article read <id> # Mark as read
-u, --unread # Mark as unread
rss article star <id> # Star article
-u, --unstar # Remove star
rss article read-all # Mark all as read
-f, --feed <id> # Only this feed
--older-than <date> # Only older articles
rss article stats # Article statistics
rss article check <url> # Check URL accessibility
--fix # Try to repair
rss article cleanup --read --force # Remove articles by criteria
--broken / --read # Filter criteria
--older-than <date> # Age filter
-f, --feed <id> # Scope to feed
--dry-run # Preview only
--force # Required for deletion
rss article remove-broken --force # Remove definitively missing (404/410) articles
-f, --feed <id> # Scope to feed
--dry-run # Preview only
--force # Required for deletionBulk cleanup commands never delete without --force; use --dry-run to preview first. For article cleanup,
--feed only limits scope, so one of --broken, --read, or --older-than is also required.
Import & Export
rss import <file|url> # Import feeds from OPML
-c, --category <category> # Default category
--dry-run # Preview only
--skip-errors # Skip invalid feeds
rss export opml # Export feeds to OPML
-o, --output <file> # Output file (default: stdout)
-f, --force # Atomically replace an existing file
rss export json # Export articles to JSON
-o, --output <file> # Output file (default: stdout)
--force # Atomically replace an existing file
-f, --feed <id> # Scope to feed
-l, --limit <n> # Max articles (default: 1000)File exports never overwrite an existing path by default. They are written and synced to a temporary file in the
destination directory, then published atomically, so a failed export does not leave a partial target. Pass --force
to atomically replace an existing file only after the complete replacement has been written and synced.
Database
rss db init # Initialize database (auto on first use)
rss db stats # Show feed/article counts
rss db vacuum # Compact database file
rss db cleanup --days <n> # Remove old cache entries (default: 30)Interactive TUI
rss tui # Launch interactive terminal UI| Key | Action | Key | Action |
|-----|--------|-----|--------|
| q | Quit | / | Search |
| Tab | Next panel | ? | Help |
| ↑/↓ or j/k | Navigate | Enter | Select/Open |
| a | Add feed | d | Delete feed |
| f | Fetch feed | F | Fetch all |
| r | Toggle read | s | Toggle star |
| u | Toggle unread filter | t | Cycle sort |
| o | Open in browser | h/Esc | Go back |
| n/p | Next/prev article | Space | Page down |
| g/G | Top/bottom | R | Mark all read |
| [ / ] | Resize sidebar | 1/2/3 | Layout mode |
Date Formats
| Format | Examples |
|--------|---------|
| ISO | 2024-01-15, 2024-01-15T10:30:00 |
| Relative | today, yesterday, week, month, year, this week, this month, this year |
| Expressions | 3 days ago, 2 weeks ago, 6 months ago |
| Ranges | 2024-01-01..2024-06-30, 2024-01-01 to 2024-06-30 |
Smart Caching
Three-layer caching minimizes bandwidth:
- HTTP conditional requests -- ETag and If-Modified-Since headers. Servers return 304 Not Modified instantly.
- Content hashing -- MD5 of response body detects changes even without HTTP cache headers.
- Article deduplication -- Incoming GUIDs and canonical, fragment-free HTTP links are matched with indexed lookups and in-memory Sets, preventing duplicate inserts without loading the full Article history.
Use --force to bypass conditional requests and content-hash short-circuiting. Identity deduplication always remains
active so a forced refresh cannot create duplicate Articles.
Resource Limits
Publisher-controlled input is bounded before expensive parsing and again before persistence:
| Limit | Value | |-------|-------| | Feed/OPML response | 10 MB | | Feed XML structure | 50,000 elements / 256 levels | | Feed title / description | 1,000 / 20,000 characters | | Feed category | 512 characters | | HTTP URL identities | 8,192 characters | | Article title / author / summary / content | 2,000 / 2,000 / 20,000 / 100,000 characters | | Article GUID / link | 4,096 / 8,192 characters; overlong identities are discarded or rejected, never truncated | | Article categories | 100 entries, 512 characters each, 10,000 characters aggregate | | OPML structure | 10,000 outlines / 50 levels |
Error Handling
Feeds with repeated failures are auto-deactivated:
| Error Type | Examples | Deactivation Threshold | |-----------|----------|------------------------| | Permanent | 404, 410, 403, parse failures | 3 consecutive errors | | Transient | 5xx, 429, timeouts, DNS | 5 consecutive errors |
Error count resets on successful fetch. Reactivate with rss feed update <id> --activate.
Environment Variables
| Variable | Description |
|----------|-------------|
| RSS_DB_PATH | Default database path (overridden by --database) |
| RSS_DEBUG=1 | Include stack traces in error output (equivalent to verbose error reporting) |
| RSS_ALLOW_PRIVATE_NETWORK=1 | Allow Article content requests to private/LAN destinations; metadata remains blocked |
Development
# Clone and install
git clone https://github.com/briansunter/rss-ai.git
cd rss-ai && bun install
# Run in development
bun run src/cli/index.ts <command>
# Run tests
bun test
# Lint
bun run lint
# Build standalone binary
bun run build
./rss-ai --help
# Generate migration after schema change
bun run db:generate