Rollup configuration¶
Optional TOML config, run profiles, folder themes, and path discovery.
CLI flags always win over files. The web Configuration Centre (/settings)
edits the same TOML via a shared config service (atomic write, backup, optimistic
concurrency) — there is no separate SQLite “web settings” store for digest config.
Precedence¶
Built-in defaults (
weeklyprofile,balancedeffort, deterministic folder accents)~/.config/rollup/config.toml./rollup.toml(current working directory)Selected
--profile(builtin or[profiles.*]from TOML)Explicit CLI flags
Pass --config PATH to load a single file instead of the search paths.
The Configuration Centre writes to --config when the web process was started with
it; otherwise ./rollup.toml when that file exists; otherwise
~/.config/rollup/config.toml (created on first save).
Inspect the merge:
rollup config print
rollup --config ./rollup.toml config print --profile daily
Sticky keys ↔ CLI flags¶
Sticky TOML keys (top-level or under [profiles.*]) map to digest CLI flags
through a single registry in rollup.sticky_flags. That module drives both:
applying sticky values onto argparse when the flag was not passed on the CLI (
apply_sticky_to_namespace)building Run Studio / display argv from an effective sticky view (
config_service.build_digest_argv→sticky_to_argv)
When you add a new sticky key that should appear on the CLI, extend
STICKY_FLAG_SPECS (and STICKY_KEYS in user_config) together — coverage is
asserted in tests. The sticky key profile is resolved via --profile /
EffectiveConfigView.profile_name, not the sticky→argv body.
TOML key |
CLI |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
matching |
|
matching |
|
|
|
|
|
|
[linkedin], [reddit], [scholar], [folders], [profiles.*], [efforts.*], and [ui] are
separate TOML tables, not sticky keys. [linkedin].enabled / [reddit].enabled
still inject --linkedin / --reddit when neither enable nor disable flag is on
the CLI. [scholar].mode = "detailed" (or --scholar-mode detailed) is mail
enrichment, not a new ingest source.
Scheduler helpers use a separate cron_helpers.build_scheduled_digest_argv (paths +
--cron); do not confuse it with the sticky registry.
Minimal config¶
root = "~/Library/Thunderbird/Profiles/….default/Mail/…/Newsletters.sbd"
mail_root = "~/Library/Thunderbird/Profiles/….default/Mail/…"
output_dir = "~/Documents/rollup-outputs"
lookback_days = 7
effort = "balanced"
output_dir should live outside the mail root (and preferably outside the
git checkout). Default is ~/Documents/rollup-outputs. Override with
--output-dir or sticky output_dir in config.toml.
Each digest run keeps only the current batch in the output root (dated
Markdown/HTML plus any writer artifacts). Prior batches are moved into
output_dir/archive/ automatically. latest.md / latest.html (when
--latest is used) and branding assets stay in the root.
Output writers¶
By default every discovered writer runs (xteink, txt, json, epub, …). Restrict or disable via CLI or sticky config:
output = ["json", "txt"] # subset
# output = "none" # Markdown/HTML only
# output = "all" # explicit default-all
Folder themes¶
By default folders get a stable accent color from their name (no emoji). Override per folder:
[folders.tech]
emoji = "💻"
accent = "#4a7fd4"
display_name = "Technology"
order = 10
[folders.hoops]
emoji = "🏀"
accent = "#e8923a"
order = 20
display_name replaces the raw folder name in digests; order sorts sections
(lower first), then alphabetical.
Summaries (sticky)¶
ollama = true # enable LLM calls (not Ollama-only)
ollama_model = "llama3.2" # Ollama default/fallback/group model
llm_provider = "ollama" # ollama | litellm (fallback/group path)
llm_model = "openai/gpt-4o" # LiteLLM model when llm_provider = litellm
summary_profile = "standard"
effort = "balanced"
API keys and --llm-api-base are not sticky (CLI/env only). Custom summary
profile JSON may set "provider": "litellm" per profile; that overrides the
global llm_provider for those jobs only.
--final-review is independent of --ollama: it can run whole-digest QA on a
preview-summary digest. --group-summaries still requires --ollama.
Built-in summary profiles (--list-summary-profiles) and default type routes:
Profile |
Typical model ( |
Use |
|---|---|---|
|
|
Short updates and link roundups |
|
|
Default balanced profile |
|
|
Analytical or policy-heavy items |
|
|
Long essays |
Newsletter type |
Profile |
|---|---|
|
|
|
|
|
|
|
|
|
|
Custom profile JSON, think / num_predict, and routing flags: EXAMPLES.md.
Effort model overrides¶
Built-in light / balanced / high ladders keep the same profile names and
type routes. --list-efforts prints the effective bundle. Built-in defaults:
Effort |
Rough |
Standard |
Deep |
Max |
Group / fallback |
Final review |
|
|---|---|---|---|---|---|---|---|
|
|
|
|
|
|
|
20_000 |
|
|
|
|
|
|
|
30_000 |
|
|
|
|
|
|
|
50_000 |
Override models per effort in TOML (or the Configuration Centre):
[efforts.high]
rough = "qwen2.5:14b"
standard = "gpt-oss:20b"
deep = "qwen3:32b"
max = "qwen3:32b"
ollama_model = "qwen2.5:14b"
final_review_model = "gpt-oss:20b"
Omitted keys keep the built-in default. A sticky ollama_model or CLI
--ollama-model / --final-review-model still wins for the group/fallback and
review companions. --list-efforts and doctor show the effective models after
overrides. Custom --summary-profile-set JSON is unchanged (and still cannot
combine with --effort).
Single-model override (run only)¶
--single-model NAME points every summary profile, group/fallback, and final
review at one model for this run only (not sticky). Effort still controls
max_chars_for_llm, timeouts, and routing. Useful when you want one Ollama tag
without editing the whole ladder:
rollup digest --ollama --effort balanced --single-model qwen2.5:7b
rollup doctor --ollama --single-model qwen2.5:7b --network
Run Studio exposes the same knob in Compose: a checkbox plus, for Ollama, a dropdown of local tags (filled after the page loads via POST /run/ollama-models; GET /run never contacts Ollama). LiteLLM uses a text field. Checking the box enables LLM summaries for that run. Not saved to TOML.
Explicit --ollama-model / --final-review-model still win when set.
UI preferences¶
Web-only preferences live in the same TOML under [ui] (still not SQLite):
[ui]
landing_page = "archive" # archive | run | settings
preferred_view = "html" # html | markdown | entries
onboarding_complete = false
LinkedIn content searches (optional)¶
Each saved LinkedIn content search URL is ingested like a mailbox folder
(include/exclude, themes, grouping, rendering). Fetch is opt-in network
(same idea as --ollama): off unless [linkedin].enabled = true and/or you
pass --linkedin.
TOC layout ([linkedin].layout, default feed) controls section names:
Layout |
Digest section(s) |
|---|---|
|
one |
|
one section per author ( |
|
one section per named search ( |
v1 supports faceted author lists (fromMember=… on a content-search URL).
Rollup fetches each author’s recent posts via LinkedIn’s Voyager
profileUpdatesV2 API using your logged-in session. Keyword-only content
search, company pages, follows, and mentions are not supported yet (see
ROADMAP.md).
[linkedin]
enabled = true # opt-in fetch on digest; default false
article_fetch = true # fetch linked article bodies for link posts (default on)
fetch_ttl_hours = 24 # reuse listing/article cache within this window; 0 = always fetch
layout = "feed" # feed (default) | per_source | per_search
[linkedin.searches.general]
url = "https://www.linkedin.com/search/results/content/?origin=FACETED_SEARCH&fromMember=%5B%22ACo…%22%5D"
display_name = "General"
enabled = true
[linkedin.searches.bbnj]
url = "https://www.linkedin.com/search/results/content/?origin=FACETED_SEARCH&fromMember=%5B%22ACo…%22%5D"
display_name = "BBNJ"
enabled = true
Behaviour¶
Enable:
[linkedin].enabled = trueand/orrollup digest --linkedin.--no-linkedinturns fetch off for that run.Listing cache:
fetch_ttl_hours(default24) skips Voyager when the last listing snapshot is still fresh.0always fetches (but still persists).--linkedin-refreshbypasses the cache for one run. On fetch failure, a stale snapshot is reused when available (linkedin_cache_stale).Article fetch:
[linkedin].article_fetch = trueby default — link posts also fetch the URL from VoyagerArticleComponent(external blogs, Pulse). Adds HTTP beyond Voyager; disable with[linkedin].article_fetch = falseor--no-linkedin-article-fetch. Failures leave the commentary teaser and add a parse warning (linkedin_article_fetch_failed,linkedin_article_empty, …); they do not fail the digest.URL must be
https://www.linkedin.com/search/results/content/…with afromMemberfacet (authorACo…ids). Copy it from LinkedIn after filtering Content search by people.--folder/--exclude-folderaccept the layout’s folder names (linkedin:feedby default;linkedin:<slug>whenlayout = "per_search").Posts are dated from LinkedIn activity ids; the digest lookback window still applies after ingest (older posts are skipped, not listed as undated).
linkedin:*folders stay standalone (nonotification_streamgrouping). Subject prefers the article title when present; preview keeps the full body up to 2000 characters.Caps (per search): 20 authors, 2 pages × 10 posts each, 100 posts total, 2s backoff between requests. Article fetch: 50 URLs per run, 1s backoff.
Message identity is
li:activity:…; author source key isli:member:…(mute withrollup sources disable li:member:…).Fetch failure partial (exit 2) when mail still publishes; LinkedIn-only runs hard-fail (exit 1) when fetch cannot proceed.
--dry-runand web GET routes never contact LinkedIn (dry-run warns if cookies are missing).
Configure named search URLs in the web LinkedIn page (/linkedin). Settings
only toggles fetch on/off. The LinkedIn page stores URLs and display names only.
How to copy cookies and run: EXAMPLES.md. Failures: TROUBLESHOOTING.md.
[linkedin].layout is also editable on the LinkedIn page (/linkedin). per_search is the legacy one-section-per-saved-search mode.
Reddit subreddits (optional)¶
Opt-in like LinkedIn: off unless [reddit].enabled = true and/or --reddit.
Fetch uses a transport ladder (first success wins): optional OAuth JSON (when env credentials are set) → public JSON → www RSS → old.reddit RSS. No Reddit account is required. Sorts rising and controversial map to hot on RSS/JSON listing paths. Unauthenticated feeds are often rate-limited (~1 request per minute per sub).
[reddit]
enabled = true
fetch_ttl_hours = 24 # reuse listing cache within this window; 0 = always fetch
layout = "feed" # feed | per_source
sort = "hot" # hot | new | top | rising | controversial
limit = 10
mode = "summary" # summary | posts
# time_filter = "week" # optional; for sort=top, otherwise derived from lookback
[reddit.subs.python]
enabled = true
[reddit.subs.machinelearning]
enabled = true
mode = "posts"
limit = 5
# display_name / emoji / accent / order work like [folders.*]
Optional OAuth (environment only)¶
A Reddit script app is optional. When all four variables are set, Rollup tries oauth.reddit.com JSON first (then falls through the public ladder). Never put them in TOML or Settings. They belong in ~/.config/rollup/env (or ROLLUP_ENV_FILE) next to LinkedIn cookies:
# ~/.config/rollup/env
ROLLUP_REDDIT_CLIENT_ID=…
ROLLUP_REDDIT_CLIENT_SECRET=…
ROLLUP_REDDIT_USERNAME=…
ROLLUP_REDDIT_PASSWORD=…
Behaviour¶
Enable:
[reddit].enabled = trueand/orrollup digest --reddit.--no-redditturns fetch off for that run.Listing cache:
fetch_ttl_hours(default24, max168) skips the network when the last listing snapshot is still fresh and large enough for the requested cap.0always fetches (but still persists).--reddit-refreshbypasses the cache for one run. On fetch failure, a stale snapshot is reused when available (reddit_cache_stale).Reddit page (
/reddit): add subreddit names, checkbox which subs to include, per-sub overrides for mode/sort/cap, layout, and TTL. Selections saved in TOML.layout = feed: all posts inreddit:feed; summary-mode subs becomesubreddit_digestgroups, per-post subs stay standalone.layout = per_source: one folderreddit:<sub>per enabled sub.Global defaults apply when per-sub keys are omitted.
time_filterapplies tosort = top(day/week/month/year). If omitted, Rollup derives it from--lookback-days.Caps: 10 posts/sub default (max 50), max 100 enabled subs, 70s backoff between subs (~1 request/minute). CLI logs and the Reddit / Run Studio pages show the estimated wait (cached subs are not counted). HTTP 429 adds extra wait (retry remaining subs once after a longer backoff).
Message identity
reddit:t3:<id>; source keyreddit:sub:<name>(mute withrollup sources disable reddit:sub:…).Fetch failure partial (exit 2) when mail still publishes; Reddit-only runs hard-fail (exit 1).
--dry-runand web GET never contact Reddit.
Webpage articles (optional)¶
HTTPS article URLs live in SQLite (webpage_queue), not TOML. Add URLs from the web Articles page (/articles) or the Firefox Add to Rollup add-on (POST /articles/capture). The next digest fetches pending rows once into folder webpage:queue and stores the extracted body. Later runs reuse that cache (no refetch) and include an article when it was saved within the lookback window, the same date rule as mbox messages. date_parsed is the save time (created_at). Failed fetches are retryable from the GUI. Pass --no-webpage to skip. Caps: 50 fetches per run, 1s backoff, 2MB per page. Message identity is web:url:<sha256(canonical_url)>; source key is web:host:<netloc>.
Google Scholar alerts (optional detailed mode)¶
Scholar alert emails already live in Thunderbird. Rollup detects them from
scholaralerts-noreply@google.com / List-ID scholar-alerts.google.com (or a
Scholar Alert: subject). This is mail enrichment, not a new ingest
transport — there is no Scholar API and no extra mailbox.
[scholar]
mode = "default" # default | detailed
max_papers_per_email = 8
max_fetches_per_run = 40
Mode |
Behaviour |
Network |
|---|---|---|
|
Treat as other mail. Auto-force newsletter type |
None |
|
Replace the alert card with one card per paper. Fetch each landing-page abstract (cached in |
HTTPS at digest time |
CLI --scholar-mode default|detailed wins over TOML. Settings (/settings) has the same controls. Dry-run never fetches.
detailed unwraps scholar.google.com/scholar_url?url=…, rewrites arxiv.org/pdf/… to /abs/…, and skips raw PDFs (scholar_pdf_skipped — email title/authors/snippet only). Fetch failures fall back to the email snippet (scholar_fetch_failed) and do not fail the digest. Papers stay in the Thunderbird folder section, standalone, with message_key scholar:paper:<sha256(url)> and the parent Scholar source_key. They are not added to the Articles queue.
Run profiles vs effort¶
Lever |
Controls |
|---|---|
|
Digest habits: lookback, folders, grouping, optional sticky |
|
LLM model ladder only ( |
Built-in profiles:
Name |
lookback_days |
grouping |
|---|---|---|
|
7 |
on |
|
1 |
on |
Custom profiles:
[profiles.sports]
lookback_days = 3
folder = ["hoops"]
effort = "light"
rollup digest --profile sports
rollup digest --list-profiles
Path discovery¶
If root / mail_root are not set in config or CLI:
Use
~/email/gmail/Newsletters.sbdwhen that directory exists (back-compat).Otherwise scan macOS Thunderbird profiles for a single
Newsletters.sbd.If zero or multiple candidates are found, set paths explicitly (doctor explains how).