Mouth-plus-plus — Discord OpenCode Voice Bot
Discord bot that bridges voice and text channels with OpenCode AI sessions. Mention the bot in a monitored channel to create a thread-bound OpenCode session, then use /join to add voice interaction.
Quick Start
# Install dependencies (Node.js/pnpm required)
pnpm install
# Copy environment template
cp .env.example .env
# Edit .env with: DISCORD_BOT_TOKEN, DISCORD_CLIENT_ID, DISCORD_MONITORED_CHANNEL_ID, DISCORD_GUILD_ID, OPENCODE_API_KEY
# Build TypeScript
pnpm build
# Register slash commands (/join, /leave) with Discord
pnpm register:commands
# Start in dev mode (watch)
pnpm dev
# Production start
pnpm start
Commands
| Command | Description |
|---|---|
pnpm build | Compile TypeScript to dist/ |
pnpm typecheck | Type check without emitting |
pnpm dev | Watch mode with hot reload |
pnpm start | Run compiled dist/app.js |
pnpm register:commands | Register Discord slash commands |
Architecture Overview
Runtime: Node.js (>=22), pnpm (>=9)
Entry Point: src/app.ts → creates Discord client, initializes SQLite, registers event handlers
Build Output: dist/ (ES2022, NodeNext modules)
Key Directories
src/
├── app.ts # Bootstrap: client, db, services, event handlers
├── config.ts # Environment validation (zod), Discord intents/permissions
├── types.ts # Shared TypeScript types
├── register-commands.ts # CLI to register /join and /leave with Discord API
├── bot/
│ ├── client.ts # Discord client factory, event handler wiring
│ ├── commands/ # /join, /leave slash command handlers
│ └── events/ # ready, messageCreate, interactionCreate handlers
├── discord/
│ ├── sessionThreads.ts # Thread creation and session binding logic
│ ├── threadGuards.ts # Validation: managed thread context checks
│ ├── partRenderer.ts # Convert OpenCode SDK parts → Discord messages
│ ├── messageSplitter.ts # Chunk messages to <2000 chars, preserve code fences
│ └── replies.ts # Discord message sending utilities
├── opencode/
│ ├── sdk.ts # OpenCode SDK context wrapper
│ ├── sessions.ts # Session lifecycle management
│ ├── parts.ts # Part type normalization (text, reasoning, tool, error, unknown)
│ └── events.ts # SDK event handling
├── pipeline/
│ ├── enqueue.ts # Per-thread task queue (serializes all work per thread)
│ ├── handleThreadMessage.ts# Process text messages through OpenCode session
│ ├── handleAssistantParts.ts # Render assistant output parts to Discord
│ └── handleVoiceSegment.ts # Process voice transcript through same session pipeline
├── voice/
│ ├── runtime.ts # In-memory voice runtime registry (one per guild)
│ ├── joinLeave.ts # /join, /leave command logic, validation
│ ├── receiver.ts # Discord voice audio receiver
│ ├── segmenter.ts # Speaker buffer tracking, utterance segmentation
│ ├── normalizer.ts # Convert to mono fixed-rate WAV
│ └── transport.ts # Voice connection abstraction
├── asr/
│ ├── client.ts # ASR HTTP client
│ └── transcribe.ts # Transcription with retry/timeout/empty handling
├── storage/
│ ├── db.ts # SQLite wrapper (better-sqlite3), WAL mode, thread_sessions table
│ └── threadSessionRepo.ts # thread_id ↔ session_id persistence
└── utils/
├── logger.ts # Pino logger, structured JSON logging
├── errors.ts # Custom error types (RuntimeError, ConfigValidationError)
└── time.ts # Time utilities
Critical Implementation Rules
These constraints are hard requirements. Violations break core functionality.
OpenCode SDK Usage
- Always use the official
@opencode-ai/sdk— never raw HTTP - Always wrap input in
parts— never send raw strings tosession.prompt() - Always process every assistant output part type:
text,reasoning,tool,error,unknown - Never ignore any part type — all must produce Discord-visible output
Thread Safety
- All work for a single thread must be serialized through
pipeline/enqueue.tsqueue - Voice transcripts and text messages for the same thread must never execute concurrently
- Queue errors must not crash the bot — log and continue
Code Formatting
- Always run
pnpm formatbefore completing any task that modifies source files - Never submit unformatted code — Prettier is a hard requirement for this codebase
- Format the entire project with
pnpm format:allwhen requested
Discord Message Constraints
- Never exceed 2000 characters per Discord message
- Use
discord/messageSplitter.tswhich: splits by paragraph → newline → whitespace → hard split - Preserve code fences — never split inside triple backticks
- Send parts sequentially — never merge different assistant parts into one message
Voice Runtime
- One active voice runtime per guild — enforced in
voice/runtime.tsregistry /joinvalidates: managed thread context, user in voice channel, no existing runtime- Voice receiver only starts after successful runtime creation
Message Handling
- Never re-consume bot-authored messages — check
message.author.botearly in handlers - Main channel mentions create threads; thread messages reuse stored session
Database
- SQLite file at
.data/thread-sessions.sqlite(created automatically) - Uses WAL mode (
PRAGMA journal_mode = WAL) - Single table:
thread_sessions(thread_id PRIMARY KEY, session_id)
Environment Variables
Required (bot fails fast if missing):
DISCORD_BOT_TOKEN— Discord bot tokenDISCORD_CLIENT_ID— Discord application/client IDDISCORD_MONITORED_CHANNEL_ID— Main text channel to monitor for mentionsDISCORD_GUILD_ID— Test guild ID for command registration
Required for AI features:
OPENCODE_API_KEY— OpenCode API key
Optional:
OPENCODE_BASE_URL— defaults tohttps://api.opencode.aiASR_API_KEY,ASR_BASE_URL,ASR_MODEL— ASR provider configLOG_LEVEL—fatal|error|warn|info|debug|trace|silent(default:info)NODE_ENV—development|test|production
OpenCode Configuration
This repo uses the oh-my-openagent plugin. Agent definitions are in .opencode/oh-my-openagent.json. Key agents:
hephaestus— Primary implementation agent (GPT-5.4, edit+test permissions)sisyphus— Orchestrator (Kimi K2.5)oracle— Architecture/debugging consultant (GPT-5.4 high)explore,librarian— Background search agents
Testing & Validation
# Type check
pnpm typecheck
# Build verification
pnpm build
# Manual test flow:
# 1. Mention bot in monitored channel → thread created, session started
# 2. Reply in thread → same session used
# 3. /join from thread → bot joins your voice channel
# 4. Speak → transcript sent to session, response in thread
# 5. /leave → bot disconnects
Common Issues
Build fails: Ensure Node.js >=22 and pnpm >=9 are installed (node -v, pnpm -v)
Commands not appearing: Run pnpm register:commands after changing command definitions
Voice not connecting: Check bot has Connect and Speak permissions in voice channel
SQLite errors: Ensure .data/ directory is writable
Dependencies of Note
@opencode-ai/sdk— AI session management (MUST use for all OpenCode interactions)discord.js+@discordjs/voice— Discord bot and voice handlingbetter-sqlite3— SQLite3 library for Node.js (no external dep)pino— Structured loggingzod— Runtime config validationopusscript— Opus codec for voice (lightweight, no native deps)