I Built a CLI That Turns My 2am Discord Idea Dumps Into Actual Plans
How I built Donna — a local-first Python CLI that reads raw ideas from Discord, runs them through Gemini, and posts a structured briefing back. No cloud, no account, no app.
I have a problem. I get ideas constantly — walking to class, in the middle of a lab, at 2am when I should be sleeping. Most of them are half-formed garbage. Some of them are actually good. The issue is I can never tell which is which at the time, so I dump everything into a private Discord channel called #ideas.
After a few weeks I had 200+ messages in there. A wall of noise I never went back to read. Ideas were dying in a channel that was supposed to save them.
So I built Donna.
What Donna does
One command:
donna brief --dry-run
Donna briefing - 25 Apr 2026
Processed 14 messages from #ideas
Summary
Most ideas cluster around building a personal intelligence
layer. Three are actionable now.
Decisions
- Use Gemini for structuring, not Claude (cost + speed)
- Keep everything local-first, no hosted backend
Build
- Universal capture layer (voice, screenshots, telegram)
- Background daemon mode for real-time processing
- Persistent idea memory with vector search
Research
- RAG pipelines for connecting old ideas to new ones
- How Linear's API handles bulk issue creation
Tasks
- Set up cron job for daily 5am briefing
- Add email output for mobile reading
Someday
- Browser extension for one-click capture
- Team mode with per-person briefings
Discard
- "maybe telegram would be easier" (already using Discord)
- "forgot what I was going to say but" (empty thought)
Top 3
1. Build the universal capture layer
2. Add persistent idea memory with vectors
3. Ship background daemon mode
It reads raw messages from #ideas, sends them to Gemini with a structured prompt, gets back classified JSON, formats it, and posts the result to #briefings. Or prints it if you pass --dry-run.
That's it. No web UI, no database, no cloud service. Just a Python CLI that lives on your machine.
The pipeline
#ideas (Discord) → fetch → Gemini → structure → format → #briefings (Discord)
Five stages, five modules. Each one does exactly one thing.
Fetcher pulls messages from the Discord API using bot authentication. It paginates through up to 500 messages per run, handles rate limiting with automatic retry, skips bot messages and empty content, and deduplicates by message ID. It uses a watermark system — after each successful run, it saves the last processed message ID so the next run only fetches new messages.
Agent sends the raw messages to Gemini's API with a system instruction that forces JSON output:
SYSTEM_INSTRUCTION = """You are Donna, a private idea-intake assistant.
Return only valid JSON. Do not wrap the JSON in markdown.
Merge duplicates. Preserve the user's intent. Do not invent facts.
Keep bullets concise and action-oriented.
All fields are required:
summary, decisions, build, research, tasks, someday, discard, top3"""
The responseMimeType: "application/json" parameter in the Gemini request helps, but LLMs still occasionally wrap JSON in markdown fences. There's a repair step — if the first response doesn't parse, it sends the invalid output back with a "fix this" prompt. Two tries is enough.
Formatter takes the structured data and turns it into clean text with markdown headers and bullet points. Simple string assembly, nothing clever.
Dispatcher posts the formatted briefing to Discord. If the content exceeds Discord's 2000-character limit, it splits on line boundaries rather than cutting mid-sentence. It also supports email via SMTP — I have it configured but rarely use it.
The watermark trick
Donna is stateful. After each run, it saves to ~/.donna/state.json:
{
"last_successful_run": "2026-04-25T18:56:12.639288+00:00",
"last_message_id": "1497609385801682974",
"last_message_timestamp": "2026-04-25T18:51:09.338000+00:00",
"last_processed_count": 5,
"run_count": 2
}
The last_message_id is a Discord snowflake — a 64-bit integer that encodes the creation timestamp. When Donna fetches messages, it passes this as the after parameter to Discord's API, so it only gets messages newer than the watermark.
This means you can run donna brief on a cron and it'll process exactly the ideas you dumped since the last run. No duplicates, no re-processing.
If you want to override this — say, to review the last week — there's --since:
donna brief --since 7d --dry-run
The --since flag converts human-readable durations (24h, 7d, 2w) into Discord snowflakes using the epoch math:
DISCORD_EPOCH_MS = 1420070400000
def datetime_to_discord_snowflake(dt: datetime) -> str:
millis = int(dt.astimezone(UTC).timestamp() * 1000)
return str((millis - DISCORD_EPOCH_MS) << 22)
Discord snowflakes encode milliseconds since Discord's epoch (Jan 1, 2015) in the upper bits. Shift left by 22, and you get a synthetic snowflake that acts as a "messages after this time" filter.
Config lives in ~/.donna
Everything resolves from your home directory:
~/.donna/
├── config.toml # channels, model, output settings
├── .env # secrets (Discord token, Gemini key)
└── state.json # runtime watermark
This means donna brief works from any directory. No need to cd into a project folder or activate a virtualenv. You install it once, run donna setup to configure it, and it just works from wherever you are.
The setup wizard is interactive:
donna setup
Donna setup
Press Enter to keep an existing value shown in brackets.
Discord bot token: ****
Gemini API key: ****
Discord #ideas channel ID: 1497596322411184238
Discord #briefings channel ID: 1497596367462469642
Gemini model [gemini-2.5-flash]:
Max output tokens [2000]:
Post briefings to Discord [Y/n]:
Send briefings by email [y/N]:
Donna setup complete.
Next: run `donna doctor`, then `donna brief --dry-run`.
Self-diagnostics
Before you start debugging "why isn't it working", just run:
donna doctor
Donna doctor
CLI on PATH: yes (/Users/harshit/.local/bin/donna)
Config: loaded /Users/harshit/.donna/config.toml
LLM: gemini / gemini-2.5-flash
Outputs: discord
Last successful run: 2026-04-25T18:56:12+00:00
Ideas channel: accessible (#ideas, ****4238)
Briefings channel: accessible (#briefings, ****9642)
Recent raw #ideas messages checked: 10
Recent usable user messages: 10
Recent bot messages ignored: 0
Recent empty messages ignored: 0
Gemini: ok - reachable
It checks everything — is the CLI on PATH, can it load the config, can it reach both Discord channels, are there actual usable messages in #ideas, can it talk to Gemini. Each check is a real API call, not a config lint.
How I actually use it
I have a private Discord server with two channels: #ideas and #briefings. Throughout the day, I dump thoughts into #ideas from my phone — voice-to-text, screenshots, half-sentences, whatever. No formatting, no overthinking.
Every morning, either I run donna brief manually or a cron does it at 5am:
0 5 * * * donna brief >> ~/.donna/donna.log 2>&1
The briefing shows up in #briefings. I read it over coffee. Donna has already merged duplicates ("you said this three times in different ways"), discarded noise ("forgot what I was going to say but"), and surfaced the top 3 things I should actually act on.
It's like having an executive assistant who reads your brain dump and gives you a clean todo list.
The stack
Intentionally minimal:
- Python 3.11+ — stdlib only for most things
- requests — for Discord and Gemini API calls
- python-dotenv — for loading secrets from
.env - tomllib — for config parsing (built into Python 3.11)
- Gemini 2.5 Flash — fast, cheap, follows JSON schemas well
No frameworks, no ORM, no async, no dependency tree from hell. The entire codebase is ~800 lines across 10 modules. Every module is under 150 lines.
The error handling is aggressive. Custom exception hierarchy (DonnaError → ConfigError, StateError, DiscordError, AgentError, DispatchError) so failures are always traced to the right layer. State only updates after successful dispatch — if Gemini times out or Discord rejects the post, the watermark doesn't move, and the next run picks up where this one failed.
What's next
Donna v1 is a CLI that you trigger manually. The vision is much bigger:
- Universal capture — not just Discord. Voice memos, screenshots, Telegram, email, a browser extension. Zero-friction input from everywhere.
- Background daemon — instead of
donna briefonce a day, Donna runs continuously, watching all your inboxes and delivering briefings on a schedule. - Idea memory — a persistent knowledge graph that links related ideas across weeks and months. "You said something about auth middleware three weeks ago, and today's idea connects to it."
- Action pipeline — one tap from a briefing item to a GitHub issue, a Linear task, a calendar block. The gap between intent and execution, collapsed.
- Pattern intelligence — Donna learns when you're most creative, what topics you obsess over, your idea-to-action conversion rate. A mirror for how you think.
The architecture is already composable — fetcher, agent, formatter, dispatcher are independent modules. Swapping Gemini for Claude or Discord for Telegram is a config change, not a rewrite.
Try it
pip install -e .
donna setup
donna doctor
donna brief --dry-run
The whole thing is on GitHub. You'll need a Discord bot and a Gemini API key, both free.
Built because ideas deserve better than dying in a Discord channel you never scroll back to.