TypeScript Discord bot that answers /aufhocker queries with Claude, using recent channel history as context.
  • TypeScript 99.4%
  • JavaScript 0.4%
  • Dockerfile 0.1%
Find a file
joeyr 722e49b7c7
All checks were successful
CI / build-and-test (push) Successful in 2m33s
CI / docker-build (push) Successful in 29s
feat: debug footer names the model and goes public with the answer
The debug:True footer gains a last line with the model the API says
answered, and the effort and thinking setting the request sent:
"-# Model: claude-sonnet-5-5 · effort high · thinking on". ClaudeClient
hands these back on ClaudeResponse.settings. Thinking is reported from
the request rather than the response: Sonnet 5.5 returns its thinking
blocks empty and also uses them for progress notes between tool calls,
so a block in the response does not show that it reasoned.

Make Public now posts the footer under the answer, so the public post
matches the whisper. The footer rides on the pending entry beside the
answer rather than inside it: the saved conversation turn and the
shared public-answer context still record the answer alone, and the
bot's own channel posts were never read back into history.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 16:50:34 -05:00
.forgejo/workflows feat: run on Node 24, the current LTS 2026-10-02 18:21:49 -05:00
docker/pgadmin Fix P2-2: run migrations at startup; migrations are the only schema source 2026-07-10 21:19:27 -05:00
docs fix: Claude call time limits, MCP close after drain, guild clear, 6/6 spawn 2026-10-10 00:30:20 -05:00
src feat: debug footer names the model and goes public with the answer 2026-10-11 16:50:34 -05:00
.dockerignore Initial import of aufhocker Discord bot 2026-07-08 19:05:04 -05:00
.env.config fix: the 22 low confirmed findings from the whole-repo review 2026-10-10 12:45:13 -05:00
.env.development feat: run chat on Sonnet 5.5 and let lane detection think 2026-10-09 12:16:00 -05:00
.env.example fix: Claude call time limits, MCP close after drain, guild clear, 6/6 spawn 2026-10-10 00:30:20 -05:00
.env.production feat: run chat on Sonnet 5.5 and let lane detection think 2026-10-09 12:16:00 -05:00
.gitattributes Bring repo tooling and CI up to parity with other forgejo projects 2026-07-08 20:31:21 -05:00
.gitignore Relicense MIT, split secrets/config, pace Riot API, fix compose drift 2026-07-12 11:52:13 -05:00
.markdownlint.json chore: let the changelog repeat its section headings 2026-09-27 10:04:36 -05:00
.nvmrc feat: run on Node 24, the current LTS 2026-10-02 18:21:49 -05:00
.prettierignore Bring repo tooling and CI up to parity with other forgejo projects 2026-07-08 20:31:21 -05:00
.prettierrc.json Relicense MIT, split secrets/config, pace Riot API, fix compose drift 2026-07-12 11:52:13 -05:00
CHANGELOG.md feat: debug footer names the model and goes public with the answer 2026-10-11 16:50:34 -05:00
CLAUDE.md feat: run chat on Sonnet 5.5 and let lane detection think 2026-10-09 12:16:00 -05:00
docker-compose.production.yml fix(deploy): restore .env.config to the production compose env_file (1.4.4) 2026-07-14 09:06:54 -05:00
docker-compose.yml Relicense MIT, split secrets/config, pace Riot API, fix compose drift 2026-07-12 11:52:13 -05:00
Dockerfile feat: run on Node 24, the current LTS 2026-10-02 18:21:49 -05:00
eslint.config.js Relicense MIT, split secrets/config, pace Riot API, fix compose drift 2026-07-12 11:52:13 -05:00
jest.config.js P4-5: test-suite structure, AdminCommandHandler + connection coverage 2026-07-12 01:25:43 -05:00
LICENSE Relicense MIT, split secrets/config, pace Riot API, fix compose drift 2026-07-12 11:52:13 -05:00
package-lock.json feat: debug footer names the model and goes public with the answer 2026-10-11 16:50:34 -05:00
package.json feat: debug footer names the model and goes public with the answer 2026-10-11 16:50:34 -05:00
README.md feat: debug footer names the model and goes public with the answer 2026-10-11 16:50:34 -05:00
tsconfig.json Fix P0-2: enable noImplicitOverride in tsconfig 2026-07-10 17:51:35 -05:00
tsconfig.test.json Modernize toolchain: eslint 10, jest 30, TS 6, Node 22, zero CVEs 2026-07-08 23:00:24 -05:00
user-mappings.example.json Fix P0-5: stop tracking user-mappings.json (real Discord IDs + names) 2026-07-10 17:57:26 -05:00

