CommandKit uses a file-based routing system for commands, where each file in your src/app/commands/ directory automatically becomes a command. This approach provides a clean, organized structure for your Discord bot’s commands.
Command file structure
Commands are defined by exporting specific functions and objects from TypeScript files in your commands directory:
import type { CommandData , ChatInputCommand } from 'commandkit' ;
export const command : CommandData = {
name: 'ping' ,
description: 'Pong!' ,
};
export const chatInput : ChatInputCommand = async ( ctx ) => {
await ctx . interaction . reply ( 'Pong!' );
};
Command exports
Each command file can export the following:
The command definition containing the name, description, and options. This export is required for all commands. export const command : CommandData = {
name: 'avatar' ,
description: 'Get a user \' s avatar' ,
options: [
{
name: 'user' ,
description: 'The user to get the avatar for' ,
type: ApplicationCommandOptionType . User ,
},
],
};
Handler for slash command interactions. Receives a context object with the interaction, client, and command data. export const chatInput : ChatInputCommand = async ( ctx ) => {
const user = ctx . options . getUser ( 'user' ) ?? ctx . interaction . user ;
await ctx . interaction . reply ( `Avatar: ${ user . displayAvatarURL () } ` );
};
Handler for prefix-based message commands. Allows your bot to respond to traditional text commands. export const message : MessageCommand = async ( ctx ) => {
await ctx . message . reply ( 'Pong!' );
};
Handler for user context menu commands (right-click on a user). export const userContextMenu : UserContextMenuCommand = async ( ctx ) => {
const target = ctx . interaction . targetUser ;
await ctx . interaction . reply ( `Selected user: ${ target . username } ` );
};
Handler for message context menu commands (right-click on a message). export const messageContextMenu : MessageContextMenuCommand = async ( ctx ) => {
const target = ctx . interaction . targetMessage ;
await ctx . interaction . reply ( `Message author: ${ target . author . username } ` );
};
Handler for autocomplete interactions. Provides dynamic option suggestions as users type. export async function autocomplete ( ctx : AutocompleteCommandContext ) {
const arg = ctx . interaction . options . getString ( 'test' , false );
const choices = [
{ name: 'Option 1' , value: 'opt1' },
{ name: 'Option 2' , value: 'opt2' },
];
await ctx . interaction . respond ( choices );
}
Additional command configuration for permissions, guilds, and aliases. export const metadata : CommandMetadata = {
userPermissions: 'Administrator' ,
botPermissions: [ 'KickMembers' , 'BanMembers' ],
guilds: [ '1234567890' ],
aliases: [ 'p' , 'pong' ],
};
Function to dynamically generate command metadata at runtime. export const generateMetadata : CommandMetadataFunction = async () => {
return {
userPermissions: 'Administrator' ,
guilds: await fetchGuildIds (),
};
};
Command types
Slash commands
Slash commands are Discord’s modern command interface. They provide autocomplete, validation, and a better user experience:
import type { CommandData , ChatInputCommand } from 'commandkit' ;
import { ApplicationCommandOptionType } from 'discord.js' ;
export const command : CommandData = {
name: 'greet' ,
description: 'Greet a user' ,
options: [
{
name: 'user' ,
description: 'User to greet' ,
type: ApplicationCommandOptionType . User ,
required: true ,
},
],
};
export const chatInput : ChatInputCommand = async ( ctx ) => {
const user = ctx . options . getUser ( 'user' , true );
await ctx . interaction . reply ( `Hello, ${ user } !` );
};
Prefix commands
Prefix commands allow users to trigger commands with a text prefix (e.g., !ping):
import type { MessageCommand } from 'commandkit' ;
export const message : MessageCommand = async ( ctx ) => {
const args = ctx . args ();
await ctx . message . reply ( `You passed ${ args . length } arguments` );
};
Prefix commands require configuration in your bot’s main file to set the command prefix.
Context menu commands
Context menus appear when right-clicking users or messages in Discord:
import type {
CommandData ,
UserContextMenuCommand ,
MessageContextMenuCommand ,
CommandMetadata ,
} from 'commandkit' ;
export const command : CommandData = {
name: 'avatar' ,
description: 'Get avatar' ,
};
export const metadata : CommandMetadata = {
nameAliases: {
user: 'View Avatar' ,
message: "View Author's Avatar" ,
},
};
export const userContextMenu : UserContextMenuCommand = async ( ctx ) => {
const target = ctx . interaction . targetUser ;
await ctx . interaction . reply ({
embeds: [{
title: ` ${ target . username } 's Avatar` ,
image: { url: target . displayAvatarURL ({ size: 2048 }) },
}],
});
};
export const messageContextMenu : MessageContextMenuCommand = async ( ctx ) => {
const target = ctx . interaction . targetMessage . author ;
await ctx . interaction . reply ({
embeds: [{
title: ` ${ target . username } 's Avatar` ,
image: { url: target . displayAvatarURL ({ size: 2048 }) },
}],
});
};
Directory-based grouping
Organize commands into directories using parentheses for grouping without affecting command names:
src/app/commands/
├── (general)/
│ ├── ping.ts → /ping
│ ├── help.ts → /help
│ └── (animal)/
│ ├── cat.ts → /cat
│ └── dog.ts → /dog
└── (admin)/
└── ban.ts → /ban
Directory names wrapped in parentheses (like-this) are ignored in the command path. This allows you to organize commands without affecting their names.
Command context
All command handlers receive a context object with useful properties and methods:
Context properties
Context methods
Shared store
export const chatInput : ChatInputCommand = async ( ctx ) => {
// Discord.js objects
ctx . interaction // The interaction object
ctx . client // The Discord.js client
ctx . guild // The guild (null in DMs)
ctx . channel // The channel
// Command info
ctx . commandName // The command name
ctx . invokedCommandName // The invoked name (could be alias)
ctx . command // The loaded command object
// Utilities
ctx . options // Command options
ctx . store // Shared key-value store
ctx . commandkit // The CommandKit instance
};
export const chatInput : ChatInputCommand = async ( ctx ) => {
// Type guards
ctx . isInteraction () // Check if interaction-based
ctx . isChatInputCommand () // Check if slash command
ctx . isMessage () // Check if message command
// Locale
ctx . getLocale () // Get user/guild locale
ctx . getUserLocale () // Get user's locale
ctx . getGuildLocale () // Get guild's locale
// Forwarding
await ctx . forwardCommand ( 'other-command' );
// For message commands
ctx . args () // Get command arguments as array
};
export const chatInput : ChatInputCommand = async ( ctx ) => {
// Store data accessible across middlewares and commands
ctx . store . set ( 'userId' , ctx . interaction . user . id );
ctx . store . set ( 'timestamp' , Date . now ());
const userId = ctx . store . get ( 'userId' );
const hasKey = ctx . store . has ( 'userId' );
ctx . store . delete ( 'userId' );
};
Use metadata to configure permissions, guild restrictions, and aliases:
import type { CommandMetadata } from 'commandkit' ;
export const metadata : CommandMetadata = {
// User must have these permissions
userPermissions: 'Administrator' ,
// Bot must have these permissions
botPermissions: [ 'KickMembers' , 'BanMembers' ],
// Only available in these guilds
guilds: [ '1234567890' , '0987654321' ],
// Aliases for message commands
aliases: [ 'p' , 'pong' ],
// Custom names for context menus
nameAliases: {
user: 'View Avatar' ,
message: 'View Author Avatar' ,
},
};
Generate metadata dynamically at runtime:
import type { CommandMetadataFunction } from 'commandkit' ;
export const generateMetadata : CommandMetadataFunction = async () => {
const adminGuilds = await fetchAdminGuilds ();
return {
userPermissions: 'Administrator' ,
guilds: adminGuilds . map ( g => g . id ),
};
};
Dynamic metadata is evaluated when commands are loaded. Changes won’t take effect until the bot restarts or commands are reloaded.
Real-world examples
Multi-handler command
A command that works with both slash commands and prefix commands:
src/app/commands/greet.ts
import type { CommandData , ChatInputCommand , MessageCommand } from 'commandkit' ;
export const command : CommandData = {
name: 'greet' ,
description: 'Greet command' ,
};
export const chatInput : ChatInputCommand = async ( ctx ) => {
await ctx . interaction . reply ( `Hello, ${ ctx . interaction . user } !` );
};
export const message : MessageCommand = async ( ctx ) => {
await ctx . message . reply ( `Hello, ${ ctx . message . author } !` );
};
Command with options
src/app/commands/avatar.ts
import type { CommandData , ChatInputCommand } from 'commandkit' ;
import { ApplicationCommandOptionType } from 'discord.js' ;
export const command : CommandData = {
name: 'avatar' ,
description: 'Get a user \' s avatar' ,
options: [
{
name: 'user' ,
description: 'The user to get the avatar for' ,
type: ApplicationCommandOptionType . User ,
},
],
};
export const chatInput : ChatInputCommand = async ( ctx ) => {
const user = ctx . options . getUser ( 'user' ) ?? ctx . interaction . user ;
await ctx . interaction . reply ({
embeds: [{
title: ` ${ user . username } 's Avatar` ,
image: { url: user . displayAvatarURL ({ size: 2048 }) },
color: 0x7289da ,
}],
});
};
Admin-only command
src/app/commands/(admin)/ban.ts
import type { CommandData , ChatInputCommand , CommandMetadata } from 'commandkit' ;
import { ApplicationCommandOptionType } from 'discord.js' ;
export const command : CommandData = {
name: 'ban' ,
description: 'Ban a user from the server' ,
options: [
{
name: 'user' ,
description: 'User to ban' ,
type: ApplicationCommandOptionType . User ,
required: true ,
},
{
name: 'reason' ,
description: 'Reason for ban' ,
type: ApplicationCommandOptionType . String ,
},
],
};
export const metadata : CommandMetadata = {
userPermissions: 'BanMembers' ,
botPermissions: 'BanMembers' ,
};
export const chatInput : ChatInputCommand = async ( ctx ) => {
const user = ctx . options . getUser ( 'user' , true );
const reason = ctx . options . getString ( 'reason' ) ?? 'No reason provided' ;
await ctx . guild ?. members . ban ( user , { reason });
await ctx . interaction . reply ( `Banned ${ user . username } : ${ reason } ` );
};
Next steps
Events Learn how to handle Discord events
Middlewares Add middleware to your commands