Adding a Slash Command
This guide walks through the full process of creating a new slash command in TomoriBot.
-
Create the file in the correct path:
src/commands/{command}.tsfor root commandssrc/commands/{category}/{subcommand}.ts- or
src/commands/{category}/{group}/{subcommand}.tsfor sub-group commands
-
Export the required entry points:
configureCommand(command)for root commands, orconfigureSubcommand(subcommand)for category commands — registers the command metadata (name, description, options)execute(client, interaction, userData, locale)— the runtime handler
-
Use
localizer("en-US", ...)for command description and options so the loader can auto-register locale strings. -
Add locale keys to both locales. Command keys live in
src/locales/{locale}/commands/{category}.ts. Other key types live in the matching top-level file (general.ts,tools.ts, etc.) undersrc/locales/{locale}/.- Option descriptions:
{option_name}_description - Choice labels:
{choice_value}_option - Do NOT use
{option_name}_optionfor option descriptions — it silently fails auto-localization.
- Option descriptions:
-
Respect the 3-second interaction timing rule:
- Fast commands can call
reply()immediately. - Async/heavy commands must call
deferReply()before any await. - Modal and pagination helpers must NOT be pre-deferred — they handle acknowledgement internally.
- For full timing patterns and representative command groups, see
docs/subsystems/command-system.md.
- Fast commands can call
-
The command auto-registers on next startup unless it exports
isCommandEnabled()and that gate returnsfalse.commandLoader.tsdiscovers any.tsfile placed in the correctsrc/commands/path.
For production-only or feature-gated commands, export a gate:
export const isCommandEnabled = () => process.env.RUN_ENV === "production" && process.env.TOMORI_SUPPORTER_BILLING_ENABLED === "true";Keep production-only side effects out of module imports. The loader must be able to import the file safely even when the gate later skips registration.
Quality Gate
Section titled “Quality Gate”bun run check-locales # verify locale key paritybun run check # TypeScript strict modebun run lint # Biome formattingThen test the command end-to-end in Discord (slash shows up, options work, locale renders correctly).
Naming Conventions
Section titled “Naming Conventions”Command names should read like user-facing product language, not internal implementation names.
- Prefer explicit nouns for durable settings:
- Good:
crosschannel-blocklist - Weaker:
block-crossmsg-channels
- Good:
- Multi-word flat settings can use hyphenation when it improves clarity.
- Avoid abbreviations like
msg,cfg, orstmunless they are already established user-facing terms. - When a command edits a stored set, name it after the setting, not an action pair.
- Avoid shorthand that needs project context to decode.
When a command opens a modal showing the full saved set, name it after the setting itself (e.g. /server crosschannel-blocklist). This fits better than separate add and remove flows because the user is managing one persisted list, not issuing a one-off mutation.
Persistent Checklist Pattern
Section titled “Persistent Checklist Pattern”Use this pattern for durable settings where the best UX is “show me the whole set and let me tick what should be enabled.”
When to use it:
- The setting is naturally a set of channels, roles, notice types, or similar entries.
- Users usually think in terms of reviewing the whole configuration, not adding one item at a time.
- The saved state should be obvious on reopen.
Behavior:
- One command owns the full saved set.
- The modal opens with every currently-enabled item pre-checked.
- Submitting writes the full checked set back to the database.
Pagination rule: If the option set fits in one modal (<= 50 items), open the checkbox modal directly. If it exceeds one modal, show a page-selection message first; each page modal should preload saved state for just that page.
Status rule: If the command changes durable config, add that state to /tool status so the current configuration is visible without reopening the command.
Reference commands: /server crosschannel-blocklist, /server private-channels, /server rp-channels.
Migration Reference
Section titled “Migration Reference”Current commands that define or inspire the v2 design direction:
| Command | Pattern | Notes |
|---|---|---|
/server crosschannel-blocklist | Checklist-setting template | Single persistent command, hyphenated flat name, saved state mirrored in /tool status. Blocking a forum/media parent also blocks tool-driven visits into threads under it. |
/server private-channels | Checklist-setting for a durable channel set | Single persistent command with saved-state preload and paginated fallback. |
/server rp-channels | Checklist-setting for a durable channel set | Companion example alongside private-channels. |
Related Docs
Section titled “Related Docs”docs/subsystems/command-system.md— interaction timing rules, pagination helpers, modal patternsdocs/subsystems/localization.md— key naming conventions,localizer()API