CommandRegistry yourself: client.commands is one, already wired to the client’s dispatch. The standalone helpers exist for tests, for tooling, and for anything you build on top of raw frames. For how the pieces fit together, read slash commands first.
CommandRegistry
The bot’s local command set: declaration plus handler, keyed by name.client.commands returns the client’s instance. register() is synchronous and callable before login().
register
attachment option type, a required option that follows an optional one, a bad choice list, the 50-command cap, or a serialized menu over MAX_COMMANDS_PAYLOAD. On the payload-cap throw the command is removed again, so the registry is never left over the limit.
get
undefined when it is not registered.
allowsDm
name may be invoked from a DM: true unless the declaration says dm: false. Also true for an unregistered name, so the caller’s own “unregistered” rule applies first. The client consults it on every DM invocation.
list
help response.
size
accepts
CommandLookup implementation.
parseFromText
prefix defaults to DEFAULT_COMMAND_PREFIX.
An unregistered /foo returns { kind: 'none' }, which is why running this on every inbound message is cheap.
The text lane cannot address a bot. Every bot in the channel with
/play registered will match a text /play. That is why the structured lane carries a target bot id and why structured wins.canonicalMenu
canonicalizeMenu(registry.list()).
hash
computeMenuHash(registry.list()). A session-local skip guard that never goes on the wire.
Declaration types
CommandDeclaration
string
required
Lowercase, matching
^[a-z0-9_-]{1,32}$. The server casefolds and stores the casefolded form, and the text lane lowercases before matching.string
required
1 to
MAX_COMMAND_DESC characters. Served in plaintext to every member of every server the bot is in.CommandOption[]
The command’s arguments. Declared order is load-bearing: it is the positional text grammar, and every required option must precede every optional one.
boolean
default:"true"
Whether the command may be invoked from a 1:1 DM with the bot.
false sets bit 0 (COMMAND_FLAG_NO_DM) of the canonical declaration’s flags element, which the clients’ DM picker filters on, and a DM invocation of it is dropped locally with a debug warning, never answered. Server-channel behavior is unaffected. Must be a boolean when set. See Commands in direct messages.CommandOption
string
required
Same rule as a command name, and unique within the command.
string
required
1 to
MAX_COMMAND_DESC characters. Also plaintext.CommandOptionType
required
See below.
boolean
Must be a real boolean when present. Defaults to optional.
string[]
Values, as strings. The choice string is both the label and the value, because the wire has no slot for a separate label. Up to
MAX_CHOICES_PER_OPTION entries of up to MAX_CHOICE_LEN characters. A boolean option cannot carry choices, and for integer and number options every choice must coerce.CommandOptionType
user, channel and role values are opaque id strings. The SDK does no resolution and the clients do no argument autocomplete, so read them with getString() and normalize before comparing.
There is no attachment type. Declaring one throws at register(): command invocations cannot carry a file in v1, so the option could never be filled. Sending and receiving files is its own lane, see File attachments.
COMMAND_OPTION_TYPE_CODES
The integer code each option type is published under.canonicalizeMenu() applies it for you.
Invocation types
CommandArgValue
CommandArgs
null.
CommandInvocation
string
The lowercased command name.
CommandArgs
The coerced arguments.
'structured' | 'text'
Which lane it arrived on.
Message.command, which is set only on live messages the registry recognises. It is never set on fetched history, and never on the bot’s own messages.
CommandContext
What a handler is handed.serverId, dmId, channelId, and userId say where the invocation came from and who sent it. In a server channel, serverId is set, dmId is null, and channelId is the channel, thread, or post id. In a DM, serverId is null, dmId is set, and channelId is the same dm id. userId is the invoker, normalized.
Values are already coerced to their declared type by both lanes. The getters add null safety:
getStringstringifies whatever is there, or returnsnullwhen the option was not filled.getIntegerreturnsnullfor an unfilled option, for a boolean, and for anything that is not an integer.getNumberreturnsnullfor an unfilled option, for a boolean, and for anything non-finite.getBooleanpasses a real boolean through, and otherwise acceptstrue/yes/1andfalse/no/0case-insensitively. Anything else isnull.
reply is identical to message.reply: a true reply into the invoking channel, or into the DM, which fires a “replied to you” notification that pierces mutes. Use ctx.message.channel.send(text, opts) for a plain response. Both are fire-and-forget, and a server-side denial arrives on sendRejected rather than rejecting the promise.
CommandHandler
commandError with reason: 'handler'.
CommandLookup
decodeCommandData() needs. CommandRegistry satisfies it.
TextParseResult
CommandRegistry.parseFromText() returns. 'none' covers both “not an invocation” and “an unregistered command”. 'error' is what the client turns into a commandError with reason: 'parse'.
Caps
Every cap is checked locally, so a violation surfaces at your call site instead of as a rejected frame later. The declaration caps throw atregister(); MAX_COMMAND_WIRE rejects the sendCommand() promise before the frame is built.
COMMAND_FLAG_NO_DM (1) is the one defined flag: bit 0, set by dm: false. declarationFlags(decl) returns the flags integer a declaration canonicalizes to, 0 for the default.
Command and option names are capped at 32 characters by
^[a-z0-9_-]{1,32}$, enforced by validateDeclaration(). The constant and the regex behind that rule are internal and are not exported, so do not write an import for them.DEFAULT_COMMAND_PREFIX
prefix parameter below. Override it per client with ClientOptions.commandPrefix. The structured lane carries the command name outright and is prefix-independent.
COMMAND_DATA_VERSION
encodeCommandData(). decodeCommandData() returns null for anything else.
Menu helpers
canonicalizeMenu
computeMenuHash
validateDeclaration
register() calls it for you. Call it directly when you are validating a declaration you did not write, such as a foreign bot’s declaration before passing it to sendCommand().
CanonicalOption, CanonicalCommand, CanonicalMenu
CanonicalOption is [name, typeCode, description, required, choices]. Note the element order: the integer type code is at index 1 and the description at index 2. required must be a real boolean, since the server coerces it strictly and a 1 would read as false, quietly making every required option optional.
A CanonicalCommand is [name, description, options], or [name, description, options, flags] when the declaration’s flags are non-zero. Today the only flag is COMMAND_FLAG_NO_DM (bit 0), set by dm: false. The fourth element rides only when non-zero, so a menu whose commands all allow DMs publishes the exact three-element payload it always did and its server-side hash never moves. The server accepts either length and stores the flags verbatim, up to MAX_COMMAND_FLAGS.
These are arrays rather than objects so the hash cannot shift with key order. If you build one by hand, use COMMAND_OPTION_TYPE_CODES for the type slot.
Text lane helpers
matchCommandName
null when the line is not an invocation or the name does not match the name rule. Cheap enough to run on every inbound message.
parseInvocationText
{ error }.
formatInvocationText
sendCommand() puts in the message content, and it is required, not a compatibility shim. That text is what full-text search sees, what push notifications and channel previews are built from, what a reply quotes, and what every surface without a chip renderer shows.
Arguments are emitted positionally up to the first unfilled option, and from there on in name:value form, because positional parsing cannot see a hole. A greedy trailing string is emitted bare unless quoting is needed to survive a round trip.
Unlike the parser, this one throws: on an unknown option, a missing required option, or a value that is not one of the option’s choices. Those are call-site bugs.
Sidecar codec
The structured lane’s payload. The client encodes it onsendCommand() and decodes it on inbound messages, so most bots never call these directly.
encodeCommandData
CommandData to the JSON payload that gets encrypted into the message sidecar. The bot id is canonicalized to undashed lowercase hex and the command name is lowercased.
decodeCommandData
null for bad JSON, a version other than COMMAND_DATA_VERSION, a missing or non-string bot or name, an args that is not a plain object, or an argument value that is not a string, finite number, or boolean.
registry is optional, and whether you pass it matters. A payload that is structurally valid but addressed to another bot must be dropped without falling back to text, while a merely malformed payload must fall back. Only a structural decode can tell those apart, so the client decodes without a registry first, checks the target bot, and applies the registry after. Pass registry when you do not need that distinction.
CommandData
string
The target bot’s id, the dispatch discriminator. Two bots in one channel can both declare
/play, and every bot in the channel holds the channel key, so without this both would dispatch. Compare it canonicalized, never with a raw === on the wire string.string
The lowercased command name.
CommandArgs
The arguments. Unfilled optional options are absent.
Next
Slash commands
The guide: declaring, dispatching, publishing, and the two lanes.
Client
client.commands, sendCommand(), and ClientOptions.commandPrefix.Types
Message, SendOptions, and the rest of the shapes a handler touches.Events
commandError and sendRejected.