Aufhocker Discord Bot

Aufhocker is a Discord bot that integrates with Claude AI, providing AI assistance directly within Discord servers using conversation history as context.

Features

  • Claude AI Integration: Interact with Claude AI through Discord slash commands
  • Private Responses by Default: All responses are private to you initially for privacy
  • Make Public Button: Share responses with everyone using the "📢 Make Public" button
  • Secure Sharing: Only you can make your responses public, with 1-hour expiry for security
  • Conversation Context: Uses server message history to provide relevant context to Claude
  • Configurable Context: Users can specify context scope (message count or time duration)
  • Follow-up Questions: Each user's conversation is remembered for an hour after their last question (per channel, shared by query-chat and espnff mcp), with prompt caching keeping follow-ups cheap
  • Shared Public Answers: Answers someone makes public become context for everyone in that channel; private answers never do
  • ESPN Fantasy Football: /aufhocker espnff mcp answers questions about your league through an MCP server's tools, and /aufhocker espnff matchup posts your matchup as a score table
  • Built-in Help: /aufhocker help, plus espnff help, lol help and d2 help
  • Configurable Message Retention: Automatically stores and prunes message history (default 30 days, via RETENTION_DAYS)
  • Admin Tools (opt-in): With ENABLE_ADMIN_TOOLS, Claude can use web_search (Brave) and web_fetch (SSRF-guarded) for admin users
  • Rate Limiting: Built-in per-user rate limiting with admin bypass capabilities
  • Admin Emergency Controls: Temporary bypasses, rate limit management, and audit trail via the /aufadmin command
  • Optional Plugins: League of Legends live-game monitor and a Diablo 2 (d2emu) Terror Zone / DClone tracker whose alerts ping only the people who signed up for them — both off by default
  • Notification Sign-up Board: Buttons that let people join or leave the D2 and LoL notification channels themselves
  • Comprehensive Logging: Detailed logging for debugging and monitoring

Quick Start

Prerequisites

  • Node.js 22.0.0 or higher
  • PostgreSQL database (or use the bundled Docker Compose stack)
  • Discord Bot Token
  • Claude API Key

Installation

  1. Clone the repository:
git clone <repository-url>
cd aufhocker
  1. Install dependencies:
npm install
  1. Set up environment variables:
cp .env.example .env
# Edit .env with your configuration
  1. Set up the database:
# Create database and run migrations
npm run db:migrate
  1. Build and start the bot:
npm run build
npm start

For development:

npm run dev

Local Development with Docker

For a quick local development setup using Docker:

  1. Start PostgreSQL and pgAdmin:
docker-compose up -d
  1. Access the services:

    • PostgreSQL: localhost:5432
    • pgAdmin: http://localhost:5050
      • Email: admin@aufhocker.local
      • Password: admin
  2. The database is automatically initialized with the required schema.

  3. To stop the services:

docker-compose down
  1. To reset the database (removes all data):
docker-compose down -v
docker-compose up -d

Docker Environment Variables

The following environment variables can be customized in your .env file for Docker:

Variable Default Description
POSTGRES_USER aufhocker PostgreSQL username
POSTGRES_PASSWORD aufhocker_dev PostgreSQL password
POSTGRES_DB aufhocker Database name
POSTGRES_PORT 5432 PostgreSQL port
PGADMIN_EMAIL admin@aufhocker.local pgAdmin login email
PGADMIN_PASSWORD admin pgAdmin login password
PGADMIN_PORT 5050 pgAdmin web interface port

Configuration

Configuration is split across two files so secrets and non-secret tuning have different lifecycles:

  • .env (git-ignored) — secrets and deployment identity only. Copy .env.example to .env and fill it in.
  • .env.config (committed) — non-secret defaults: models, intervals, feature flags, pool sizes. Edit this for behaviour changes; it contains no credentials.

Both are loaded together (docker-compose env_file: [.env.config, .env]; local runs load .env then .env.config via dotenv). Their keys are disjoint, and a value in .env wins on any overlap. .env.development / .env.production remain full single-file templates for setups that prefer one file.

Required Configuration

  • DISCORD_TOKEN: Your Discord bot token
  • DISCORD_CLIENT_ID: Your Discord application client ID
  • CLAUDE_API_KEY: Your Anthropic Claude API key
  • DATABASE_URL: PostgreSQL connection string

