readwise-summarize
v1.1.0
Published
CLI tools for fetching Readwise Reader content and generating AI summaries
Maintainers
Readme
📚 readwise-summarize

CLI tools for fetching content from Readwise Reader and generating AI-powered article summaries via OpenRouter.
✅ Prerequisites
- Node.js 22+
- pnpm
- A Readwise Reader account and API token
- An OpenRouter API key (required for
rws summarizeandrws models rank-free)
⚙️ Setup
pnpm install📦 Install From npm
Install the published CLI globally from npm:
npm install -g readwise-summarizeThis installs one command on your machine:
rws
🔑 Configuration
Add credentials to .env:
cp .env.example .envREADWISE_TOKEN=your_token_here
OPEN_ROUTER_SUMMARIZE_API=your_key_hereGet your Readwise token at readwise.io/access_token.
🧰 Package As CLI (Local Only)
Build and expose commands locally without publishing:
pnpm build
pnpm link --globalThis installs one command on your machine:
rws
If installed globally, you can run it directly without the pnpm prefix. For example:
rws fetch --limit 5
rws fetch --category rss --limit 5 --with-content | rws summarize --verboseTo create a distributable tarball (still local):
pnpm pack🚢 Release Automation
This repo currently supports two release paths.
🖥️ Option 1: Local release command
Use the repo-local release command when publishing from your machine:
pnpm rws -- releaseTypical flow:
# 1. Prepare the release
# - update package.json version
# - update CHANGELOG.md
# - commit changes
# 2. Preview the release plan
pnpm rws -- release --dry-run
# Optional: preview the generated release notes
pnpm rws -- release-notes
# 3. Run the release
pnpm rws -- release --otp 123456pnpm rws -- release uses the generated pnpm rws -- release-notes output for the GitHub release body automatically.
Useful flags:
pnpm rws -- release --dry-runpnpm rws -- release --otp 123456pnpm rws -- release --skip-publishpnpm rws -- release --skip-github-releasepnpm rws -- release --skip-pushpnpm rws -- release-notes
☁️ Option 2: GitHub release publish
If GitHub trusted publishing is configured, publishing can also happen from GitHub:
# 1. Prepare the release
# - update package.json version
# - update CHANGELOG.md
# - commit changes
# - push branch
# 2. Create and push the tag
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin main
git push origin vX.Y.Z
# 3. Publish the GitHub release for that tag
pnpm rws -- release-notes X.Y.Z > /tmp/readwise-summarize-vX.Y.Z-notes.md
gh release create vX.Y.Z --verify-tag --title "vX.Y.Z" --notes-file /tmp/readwise-summarize-vX.Y.Z-notes.mdPublishing a GitHub release for the matching tag triggers the automated npm publish workflow.
The GitHub publish workflow upgrades npm before publishing because npm trusted publishing currently requires npm CLI 11.5.1+.
To configure the required GitHub Actions secrets from your machine:
pnpm rws -- github-secrets setThe helper uses gh secret set to upload:
READWISE_TOKENOPEN_ROUTER_SUMMARIZE_API
You can also pass the values non-interactively:
READWISE_TOKEN=your_token_here \
OPEN_ROUTER_SUMMARIZE_API=your_key_here \
pnpm rws -- github-secrets setThe helper now prefers exported environment variables first and falls back to .env automatically.
🔄 Pipeline
The tools are designed for a two-step daily workflow: fetch first, summarize second.
# Step 1: fetch and write to a dated file
rws fetch --category rss --with-content --all --output-dir /data/articles
# Step 2: summarize from that file, write summaries to a dated file
rws summarize /data/articles/articles-2026-03-05.json --output-dir /data/summariesrws fetch writes articles-YYYY-MM-DD.json using the machine's local timezone. The file is written atomically (.tmp + rename), so rws summarize will never read a partially-written file. The JSON envelope includes a complete: true field as a secondary integrity check.
Stdin piping is still supported for ad-hoc use:
pnpm --silent rws -- fetch --category rss --limit 5 --with-content | pnpm rws -- summarize --verbose📥 rws fetch
Fetches articles from Readwise Reader and writes a dated JSON file to disk.
pnpm rws -- fetch [options]⚙️ Options
API-side (sent to Readwise Reader):
| Flag | If omitted |
| ------------------------------------------------------------------------------------------- | --------------------------- |
| --location <loc> — feed | new | later | shortlist | archive | All locations |
| --category <cat> — rss | article | email | pdf | epub | tweet | video | All categories |
| --tag <tag> — tag name; empty string for untagged | All tags |
| --updated-after <date> — ISO 8601 or natural language (e.g. yesterday, 1 week ago) | No date filter |
| --limit <n> — results per API request, 1-100 | 100 per page |
| --all — paginate through all pages (3s delay between requests) | First page only |
| --with-content — include full HTML content (html_content field) | html_content not included |
Client-side (applied after fetch):
| Flag | If omitted |
| --------------------------------------------------------------- | -------------------------- |
| --published-since <date> — ISO 8601 or natural language | No date filter |
| --author <name> — case-insensitive substring match | All authors |
| --fields <fields> — comma-separated list of fields to include | Default fields (see below) |
| --output-dir <dir> — directory to write the output file | Current working directory |
| --prefix <name> — filename prefix for output file | articles |
| --verbose — print progress to stderr | off |
Default output fields: id, title, author, site_name, url, source_url, summary, tags, published_date, category
💡 Examples
# First 5 articles from your feed
pnpm rws -- fetch --limit 5
# All email newsletters updated in the last week
pnpm rws -- fetch --category email --updated-after "1 week ago" --all
# RSS articles with full HTML content (for summarization), saved to /data/articles
pnpm rws -- fetch --category rss --limit 10 --with-content --output-dir /data/articles
# Articles by a specific author updated yesterday
pnpm rws -- fetch --author "Lenny" --category email --updated-after yesterday
# All RSS articles published since Feb 1
pnpm rws -- fetch --category rss --published-since 2026-02-01 --allOutput Format
Writes <prefix>-YYYY-MM-DD.json to the output directory (date from machine local timezone; prefix defaults to articles). Progress and errors go to stderr.
{
"complete": true,
"count": 42,
"generated_at": "2026-03-05T10:00:00.000Z",
"documents": [
{
"id": "01kjpww0aa1w5wc2gfvv9m3jr5",
"title": "Article Title",
"author": "Author Name",
"site_name": "Site Name",
"url": "https://read.readwise.io/read/...",
"source_url": "https://original-site.com/article",
"summary": "A brief summary of the article.",
"tags": ["ai", "research"],
"published_date": "2026-02-26T00:00:00.000Z",
"category": "rss"
}
]
}Field notes:
complete— alwaystrue; signals the file was fully and successfully writtentags— flattened to an array of stringssource_url— original article URL;urlis the Readwise Reader URLpublished_date— ISO 8601 string, ornullif not sethtml_content— only present when--with-contentis passed; may benullfor some document types
rws summarize
Reads an rws fetch output file and writes AI-generated summaries to stdout.
pnpm rws -- summarize [options] <file>Falls back to reading a JSON array from stdin if no file argument is given (legacy pipe usage).
Options
| Flag | Default |
| ---------------------------------------------------------------------------------- | ------------------------------ |
| --model <id> — model ID e.g. google/gemma-3-27b-it:free | config.summarize.model |
| --scan-free — scan OpenRouter for the best free model, save it to config, use it | — |
| --with-original — include the original Readwise summary field in output | off |
| --concurrency <n> — parallel workers | config.summarize.concurrency |
| --max-tokens <n> — max tokens per summary | config.summarize.max_tokens |
| --timeout <ms> — per-request timeout | config.summarize.timeout_ms |
| --verbose — print progress to stderr | off |
| --output-dir <dir> — directory to write <prefix>-YYYY-MM-DD.json | stdout |
| --prefix <name> — filename prefix for output file | summaries |
Model resolution order: --model → --scan-free result → config.summarize.model → error
LLM Configuration (config.toml)
[summarize]
model = "" # default model; overridden by --model or --scan-free
max_tokens = 0
timeout_ms = 30000
concurrency = 1
temperature = 0.7
system_prompt = "You are a concise article summarizer. Summarize the article in 3-5 sentences focusing on key insights and takeaways."
user_prompt_template = "Title: {title}\nAuthor: {author}\n\n{html_content}"The user_prompt_template supports {title}, {author}, {url}, and {html_content} placeholders.
User overrides are loaded from a writable per-user config file instead of modifying the packaged config.toml.
- macOS:
~/Library/Application Support/readwise-summarize/config.toml - Linux:
$XDG_CONFIG_HOME/readwise-summarize/config.tomlor~/.config/readwise-summarize/config.toml - Windows:
%APPDATA%\\readwise-summarize\\config.toml - Override location for tests or automation:
READWISE_SUMMARIZE_CONFIG_DIR
Examples
# Summarize today's fetch using the configured model
pnpm rws -- summarize articles-2026-03-05.json --verbose
# Write summaries to a dated file instead of stdout
pnpm rws -- summarize articles-2026-03-05.json --output-dir /data/summaries
# Auto-select the best free model, then summarize
pnpm rws -- summarize articles-2026-03-05.json --scan-free --verbose
# Include the original Readwise summary alongside the AI summary
pnpm rws -- summarize articles-2026-03-05.json --with-originalOutput Format
[
{
"id": "01kjpww0aa1w5wc2gfvv9m3jr5",
"title": "Article Title",
"author": "Author Name",
"site_name": "Site Name",
"readwise": "https://read.readwise.io/read/...",
"source_url": "https://original-site.com/article",
"ai_summary": "AI-generated summary of the article.",
"original_summary": "Original Readwise summary (only with --with-original)."
}
]rws models rank-free
Scans OpenRouter for free models, probes each one, and ranks them by quality and speed. Useful for finding the best available free model for summarization.
pnpm rws -- models rank-free [options]Options
| Flag | Default |
| ------------------------------------------------------------ | ---------------------------------- |
| --min-params <Nb> — minimum parameter count e.g. 27b | config.openrouter.min_param_b |
| --max-age-days <n> — max model age in days; 0 to disable | config.openrouter.max_age_days |
| --concurrency <n> — parallel test workers | config.openrouter.concurrency |
| --timeout <ms> — per-model timeout | config.openrouter.timeout_ms |
| --candidates <n> — max candidates to output | config.openrouter.max_candidates |
| --smart <n> — smart-first picks (newest + largest context) | config.openrouter.smart_picks |
| --runs <n> — extra timing runs for median latency | config.openrouter.extra_runs |
| --verbose — print progress to stderr | off |
Example
pnpm rws -- models rank-free --verboseDevelopment
# Type-check
pnpm exec tsc --noEmit
# Run unit tests
pnpm test
# Watch mode
pnpm test:watch
# Run integration tests (requires live credentials in .env)
pnpm test:integrationProject Structure
src/
├── lib/
│ ├── types.ts # Shared interfaces (ReaderDocument, OutputDocument)
│ ├── app.ts # App identity and user-config path helpers
│ ├── config.ts # config.toml loader
│ ├── config.test.ts
│ ├── api.ts # Readwise Reader API client (fetch + pagination)
│ ├── api.test.ts
│ ├── transform.ts # Document transformation, filtering, field selection
│ ├── transform.test.ts
│ ├── parse-date.ts # Natural language + ISO 8601 date parsing
│ ├── parse-date.test.ts
│ ├── openrouter.ts # OpenRouter model scanning, probing, ranking
│ ├── openrouter.test.ts
│ ├── release.ts # Release plan validation and command generation
│ ├── release.test.ts
│ ├── summarize.ts # LLM summarization logic
│ └── summarize.test.ts
├── github-secrets-set.ts # CLI subcommand: upload GitHub Actions secrets
├── openrouter-rank-free.ts # CLI subcommand: scan and rank free OpenRouter models
├── reader-fetch.ts # CLI subcommand: fetch articles from Readwise Reader
├── release-notes.ts # CLI subcommand: changelog-driven release notes
├── release.ts # CLI subcommand: prepared npm release workflow
├── release.test.ts
├── rws.test.ts
├── rws.ts # Root CLI: unified command entrypoint
├── summarize.ts # CLI subcommand: generate AI summaries via OpenRouter
└── integration.test.ts # Integration tests (live API credentials required)
.github/workflows/publish-npm.yml # GitHub release -> npm publish workflow
config.toml # All runtime configuration
vitest.config.ts # Unit test config
vitest.integration.config.ts # Integration test configCredit
https://github.com/steipete/summarize for the summarize tool
License
MIT
