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) |
|
Git checkout |
|
Editable + tests |
|
Web UI only |
|
LiteLLM providers |
|
EPUB writer |
|
uv |
|
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¶
Log in at linkedin.com in a desktop browser.
Open Search, switch the results type to Content (not People or Jobs).
Filter by the people you want (
fromMember). The address bar must contain/search/results/content/andfromMember=withACo…ids.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.
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.
Digest with Ollama (recommended full run)¶
Requires a running local Ollama server (--ollama enables network calls to loopback by default).
Recommended full run (all folders, 7-day lookback, per-type model routing):
python -m rollup digest --ollama --summary-routing-report
--ollama alone enables type routing by default. Use --summary-routing-report to print which profiles and models were used.
LiteLLM / external providers¶
Requires pip install 'rollup[llm]' and provider credentials in the environment
(for example OPENAI_API_KEY). --ollama still enables LLM calls; choose the
transport with --llm-provider:
python -m rollup digest --ollama \
--llm-provider litellm \
--llm-model openai/gpt-4o \
--summary-routing-report
Optional OpenAI-compatible / proxy base (CLI only, never sticky):
python -m rollup digest --ollama \
--llm-provider litellm \
--llm-model openai/gpt-4o \
--llm-api-base http://127.0.0.1:4000
Do not use LiteLLM model strings that route native Ollama (ollama/…); use
--llm-provider ollama instead. Cron/launchd snippets may include
--llm-provider / --llm-model; secrets must be present in the scheduler
environment.
Inspect profiles and routes¶
python -m rollup digest --list-summary-profiles
python -m rollup digest --list-newsletter-types
Routing modes¶
Single profile for the whole digest:
python -m rollup digest --ollama --summary-profile standard
python -m rollup digest --ollama --summary-profile deep
Explicit per-type routing (same as --ollama default):
python -m rollup digest --ollama --summary-type-routing --summary-routing-report
Disable per-type routing and use the standard profile for every message:
python -m rollup digest --ollama --no-summary-type-routing --summary-routing-report
Compare multiple profiles side by side (writes one rollup set per profile):
python -m rollup digest --ollama --summary-variants rough,standard,deep --summary-routing-report
Variant mode writes files such as:
output/2026-07-02-newsletter-digest.rough.mdoutput/2026-07-02-newsletter-digest.deep.htmloutput/rollup_logo.pngoutput/favicon.ico
Smoke tests and cache control¶
python -m rollup digest --ollama --folder tech --lookback-days 7 --summary-routing-report
python -m rollup digest --ollama --rebuild-summaries --folder tech --lookback-days 7
python -m rollup digest --ollama --rebuild-summaries --summary-routing-report
Re-run without --rebuild-summaries to confirm cache hits in the stats block.
Effort presets (machine power)¶
Swap the whole summary ladder and companion defaults in one flag:
python -m rollup digest --list-efforts
python -m rollup digest --ollama --effort light --lookback-days 7
python -m rollup digest --ollama --effort high --lookback-days 7 --summary-routing-report
python -m rollup doctor --ollama --effort high --network
balanced matches today’s built-in defaults. Override models inside an effort with [efforts.high] (and friends) in TOML or Settings. Explicit --ollama-model / --final-review-model / --max-chars-for-llm still win over the preset. Do not combine --effort with --summary-profile-set.
Use one model for every profile on a single run (not sticky):
python -m rollup digest --ollama --effort balanced --single-model qwen2.5:7b --lookback-days 7
python -m rollup doctor --ollama --single-model qwen2.5:7b --network
XTEINK e-ink output¶
Write a second, device-optimized digest (short lines, no URLs) beside the normal files via the xteink output writer:
python -m rollup digest --xteink --lookback-days 7
python -m rollup digest --output xteink --lookback-days 7
python -m rollup digest --xteink --ollama --effort high --lookback-days 7
Outputs look like …-newsletter-digest.xteink.md. Skipped under --dry-run. See XTEINK_USAGE.md.
JSON, TXT, and EPUB outputs¶
By default these run automatically with every digest. To write only a subset (or Markdown/HTML alone):
python -m rollup digest --output json --output txt --lookback-days 7
python -m rollup digest --output none --lookback-days 7
# EPUB needs: pip install 'rollup[epub]'
python -m rollup digest --output epub --lookback-days 7
Same run stem as the core digest with .json / .txt / .epub extensions. See OUTPUT_WRITERS.md.
Custom profile sets¶
Export built-in profiles, edit model names to match your local Ollama library, then run:
python -m rollup digest --export-summary-profile-set ./output/summary_profiles.json
python -m rollup digest --ollama --summary-profile-set ./output/summary_profiles.json --summary-routing-report
Each profile supports Ollama generation fields:
Field |
Default |
Notes |
|---|---|---|
|
|
Max generated tokens ( |
|
|
Top-level Ollama |
|
|
Additional Ollama model options (temperature is set separately via |
Example fragment after export — adjust model names and generation settings:
{
"schema_version": 1,
"default_profile": "standard",
"profiles": {
"max": {
"provider": "ollama",
"model": "qwen3.6:27b",
"prompt_style": "deep",
"temperature": 0.2,
"num_ctx": 65536,
"timeout_seconds": 600,
"num_predict": 2048,
"think": false,
"enabled": true,
"description": "Long essays",
"options": {}
}
},
"type_routes": {
"essay": "max"
}
}
Do not put think or num_predict inside options — Ollama ignores think there on /api/generate, which causes empty summaries on thinking models. For GPT-OSS, set "think": "low" (not false) and keep enough num_predict headroom for reasoning + the visible summary.
Inspect loaded profiles:
python -m rollup digest --list-summary-profiles
Example model pulls for the built-in profile set:
ollama pull llama3.2:3b
ollama pull qwen2.5:7b
ollama pull gpt-oss:20b
ollama pull qwen3.6:27b
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