Optional Configuration

  • CLAUDE_MODEL: Claude model to use (default: claude-sonnet-5-5). It must accept the effort setting
  • CLAUDE_MAX_TOKENS: Maximum tokens per request, thinking included (default: 10000)
  • CLAUDE_CHAT_EFFORT: How hard query-chat and espnff mcp work: low, medium, high, xhigh or max (default: high)
  • CLAUDE_MAX_CONTEXT_TOKENS: Token budget for channel-history context sent to Claude (default: 3000)
  • CONVERSATION_MEMORY_MINUTES: How long a follow-up still continues the asker's conversation, counted from their previous question (default: 60; 0 turns memory off)
  • PROMPT_CACHE_TTL: How long Claude keeps a conversation cached for its next follow-up, 5m or 1h (default: 1h). Saving costs 1.25x normal input for 5m and 2x for 1h; every cached re-read is 0.1x
  • RETENTION_DAYS: Message and public-answer retention period in days (default: 30)
  • ENABLE_ADMIN_TOOLS: Enable Claude tool use (web_search/web_fetch) for admin users (default: false; web_search also needs BRAVE_SEARCH_API_KEY)
  • LOG_LEVEL: Logging level, one of error, warn, info, http, verbose, debug, silly, in any case (default: info). warning reads as warn; anything else logs at info, with a warning at startup

Usage

/aufhocker help lists every command the bot has turned on. Each command group has its own detail: /aufhocker espnff help, /aufhocker lol help, /aufhocker d2 help.

Ask Claude

  • /aufhocker query-chat query:<your question> - Ask Claude privately, with the last hour of channel chat as context
  • /aufhocker query-chat query:<your question> context:20 messages - Pick a different window of chat
  • /aufhocker query-chat query:<your question> new:True - Start a fresh conversation
  • /aufhocker query-chat query:<your question> debug:True - Also show, under the answer, what Claude looked up round by round, the tokens in and out, and the model, effort and thinking setting it ran with. Make Public posts this footer too

Fantasy Football

  • /aufhocker espnff mcp query:<your question> - Ask about your ESPN fantasy league. new:True and debug:True work as they do for query-chat
  • /aufhocker espnff matchup [week:<n>] - Your matchup as a score table, straight from the fantasy server (no Claude call, no rate limit)
  • /aufhocker espnff help - What the fantasy server can answer, and whether it is connected

It reads the league through the fantasy server's tools, plus any answers people made public in the channel. It never reads the channel chat.

The team's own manager (ESPNFF_OWNER_IDS) can also change the team from here: set the lineup, put in waiver claims, and reorder them. A lineup move happens straight away. A waiver claim or a new claim order does not: the answer comes with a Confirm button for each change, showing the players as ESPN has them, and nothing is sent until that person presses it. Everyone else only ever sees the read-only tools. The fantasy server needs ESPN_WRITES set to dry_run or live; in dry_run every step works and nothing reaches ESPN.

Diablo 2 Alerts

The D2 tracker pings you only for what you signed up for. Nobody signed up means no post. A Terror Zone alert deletes itself 15 minutes after that zone rotates out. A hosted walk goes a similar way: once D2R Clone Hunters strike their post through, the walk is over, and 30 minutes later the bot deletes their post and its own reply under it (this needs the bot to have Manage Messages in the D2 channel). A post they never strike goes 24 hours after the walk time it names. A DClone alert is deleted when its game moves on to the next step, and a spawn after 12 hours.

  • /aufhocker d2 tz-alerts - Pick the Terror Zones you want pings for, and when: as the zone starts, 5, 10, 15 or 20 minutes before, or any mix. A warning stays up through its zone and deletes itself 15 minutes after the zone ends
  • /aufhocker d2 dclone-alerts - Pick DClone game modes (softcore or hardcore, ladder or non-ladder), regions (USA, EU, KR) and steps (2/6 to 5/6, and the spawn); a ping needs a mode, a region and a step you picked. Pick no steps and you get no DClone pings; the status message shows every step anyway
  • /aufhocker d2 walk-alerts - The same, for hosted DClone walks announced by D2R Clone Hunters (when D2EMU_WALK_SOURCE_CHANNEL_ID is set)
  • /aufhocker d2 tz / /aufhocker d2 dclone - Check the current Terror Zone, or DClone progress in all twelve Reign of the Warlock games, privately

