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.
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.
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(): upload is closed to bots, so an attachment option could never be filled on the bot’s side.
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.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, 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 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].
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.