Initial commit: Discord TLDR - automated server summaries

This commit is contained in:
Jake Shore 2026-01-25 01:12:03 -05:00
commit 2378565a79
10 changed files with 759 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Environment
.env
*.env.local
# Python
__pycache__/
*.py[cod]
*$py.class
.Python
venv/
.venv/
*.egg-info/
dist/
build/
# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Logs
*.log
logs/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

88
README.md Normal file
View File

@ -0,0 +1,88 @@
# Discord TLDR
Automatically summarize Discord server conversations and post digests to a dedicated channel. Never miss important discussions again.
## Two Ways to Use
### 🤖 Option 1: Clawdbot Skill (Recommended if you use Clawdbot)
Zero code required. Just configure cron jobs and let Clawdbot handle the rest.
**Pros:**
- No separate bot to host
- Uses your existing Clawdbot setup
- AI summarization built-in
- Easy to customize prompts
**Setup:** See [`skill/SKILL.md`](skill/SKILL.md)
---
### 🐍 Option 2: Standalone Bot
A self-contained Python Discord bot. Deploy anywhere.
**Pros:**
- Works without Clawdbot
- Full control over deployment
- Docker-ready
- Manual trigger commands
**Setup:** See [`standalone/README.md`](standalone/README.md)
---
## How It Works
1. **Collect** - Reads messages from all accessible channels since last summary
2. **Summarize** - Uses Claude/GPT to create a concise digest
3. **Post** - Sends the summary to your #tldr channel
4. **Repeat** - Runs on schedule (default: 6 AM, 1 PM, 10 PM)
## Example Output
```
📋 **TLDR Summary** (Jan 25, 2026 - 1:00 PM)
Busy morning! Major progress on the API, some hiring discussions,
and the eternal tabs-vs-spaces debate resurfaces.
**#general**
• Welcomed @newbie to the team
• Q1 roadmap discussion - focusing on mobile first
• Friday team lunch @ 12:30
**#engineering**
• Shipped v2.1.0 to staging
• Found and fixed the auth token bug
• Code review backlog cleared
**#hiring**
• 3 new senior eng candidates in pipeline
• Technical interview scheduled for Thursday
**Action Items:**
- [ ] @alice: Finalize API docs by Wednesday
- [ ] @bob: Set up staging environment monitoring
- [ ] Everyone: Submit Q1 goals by EOW
```
## Why?
- **Async-friendly**: Not everyone can keep up with Discord in real-time
- **Context preservation**: Important decisions don't get buried
- **Onboarding**: New members can quickly catch up on team dynamics
- **Documentation**: Creates a lightweight log of team activity
## Contributing
PRs welcome! Ideas for improvement:
- Slack/Teams support
- Weekly digest mode
- Thread summarization
- Sentiment analysis
- Custom summary templates
## License
MIT - do whatever you want with it

111
skill/SKILL.md Normal file
View File