Each menu is private and saves as you pick. Set the D2 channel to Only @mentions so the alerts you picked are the only ones that reach you. An admin posts a sign-up board with buttons for all three menus with /aufadmin d2 signup-board, and a DClone status message with /aufadmin d2 dclone-board: all twelve games in one grid, kept up to date by the bot, never pinging anyone. Pin both.

Notification Sign-up

The D2 and LoL notification channels open to one role each. An admin posts a board with a Diablo 2 and a League of Legends button with /aufadmin roles signup-board, in a channel everyone can see. Clicking a button joins that channel; clicking it again leaves. The bot needs Manage Roles, with its role above both.

Context Parameter (Optional)

The context parameter on query-chat is optional and controls how much conversation history Claude sees:

When omitted: Defaults to the last 1 hour of messages Message count: context:20 messages or context:50 (1-100 messages) Time duration: context:2 hours or context:3 days (1-23 hours, 1-30 days) Relative dates: context:yesterday or context:today Specific dates: context:2026-01-10 (ISO format, all times in UTC)

💡 Tip: For simple questions, leave context empty to use the default (last 1 hour).

Follow-up Questions

Both query-chat and espnff mcp remember your conversation: a question asked within an hour of your previous one (CONVERSATION_MEMORY_MINUTES) is sent with the questions and answers before it, so "what about next week?" works.

  • Each person has their own conversation per channel, and both commands share it: ask about your lineup in espnff mcp, then follow up in query-chat. The first question after switching command is read at full price; follow-ups in the same command are cached again.
  • A follow-up adds only what was said in the channel since your last question. Passing context: on a follow-up pulls that window again instead.
  • new:True starts over. So does a gap longer than the memory window, or a conversation reaching 20 questions.
  • Earlier turns are re-sent from Claude's prompt cache (PROMPT_CACHE_TTL), at about a tenth of the normal input price.

Privacy & Sharing

🔒 Private by Default: All responses are initially private - only you can see them.

📢 Make Public: Use the "Make Public" button to share responses with everyone in the channel. A public answer also becomes context for everyone's next question in that channel, on both commands. Private answers stay in your own conversation.

🛡️ Security Features:

  • Only the original user can make their response public
  • The Make Public button expires after 1 hour
  • Channel history and public answers are passed to Claude as data, never as instructions

Examples

/aufhocker query-chat query:What's the main topic we've been discussing?
/aufhocker query-chat query:Can you summarize what John said? context:10 messages
/aufhocker query-chat query:What decisions did we make about the project? context:2 hours
/aufhocker query-chat query:What was discussed yesterday? context:yesterday
/aufhocker espnff mcp query:How did my team do this week?
/aufhocker espnff mcp query:And who should I pick up?

Workflow:

  1. Use /aufhocker query-chat query:<your question>
  2. Optionally add context: to specify the conversation history scope
  3. Receive a private response: your question, then Claude's answer, exactly as Make Public would post it
  4. Ask a follow-up the same way; it remembers the conversation
  5. Optionally click "📢 Make Public" to share with everyone (within an hour)

Privacy & Security

Aufhocker is designed with privacy and security in mind:

Private Responses by Default

  • All Claude responses are ephemeral (private) by default
  • Only the user who asked the question can see the initial response
  • This prevents accidental sharing of sensitive information

Controlled Public Sharing

  • Users can choose to make responses public using the "📢 Make Public" button
  • Security: Only the original user can make their own response public
  • Expiry: Public sharing capability expires after 1 hour for security
  • One-time: Each response can only be made public once

Context Privacy

  • Message history is used appropriately for context
  • The bot's own messages are excluded from context, except answers someone chose to make public
  • A private answer is only ever replayed to the person who asked for it, in their own conversation
  • Context is channel-specific (no cross-channel data leakage)
  • Conversations are deleted once they pass the memory window; public answers follow RETENTION_DAYS like chat
  • Automatic retention policy (default 30 days, configurable via RETENTION_DAYS) protects long-term privacy

Technical Security Features

  • Response IDs include user verification to prevent unauthorized sharing
  • Automatic cleanup of expired sharing tokens
  • Secure button interaction handling with user validation
  • Comprehensive error handling to prevent information leakage

Development

Scripts

  • npm run build - Build TypeScript to JavaScript
  • npm run dev - Run in development mode with hot reload
  • npm test - Run all tests
  • npm run test:watch - Run tests in watch mode
  • npm run test:coverage - Run tests with coverage report
  • npm run lint - Run ESLint
  • npm run lint:fix - Fix ESLint issues automatically

