# Show HN: Unread – CLI that turns Telegram, YouTube and files into cited reports

> Source: <https://github.com/maxbolgarin/unread>
> Published: 2026-06-25 12:54:59+00:00

*Read your Telegram unread. Without reading it.*

A local CLI that turns Telegram chats, YouTube videos, web pages, and files into reports with citations — using whichever LLM you keep an API key for.

You have 47 unread Telegram groups. You will never read them. You will now.

```
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install unread && unread init
unread @somegroup --last-days 7
```

It pulls the chat, runs it through whichever LLM you keep an API key
for, and hands you a report with clickable citations back to
every claim. Same shape for YouTube videos, web pages, voice messages,
recorded meetings, podcasts, PDFs, and stdin. Or run it as a
self-hosted Telegram bot and forward anything weird at it — see
[Self-hosted Telegram bot](#self-hosted-telegram-bot) below.

See [a real report](/maxbolgarin/unread/blob/main/.github/examples/summary.md) from `@thehackernews`

— 99 messages over two weeks, four chunks, $0.016, every bullet linked back to its source.

Three verbs. The same `<ref>`

shape works on all of them.

`unread <ref>`

—**analyze**. Map-reduce the source into a Markdown report. Every claim links back to its message / paragraph / timestamp.`unread ask <ref> "Q"`

—**ask**. One-shot Q&A with citations. Multi-turn follow-ups are one keystroke away.`unread dump <ref>`

—**dump**. Save the original — chat history, transcript, article — verbatim. No LLM call.

`<ref>`

is any of:

```
unread @somegroup --last-days 7              # Telegram chat / channel / forum
unread "https://youtu.be/jmzoJCn8evU"        # YouTube video (captions or Whisper)
unread "https://paulgraham.com/greatwork"    # Web page / article
unread ./meeting.mp3                         # Local file (PDF, DOCX, audio, video, image)
cat notes.txt | unread                       # Stdin
```

This is the bit you actually want.

Use `unread tg`

to analyze any yours chats with interactive picker.

Source language and report language are independent. The chat can be in Russian, the report in English. Or English source → Spanish report. Or anything → anything — the LLM does the heavy lifting and the source hint is optional.

```
# Russian-language group, English summary
unread @forklog --last-days 7 --report-language en

# English channel, Russian summary
unread @thehackernews --last-days 14 --report-language ru
```

Out of the box, hand-tuned preset *structures* ship for `en`

and `ru`

under [ presets/<lang>/*.md](/maxbolgarin/unread/blob/main/presets) — section names, the forum
addendum, the report skeleton. For any other report language the LLM
writes the structure on the fly using the English preset as a template.
It works. It's not as polished as the native trees.

What you get back, for a Russian chat with `--report-language en`

:

```
## Decisions
- Migrate to indexed fund structures starting Q1 2026. [#1586](https://t.me/c/3865481227/584/1586)
- Drop the legacy K8s 1.27 cluster by end of month — Bob owns the rollout. [#1604](...)

## Open questions
- Who pays the OpenAI bill across the joint team? [#1612](...)
```

Every citation is a `t.me`

link. Click → Telegram opens that message.

`unread`

handles forums (topics), channel comments, voice notes
(transcribed), photos (described), forwarded media (deduped — Whisper
runs once across N chats), and your folder structure (`--folder Work`

batches everything in that Telegram folder). The full source-shape
matrix is in [ docs/sources.md](/maxbolgarin/unread/blob/main/docs/sources.md).

`unread <youtube-url>`

does the obvious thing: tries captions first,
falls back to Whisper if the video has none. Every citation in the
report becomes a `t=SECONDS`

deep link, so clicking jumps you to that
moment of the video.

```
unread https://www.youtube.com/watch?v=Pmd6knanPKw
unread https://www.youtube.com/watch?v=SBEtiXnLtpw --report-language de
```

from what timecode should i start watching if i want to know about RAG?

```
unread ask https://youtube.com/watch\?v\=k1njvbBmfsw "from what timecode should i start watching if i want to know about RAG?"
unread dump https://www.youtube.com/watch?v=BDqvzFY72mg --mode=transcript
```

Cached after the first run. Re-asking a question about the same video costs only the answer call — no yt-dlp, no Whisper, no re-spend.

Plain web pages (`unread <url>`

), PDFs, DOCX, Markdown, audio, video,
and images all use the same `<ref>`

syntax. See
[ docs/sources.md](/maxbolgarin/unread/blob/main/docs/sources.md) for the full list of supported
extensions and the cache rules.

That 12-minute voice message someone sent you. The 45-minute meeting recording you'll never play back. The hour-long lecture you wanted to skim. Drop any of them in and get a Markdown summary:

```
unread ./meeting.mp3                 # audio → Whisper → summary
unread ./standup.mp4                 # video → ffmpeg → Whisper → summary
unread ./voice-note.ogg              # forwarded voice message saved to disk
unread ./report.pdf                  # PDF
unread ./screenshot.png              # image (vision)
```

Inside a Telegram chat the same step runs invisibly: voice notes and
video circles are transcribed, photos are described, the analysis
treats them as text. Forward a voice across five chats — Whisper runs
once, cached by Telegram's stable `document_id`

.

Whisper is roughly $0.006 per minute of audio. A 30-minute podcast costs under twenty cents. Re-running on the same file is free when it hits the cache.

The full kind matrix and cache rules are in
[ docs/sources.md#media-enrichment](/maxbolgarin/unread/blob/main/docs/sources.md#media-enrichment).

Drop in a key for any of these. Switch at any time with
`unread settings`

— caches and analyses persist across switches.

| Provider | What you get |
|---|---|
OpenAI |
Chat models + Whisper (audio) + embeddings + vision. The full toolkit. |
Anthropic (Claude) |
Chat models only. Pair with an OpenAI key if you also want voice / image enrichment. |
Google (Gemini) |
Chat models only. Same pairing note. |
OpenRouter |
One key, dozens of chat models — handy for trying Llama / DeepSeek / Mistral without separate signups. |
Local (Ollama / LM Studio / vLLM) |
OpenAI-compatible HTTP. Zero cost, zero data leakage, your own model. |

Whisper / vision / embeddings are OpenAI-only. If you pick Anthropic or
Google as your chat provider and also want media enrichment, add an
OpenAI key alongside — `unread init`

will offer it.

```
# 1. Install. uv handles the Python venv and binary.
curl -LsSf https://astral.sh/uv/install.sh | sh         # macOS / Linux
uv tool install unread

# 2. Interactive setup. Picks install folder, AI provider, Telegram (optional).
unread init

# 3. Run something.
unread "https://paulgraham.com/greatwork.html"          # any web page
unread ./meeting.mp3                                    # any local file
unread @somegroup --last-days 7                         # last week of a chat
unread doctor                                           # confirm everything works
unread help                                           # show help
```

No virtualenv to activate, no `pip`

conflicts, no global Python
pollution. Upgrade later with `uv tool upgrade unread`

.

Skip the Telegram step if you only want YouTube / web / file analysis — those work with only an AI key.

Full install matrix (Windows / ffmpeg / dev install / editable) is in
[ docs/install.md](/maxbolgarin/unread/blob/main/docs/install.md).

Run the same pipeline as a Telegram bot. Forward it a voice message
you don't feel like listening to, a PDF you don't feel like reading, a
YouTube link, a `t.me/...`

post from a channel you're not sure you
want to subscribe to, or that suspicious link a friend just sent —
you get a Markdown summary back as a document, with cost and timing
in the caption.

Single-user by design: the bot only answers ONE Telegram ID. The
allowlist is auto-derived from the user session you give it (mounted
or sent via `/upload_session`

); `UNREAD_BOT_OWNER_ID`

is only a
bootstrap fallback for the case where no session is installed yet.
Everyone else is silently dropped.

Three install paths — pick by environment:

**1. Local laptop / dev** (`unread bot run`

foreground):

```
uv tool install unread
export UNREAD_BOT_TOKEN=123:abc...   # from @BotFather
unread bot run
```

**2. Fresh Linux VM** — one line, blank disk to running systemd service:

```
UNREAD_EXECUTABLE="install-unread-bot.sh" && curl -fsSL https://raw.githubusercontent.com/maxbolgarin/unread/main/scripts/install-bot.sh > $UNREAD_EXECUTABLE && chmod +x $UNREAD_EXECUTABLE && ./$UNREAD_EXECUTABLE && rm ./$UNREAD_EXECUTABLE
```

Installs uv + ffmpeg + libpango, runs `unread init`

, asks for the
`@BotFather`

token, drops a `systemd --user`

unit that auto-restarts
on crash and survives SSH disconnect.

**3. Docker** — pull the generic `unread`

image from GHCR (same image
can also run one-off CLI commands; `command: ["unread","bot","run"]`

in the compose file specifies the bot mode):

```
cp .env.bot.example .env.bot && $EDITOR .env.bot
docker compose -f docker-compose.bot.yml --env-file .env.bot up -d
```

Reports default to PDF; set `UNREAD_BOT_REPORT_FORMAT=md`

in `.env.bot`

to skip PDF rendering (`.md`

upload instead). The first time you
message the bot with a `t.me/...`

link it'll ask for `/upload_session`

— send your laptop's `~/.unread/storage/session.sqlite`

as a Telegram
document and it's ready.

Full feature reference (slash commands, the confirm panel, what each
input kind does) is in [ docs/bot.md](/maxbolgarin/unread/blob/main/docs/bot.md). End-to-end VM
deployment guide covering all three paths is in

[.](/maxbolgarin/unread/blob/main/docs/bot-vm-deploy.md)

`docs/bot-vm-deploy.md`

I have ~50 Telegram groups I genuinely want to follow and not enough hours to read them. The same is true for the videos I bookmark and the articles I save to "read later." LLMs are now cheap enough that analyzing a week of group chat costs less than a coffee. My time is not. So I built the CLI I wanted to use — local-first, citation-backed, provider-agnostic — and now I open Telegram a lot less.

**Local-first.** SQLite under`~/.unread/`

. Your messages, embeddings, analyses, and secrets stay on your disk. The only network calls are to Telegram, your chosen AI provider, and any URLs you point at.**Security.** Your API keys, Telegram session, and other secrets can be encrypted at rest. You can use your OS keychain (default) or passphrase to unlock the CLI.**Citations on every claim.** Reports link back to the source message / paragraph / timestamp.`--cite-context N`

expands citations into`<details>`

blocks with surrounding context, so the report is auditable without re-opening Telegram.**Two-layer cache.** Local analysis cache (re-running an unchanged chat is free) + the AI provider's prompt cache (server-side discount on repeated prefixes).`unread cache stats`

shows the hit rate.**Cost guardrails.**`--max-cost 0.50`

aborts or confirms before you spend more than that.`--dry-run`

estimates without calling the model.`unread stats`

shows lifetime spend by chat / preset / day.**PII redaction.**`--redact`

scrubs phones, emails, IBANs, and Luhn-valid card numbers from what gets sent to the LLM. Originals stay in the local DB.**Map-reduce, automatic.** Big histories get chunked, summarized in parallel, then merged. Each chunk is cached independently — adding one message to the tail re-costs only the last chunk.**Forums, channels with comments, folders.** Telegram's awkward shapes are first-class.`--all-per-topic`

,`--with-comments`

,`--folder Work`

.

Encryption modes (`keystore`

, passphrase-derived `pass`

), session
hygiene, and the threat model are documented in
[ docs/security.md](/maxbolgarin/unread/blob/main/docs/security.md).

**Does this ship my Telegram history to OpenAI?**
Only the messages in the window you asked about, only after PII
redaction if you set `--redact`

, and only to the provider whose key you
configured. Nothing else leaves the machine. The local cache is in
your install dir; you can wipe it any time with `unread cache ai purge`

.

**What if I don't use Telegram?**
Skip `unread init`

's Telegram step. `unread <url>`

and `unread <file>`

work with only an AI key. Most of the codebase is source-agnostic.

**What languages does it actually support?**
Source content: anything Whisper auto-detects (audio) or your LLM can
read (text) — pretty much everything human languages cover. Reports:
anything your LLM can write. EN and RU get hand-tuned preset
structures; other report languages use the English preset as a
template that the LLM translates on the fly.

**Will it cost me money?**
Yes — your AI provider charges. `unread`

itself is free. Per-call USD
is logged; `--max-cost N`

caps a single run. Re-running an unchanged
chat is free (local cache hit). With cheap models (`gpt-5.4-mini`

-class,
Gemini Flash, Claude Haiku) the bill is small enough that most users
stop reading the cost reports after a week.

**Is it actually fast?**
Fast enough that I stopped reading group chats. No benchmark table —
speed depends on chat size, model choice, and your network. Try it
with `unread @somegroup --last-days 1 --dry-run`

to see the estimate
before any LLM call.

**Can I run it on a server / in cron?**
Yes. Non-TTY mode skips interactive prompts. `unread watch --interval 1h ...`

loops in the foreground (run under tmux / systemd / nohup). API keys
can come from env vars or `~/.unread/.env`

. The passphrase backend
reads `UNREAD_PASSPHRASE`

for headless unlock.

**Can I plug in my own preset / prompt?**
`--preset custom --prompt-file my-prompt.md`

. Same frontmatter format
as the bundled ones in [ presets/](/maxbolgarin/unread/blob/main/presets). Bump

`prompt_version`

in the frontmatter when you edit, otherwise the cache won't notice.The full reference manual lives under `docs/`

:

| Topic | File |
|---|---|
| Install on macOS / Linux / Windows + first-run setup + where files live |
`docs/install.md` |

`docs/sources.md`

`watch`

, subscriptions`docs/reference.md`

`config.toml`

, maintenance, troubleshooting, architecture`docs/configuration.md`

`docs/security.md`

Useful inline help: `unread --help`

, `unread <subcommand> --help`

,
`unread help`

, `unread doctor`

.

PRs welcome. Read [ CLAUDE.md](/maxbolgarin/unread/blob/main/CLAUDE.md) first — it's the contributor
map (DB invariants, cache keys, preset version bumps, the three
language axes).

[covers the bench (lint / format / tests).](/maxbolgarin/unread/blob/main/CONTRIBUTING.md)

`CONTRIBUTING.md`

[for vulnerability reports.](/maxbolgarin/unread/blob/main/SECURITY.md)

`SECURITY.md`

```
uv sync --extra dev
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
```

Issues and feature requests: [https://github.com/maxbolgarin/unread/issues](https://github.com/maxbolgarin/unread/issues).

Standing on the shoulders of:
[Telethon](https://github.com/LonamiWebs/Telethon) for the Telegram side,
[OpenAI Whisper](https://platform.openai.com/docs/guides/speech-to-text) /
[Anthropic](https://www.anthropic.com/) /
[Google Gemini](https://ai.google.dev/) /
[OpenRouter](https://openrouter.ai/) /
[Ollama](https://ollama.com/) for the LLM side,
[yt-dlp](https://github.com/yt-dlp/yt-dlp) for YouTube,
[trafilatura](https://github.com/adbar/trafilatura) for article extraction,
[tiktoken](https://github.com/openai/tiktoken) for token counting,
[Typer](https://github.com/tiangolo/typer) /
[Rich](https://github.com/Textualize/rich) /
[structlog](https://www.structlog.org/) for the CLI shell,
and [uv](https://github.com/astral-sh/uv) for keeping all of the above
out of your system Python.

Apache 2.0 — see [LICENSE](/maxbolgarin/unread/blob/main/LICENSE).
