A personal news radar that tracks storylines, learns your taste, and runs locally on your Mac.
Most news aggregators give you a flat list that resets every day. "GPT-5 released" Monday and "GPT-5 benchmarks land" Wednesday show up as two unrelated rows; the story isn't carried.
newsline builds on the same shape as Horizon (fetch from many sources, score with AI, deliver a briefing) but takes three swings Horizon doesn't:
- Storyline tracking โ items cluster into stories that grow over time, not daily disposable rows.
- Personalization that learns โ open / dwell / ๐๐ / dismiss signals get recorded for a future ranker.
- Local-first, Mac-native โ Python daemon writes a SQLite file; a SwiftUI app reads it. No server, no account, no cloud round-trip.
- ๐ก Multi-source ingest โ Hacker News, RSS / Atom (8 feeds out of the box), Reddit (subreddits via public JSON). GitHub releases & Twitter scaffolded.
- ๐ค AI scoring โ Each item rated 0-10 on a calibrated importance rubric. Pluggable provider: Anthropic, OpenAI, MiniMax (ๅฝๅ & ๅฝ้ endpoints), any OpenAI-compatible URL.
- ๐งญ Storyline clustering โ Entity extraction + entity-overlap inverted index + LLM verify. Cross-source items about the same event merge into one story (the "TanStack supply-chain attack" cluster from HN + r/programming was the first real-data validation).
- ๐ Evolving summaries โ When new events attach to a story, the summary is rewritten to fold in the new information. Single-event stories use their original title and AI summary.
- ๐ฌ Chat over a story โ Streaming chat sidecar (aiohttp on 127.0.0.1) lets you ask follow-up questions grounded in the events. Multi-turn โ the model carries the prior conversation. Reasoning-model
<think>blocks suppressed. - ๐ฏ Personal signals โ Opens, dwell time (โฅ1s), thumbs, dismiss are all written to
user_signals. Building the data set now so a ranker has something to train on later. - ๐ Full Chinese support โ One config flag (
ai.language: "zh") flips every user-facing LLM output to ็ฎไฝไธญๆ. App UI and date formatters follow your macOS language. Titles are translated while preserving repo paths, version strings, and code identifiers. - ๐ฅ Native Mac reader โ SwiftUI
NavigationSplitViewwith time-grouped storyline sidebar, score-filter slider, full-text search, tag-click filtering, source icons, read state, dismiss with undo, keyboard nav (j/k/space/Enter/โF), and a chat panel that streams tokens. - ๐ Manual or scheduled โ Tap the Fetch button in the app, or install the
com.newsline.dailyLaunchAgent for an automated daily pull. - ๐ Markdown digest โ
news digest --days 7 --top 10renders a shareable rollup of the recent high-score storylines.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Mac App (SwiftUI) โ
โ โข read sidebar / detail โข record signals โข chat UI โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SQLite (WAL) + localhost:8137 HTTP
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Python daemon โ
โ fetch โ AI score โ storyline match โ summary rewrite โ
โ โ โ
โ chat sidecar (aiohttp) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โฒ
โ HTTP
Anthropic / OpenAI / MiniMax / OpenAI-compatible
The Python pipeline owns the database. The Mac app reads it directly (RW connection, SELECT-only by convention โ WAL mode lets the two coexist). A separate newsline serve HTTP sidecar handles chat streaming so the UI can render tokens as they arrive instead of waiting on a subprocess to spawn.
git clone https://github.com/wislonl/newsline
cd newsline
uv sync # or: pip install -e .
cp .env.example .env # add your LLM API key
cp config.example.json config.json # pick provider, sources, language
# Single command โ builds the .app on first run, then opens it
news
# Other useful subcommands
news run # fetch + score + match + rewrite (manual refresh)
news stories # CLI: list active storylines
news story <id> # CLI: show one story's timeline
news chat <id> 'โฆ' # CLI: ground a question in one story
news digest --days 7 --top 10
news retranslate # re-translate after switching ai.language
news where # print local data dir
news helpApp keyboard shortcuts: j / k next/prev, space advance, Enter open original, โF search, โR fetch, โZ undo dismiss.
See config.example.json for the full schema.
Three built-in providers: anthropic, openai, minimax.
Just change the model name (same provider):
{ "ai": { "provider": "anthropic", "model": "claude-sonnet-4-5-20251022",
"api_key_env": "ANTHROPIC_API_KEY" } }Use any OpenAI-compatible API without writing code โ set provider: "openai" and a base_url. DeepSeek, Doubao, Together, Groq, Moonshot, OpenRouter, your own proxy, etc. all work:
// DeepSeek
{ "ai": { "provider": "openai", "model": "deepseek-chat",
"base_url": "https://api.deepseek.com/v1",
"api_key_env": "DEEPSEEK_API_KEY" } }
// Groq
{ "ai": { "provider": "openai", "model": "llama-3.3-70b-versatile",
"base_url": "https://api.groq.com/openai/v1",
"api_key_env": "GROQ_API_KEY" } }
// Doubao (Volcengine Ark)
{ "ai": { "provider": "openai", "model": "doubao-pro-32k",
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
"api_key_env": "DOUBAO_API_KEY" } }MiniMax has two non-interchangeable endpoints:
// ๅฝๅ
(platform.minimaxi.com)
{ "ai": { "provider": "minimax", "model": "MiniMax-M2.7-highspeed",
"base_url": "https://api.minimaxi.com/v1",
"api_key_env": "MINIMAX_API_KEY" } }
// International (api.minimax.io)
{ "ai": { "provider": "minimax", "model": "MiniMax-M2.7-highspeed",
"base_url": "https://api.minimax.io/v1",
"api_key_env": "MINIMAX_API_KEY" } }A domestic key returns 401 against the international endpoint and vice versa. Confirm which console issued your key before setting base_url.
Adding a genuinely new (non-OpenAI-compatible) provider is a ~20-line class in newsline/ai/client.py implementing complete_json, complete_text, and stream_chat, plus one branch in create_client(). The existing AnthropicClient and OpenAIClient are templates to copy.
After any provider change, drop your key into .env and restart the app (or run news run from a fresh shell so the daemon picks up the new env).
- Database:
~/Library/Application Support/newsline/newsline.db(SQLite WAL) - LaunchAgent:
~/Library/LaunchAgents/com.newsline.daily.plist(when installed) - Daemon log:
~/Library/Logs/newsline.log
Nothing leaves your machine except the outbound calls to your configured LLM provider.
newsline/
โโโ newsline/ # Python daemon
โ โโโ scrapers/ # base, rss, hackernews, reddit, github, twitter, telegram
โ โโโ ai/ # client, scorer, extractor, rewriter, chatter, translator, language
โ โโโ storyline/ # matcher
โ โโโ services/ # email, webhook
โ โโโ db.py ยท models.py ยท config.py ยท pipeline.py ยท cli.py ยท server.py ยท digest.py
โโโ apps/macos/ # SwiftPM macOS app
โ โโโ Sources/Newsline/ # NewslineApp, ContentView, Store, Signals, ChatStore,
โ # ChatService, PipelineService, Sidecar, DBWatcher, L10n
โโโ scripts/ # news, build-app.sh, install-app.sh, install-daemon.sh
โโโ tests/ # pytest smoke tests
| Status | |
|---|---|
| M0 Python daemon + SQLite + scrapers + AI scoring | โ |
| M1 Storyline engine (entity overlap + LLM verify) | โ |
| M1.5 Multi-event summary rewrite + dormant archive | โ |
| M2 Mac app shell (NavigationSplitView, 3-pane, watcher) | โ |
| M3 Behavior signals collection (open / dwell / ๐๐ / dismiss) | โ |
| M3.5 Personalized ranker on top of base score | โณ (need 1โ2 weeks of signals) |
| M4 Streaming chat over a story (multi-turn) | โ |
| i18n Full Chinese stack โ output, UI, dates, titles | โ |
| Polish Read state ยท dismiss undo ยท keyboard nav ยท search ยท tags ยท auto-scroll | โ |
| Multi-modal sources (podcasts, YouTube transcripts) | โณ |
| Reddit OAuth (replace UA-spoofing) | โณ |
| Breaking-news push notifications | โณ |
- Scrapers adapted from Horizon โ MIT, see NOTICE.
- Mac app architecture borrowed from Hermes Agent's SwiftPM-only macOS pattern.

{ "ai": { "provider": "minimax", "model": "MiniMax-M2.7-highspeed", "base_url": "https://api.minimaxi.com/v1", "api_key_env": "MINIMAX_API_KEY", "language": "zh" }, "filtering": { "time_window_hours": 168, "ai_score_threshold": 4.0, "dormant_after_days": 30, "source_bias": { "hackernews": 0.0, "reddit": 0.0 } }, "sources": { "rss": [ { "name": "Simon Willison", "url": "https://simonwillison.net/atom/everything/" }, { "name": "Anthropic News", "url": "https://www.anthropic.com/news/rss.xml" } ], "hackernews": { "enabled": true, "top_n": 100, "min_points": 50 }, "reddit": { "enabled": true, "subreddits": [ { "subreddit": "MachineLearning", "sort": "hot", "fetch_limit": 25, "min_score": 50 } ]} } }