Rollup logo

Rollup example commands

Runnable examples for inventory, digest generation, summary routing, the local web UI, and local tooling.

The rollup is the weekly Markdown + HTML digest Rollup writes to --output-dir (default ~/Documents/rollup-outputs). Each run also copies rollup_logo.png and favicon.ico beside the HTML file, and by default runs every output writer (xteink / txt / json / epub). See OUTPUT_WRITERS.md.

See README.md for setup, safety guarantees, and configuration defaults. Optional sticky config: CONFIG.md. Product shape: CONTRACT.md. Position: COMPARISON.md. Roadmap: ROADMAP.md. Default deploy: DOCKER.md.

Most recipes below use the host CLI (python -m rollup / rollup). With Docker, run the same commands via docker compose exec rollup … (paths inside the container are under /data/* — see DOCKER.md).

Run from the project root with the virtualenv active (host Python path only):

cd /path/to/rollup
source .venv/bin/activate

Default digest mode needs no Ollama server and makes no network calls unless you pass --linkedin (or enable [linkedin] in TOML), --reddit (or enable [reddit] in TOML), webpage queue fetches (on by default; --no-webpage to skip), --scholar-mode detailed (or [scholar].mode = "detailed"), or --ollama. Pass --ollama only when you want LLM summaries from a local Ollama instance.

If you pass summary flags (for example --summary-profile) without --ollama, Rollup ignores them and prints a warning.

Mode shorthand: --dry-run parses only and writes nothing; preview-summary mode is the normal no-Ollama digest; --ollama enables entry LLM summaries; --final-review runs whole-digest QA and can use Ollama even without --ollama; --final-review-mode report writes advisory QA only, while apply applies validated summary-only fixes.

Install

Default: Docker

mkdir -p ~/.config/rollup ~/Documents/rollup-outputs ./state ./logs
touch ~/.config/rollup/config.toml ~/.config/rollup/env
chmod 600 ~/.config/rollup/env
cp docker-compose.override.yml.example docker-compose.override.yml
docker compose up -d --build
open http://localhost:8765

Full guide: DOCKER.md.

Advanced: host Python / venv

Method

Command

PyPI (when published)

pip install rollup

Git checkout

pip install .

Editable + tests

pip install -e ".[dev,web]"

Web UI only

pip install 'rollup[web]'

LiteLLM providers

pip install 'rollup[llm]'

EPUB writer

pip install 'rollup[epub]'

uv

uv sync --extra dev --extra web

Inventory

Discover mbox folders and message counts (read-only; no body parsing):

python -m rollup inventory
python -m rollup inventory --root tests/fixtures/Newsletters.sbd
python -m rollup inventory --json-out ./output/inventory.json
# After setting root in ~/.config/rollup/config.toml, bare inventory uses that path:
python -m rollup inventory

Digest without Ollama (default)

Preview and generate digests with no Ollama server and no network calls (unless LinkedIn, Reddit, or webpage fetch is enabled). --no-ollama is optional — it is the default when neither --ollama nor --no-ollama is passed.

python -m rollup digest
python -m rollup digest --root tests/fixtures/Newsletters.sbd
python -m rollup digest --lookback-days 7
python -m rollup digest --dry-run --root tests/fixtures/Newsletters.sbd
python -m rollup digest --folder tech --exclude-folder hoops
python -m rollup digest --include-seen-undated
python -m rollup digest --cron --root tests/fixtures/Newsletters.sbd
python -m rollup digest --no-grouping --root tests/fixtures/Newsletters.sbd
python -m rollup digest --grouping-report --root tests/fixtures/Newsletters.sbd

After a digest, browse the indexed archive (requires pip install 'rollup[web]'):

python -m rollup web --open

LinkedIn content searches (opt-in network)

Author-list (fromMember) content-search URLs become digest sections. Default layout is feed (one linkedin:feed section). Set layout = "per_search" to keep each named search as linkedin:<slug> (needed for --folder linkedin:general below). Fetch uses LinkedIn Voyager with your browser session. Link posts also fetch the linked article body by default (external blogs, Pulse). Reference: CONFIG.md.

1. Save a fromMember search URL

  1. Log in at linkedin.com in a desktop browser.

  2. Open Search, switch the results type to Content (not People or Jobs).

  3. Filter by the people you want (fromMember). The address bar must contain /search/results/content/ and fromMember= with ACo… ids.

  4. Copy that URL. Save it on the web LinkedIn page (/linkedin) with a display name, or in TOML. Store URLs only — never cookies.

# ~/.config/rollup/config.toml  (or ./rollup.toml)
[linkedin]
enabled = true
article_fetch = true   # default; set false to skip linked-article HTTP
layout = "per_search"  # named section per search; omit for default feed (`linkedin:feed`)

[linkedin.searches.general]
url = "https://www.linkedin.com/search/results/content/?origin=FACETED_SEARCH&datePosted=%5B%22past-week%22%5D&fromMember=%5B%22ACoAAAMN5aEBk7L5BGyjHbFsDr40zYqwuSB7tlw%22%2C%22ACoAAA5GcN4BlMrjuK1OVX4Q63rShHLMZuQ1Qyg%22%5D"
display_name = "General"
enabled = true

Keyword-only search, company pages, follows, and mentions are not supported yet.

2. Copy session cookies (li_at and JSESSIONID)

Voyager needs two cookies from the same logged-in browser pane. They are session secrets: put them in the process environment only. Never TOML, Settings, git, or screenshots you share.

Cookie

Environment variable

Role

li_at

ROLLUP_LINKEDIN_LI_AT

Session

JSESSIONID

ROLLUP_LINKEDIN_JSESSIONID

Voyager CSRF (ajax:…)

Chrome / Edge / Brave

  1. Stay logged in on https://www.linkedin.com.

  2. Open DevTools (Cmd+Option+I on macOS, F12 elsewhere).

  3. Application (Chrome/Edge) or Application / Storage → Cookies → https://www.linkedin.com.

  4. Find li_at. Copy the Value column (a long AQE… string).

  5. Find JSESSIONID. Copy the value. It usually looks like ajax:123… or "ajax:123…". Surrounding quotes are optional; Rollup accepts either.

Firefox

  1. DevTools (Cmd+Option+I) → Storage → Cookies → https://www.linkedin.com.

  2. Copy li_at and JSESSIONID values as above.

Safari

  1. Enable Develop menu (Settings → Advanced → Show features for web developers).

  2. Develop → Show Web Inspector → Storage → Cookies for linkedin.com.

  3. Copy li_at and JSESSIONID.

They rotate together. After a 401, refresh both from the same pane. Treat a new li_at with a stale JSESSIONID as expired.

3. Export and run

In the same shell you use for rollup (do not commit these lines):

export ROLLUP_LINKEDIN_LI_AT='AQE…'          # li_at value
export ROLLUP_LINKEDIN_JSESSIONID='ajax:…'  # JSESSIONID value

Dry-run first (no LinkedIn HTTP; warns if cookies are missing):

python -m rollup digest --linkedin --dry-run

Then fetch. --linkedin is required unless [linkedin].enabled = true in TOML:

python -m rollup digest --linkedin --folder linkedin:general
python -m rollup digest --linkedin --lookback-days 7
python -m rollup digest --linkedin --linkedin-refresh   # bypass listing cache
python -m rollup digest --no-linkedin          # mail only, even if TOML enables LinkedIn
python -m rollup digest --linkedin --no-linkedin-article-fetch   # posts only, no article HTTP

A successful fetch logs Fetching LinkedIn fromMember feed (N authors) via Voyager. With layout = "per_search" (as in the TOML above), posts land in linkedin:<slug> (here linkedin:general) as standalone entries. Default feed layout uses linkedin:feed instead. Link posts use the article title as the subject when Voyager exposes one, and append the fetched article body after the commentary teaser.

Mute a noisy author:

python -m rollup sources disable li:member:ACoAAA5GcN4BlMrjuK1OVX4Q63rShHLMZuQ1Qyg

For launchd/cron, ~/.config/rollup/env is loaded at CLI startup. You can also put the same two variables in the job environment (plist EnvironmentVariables). See CRON.md. If fetch fails, see TROUBLESHOOTING.md.

Reddit subreddits (opt-in network)

Public listings via a transport ladder (optional OAuth JSON → public JSON → www RSS → old.reddit RSS). No Reddit account is required. Sections land in reddit:feed or reddit:<sub> depending on layout. Listings reuse a SQLite cache within fetch_ttl_hours (default 24). Reference: CONFIG.md.

TOML

[reddit]
enabled = true
layout = "feed"          # feed | per_source
sort = "hot"

[reddit.subs.python]
enabled = true

[reddit.subs.machinelearning]
enabled = true
mode = "posts"
limit = 5

Or use Configuration Centre → Reddit (/reddit) to add subs, toggle inclusion, and set per-sub mode/sort/cap (saved in TOML). The Reddit page and Run Studio show the estimated fetch wait (70s between subs; ~1 request/minute).

CLI

Dry-run first (no Reddit HTTP):

python -m rollup digest --reddit --dry-run

Then fetch. --reddit is required unless [reddit].enabled = true in TOML:

python -m rollup digest --reddit --lookback-days 7
python -m rollup digest --reddit --folder reddit:feed
python -m rollup digest --reddit --reddit-refresh   # bypass listing cache
python -m rollup digest --no-reddit          # mail only, even if TOML enables Reddit

Mute a noisy subreddit:

rollup sources disable reddit:sub:python

Optional script-app OAuth (never TOML) goes in ~/.config/rollup/env — see CONFIG.md. Failures: TROUBLESHOOTING.md.

Google Scholar alerts

Scholar emails already in Thunderbird. Default mode keeps them as item-list mail (no extra network). Detailed mode fetches each paper and summarises it. Reference: CONFIG.md.

[scholar]
mode = "detailed"
max_papers_per_email = 8
max_fetches_per_run = 40
python -m rollup digest --scholar-mode default
python -m rollup digest --ollama --scholar-mode detailed
python -m rollup digest --scholar-mode detailed --dry-run   # parse papers, no HTTP

Or set mode and caps in Settings → Google Scholar. Registry source detail shows a banner when the source looks like Scholar.

Webpage articles (opt-in network)

HTTPS article URLs live in SQLite (webpage_queue), not TOML. Add URLs from the web Articles page (/articles), the Firefox Add to Rollup add-on, or enqueue programmatically. The next digest fetches pending rows once into folder webpage:queue and caches the body. Later digests reuse that cache and include an article when it was saved within the lookback window (same rule as email dates). Failed fetches stay failed for retry. Pass --no-webpage to skip. Reference: CONFIG.md.

Web UI

python -m rollup web --open
# Articles → add HTTPS URL → Run Studio → run digest

Firefox extension

Load extension/firefox as a temporary add-on (about:debugging#/runtime/this-firefox). Copy the capture token from Articles, paste it into add-on options, then use the toolbar or context menu Add to Rollup. The add-on POSTs to /articles/capture; rollup web must be running. Capture is ingest-only (the next digest fetches the URL).

CLI

Digest with fixture mail plus any pending queue URLs (network fetch for pending rows):

python -m rollup digest \
  --root tests/fixtures/Newsletters.sbd \
  --mail-root tests/fixtures \
  --lookback-days 3650 \
  --no-ollama

python -m rollup digest --no-webpage   # skip webpage queue even when pending rows exist
python -m rollup digest --folder webpage:queue --no-ollama   # queue only

Mute a noisy site in the source registry:

python -m rollup sources disable web:host:example.com

Doctor

python -m rollup doctor --root tests/fixtures/Newsletters.sbd
python -m rollup doctor --json --root tests/fixtures/Newsletters.sbd
python -m rollup doctor --full --root tests/fixtures/Newsletters.sbd

Inspect merged TOML + profile:

python -m rollup config print
python -m rollup --config ./rollup.toml config print --profile daily

Cron helpers (launchd preferred on macOS)

python -m rollup cron print-launchd --python "$(which python)" --workdir .
python -m rollup cron print-crontab --python "$(which python)" --workdir .
python -m rollup cron status

See docs/CRON.md for weekly non-AI digest scheduling.

Live-run workflow

Incremental checks before a full live digest:

python -m rollup inventory --root tests/fixtures/Newsletters.sbd
python -m rollup digest --root tests/fixtures/Newsletters.sbd
# Prefer sticky paths in ~/.config/rollup/config.toml, then:
python -m rollup inventory
python -m rollup digest --folder tech
python -m rollup digest --profile weekly
python -m rollup digest
python -m rollup digest --ollama --folder tech --summary-routing-report
python -m rollup digest --ollama --summary-routing-report

Migrate from hardcoded paths

# ~/.config/rollup/config.toml
root = "~/email/gmail/Newsletters.sbd"
mail_root = "~/email/gmail"

[folders.tech]
emoji = "💻"
accent = "#4a7fd4"
rollup config print
rollup doctor
rollup digest

Explicit --no-ollama is equivalent to omitting both --ollama and --no-ollama.

Optional gitignored local mail copy:

cp -R "$HOME/email/gmail/Newsletters.sbd" ./fixtures/Newsletters.sbd
python -m rollup digest --root ./fixtures/Newsletters.sbd

Ollama validation sequence

python -m rollup digest --list-summary-profiles
python -m rollup digest --list-newsletter-types
python -m rollup digest --ollama --folder tech --lookback-days 7 --summary-routing-report
python -m rollup digest --ollama --summary-profile standard --folder tech --lookback-days 7
python -m rollup digest --ollama --rebuild-summaries --folder tech --lookback-days 7
python -m rollup digest --ollama --summary-routing-report

Stop Ollama and re-run a smoke command to confirm preview fallback without crashing:

python -m rollup digest --ollama --folder tech --lookback-days 7

Final review (editorial QA)

Run a whole-digest editorial QA pass after assembly. Report-only by default: writes a JSON sidecar and does not change digest content. Advisory only.

python -m rollup digest --root ./fixtures/Newsletters.sbd --final-review
python -m rollup digest --root ./fixtures/Newsletters.sbd --final-review --final-review-profile concise
python -m rollup digest --root ./fixtures/Newsletters.sbd --final-review --final-review-report ./output/review.json
python -m rollup digest --root ./fixtures/Newsletters.sbd --final-review --no-final-review-cache

Final review does not require --ollama on the digest summarisation path (it uses Ollama independently when enabled). Apply mode (--final-review-mode apply) applies validated summary-only patches linked to safe_auto_fix: true issues; missing fingerprint echoes skip the whole set. Under --cron, apply also requires --final-review-allow-cron-apply and conservative whole-set caps. When enabled, a short QA summary also appears in the digest’s collapsed “Digest generation details” section at the end.

# Apply mode (interactive / manual)
# mail_root must contain the newsletter root (omit --mail-root to infer the .sbd parent)
rollup digest --root tests/fixtures/Newsletters.sbd --final-review \
  --final-review-mode apply \
  --output-dir /tmp/rollup-out --state-dir /tmp/rollup-state

# Group-level LLM summaries (opt-in; requires Ollama)
rollup digest --root tests/fixtures/Newsletters.sbd --ollama --group-summaries \
  --output-dir /tmp/rollup-out --state-dir /tmp/rollup-state

Source registry

Persistent per-newsletter controls (see SOURCES.md):

# After a digest has observed senders
rollup sources list --state-dir /tmp/rollup-state
rollup sources show from:alerts@github.com --state-dir /tmp/rollup-state --json

# Disable a noisy source
rollup sources disable from:noisy@example.com --state-dir /tmp/rollup-state

# Force a daily newsletter into weekly sender batches
rollup sources set list:news.example.com --grouping sender_batch --priority 80 \
  --state-dir /tmp/rollup-state

# Always surface undated notifications from a source (still within lookback)
rollup sources set from:alerts@github.com --always-surface --state-dir /tmp/rollup-state

# Override a misclassified type
rollup sources set from:editor@daily.example --type essay --state-dir /tmp/rollup-state

# Backup / restore overrides
rollup sources export --out /tmp/sources.json --state-dir /tmp/rollup-state
rollup sources import --from /tmp/sources.json --state-dir /tmp/rollup-state

Local web UI

Preferred: Docker Compose — see Docker below and DOCKER.md.

Advanced: host Python

Browse indexed rollups, rate emails, and review newsletter quality. Binds to loopback only (127.0.0.1 by default). Pass --allow-non-loopback-bind only for Docker port mapping. Requires the optional Flask extra.

Bring-up (install → digest that indexes into state → start the UI):

pip install 'rollup[web]'   # or from a checkout: pip install -e '.[web]'
python -m rollup digest --root tests/fixtures/Newsletters.sbd
python -m rollup web --open

Configuration Centre (/settings) edits the real digest TOML (paths, profiles, writers, folder themes). Run Studio (/run) previews the effective run, dry-runs discovery, and starts a digest without composing CLI by hand. Articles / LinkedIn / Reddit manage those sources. See WEB.md and CONFIG.md.

Variants:

python -m rollup web
python -m rollup web --host 127.0.0.1 --port 8765 --open
python -m rollup web --host 0.0.0.0 --port 8765 --allow-non-loopback-bind   # Docker only
python -m rollup web --config ~/.config/rollup/config.toml --open
python -m rollup web reindex --state-dir ./state --output-dir ./output

See WEB.md for security model, Settings / Run Studio / Articles / LinkedIn / Reddit, quality score, and backup notes.

Reader-body cache (same SQLite; Admin can prune/backfill too):

python -m rollup bodies stats
python -m rollup bodies check
python -m rollup bodies backfill --dry-run

Docker

Default deploy: web + digest in one container while sharing the same config, state, output, and mail paths as a host CLI. See DOCKER.md.

mkdir -p ~/.config/rollup ~/Documents/rollup-outputs ./state ./logs
touch ~/.config/rollup/config.toml ~/.config/rollup/env
chmod 600 ~/.config/rollup/env
cp docker-compose.override.yml.example docker-compose.override.yml
docker compose up -d --build
open http://localhost:8765

The image binds with --allow-non-loopback-bind so Flask can listen on 0.0.0.0 inside the container. Compose publishes 127.0.0.1:8765:8765; access from the host uses http://localhost:8765 (Host-header loopback checks unchanged). Override the port mapping only if you intentionally need LAN access.

Do not run native rollup web and Docker web at the same time.

Fixture smoke test without real mail:

docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
curl -fsS http://127.0.0.1:8765/rollups

Benchmark local models

Compare local Ollama-compatible models on fixed prompts:

python scripts/benchmark_ollama_models.py \
  --models llama3.2:3b,qwen2.5:7b,gpt-oss:20b,qwen3.6:27b \
  --runs 2 \
  --num-ctx 16384 \
  --out benchmarks/ollama_benchmark.json \
  --markdown-out benchmarks/ollama_benchmark.md

Tests and fixtures

python -m pytest tests/ -v
python tests/generate_fixtures.py