---
description: discord.js bot conventions
globs: ["src/**/*.{ts,js}"]
alwaysApply: false
---

# Discord bot (discord.js)

## Commands
`npm run dev` · `npm run build` · `npm test` · register slash commands: `npm run deploy-commands` (guild-scoped for development, global only for releases).

## Structure
- One file per slash command in `src/commands/` exporting `data` (a `SlashCommandBuilder`) and `execute(interaction)`; one file per event in `src/events/`. A loader registers them; no giant `switch` in the entry file.
- Command registration is a separate script using the REST API, never run on every bot start.

## Rules
- Request only the gateway intents the features need (`Guilds` by default). Privileged intents (`MessageContent`, `GuildMembers`, `GuildPresences`) need a written reason in the README and must be enabled in the developer portal.
- Interactions must be acknowledged within 3 seconds: call `interaction.deferReply()` (ephemeral where appropriate) before any slow work, then `editReply`. Use `followUp` for extra messages.
- Ephemeral replies for errors and user-specific info. Never echo tokens, IDs of other users' private data or stack traces into channels.
- Check permissions explicitly: `setDefaultMemberPermissions` on admin commands plus a runtime check of `interaction.memberPermissions` and the bot's own permissions before acting.
- Validate options from `interaction.options.get*()`; treat all user text as untrusted. Disable mass mentions with `allowedMentions: { parse: [] }` when echoing user input.
- Rate limits: discord.js queues REST calls; don't loop bulk actions without batching. Use bulk endpoints where they exist.
- Wrap every `execute` in try/catch: log the error with the command name and reply (or editReply) with a friendly ephemeral message.
- Long-lived state goes in a database, not in-memory maps, so restarts and shards don't lose it.
- Token and IDs from environment variables (`DISCORD_TOKEN`, `CLIENT_ID`); `.env` is gitignored. Never commit or log the token.

## Done when
Typecheck and tests pass, commands deployed to the dev guild, and the changed command works there.