@ -0,0 +1,111 @@
# Discord TLDR Skill
Automatically summarize Discord server conversations and post digests to a dedicated #tldr channel.
## Overview
This skill configures scheduled summaries of your Discord server's activity. Perfect for:
- Teams who want async-friendly catch-up
- Servers with multiple active channels
- Anyone who doesn't want to scroll through hundreds of messages
## Requirements
- Clawdbot with Discord channel configured
- Bot must have read access to channels you want summarized
- A dedicated #tldr channel for posting summaries
## Setup
### 1. Create your #tldr channel
Create a text channel in your Discord server (e.g., `#tldr` or `#daily-digest`). Note the channel ID.
### 2. Get your Guild ID
Your Discord server's guild ID. You can get this by enabling Developer Mode in Discord, then right-clicking your server name.
### 3. Add the cron jobs
Run these commands in Clawdbot or add to your config:
```bash
# Morning summary (6 AM)
clawdbot cron add \
--name "tldr-morning" \
--schedule "0 6 * * *" \
--tz "America/New_York" \
--message "Read all recent messages from the Discord server (guild YOUR_GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id YOUR_TLDR_CHANNEL_ID)." \
--to "channel:YOUR_TLDR_CHANNEL_ID" \
--channel discord
# Afternoon summary (1 PM)
clawdbot cron add \
--name "tldr-afternoon" \
--schedule "0 13 * * *" \
--tz "America/New_York" \
--message "Read all recent messages from the Discord server (guild YOUR_GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id YOUR_TLDR_CHANNEL_ID)." \
--to "channel:YOUR_TLDR_CHANNEL_ID" \
--channel discord
# Night summary (10 PM)
clawdbot cron add \
--name "tldr-night" \
--schedule "0 22 * * *" \
--tz "America/New_York" \
--message "Read all recent messages from the Discord server (guild YOUR_GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id YOUR_TLDR_CHANNEL_ID)." \
--to "channel:YOUR_TLDR_CHANNEL_ID" \
--channel discord
```
### 4. Customize (optional)
**Change frequency:** Modify the cron expressions:
- `0 9 * * *` = 9 AM daily
- `0 */6 * * *` = every 6 hours
- `0 9 * * 1-5` = 9 AM weekdays only
**Change timezone:** Replace `America/New_York` with your timezone.
**Customize the prompt:** Edit the message to:
- Exclude certain channels
- Focus on specific topics
- Change summary format (bullets, paragraphs, etc.)
- Add action item extraction
- Tag specific roles for important updates
## Example Output
```
📋 **TLDR Summary** (Jan 25, 2026 - Morning)
**#general**
• Team discussed jacket-making skills and the legend of Nicholai's sibling
• Buba wrote an epic poem about said jacket (instant classic)
**#dev**
• Merged PR #42 for the new API endpoint
• Blocked on auth token refresh issue - @jake investigating
**#random**
• Heated debate about best pizza toppings (pineapple discourse continues)
**Action Items:**
- [ ] Jake: Fix token refresh bug
- [ ] Team: Review Q1 roadmap by Friday
```
## Troubleshooting
**Summaries not posting?**
- Check that Clawdbot has message read permissions in all channels
- Verify the tldr channel ID is correct
- Run `clawdbot cron list` to confirm jobs are enabled
**Summaries missing channels?**
- Bot needs explicit read access to private channels
- Some channels may be excluded by Discord permissions
## License
MIT - do whatever you want with it

52
skill/cron-config.json Normal file
View File

@ -0,0 +1,52 @@
{
"jobs": [
{
"name": "tldr-morning",
"schedule": {
"kind": "cron",
"expr": "0 6 * * *",
"tz": "America/New_York"
},
"sessionTarget": "isolated",
"wakeMode": "next-heartbeat",
"payload": {
"kind": "agentTurn",
"message": "Read all recent messages from the Discord server (guild GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id TLDR_CHANNEL_ID).",
"to": "channel:TLDR_CHANNEL_ID",
"channel": "discord"
}
},
{
"name": "tldr-afternoon",
"schedule": {
"kind": "cron",
"expr": "0 13 * * *",
"tz": "America/New_York"
},
"sessionTarget": "isolated",
"wakeMode": "next-heartbeat",
"payload": {
"kind": "agentTurn",
"message": "Read all recent messages from the Discord server (guild GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id TLDR_CHANNEL_ID).",
"to": "channel:TLDR_CHANNEL_ID",
"channel": "discord"
}
},
{
"name": "tldr-night",
"schedule": {
"kind": "cron",
"expr": "0 22 * * *",
"tz": "America/New_York"
},
"sessionTarget": "isolated",
"wakeMode": "next-heartbeat",
"payload": {
"kind": "agentTurn",
"message": "Read all recent messages from the Discord server (guild GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id TLDR_CHANNEL_ID).",
"to": "channel:TLDR_CHANNEL_ID",
"channel": "discord"
}
}
]
}

18
standalone/.env.example Normal file
View File

@ -0,0 +1,18 @@
# Discord Configuration
DISCORD_TOKEN=your_discord_bot_token_here
DISCORD_GUILD_ID=123456789012345678
DISCORD_TLDR_CHANNEL_ID=123456789012345678
# LLM Provider (anthropic or openai)
LLM_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-xxxxx
# OPENAI_API_KEY=sk-xxxxx
# Schedule (24h format, comma-separated)
SUMMARY_HOURS=6,13,22
# How far back to look for messages (hours)
LOOKBACK_HOURS=8
# Channels to exclude (comma-separated channel IDs)
# EXCLUDE_CHANNELS=123456789,987654321

