Jeeves: Discord
Run the Jeeves habitat as a Discord bot: one main persona (butler) in most channels, optional sub-agents per channel or thread. Routing is stored in discord.json under your work directory; you usually set it with slash commands instead of editing the file by hand.
For how Jeeves relates to Habitat, agents, and tools, see Jeeves Bot. This page is the Discord quickstart.
Prerequisites
- Discord application — Discord Developer Portal → New Application → Bot → Reset Token and copy it.
- Message Content Intent — Bot → Privileged Gateway Intents → turn ON Message Content Intent. Without this, the gateway closes with disallowed intents (e.g. close code 4014).
- Invite URL — OAuth2 → URL Generator: scopes
bot+applications.commands. Grant permissions to read/send messages (and Manage Channels if you will use/provisionor channel creation). - LLM API key — e.g.
GOOGLE_GENERATIVE_AI_API_KEYfor the default Google provider (seeexamples/jeeves-bot/env.example).
1. Environment and work directory
Copy the example env and set at least the token and keys:
cd examples/jeeves-bot
cp env.example .env
# Edit .env: DISCORD_BOT_TOKEN, GOOGLE_GENERATIVE_AI_API_KEY, JEEVES_WORK_DIR (optional)JEEVES_WORK_DIR— Config, prompts,config.json, optionaldiscord.json. Default:~/.jeeves.JEEVES_SESSIONS_DIR— Transcripts and media per Discord channel/thread. Default:~/.jeeves-sessions.DISCORD_GUILD_ID— Recommended for development. Registers slash commands in one guild so new commands appear quickly. Omit for global registration (Discord can take up to about an hour).DISCORD_ROUTING_PATH— Optional absolute path to routing JSON (default:<JEEVES_WORK_DIR>/discord.json).DISCORD_BACKFILL_ON_START— Default (unset): after login, the bot fetches recent history (REST) for Discord session channels that already have a folder underJEEVES_SESSIONS_DIRwithlastUsedwithin ~7 days, and replies once to user text left while it was offline (several messages in a row are combined into one model turn). The gateway does not replay missedmessageCreateevents, so this fills the gap. Limitation: a thread or DM that never had a session on disk yet (no prior bot traffic in that channel) is not scanned until the first live message createsdiscord-{id}/. Set to0to disable. Attachment-only messages while offline are not replayed in this pass.
2. Check the connection (script)
From the repository root (with dotenvx and the repo’s pnpm deps):
mise run jeeves-discord-checkOr:
dotenvx run -f examples/jeeves-bot/.env -- pnpm exec tsx examples/jeeves-bot/scripts/check-discord-setup.tsThe script verifies:
DISCORD_BOT_TOKENis set and valid (calls Discord’s@meAPI).- Work directory and
config.json(when present). - Optional
discord.json. - Provider API key for
JEEVES_PROVIDER/JEEVES_MODEL.
Exit code 1 if something critical is missing or the token is invalid.
From examples/jeeves-bot (with local pnpm install):
pnpm run check-discord3. Run the bot
Repository root (same env pattern as the check):
mise run jeeves-discordOr:
dotenvx run -f examples/jeeves-bot/.env -- pnpm run cli -- habitat discord --env-prefix JEEVESFrom examples/jeeves-bot (after pnpm install there):
dotenvx run -- pnpm run discordGeneric Habitat CLI (any work dir; --env-prefix matches your .env keys):
dotenvx run -- pnpm run cli -- habitat discord -w ~/.jeeves --env-prefix JEEVES \
-p google -m gemini-3-flash-preview --token "$DISCORD_BOT_TOKEN"Optional: --discord-guild <snowflake> or DISCORD_GUILD_ID for guild-scoped slash commands.
4. Map channels to agents
Easiest: in Discord, use slash commands (requires Manage Channels on your user for bind/unbind; the bot needs View Channel, Send Messages, and Send Messages in Threads when binding in a thread, or Discord returns 50001 Missing Access when posting the card). Manage Messages lets the bot pin the binding card.
| Command | Purpose |
|---|---|
/bind-agent | Set this channel or thread to a habitat agent id from config.json. Optional runtime: default (Jeeves / habitat model + tools) vs Claude SDK (text-only pass-through). Posts and pins a short status card. |
/set-agent-runtime | Switch default vs claude-sdk for an existing explicit binding on this channel/thread (does not apply to inherited-only routes). |
/unbind-agent | Remove mapping; thread inherits parent channel, then defaults / main. Unpins/deletes the binding card when possible. |
/start / /reset | Start fresh or clear history for this channel/thread. |
/help | Short in-app help. |
Admins only:
| Command | Purpose |
|---|---|
/reload-routing | Clears all in-memory Discord sessions (pick up discord.json changes). |
/provision | Create a new text channel for an agent (needs DISCORD_AUTO_CHANNELS=1 and bot Manage Channels). |
Routing rules:
- Exact match for the current channel or thread snowflake.
- If you are in a thread and it is not mapped, the parent text channel mapping is used.
- Otherwise main Jeeves stimulus (or
defaultAgentId/mainChannelIdif set indiscord.json).
Thread IDs are separate snowflakes: bind the thread explicitly if you want a different agent than the parent channel.
Binding card and runtimes
After /bind-agent, the bot posts a pinned message summarizing the agent, project path, and runtime. default uses the configured LLM plus habitat tools (attachments/vision where supported). claude-sdk sends plain text through runClaudeSDK on the agent’s projectPath (each message is a separate SDK run; attachments are not passed in this mode — users get a short explanation). Set ANTHROPIC_API_KEY for Claude SDK pass-through. You can also set runtime in discord.json (see examples/jeeves-bot/discord.json.example) or pass runtime to the discord_route_bind tool.
When the bot replies (Jeeves / habitat discord)
- Sub-agent bound (
/bind-agentordiscord.jsonmaps this channel/thread to a real agent inconfig.json): the bot answers every message in that channel or thread. Withdefaultruntime it also handles attachments; withclaude-sdk, only text is delegated to the SDK. - Not bound (main / unmapped / unknown agent id in the map):
- @mention is required for the first message in a DM or thread, and for every message in a parent text channel (the bot opens a thread from that message).
- Follow-ups in the same DM or thread do not need @mention after the bot has replied once, until
/resetor/reload-routing. After a bot restart, that behavior is restored fromtranscript.jsonl: if the session directory already contains an assistant line, the thread/DM stays unlocked without another @mention. - Where it replies: DM — in the DM. Existing thread — in that thread (each thread uses a stable on-disk session
discord-{threadId}). Parent text channel — creates a new thread (needs Create Public Threads + Send Messages in Threads), names it from your message, replies there, and renames the thread periodically (about every 90s max) asJeeves · ….
defaultAgentIdindiscord.jsoncounts as a dedicated agent route for unlisted channels — those channels get full auto-reply (same as a bound sub-agent).
Slash commands (/start, /help, etc.) always work when Discord shows them; this gate applies to normal messages and attachments only. /start and /reset in a Discord thread clear that thread’s session in place (stable mode).
Optional file: discord.json in the work dir (see examples/jeeves-bot/discord.json.example). You can still edit it by hand; slash commands and tools update the same file.
5. Smoke test in Discord
- Invite the bot to a server; open a channel it can read.
- Run
/help— confirms slash commands registered. - Send a short message — confirms Message Content Intent and model credentials.
- (Optional) Run
/bind-agentwith an agent id from yourconfig.json, send another message — should use that agent’s stimulus.
Sessions and media live under:
{JEEVES_SESSIONS_DIR}/discord-{channelOrThreadId}/
DMs (private messages to the bot)
The bot listens in servers and in DMs with the same code path. Your DM with the bot is a separate “channel” (its own snowflake); sessions are stored under discord-<that id>/.
/bind-agent//unbind-agentonly work in a server (they need guild context and Manage Channels). In DMs you always get the main Jeeves stimulus unless you change routing another way.- Use
/start,/reset,/helpin DMs if slash commands show up there (depends on how Discord lists the app’s commands for you).
Troubleshooting
I message the bot in DM and nothing happens
- Restart the bot after updating umwelten — the adapter enables channel partials so the first DM is not dropped when the DM channel was not cached yet (discord.js would otherwise skip
messageCreate). - Message Content Intent must be ON in the Developer Portal (Bot tab).
- Direct messages: the client requests the Direct Messages intent; no extra portal toggle beyond the usual privileged intents bundle, but the bot must not be blocked by the user.
- Confirm the process is running and the terminal shows an incoming line like
[hh:mm:ss] ← [<channelId>] @you: ...when you send a message. If guild messages log but DMs do not, it was almost certainly the partials/cache issue (fixed in current code).
Slash commands missing
- Set
DISCORD_GUILD_ID(or--discord-guild) while testing so commands register to one server quickly. Global commands can take up to about an hour.
Bot answers in the server but errors or is empty
- Check provider API keys and model id in
.env. See terminal stderr for stack traces.
Claude SDK pass-through returns errors or empty
- Ensure
ANTHROPIC_API_KEYis set in the environment that runs the bot. Pass-through does not use the Google/OpenRouter key from Jeeves’ default model.
See also
- Jeeves Bot — full Jeeves overview, Telegram, CLI, agents.
- Habitat — work directory, tools, sessions.
- examples/jeeves-bot README — env reference and feature list.