# 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

```bash
# Run directly with bunx (no install required, requires Bun)
bunx rss-ai --help

# Or install globally
bun install -g rss-ai
rss-ai --help
```

The 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

```bash
# 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 tui
```

## Features

- **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

```bash
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 deletion
```

### Fetching

```bash
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

```bash
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 deletion
```

Bulk 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

```bash
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

```bash
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

```bash
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:

1. **HTTP conditional requests** -- ETag and If-Modified-Since headers. Servers return 304 Not Modified instantly.
2. **Content hashing** -- MD5 of response body detects changes even without HTTP cache headers.
3. **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

```bash
# 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
```

## License

[MIT](LICENSE)