10
standalone/Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY discord_tldr.py .
CMD ["python", "discord_tldr.py"]

126
standalone/README.md Normal file
View File

@ -0,0 +1,126 @@
# Discord TLDR Bot (Standalone)
A standalone Discord bot that automatically summarizes server conversations and posts digests to a dedicated channel.
## Features
- 📋 Automatic scheduled summaries (configurable times)
- 🤖 AI-powered summarization (Claude or GPT)
- 📊 Channel-by-channel breakdown
- ⚡ Manual trigger via `!tldr` command
- 🐳 Docker-ready for easy deployment
- 🔒 Respects channel permissions
## Quick Start
### 1. Create a Discord Bot
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
2. Create a new application
3. Go to Bot → Add Bot
4. Enable these Privileged Intents:
- Message Content Intent
- Server Members Intent (optional)
5. Copy the bot token
### 2. Invite the Bot
Generate an invite URL with these permissions:
- Read Messages/View Channels
- Send Messages
- Read Message History
```
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=68608&scope=bot
```
### 3. Configure Environment
```bash
cp .env.example .env
# Edit .env with your values
```
### 4. Run
**Direct:**
```bash
pip install -r requirements.txt
python discord_tldr.py
```
**Docker:**
```bash
docker build -t discord-tldr .
docker run --env-file .env discord-tldr
```
**Docker Compose:**
```yaml
version: '3.8'
services:
tldr:
build: .
env_file: .env
restart: unless-stopped
```
## Commands
| Command | Description |
|---------|-------------|
| `!tldr` | Generate summary now (uses default lookback) |
| `!tldr 24` | Generate summary for last 24 hours |
| `!tldr-status` | Check bot status and schedule |
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `DISCORD_TOKEN` | required | Bot token |
| `DISCORD_GUILD_ID` | required | Server ID |
| `DISCORD_TLDR_CHANNEL_ID` | required | Channel for summaries |
| `LLM_PROVIDER` | `anthropic` | `anthropic` or `openai` |
| `ANTHROPIC_API_KEY` | - | Claude API key |
| `OPENAI_API_KEY` | - | GPT API key |
| `SUMMARY_HOURS` | `6,13,22` | Hours to post (24h format) |
| `LOOKBACK_HOURS` | `8` | Hours to look back |
| `EXCLUDE_CHANNELS` | - | Channel IDs to skip |
## Example Output
```
📋 **TLDR Summary** (Jan 25, 2026 - 6:00 AM)
Quick catch-up: Active discussions about the new API launch and some
spirited debate about code review practices.
**#general**
• Team welcomed new member @sarah
• Discussion about Q1 roadmap priorities
• Decided to postpone the demo to next week
**#dev**
• Merged the authentication refactor (PR #142)
• Debugging session for the webhook timeout issue
@jake will investigate the memory leak
**#random**
• Friday lunch plans: Thai food won the vote
• Shared some quality memes about merge conflicts
**Action Items:**
- @jake: Investigate memory leak by EOD Monday
- @team: Review roadmap doc before Wednesday standup
```
## Hosting Options
- **VPS**: Any cheap VPS (DigitalOcean, Linode, Vultr)
- **Railway/Render**: Easy container deployment
- **Home server**: Run in Docker with auto-restart
- **Raspberry Pi**: Works great for small servers
## License
MIT

299
standalone/discord_tldr.py Normal file
View File