Testing

The project includes comprehensive testing:

  • Unit Tests: Test individual components in isolation
  • Property-Based Tests: Validate universal correctness properties
  • Integration Tests: Test component interactions

Run tests:

npm test

Project Structure

src/
├── admin/         # Admin domain: AdminService, RateLimiter, AdminAuditService, UserMapper
├── bot/           # Thin host (AufhockerBot): infra + plugin registry dispatch
├── claude/        # Anthropic SDK client, tool executor, SSRF guard, ToolAuditService
├── commands/      # Slash-command parsing + admin command handling
├── config/        # Configuration management
├── context/       # Channel history → Claude context assembly
├── database/      # Postgres connection, message store, migrations
├── discord/       # Discord client, command registration, routing
├── message/       # Message processing and 2000-char splitting
├── plugins/       # Plugin contract + features: query/ (core chat), lol/, d2emu/
├── types/         # Shared TypeScript type definitions
├── utils/         # Cross-cutting infra only: logger, ErrorHandler, ShutdownHandler
└── test/          # Test setup and utilities

Database Schema

The bot uses PostgreSQL. Migrations under src/database/migrations/scripts/ are the single source of truth; they are applied automatically at startup. Core tables:

  • messages — message history (pruned per RETENTION_DAYS, default 30)
  • conversation_turns — each user's private questions and answers, replayed on follow-ups (deleted once a conversation passes the memory window)
  • public_answers — answers made public, shared as channel context (pruned per RETENTION_DAYS)
  • user_mappings — Discord-ID → display-name mappings
  • admin_audit_trail — audit log of all admin actions
  • temporary_rate_limit_bypasses — active/expired admin-granted bypasses
  • schema_migrations — applied-migration bookkeeping

Plugin tables (created only by their migrations): lol_watchlist, lol_active_games, lol_monitor_config, d2emu_config, d2emu_subscriptions (who signed up for which D2 alerts, per server).

Error Handling

The bot includes comprehensive error handling for:

  • Authentication failures (Discord/Claude)
  • Rate limiting (with exponential backoff)
  • Network errors (with retry logic)
  • Database connection issues
  • Invalid command parameters

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass
  6. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Deployment

For detailed deployment instructions, see the Deployment Guide:

  • Local Development: Docker Compose setup with PostgreSQL and pgAdmin
  • Production: Docker Swarm — registry-pulled image, shared Postgres, Swarm secrets (the live deployment)
  • Alternative: single-host self-contained stack via docker-compose.production.yml
  • Database Migrations: Schema management and updates
  • Troubleshooting: Common issues and solutions

Quick links:

Admin Controls

For global administrators, Aufhocker provides emergency controls and administrative features through the separate /aufadmin command. See the Admin Controls Guide for details:

  • Separate Admin Command: /aufadmin command with Discord Administrator permission requirement
  • Dual Permission System: Requires both Discord Administrator role AND global admin user ID configuration
  • Temporary Rate Limit Bypasses: Grant temporary relief to users
  • Rate Limit Management: Clear rate limits for users or all users
  • Status Monitoring: Check rate limit status for any user
  • Audit Trail: All admin actions are logged for security and compliance

Admin Commands

Important: Admin commands require both Discord Administrator permission AND being configured as a global admin in the bot settings.

  • /aufadmin quota bypass - Grant temporary rate limit bypass to a user
  • /aufadmin quota revoke - Revoke temporary bypass for a user
  • /aufadmin quota status - Check user rate limit status
  • /aufadmin quota clear - Clear rate limits for a user or all users
  • /aufadmin reload-mappings - Re-read user-mappings.json and sync it to the database
  • /aufadmin d2 notifications-channel - Set or view the channel D2 alerts post in
  • /aufadmin d2 signup-board - Post the D2 alert sign-up board in that channel (then pin it)
  • /aufadmin d2 dclone-board - Post the DClone status message in that channel (then pin it); running it again replaces it

Visibility: Non-admin users will not see the /aufadmin command in their command list. Only users with Discord Administrator role will see it, but execution still requires global admin user ID configuration.

Quick links:

Support

For issues and questions:

  1. Check the logs in the logs/ directory
  2. Review the Deployment Guide troubleshooting section
  3. Ensure all required environment variables are set
  4. Check database connectivity

Security

  • Never commit your .env file
  • Rotate API keys regularly
  • Use environment-specific configurations
  • Monitor usage and rate limits