@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""
Discord TLDR Bot
Summarizes Discord server conversations and posts to a #tldr channel.
Standalone version - no Clawdbot required.
Uses Discord.py for Discord API and Anthropic/OpenAI for summarization.
"""
import os
import asyncio
import logging
from datetime import datetime, timedelta, timezone
from typing import Optional
import discord
from discord.ext import commands, tasks
# Optional: use anthropic or openai for summarization
try:
import anthropic
HAS_ANTHROPIC = True
except ImportError:
HAS_ANTHROPIC = False
try:
import openai
HAS_OPENAI = True
except ImportError:
HAS_OPENAI = False
# Configuration (override with environment variables)
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
GUILD_ID = int(os.getenv("DISCORD_GUILD_ID", "0"))
TLDR_CHANNEL_ID = int(os.getenv("DISCORD_TLDR_CHANNEL_ID", "0"))
# LLM Configuration
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "anthropic") # "anthropic" or "openai"
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
# Summary schedule (hours in 24h format, comma-separated)
SUMMARY_HOURS = [int(h) for h in os.getenv("SUMMARY_HOURS", "6,13,22").split(",")]
# Channels to exclude (comma-separated channel IDs)
EXCLUDE_CHANNELS = [int(c) for c in os.getenv("EXCLUDE_CHANNELS", "").split(",") if c]
# How far back to look for messages (hours)
LOOKBACK_HOURS = int(os.getenv("LOOKBACK_HOURS", "8"))
# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("discord-tldr")
# Bot setup
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
class MessageCollector:
"""Collects messages from Discord channels."""
def __init__(self, guild: discord.Guild, exclude_channels: list[int]):
self.guild = guild
self.exclude_channels = exclude_channels
async def collect_messages(self, since: datetime) -> dict[str, list[dict]]:
"""Collect messages from all text channels since the given time."""
messages_by_channel = {}
for channel in self.guild.text_channels:
if channel.id in self.exclude_channels:
continue
if channel.id == TLDR_CHANNEL_ID:
continue # Don't summarize the tldr channel itself
try:
messages = []
async for msg in channel.history(after=since, limit=500):
if msg.author.bot:
continue
messages.append({
"author": msg.author.display_name,
"content": msg.content,
"timestamp": msg.created_at.isoformat(),
"attachments": len(msg.attachments),
"reactions": sum(r.count for r in msg.reactions) if msg.reactions else 0
})
if messages:
messages_by_channel[channel.name] = list(reversed(messages))
logger.info(f"Collected {len(messages)} messages from #{channel.name}")
except discord.Forbidden:
logger.warning(f"No access to #{channel.name}")
except Exception as e:
logger.error(f"Error collecting from #{channel.name}: {e}")
return messages_by_channel
class Summarizer:
"""Summarizes messages using an LLM."""
def __init__(self, provider: str = "anthropic"):
self.provider = provider
if provider == "anthropic" and HAS_ANTHROPIC:
self.client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
elif provider == "openai" and HAS_OPENAI:
self.client = openai.OpenAI(api_key=OPENAI_API_KEY)
else:
self.client = None
logger.warning(f"LLM provider '{provider}' not available, using basic summarization")
def _format_messages_for_prompt(self, messages_by_channel: dict) -> str:
"""Format collected messages into a prompt-friendly string."""
parts = []
for channel, messages in messages_by_channel.items():
parts.append(f"\n## #{channel}")
for msg in messages:
parts.append(f"[{msg['author']}]: {msg['content']}")
return "\n".join(parts)
async def summarize(self, messages_by_channel: dict) -> str:
"""Generate a summary of the messages."""
if not messages_by_channel:
return "No new messages to summarize."
formatted = self._format_messages_for_prompt(messages_by_channel)
prompt = f"""Summarize the following Discord server conversations into a concise TLDR digest.
Format your response as:
1. A brief overview (1-2 sentences)
2. Bullet points organized by channel, highlighting:
- Key discussions and decisions
- Important ideas or proposals
- Action items or next steps
- Notable moments or highlights
Keep it scannable and useful for someone catching up.
---
MESSAGES:
{formatted}
---
Generate the TLDR summary:"""
if self.provider == "anthropic" and self.client:
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1500,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
elif self.provider == "openai" and self.client:
response = self.client.chat.completions.create(
model="gpt-4o",
max_tokens=1500,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
else:
# Basic fallback: just list message counts
lines = ["📋 **Activity Summary**\n"]
for channel, messages in messages_by_channel.items():
lines.append(f"• **#{channel}**: {len(messages)} messages")
lines.append("\n*Install anthropic or openai package for AI summaries*")
return "\n".join(lines)
class TLDRBot:
"""Main bot class that orchestrates collection and summarization."""
def __init__(self):
self.collector = None
self.summarizer = Summarizer(LLM_PROVIDER)
self.last_summary_time = None
async def generate_and_post_summary(self):
"""Generate a summary and post it to the TLDR channel."""
guild = bot.get_guild(GUILD_ID)
if not guild:
logger.error(f"Guild {GUILD_ID} not found")
return
tldr_channel = guild.get_channel(TLDR_CHANNEL_ID)
if not tldr_channel:
logger.error(f"TLDR channel {TLDR_CHANNEL_ID} not found")
return
# Determine lookback time
if self.last_summary_time:
since = self.last_summary_time
else:
since = datetime.now(timezone.utc) - timedelta(hours=LOOKBACK_HOURS)
# Collect messages
self.collector = MessageCollector(guild, EXCLUDE_CHANNELS)
messages = await self.collector.collect_messages(since)
if not messages:
logger.info("No new messages to summarize")
return
# Generate summary
summary = await self.summarizer.summarize(messages)
# Format and post
now = datetime.now()
header = f"📋 **TLDR Summary** ({now.strftime('%b %d, %Y - %I:%M %p')})\n\n"
# Discord has a 2000 char limit
full_message = header + summary
if len(full_message) > 2000:
# Split into multiple messages
chunks = [full_message[i:i+1990] for i in range(0, len(full_message), 1990)]
for chunk in chunks:
await tldr_channel.send(chunk)
else:
await tldr_channel.send(full_message)
self.last_summary_time = datetime.now(timezone.utc)
logger.info("Posted TLDR summary")
tldr_bot = TLDRBot()
@bot.event
async def on_ready():
logger.info(f"Logged in as {bot.user}")
check_summary_time.start()
@tasks.loop(minutes=1)
async def check_summary_time():
"""Check if it's time to post a summary."""
now = datetime.now()
if now.hour in SUMMARY_HOURS and now.minute == 0:
await tldr_bot.generate_and_post_summary()
@bot.command(name="tldr")
async def manual_tldr(ctx, hours: int = None):
"""Manually trigger a TLDR summary. Usage: !tldr [hours]"""
if hours:
tldr_bot.last_summary_time = datetime.now(timezone.utc) - timedelta(hours=hours)
await ctx.send("Generating TLDR summary...")
await tldr_bot.generate_and_post_summary()
@bot.command(name="tldr-status")
async def tldr_status(ctx):
"""Check TLDR bot status."""
last = tldr_bot.last_summary_time
last_str = last.strftime('%Y-%m-%d %H:%M UTC') if last else "Never"
schedule = ", ".join(f"{h}:00" for h in SUMMARY_HOURS)
await ctx.send(
f"**TLDR Bot Status**\n"
f"• Last summary: {last_str}\n"
f"• Schedule: {schedule}\n"
f"• Lookback: {LOOKBACK_HOURS} hours\n"
f"• LLM Provider: {LLM_PROVIDER}\n"
f"• Excluded channels: {len(EXCLUDE_CHANNELS)}"
)
def main():
if not DISCORD_TOKEN:
print("Error: DISCORD_TOKEN environment variable not set")
print("\nRequired environment variables:")
print(" DISCORD_TOKEN - Your Discord bot token")
print(" DISCORD_GUILD_ID - Your server's guild ID")
print(" DISCORD_TLDR_CHANNEL_ID - Channel ID for posting summaries")
print("\nOptional:")
print(" LLM_PROVIDER - 'anthropic' or 'openai' (default: anthropic)")
print(" ANTHROPIC_API_KEY - Anthropic API key (if using Claude)")
print(" OPENAI_API_KEY - OpenAI API key (if using GPT)")
print(" SUMMARY_HOURS - Comma-separated hours (default: 6,13,22)")
print(" LOOKBACK_HOURS - Hours to look back (default: 8)")
print(" EXCLUDE_CHANNELS - Comma-separated channel IDs to skip")
return
if not GUILD_ID or not TLDR_CHANNEL_ID:
print("Error: DISCORD_GUILD_ID and DISCORD_TLDR_CHANNEL_ID must be set")
return
bot.run(DISCORD_TOKEN)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,9 @@
# Core
discord.py>=2.3.0
# LLM providers (install at least one)
anthropic>=0.18.0
openai>=1.12.0
# Optional: for better async handling
aiohttp>=3.9.0