# CommandKit Documentation ## Introduction import Logo from './Logo';

support discord server npm version npm downloads
CommandKit is a meta-framework for building Discord bots with discord.js. It provides a structured, convention-based approach to command and event handling, JSX components, middleware, and more, so you can focus on writing bot logic instead of boilerplate setup. Whether you're a beginner looking for an easy way to get started or an experienced developer tired of rebuilding the same architecture for every new bot, CommandKit has you covered. ## Get started import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```bash npm2yarn npm create commandkit ``` Then run `commandkit dev` and start building. ```bash npm2yarn npx skills add neplextech/commandkit ``` ## Why CommandKit? Every time you start a new discord.js bot, you end up solving the same problems from scratch: - Writing a command handler that reads files, registers slash commands, and routes interactions - Building an event handler that loads listeners and manages their lifecycle - Wiring up interaction collectors for buttons, select menus, and modals — then cleaning them up - Manually syncing command definitions with Discord's API every time you change something - Setting up the same project structure, over and over If you've built more than one bot, you know the drill. Hours of boilerplate before you write a single line of actual bot logic. And if you're a beginner, figuring all of this out for the first time is overwhelming. ## How is this different from other frameworks? There's no shortage of discord.js frameworks out there, but most of them solve a very narrow problem: they load your command and event files so you don't have to write a file reader yourself. You instantiate their handler class, point it at a folder, and that's about it. The rest — project structure, compilation, component handling, command syncing — is still on you. Frameworks like Sapphire go further and have decent features, but they're built around a heavy object-oriented design. Everything is a class. Commands extend base classes, listeners extend base classes, preconditions extend base classes. It works, but it demands a lot of boilerplate and a specific programming style that not everyone wants. **CommandKit takes a fundamentally different approach.** Instead of being a library you wire into your own setup, it works like a proper meta-framework — think Next.js for Discord bots. It ships with a smart CLI that handles bundling, TypeScript/JSX compilation, hot-reloading, and command registration automatically. You don't configure a bundler. You don't set up `tsconfig.json` for JSX. You don't write glue code to connect things together. You run `commandkit dev`, and everything just works. This is the key difference: most discord.js frameworks are **loaders**. CommandKit is a **framework** — it manages the entire development lifecycle, from project scaffolding to production builds, so you can focus entirely on your bot's logic. ## Convention over configuration CommandKit follows a **convention-over-configuration** philosophy. Instead of requiring you to wire things together with config files, decorators, or registration calls, it uses sensible defaults and file-system conventions that just work. Put a file in `src/app/commands/` — it's a command. Put a file in `src/app/events/` — it's an event listener. Export a `chatInput` function — it handles slash commands. Export a `message` function in the same file — it handles prefix commands too. There's nothing to register, no class to extend, no decorator to apply. This matters because Discord bot projects aren't complex enterprise applications — they shouldn't need complex enterprise tooling. You want to write bot logic, not plumbing. Convention-over-configuration means the framework already knows what to do with your code based on _where it is and what it exports_, so you skip the setup entirely and go straight to building. If you've used Next.js, this will feel familiar: file-system routing, zero-config defaults, and a CLI that handles the build pipeline. Same idea, applied to Discord bots. ## What makes it actually useful ### Command and event handling — done for you This is the core of CommandKit. Drop your command files in a folder, export a function, and they just work. Slash commands, context menu commands, and even prefix (message) commands are all supported under a single unified command structure — no separate handler for each type. Commands are automatically registered and kept in sync with Discord. Events work the same way: create a file, export a listener, and CommandKit handles the rest. No more writing your own `loadCommands()` and `loadEvents()` utilities for every project. ### JSX components — clean, readable Discord UI code If you've used React, you already know how much cleaner JSX makes UI code. CommandKit brings that same idea to Discord. Instead of chaining `ActionRowBuilder`, `ButtonBuilder`, `Poll`, and friends — which gets unreadable fast with even moderately complex layouts — you write declarative JSX that looks like what it produces. This isn't a gimmick. discord.js builder chains become genuinely hard to maintain as your components grow. JSX fixes that. ### Built-in component handlers — forget about collectors This is where CommandKit goes beyond a basic command handler. Buttons, select menus, and modals get `onClick`, `onSelect`, and `onSubmit` handlers that are directly bound to the component. No more setting up `InteractionCollector` instances, managing timeouts, filtering interactions, and cleaning up afterwards. You define the component and its behavior in one place. CommandKit handles the interaction routing for you. ### Command middlewares — run logic before and after execution Need to check permissions, log analytics, or gate commands behind a cooldown? Middlewares let you run code before and/or after any command executes, without modifying the command itself. This is a step above what a typical command handler gives you — it's a proper middleware layer, similar to what you'd find in web frameworks like Express. ### Prefix commands — unified with slash commands CommandKit supports message-based (prefix) commands within the same command file that already handles slash commands, user context menus, and message context menus. One file, all command types. No separate prefix command handler needed. ### Caching with Redis support CommandKit ships with a [`@commandkit/cache`](../05-official-plugins/03-commandkit-cache.mdx) plugin that provides a customizable caching layer. Need fast in-memory caching? Done. Need Redis-backed caching for persistence across restarts? Also done. No need to wire up your own caching logic. Just attach the `'use cache'` directive to a function and call it a day! ```ts // The result of this function is automatically cached async function fetchUserData(userId: string) { 'use cache'; return db.users.findUnique({ id: userId }); } ``` Learn more about caching in [the cache plugin documentation](../05-official-plugins/03-commandkit-cache.mdx). ### Plugin system and custom events CommandKit is extensible through plugins. Plugins can hook into the framework lifecycle, add new behaviors, and even **emit custom events** — for example, a plugin could listen to an external API (a payment webhook, a game server, etc.) and fire events that your bot code can react to. This isn't just a wrapper around `node:events`; it's a structured extension point that lets plugins and your bot code communicate through a well-defined event system. Other official plugins include [`@commandkit/i18n`](../05-official-plugins/05-commandkit-i18n.mdx) for localization and [`@commandkit/analytics`](../05-official-plugins/02-commandkit-analytics.mdx) for usage tracking. ### AI-powered command execution The [`@commandkit/ai`](../05-official-plugins/01-commandkit-ai.mdx) plugin lets users interact with your bot through natural language. Instead of memorizing slash command names and options, users can just talk to your bot — and the AI figures out which command to run and with what parameters. Under the hood, it uses the [AI SDK](https://ai-sdk.dev) to connect to models like Google Gemini, OpenAI, and others. You define an `ai` export in your command file with a Zod schema describing the expected parameters, and the AI handles the rest — parsing natural language into structured input and calling your command function. ```ts title="src/app/commands/greet.ts" import type { AiConfig, AiCommand } from '@commandkit/ai'; import { z } from 'zod'; export const aiConfig: AiConfig = { inputSchema: z.object({ username: z.string().describe('The username to greet'), }), }; export const ai: AiCommand = async (ctx) => { const { username } = ctx.ai.params; await ctx.message.reply(`Hello, ${username}!`); }; ``` Now users can say `@bot say hi to John` and it just works. You also get built-in tools (fetching users, channels, guild info), custom tool support, lifecycle hooks, and per-user model selection. It's a full AI integration layer, not just a chatbot wrapper. Learn more in [the AI plugin documentation](../05-official-plugins/01-commandkit-ai.mdx). ### TypeScript and JavaScript — zero config CommandKit works with both TypeScript and JavaScript out of the box — including native JSX support with no configuration. No fiddling with `tsconfig.json`, no setting up a separate build step, no wiring up a bundler. The CLI handles compilation, bundling, hot-reloading, and project scaffolding. Run `commandkit dev` and start building. ## Who is this for? - **Beginners** who don't want to spend hours building command/event handlers before they can do anything useful - **Experienced developers** who are tired of rebuilding the same boilerplate for every new bot project - **Teams and freelancers** working on client projects who need a reliable, structured foundation they can build on quickly CommandKit has been used in production client projects and has saved hours of setup time on each one. It's not overhead — it's the setup work you'd do anyway, already done well. ## Support and suggestions - [GitHub repository](https://github.com/neplexlabs/commandkit) - [Discord community](https://commandkit.dev/discord) --- ## Setup CommandKit import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; :::info - CommandKit requires at least [Node.js](https://nodejs.org/) v24 - This documentation assumes that you have a basic understanding of how to use [discord.js](https://discord.js.org/). If you are new to discord.js, we recommend you to read the [discord.js guide](https://discordjs.guide/) first. ::: The quickest way to setup a new CommandKit project is to use the `create-commandkit` CLI. To get started, run the following command in your terminal: ```sh npm2yarn npm create commandkit@latest ``` This will start the CLI in an interactive mode. You can follow the prompts to setup your project. ## Available Examples CommandKit comes with several pre-built examples to help you get started quickly. You can list all available examples using: ```sh npm2yarn npm create commandkit@latest --list-examples ``` ### Using Examples You can create a project from any example using the `--example` flag: ```sh npm2yarn # Create a basic TypeScript bot npm create commandkit@latest --example basic-ts # Create a basic JavaScript bot npm create commandkit@latest --example basic-js # Create a Deno TypeScript bot npm create commandkit@latest --example deno-ts # Create a bot without CLI integration npm create commandkit@latest --example without-cli ``` ### CLI Options The CLI supports various options for customization: ```sh npm2yarn # Skip prompts and use defaults npm create commandkit@latest --yes # Use a specific package manager npm create commandkit@latest --use-pnpm npm create commandkit@latest --use-yarn npm create commandkit@latest --use-bun npm create commandkit@latest --use-deno # Skip dependency installation npm create commandkit@latest --skip-install # Skip git initialization npm create commandkit@latest --no-git # Create in a specific directory npm create commandkit@latest my-bot # Use a custom GitHub repository npm create commandkit@latest --example "https://github.com/user/repo" ``` ## Project structure By using the CLI to create a base project, you should get a file tree that looks something like this: ``` . ├── src/ │ ├── app/ │ │ ├── commands/ │ │ │ └── ping.ts │ │ └── events/ │ │ └── clientReady/ │ │ └── log.ts │ └── app.ts ├── .env ├── .gitignore ├── commandkit.config.ts ├── package.json └── tsconfig.json ``` :::note The exact project structure may vary depending on the example you choose. Each example comes with its own configuration and dependencies tailored to its specific use case. ::: ## Entry point The `src/app.ts` file is the main entry point for your application. This file default exports the discord.js client instance which CommandKit loads at runtime. ```ts title="src/app.ts" import { Client } from 'discord.js'; const client = new Client({ intents: ['Guilds', 'GuildMessages', 'MessageContent'], }); // Optional: Setting up the token manually client.token = process.env.MY_BOT_TOKEN; export default client; ``` ```js title="src/app.js" import { Client } from 'discord.js'; const client = new Client({ intents: ['Guilds', 'GuildMessages', 'MessageContent'], }); // Optional: Setting up the token manually client.token = process.env.MY_BOT_TOKEN; export default client; ``` Notice how there's no `client.login()` in this file. This is because CommandKit will automatically handle the login process for you when the application starts. ## Development version If you would like to try the latest development builds, you can use the `@dev` tag like so: ```sh npm2yarn npm create commandkit@dev ``` :::warning The development version is likely to have bugs. ::: ## Getting Help For more information about the CLI options, you can use the help flag: ```sh npm2yarn npm create commandkit@latest --help ``` This will display all available options and usage examples. ## Manual setup Alternatively, if you would like to setup a new CommandKit project manually, you can follow [this guide](../08-advanced/01-setup-commandkit-manually.mdx). --- ## Chat Input Commands import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Chat input commands, more commonly known as 'Slash Commands', are a way for users on Discord to perform actions using your bot. CommandKit provides a very simple way to manage your chat input commands, including registering them to the Discord API, as well as handling their execution. To create a new chat input command, create a new file in the `src/app/commands` directory with the name of the command. This guide will create a basic ping command which will respond with "Pong!" when executed. ```ts title="src/app/commands/ping.ts" import type { CommandData, ChatInputCommand } from 'commandkit'; export const command: CommandData = { name: 'ping', description: 'Replies with Pong!', }; export const chatInput: ChatInputCommand = async (ctx) => { await ctx.interaction.reply('Pong!'); }; ``` ```js title="src/app/commands/ping.js" /** * @typedef {import('commandkit').CommandData} CommandData * @typedef {import('commandkit').ChatInputCommand} ChatInputCommand */ /** @type {CommandData} */ export const command = { name: 'ping', description: 'Replies with Pong!', }; /** @type {ChatInputCommand} */ export const chatInput = async (ctx) => { await ctx.interaction.reply('Pong!'); }; ``` ## Exports explained ### `command` This is the command data object which defines the shape of your command. This is directly registered to the Discord API everytime your commands are refreshed. ### `chatInput` This is the main handler function for your chat input command. By exporting this function, CommandKit will know that it's a chat input command and will register and handle it accordingly. :::tip Chat input commands are just one of the command types that CommandKit supports. Learn more about [message commands](./04-message-commands.mdx) and [context menu commands](./03-context-menu-commands.mdx). ::: --- ## Options Autocomplete import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; :::warning This feature is only available for [chat input commands](./01-chat-input-commands.mdx). ::: In your chat input command options, autocomplete allows users on Discord to see a list of suggested choices based on what they're actively typing. ## Managing autocomplete choices In addition to registering your application commands, CommandKit can also handle any options you mark as 'autocomplete'. This means that CommandKit will automatically identify which command the autocomplete interaction belongs to. This will save you the hassle of registering and managing your own `interactionCreate` event listener. To begin, export the `autocomplete` function from the command where you have an option with `autocomplete` set to `true`. ```ts title="src/app/commands/pet.ts" import type { CommandData, AutocompleteCommand, ChatInputCommand } from 'commandkit'; import { ApplicationCommandOptionType } from 'discord.js'; export const command: CommandData = { name: 'pet', description: 'Check in one of your pets.', options: [ { name: 'name', description: 'The name of your pet', type: ApplicationCommandOptionType.String, autocomplete: true, required: true, }, ], }; const pets = Array.from({ length: 20 }, (_, i) => ({ name: `Pet ${i + 1}`, // e.g. Pet 1, Pet 2, Pet 3, etc. value: `petId_${i}`, })); export const autocomplete: AutocompleteCommand = async ({ interaction }) => { try { // Get the input value of your autocomplete option const input = interaction.options.getString('name', false); if (!input) { interaction.respond(pets); return; } const filteredPets = pets.filter((pet) => pet.name.toLowerCase().includes(input.toLowerCase()), ); // Respond with what you think the user may trying to find interaction.respond(filteredPets); } catch (error) { console.error('Autocomplete error', error); } }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const chosenPetId = interaction.options.getString('name', true); const chosenPet = pets.find((pet) => pet.value === chosenPetId); if (!chosenPet) { interaction.reply('Pet not found.'); return; } interaction.reply(`You chose ${chosenPet.name}`); }; ``` ```js title="src/app/commands/pet.js" /** * @typedef {import('commandkit').CommandData} CommandData * @typedef {import('commandkit').AutocompleteCommand} AutocompleteCommand * @typedef {import('commandkit').ChatInputCommand} ChatInputCommand */ import { ApplicationCommandOptionType } from 'discord.js'; /** @type {CommandData} */ export const command = { name: 'pet', description: 'Check in one of your pets.', options: [ { name: 'name', description: 'The name of your pet', type: ApplicationCommandOptionType.String, autocomplete: true, required: true, }, ], }; const pets = Array.from({ length: 20 }, (_, i) => ({ name: `Pet ${i + 1}`, // e.g. Pet 1, Pet 2, Pet 3, etc. value: `petId_${i}`, })); /** @type {AutocompleteCommand} */ export const autocomplete = async ({ interaction }) => { try { // Get the input value of your autocomplete option const input = interaction.options.getString('name', false); if (!input) { interaction.respond(pets); return; } const filteredPets = pets.filter((pet) => pet.name.toLowerCase().includes(input.toLowerCase()), ); // Respond with what you think the user may trying to find interaction.respond(filteredPets); } catch (error) { console.error('Autocomplete error', error); } }; /** @type {ChatInputCommand} */ export const chatInput = async ({ interaction }) => { const chosenPetId = interaction.options.getString('name', true); const chosenPet = pets.find((pet) => pet.value === chosenPetId); if (!chosenPet) { interaction.reply('Pet not found.'); return; } interaction.reply(`You chose ${chosenPet.name}`); }; ``` :::warning The Discord API only allows a maximum of 25 autocomplete choices per response. To ensure you don't run into errors, it's highly recommended to manually limit your choices response array to this amount. ::: :::tip It's highly recommended to use try/catch blocks in your `autocomplete` function as it's very common for autocomplete responses to run into errors, as they may take longer due to their dynamic nature. ::: --- ## Context Menu Commands import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Context menu commands allows users on Discord to run commands by either referencing a message or a user. Context menu commands can be grouped to either 'Message' or 'User' context menu commands. Alongside chat input commands, you can use CommandKit to also register and handle context menu commands. Context menu commands live in the same directory as your chat input commands. To begin, create a command file in your `src/app/commands` directory. This guide will create 2 simple `report-message` and `report-user` commands. It's not an actual reporting system, but it should make understanding the nature of these context menu commands easier. ## Message context menu command ```ts title="src/app/commands/report-message.ts" import type { CommandData, MessageContextMenuCommand } from 'commandkit'; import { MessageFlags } from 'discord.js'; export const command: CommandData = { name: 'report-message', description: 'Report a message to moderators.', }; export const messageContextMenu: MessageContextMenuCommand = async ({ interaction, }) => { const content = interaction.targetMessage.content; // you could add your message reporting logic here await interaction.reply({ content: `You have reported the following message: ${content}`, flags: MessageFlags.Ephemeral, }); }; ``` ```js title="src/app/commands/report-message.js" /** * @typedef {import('commandkit').CommandData} CommandData * @typedef {import('commandkit').MessageContextMenuCommand} MessageContextMenuCommand */ import { MessageFlags } from 'discord.js'; /** @type {CommandData} */ export const command = { name: 'report-message', description: 'Report a message to moderators.', }; /** @type {MessageContextMenuCommand} */ export const messageContextMenu = async ({ interaction }) => { const content = interaction.targetMessage.content; // you could add your message reporting logic here await interaction.reply({ content: `You have reported the following message: ${content}`, flags: MessageFlags.Ephemeral, }); }; ``` Once the command file is loaded, CommandKit also materializes the context menu variants in the command handler cache. This means introspection APIs such as `commandHandler.getCommandsArray()` and tools built on top of it can see the context menu entries as distinct commands immediately after load. By just exporting the `messageContextMenu` function from your command file, CommandKit will properly update your command type before registering it to the Discord API. You don't have to manually set a `type` in your `command` object, as you would with most command handlers. ## User context menu command The logic for registering and handling user context menu commands is very similar to message context menu commands. ```ts title="src/app/commands/report-user.ts" import type { CommandData, UserContextMenuCommand } from 'commandkit'; import { MessageFlags } from 'discord.js'; export const command: CommandData = { name: 'report-user', description: 'Report a user to moderators.', }; export const userContextMenu: UserContextMenuCommand = async ({ interaction, }) => { const userId = interaction.targetUser.id; // you could add your user reporting logic here await interaction.reply({ content: `You have reported a user with the ID: ${userId}`, flags: MessageFlags.Ephemeral, }); }; ``` ```js title="src/app/commands/report-user.js" /** * @typedef {import('commandkit').CommandData} CommandData * @typedef {import('commandkit').UserContextMenuCommand} UserContextMenuCommand */ import { MessageFlags } from 'discord.js'; /** @type {CommandData} */ export const command = { name: 'report-user', description: 'Report a user to moderators.', }; /** @type {UserContextMenuCommand} */ export const userContextMenu = async ({ interaction }) => { const userId = interaction.targetUser.id; // you could add your user reporting logic here await interaction.reply({ content: `You have reported a user with the ID: ${userId}`, flags: MessageFlags.Ephemeral, }); }; ``` You may have noticed that the context menu command names are the same as the message command names. This is because the context menu command names are borrowed from the non-context menu command names. In order to change the context menu command name, you can use the `nameAliases` option in the `CommandMetadata` object. :::warning If you are using the [i18n plugin](../05-official-plugins/05-commandkit-i18n.mdx), the plugin will add `name_localizations` to the `nameAliases` option if localization for the context menu command name is provided. ```json title="src/app/locales/en/command.json" { "$command": { "name": "Report", // this will add `name_localizations` to the regular command name "description": "Report a message or user to moderators." // this will add `description_localizations` to the regular command description }, "$command:user-ctx": { "name": "Report User" // this will add `name_localizations` to the `nameAliases.user` option }, "$command:message-ctx": { "name": "Report Message" // this will add `name_localizations` to the `nameAliases.message` option } } ``` ::: ```ts title="src/app/commands/report-user.ts" import type { CommandMetadata } from 'commandkit'; import { MessageFlags } from 'discord.js'; export const metadata: CommandMetadata = { nameAliases: { user: 'Report User', message: 'Report Message', } }; ``` ```js title="src/app/commands/report-user.js" /** @type {CommandMetadata} */ export const metadata = { nameAliases: { user: 'Report User', message: 'Report Message', } }; ``` --- ## Message Commands import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Message commands, which are generally considered 'Legacy', are a way to run commands on Discord bots using regular messages. Before the introduction of [Slash/Chat Input Commands](./01-chat-input-commands.mdx), this was the primary way commands were handled. They were usually triggered by using a prefix set by the developer of the bot (for example: `!ping`). :::warning While CommandKit allows developers to create and handle message commands, it's important to note that Discord generally discourages message commands in favor of the more modern [Chat Input Commands](./01-chat-input-commands.mdx). ::: ## Basic setup Message commands, like all the other command types, live in your `src/app/commands` directory. To let CommandKit know that this is a message command, just export the `message` function from your command file. ```ts title="src/app/commands/ping.ts" import type { CommandData, MessageCommand } from 'commandkit'; export const command: CommandData = { name: 'ping', }; export const message: MessageCommand = async (ctx) => { await ctx.message.reply('Pong!'); }; ``` ```js title="src/app/commands/ping.js" /** * @typedef {import('commandkit').CommandData} CommandData * @typedef {import('commandkit').MessageCommand} MessageCommand */ /** @type {CommandData} */ export const command = { name: 'ping', }; /** @type {MessageCommand} */ export const message = async (ctx) => { await ctx.message.reply('Pong!'); }; ``` ## Message command options (arguments) Since a message command - on a technical level - is just a string, the idea of message command options will be different compared to [chat input command](./01-chat-input-commands.mdx) options. Instead of getting parsed data, you'll essentially be dealing with string 'arguments'. ```ts title="src/app/commands/ping.ts" {8} import type { CommandData, MessageCommand } from 'commandkit'; export const command: CommandData = { name: 'ping', }; export const message: MessageCommand = async (ctx) => { const args = ctx.args(); // string[] }; ``` ```js title="src/app/commands/ping.js" {13} /** * @typedef {import('commandkit').CommandData} CommandData * @typedef {import('commandkit').MessageCommand} MessageCommand */ /** @type {CommandData} */ export const command = { name: 'ping', }; /** @type {MessageCommand} */ export const message = async (ctx) => { const args = ctx.args(); // string[] }; ``` With the above command as an example, if a user on Discord runs `!ping john jack jane`, the value of `args` will equal `['john', 'jack', 'jane']`. ## Custom prefixes The simplest way to set a custom prefix is using the `setPrefixResolver` method on the `commandkit` instance: ```ts title="src/app.ts" import { commandkit } from 'commandkit'; commandkit.setPrefixResolver(async (message) => { return '?'; }); ``` ```js title="src/app.js" import { commandkit } from 'commandkit'; commandkit.setPrefixResolver(async (message) => { return '?'; }); ``` This sets a global prefix of `?` for all message commands. Your bot will now respond to commands like `?help` or `?ping`. :::tip You can return an array of strings to support multiple prefixes simultaneously: ```js return ['?', '!', '>']; ``` ::: ### Guild-specific prefixes For a more dynamic approach, you might want different prefixes for different guilds. Here's how to implement that: ```ts title="src/app.ts" import { commandkit } from 'commandkit'; import { database } from '../database'; // This is a mock database commandkit.setPrefixResolver(async (message) => { if (!message.guildId) { return '!'; // Fallback to '!' if command is used in a DM } const guildSettings = await database.findUnique({ where: { guildId: message.guildId, }, select: { prefix: true, }, }); return guildSettings?.prefix ?? '!'; // Fallback to '!' if no prefix is found }); ``` ```js title="src/app.js" import { commandkit } from 'commandkit'; import { database } from '../database'; // This is a mock database commandkit.setPrefixResolver(async (message) => { if (!message.guildId) { return '!'; // Fallback to '!' if command is used in a DM } const guildSettings = await database.findUnique({ where: { guildId: message.guildId, }, select: { prefix: true, }, }); return guildSettings?.prefix ?? '!'; // Fallback to '!' if no prefix is found }); ``` :::warning The `setPrefixResolver` method runs on every message, so it's important to make sure that it's as lightweight as possible. To help with performance, you may want to implement caching, which CommandKit also supports with the [`@commandkit/cache`](../05-official-plugins/03-commandkit-cache.mdx) official plugin. ::: --- ## Command Metadata import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Command metadata is a way to add additional information to your commands that CommandKit will handle. To get started, you can export a `metadata` object from your command: ```ts title="src/app/commands/ping.ts" {8-10} import type { CommandData, CommandMetadata } from 'commandkit'; export const command: CommandData = { name: 'ping', description: 'Replies with Pong!', }; export const metadata: CommandMetadata = { // Add your metadata here }; ``` ```js title="src/app/commands/ping.js" {13-15} /** * @typedef {import('commandkit').CommandData} CommandData * @typedef {import('commandkit').CommandMetadata} CommandMetadata */ /** @type {CommandData} */ export const command = { name: 'ping', description: 'Replies with Pong!', }; /** @type {CommandMetadata} */ export const metadata = { // Add your metadata here }; ``` ## Metadata properties ### `userPermissions` This is a string, or array of user permission strings that will be required by the person executing the command. ```ts title="src/app/commands/ping.ts" export const metadata: CommandMetadata = { // If the user does not have the Administrator permission, CommandKit will let them know userPermissions: 'Administrator', }; ``` ```js title="src/app/commands/ping.js" /** @type {CommandMetadata} */ export const metadata = { // If the user does not have the Administrator permission, CommandKit will let them know userPermissions: 'Administrator', }; ``` ### `botPermissions` This is a string, or array of bot permission strings that will be required by your bot to execute the command. This is useful for commands where your bot needs to have certain permissions in a guild e.g. moderation commands. ```ts title="src/app/commands/ping.ts" export const metadata: CommandMetadata = { // If the bot does not have these permissions, CommandKit will let them know botPermissions: ['KickMembers', 'BanMembers'], }; ``` ```js title="src/app/commands/ping.js" /** @type {CommandMetadata} */ export const metadata = { // If the bot does not have these permissions, CommandKit will let them know botPermissions: ['KickMembers', 'BanMembers'], }; ``` ### `guilds` This is an array of guild IDs that the command will be registered in, or be available to be executed (message commands). ```ts title="src/app/commands/ping.ts" export const metadata: CommandMetadata = { guilds: ['1234567890', '1234567891'], }; ``` ```js title="src/app/commands/ping.js" /** @type {CommandMetadata} */ export const metadata = { guilds: ['1234567890', '1234567891'], }; ``` ### `aliases` This is an array of alternative command names that will be available for users to use to execute the command. :::warning This only works for [message commands](./04-message-commands.mdx). ::: ```ts title="src/app/commands/ping.ts" export const metadata: CommandMetadata = { aliases: ['p', 'pong'], }; ``` ```js title="src/app/commands/ping.js" /** @type {CommandMetadata} */ export const metadata = { aliases: ['p', 'pong'], }; ``` ### `nameAliases` This is a way to use a different context menu command name from the one defined in your `command` object. This is helpful when you want a different name compared to your chat input or message command while still using the same command file to handle them. :::warning When the [@commandkit/i18n](../05-official-plugins/05-commandkit-i18n.mdx) plugin is in use and a translation for the context menu command is provided, this will be taken as base name and translations will be applied on top of it. ::: ```ts title="src/app/commands/avatar.ts" export const command: CommandData = { name: 'avatar' } export const metadata: CommandMetadata = { nameAliases: { message: 'Author Avatar', user: 'Profile Picture' } }; // this slash command will run using the name `avatar` export const chatInput: ChatInputCommand = () => {}; // this message command will run using the name `avatar` export const message: MessageCommand = () => {}; // this message context menu command will run using the name `author-avatar` export const messageContextMenu: MessageContextMenuCommand = () => {}; // this user context menu command will run using the name `profile-picture` export const userContextMenu: UserContextMenuCommand = () => {}; ``` ```js title="src/app/commands/avatar.js" export const command = { name: 'avatar' } export const metadata = { nameAliases: { message: 'author-avatar', user: 'profile-picture' } }; // this slash command will run using the name `avatar` export const chatInput = () => {}; // this message command will run using the name `avatar` export const message = () => {}; // this message context menu command will run using the name `author-avatar` export const messageContextMenu = () => {}; // this user context menu command will run using the name `profile-picture` export const userContextMenu = () => {}; ``` ## Generated metadata If you'd like to generate metadata dynamically, you can export a `generateMetadata` function from your command file that should return a `CommandMetadata` object. ```ts title="src/app/commands/ping.ts" import type { CommandMetadataFunction } from 'commandkit'; export const generateMetadata: CommandMetadataFunction = async () => { // Dynamically determine the metadata for the command return { userPermissions: 'Administrator', botPermissions: ['KickMembers', 'BanMembers'], guilds: ['1234567890', '1234567891'], aliases: ['p', 'pong'], }; }; ``` ```js title="src/app/commands/ping.js" /** * @typedef {import('commandkit').CommandMetadataFunction} CommandMetadataFunction */ /** @type {CommandMetadataFunction} */ export const generateMetadata = async () => { // Dynamically determine the metadata for the command return { userPermissions: 'Administrator', botPermissions: ['KickMembers', 'BanMembers'], guilds: ['1234567890', '1234567891'], aliases: ['p', 'pong'], }; }; ``` --- ## after Function import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; :::warning UNSTABLE The `after()` function is currently marked as unstable and may change in the future. ::: The `after()` function allows you to execute a callback after a command has been executed. This is useful for performing actions that should occur after the command has completed, such as logging or cleanup tasks. ## Usage ```ts title="src/app/commands/ping.ts" import { type CommandData, type ChatInputCommand, unstable_after as after, } from 'commandkit'; export const command: CommandData = {}; export const chatInput: ChatInputCommand = async (ctx) => { after(() => { // This code will be executed after the command has been executed // Perform any cleanup here console.log('Command has been executed'); }); await ctx.interaction.reply({ content: 'Hello World', ephemeral: true, }); }; ``` ```js title="src/app/commands/ping.js" /** * @typedef {import('commandkit').CommandData} CommandData * @typedef {import('commandkit').ChatInputCommand} ChatInputCommand */ import { unstable_after as after } from 'commandkit'; /** @type {CommandData} */ export const command = {}; /** @type {ChatInputCommand} */ export const chatInput = async (ctx) => { after(() => { // This code will be executed after the command has been executed // Perform any cleanup here console.log('Command has been executed'); }); await ctx.interaction.reply({ content: 'Hello World', ephemeral: true, }); }; ``` :::info The `after()` function will always be called, regardless of whether the command execution was successful or not. ::: ## Cancelling the after function You can cancel the after function by calling the `cancelAfter` function with the ID of the after function. ```ts title="src/app/commands/ping.ts" import { unstable_after as after, unstable_cancelAfter as cancelAfter, } from 'commandkit'; export const chatInput: ChatInputCommand = async (ctx) => { const id = after(() => { console.log('This will run after the command has finished executing.'); }); if (something) { cancelAfter(id); } }; ``` ```js title="src/app/commands/ping.js" /** * @typedef {import('commandkit').ChatInputCommand} ChatInputCommand */ import { unstable_after as after, unstable_cancelAfter as cancelAfter, } from 'commandkit'; /** @type {ChatInputCommand} */ export const chatInput = async (ctx) => { const id = after(() => { console.log('This will run after the command has finished executing.'); }); if (something) { cancelAfter(id); } }; ``` --- ## Category Directory Category directories are special directories inside `src/app/commands`, with `(categoryName)` as their name. It is primarily used to group related commands together. This is useful for organizing your commands into logical groups, making it easier to manage and maintain your code. You can even nest multiple category directories to create a hierarchy of commands. This can then be used across your application in places such as middlewares, plugins, or even other commands through your `commandkit` instance. ## Example structure ```title="" {4-15} . ├── src/ │ ├── app/ │ │ ├── commands/ │ │ │ ├── (Fun)/ │ │ │ │ ├── (Jokes)/ │ │ │ │ │ ├── random-joke.ts │ │ │ │ │ └── dad-joke.ts │ │ │ │ ├── 8ball.ts │ │ │ │ ├── cat.ts │ │ │ │ └── dog.ts │ │ │ └── (Moderation)/ │ │ │ ├── timeout.ts │ │ │ ├── kick.ts │ │ │ └── ban.ts │ │ └── events/ │ │ └── clientReady/ │ │ └── log.ts │ └── app.ts ├── .env ├── .gitignore ├── commandkit.config.ts ├── package.json └── tsconfig.json ``` > [!NOTE] Transparent Categories vs Subcommand Hierarchy: Folders > using parenthesis `(Category)` strictly serve to group handlers > mentally and apply transparent middlewares. They do **not** change > the execution slash command (`/ban`). If you are designing > comprehensive setups combining sub-commands, you should establish a > Root Command directory using square brackets `[commandName]`. Read > more in the [Subcommand Hierarchy Guide](./09-subcommand-hierarchy). --- ## Middlewares import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Middlewares in CommandKit allow you to execute code before and after command execution. This is incredibly powerful for implementing features like logging, authentication, permission checks, or any other cross-cutting concerns for your Discord bot. CommandKit supports three different types of middleware files, each serving different scoping purposes for your commands. ## Basic middleware structure All middleware files follow the same export pattern. You can export `beforeExecute` and `afterExecute` functions that will run at their respective times during command execution. ```ts title="src/app/commands/+middleware.ts" import { MiddlewareContext } from 'commandkit'; export function beforeExecute(ctx: MiddlewareContext) { // This function will be executed before the command is executed console.log(`User ${ctx.interaction.user.id} is about to execute a command`); } export function afterExecute(ctx: MiddlewareContext) { // This function will be executed after the command is executed console.log( `Command execution completed for user ${ctx.interaction.user.id}`, ); } ``` ```js title="src/app/commands/+middleware.js" /** * @typedef {import('commandkit').MiddlewareContext} MiddlewareContext */ /** * Executed before command execution * @param {MiddlewareContext} ctx - The middleware context */ export function beforeExecute(ctx) { // This function will be executed before the command is executed console.log(`User ${ctx.interaction.user.id} is about to execute a command`); } /** * Executed after command execution * @param {MiddlewareContext} ctx - The middleware context */ export function afterExecute(ctx) { // This function will be executed after the command is executed console.log( `Command execution completed for user ${ctx.interaction.user.id}`, ); } ``` ## Stop command execution You can stop a command from running by calling `stopMiddlewares()` in the `beforeExecute` function. ```ts title="src/app/commands/+middleware.ts" import { type MiddlewareContext, stopMiddlewares } from 'commandkit'; export function beforeExecute(ctx: MiddlewareContext) { if (ctx.interaction.user.id !== '1234567890') { // Conditionally stop command execution console.log(`${ctx.commandName} will not be executed!`); stopMiddlewares(); } // Continue with command execution console.log(`${ctx.commandName} will be executed!`); } ``` ```js title="src/app/commands/+middleware.js" /** * @typedef {import('commandkit').MiddlewareContext} MiddlewareContext */ /** * @param {MiddlewareContext} ctx - The middleware context */ export function beforeExecute(ctx) { if (ctx.interaction.user.id !== '1234567890') { // Conditionally stop command execution console.log(`${ctx.commandName} will not be executed!`); stopMiddlewares(); } // Continue with command execution console.log(`${ctx.commandName} will be executed!`); } ``` :::tip You can also use `stopMiddlewares()` inside a command function to stop any `afterExecute` middlewares from running. In addition, you can also use `stopMiddlewares()` inside any `afterExecute` middleware function to stop any remaining middlewares from running. ::: :::warning Calling `stopMiddlewares()` in a try/catch block may lead to unexpected behavior. If you still want to use `stopMiddlewares()` in a try/catch block, you can use the `isErrorType` function to check if the error is an instance of the `CommandKitErrorCodes.StopMiddlewares` error. ```ts title="src/app/commands/+middleware.ts" import { type MiddlewareContext, stopMiddlewares } from 'commandkit'; export function beforeExecute(ctx: MiddlewareContext) { try { // code that may throw an error stopMiddlewares(); // conditionally stop the middleware chain } catch (error) { if (isErrorType(error, CommandKitErrorCodes.StopMiddlewares)) { // if stopMiddlewares() is called in the try block, throw it so CommandKit can stop the middleware chain throw error; } // this means that the code threw the error, and stopMiddlewares() was not called // the rest of the middlewares will be executed as normal console.error(error); } } ``` ```js title="src/app/commands/+middleware.js" /** * @typedef {import('commandkit').MiddlewareContext} MiddlewareContext */ /** * @param {MiddlewareContext} ctx - The middleware context */ export function beforeExecute(ctx) { try { // code that may throw an error stopMiddlewares(); // conditionally stop the middleware chain } catch (error) { if (isErrorType(error, CommandKitErrorCodes.StopMiddlewares)) { // if stopMiddlewares() is called in the try block, throw it so CommandKit can stop the middleware chain throw error; } // this means that the code threw the error, and stopMiddlewares() was not called // the rest of the middlewares will be executed as normal console.error(error); } } ``` ::: ## Middleware types ### Directory-scoped middleware Create a `+middleware.ts` file in any commands directory to apply middleware to commands whose definition files live in that same directory. ```ts title="src/app/commands/(Moderation)/+middleware.ts" import type { MiddlewareContext } from 'commandkit'; export function beforeExecute(ctx: MiddlewareContext) { // This middleware will run before commands defined in this directory if (!ctx.interaction.member.permissions.has('KickMembers')) { throw new Error('You need moderation permissions to use this command'); } } ``` ```js title="src/app/commands/(Moderation)/+middleware.js" /** * @typedef {import('commandkit').MiddlewareContext} MiddlewareContext */ /** * @param {MiddlewareContext} ctx - The middleware context */ export function beforeExecute(ctx) { // This middleware will run before commands defined in this directory if (!ctx.interaction.member.permissions.has('KickMembers')) { throw new Error('You need moderation permissions to use this command'); } } ``` For hierarchical command trees, "current directory" means the directory that contains the leaf definition: - a shorthand leaf such as `add.subcommand.ts` uses middleware from the directory that contains that file - a folder leaf such as `[archive]/command.ts` uses middleware from the `[archive]` directory - parent command and group directories do not automatically contribute `+middleware.ts` files to deeper leaves ### Command-specific middleware For command-specific middleware, create a file named `+.middleware.ts` where `` matches your command file name. ```ts title="src/app/commands/+ban.middleware.ts" import type { MiddlewareContext } from 'commandkit'; export function beforeExecute(ctx: MiddlewareContext) { // This middleware only runs before the ban command console.log('Ban command is about to be executed'); } ``` ```js title="src/app/commands/+ban.middleware.js" /** * @typedef {import('commandkit').MiddlewareContext} MiddlewareContext */ /** * @param {MiddlewareContext} ctx - The middleware context */ export function beforeExecute(ctx) { // This middleware only runs before the ban command console.log('Ban command is about to be executed'); } ``` ### Global middleware Create a `+global-middleware.ts` file in your commands directory to apply middleware to every command in your entire Discord bot. ```ts title="src/app/commands/+global-middleware.ts" import type { MiddlewareContext } from 'commandkit'; export function beforeExecute(ctx: MiddlewareContext) { // This middleware runs before ANY command in your bot console.log( `Command executed by ${ctx.interaction.user.tag} in ${ctx.interaction.guild?.name || 'DMs'}`, ); } ``` ```js title="src/app/commands/+global-middleware.js" /** * @typedef {import('commandkit').MiddlewareContext} MiddlewareContext */ /** * @param {MiddlewareContext} ctx - The middleware context */ export function beforeExecute(ctx) { // This middleware runs before ANY command in your bot console.log( `Command executed by ${ctx.interaction.user.tag} in ${ctx.interaction.guild?.name || 'DMs'}`, ); } ``` :::tip Middleware execution order is deterministic: - global middlewares run first - current-directory middlewares run second - same-directory command-scoped middlewares run last ::: --- ## Subcommand Hierarchy :::info This feature is currently available under `commandkit@dev` release only. ::: CommandKit supports fully filesystem-based slash command trees. The filesystem tokens are explicit: - `[command]` defines a root command directory - `{group}` defines a subcommand group directory - `command.ts` defines a directory-backed command node - `group.ts` defines a group node - `*.subcommand.ts` defines a shorthand subcommand leaf ## Root Command Directories `[command]` Wrap a directory name in square brackets to declare a root slash command. The root directory must contain a `command.ts` file. ```text title="Directory Structure" src/app/commands/ └── (general)/ └── [settings]/ ├── command.ts ├── profile.subcommand.ts └── security.subcommand.ts ``` This produces: - `/settings profile` - `/settings security` Use `command.ts` to define the root command's description and shared options or metadata for the `/settings` root. ## Group Directories `{group}` Use curly braces inside a root command directory to define a subcommand group. Group directories must contain a `group.ts` file. ```text title="Directory Structure" src/app/commands/ └── [settings]/ ├── command.ts └── {notifications}/ ├── group.ts ├── enable.subcommand.ts └── disable.subcommand.ts ``` This produces: - `/settings notifications enable` - `/settings notifications disable` ## Folder Leaves And Shorthand Leaves You can define leaves in two ways: 1. Shorthand files with `*.subcommand.ts` 2. Directory-backed leaves with `[leaf]/command.ts` Both compile to executable subcommands. ```text title="Directory Structure" src/app/commands/ └── [ops]/ ├── command.ts ├── status.subcommand.ts └── [deploy]/ └── command.ts ``` This produces: - `/ops status` - `/ops deploy` ## Non-Leaf Command Files A hierarchical node with children is a non-leaf node. When a directory such as `[admin]` contains child subcommands or groups, its `command.ts` file can define command metadata and configuration, but it must not export executable handlers such as `chatInput`, `message`, or `autocomplete`. Executable handlers belong in leaf nodes, such as `[logs]/command.ts` or `logs.subcommand.ts`. Incorrect: ```text title="Directory Structure" src/app/commands/ └── [admin]/ ├── command.ts # Incorrect: exports chatInput while admin has children └── [logs]/ └── command.ts ``` Correct: ```text title="Directory Structure" src/app/commands/ └── [admin]/ ├── command.ts # Correct: metadata/config only └── [logs]/ └── command.ts # Correct: executable handlers live here ``` You can also use a shorthand leaf: ```text title="Directory Structure" src/app/commands/ └── [admin]/ ├── command.ts # metadata/config only └── logs.subcommand.ts ``` ## Middleware Scope In Hierarchical Trees Hierarchical commands use the same middleware model as flat commands: - `+global-middleware.ts` always applies - `+middleware.ts` applies only from the current directory - `+.middleware.ts` applies only from that same current directory That means hierarchical leaves do not inherit `+middleware.ts` from ancestor command or group directories. Example: ```text title="Directory Structure" src/app/commands/ └── [workspace]/ ├── command.ts ├── +middleware.ts └── {notes}/ ├── group.ts ├── +middleware.ts ├── +add.middleware.ts ├── add.subcommand.ts └── [archive]/ ├── +middleware.ts └── command.ts ``` `add.subcommand.ts` uses: - global middleware - `{notes}/+middleware.ts` - `{notes}/+add.middleware.ts` `[archive]/command.ts` uses: - global middleware - `[archive]/+middleware.ts` ## Categories Still Work Parenthesis directories such as `(general)` remain organizational only. They do not change the slash route shape and they do not create command or group nodes by themselves. :::info Use `(category)` for organization, `[command]` for root commands, and `{group}` for subcommand groups. That separation keeps the filesystem shape and the generated Discord route aligned. ::: --- ## Discord.js Events import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Events allow your Discord bot to react to various actions happening on Discord, such as when users send messages, join guilds, or when your bot comes online. CommandKit provides a simple way to organize and handle Discord.js events using the `events` directory inside your `src/app` directory. This approach makes it easy to manage multiple events and keep your code organized. ## Basic event directory structure Each Discord.js event gets its own directory within `src/app/events`, and the directory name must match the exact Discord.js event name. Inside each event directory, you can create multiple handler files that will all execute when that event is triggered. Here's how your project structure might look with the `"messageCreate"` and `"clientReady"` events: ```title="" {4-9} . ├── src/ │ ├── app/ │ │ └── events/ │ │ ├── messageCreate/ │ │ │ ├── give-xp.ts │ │ │ └── log-message.ts │ │ └── clientReady/ │ │ └── log.ts │ └── app.ts ├── .env ├── .gitignore ├── commandkit.config.ts ├── package.json └── tsconfig.json ``` :::tip You can see what events are available in Discord.js by checking the [Discord.js documentation](https://discord.js.org/docs/packages/discord.js/main/ClientEventTypes:Interface). ::: ## Creating an event handler To create an event handler, create a folder inside the `src/app/events` directory which should match the event name from Discord.js. This example will use the `"clientReady"` event. ```ts title="src/app/events/clientReady/log.ts" import type { EventHandler } from 'commandkit'; const handler: EventHandler<'clientReady'> = (client) => { console.log(`🤖 ${client.user.displayName} is online!`); }; export default handler; ``` ```js title="src/app/events/clientReady/log.js" /** * @typedef {import('commandkit').EventHandler<'clientReady'>} ClientReadyHandler */ /** @type {ClientReadyHandler} */ const handler = (client) => { console.log(`🤖 ${client.user.displayName} is online!`); }; export default handler; ``` That's it! CommandKit will automatically detect this file and register it as one of the handler functions for the `"clientReady"` event. Just like the Discord.js `client.on()` method, the parameters you receive in your event file will be based on what event you're trying to handle. In the example above, the `"clientReady"` event should give you a [`Client`](https://discord.js.org/docs/packages/discord.js/main/Client:Class) instance. :::tip By using the [`EventHandler`](../../api-reference/commandkit/types/event-handler.mdx) type, you can get type safety for the parameters you receive in your event file, without having to manually typing each Discord.js type definition. ::: ## Once events You may want to have some events to only get called once. For this, you can export a variable called `once` from your event function file. ```ts title="src/app/events/clientReady/log.ts" import type { EventHandler } from 'commandkit'; export const once = true; const handler: EventHandler<'clientReady'> = (client) => { console.log(`🤖 ${client.user.displayName} is online!`); }; export default handler; ``` ```js title="src/app/events/clientReady/log.js" /** * @typedef {import('commandkit').EventHandler<'clientReady'>} ClientReadyHandler */ export const once = true; /** @type {ClientReadyHandler} */ const handler = (client) => { console.log(`🤖 ${client.user.displayName} is online!`); }; export default handler; ``` ## Events with multiple parameter Some Discord.js events have multiple parameters, such as the `"messageUpdate"` event. ```ts title="src/app/events/messageUpdate/log-message-update.ts" import type { EventHandler } from 'commandkit'; const handler: EventHandler<'messageUpdate'> = (oldMessage, newMessage) => { console.log( `Message from ${oldMessage.author?.username} updated: ${oldMessage.content} -> ${newMessage.content}`, ); }; export default handler; ``` ```js title="src/app/events/messageUpdate/log-message-update.js" /** * @typedef {import('commandkit').EventHandler<'messageUpdate'>} MessageUpdateHandler */ /** @type {MessageUpdateHandler} */ const handler = (oldMessage, newMessage) => { console.log( `Message from ${oldMessage.author?.username} updated: ${oldMessage.content} -> ${newMessage.content}`, ); }; export default handler; ``` ## Additional parameters Outside of the parameters provided by Discord.js, CommandKit will pass additional parameters to the event handler, including a [`Client`](https://discord.js.org/docs/packages/discord.js/main/Client:Class) and [`CommandKit`](../../api-reference/commandkit/classes/command-kit.mdx) instance. ```ts title="src/app/events/messageCreate/log.ts" import type { EventHandler } from 'commandkit'; const handler: EventHandler<'messageCreate'> = ( message, client, commandkit, ) => { console.log(`Message from ${message.author.username}: ${message.content}`); }; export default handler; ``` ```js title="src/app/events/messageCreate/log.js" /** * @typedef {import('commandkit').EventHandler<'messageCreate'>} MessageCreateHandler */ /** @type {MessageCreateHandler} */ const handler = (message, client, commandkit) => { console.log(`Message from ${message.author.username}: ${message.content}`); }; export default handler; ``` ## Multiple handlers for the same event One of the powerful features of CommandKit's event system is the ability to have multiple handlers for the same event. Each handler file you create in an event directory will be executed when that event is called. For example, you might want to do several things when a message is created: ```ts title="src/app/events/messageCreate/give-xp.ts" import type { EventHandler } from 'commandkit'; const handler: EventHandler<'messageCreate'> = (message) => { // Don't give XP to bots if (message.author.bot) return; // Give XP to the user console.log(`Giving XP to ${message.author.username}`); }; export default handler; ``` ```js title="src/app/events/messageCreate/give-xp.js" /** * @typedef {import('commandkit').EventHandler<'messageCreate'>} MessageCreateHandler */ /** @type {MessageCreateHandler} */ const handler = (message) => { // Don't give XP to bots if (message.author.bot) return; // Give XP to the user console.log(`Giving XP to ${message.author.username}`); }; export default handler; ``` ```ts title="src/app/events/messageCreate/log-message.ts" import type { EventHandler } from 'commandkit'; const handler: EventHandler<'messageCreate'> = (message) => { console.log(`Message from ${message.author.username}: ${message.content}`); }; export default handler; ``` ```js title="src/app/events/messageCreate/log-message.js" /** * @typedef {import('commandkit').EventHandler<'messageCreate'>} MessageCreateHandler */ /** @type {MessageCreateHandler} */ const handler = (message) => { console.log(`Message from ${message.author.username}: ${message.content}`); }; export default handler; ``` Both handler functions will be called whenever a message is sent in Discord. :::tip Event handler files can be named anything you want - CommandKit identifies them by their location in the event directory, not by their filename. However, since each file is treated as a handler function, they're executed one-by-one in the order of their file name. Example: `01-give-xp.ts` will be executed before `02-log-message.ts`. ::: --- ## Custom Events CommandKit also supports using custom events in the `events` directory. Custom events must be defined in a namespaced directory, which are primarily handled by plugins. For example, if you have a plugin that is handling a custom event called `"guildMemberBoost"` under a namespace called `"custom"`, you can create a file in the `src/app/events/(custom)/guildMemberBoost` directory which will be handled by the appropriate plugin. :::warning The `"custom"` namespace above is just an example. Namespaces will generally be set by plugins, so make sure to check the plugin's documentation for the correct one. ::: :::danger Event namespace directories are **not** the same as [command categories](../02-commands/07-category-directory.mdx). ::: ## How plugins execute namespaced events If you're developing a plugin, you can trigger a custom event in your application. Here's an example of how you can trigger a custom `guildMemberBoost` event under the `custom` namespace. ```ts commandkit.events.to('custom').emit('guildMemberBoost', { member: member, guild: guild, }); ``` This will ensure every file inside the `src/app/events/(custom)/guildMemberBoost` directory is executed just like regular [Discord.js events](./01-discordjs-events.mdx) --- ## stopEvents Function The `stopEvents()` function is a utility function that allows you to stop the execution of the events chain in CommandKit. This is useful when you want to prevent further event handlers from being executed after a certain point. It is typically used in the context of event handlers to control the flow of execution. ## Usage ```ts title="src/app/events/messageCreate/event.ts" import { type EventHandler, stopEvents } from 'commandkit'; const handler: EventHandler<'messageCreate'> = (message) => { console.log('Message received:', message.content); // Stop further event handlers of messageCreate from being executed after this point stopEvents(); // code below, and the rest of the event handlers, will not be executed }; export default handler; ``` :::warning Calling `stopEvents()` in a try/catch block may lead to unexpected behavior. If you still want to use `stopEvents()` in a try/catch block, you can use the `isErrorType` function to check if the error is an instance of the `CommandKitErrorCodes.StopEvents` error. ```ts title="src/app/events/messageCreate/event.ts" import { type EventHandler, stopEvents, isErrorType, CommandKitErrorCodes, } from 'commandkit'; const handler: EventHandler<'messageCreate'> = (message) => { try { // code that may throw an error stopEvents(); // conditionally stop the event chain } catch (error) { if (isErrorType(error, CommandKitErrorCodes.StopEvents)) { // if stopEvents() is called in the try block, throw it so CommandKit can stop the event chain throw error; } // this means that the code threw the error, and stopEvents() was not called // the rest of the event handlers will be executed as normal console.error(error); } }; export default handler; ``` ::: --- ## Using JSX import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; CommandKit provides first-class JSX support for both TypeScript and JavaScript projects. This allows you to write Discord message components using a familiar and intuitive syntax if you're already familiar with React. ## Setup CommandKit automatically configures JSX support in your project. If you are doing a manual setup, ensure you have the following in your configuration: ```json title="tsconfig.json" { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "commandkit" } } ``` ```json title="jsconfig.json" { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "commandkit" } } ``` ## Available components CommandKit provides JSX versions for the following Discord.js builders: - [`ActionRow`](./02-interactive-components/01-action-row.mdx) - [`Button`](./02-interactive-components/02-button.mdx) - [`SelectMenu`](./02-interactive-components/03-select-menu.mdx) - [`Modal`](./02-interactive-components/04-modal.mdx) - [`TextDisplay`](./03-display-components/01-text-display.mdx) - [`Container`](./03-display-components/02-container.mdx) - [`MediaGallery`](./03-display-components/03-media-gallery.mdx) - [`Separator`](./03-display-components/04-separator.mdx) - [`File`](./03-display-components/05-file.mdx) - [`Section`](./03-display-components/06-section.mdx) ## Example usage ```tsx title="example.tsx" import { Container, TextDisplay, ActionRow, Button } from 'commandkit'; import { ButtonStyle } from 'discord.js'; const message = ( Welcome to our server! ); interaction.reply({ components: [message], }); ``` ```jsx title="example.jsx" import { Container, TextDisplay, ActionRow, Button } from 'commandkit'; import { ButtonStyle } from 'discord.js'; const message = ( Welcome to our server! ); interaction.reply({ components: [message], }); ``` ## JSX fragments CommandKit fully supports JSX fragments for grouping multiple elements without adding extra nodes: ```tsx title="fragments.tsx" const elements = ( <> First message Second message ); interaction.reply({ components: elements, }); ``` ```jsx title="fragments.jsx" const elements = ( <> First message Second message ); interaction.reply({ components: elements, }); ``` --- ## Action Row The `ActionRow` component is a container that groups interactive components like buttons and select menus together in a single row. ## Basic usage ```tsx title="src/app/commands/example.tsx" import { type ChatInputCommand, ActionRow, Button } from 'commandkit'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const row = ( ); await interaction.reply({ content: 'Choose an option:', components: [row], }); }; ``` ## With select menus ```tsx title="src/app/commands/select-example.tsx" import { type ChatInputCommand, ActionRow, StringSelectMenu, StringSelectMenuOption, } from 'commandkit'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const row = ( ); await interaction.reply({ content: 'Select from the dropdown:', components: [row], }); }; ``` ## Multiple action rows You can use multiple ActionRows in a single message to organize your components: ```tsx title="src/app/commands/multiple-rows.tsx" import { type ChatInputCommand, ActionRow, Button } from 'commandkit'; import { ButtonStyle } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const firstRow = ( ); const secondRow = ( ); await interaction.reply({ content: 'Multiple action rows:', components: [firstRow, secondRow], }); }; ``` --- ## Button The `Button` component allows you to create interactive buttons in your Discord messages. Buttons can have different styles, labels, and emojis. ## Basic usage ```tsx title="src/app/commands/button-example.tsx" import { type ChatInputCommand, ActionRow, Button } from 'commandkit'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const buttonRow = ( ); await interaction.reply({ content: "Here's a button:", components: [buttonRow], }); }; ``` ## Button styles Discord provides several button styles that you can import from the [`discord.js`](https://discord.js.org) package: ```tsx title="src/app/commands/button-styles.tsx" import { type ChatInputCommand, ActionRow, Button } from 'commandkit'; import { ButtonStyle } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const buttons = ( ); await interaction.reply({ content: 'Different button styles:', components: [buttons], }); }; ``` ## Link buttons Link buttons redirect users to external URLs: ```tsx title="src/app/commands/link-button.tsx" import { type ChatInputCommand, ActionRow, Button } from 'commandkit'; import { ButtonStyle } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const linkButton = ( ); await interaction.reply({ content: 'Click to visit Discord:', components: [linkButton], }); }; ``` ## Buttons with emojis Add emojis to make your buttons more visually appealing: ```tsx title="src/app/commands/emoji-button.tsx" import { type ChatInputCommand, ActionRow, Button } from 'commandkit'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const emojiButton = ( ); await interaction.reply({ content: 'Buttons with emojis:', components: [emojiButton], }); }; ``` ## Disabled buttons Disable buttons to prevent interaction: ```tsx title="src/app/commands/disabled-button.tsx" import { type ChatInputCommand, ActionRow, Button } from 'commandkit'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const disabledButton = ( ); await interaction.reply({ content: 'Disabled vs enabled buttons:', components: [disabledButton], }); }; ``` ## Handling button clicks Handle button interactions with the `onClick` prop: ```tsx title="src/app/commands/interactive-button.tsx" import { type ChatInputCommand, type OnButtonKitClick, ActionRow, Button, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleClick: OnButtonKitClick = async (interaction, context) => { await interaction.reply({ content: 'Button clicked!', flags: MessageFlags.Ephemeral, }); // Clean up the button context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const interactiveButton = ( ); await interaction.reply({ content: 'Click the button to see it work:', components: [interactiveButton], }); }; ``` --- ## Select Menu CommandKit provides several types of select menus for different use cases. ## String select menu The most common type of select menu that allows users to select from predefined string options: ```tsx title="src/app/commands/string-select.tsx" import { type ChatInputCommand, type OnStringSelectMenuKitSubmit, ActionRow, StringSelectMenu, StringSelectMenuOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSelect: OnStringSelectMenuKitSubmit = async ( interaction, context, ) => { const selection = interaction.values[0]; await interaction.reply({ content: `You selected: ${selection}`, flags: MessageFlags.Ephemeral, }); // Clean up the select menu context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const selectMenu = ( ); await interaction.reply({ content: 'Please make a selection:', components: [selectMenu], }); }; ``` ## Channel select menu Allows users to select a channel from the server: ```tsx title="src/app/commands/channel-select.tsx" import { type ChatInputCommand, type OnChannelSelectMenuKitSubmit, ActionRow, ChannelSelectMenu, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSelect: OnChannelSelectMenuKitSubmit = async ( interaction, context, ) => { const channel = interaction.values[0]; await interaction.reply({ content: `Selected channel: <#${channel}>`, flags: MessageFlags.Ephemeral, }); // Clean up the select menu context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const selectMenu = ( ); await interaction.reply({ content: 'Select a channel:', components: [selectMenu], }); }; ``` ## Role select menu Allows users to select a role from the server: ```tsx title="src/app/commands/role-select.tsx" import { type ChatInputCommand, type OnRoleSelectMenuKitSubmit, ActionRow, RoleSelectMenu, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSelect: OnRoleSelectMenuKitSubmit = async ( interaction, context, ) => { const role = interaction.values[0]; await interaction.reply({ content: `Selected role: <@&${role}>`, flags: MessageFlags.Ephemeral, }); // Clean up the select menu context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const selectMenu = ( ); await interaction.reply({ content: 'Select a role:', components: [selectMenu], }); }; ``` ## User select menu Allows users to select a user from the server: ```tsx title="src/app/commands/user-select.tsx" import { type ChatInputCommand, type OnUserSelectMenuKitSubmit, ActionRow, UserSelectMenu, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSelect: OnUserSelectMenuKitSubmit = async ( interaction, context, ) => { const user = interaction.values[0]; await interaction.reply({ content: `Selected user: <@${user}>`, flags: MessageFlags.Ephemeral, }); // Clean up the select menu context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const selectMenu = ( ); await interaction.reply({ content: 'Select a user:', components: [selectMenu], }); }; ``` ## Mentionable select menu Allows users to select a user or role from the server: ```tsx title="src/app/commands/mentionable-select.tsx" import { type ChatInputCommand, type OnMentionableSelectMenuKitSubmit, ActionRow, MentionableSelectMenu, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSelect: OnMentionableSelectMenuKitSubmit = async ( interaction, context, ) => { const mentionable = interaction.values[0]; await interaction.reply({ content: `Selected: <@${mentionable}>`, flags: MessageFlags.Ephemeral, }); // Clean up the select menu context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const selectMenu = ( ); await interaction.reply({ content: 'Select a user or role:', components: [selectMenu], }); }; ``` ## Multiple selection You can allow multiple selections by setting the `minValues` and `maxValues` properties: ```tsx title="src/app/commands/multi-select.tsx" import { type ChatInputCommand, type OnStringSelectMenuKitSubmit, ActionRow, StringSelectMenu, StringSelectMenuOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSelect: OnStringSelectMenuKitSubmit = async ( interaction, context, ) => { const selections = interaction.values; await interaction.reply({ content: `You selected: ${selections.join(', ')}`, flags: MessageFlags.Ephemeral, }); // Clean up the select menu context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const selectMenu = ( ); await interaction.reply({ content: 'Select your favorite foods (1-3 options):', components: [selectMenu], }); }; ``` --- ## Modal The `Modal` component allows you to create interactive forms that appear as popups in Discord. ## Basic usage ```tsx title="src/app/commands/user-profile.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, ShortInput, ParagraphInput, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const name = interaction.fields.getTextInputValue('name'); const description = interaction.fields.getTextInputValue('description'); await interaction.reply({ content: `**Profile Created!**\n**Name:** ${name}\n**Description:** ${description}`, flags: MessageFlags.Ephemeral, }); // Clean up the modal context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` --- ## Poll The `Poll` component allows you to create interactive polls in Discord messages. Users can vote on poll questions with multiple answer options, and the results are displayed in real-time. Polls can be open for up to 32 days (768 hours). ## Basic usage ```tsx title="src/app/commands/poll-example.tsx" import { CommandData, Poll, PollQuestion, PollAnswer, ChatInputCommand, } from 'commandkit'; import { PollData } from 'discord.js'; export const command: CommandData = { name: 'poll', description: 'Create a poll', }; export const chatInput: ChatInputCommand = async (ctx) => { const poll: PollData = ( What's your favorite programming language? JavaScript TypeScript Python Rust ); await ctx.interaction.reply({ poll }); }; ``` ## Poll duration Set how long the poll should be active (in hours). The duration defaults to 24 hours and can be up to 32 days (768 hours): ```tsx title="src/app/commands/poll-duration.tsx" import { CommandData, Poll, PollQuestion, PollAnswer, ChatInputCommand, } from 'commandkit'; import { PollData } from 'discord.js'; export const command: CommandData = { name: 'quickpoll', description: 'Create a quick poll', }; export const chatInput: ChatInputCommand = async (ctx) => { const poll: PollData = ( Quick question: Coffee or tea? Coffee Tea ); await ctx.interaction.reply({ poll }); }; ``` For longer polls, you can specify durations up to 32 days: ```tsx title="src/app/commands/long-poll.tsx" import { CommandData, Poll, PollQuestion, PollAnswer, ChatInputCommand, } from 'commandkit'; import { PollData } from 'discord.js'; export const command: CommandData = { name: 'weeklypoll', description: 'Create a weekly poll', }; export const chatInput: ChatInputCommand = async (ctx) => { const poll: PollData = ( Weekly poll: What should we work on next? New features Bug fixes Documentation UI improvements ); await ctx.interaction.reply({ poll }); }; ``` ## Multiple choice polls Allow users to select multiple answers: ```tsx title="src/app/commands/multiselect-poll.tsx" import { CommandData, Poll, PollQuestion, PollAnswer, ChatInputCommand, } from 'commandkit'; import { PollData } from 'discord.js'; export const command: CommandData = { name: 'multiselect', description: 'Create a multiple choice poll', }; export const chatInput: ChatInputCommand = async (ctx) => { const poll: PollData = ( Which social media platforms do you use? (Select all that apply) Facebook Twitter Instagram LinkedIn TikTok YouTube ); await ctx.interaction.reply({ poll }); }; ``` ## Poll layout types Customize how your poll appears using different layout types: ```tsx title="src/app/commands/poll-layouts.tsx" import { CommandData, Poll, PollQuestion, PollAnswer, ChatInputCommand, } from 'commandkit'; import { PollData, PollLayoutType } from 'discord.js'; export const command: CommandData = { name: 'layoutpoll', description: 'Create a poll with custom layout', }; export const chatInput: ChatInputCommand = async (ctx) => { const poll: PollData = ( List layout poll First item Second item Third item ); await ctx.interaction.reply({ poll }); }; ``` ## Poll with media Add images or other media to your poll questions: ```tsx title="src/app/commands/media-poll.tsx" import { CommandData, Poll, PollQuestion, PollAnswer, ChatInputCommand, } from 'commandkit'; import { PollData } from 'discord.js'; export const command: CommandData = { name: 'mediapoll', description: 'Create a poll with media', }; export const chatInput: ChatInputCommand = async (ctx) => { const poll: PollData = ( Choose your favorite logo design Design A Design B Design C ); await ctx.interaction.reply({ poll }); }; ``` --- ## Checkbox CommandKit provides `Checkbox`, `CheckboxGroup`, and `CheckboxGroupOption` for building checkbox inputs using Discord's component builders. ## Basic usage Use `CheckboxGroup` when you want the user to pick one or more items from a list of options: ```tsx title="src/app/commands/checkbox-group.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, Label, CheckboxGroup, CheckboxGroupOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { // returns a string[] of all selected checkbox values const preferences = interaction.fields.getCheckboxGroup('notifications'); await interaction.reply({ content: 'Thanks! Preferences saved.', flags: MessageFlags.Ephemeral, }); // Clean up the modal context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` ## Descriptions To provide more context about the options, you can add descriptions to `CheckboxGroupOption`: ```tsx title="src/app/commands/checkbox-descriptions.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, Label, CheckboxGroup, CheckboxGroupOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const preferences = interaction.fields.getCheckboxGroup('features'); await interaction.reply({ content: 'Saved!', flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` ## Default selections Use `default` on `CheckboxGroupOption` to pre-select options: ```tsx title="src/app/commands/default-checkboxes.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, Label, CheckboxGroup, CheckboxGroupOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const preferences = interaction.fields.getCheckboxGroup('topics'); await interaction.reply({ content: 'Saved!', flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` ## Min/max values You can enforce how many options the user must pick via `minValues` and `maxValues`: ```tsx title="src/app/commands/checkbox-limits.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, Label, CheckboxGroup, CheckboxGroupOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const preferences = interaction.fields.getCheckboxGroup('topics'); await interaction.reply({ content: 'Saved!', flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` ## Single checkbox Use `Checkbox` for a single boolean input. It can be useful when you want a simple "yes/no" toggle: ```tsx title="src/app/commands/single-checkbox.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, Label, Checkbox, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { // returns a boolean indicating whether the checkbox is checked const agree = interaction.fields.getCheckbox('agree'); await interaction.reply({ content: 'Saved!', flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` --- ## Radio CommandKit provides `RadioGroup` and `RadioGroupOption` for building a single-choice input (exactly one option) using Discord's component builders. ## Basic usage Use `RadioGroup` when you want the user to pick exactly one item from a list: ```tsx title="src/app/commands/radio-group.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, Label, RadioGroup, RadioGroupOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { // returns the selected option's value const plan = interaction.fields.getRadioGroup('plan', true); await interaction.reply({ content: `Selected plan: ${plan}`, flags: MessageFlags.Ephemeral, }); // Clean up the modal context context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` ## Descriptions To add more context to each option, use the `description` prop on `RadioGroupOption`. This will render a smaller text below the option label, giving users more information to make their choice: ```tsx title="src/app/commands/radio-descriptions.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, Label, RadioGroup, RadioGroupOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const choice = interaction.fields.getRadioGroup('choice', true); await interaction.reply({ content: `You chose: ${choice}`, flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` ## Default selection Use `default` on `RadioGroupOption` to pre-select an option: ```tsx title="src/app/commands/default-radio.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, Label, RadioGroup, RadioGroupOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const frequency = interaction.fields.getRadioGroup('frequency', true); await interaction.reply({ content: `Notification frequency: ${frequency}`, flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` ## Required vs optional If `required` is set, users must pick an option before submitting the modal. When `required` is not set (or is `false`), you can read the value as `string | null`: ```tsx title="src/app/commands/optional-radio.tsx" import { type ChatInputCommand, type OnModalKitSubmit, Modal, Label, RadioGroup, RadioGroupOption, } from 'commandkit'; import { MessageFlags } from 'discord.js'; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const choice = interaction.fields.getRadioGroup('choice'); await interaction.reply({ content: `Choice: ${choice ?? 'skipped'}`, flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const modal = ( ); await interaction.showModal(modal); }; ``` --- ## Text Display The `TextDisplay` component is the most basic component that allows you to show text content in your Discord messages. ## Basic usage ```tsx title="src/app/commands/text-example.tsx" import { type ChatInputCommand, TextDisplay } from 'commandkit'; import { MessageFlags } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const textComponent = ; await interaction.reply({ components: [textComponent], flags: MessageFlags.IsComponentsV2, }); }; ``` --- ## Container The `Container` component is a building block that allows you to organize and style multiple components together. ## Basic usage ```tsx title="src/app/commands/container-example.tsx" import { type ChatInputCommand, Container, TextDisplay } from 'commandkit'; import { Colors, MessageFlags } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const container = ( ); await interaction.reply({ components: [container], flags: MessageFlags.IsComponentsV2, }); }; ``` ## Container styling The `Container` component accepts an `accentColor` prop that can be used to customize its appearance: ```tsx title="src/app/commands/styled-container.tsx" import { type ChatInputCommand, Container, TextDisplay } from 'commandkit'; import { Colors, MessageFlags } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const containers = [ , , , , ]; await interaction.reply({ components: containers, flags: MessageFlags.IsComponentsV2, }); }; ``` ## Nesting components You can nest various components inside a Container: ```tsx title="src/app/commands/nested-container.tsx" import { type ChatInputCommand, Container, TextDisplay, Separator, } from 'commandkit'; import { Colors, MessageFlags, SeparatorSpacingSize } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const container = ( ); await interaction.reply({ components: [container], flags: MessageFlags.IsComponentsV2, }); }; ``` ## Multiple containers You can use multiple containers to organize different sections: ```tsx title="src/app/commands/multi-container.tsx" import { type ChatInputCommand, Container, TextDisplay } from 'commandkit'; import { Colors, MessageFlags } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const headerContainer = ( `} /> ); const statsContainer = ( ); const alertsContainer = ( ); await interaction.reply({ components: [headerContainer, statsContainer, alertsContainer], flags: MessageFlags.IsComponentsV2, }); }; ``` --- ## Media Gallery The `MediaGallery` component allows you to display multiple media items in a grid layout. ## Basic usage ```tsx title="src/app/commands/gallery-example.tsx" import { type ChatInputCommand, MediaGallery, MediaGalleryItem, TextDisplay, } from 'commandkit'; import { MessageFlags } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const images = [ 'https://cdn.discordapp.com/embed/avatars/0.png', 'https://cdn.discordapp.com/embed/avatars/1.png', 'https://cdn.discordapp.com/embed/avatars/2.png', ]; const components = [ , {images.map((url, index) => ( ))} , ]; await interaction.reply({ components: components, flags: MessageFlags.IsComponentsV2, }); }; ``` --- ## Separator The `Separator` component creates visual spacing between sections in your message components. ## Basic usage ```tsx title="src/app/commands/separator-example.tsx" import { type ChatInputCommand, Container, TextDisplay, Separator, } from 'commandkit'; import { Colors, MessageFlags, SeparatorSpacingSize } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const container = ( ); await interaction.reply({ components: [container], flags: MessageFlags.IsComponentsV2, }); }; ``` --- ## File The `File` component allows you to display file attachments in your Discord messages. ## Basic usage ```tsx title="src/app/commands/file-example.tsx" import { type ChatInputCommand, Container, File, TextDisplay, } from 'commandkit'; import { AttachmentBuilder, Colors, MessageFlags } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const fileContent = '# Hello World\nThis is a test markdown file.'; const container = ( ); await interaction.reply({ components: [container], files: [ new AttachmentBuilder(Buffer.from(fileContent), { name: 'example.md', }), ], flags: MessageFlags.IsComponentsV2, }); }; ``` --- ## Section The `Section` component allows you to combine a text component with an accessory component. ## Basic usage ```tsx title="src/app/commands/section-example.tsx" import { type ChatInputCommand, Section, TextDisplay, Thumbnail, } from 'commandkit'; import { MessageFlags } from 'discord.js'; export const chatInput: ChatInputCommand = async ({ interaction }) => { const section = (
); await interaction.reply({ components: [section], flags: MessageFlags.IsComponentsV2, }); }; ``` --- ## Label The `Label` component wraps modal components with text as a label and optional description, providing better organization and context for form elements. ## Basic usage ```tsx title="src/app/commands/label-example.tsx" import { CommandData, Modal, ShortInput, Label, OnModalKitSubmit, ChatInputCommandContext, } from 'commandkit'; import { MessageFlags } from 'discord.js'; export const command: CommandData = { name: 'profile', description: 'Create your profile', }; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const name = interaction.fields.getTextInputValue('name'); await interaction.reply({ content: `Hello, ${name}!`, flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommandContext = async (ctx) => { const modal = ( ); await ctx.interaction.showModal(modal); }; ``` ## With description Labels can include descriptive text to provide additional context: ```tsx title="src/app/commands/label-description.tsx" import { CommandData, Modal, ShortInput, ParagraphInput, Label, OnModalKitSubmit, ChatInputCommandContext, } from 'commandkit'; import { MessageFlags } from 'discord.js'; export const command: CommandData = { name: 'profile', description: 'Create your profile', }; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const username = interaction.fields.getTextInputValue('username'); const bio = interaction.fields.getTextInputValue('bio'); await interaction.reply({ content: `**Username:** ${username}\n**Bio:** ${bio}`, flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommandContext = async (ctx) => { const modal = ( ); await ctx.interaction.showModal(modal); }; ``` ## Multiple labels Use multiple labels to organize different sections of your modal: ```tsx title="src/app/commands/multiple-labels.tsx" import { CommandData, Modal, ShortInput, ParagraphInput, Label, OnModalKitSubmit, ChatInputCommandContext, } from 'commandkit'; import { MessageFlags } from 'discord.js'; export const command: CommandData = { name: 'contact', description: 'Send us a message', }; const handleSubmit: OnModalKitSubmit = async (interaction, context) => { const firstName = interaction.fields.getTextInputValue('firstName'); const lastName = interaction.fields.getTextInputValue('lastName'); const email = interaction.fields.getTextInputValue('email'); const message = interaction.fields.getTextInputValue('message'); await interaction.reply({ content: `**Contact Form Submitted**\n**Name:** ${firstName} ${lastName}\n**Email:** ${email}\n**Message:** ${message}`, flags: MessageFlags.Ephemeral, }); context.dispose(); }; export const chatInput: ChatInputCommandContext = async (ctx) => { const modal = ( ); await ctx.interaction.showModal(modal); }; ``` --- ## @commandkit/ai The `@commandkit/ai` plugin allows you to execute your bot commands using large language models, enabling users to interact with your bot entirely through natural language. ## Architecture overview ```mermaid graph TD A[Discord Message] --> B[Message Filter] B --> C[AI Model Selection] C --> D[System Prompt Generation] D --> E[AI Processing] E --> F[Tool Execution] F --> G[Response Generation] G --> H[Discord Reply] I[Built-in Tools] --> F J[Custom Tools] --> F K[AI Commands] --> F ``` The `@commandkit/ai` plugin basically acts as a smart command handler that decides which command to execute based on the given prompt. Similarly, the AI commands are basically an abstraction for tool calling feature of the large language models. This plugin uses the [AI SDK](https://ai-sdk.dev) under the hood to interact with large language models. ## Installation Install the plugin and your preferred AI SDK: ```bash npm2yarn npm install @commandkit/ai ``` You also need to install the AI SDK for the model you want to use. For example, if you want to use Google Gemini: ```bash npm2yarn npm install @ai-sdk/google ``` Refer to the [AI SDK documentation](https://ai-sdk.dev) for more information on setting up different AI providers. ## Basic setup First, add the AI plugin to your CommandKit configuration: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { ai } from '@commandkit/ai'; export default defineConfig({ plugins: [ai()], }); ``` Then create an AI configuration file to set up your AI model. This can be done in two ways, one via CLI and another manually. ### With CLI Use the following command to automatically generate `ai.ts` (or js) file in the project: ```sh commandkit create ai ``` This command creates `ai.ts` (or js) file inside the `src` directory with openai o3-mini model. You can change it to any other supported models if needed. ### Manual ```ts title="src/ai.ts" import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { configureAI } from '@commandkit/ai'; const google = createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_API_KEY, }); configureAI({ selectAiModel: async (ctx, message) => ({ model: google.languageModel('gemini-2.0-flash'), maxSteps: 5, temperature: 0.7, }), messageFilter: async (commandkit, message) => { // Only respond when the bot is mentioned return message.mentions.users.has(message.client.user.id); }, }); ``` The AI plugin will load the `ai.ts` (or js) file automatically. ## Creating AI commands To make a command accessible to AI, export an `ai` function and optional `aiConfig` object from your command file: ```ts title="src/app/commands/greet.ts" import type { CommandData, ChatInputCommand } from 'commandkit'; import type { AiConfig, AiCommand } from '@commandkit/ai'; import { z } from 'zod'; export const command: CommandData = { name: 'greet', description: 'Greet a user', }; export const aiConfig: AiConfig = { inputSchema: z.object({ username: z.string().describe('The username to greet'), message: z.string().optional().describe('Optional custom greeting message'), }), }; export const chatInput: ChatInputCommand = async (ctx) => { const { interaction } = ctx; // Handle slash command normally await interaction.reply('Hello!'); }; // AI will call this function with natural language parameters export const ai: AiCommand = async (ctx) => { const { username, message } = ctx.ai.params; const greeting = message || `Hello, ${username}!`; await ctx.message.reply(greeting); return { greeting, username }; }; ``` Now users can interact with your command naturally: ``` @bot greet John with a friendly message @bot say hello to Sarah ``` ## Configuration options The `configureAI` function accepts several configuration options: ### Model selection The `selectAiModel` function is required and determines which AI model to use: ```ts configureAI({ selectAiModel: async (ctx, message) => { // Use different models for different contexts if (message.member?.permissions.has('Administrator')) { return { model: google.languageModel('gemini-2.0-flash'), maxSteps: 10, temperature: 0.6, }; } // Default model for regular users return { model: google.languageModel('gemini-1.5-flash'), maxSteps: 5, temperature: 0.7, }; }, }); ``` ### Message filtering Control which messages trigger AI processing: ```ts messageFilter: async (commandkit, message) => { return ( !message.author.bot && // Ignore bots message.inGuild() && // Only in servers (message.mentions.users.has(message.client.user.id) || // Bot mentions message.content.toLowerCase().includes('hey bot')) // Keywords ); }, ``` ### System prompts Customize the AI's behavior and context: ```ts prepareSystemPrompt: async (ctx, message) => { const serverName = message.guild?.name || 'Direct Message'; return `You are ${message.client.user.username}, a helpful Discord bot. **Context:** - Server: ${serverName} - Channel: ${message.channel.name || 'DM'} **Guidelines:** - Keep responses under 2000 characters - Be helpful and friendly - Use available commands when appropriate`; }, ``` ### Lifecycle hooks Configure functions that run during AI processing: ```ts configureAI({ // Called when AI processing starts onProcessingStart: async (ctx, message) => { await message.channel.sendTyping(); }, // Called with the AI response onResult: async (ctx, message, result) => { if (result.text) { await message.reply({ content: result.text.substring(0, 2000), allowedMentions: { parse: [] }, }); } }, // Called when an error occurs onError: async (ctx, message, error) => { console.error('AI error:', error.message); await message.reply( 'Sorry, I encountered an error processing your request.', ); }, }); ``` ## Built-in tools CommandKit provides built-in tools that the AI can use automatically: - **`getAvailableCommands`** - Lists all available bot commands - **`getChannelById`** - Fetches channel information by ID - **`getCurrentClientInfo`** - Gets information about the bot - **`getGuildById`** - Fetches server information by ID - **`getUserById`** - Fetches user information by ID - **`getMemberById`** - Fetches guild member information by ID - **`createEmbed`** - Creates and sends an embed message ### Disabling built-in tools You can disable all built-in tools if needed: ```ts configureAI({ disableBuiltInTools: true, // ... other options }); ``` ### Selectively enabling built-in tools In many cases, you may not need all built-in tools. You can selectively enable specific tools by disabling built-in tools and then manually adding only the ones you need using the exported `defaultTools` record: ```ts title="src/ai.ts" import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { configureAI, defaultTools } from '@commandkit/ai'; const google = createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_API_KEY, }); configureAI({ // Disable all built-in tools disableBuiltInTools: true, selectAiModel: async (ctx, message) => ({ model: google.languageModel('gemini-2.0-flash'), maxSteps: 5, temperature: 0.7, // Manually add only the tools you need tools: { getAvailableCommands: defaultTools.getAvailableCommands, getUserById: defaultTools.getUserById, getGuildById: defaultTools.getGuildById, // Omit tools you don't want, like createEmbed }, }), messageFilter: async (commandkit, message) => { return message.mentions.users.has(message.client.user.id); }, }); ``` This approach gives you full control over which built-in tools are available to the AI, making your implementation cleaner and more intentional. ## Creating custom tools Extend the AI's capabilities by creating custom tools: ```ts title="src/tools/weather.ts" import { createTool } from '@commandkit/ai'; import { z } from 'zod'; export const getWeather = createTool({ name: 'getWeather', description: 'Get current weather information for a location', inputSchema: z.object({ location: z.string().describe('The city or location to get weather for'), units: z.enum(['celsius', 'fahrenheit']).default('celsius'), }), async execute(ctx, params) { const { location, units } = params; try { const weatherData = await fetchWeatherAPI(location); return { location, temperature: units === 'celsius' ? weatherData.tempC : weatherData.tempF, condition: weatherData.condition, humidity: weatherData.humidity, }; } catch (error) { return { error: `Failed to get weather: ${error.message}`, }; } }, }); ``` Register your custom tools with the AI model: ```ts title="src/ai.ts" import { getWeather } from './tools/weather'; configureAI({ selectAiModel: async (ctx, message) => ({ model: google.languageModel('gemini-2.0-flash'), tools: { getWeather, // Add your custom tools here }, }), }); ``` ## AI context and hooks The AI system provides helpful context and hooks for accessing the current AI execution environment: ### Using AI context ```ts export const ai: AiCommand = async (ctx) => { // AI-generated parameters based on your schema const params = ctx.ai.params; // Original Discord message that triggered the AI const message = ctx.message; // CommandKit instance const commandkit = ctx.commandkit; // Key-value store for temporary data ctx.store.set('startTime', Date.now()); const startTime = ctx.store.get('startTime'); }; ``` ### Using hooks Access the AI context from anywhere in your code: ```ts import { useAIContext } from '@commandkit/ai'; function someUtilityFunction() { try { const ctx = useAIContext(); console.log('Current AI parameters:', ctx.ai.params); return ctx.message.author.id; } catch (error) { // Not in an AI context return null; } } ``` ## Multiple AI providers You can configure multiple AI providers and choose between them dynamically: ```ts title="src/ai.ts" import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { createOpenAI } from '@ai-sdk/openai'; const google = createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_API_KEY, }); const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY, }); configureAI({ selectAiModel: async (ctx, message) => { // Use different models for different channels if (message.channelId === 'premium-channel-id') { return { model: openai('gpt-4'), maxSteps: 10, }; } // Default to Google Gemini return { model: google.languageModel('gemini-2.0-flash'), maxSteps: 5, }; }, }); ``` ## Best practices ### Security Always validate user permissions before executing sensitive operations: ```ts export const ai: AiCommand = async (ctx) => { const { action } = ctx.ai.params; // Check permissions if (!ctx.message.member?.permissions.has('ManageMessages')) { await ctx.message.reply( '❌ You need "Manage Messages" permission to use this command.', ); return; } // Validate AI-generated parameters if (action === 'ban' && !isModeratorRole(ctx.message.member)) { await ctx.message.reply('❌ Only moderators can perform ban actions.'); return; } // Proceed with the action }; ``` ### Rate limiting Implement rate limiting to prevent abuse: ```ts const userCooldowns = new Map(); const COOLDOWN_DURATION = 30000; // 30 seconds export const ai: AiCommand = async (ctx) => { const userId = ctx.message.author.id; const now = Date.now(); const cooldownEnd = userCooldowns.get(userId) || 0; if (now < cooldownEnd) { const remaining = Math.ceil((cooldownEnd - now) / 1000); await ctx.message.reply( `⏰ Please wait ${remaining} seconds before using this command again.`, ); return; } userCooldowns.set(userId, now + COOLDOWN_DURATION); // Process command await processCommand(ctx); }; ``` ### Error handling Implement robust error handling: ```ts export const ai: AiCommand = async (ctx) => { try { const result = await performOperation(ctx.ai.params); await ctx.message.reply(`✅ ${result}`); } catch (error) { console.error('AI command error:', { command: 'example', userId: ctx.message.author.id, error: error.message, }); await ctx.message.reply('❌ An unexpected error occurred.'); } }; ``` ## Troubleshooting ### AI not responding If the AI isn't responding to messages: 1. **Check the AI plugin is registered** in your `commandkit.config.ts` 2. **Verify your message filter** isn't too restrictive 3. **Ensure required Discord intents** are enabled (especially `MessageContent`) ### Model configuration errors 1. **Check API keys** are set in environment variables 2. **Verify model initialization** doesn't throw errors 3. **Handle model availability** gracefully ### Parameter issues 1. **Check Zod schema definitions** match expected parameters 2. **Add parameter validation** in your AI commands 3. **Improve parameter descriptions** for better AI understanding For more detailed troubleshooting, check the console logs for specific error messages and verify your environment configuration. ## Environment configuration Configure different settings for development and production: ```ts title="src/ai.ts" const isDevelopment = process.env.NODE_ENV === 'development'; configureAI({ selectAiModel: async (ctx, message) => ({ model: google.languageModel( isDevelopment ? 'gemini-1.5-flash' : 'gemini-2.0-flash', ), maxSteps: isDevelopment ? 3 : 8, temperature: isDevelopment ? 0.8 : 0.6, }), onError: async (ctx, message, error) => { if (isDevelopment) { // Show detailed errors in development await message.reply(`Debug Error: ${error.message}`); } else { // Generic error in production await message.reply('An error occurred while processing your request.'); } }, }); ``` --- ## @commandkit/analytics CommandKit provides a powerful analytics system that helps you track events and metrics in your Discord bot while maintaining user privacy. The `@commandkit/analytics` plugin acts as a bridge between your application and your chosen analytics provider, without storing any data itself. ## Installation Install the analytics package to get started: ```bash npm2yarn npm install @commandkit/analytics ``` ## How it works CommandKit's analytics system is designed to be provider-agnostic and privacy-focused: 1. **No data storage**: CommandKit doesn't store any analytics data itself 2. **Direct transmission**: All data is sent directly to your configured analytics provider 3. **Multiple providers**: You can easily switch between different providers or use multiple providers simultaneously 4. **Extensible**: The system allows you to create custom providers for your specific needs 5. **Anonymous tracking**: Focus on anonymous, aggregated metrics to respect user privacy ## Automatic tracking CommandKit automatically tracks various anonymous metrics when analytics is enabled: - **Command execution events** (`command_execution`) - **Cache performance metrics**: - Cache hits (`cache_hit`) - Cache misses (`cache_miss`) - Cache revalidations (`cache_revalidated`) - **Feature flag metrics**: - Feature flag metrics (`feature_flag_metrics`) - Feature flag decisions (`feature_flag_decision`) - **Anonymous interaction patterns** ## Available providers CommandKit comes with built-in support for popular analytics providers: ### PostHog PostHog is an open-source product analytics platform that helps you understand user behavior: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { posthog } from '@commandkit/analytics/posthog'; export default defineConfig({ plugins: [ posthog({ posthogOptions: { apiKey: 'YOUR_POSTHOG_API_KEY', }, }), ], }); ``` ### Umami Umami is a simple, fast, privacy-focused alternative to Google Analytics: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { umami } from '@commandkit/analytics/umami'; export default defineConfig({ plugins: [ umami({ umamiOptions: { hostUrl: 'YOUR_UMAMI_HOST_URL', websiteId: 'YOUR_UMAMI_WEBSITE_ID', // Optional: Additional configuration sessionId: 'YOUR_UMAMI_SESSION_ID', userAgent: 'YOUR_UMAMI_USER_AGENT', }, }), ], }); ``` ## Basic usage Track custom events in your Discord bot using the `track` function: ```ts title="src/app/commands/ban.ts" import { track } from 'commandkit/analytics'; import type { ChatInputCommand, CommandData } from 'commandkit'; export const command: CommandData = { name: 'ban', description: 'Ban a user from the server', options: [ { name: 'user', description: 'The user to ban', type: 6, // User required: true, }, ], }; export const chatInput: ChatInputCommand = async ({ interaction }) => { const user = interaction.options.getUser('user', true); // Perform the ban await interaction.guild?.members.ban(user); // Track the moderation action with anonymous data await track({ name: 'moderation_action', data: { actionType: 'ban', // Track performance metrics responseTime: Date.now() - interaction.createdTimestamp, // Track anonymous usage patterns timeOfDay: new Date().getHours(), dayOfWeek: new Date().getDay(), }, }); await interaction.reply(`Successfully banned ${user.tag}`); }; ``` ## Identifying anonymous sessions You can identify anonymous sessions in your analytics provider using the `identify` function: ```ts import { useAnalytics } from 'commandkit/analytics'; const analytics = useAnalytics(); await analytics.identify({ // Use a hashed or anonymous identifier distinctId: 'session_' + Math.random().toString(36).substring(7), properties: { // Track anonymous session properties sessionStart: Date.now(), environment: process.env.NODE_ENV, version: '1.0.0', }, }); ``` ## Filtering events You can filter out specific events using the `setFilter` function: ```ts import { useAnalytics } from 'commandkit/analytics'; const analytics = useAnalytics(); // Filter out events based on anonymous criteria analytics.setFilter((engine, event) => { // Skip events from development environments if (process.env.NODE_ENV === 'development') { return false; // false means skip this event } // Skip specific event types if (event.name === 'debug_event') { return false; } return true; // true means track this event }); ``` ## Disabling analytics You can disable analytics for specific requests using the `noAnalytics` function: ```ts import { noAnalytics } from 'commandkit/analytics'; // Disable analytics for specific commands or events if (interaction.commandName === 'debug') { noAnalytics(); } ``` ## Creating custom providers You can create your own analytics provider by implementing the `AnalyticsProvider` interface: ```ts title="src/providers/custom-analytics.ts" import { AnalyticsProvider, AnalyticsEvent, IdentifyEvent, } from 'commandkit/analytics'; class CustomAnalyticsProvider implements AnalyticsProvider { readonly name = 'custom-analytics'; constructor(private readonly analytics: YourAnalyticsService) {} async track(engine: AnalyticsEngine, event: AnalyticsEvent): Promise { // Implement your tracking logic here const { name, data } = event; // Example: Send anonymous data to your analytics service await this.analytics.track({ name, // Only include anonymous data data: { ...data, // Add anonymous session info sessionId: this.generateSessionId(), timestamp: Date.now(), }, }); } async identify(engine: AnalyticsEngine, event: IdentifyEvent): Promise { // Implement anonymous session identification await this.analytics.identify({ // Use anonymous session identifiers sessionId: this.generateSessionId(), timestamp: Date.now(), // Include anonymous session properties properties: { environment: process.env.NODE_ENV, version: '1.0.0', }, }); } private generateSessionId(): string { return 'session_' + Math.random().toString(36).substring(7); } } ``` ### Creating a plugin Create a plugin class that extends `RuntimePlugin`: ```ts title="src/plugins/custom-analytics-plugin.ts" import { CommandKitPluginRuntime, RuntimePlugin } from 'commandkit/plugin'; import { CustomAnalyticsProvider } from '../providers/custom-analytics'; export interface CustomAnalyticsPluginOptions { analyticsOptions: { apiKey: string; options?: YourAnalyticsOptions; }; } export class CustomAnalyticsPlugin extends RuntimePlugin { public readonly name = 'CustomAnalytics'; private provider: CustomAnalyticsProvider | null = null; public async activate(ctx: CommandKitPluginRuntime): Promise { const analytics = new YourAnalyticsService( this.options.analyticsOptions.apiKey, this.options.analyticsOptions.options, ); this.provider = new CustomAnalyticsProvider(analytics); ctx.commandkit.analytics.registerProvider(this.provider); } public async deactivate(ctx: CommandKitPluginRuntime): Promise { if (!this.provider) return; // Cleanup if needed await this.provider.analytics.shutdown?.(); ctx.commandkit.analytics.removeProvider(this.provider); } } ``` ### Using your custom provider Add your custom provider to your CommandKit configuration: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { CustomAnalyticsPlugin } from './src/plugins/custom-analytics-plugin'; export default defineConfig({ plugins: [ new CustomAnalyticsPlugin({ analyticsOptions: { apiKey: 'YOUR_API_KEY', options: { // Your analytics service options }, }, }), ], }); ``` ## Privacy considerations When implementing analytics in your bot, always consider: 1. **Anonymous data only**: Only track anonymous, aggregated data 2. **No personal information**: Avoid storing personal information 3. **Hashed identifiers**: Use hashed identifiers when necessary 4. **Transparency**: Be transparent about what data you collect 5. **Discord compliance**: Respect user privacy and Discord's Terms of Service 6. **Data minimization**: Only collect data that's necessary for your use case ## Common use cases ### Performance monitoring ```ts await track({ name: 'command_performance', data: { commandName: 'complex-calculation', executionTime: Date.now() - startTime, memoryUsage: process.memoryUsage().heapUsed, }, }); ``` ### Feature usage analytics ```ts await track({ name: 'feature_usage', data: { featureName: 'auto-moderation', configurationUsed: 'strict', triggerCount: violations.length, }, }); ``` ### Error tracking ```ts await track({ name: 'error_occurred', data: { errorType: 'api_timeout', commandContext: 'user-info', recoveryAction: 'retry_successful', }, }); ``` :::tip The `@commandkit/analytics` plugin works seamlessly with other CommandKit features and plugins. You can use it alongside features like caching and feature flags to get comprehensive insights into your bot's performance and usage patterns. ::: --- ## @commandkit/cache CommandKit provides a powerful caching system that helps you optimize your bot's performance by storing frequently accessed data in memory. The `@commandkit/cache` plugin allows you to cache expensive operations like API calls and database queries, significantly improving response times and reducing external service usage. ## Installation Install the cache package to get started: ```bash npm2yarn npm install @commandkit/cache ``` ## Setup Add the cache plugin to your CommandKit configuration: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { cache } from '@commandkit/cache'; export default defineConfig({ plugins: [cache()], }); ``` ## Basic usage The simplest way to use caching is with the `"use cache"` directive. This tells CommandKit to cache the result of your function: ```ts title="src/app/commands/weather.ts" import type { ChatInputCommand, CommandData } from 'commandkit'; export const command: CommandData = { name: 'weather', description: 'Get weather information for a city', options: [ { name: 'city', description: 'The city to get weather for', type: 3, // String required: true, }, ], }; async function fetchWeatherData(city: string) { 'use cache'; // This expensive API call will only run once for each unique city const response = await fetch(`https://api.weather.com/v1/current?q=${city}`); return response.json(); } export const chatInput: ChatInputCommand = async ({ interaction }) => { const city = interaction.options.getString('city', true); const weather = await fetchWeatherData(city); await interaction.reply(`Weather in ${city}: ${weather.description}`); }; ``` When you call `fetchWeatherData` multiple times with the same city, it will only perform the API call once and return the cached result for subsequent calls. ## How caching works CommandKit's caching system works by: 1. **Generating a cache key**: Each cached function call generates a unique key based on the function's identity, arguments, and build ID 2. **Storing results**: When a function is called, if the result isn't cached, the function executes and stores its result. If cached, it returns immediately 3. **Automatic cleanup**: The cache system automatically removes stale entries and manages memory usage ## Controlling cache behavior ### Setting cache duration Use `cacheLife` to control how long entries stay in the cache: ```ts import { cacheLife } from '@commandkit/cache'; async function fetchUserData(userId: string) { 'use cache'; // Cache for 1 hour cacheLife('1h'); const userData = await db.users.findOne(userId); return userData; } ``` Supported time formats: - `'5s'` - 5 seconds - `'1m'` - 1 minute - `'2h'` - 2 hours - `'1d'` - 1 day - `60000` - Direct milliseconds ### Using cache tags Use `cacheTag` to group related cache entries for easier management: ```ts import { cacheTag } from '@commandkit/cache'; async function fetchGuildSettings(guildId: string) { 'use cache'; // Tag this entry for easy invalidation cacheTag(`guild:${guildId}`); cacheTag('settings'); cacheLife('1h'); return await db.guilds.findOne(guildId); } ``` ### Invalidating cache entries Use `revalidateTag` to invalidate specific cache entries: ```ts import { revalidateTag } from '@commandkit/cache'; async function updateGuildSettings(guildId: string, settings: any) { // Update the database await db.guilds.update(guildId, settings); // Invalidate all cached guild data await revalidateTag(`guild:${guildId}`); } ``` ## Common use cases ### Caching API responses ```ts async function fetchGameStats(gameId: string) { 'use cache'; cacheTag(`game:${gameId}`); cacheLife('30m'); // API updates every 30 minutes const response = await fetch(`https://api.game.com/stats/${gameId}`); return response.json(); } ``` ### Caching database queries ```ts async function getUserProfile(userId: string) { 'use cache'; cacheTag(`user:${userId}`); cacheLife('1h'); // User profiles don't change often return await db.users.findOne({ where: { id: userId }, include: ['profile', 'settings'], }); } ``` ### Caching computed results ```ts async function calculateUserStats(userId: string) { 'use cache'; cacheTag(`user:${userId}`); cacheTag('stats'); cacheLife('5m'); // Recalculate every 5 minutes const user = await db.users.findOne(userId); return { level: calculateLevel(user.xp), rank: await calculateRank(userId), achievements: await getAchievements(userId), }; } ``` ## Advanced configuration ### Custom cache provider For distributed caching, you can use a custom cache provider like Redis: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { cache, setCacheProvider } from '@commandkit/cache'; import { RedisCache } from '@commandkit/cache/redis'; // Set up Redis as the cache provider setCacheProvider( new RedisCache({ host: 'localhost', port: 6379, }), ); export default defineConfig({ plugins: [cache()], }); ``` ### Manual cache cleanup Clean up stale cache entries to manage memory usage: ```ts import { cleanup } from '@commandkit/cache'; // Clean up entries older than 24 hours await cleanup(24 * 60 * 60 * 1000); // Set up regular cleanup setInterval( async () => { await cleanup(24 * 60 * 60 * 1000); }, 24 * 60 * 60 * 1000, ); // Run daily ``` --- ## @commandkit/devtools The `@commandkit/devtools` plugin provides a comprehensive set of development tools and utilities designed to enhance your CommandKit development experience. It includes features like command inspection, performance monitoring, debugging tools, and a web-based interface for managing your Discord bot during development. :::warning This plugin is deprecated. ::: ## Features - **Command Inspection**: View and analyze all registered commands in your bot - **Performance Monitoring**: Track command execution times and performance metrics - **Debugging Tools**: Enhanced logging and debugging capabilities - **Web Interface**: Browser-based dashboard for managing your bot - **Real-time Updates**: Live updates of bot status and metrics - **Development Utilities**: Additional tools to streamline development workflow ## Installation Install the devtools plugin using your preferred package manager: ```sh npm2yarn npm install @commandkit/devtools ``` ## Setup Add the devtools plugin to your CommandKit configuration: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { devtools } from '@commandkit/devtools'; export default defineConfig({ plugins: [devtools()], }); ``` That's it! The devtools plugin will automatically start when you run your application in development mode. ## Using the devtools Once the plugin is installed and configured, the devtools interface will be available at `http://localhost:4356` when you start your application with `commandkit dev`. ```bash npx commandkit dev ``` The web interface provides access to all devtools features and allows you to: - Monitor your bot's status and performance - Inspect registered commands and their configurations - View real-time logs and debug information - Access development utilities and tools ## Configuration options The devtools plugin accepts several configuration options to customize its behavior: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { devtools } from '@commandkit/devtools'; export default defineConfig({ plugins: [ devtools({ // Custom port for the devtools interface port: 4356, // Enable or disable specific features features: { commandInspection: true, performanceMonitoring: true, debugging: true, }, // Additional configuration options enableLogging: true, }), ], }); ``` ### Configuration properties - `port` (number): The port number for the devtools web interface (default: `4356`) - `features` (object): Enable or disable specific devtools features - `enableLogging` (boolean): Enable enhanced logging capabilities ## Development workflow The devtools plugin integrates seamlessly with CommandKit's development workflow: 1. **Start Development**: Run `commandkit dev` to start your bot in development mode 2. **Access Devtools**: Open `http://localhost:4356` in your browser 3. **Monitor & Debug**: Use the web interface to monitor your bot and debug issues 4. **Hot Reload**: Make changes to your code and see updates in real-time :::tip Development Only The devtools plugin is designed for development use only and should not be included in production builds. Consider using environment-based configuration to conditionally include the plugin: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { devtools } from '@commandkit/devtools'; export default defineConfig({ plugins: [ // Only include devtools in development ...(process.env.NODE_ENV === 'development' ? [devtools()] : []), ], }); ``` ::: ## Preview The devtools interface provides a modern, intuitive dashboard for managing your Discord bot during development: CommandKit DevTools Interface ## Troubleshooting ### Port already in use If port `4356` is already in use, you can specify a different port in the configuration: ```ts devtools({ port: 4357 }); ``` ### Interface not loading Ensure that: 1. The devtools plugin is properly configured 2. Your application is running in development mode 3. No firewall is blocking the specified port 4. You're accessing the correct URL (`http://localhost:4356`) ### Performance issues If you experience performance issues: 1. Disable unnecessary features in the configuration 2. Reduce logging verbosity 3. Consider using a different port ## Feedback and contributions As this plugin is in early development, your feedback is valuable for improving the devtools experience. Please report issues, suggest features, or contribute to the development through: - [GitHub Issues](https://github.com/neplexlabs/commandkit/issues) - [Discord Community](https://commandkit.dev/discord) --- ## @commandkit/i18n import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; The `@commandkit/i18n` plugin integrates [i18next](https://www.i18next.com/) into CommandKit, enabling you to create multilingual Discord bots that automatically adapt to your users' preferred languages. This plugin provides seamless internationalization support for commands, events, and all bot interactions. ## Features - **Automatic locale detection** - Automatically uses Discord's guild preferred locale - **Easy setup** - Simple configuration with sensible defaults - **File-based translations** - Organize translations in JSON files - **Context-aware** - Access translations in commands, events, and legacy handlers - **i18next ecosystem** - Full compatibility with i18next plugins and features - **Command metadata localization** - Localize command names, descriptions, and options ## Installation ```sh npm2yarn npm install @commandkit/i18n ``` ## Basic setup Add the i18n plugin to your CommandKit configuration: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { i18n } from '@commandkit/i18n'; export default defineConfig({ plugins: [i18n()], }); ``` ## Advanced configuration You can customize the i18n plugin by passing options to it: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { i18n } from '@commandkit/i18n'; export default defineConfig({ plugins: [ i18n({ plugins: [someI18nextPlugin], // Add other i18next configuration options as needed }), ], }); ``` ## Translation files structure Create a `locales` directory inside your `src/app` folder with subdirectories for each language. Each language directory should contain JSON files for your translations. ``` src └── app ├── locales │ ├── en-US │ │ └── ping.json │ │ └── messageCreate.event.json │ └── fr │ └── ping.json │ └── messageCreate.event.json ├── commands │ ├── ping.ts │ └── help.ts └── events └── messageCreate └── handler.ts ``` ### Supported locales CommandKit uses Discord's locale identifiers. Please refer to [Discord's Locales documentation](https://discord.com/developers/docs/reference#locales) for a complete list. ## Quick start example Here's a complete example to get you started: 1. **Configure the plugin**: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { i18n } from '@commandkit/i18n'; export default defineConfig({ plugins: [i18n()], }); ``` 2. **Create translation files**: ```json title="src/app/locales/en-US/ping.json" { "$command": { "name": "ping", "description": "Check the bot's latency" }, "response": "🏓 Pong! Latency: **{{latency}}ms**" } ``` ```json title="src/app/locales/fr/ping.json" { "$command": { "name": "ping", "description": "Vérifier la latence du bot" }, "response": "🏓 Pong! Latence: **{{latency}}ms**" } ``` 3. **Use translations in your command**: ```ts title="src/app/commands/ping.ts" import type { ChatInputCommand, CommandData } from 'commandkit'; export const command: CommandData = { name: 'ping', description: "Check the bot's latency", }; export const chatInput: ChatInputCommand = async (ctx) => { const { t } = ctx.locale(); await ctx.interaction.reply({ content: t('response', { latency: ctx.client.ws.ping }), }); }; ``` That's it! Your bot will now automatically respond in the user's guild preferred language. ## Commands localization CommandKit's i18n plugin provides powerful localization features for chat input commands, allowing you to translate command metadata (names, descriptions, options) and responses to match your users' preferred languages. ### Translation file structure Translation files should be placed in your `locales` directory and named after the command they translate. For example, translations for a `ping` command should be in `ping.json`. #### Basic translation file ```json title="src/app/locales/en-US/ping.json" { "response": "🏓 Pong! Latency: **{{latency}}ms**", "error": "❌ Failed to ping the server", "database_response": "📊 Database latency: **{{dbLatency}}ms**" } ``` #### Command metadata localization Use the special `$command` key to localize command metadata that appears in Discord's interface: ```json title="src/app/locales/en-US/ping.json" { "$command": { "name": "ping", "description": "Check the bot's latency and response time", "options": [ { "name": "database", "description": "Also check database connection latency" }, { "name": "target", "description": "Specify a target server to ping", "choices": [ { "name": "Main Server", "value": "main" }, { "name": "Backup Server", "value": "backup" } ] } ] }, "response": "🏓 Pong! Latency: **{{latency}}ms**", "database_response": "📊 Database latency: **{{dbLatency}}ms**" } ``` The `$command` object structure mirrors Discord's application command structure: - `name`: Command name (shown in Discord's command picker) - `description`: Command description (shown in Discord's command picker) - `options`: Array of option localizations - `name`: Option name - `description`: Option description - `choices`: Array of choice localizations (for string options with predefined choices) ### Using translations in commands The `locale()` function in your command context provides access to translations and i18next features: ```ts title="src/app/commands/ping.ts" import type { ChatInputCommand } from 'commandkit'; export const chatInput: ChatInputCommand = async (ctx) => { // Get translation function and i18next instance for the current guild's locale const { t, i18n } = ctx.locale(); const latency = ctx.client.ws.ping; // Use the translation function with interpolation await ctx.interaction.reply({ content: t('response', { latency }), ephemeral: true, }); }; ``` #### Manual locale override You can specify a particular locale instead of using the guild's preferred locale: ```ts export const chatInput: ChatInputCommand = async (ctx) => { // Force French locale const { t } = ctx.locale('fr'); await ctx.interaction.reply({ content: t('response', { latency: ctx.client.ws.ping }), }); }; ``` ### Advanced translation features #### Pluralization i18next supports automatic pluralization: ```json title="locales/en-US/user.json" { "member_count": "{{count}} member", "member_count_plural": "{{count}} members" } ``` ```ts const { t } = ctx.locale(); const memberCount = guild.memberCount; // Automatically chooses singular or plural form const message = t('member_count', { count: memberCount }); ``` #### Nested Translations Organize translations using nested objects: ```json title="locales/en-US/errors.json" { "validation": { "required": "This field is required", "invalid_format": "Invalid format provided", "too_long": "Input is too long (max {{max}} characters)" }, "permissions": { "insufficient": "You don't have permission to use this command", "missing_role": "You need the {{role}} role to use this command" } } ``` ```ts const { t } = ctx.locale(); // Access nested translations with dot notation await ctx.interaction.reply({ content: t('errors.permissions.insufficient'), ephemeral: true, }); ``` #### Context and Namespaces Use different translation contexts for better organization: ```ts const { t } = ctx.locale(); // Default namespace (command file name) t('response'); // Specific namespace t('common:greeting', { name: user.displayName }); // Multiple namespaces t(['errors:validation.required', 'common:error']); ``` ## Events localization Event handlers in CommandKit can also benefit from localization, allowing you to create multilingual responses and messages for various Discord events like message creation, member joins, and more. ### Using translations in event handlers Since event handlers don't have the same context as commands, you need to import the `locale` function directly from the i18n plugin: ```ts title="src/app/events/messageCreate/handler.ts" import { locale } from '@commandkit/i18n'; import type { Message } from 'discord.js'; export default async function onMessageCreate(message: Message) { // Skip bot messages if (message.author.bot) return; // Get translations for the guild's preferred locale const { t } = locale(message.guild?.preferredLocale); if (message.content.toLowerCase() === 'hello') { await message.reply(t('greeting', { user: message.author.displayName })); } if (message.content.toLowerCase() === 'help') { await message.reply(t('help_message')); } } ``` ### Event translation files Create translation files for your events using descriptive names: ```json title="src/app/locales/en-US/messageCreate.event.json" { "greeting": "👋 Hello {{user}}! Welcome to our server!", "help_message": "📖 Use `/help` to see all available commands", "auto_mod": { "warning": "⚠️ {{user}}, please watch your language!", "timeout": "🔇 {{user}} has been timed out for inappropriate language" } } ``` ```json title="src/app/locales/fr/messageCreate.event.json" { "greeting": "👋 Salut {{user}} ! Bienvenue sur notre serveur !", "help_message": "📖 Utilisez `/help` pour voir toutes les commandes disponibles", "auto_mod": { "warning": "⚠️ {{user}}, attention à votre langage !", "timeout": "🔇 {{user}} a été mis en sourdine pour langage inapproprié" } } ``` ### Advanced event localization #### Guild welcome messages ```ts title="src/app/events/guildMemberAdd/handler.ts" import { locale } from '@commandkit/i18n'; import type { GuildMember } from 'discord.js'; export default async function onGuildMemberAdd(member: GuildMember) { const { t } = locale(member.guild.preferredLocale); // Find welcome channel const welcomeChannel = member.guild.channels.cache.find( (channel) => channel.name === 'welcome', ); if (welcomeChannel?.isTextBased()) { const memberCount = member.guild.memberCount; await welcomeChannel.send({ content: t('welcome.message', { user: member.displayName, guild: member.guild.name, count: memberCount, }), // You can also send embeds with localized content embeds: [ { title: t('welcome.embed.title'), description: t('welcome.embed.description', { user: member.displayName, }), color: 0x00ff00, fields: [ { name: t('welcome.embed.fields.rules'), value: t('welcome.embed.fields.rules_description'), }, { name: t('welcome.embed.fields.channels'), value: t('welcome.embed.fields.channels_description'), }, ], }, ], }); } } ``` ```json title="src/app/locales/en-US/guildMemberAdd.event.json" { "welcome": { "message": "🎉 Welcome {{user}} to **{{guild}}**! You're our **{{count}}** member!", "embed": { "title": "Welcome to the Server!", "description": "Hi {{user}}, we're glad to have you here!", "fields": { "rules": "📋 Rules", "rules_description": "Please read our rules in #rules channel", "channels": "💬 Important Channels", "channels_description": "Check out #announcements and #general" } } } } ``` #### Locale detection strategies When working with events, you have several options for determining the appropriate locale: ##### 1. Guild preferred locale (recommended) ```ts // Use the guild's preferred locale set by server admins const { t } = locale(message.guild?.preferredLocale); ``` ##### 2. User locale ```ts // Use the individual user's Discord locale const { t } = locale(message.author.locale); ``` ##### 3. Fallback chain ```ts // Try multiple locale sources with fallback const detectedLocale = message.guild?.preferredLocale || message.author.locale || 'en-US'; const { t } = locale(detectedLocale); ``` ##### 4. Custom locale detection ```ts async function detectLocale(message: Message): Promise { // Custom logic: check database for user preferences const userSettings = await getUserSettings(message.author.id); if (userSettings?.language) { return userSettings.language; } // Fall back to guild locale return message.guild?.preferredLocale || 'en-US'; } export default async function onMessageCreate(message: Message) { const userLocale = await detectLocale(message); const { t } = locale(userLocale); // Use translations... } ``` ### Error handling with localization Handle translation errors gracefully in event handlers: ```ts title="src/app/events/messageCreate/autoMod.ts" import { locale } from '@commandkit/i18n'; import type { Message } from 'discord.js'; const FORBIDDEN_WORDS = ['spam', 'scam', 'hack']; export default async function autoModerator(message: Message) { if (message.author.bot) return; try { const { t } = locale(message.guild?.preferredLocale); const hasViolation = FORBIDDEN_WORDS.some((word) => message.content.toLowerCase().includes(word), ); if (hasViolation) { // Delete the message await message.delete(); // Send warning await message.channel.send({ content: t('moderation.auto_warning', { user: message.author.displayName, }), }); // Log the action console.log( t('moderation.log_message', { user: message.author.tag, guild: message.guild?.name, channel: message.channel.name, }), ); } } catch (error) { // Fallback to English if translation fails console.error('Translation error:', error); await message.channel.send( `⚠️ ${message.author.displayName}, please follow our community guidelines.`, ); } } ``` ### Best practices for event localization 1. **Always provide fallbacks**: Event handlers should gracefully handle missing translations 2. **Use appropriate locale sources**: Choose between guild, user, or custom locale detection based on context 3. **Keep translations consistent**: Use the same tone and style across events and commands 4. **Test with different locales**: Ensure your events work correctly with various language settings 5. **Cache translations when possible**: For high-frequency events, consider caching translation functions ## Usage with legacy commands The i18n plugin is fully compatible with the [`@commandkit/legacy`](./06-commandkit-legacy.mdx) plugin, allowing you to add internationalization to existing projects without major refactoring. ### Translations in legacy commands For legacy commands, import the `locale` function directly from the i18n plugin: ```ts title="src/app/commands/legacy/ping.js" import { locale } from '@commandkit/i18n'; export async function run({ interaction, client }) { // The locale function can automatically infer the locale from the interaction const { t } = locale(); const latency = client.ws.ping; return interaction.reply({ content: t('response', { latency }), ephemeral: true, }); } // Legacy command metadata export const data = { name: 'ping', description: 'Check bot latency', }; ``` #### Manual locale specification You can also specify a locale manually: ```ts export async function run({ interaction, client }) { // Use a specific locale const guildLocale = interaction.guild?.preferredLocale || 'en-US'; const { t } = locale(guildLocale); return interaction.reply({ content: t('response', { latency: client.ws.ping }), ephemeral: true, }); } ``` ### Legacy command translation files Translation files for legacy commands work the same way as modern commands: ```json title="src/app/locales/en-US/ping.json" { "response": "🏓 Pong! Latency: **{{latency}}ms**", "error": "❌ Could not determine latency" } ``` ```json title="src/app/locales/fr/ping.json" { "response": "🏓 Pong! Latence: **{{latency}}ms**", "error": "❌ Impossible de déterminer la latence" } ``` ### Migrating legacy commands Here's how to migrate an existing legacy command to use i18n: #### Before (No localization) ```ts title="src/app/commands/legacy/userinfo.js" export async function run({ interaction }) { const user = interaction.options.getUser('user') || interaction.user; const member = interaction.guild?.members.cache.get(user.id); const embed = { title: `User Information - ${user.username}`, fields: [ { name: 'Username', value: user.username, inline: true }, { name: 'ID', value: user.id, inline: true }, { name: 'Account Created', value: user.createdAt.toDateString(), inline: true, }, ], color: 0x0099ff, }; if (member) { embed.fields.push( { name: 'Joined Server', value: member.joinedAt?.toDateString() || 'Unknown', inline: true, }, { name: 'Roles', value: member.roles.cache.map((role) => role.name).join(', '), inline: false, }, ); } return interaction.reply({ embeds: [embed] }); } export const data = { name: 'userinfo', description: 'Get information about a user', options: [ { name: 'user', description: 'The user to get information about', type: 6, // User required: false, }, ], }; ``` #### After (With localization) ```ts title="src/app/commands/legacy/userinfo.js" import { locale } from '@commandkit/i18n'; export async function run({ interaction }) { const { t } = locale(); const user = interaction.options.getUser('user') || interaction.user; const member = interaction.guild?.members.cache.get(user.id); const embed = { title: t('embed.title', { username: user.username }), fields: [ { name: t('embed.fields.username'), value: user.username, inline: true, }, { name: t('embed.fields.id'), value: user.id, inline: true }, { name: t('embed.fields.created'), value: user.createdAt.toDateString(), inline: true, }, ], color: 0x0099ff, }; if (member) { embed.fields.push( { name: t('embed.fields.joined'), value: member.joinedAt?.toDateString() || t('embed.unknown'), inline: true, }, { name: t('embed.fields.roles'), value: member.roles.cache.map((role) => role.name).join(', '), inline: false, }, ); } return interaction.reply({ embeds: [embed] }); } export const data = { name: 'userinfo', description: 'Get information about a user', options: [ { name: 'user', description: 'The user to get information about', type: 6, // User required: false, }, ], }; ``` ```json title="src/app/locales/en-US/userinfo.json" { "embed": { "title": "User Information - {{username}}", "unknown": "Unknown", "fields": { "username": "Username", "id": "ID", "created": "Account Created", "joined": "Joined Server", "roles": "Roles" } } } ``` ```json title="src/app/locales/es/userinfo.json" { "embed": { "title": "Información del Usuario - {{username}}", "unknown": "Desconocido", "fields": { "username": "Nombre de Usuario", "id": "ID", "created": "Cuenta Creada", "joined": "Se Unió al Servidor", "roles": "Roles" } } } ``` ### Best practices for legacy commands 1. **Gradual Migration**: You can migrate commands one at a time without affecting others 2. **Consistent Naming**: Use the same translation keys across legacy and modern commands when possible 3. **Error Handling**: Always provide fallback text for missing translations 4. **Testing**: Test legacy commands with different locales to ensure compatibility ### Mixed command types You can use both legacy and modern commands with i18n in the same project: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { i18n } from '@commandkit/i18n'; export default defineConfig({ // Enable both legacy and modern command support plugins: [i18n()], // Configuration for mixed command types paths: { commands: 'src/app/commands', events: 'src/app/events', }, }); ``` This allows you to: - Keep existing legacy commands working - Add new commands using the modern syntax - Gradually migrate legacy commands when convenient - Maintain consistent localization across all command types ## Complete example Here's a comprehensive example showing various localization features: ```json title="src/app/locales/en-US/moderation.json" { "$command": { "name": "ban", "description": "Ban a user from the server", "options": [ { "name": "user", "description": "The user to ban" }, { "name": "reason", "description": "Reason for the ban" }, { "name": "duration", "description": "Ban duration", "choices": [ { "name": "Permanent", "value": "permanent" }, { "name": "1 Day", "value": "1d" }, { "name": "1 Week", "value": "1w" } ] } ] }, "success": "✅ **{{user}}** has been banned", "success_with_reason": "✅ **{{user}}** has been banned\n**Reason:** {{reason}}", "errors": { "user_not_found": "❌ User not found", "insufficient_permissions": "❌ I don't have permission to ban this user", "cannot_ban_self": "❌ You cannot ban yourself" } } ``` ```ts title="src/app/commands/moderation.ts" import type { ChatInputCommand } from 'commandkit'; export const chatInput: ChatInputCommand = async (ctx) => { const { t } = ctx.locale(); const user = ctx.interaction.options.getUser('user', true); const reason = ctx.interaction.options.getString('reason'); try { // Attempt to ban the user await ctx.interaction.guild?.members.ban(user, { reason: reason || undefined, }); // Send localized success message const successKey = reason ? 'success_with_reason' : 'success'; await ctx.interaction.reply({ content: t(successKey, { user: user.displayName, reason, }), }); } catch (error) { // Send localized error message await ctx.interaction.reply({ content: t('errors.insufficient_permissions'), ephemeral: true, }); } }; ``` --- ## @commandkit/legacy :::danger ❌ This package has been deprecated and will not work in newer versions of CommandKit. Last supported with [v1.2.0-rc.10](https://www.npmjs.com/package/commandkit/v/1.2.0-rc.10) ::: The `@commandkit/legacy` plugin enables support for legacy command and event handlers. This is particularly useful when migrating from an older version of CommandKit or another framework, as it allows you to incrementally upgrade your application without requiring immediate major changes to your existing code. ## Installation ```sh npm2yarn npm install @commandkit/legacy ``` ## Usage Add the Legacy plugin to your CommandKit configuration: ```js import { defineConfig } from 'commandkit'; import { legacy } from '@commandkit/legacy'; export default defineConfig({ plugins: [legacy()], }); ``` ## Features The Legacy plugin provides the following features: 1. **Legacy Command Handler Support**: Enables support for older command handler formats, making migration easier. 2. **Legacy Event Handler Support**: Allows you to continue using your existing event handler structure. 3. **Legacy Validation Support**: Maintains compatibility with existing validation methods. 4. **Hot Module Replacement (HMR)**: The plugin automatically enables HMR for legacy commands, events, and validations, improving the development experience by allowing you to see changes without restarting your bot. ## Migration Strategy The Legacy plugin is designed as a transitional tool. While it allows you to continue using your existing code structure, it's recommended to gradually migrate your commands, events, and validators to the new CommandKit format over time. This approach enables you to: 1. Upgrade your bot's framework immediately without breaking existing functionality 2. Incrementally refactor your code at your own pace 3. Take advantage of new CommandKit features for new commands while maintaining backward compatibility --- ## @commandkit/ratelimit `@commandkit/ratelimit` is the official CommandKit plugin for advanced rate limiting. It provides multi-window policies, role overrides, queueing, exemptions, and multiple algorithms while keeping command handlers lean. The `ratelimit()` factory returns two plugins in order: the compiler plugin for the "use ratelimit" directive and the runtime plugin that enforces limits. Runtime options must be configured before the runtime plugin activates. ## Installation Install the ratelimit plugin to get started: ```bash npm2yarn npm install @commandkit/ratelimit ``` ## Setup Add the ratelimit plugin to your CommandKit configuration and define a runtime config file. ### Quick start Create an auto-loaded runtime config file (for example `src/ratelimit.ts`) and configure the default limiter: ```ts title="src/ratelimit.ts" import { configureRatelimit } from '@commandkit/ratelimit'; configureRatelimit({ defaultLimiter: { maxRequests: 5, interval: '1m', scope: 'user', algorithm: 'fixed-window', }, }); ``` Register the plugin in your config: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { ratelimit } from '@commandkit/ratelimit'; export default defineConfig({ plugins: [ratelimit()], }); ``` The runtime plugin auto-loads `ratelimit.ts` or `ratelimit.js` on startup before commands execute. ## Runtime configuration lifecycle ### Runtime lifecycle diagram ```mermaid graph TD A[App startup] --> B[Auto-load ratelimit.ts/js] B --> C["configureRatelimit()"] C --> D["Runtime plugin activate()"] D --> E[Resolve storage] E --> F[Resolve limiter config] F --> G[Consume algorithm] G --> H[Aggregate result] H --> I[Default response / hooks / events] ``` ### `configureRatelimit` is required `RateLimitPlugin.activate()` throws if `configureRatelimit()` was not called. This is enforced to avoid silently running without your intended defaults. :::warning Runtime configuration required Make sure `configureRatelimit()` runs at startup (for example in `ratelimit.ts` or `ratelimit.js`) before the runtime plugin activates. If it does not, the plugin will throw on startup. ::: ### How configuration is stored `configureRatelimit()` merges your config into an in-memory object and sets the configured flag. `getRateLimitConfig()` returns the current object, and `isRateLimitConfigured()` returns whether initialization has happened. If a runtime context is already active, `configureRatelimit()` updates it immediately. ### Runtime storage selection Storage is resolved in this order: | Order | Source | Notes | | ----- | -------------------------- | ------------------------------------------------------------------------------- | | 1 | Limiter `storage` override | `RateLimitLimiterConfig.storage` for the command being executed. | | 2 | Plugin `storage` option | `RateLimitPluginOptions.storage`. | | 3 | Process default | Set via `setRateLimitStorage()` or `setDriver()`. | | 4 | Default memory storage | Used unless `initializeDefaultStorage` or `initializeDefaultDriver` is `false`. | If no storage is resolved and defaults are disabled, the plugin logs once and stores an empty result without limiting. ### Runtime helpers These helpers are process-wide: | Helper | Purpose | | ------------------------- | ---------------------------------------------------------- | | `configureRatelimit` | Set runtime options and update active runtime state. | | `getRateLimitConfig` | Read the merged in-memory runtime config. | | `isRateLimitConfigured` | Check whether `configureRatelimit()` was called. | | `setRateLimitStorage` | Set the default storage for the process. | | `getRateLimitStorage` | Get the process default storage (or `null`). | | `setDriver` / `getDriver` | Aliases for `setRateLimitStorage` / `getRateLimitStorage`. | | `setRateLimitRuntime` | Set the active runtime context for APIs and directives. | | `getRateLimitRuntime` | Get the active runtime context (or `null`). | ## Basic usage Use command metadata or the `use ratelimit` directive to enable rate limiting. This section focuses on command metadata; see the directive section for function-level usage. ### Command metadata and enablement Enable rate limiting by setting `metadata.ratelimit`: ```ts title="src/app/commands/ping.ts" export const metadata = { ratelimit: { maxRequests: 3, interval: '10s', scope: 'user', algorithm: 'sliding-window', }, }; ``` `metadata.ratelimit` can be one of: | Value | Meaning | | ------------------------ | -------------------------------------------------- | | `false` or `undefined` | Plugin does nothing for this command. | | `true` | Enable rate limiting using resolved defaults. | | `RateLimitCommandConfig` | Enable rate limiting with command-level overrides. | If `env.context` is missing in the execution environment, the plugin skips rate limiting. ### Named limiter example ```ts title="commandkit.config.ts" configureRatelimit({ limiters: { heavy: { maxRequests: 1, interval: '10s', algorithm: 'fixed-window' }, }, }); ``` ```ts title="src/app/commands/report.ts" export const metadata = { ratelimit: { limiter: 'heavy', scope: 'user', }, }; ``` ## Configuration reference ### RateLimitPluginOptions | Field | Type | Default or resolution | Notes | | -------------------------- | ---------------------------------------- | ---------------------------- | -------------------------------------------------------------- | | `defaultLimiter` | `RateLimitLimiterConfig` | `DEFAULT_LIMITER` when unset | Base limiter for all commands and directives. | | `limiters` | `Record` | `undefined` | Named limiter presets. | | `storage` | `RateLimitStorageConfig` | `undefined` | Resolved before default storage. | | `keyPrefix` | `string` | `undefined` | Prepended before `rl:`. | | `keyResolver` | `RateLimitKeyResolver` | `undefined` | Used for `custom` scope when the limiter does not override it. | | `bypass` | `RateLimitBypassOptions` | `undefined` | Permanent allowlists and optional check. | | `hooks` | `RateLimitHooks` | `undefined` | Lifecycle callbacks. | | `onRateLimited` | `RateLimitResponseHandler` | `undefined` | Overrides default reply. | | `queue` | `RateLimitQueueOptions` | `undefined` | If any queue config exists, `enabled` defaults to `true`. | | `roleLimits` | `Record` | `undefined` | Base role limits. | | `roleLimitStrategy` | `RateLimitRoleLimitStrategy` | `highest` when resolving | Used when multiple roles match. | | `initializeDefaultStorage` | `boolean` | `true` | Disable to prevent memory fallback. | | `initializeDefaultDriver` | `boolean` | `true` | Alias for `initializeDefaultStorage`. | ### RateLimitLimiterConfig | Field | Type | Default or resolution | Notes | | ------------------- | ---------------------------------------- | ------------------------------------ | ------------------------------------------------ | | `maxRequests` | `number` | `10` when missing or `<= 0` | Used by fixed and sliding windows. | | `interval` | `DurationLike` | `60s` when missing or invalid | Parsed and clamped to `>= 1ms`. | | `scope` | `RateLimitScope` or `RateLimitScope[]` | `user` | Arrays are deduplicated. | | `algorithm` | `RateLimitAlgorithmType` | `fixed-window` | Unknown values fall back to fixed-window. | | `burst` | `number` | `maxRequests` when missing or `<= 0` | Capacity for token or leaky buckets. | | `refillRate` | `number` | `maxRequests / intervalSeconds` | Must be `> 0` for token bucket. | | `leakRate` | `number` | `maxRequests / intervalSeconds` | Must be `> 0` for leaky bucket. | | `keyResolver` | `RateLimitKeyResolver` | `undefined` | Used only for `custom` scope. | | `keyPrefix` | `string` | `undefined` | Overrides plugin prefix for this limiter. | | `storage` | `RateLimitStorageConfig` | `undefined` | Overrides storage for this limiter. | | `violations` | `ViolationOptions` | `undefined` | Enables escalation unless `escalate` is `false`. | | `queue` | `RateLimitQueueOptions` | `undefined` | Overrides queue settings at this layer. | | `windows` | `RateLimitWindowConfig[]` | `undefined` | Enables multi-window behavior. | | `roleLimits` | `Record` | `undefined` | Role overrides at this layer. | | `roleLimitStrategy` | `RateLimitRoleLimitStrategy` | `highest` when resolving | Used when role limits match. | ### RateLimitWindowConfig | Field | Type | Default or resolution | Notes | | ------------- | ------------------------ | -------------------------- | --------------------------------------- | | `id` | `string` | `w1`, `w2`, ... | Auto-generated if empty or missing. | | `maxRequests` | `number` | Inherits from base limiter | Applies only to this window. | | `interval` | `DurationLike` | Inherits from base limiter | Parsed like the base limiter. | | `algorithm` | `RateLimitAlgorithmType` | Inherits from base limiter | Usually keep consistent across windows. | | `burst` | `number` | Inherits from base limiter | Used for token or leaky buckets. | | `refillRate` | `number` | Inherits from base limiter | Must be `> 0` for token bucket. | | `leakRate` | `number` | Inherits from base limiter | Must be `> 0` for leaky bucket. | | `violations` | `ViolationOptions` | Inherits from base limiter | Overrides escalation for this window. | ### RateLimitQueueOptions | Field | Type | Default or resolution | Notes | | ------------------ | -------------- | ----------------------------------- | ----------------------------------- | | `enabled` | `boolean` | `true` when any queue config exists | Otherwise `false`. | | `maxSize` | `number` | `3` and clamped to `>= 1` | Queue size is pending plus running. | | `timeout` | `DurationLike` | `30s` and clamped to `>= 1ms` | Applies per queued task. | | `deferInteraction` | `boolean` | `true` unless explicitly `false` | Only used for interactions. | | `ephemeral` | `boolean` | `true` unless explicitly `false` | Applies to deferred replies. | | `concurrency` | `number` | `1` and clamped to `>= 1` | Controls per-key queue concurrency. | ### ViolationOptions | Field | Type | Default or resolution | Notes | | ---------------------- | -------------- | ------------------------------- | ---------------------------------- | | `escalate` | `boolean` | `true` when `violations` is set | Set `false` to disable escalation. | | `maxViolations` | `number` | `5` | Maximum escalation steps. | | `escalationMultiplier` | `number` | `2` | Multiplies cooldown per violation. | | `resetAfter` | `DurationLike` | `1h` | TTL for violation state. | ### RateLimitCommandConfig `RateLimitCommandConfig` extends `RateLimitLimiterConfig` and adds: | Field | Type | Default or resolution | Notes | | --------- | -------- | --------------------- | ----------------------------------------- | | `limiter` | `string` | `undefined` | References a named limiter in `limiters`. | ### Result shapes RateLimitStoreValue: | Field | Type | Meaning | | ------------ | ------------------- | ------------------------------------------ | | `limited` | `boolean` | `true` if any scope or window was limited. | | `remaining` | `number` | Minimum remaining across all results. | | `resetAt` | `number` | Latest reset timestamp across all results. | | `retryAfter` | `number` | Max retry delay across limited results. | | `results` | `RateLimitResult[]` | Individual results per scope and window. | RateLimitResult: | Field | Type | Meaning | | ------------ | ------------------------ | ------------------------------------------------------------------------- | | `key` | `string` | Storage key used for the limiter. | | `scope` | `RateLimitScope` | Scope applied for the limiter. | | `algorithm` | `RateLimitAlgorithmType` | Algorithm used for the limiter. | | `windowId` | `string` | Present for multi-window limits. | | `limited` | `boolean` | Whether this limiter hit its limit. | | `remaining` | `number` | Remaining requests or capacity. | | `resetAt` | `number` | Absolute reset timestamp in ms. | | `retryAfter` | `number` | Delay until retry is allowed, in ms. | | `limit` | `number` | `maxRequests` for fixed and sliding, `burst` for token and leaky buckets. | ## Limiter resolution and role strategy Limiter configuration is layered in this exact order, with later layers overriding earlier ones: | Order | Source | Notes | | ----- | ----------------- | ----------------------------------------- | | 1 | `DEFAULT_LIMITER` | Base defaults. | | 2 | `defaultLimiter` | Runtime defaults. | | 3 | Named limiter | When `metadata.ratelimit.limiter` is set. | | 4 | Command overrides | `metadata.ratelimit` config. | | 5 | Role override | Selected by role strategy. | ### Limiter resolution diagram ```mermaid graph TD A[DEFAULT_LIMITER] --> B[defaultLimiter] B --> C[Named limiter] C --> D[Command overrides] D --> E["Role override (strategy)"] ``` Role limits are merged in this order, with later maps overriding earlier ones for the same role id: | Order | Source | | ----- | --------------------------- | | 1 | Plugin `roleLimits` | | 2 | `defaultLimiter.roleLimits` | | 3 | Named limiter `roleLimits` | | 4 | Command `roleLimits` | Role strategies: | Strategy | Selection rule | | --------- | -------------------------------------------------------------------------- | | `highest` | Picks the role with the highest request rate (`maxRequests / intervalMs`). | | `lowest` | Picks the role with the lowest request rate. | | `first` | Uses insertion order of the merged role limits object. | For multi-window limiters, the score uses the minimum rate across windows. ## Scopes and keying Supported scopes: | Scope | Required IDs | Key format (without `keyPrefix`) | Skip behavior | | ------------ | ------------------- | ------------------------------------------------ | ---------------------------------------------- | | `user` | `userId` | `rl:user:{userId}:{commandName}` | Skips if `userId` is missing. | | `guild` | `guildId` | `rl:guild:{guildId}:{commandName}` | Skips if `guildId` is missing. | | `channel` | `channelId` | `rl:channel:{channelId}:{commandName}` | Skips if `channelId` is missing. | | `global` | none | `rl:global:{commandName}` | Never skipped. | | `user-guild` | `userId`, `guildId` | `rl:user:{userId}:guild:{guildId}:{commandName}` | Skips if either id is missing. | | `custom` | `keyResolver` | `keyResolver(ctx, command, source)` | Skips if resolver is missing or returns falsy. | Keying notes: - `DEFAULT_KEY_PREFIX` is always included in the base format. - `keyPrefix` is concatenated before `rl:` as-is, so include a trailing separator if you want one. - Multi-window limits append `:w:{windowId}`. ### Exemption keys Temporary exemptions are stored under `rl:exempt:{scope}:{id}` (plus optional `keyPrefix`). | Exemption scope | Key format | Notes | | --------------- | --------------------------------- | ------------------------------------- | | `user` | `rl:exempt:user:{userId}` | Resolved from the source user id. | | `guild` | `rl:exempt:guild:{guildId}` | Resolved from the guild id. | | `role` | `rl:exempt:role:{roleId}` | Resolved from all member roles. | | `channel` | `rl:exempt:channel:{channelId}` | Resolved from the channel id. | | `category` | `rl:exempt:category:{categoryId}` | Resolved from the parent category id. | ## Algorithms ### Algorithm matrix | Algorithm | Required config | Storage requirements | `limit` value | Notes | | ---------------- | ------------------------- | ------------------------------------------------------------------ | ------------- | --------------------------------------------------------- | | `fixed-window` | `maxRequests`, `interval` | `consumeFixedWindow` or `incr` or `get` and `set` | `maxRequests` | Fallback uses per-process lock and optimistic versioning. | | `sliding-window` | `maxRequests`, `interval` | `consumeSlidingWindowLog` or `zRemRangeByScore` + `zCard` + `zAdd` | `maxRequests` | Throws if sorted-set support is missing. | | `token-bucket` | `burst`, `refillRate` | `get` and `set` | `burst` | Throws if `refillRate <= 0`. | | `leaky-bucket` | `burst`, `leakRate` | `get` and `set` | `burst` | Throws if `leakRate <= 0`. | ### Fixed window Execution path: 1. If `consumeFixedWindow` exists, it is used. 2. Else if `incr` exists, it is used. 3. Else a fallback uses `get` and `set` with a per-process lock. The limiter is considered limited when `count > maxRequests`. The fallback path retries up to five times with optimistic versioning and is serialized only within the current process. #### Fixed window fallback diagram ```mermaid graph TD A[Consume fixed-window] --> B{consumeFixedWindow?} B -- Yes --> C[Use consumeFixedWindow] B -- No --> D{incr?} D -- Yes --> E[Use incr] D -- No --> F["get + set fallback (per-process lock)"] ``` ### Sliding window log Execution path: 1. If `consumeSlidingWindowLog` exists, it is used (atomic). 2. Else a sorted-set fallback uses `zRemRangeByScore`, `zCard`, and `zAdd`. If sorted-set methods are missing, the algorithm throws. If `zRangeByScore` is available, it is used to compute an accurate oldest timestamp for `resetAt`; otherwise `resetAt` defaults to `now + window`. The fallback is serialized per process but is not atomic across processes. #### Sliding window fallback diagram ```mermaid graph TD A[Consume sliding-window] --> B{consumeSlidingWindowLog?} B -- Yes --> C[Use consumeSlidingWindowLog] B -- No --> D{zset methods?} D -- No --> E[Throw error] D -- Yes --> F[zRemRangeByScore + zCard + zAdd fallback] ``` ### Token bucket Token bucket uses a stored `tokens` and `lastRefill` state. On each consume, tokens refill based on elapsed time and `refillRate`. If the bucket has fewer than one token, the request is limited and `retryAfter` is computed from the time required to refill one token. ### Leaky bucket Leaky bucket uses a stored `level` and `lastLeak` state. Each request adds one token, and the bucket drains at `leakRate`. If adding would exceed `capacity`, the request is limited and `retryAfter` is computed from the time required to drain the overflow. ### Multi-window limits Use `windows` to enforce multiple windows simultaneously: ```ts configureRatelimit({ defaultLimiter: { scope: 'user', algorithm: 'sliding-window', windows: [ { id: 'short', maxRequests: 10, interval: '1m' }, { id: 'long', maxRequests: 1000, interval: '1d' }, ], }, }); ``` If a window `id` is omitted, the plugin generates `w1`, `w2`, and so on. Window ids are part of the storage key and appear in results. ## Storage ### Storage interface Required methods: | Method | Used by | Notes | | -------- | --------------------------- | --------------------------------- | | `get` | All algorithms | Returns stored value or `null`. | | `set` | All algorithms | Optional `ttlMs` controls expiry. | | `delete` | Resets and algorithm resets | Removes stored state. | Optional methods and features: | Method | Feature | Notes | | ------------------------------------- | ----------------------------- | -------------------------------------------------- | | `consumeFixedWindow` | Fixed-window atomic consume | Used before `incr` and fallback. | | `incr` | Fixed-window efficiency | Returns count and TTL. | | `consumeSlidingWindowLog` | Sliding-window atomic consume | Preferred over sorted-set fallback. | | `zAdd` / `zRemRangeByScore` / `zCard` | Sliding-window fallback | Required when `consumeSlidingWindowLog` is absent. | | `zRangeByScore` | Sliding-window reset accuracy | Improves `resetAt` computation. | | `ttl` | Exemption listing | Used for `expiresInMs`. | | `expire` | Sliding-window fallback | Keeps sorted-set keys from growing indefinitely. | | `deleteByPrefix` / `deleteByPattern` | Resets | Required by `resetAllRateLimits` and HMR. | | `keysByPrefix` | Exemption listing | Required for listing without a specific id. | ### Capability matrix | Feature | Requires | Memory | Redis | Fallback | | ----------------------------- | ------------------------------------- | ------ | ----- | --------------------------- | | Fixed-window atomic consume | `consumeFixedWindow` | Yes | Yes | Conditional (both storages) | | Fixed-window `incr` | `incr` | Yes | Yes | Conditional (both storages) | | Sliding-window atomic consume | `consumeSlidingWindowLog` | Yes | Yes | Conditional (both storages) | | Sliding-window fallback | `zAdd` + `zRemRangeByScore` + `zCard` | Yes | Yes | Conditional (both storages) | | TTL visibility | `ttl` | Yes | Yes | Conditional (both storages) | | Prefix or pattern deletes | `deleteByPrefix` or `deleteByPattern` | Yes | Yes | Conditional (both storages) | | Exemption listing | `keysByPrefix` | Yes | Yes | Conditional (both storages) | ### Capability overview diagram ```mermaid graph TD A[Storage API] --> B[Required: get / set / delete] A --> C[Optional methods] C --> D[Fixed window atomic: consumeFixedWindow / incr] C --> E[Sliding window atomic: consumeSlidingWindowLog] C --> F[Sliding window fallback: zAdd + zRemRangeByScore + zCard] C --> G[Listing & TTL: keysByPrefix / ttl] C --> H[Bulk reset: deleteByPrefix / deleteByPattern] I[Fallback storage] --> J[Uses primary + secondary] J --> K[Each optional method must exist on both] ``` ### Memory storage ```ts import { MemoryRateLimitStorage, setRateLimitStorage, } from '@commandkit/ratelimit'; setRateLimitStorage(new MemoryRateLimitStorage()); ``` Notes: - In-memory only; not safe for multi-process deployments. - Implements TTL and sorted-set helpers. - `deleteByPattern` supports a simple `*` wildcard, not full glob syntax. :::warning Single-process only Memory storage is per process. For multiple bot shards or instances, use a shared storage like Redis. ::: ### Redis storage ```ts import { RedisRateLimitStorage } from '@commandkit/ratelimit/redis'; import { setRateLimitStorage } from '@commandkit/ratelimit'; setRateLimitStorage( new RedisRateLimitStorage({ host: 'localhost', port: 6379 }), ); ``` Notes: - Stores values as JSON. - Uses Lua scripts for atomic fixed and sliding windows. - Uses `SCAN` for prefix and pattern deletes and listing. ### Fallback storage ```ts import { FallbackRateLimitStorage } from '@commandkit/ratelimit/fallback'; import { MemoryRateLimitStorage } from '@commandkit/ratelimit/memory'; import { RedisRateLimitStorage } from '@commandkit/ratelimit/redis'; import { setRateLimitStorage } from '@commandkit/ratelimit'; const primary = new RedisRateLimitStorage({ host: 'localhost', port: 6379 }); const secondary = new MemoryRateLimitStorage(); setRateLimitStorage(new FallbackRateLimitStorage(primary, secondary)); ``` Notes: - Every optional method must exist on both storages or the fallback wrapper throws. - Primary errors are logged at most once per `cooldownMs` window (default 30s). ## Queue mode Queue mode retries commands instead of rejecting immediately. :::tip Use queueing to smooth bursts Queueing is useful for smoothing short bursts, but it changes response timing. Disable it with `queue: { enabled: false }` if you want strict, immediate rate-limit responses. ::: ### Queue defaults and clamps | Field | Default | Clamp | Notes | | ------------------ | --------------------------------- | -------- | ----------------------------------- | | `enabled` | `true` if any queue config exists | n/a | Otherwise `false`. | | `maxSize` | `3` | `>= 1` | Queue size is pending plus running. | | `timeout` | `30s` | `>= 1ms` | Per queued task. | | `deferInteraction` | `true` | n/a | Only applies to interactions. | | `ephemeral` | `true` | n/a | Applies to deferred replies. | | `concurrency` | `1` | `>= 1` | Per queue key. | ### Queue flow 1. Rate limit is evaluated and an aggregate result is computed. 2. If limited and queueing is enabled, the plugin tries to enqueue. 3. If the queue is full, it falls back to immediate rate-limit handling. 4. When queued, the interaction is deferred if it is repliable and not already replied or deferred. 5. The queued task waits `retryAfter`, then re-checks the limiter; if still limited it waits at least 250ms and retries until timeout. ### Queue flow diagram ```mermaid graph TD A[Evaluate limiter] --> B{Limited?} B -- No --> C[Allow command] B -- Yes --> D{Queue enabled?} D -- No --> E[Rate-limit response] D -- Yes --> F{Queue has capacity?} F -- No --> E F -- Yes --> G[Enqueue + defer if repliable] G --> H[Wait retryAfter] H --> I{Still limited?} I -- No --> C I -- Yes --> J[Wait >= 250ms] J --> K{Timed out?} K -- No --> H K -- Yes --> E ``` ## Violations and escalation Violation escalation is stored under `violation:{key}` and uses these defaults: | Option | Default | Meaning | | ---------------------- | ------------------------------- | ---------------------------------- | | `maxViolations` | `5` | Maximum escalation steps. | | `escalationMultiplier` | `2` | Multiplier per repeated violation. | | `resetAfter` | `1h` | TTL for violation state. | | `escalate` | `true` when `violations` is set | Set `false` to disable escalation. | Formula: `cooldown = baseRetryAfter * multiplier^(count - 1)` If escalation produces a later `resetAt` than the algorithm returned, the result is updated so `resetAt` and `retryAfter` stay accurate. ## Bypass and exemptions Bypass order is always: 1. `bypass.userIds`, `bypass.guildIds`, and `bypass.roleIds`. 2. Temporary exemptions stored in storage. 3. `bypass.check(source)`. Bypass example: ```ts configureRatelimit({ bypass: { userIds: ['USER_ID'], guildIds: ['GUILD_ID'], roleIds: ['ROLE_ID'], check: (source) => source.channelId === 'ALLOWLIST_CHANNEL', }, }); ``` Temporary exemptions: ```ts import { grantRateLimitExemption } from '@commandkit/ratelimit'; await grantRateLimitExemption({ scope: 'user', id: 'USER_ID', duration: '1h', }); ``` Listing behavior: - `listRateLimitExemptions({ scope, id })` reads a single key directly. - `listRateLimitExemptions({ scope })` scans by prefix and requires `keysByPrefix`. - `expiresInMs` is `null` when `ttl` is not supported. ## Responses, hooks, and events ### Default response behavior | Source | Conditions | Action | | ----------- | -------------------------------------- | ------------------------------------------- | | Message | Channel is sendable | `reply()` with cooldown embed. | | Interaction | Repliable and not replied/deferred | `reply()` with ephemeral cooldown embed. | | Interaction | Repliable and already replied/deferred | `followUp()` with ephemeral cooldown embed. | | Interaction | Not repliable | No response. | The default embed title is `:hourglass_flowing_sand: You are on cooldown` and the description uses a relative timestamp based on `resetAt`. ### Hooks | Hook | Called when | Notes | | ---------------- | ------------------------- | -------------------------------------------------- | | `onAllowed` | Command is allowed | Receives the first result. | | `onRateLimited` | Command is limited | Receives the first limited result. | | `onViolation` | A violation is recorded | Receives key and violation count. | | `onReset` | `resetRateLimit` succeeds | Not called by `resetAllRateLimits`. | | `onStorageError` | Storage operation fails | `fallbackUsed` is `false` in runtime plugin paths. | ### Analytics events The runtime plugin calls `ctx.commandkit.analytics.track(...)` with: | Event name | When | | --------------------- | ------------------------------------ | | `ratelimit_allowed` | After an allowed consume. | | `ratelimit_hit` | After a limited consume. | | `ratelimit_violation` | When escalation records a violation. | ### Event bus A `ratelimited` event is emitted on the `ratelimits` channel: ```ts commandkit.events .to('ratelimits') .on( 'ratelimited', ({ key, result, source, aggregate, commandName, queued }) => { console.log(key, commandName, queued, aggregate.retryAfter); }, ); ``` Payload fields include `key`, `result`, `source`, `aggregate`, `commandName`, and `queued`. ## Resets and HMR ### `resetRateLimit` `resetRateLimit` clears the base key, its `violation:` key, and any window variants. It accepts either a raw `key` or a scope-derived key. | Mode | Required params | Notes | | ------ | -------------------------------------- | --------------------------------------------------- | | Direct | `key` | Resets `key`, `violation:key`, and window variants. | | Scoped | `scope` + `commandName` + required ids | Throws if identifiers are missing. | ### `resetAllRateLimits` `resetAllRateLimits` supports several modes and requires storage delete helpers: | Mode | Required params | Storage requirement | | ------------ | ---------------------- | ------------------- | | Pattern | `pattern` | `deleteByPattern` | | Prefix | `prefix` | `deleteByPrefix` | | Command name | `commandName` | `deleteByPattern` | | Scope | `scope` + required ids | `deleteByPrefix` | ### HMR reset behavior When a command file is hot-reloaded, the plugin deletes keys that match: - `*:{commandName}` - `violation:*:{commandName}` - `*:{commandName}:w:*` - `violation:*:{commandName}:w:*` HMR reset requires `deleteByPattern`. If the storage does not support pattern deletes, nothing is cleared. ## Directive: `use ratelimit` The compiler plugin (`UseRateLimitDirectivePlugin`) uses `CommonDirectiveTransformer` with `directive = "use ratelimit"` and `importName = "$ckitirl"`. It transforms async functions only. The runtime wrapper: - Uses the runtime default limiter (merged with `DEFAULT_LIMITER`). - Generates a per-function key `rl:fn:{uuid}` and applies `keyPrefix` if present. - Aggregates results across windows and throws `RateLimitError` when limited. - Caches the wrapper per function and exposes it as `globalThis.$ckitirl`. Example: ```ts import { RateLimitError } from '@commandkit/ratelimit'; const heavy = async () => { 'use ratelimit'; return 'ok'; }; try { await heavy(); } catch (error) { if (error instanceof RateLimitError) { console.log(error.result.retryAfter); } } ``` ## Defaults and edge cases ### Defaults | Setting | Default | | -------------------------- | -------------- | | `maxRequests` | `10` | | `interval` | `60s` | | `algorithm` | `fixed-window` | | `scope` | `user` | | `DEFAULT_KEY_PREFIX` | `rl:` | | `RATELIMIT_STORE_KEY` | `ratelimit` | | `roleLimitStrategy` | `highest` | | `queue.maxSize` | `3` | | `queue.timeout` | `30s` | | `queue.deferInteraction` | `true` | | `queue.ephemeral` | `true` | | `queue.concurrency` | `1` | | `initializeDefaultStorage` | `true` | ### Edge cases 1. If no storage is configured and default storage is disabled, the plugin logs once and stores an empty result without limiting. 2. If no scope key can be resolved, the plugin stores an empty result and skips limiting. 3. If storage errors occur during consume, `onStorageError` is invoked and the plugin skips limiting for that execution. 4. For token and leaky buckets, `limit` equals `burst`. For fixed and sliding windows, `limit` equals `maxRequests`. ## Duration parsing `DurationLike` accepts numbers (milliseconds) or strings parsed by `ms`, plus custom units for weeks and months. | Unit | Meaning | | ------------------------ | -------------------- | | `ms`, `s`, `m`, `h`, `d` | Standard `ms` units. | | `w`, `week`, `weeks` | 7 days. | | `mo`, `month`, `months` | 30 days. | ## Exports | Export | Description | | ----------------------------- | ----------------------------------------------------------------------------- | | `ratelimit` | Plugin factory returning compiler + runtime plugins. | | `RateLimitPlugin` | Runtime plugin class. | | `UseRateLimitDirectivePlugin` | Compiler plugin for `use ratelimit`. | | `RateLimitEngine` | Algorithm coordinator with escalation handling. | | Algorithm classes | Fixed, sliding, token bucket, and leaky bucket implementations. | | Storage classes | Memory, Redis, and fallback storage. | | Runtime helpers | `configureRatelimit`, `setRateLimitStorage`, `getRateLimitRuntime`, and more. | | API helpers | `getRateLimitInfo`, resets, and exemption helpers. | | `RateLimitError` | Error thrown by the directive wrapper. | Subpath exports: - `@commandkit/ratelimit/redis` - `@commandkit/ratelimit/memory` - `@commandkit/ratelimit/fallback` --- ## @commandkit/tasks The CommandKit tasks plugin provides a powerful way to schedule and manage background tasks in your Discord application. Whether you need to run periodic maintenance, send scheduled reminders, or perform data cleanup, the tasks plugin has you covered. ## Installation ```bash npm install @commandkit/tasks ``` ## Basic setup Add the tasks plugin to your CommandKit configuration: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { tasks } from '@commandkit/tasks'; export default defineConfig({ plugins: [tasks()], }); ``` ## Setting up a driver By default, the plugin will initialize the SQLite driver. You can set up a different driver by calling `setDriver` function from the `@commandkit/tasks` package. If you want to disable the default driver initialization behavior, you can pass `initializeDefaultDriver: false` to the `tasks()` options in your commandkit config. ```ts import { setDriver } from '@commandkit/tasks'; import { SQLiteDriver } from '@commandkit/tasks/sqlite'; // For development setDriver(new SQLiteDriver('./tasks.db')); // For production, use BullMQ with Redis // setDriver(new BullMQDriver({ host: 'localhost', port: 6379 })); ``` ## Available drivers ### SQLite driver (Development) The SQLite driver provides persistent, file-based task scheduling. It's perfect for development environments and single-instance applications with job recovery on restart. **Pros:** - Persistent job storage - Jobs recoverable on restart - No external dependencies - Lightweight and reliable **Cons:** - Single instance only - No distributed scheduling support - Intended for development use **Usage:** ```ts import { SQLiteDriver } from '@commandkit/tasks/sqlite'; import { setDriver } from '@commandkit/tasks'; setDriver(new SQLiteDriver('./tasks.db')); ``` ### BullMQ driver (Production) The BullMQ driver provides robust, distributed task scheduling using Redis as the backend. It's ideal for production environments with multiple bot instances. **Pros:** - Distributed scheduling across multiple instances - Persistent task storage - Built-in retry mechanisms - Production-ready with Redis **Cons:** - Requires Redis server - More complex setup - Additional dependency **Installation:** ```bash npm install bullmq ``` **Usage:** ```ts import { BullMQDriver } from '@commandkit/tasks/bullmq'; import { setDriver } from '@commandkit/tasks'; setDriver( new BullMQDriver({ host: 'localhost', port: 6379, }), ); ``` **Advanced Redis configuration:** ```ts const driver = new BullMQDriver({ host: 'redis.example.com', port: 6379, password: 'your-password', tls: true, retryDelayOnFailover: 100, maxRetriesPerRequest: 3, }); ``` ### Environment-specific configuration ```ts import { COMMANDKIT_IS_DEV } from 'commandkit'; import { defineConfig, noBuildOnly } from 'commandkit/config'; import { tasks } from '@commandkit/tasks'; import { SQLiteDriver } from '@commandkit/tasks/sqlite'; import { BullMQDriver } from '@commandkit/tasks/bullmq'; import { setDriver } from '@commandkit/tasks'; // set up the driver in non-build processes only const setupDriver = noBuildOnly(() => { // Choose driver based on environment if (COMMANDKIT_IS_DEV) { setDriver(new SQLiteDriver('./tasks.db')); } else { setDriver( new BullMQDriver({ host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), password: process.env.REDIS_PASSWORD, }), ); } }); setupDriver(); export default defineConfig({ plugins: [tasks()], }); ``` :::warning Avoid loading the code that keeps the event loop alive inside the build process. Otherwise the build command will never exit. Use the helper functions such as `buildOnly()` or `noBuildOnly()` to conditionally load the code in appropriate places. ::: ## Creating your first task Create a file in `src/app/tasks/` to define your tasks: ```ts import { task } from '@commandkit/tasks'; export default task({ name: 'daily-backup', schedule: '0 0 * * *', // Daily at midnight (cron string) async execute(ctx) { // Your task logic here await performBackup(); console.log('Daily backup completed!'); }, }); ``` ## Task structure Every task has the following components: - **name**: A unique identifier for the task - **schedule**: When the task should run (optional for manual execution) - **execute**: The main function that performs the task work - **prepare**: Optional function to determine if the task should run ## Schedule types ### Cron schedules Use cron expressions for recurring tasks: ```ts export default task({ name: 'hourly-task', schedule: '0 * * * *', // Every hour async execute(ctx) { // Task logic }, }); ``` ### Date schedules Schedule tasks for specific times: ```ts export default task({ name: 'reminder', schedule: new Date('2024-01-01T12:00:00Z'), // Specific date async execute(ctx) { // Send reminder }, }); // Or use timestamps export default task({ name: 'timestamp-task', schedule: Date.now() + 60000, // 1 minute from now async execute(ctx) { // Task logic }, }); ``` ## Task context The `execute` function receives a context object with useful properties: ```ts export default task({ name: 'context-example', schedule: '0 */6 * * *', // Every 6 hours async execute(ctx) { // Access the Discord.js client const client = ctx.commandkit.client; // Access custom data (for dynamic tasks) const { userId, message } = ctx.data; // Use the temporary store ctx.store.set('lastRun', Date.now()); // Send a message to a channel const channel = client.channels.cache.get('channel-id'); if (channel?.isTextBased()) { await channel.send('Task executed!'); } }, }); ``` ## Conditional execution Use the `prepare` function to conditionally execute tasks: ```ts export default task({ name: 'conditional-task', schedule: '0 */2 * * *', // Every 2 hours async prepare(ctx) { // Only run if maintenance mode is not enabled return !ctx.commandkit.store.get('maintenance-mode'); }, async execute(ctx) { await performMaintenanceChecks(); }, }); ``` ## Dynamic tasks While static tasks are great for recurring operations, you often need to create tasks dynamically based on user interactions or events. The tasks plugin provides utilities for creating tasks on-demand. ### Creating dynamic tasks Use the `createTask` function to create tasks programmatically: ```ts import { createTask } from '@commandkit/tasks'; // Create a task that runs in 5 minutes const taskId = await createTask({ name: 'reminder', data: { userId: '123', message: "Don't forget your meeting!" }, schedule: Date.now() + 5 * 60 * 1000, // 5 minutes from now }); ``` ### Reminder system example Here's a complete example of a reminder command that creates dynamic tasks: ```ts import type { CommandData, ChatInputCommand } from 'commandkit'; import { ApplicationCommandOptionType } from 'discord.js'; import ms from 'ms'; import { createTask } from '@commandkit/tasks'; export const command: CommandData = { name: 'remind', description: 'remind command', options: [ { name: 'time', description: 'The time to remind after. Eg: 6h, 10m, 1d', type: ApplicationCommandOptionType.String, required: true, }, { name: 'message', description: 'The message to remind about.', type: ApplicationCommandOptionType.String, required: true, }, ], }; export const chatInput: ChatInputCommand = async (ctx) => { const time = ctx.options.getString('time', true); const message = ctx.options.getString('message', true); const timeMs = Date.now() + ms(time as `${number}`); await createTask({ name: 'remind', data: { userId: ctx.interaction.user.id, message, channelId: ctx.interaction.channelId, setAt: Date.now(), }, schedule: timeMs, }); await ctx.interaction.reply( `I will remind you for \`${message}\``, ); }; ``` ### The reminder task Create a static task definition that handles the actual reminder: ```ts // src/app/tasks/remind.ts import { task } from '@commandkit/tasks'; export interface RemindTaskData { userId: string; message: string; channelId: string; setAt: number; } export default task({ name: 'remind', async execute(ctx) { const { userId, message, channelId, setAt } = ctx.data; const channel = await ctx.client.channels.fetch(channelId); if (!channel?.isTextBased() || !channel.isSendable()) return; await channel.send({ content: `<@${userId}>`, embeds: [ { title: `You asked me to remind you about:`, description: message, color: 0x0099ff, timestamp: new Date(setAt).toISOString(), }, ], }); }, }); ``` ### Managing dynamic tasks You can also delete tasks programmatically: ```ts import { deleteTask } from '@commandkit/tasks'; // Cancel a scheduled task try { await deleteTask(taskId); console.log('Task cancelled successfully'); } catch (error) { console.error('Failed to cancel task:', error); } ``` ## Advanced patterns ### Task workflows Organize related tasks to create complex workflows: ```ts // src/app/tasks/data-processing.ts import { task } from '@commandkit/tasks'; export default task({ name: 'data-processing', schedule: '0 2 * * *', // Daily at 2 AM async execute(ctx) { // Step 1: Collect data const data = await collectData(); ctx.store.set('collectedData', data); // Step 2: Process data immediately const processedData = await processData(data); // Step 3: Send notification const channel = ctx.commandkit.client.channels.cache.get('log-channel'); if (channel?.isTextBased()) { await channel.send(`Data processing completed for ID: ${data.id}`); } }, }); ``` ### Task batching Process multiple items in batches: ```ts // src/app/tasks/batch-processing.ts import { task } from '@commandkit/tasks'; export default task({ name: 'batch-processor', schedule: '0 */6 * * *', // Every 6 hours async execute(ctx) { // Get items to process const items = await getItemsToProcess(); if (items.length === 0) { console.log('No items to process'); return; } // Process in batches of 10 const batchSize = 10; let processedCount = 0; for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); console.log( `Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(items.length / batchSize)}`, ); // Process the batch for (const item of batch) { await processItem(item); processedCount++; } } console.log(`Completed processing ${processedCount} items`); }, }); ``` ### Task state management Use the context store to manage state across task executions: ```ts // src/app/tasks/state-management.ts import { task } from '@commandkit/tasks'; export default task({ name: 'stateful-task', schedule: '0 */2 * * *', // Every 2 hours async prepare(ctx) { // Check if we're in a cooldown period const lastRun = ctx.commandkit.store.get('last-run'); const cooldown = 30 * 60 * 1000; // 30 minutes if (lastRun && Date.now() - lastRun < cooldown) { return false; // Skip execution } return true; }, async execute(ctx) { // Update last run time ctx.commandkit.store.set('last-run', Date.now()); // Get or initialize counter const counter = ctx.commandkit.store.get('execution-counter') || 0; ctx.commandkit.store.set('execution-counter', counter + 1); console.log(`Task executed ${counter + 1} times`); // Perform the actual work await performWork(); }, }); ``` ### Task cleanup Implement cleanup tasks for resource management: ```ts // src/app/tasks/cleanup.ts import { task } from '@commandkit/tasks'; export default task({ name: 'cleanup', schedule: '0 3 * * *', // Daily at 3 AM async execute(ctx) { const cutoffDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // 7 days ago // Clean up old data await cleanupOldRecords(cutoffDate); // Clean up expired tasks await cleanupExpiredTasks(); // Clean up temporary files await cleanupTempFiles(); console.log('Cleanup completed successfully'); }, }); ``` ## Best practices ### Use descriptive task names ```ts // Good await createTask({ name: 'user-reminder', data: { userId, message }, schedule: reminderTime, }); // Avoid await createTask({ name: 'task', data: { userId, message }, schedule: reminderTime, }); ``` ### Validate input data ```ts import type { CommandData, ChatInputCommand } from 'commandkit'; import { ApplicationCommandOptionType } from 'discord.js'; import ms from 'ms'; import { createTask } from '@commandkit/tasks'; export const chatInput: ChatInputCommand = async (ctx) => { const timeStr = ctx.options.getString('time', true); const message = ctx.options.getString('message', true); // Validate time format const delay = ms(timeStr as `${number}`); if (!delay || delay < 60000 || delay > 30 * 24 * 60 * 60 * 1000) { await ctx.interaction.reply( 'Please specify a time between 1 minute and 30 days.', ); return; } // Validate message length if (message.length > 1000) { await ctx.interaction.reply( 'Message too long. Please keep it under 1000 characters.', ); return; } // Create task await createTask({ name: 'reminder', data: { userId: ctx.interaction.user.id, message }, schedule: Date.now() + delay, }); await ctx.interaction.reply('Reminder scheduled successfully!'); }; ``` ### Handle errors gracefully ```ts await receive('user-events', async (message) => { try { // Process the message await processUserEvent(message); } catch (error) { // Log error but don't crash console.error('Failed to process event:', error); // Optionally retry or send to dead letter queue await handleFailedMessage(message, error); } }); ``` ### Implement proper error handling ```ts export default task({ name: 'resilient-task', async execute(ctx) { try { // Try the primary operation await performOperation(); } catch (error) { console.error('Primary operation failed:', error); // Try fallback operation try { await performFallbackOperation(); console.log('Fallback operation succeeded'); } catch (fallbackError) { console.error('Fallback operation also failed:', fallbackError); // Log the failure for manual intervention const channel = ctx.commandkit.client.channels.cache.get('error-channel'); if (channel?.isTextBased()) { await channel.send( `Task failed: ${error.message}. Fallback also failed: ${fallbackError.message}`, ); } } } }, }); ``` ## Driver comparison | Feature | SQLite | BullMQ | | -------------------- | ----------- | --------------- | | **Setup Complexity** | Simple | Moderate | | **Dependencies** | None | `bullmq`, Redis | | **Persistence** | File-based | Redis | | **Distributed** | No | Yes | | **Job Recovery** | Yes | Yes | | **Production Ready** | Development | Yes | | **Memory Usage** | Low | Moderate | ## Choosing the right driver ### Use SQLite when: - You're developing locally - You want persistent job storage during development - You have a single bot instance - You need job recovery on restart ### Use BullMQ when: - You have multiple bot instances - You need distributed task scheduling - You're deploying to production - You need advanced features like retries and monitoring --- ## @commandkit/queue The CommandKit Queue package provides a service-agnostic message queue API for inter-service communication. It allows you to send and receive messages between different parts of your application or across multiple services using a simple, unified interface. ## Features - **Service Agnostic**: Works with any message queue implementation - **Simple API**: Easy-to-use `send` and `receive` functions - **Type Safety**: Full TypeScript support with strong typing - **Driver System**: Pluggable drivers for different queue backends - **Discord.js Integration**: Built-in support for Discord.js brokers - **Redis Support**: Ready-to-use Redis PubSub driver ## Installation ```bash npm install @commandkit/queue ``` For Discord.js integration with Redis: ```bash npm install @commandkit/queue @discordjs/brokers ioredis ``` ## Basic setup ### Setting up the driver Before you can send or receive messages, you need to set up a driver. The driver handles the actual message queue backend. ```typescript import { setDriver } from '@commandkit/queue'; import { RedisPubSubDriver } from '@commandkit/queue/discordjs'; import { PubSubRedisBroker } from '@discordjs/brokers'; import Redis from 'ioredis'; // Create a Redis connection const redis = new Redis(); // Create a broker const broker = new PubSubRedisBroker(redis); // Create a driver const driver = new RedisPubSubDriver(broker); // Set the driver setDriver(driver); ``` ### Redis configuration You can configure Redis for different environments: ```typescript import Redis from 'ioredis'; // Local Redis const redis = new Redis(); // Or with configuration const redis = new Redis({ host: 'localhost', port: 6379, password: 'your-password', db: 0, }); // Cloud Redis (example with Redis Cloud) const redis = new Redis({ host: 'your-redis-host.redis.cloud.com', port: 6379, password: 'your-redis-password', tls: {}, }); ``` ## Core concepts ### Topics Topics are named channels where messages are sent and received. They function as categories for different types of messages. ### Messages Messages can be any JSON-serializable data. They're sent to topics and received by subscribers. ### Drivers Drivers are implementations that handle the actual message queue backend. CommandKit provides a unified API that works with any driver. ## Basic operations ### Sending messages Use the `send` function to publish messages to a topic. ```typescript import { send } from '@commandkit/queue'; // Send a simple message await send('user-events', { userId: '123', action: 'login' }); // Send different types of data await send('notifications', { type: 'welcome', userId: '123', message: 'Welcome to our platform!', }); await send('analytics', { event: 'page_view', page: '/dashboard', timestamp: Date.now(), }); ``` ### Receiving messages Use the `receive` function to subscribe to messages from a topic. ```typescript import { receive } from '@commandkit/queue'; // Basic message handling await receive('user-events', (message) => { console.log(`User ${message.userId} performed ${message.action}`); }); // Handle different message types await receive('notifications', (message) => { switch (message.type) { case 'welcome': console.log(`Welcome message for user ${message.userId}`); break; case 'reminder': console.log(`Reminder: ${message.message}`); break; } }); // Async message handling await receive('analytics', async (message) => { await processAnalyticsEvent(message); }); ``` ## Type safety You can define types for your messages to get better TypeScript support. ```typescript interface UserEvent { userId: string; action: 'login' | 'logout' | 'register'; timestamp?: number; } interface Notification { type: 'welcome' | 'reminder' | 'alert'; userId: string; message: string; } // Type-safe sending await send('user-events', { userId: '123', action: 'login', timestamp: Date.now(), } as UserEvent); // Type-safe receiving await receive('user-events', (message: UserEvent) => { console.log(`User ${message.userId} ${message.action}`); }); ``` You can also define typed events for better TypeScript support: ```typescript interface QueueEvents { 'user-updates': { userId: string; action: 'login' | 'logout' | 'register'; timestamp: number; }; 'guild-events': { guildId: string; event: 'member-join' | 'member-leave' | 'role-update'; data: any; }; analytics: { event: string; data: Record; timestamp: number; }; } // Create a typed driver const driver = new RedisPubSubDriver(broker); setDriver(driver); ``` ## Discord.js integration CommandKit Queue provides seamless integration with Discord.js through the `@discordjs/brokers` package and Redis PubSub. ### Cross-shard communication Send messages between different shards of your Discord.js application: ```typescript // In shard 0 await send('shard-communication', { fromShard: 0, toShard: 1, type: 'user-status-update', data: { userId: '123456789', status: 'online' }, }); // In shard 1 (receiving) await receive('shard-communication', (message) => { if (message.toShard === 1) { console.log(`Received from shard ${message.fromShard}:`, message.data); } }); ``` ### Multi-bot communication Send messages between different Discord.js bots: ```typescript // In bot A await send('bot-communication', { fromBot: 'bot-a', toBot: 'bot-b', type: 'user-data-request', data: { userId: '123456789' }, }); // In bot B await receive('bot-communication', async (message) => { if (message.toBot === 'bot-b') { const userData = await getUserData(message.data.userId); await send('bot-communication', { fromBot: 'bot-b', toBot: 'bot-a', type: 'user-data-response', data: userData, }); } }); ``` ### Real-time updates Send real-time updates to connected clients: ```typescript // When a user joins a voice channel await send('voice-updates', { userId: '123456789', guildId: '987654321', channelId: '111222333', action: 'join', timestamp: Date.now(), }); // Handle voice updates await receive('voice-updates', (message) => { // Update voice channel status updateVoiceChannelStatus( message.guildId, message.channelId, message.userId, message.action, ); // Notify other users notifyVoiceChannelUsers(message.guildId, message.channelId, message); }); ``` ## Error handling Always handle errors when sending or receiving messages. ```typescript // Handle Redis connection errors redis.on('error', (error) => { console.error('Redis connection error:', error); }); redis.on('connect', () => { console.log('Connected to Redis'); }); // Handle broker errors broker.on('error', (error) => { console.error('Broker error:', error); }); // Handle message processing errors await receive('user-updates', async (message) => { try { await processUserUpdate(message); } catch (error) { console.error('Failed to process user update:', error); // Optionally retry or send to dead letter queue await send('failed-messages', { originalTopic: 'user-updates', message, error: error.message, timestamp: Date.now(), }); } }); ``` ## Best practices ### Use descriptive topic names ```typescript // Good await send('user-authentication-events', message); await send('order-processing-updates', message); // Avoid await send('events', message); await send('data', message); ``` ### Structure your messages ```typescript // Good - structured message await send('user-events', { type: 'login', userId: '123', timestamp: Date.now(), }); // Avoid - unstructured await send('user-events', 'user logged in'); ``` ### Handle message processing gracefully ```typescript await receive('user-events', async (message) => { try { // Process the message await processUserEvent(message); // Acknowledge successful processing console.log(`Processed event for user ${message.userId}`); } catch (error) { // Log error but don't crash console.error(`Failed to process event for user ${message.userId}:`, error); // Optionally retry or send to dead letter queue await handleFailedMessage(message, error); } }); ``` ### Use appropriate message sizes ```typescript // Good - reasonable message size await send('user-profile-updates', { userId: '123', changes: { displayName: 'New Name', avatar: 'https://example.com/avatar.jpg', }, }); // Avoid - very large messages await send('user-profile-updates', { userId: '123', fullProfile: {/* massive object */}, }); ``` ## Use cases - **Inter-Service Communication**: Send messages between different parts of your application - **Event Broadcasting**: Broadcast events to multiple subscribers - **Task Distribution**: Distribute work across multiple workers - **Real-time Updates**: Send real-time updates to connected clients - **Microservices**: Enable communication between microservices ## Cleanup Always clean up resources when shutting down: ```typescript import { setDriver } from '@commandkit/queue'; // Cleanup function async function cleanup() { try { // Close the driver const driver = getDriver(); // You'll need to implement this if (driver && driver.close) { await driver.close(); } // Close Redis connection await redis.quit(); console.log('Queue cleanup completed'); } catch (error) { console.error('Error during cleanup:', error); } } // Handle graceful shutdown process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); ``` --- ## @commandkit/workflow The `@commandkit/workflow` plugin integrates [Workflow DevKit](https://useworkflow.dev) into your CommandKit application, enabling you to build durable, long-running workflows that can suspend, resume, and maintain state across restarts or failures. Workflows are perfect for building complex multi-step processes like user onboarding sequences, scheduled reminders, data processing pipelines, or any task that requires reliability and state persistence. ## Installation ```bash npm install @commandkit/workflow workflow ``` ## Basic setup Add the workflow plugin to your CommandKit configuration: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit/config'; import { workflow } from '@commandkit/workflow'; export default defineConfig({ plugins: [workflow()], }); ``` ## Understanding workflows Workflows are durable functions that can maintain their execution state across restarts, failures, or user events. They consist of two types of functions: - **Workflow Functions**: Functions that orchestrate steps using `"use workflow"` directive - **Step Functions**: Functions that perform actual work using `"use step"` directive For comprehensive documentation on workflows, steps, control flow patterns, error handling, and more, refer to the [official Workflow DevKit documentation](https://useworkflow.dev). ## Creating workflows Create workflow files in the `src/workflows` directory. Each workflow should export a workflow function that orchestrates one or more step functions. ### Basic workflow structure ```ts title="src/workflows/greet/greet.workflow.ts" import { sleep } from 'workflow'; import { greetUser } from './steps/greet.step'; export async function greetUserWorkflow(userId: string) { 'use workflow'; await greetUser(userId); await sleep('5 seconds'); await greetUser(userId, true); return { success: true }; } ``` ### Step functions Step functions perform the actual work and have full Node.js runtime access. They can use CommandKit hooks and any npm packages: ```ts title="src/workflows/greet/steps/greet.step.ts" import { useClient } from 'commandkit/hooks'; export async function greetUser(userId: string, again = false) { 'use step'; const client = useClient(); const user = await client.users.fetch(userId); const message = again ? 'Hello again!' : 'Hello!'; await user.send(message); } ``` ## Starting workflows Use the `start` function from `workflow/api` to start workflows from your commands or other parts of your application: ```ts title="src/app/commands/greet.ts" import type { CommandData, ChatInputCommand } from 'commandkit'; import { start } from 'workflow/api'; import { greetUserWorkflow } from '@/workflows/greet/greet.workflow'; export const command: CommandData = { name: 'greet', description: 'Greet a user with a workflow', }; export const chatInput: ChatInputCommand = async (ctx) => { await ctx.interaction.reply("I'm going to greet you! :wink:"); await start(greetUserWorkflow, [ctx.interaction.user.id]); }; ``` ## Using custom Worlds You can use any world providers from [useworkflow.dev/worlds](https://useworkflow.dev/worlds) or create your own custom world. Refer to the [Building a World](https://useworkflow.dev/docs/deploying/building-a-world) documentation for more details. ## Project structure Organize your workflows and steps in a clear structure: ``` src/ workflows/ greet/ greet.workflow.ts steps/ greet.step.ts userOnboarding/ index.ts steps/ send-welcome-message.ts assign-role.ts log-activity.ts shared/ validate-input.ts log-activity.ts ``` You can organize steps into a single `steps.ts` file or separate files within a `steps` folder. The `shared` folder is useful for common steps used by multiple workflows. ## Advanced examples ### User onboarding workflow Create a multi-step onboarding process: ```ts title="src/workflows/onboarding/onboarding.workflow.ts" import { sleep } from 'workflow'; import { sendWelcomeMessage } from './steps/send-welcome-message'; import { assignRole } from './steps/assign-role'; import { sendFollowUpMessage } from './steps/send-follow-up-message'; export async function userOnboardingWorkflow(userId: string, guildId: string) { 'use workflow'; await sendWelcomeMessage(userId, guildId); await assignRole(userId, guildId); await sleep('7 days'); await sendFollowUpMessage(userId, guildId); return { userId, status: 'completed' }; } ``` ### Step with error handling ```ts title="src/workflows/onboarding/steps/send-welcome-message.ts" import { FatalError } from 'workflow'; import { useClient } from 'commandkit/hooks'; export async function sendWelcomeMessage(userId: string, guildId: string) { 'use step'; const client = useClient(); try { const guild = await client.guilds.fetch(guildId); const member = await guild.members.fetch(userId); const welcomeChannel = guild.channels.cache.find( (channel) => channel.name === 'welcome', ); if (welcomeChannel?.isTextBased()) { await welcomeChannel.send({ content: `Welcome to ${guild.name}, <@${userId}>! 🎉`, embeds: [ { title: 'Welcome!', description: 'Thanks for joining our server!', color: 0x00ff00, }, ], }); } else { const user = await client.users.fetch(userId); await user.send({ embeds: [ { title: 'Welcome!', description: `Thanks for joining ${guild.name}!`, color: 0x00ff00, }, ], }); } } catch (error: any) { if (error.code === 50007) { throw new FatalError('User has DMs disabled'); } if (error.code === 10007) { throw new FatalError('Member not found in guild'); } throw error; } } ``` ### Parallel execution Execute multiple steps in parallel: ```ts title="src/workflows/memberSetup/memberSetup.workflow.ts" import { assignRoles, sendWelcomeMessage, createPrivateChannel, logMemberJoin, } from './steps'; export async function memberSetupWorkflow(userId: string, guildId: string) { 'use workflow'; const [roles, channel, log] = await Promise.all([ assignRoles(userId, guildId), createPrivateChannel(userId, guildId), logMemberJoin(userId, guildId), ]); await sendWelcomeMessage(userId, guildId, channel.id); return { userId, roles, channelId: channel.id, logged: log }; } ``` ## Using CommandKit hooks in steps Step functions have full access to CommandKit hooks: ```ts import { useClient, useCommandKit } from 'commandkit/hooks'; export async function updateMemberStats(userId: string, guildId: string) { 'use step'; const client = useClient(); const commandkit = useCommandKit(); const guild = await client.guilds.fetch(guildId); const member = await guild.members.fetch(userId); const stats = { joinedAt: member.joinedAt?.toISOString(), roles: member.roles.cache.size, lastUpdated: Date.now(), }; commandkit.store.set(`member:${userId}:stats`, stats); return { userId, stats }; } ``` ## Best practices ### Organize workflows and steps Keep workflows focused on orchestration and steps focused on individual tasks: ```ts // Good - workflow orchestrates, steps do work export async function memberVerificationWorkflow( userId: string, guildId: string, ) { 'use workflow'; const member = await fetchMember(userId, guildId); const verified = await verifyMember(member); return { userId, verified, status: 'completed' }; } // Avoid - doing too much in the workflow export async function badWorkflow(userId: string) { 'use workflow'; // Don't do actual work here - use steps instead const user = await fetch('...'); // ❌ } ``` ### Handle errors appropriately Use `FatalError` for errors that shouldn't retry and `RetryableError` for custom retry behavior: ```ts import { FatalError, RetryableError } from 'workflow'; import { useClient } from 'commandkit/hooks'; export async function sendModerationAction( userId: string, guildId: string, action: 'warn' | 'mute' | 'kick', ) { 'use step'; const client = useClient(); const guild = await client.guilds.fetch(guildId); const member = await guild.members.fetch(userId); if (!member) { throw new FatalError('Member not found in guild'); } try { if (action === 'warn') { await member.send('You have been warned for violating server rules.'); } else if (action === 'mute') { await member.timeout(60 * 60 * 1000, 'Violation of server rules'); } else if (action === 'kick') { await member.kick('Violation of server rules'); } return { userId, action, success: true }; } catch (error: any) { if (error.code === 50013) { throw new FatalError('Missing permissions to perform action'); } if (error.code === 50035) { throw new RetryableError('Rate limited by Discord', { retryAfter: '1 minute', }); } throw error; } } ``` ### Use descriptive names ```ts // Good export async function userOnboardingWorkflow(userId: string) { 'use workflow'; // ... } // Avoid export async function workflow1(userId: string) { 'use workflow'; // ... } ``` ### Keep steps idempotent Since steps can retry, ensure they're idempotent when performing side effects: ```ts export async function assignRole(userId: string, roleId: string) { 'use step'; const client = useClient(); const member = await client.guilds.cache .get('guild-id') ?.members.fetch(userId); if (!member) { throw new FatalError('Member not found'); } if (member.roles.cache.has(roleId)) { return { alreadyAssigned: true }; } await member.roles.add(roleId); return { assigned: true }; } ``` ## Troubleshooting ### Workflow not executing 1. **Check the workflow plugin is registered** in your `commandkit.config.ts` 2. **Verify workflow files are in `src/workflows`** directory 3. **Ensure you're using `start` function** from `workflow/api` to start workflows 4. **Check for compilation errors** in your workflow or step functions ### Step errors 1. **Verify CommandKit hooks** are used correctly in steps 2. **Check error handling** - use `FatalError` for non-retryable errors 3. **Ensure steps are idempotent** when performing side effects 4. **Review step retry behavior** - steps retry up to 3 times by default ### Build issues 1. **Separate workflows and steps** into different files to avoid bundler issues 2. **Check TypeScript configuration** for proper module resolution 3. **Verify workflow directives** (`"use workflow"` and `"use step"`) are present ## Learn more For comprehensive documentation on workflows, including: - Workflow and step fundamentals - Control flow patterns (sequential, parallel, conditional) - Error handling and retrying - Webhooks and external integrations - Serialization and idempotency - Observability and debugging Visit the [official Workflow DevKit documentation](https://useworkflow.dev). --- ## Community Plugins CommandKit plugins are a new way to extend the functionality of your Discord application. If you've built a plugin that you'd like to share with the community, please submit a pull request to the [CommandKit GitHub repository](https://github.com/neplexlabs/commandkit) and adding your plugin to the list below. :::info If you'd like to learn how to create a plugin for CommandKit, you can check out the following guides: - [Creating a runtime plugin](./07-creating-plugins/01-creating-runtime-plugin.mdx) - [Creating a compiler plugin](./07-creating-plugins/02-creating-compiler-plugin.mdx) ::: ### [YangonKit](https://github.com/retrouser955/yangonkit) YangonKit is a compiler plugin for CommandKit that further reduces the boilerplate of creating a CommandKit application by providing svelte-like syntax for defining commands, and options. - [Documentation](https://github.com/retrouser955/yangonkit#readme) - [GitHub repository](https://github.com/retrouser955/yangonkit) - [npm package](https://www.npmjs.com/package/yangonkit) ### [Example plugin](#) This is a short placeholder description to help with the structure of this list, and future pull requests. - [Documentation](#) - [GitHub repository](#) - [npm package](#) --- ## Creating a Runtime Plugin Runtime plugins are used to modify the way CommandKit runs your application. They are executed at runtime and can be used to modify the behavior of CommandKit. These plugins can register new commands, modify existing ones, or even change the way CommandKit works. For example, you can use a runtime plugin to add new commands or even custom handlers. ## Getting started with runtime plugins To create a runtime plugin, you need to create a class that extends the [`RuntimePlugin`](../../api-reference/commandkit/classes/runtime-plugin.mdx) class from CommandKit. This class provides various hooks that you can use to modify the behavior of CommandKit at different stages of execution. Here's a basic example of a runtime plugin that logs when the bot logs in: ```ts title="my-runtime-plugin.ts" import { type CommandKitPluginRuntime, RuntimePlugin } from 'commandkit'; export class MyRuntimePlugin extends RuntimePlugin { // Override any hooks you want to use async onAfterClientLogin(ctx: CommandKitPluginRuntime): Promise { console.log('Bot has logged in!'); } } ``` To use this plugin in your application, you can add it to your CommandKit instance. ```ts title="commandkit.config.ts" import { CommandKit } from 'commandkit'; import { MyRuntimePlugin } from './plugins/MyRuntimePlugin'; const commandkit = new CommandKit({ // ... other options plugins: [new MyRuntimePlugin()], }); ``` ## Lifecycle hooks Runtime plugins provide hooks at different stages of your application's lifecycle. These hooks allow you to perform actions before or after key events in your bot's execution. ### Initialization hooks These hooks are called during the initialization of your CommandKit application. #### `onBeforeCommandsLoad(ctx)` Called before commands are loaded into CommandKit. **Parameters:** - `ctx`: The runtime context, providing access to CommandKit internals like the client, command context, etc. **Example use case:** Initialize resources needed by commands, or modify command loading behavior. ```ts title="my-runtime-plugin.ts" async onBeforeCommandsLoad(ctx: CommandKitPluginRuntime): Promise { console.log('Loading commands soon...'); // Initialize resources needed for commands } ``` #### `onAfterCommandsLoad(ctx)` Called after all commands have been loaded into CommandKit. **Parameters:** - `ctx`: The runtime context. **Example use case:** Log the loaded commands, perform validation, or add metadata to loaded commands. ```ts title="my-runtime-plugin.ts" async onAfterCommandsLoad(ctx: CommandKitPluginRuntime): Promise { console.log(`Loaded ${ctx.commands.size} commands!`); // Do something with the loaded commands } ``` #### `onBeforeEventsLoad(ctx)` Called before event handlers are loaded into CommandKit. **Parameters:** - `ctx`: The runtime context. **Example use case:** Set up prerequisites for event handlers. ```ts title="my-runtime-plugin.ts" async onBeforeEventsLoad(ctx: CommandKitPluginRuntime): Promise { console.log('Loading event handlers soon...'); // Prepare event handling prerequisites } ``` #### `onAfterEventsLoad(ctx)` Called after all event handlers have been loaded. **Parameters:** - `ctx`: The runtime context. **Example use case:** Log the loaded events, add custom event processing. ```ts title="my-runtime-plugin.ts" async onAfterEventsLoad(ctx: CommandKitPluginRuntime): Promise { console.log('All event handlers loaded!'); // Perform post-event-loading actions } ``` #### `onBeforeClientLogin(ctx)` Called right before the Discord client attempts to log in. **Parameters:** - `ctx`: The runtime context. **Example use case:** Perform last-minute setup before connecting to Discord. ```ts title="my-runtime-plugin.ts" async onBeforeClientLogin(ctx: CommandKitPluginRuntime): Promise { console.log('Bot is about to connect to Discord...'); // Last minute preparations before login } ``` #### `onAfterClientLogin(ctx)` Called after the Discord client has successfully logged in. **Parameters:** - `ctx`: The runtime context. **Example use case:** Start additional services once the bot is online, log connection details. ```ts title="my-runtime-plugin.ts" async onAfterClientLogin(ctx: CommandKitPluginRuntime): Promise { console.log(`Bot logged in as ${ctx.client.user?.tag}!`); // Start additional services, initialize post-login resources } ``` ### Router initialization hooks These hooks are called when the command and event routers are initialized. #### `onCommandsRouterInit(ctx)` Called after the commands router is initialized. **Parameters:** - `ctx`: The runtime context. **Example use case:** Add custom command routing logic or middleware. ```ts title="my-runtime-plugin.ts" async onCommandsRouterInit(ctx: CommandKitPluginRuntime): Promise { console.log('Commands router initialized!'); // Add custom middleware or routing logic } ``` #### `onEventsRouterInit(ctx)` Called after the events router is initialized. **Parameters:** - `ctx`: The runtime context. **Example use case:** Add custom event handling logic. ```ts title="my-runtime-plugin.ts" async onEventsRouterInit(ctx: CommandKitPluginRuntime): Promise { console.log('Events router initialized!'); // Set up custom event processing } ``` ## Event handler hooks These hooks are called when processing events. #### `willEmitEvent(ctx, event)` Called before an event is emitted. **Parameters:** - `ctx`: The runtime context. - `event`: The event object being emitted. **Example use case:** Modify event data before it's emitted, or cancel the event. ```ts title="my-runtime-plugin.ts" async willEmitEvent( ctx: CommandKitPluginRuntime, event: CommandKitEventDispatch ): Promise { // prevent other plugins from handling this event event.accept(); // Modify event data event.args.push('This is a custom argument'); } ``` Now the event listener will receive the modified event data. ## Command registration hooks These hooks are called during the command registration process, allowing you to modify commands before they're registered with Discord. #### `prepareCommand(ctx, commands)` Called before a command is loaded for registration, allowing you to modify the command data. **Parameters:** - `ctx`: The runtime context. - `commands`: The command object being loaded (CommandBuilderLike). **Returns:** The modified command or `null` to use the original. **Example use case:** Add default options to commands, modify command properties. ```ts title="my-runtime-plugin.ts" async prepareCommand( ctx: CommandKitPluginRuntime, commands: CommandBuilderLike ): Promise { // Add a disclaimer to all command descriptions if (commands.description) { commands.description = `[BETA] ${commands.description}`; } return commands; } ``` #### `onBeforeRegisterCommands(ctx, event)` Called before command registration process starts, allowing you to cancel or modify the registration. **Parameters:** - `ctx`: The runtime context. - `event`: The command registration event data. **Example use case:** Log commands being registered, filter commands, or handle registration manually. ```ts title="my-runtime-plugin.ts" async onBeforeRegisterCommands( ctx: CommandKitPluginRuntime, event: PreRegisterCommandsEvent ): Promise { console.log(`Registering ${event.commands.length} commands...`); // You can modify event.handled = true to cancel standard registration } ``` #### `onBeforeRegisterGlobalCommands(ctx, event)` Called before global commands are registered with Discord. **Parameters:** - `ctx`: The runtime context. - `event`: The command registration event data. **Example use case:** Handle global command registration differently. ```ts title="my-runtime-plugin.ts" async onBeforeRegisterGlobalCommands( ctx: CommandKitPluginRuntime, event: PreRegisterCommandsEvent ): Promise { console.log(`Registering ${event.commands.length} global commands...`); // Custom global command registration logic } ``` #### `onBeforePrepareGuildCommandsRegistration(ctx, event)` Called before guild commands are prepared for registration, before guilds are resolved. **Parameters:** - `ctx`: The runtime context. - `event`: The command registration event data. **Example use case:** Modify guild commands before guild resolution. ```ts title="my-runtime-plugin.ts" async onBeforePrepareGuildCommandsRegistration( ctx: CommandKitPluginRuntime, event: PreRegisterCommandsEvent ): Promise { console.log('Preparing guild commands for registration...'); // Modify guild commands before guilds are resolved } ``` #### `onBeforeRegisterGuildCommands(ctx, event)` Called before guild commands are registered, after guilds have been resolved. **Parameters:** - `ctx`: The runtime context. - `event`: The command registration event data. **Example use case:** Custom guild command registration logic. ```ts title="my-runtime-plugin.ts" async onBeforeRegisterGuildCommands( ctx: CommandKitPluginRuntime, event: PreRegisterCommandsEvent ): Promise { console.log('Registering guild commands...'); // Custom guild command registration } ``` ## Interaction hooks These hooks are called when processing interactions and messages. #### `onBeforeInteraction(ctx, interaction)` Called before an interaction is handled. **Parameters:** - `ctx`: The runtime context. - `interaction`: The Discord.js Interaction object. **Example use case:** Log interactions, perform checks before handling, add analytics. ```ts title="my-runtime-plugin.ts" async onBeforeInteraction( ctx: CommandKitPluginRuntime, interaction: Interaction ): Promise { if (interaction.isCommand()) { console.log(`Command used: ${interaction.commandName}`); } // Add analytics, perform checks, etc. } ``` #### `onBeforeMessageCommand(ctx, message)` Called before a message command is processed. **Parameters:** - `ctx`: The runtime context. - `message`: The Discord.js Message object. **Example use case:** Filter messages, add logging, perform checks. ```ts title="my-runtime-plugin.ts" async onBeforeMessageCommand( ctx: CommandKitPluginRuntime, message: Message ): Promise { console.log(`Potential message command: ${message.content}`); // Message filtering, logging, etc. } ``` #### `executeCommand(ctx, env, source, command, execute)` Called before a command is executed, allowing you to handle command execution yourself. **Parameters:** - `ctx`: The runtime context. - `env`: The CommandKitEnvironment containing command context. - `source`: The interaction or message that triggered the command. - `command`: The prepared command execution. - `execute`: The function that would normally execute the command. **Returns:** `true` if you handled the command execution, `false` to let CommandKit handle it. **Example use case:** Custom command execution, permission checking, rate limiting. ```ts title="my-runtime-plugin.ts" async executeCommand( ctx: CommandKitPluginRuntime, env: CommandKitEnvironment, source: Interaction | Message, command: PreparedAppCommandExecution, execute: () => Promise ): Promise { // Check if command is being rate limited if (this.isRateLimited(command.name, source.user.id)) { if (source instanceof Interaction && source.isRepliable()) { await source.reply('You are being rate limited! Try again later.'); } return true; // We handled it, don't execute the normal way } // Let CommandKit handle it normally return false; } ``` #### `onAfterCommand(ctx, env)` Called after a command and all its deferred functions are executed. **Parameters:** - `ctx`: The runtime context. - `env`: The CommandKitEnvironment containing command context. **Example use case:** Logging, analytics, cleanup after command execution. ```ts title="my-runtime-plugin.ts" async onAfterCommand( ctx: CommandKitPluginRuntime, env: CommandKitEnvironment ): Promise { console.log(`Command ${env.command.name} executed by ${env.user?.tag}`); // Log command usage, perform cleanup, etc. } ``` ## Development hooks These hooks are used during development. #### `performHMR(ctx, event)` Called when a Hot Module Replacement event is received in development mode. **Parameters:** - `ctx`: The runtime context. - `event`: The HMR event data. **Example use case:** Handle reloading of specific resources during development. ```ts title="my-runtime-plugin.ts" async performHMR( ctx: CommandKitPluginRuntime, event: CommandKitHMREvent ): Promise { console.log(`HMR event: ${event.type}`); // Custom HMR handling } ``` ## Creating a Complete Runtime Plugin Now that we've covered all available hooks, let's put together a more complete example of a runtime plugin that implements several hooks: ```ts title="my-runtime-plugin.ts" import { type PreparedAppCommandExecution, type CommandKitEnvironment, type CommandKitPluginRuntime, type CommandBuilderLike, RuntimePlugin, } from 'commandkit'; import type { Interaction, Message } from 'discord.js'; export class LoggingPlugin extends RuntimePlugin { private commandUsage = new Map(); async onAfterClientLogin(ctx: CommandKitPluginRuntime): Promise { console.log(`Bot logged in as ${ctx.client.user?.tag}!`); console.log(`Serving ${ctx.client.guilds.cache.size} guilds`); } async onBeforeInteraction( ctx: CommandKitPluginRuntime, interaction: Interaction, ): Promise { if (interaction.isCommand()) { console.log( `Command "${interaction.commandName}" used by ${interaction.user.tag}`, ); } } async executeCommand( ctx: CommandKitPluginRuntime, env: CommandKitEnvironment, source: Interaction | Message, command: PreparedAppCommandExecution, execute: () => Promise, ): Promise { const startTime = Date.now(); // Let CommandKit handle normal execution await execute(); // Log execution time const execTime = Date.now() - startTime; console.log(`Command "${command.name}" executed in ${execTime}ms`); // Track usage this.commandUsage.set( command.name, (this.commandUsage.get(command.name) || 0) + 1, ); return true; // We've handled it } async prepareCommand( ctx: CommandKitPluginRuntime, command: CommandBuilderLike, ): Promise { // Add dev tag to command names in development environment if (process.env.NODE_ENV === 'development') { command.name = `dev_${command.name}`; } return command; } } ``` ## Best practices When creating runtime plugins, here are some best practices to follow: 1. **Keep it focused** - Each plugin should have a specific purpose. 2. **Handle errors** - Wrap your hook implementations in try/catch blocks to prevent crashing your bot. 3. **Be performance-conscious** - Some hooks may be called frequently, so keep your code efficient. 4. **Respect the return values** - Make sure to return the appropriate values from hooks, especially for hooks like `executeCommand` that can change the execution flow. 5. **Document your plugins** - If you're creating plugins for others to use, document how they should be configured and used. --- ## Creating a Compiler Plugin CommandKit compiler plugins are a powerful way to extend the functionality of your CommandKit application by modifying the way CommandKit compiles your application. They are used to add new features or modify existing ones. For example, you can use a compiler plugin to transform the source code itself (e.g. CommandKit's `"use cache"` or `"use macro"` directives). ## Creating a compiler plugin To create a compiler plugin, you need to create a new class that extends the [`CompilerPlugin`](../../api-reference/commandkit/classes/compiler-plugin.mdx) class. This class should implement the `transform` method, which is called by CommandKit to transform the source code. The `transform` method should return the transformed source code. ```ts title="my-compiler-plugin.ts" import { type CompilerPluginRuntime, CompilerPlugin, MaybeFalsey, PluginTransformParameters, TransformedResult, } from 'commandkit'; export interface MyPluginOptions { // you can define any options you want for your plugin here // these options can be accessed via `this.options` inside the plugin class } export class MyPlugin extends CompilerPlugin { // this is the name of the plugin public readonly name = 'my-plugin'; public async activate(ctx: CompilerPluginRuntime): Promise { // this is where you can keep initialization logic } public async deactivate(ctx: CompilerPluginRuntime): Promise { // this is where you can keep cleanup logic } public async transform( params: PluginTransformParameters, ): Promise> { const { contents } = params; // this is just an example, you can do whatever you want with the contents const transformedContents = contents.replace( /console\.log/g, 'console.warn', ); // return the transformed contents return { code: transformedContents, map: null, // you can also return a source map if you want }; } } ``` ## Extending CommandKit templates CommandKit templates are a way to autogenerate files and other contents through the `commandkit create` CLI command. CommandKit by default comes with `command` and `event` templates. Compiler plugins can register custom templates to be used by the `commandkit create` command. ```ts title="my-compiler-plugin.ts" import { type CompilerPluginRuntime, CompilerPlugin } from 'commandkit'; export class MyPlugin extends CompilerPlugin { public readonly name = 'my-plugin'; public async activate(ctx: CompilerPluginRuntime): Promise { // Registers `commandkit create event ` template ctx.registerTemplate('event', async (args: string[]) => { const [name, path] = args; const template = ` export default async function on${name[0].toUpperCase() + name.slice(1)}() { console.log('${name} event fired!'); }; `.trim(); await writeFile(join(path, 'event.ts'), template); }); } public async deactivate(ctx: CompilerPluginRuntime): Promise { // remove the template from the registry when the plugin is deactivated ctx.unregisterTemplate('event'); } } ``` :::warning The `registerTemplate` method must be called inside the `activate` method, and the `unregisterTemplate` method must be called inside the `deactivate` method. ::: :::info Plugins may override the default templates by registering their own templates with the same name. ::: ## Using rolldown plugins You can also use rolldown plugins as compiler plugins to transform your source code. ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { someRolldownPlugin } from 'some-rolldown-plugin'; export default defineConfig({ rolldownPlugins: [someRolldownPlugin()], }); ``` --- ## Setup CommandKit manually import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; If you don't want to use the [CLI](../01-getting-started/02-setup-commandkit.mdx) to automatically generate a base CommandKit project, you can integrate CommandKit manually into your application. ## Installation To install CommandKit, you can use the following command: ```sh npm2yarn npm install commandkit ``` ## Configuration file To get started, create a configuration file at the root of your project. This file is used to configure CommandKit and optionally its plugins. ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; export default defineConfig({}); ``` ```js title="commandkit.config.js" import { defineConfig } from 'commandkit'; export default defineConfig({}); ``` ## Entrypoint file Then, create a folder called `src`, followed by an app file inside it. The file should look like this: ```ts title="src/app.ts" import { Client } from 'discord.js'; const client = new Client({ intents: ['Guilds'], }); client.token = '...'; // Optional: Manually set a bot token export default client; ``` ```js title="src/app.js" import { Client } from 'discord.js'; const client = new Client({ intents: ['Guilds'], }); client.token = '...'; // Optional: Manually set a bot token export default client; ``` With the current entrypoint file created, it's important to understand that: 1. The `app.ts` file is the entrypoint for this application. This file **must** export your discord.js client instance. 2. You don't have to call `client.login()` as CommandKit will handle that for you. 3. You should store your bot token as an environment variable with either `DISCORD_TOKEN` or `TOKEN` as the name. 4. If you prefer to have a custom environment variable for your bot token, you can manually assign it to the `client.token` property. ## Adding commands To add a command, create a folder inside the `src/app` directory called `commands` and create your command file. This example will use a simple ping/pong command which will register as a chat input (slash) and message command. ```ts title="src/app/commands/ping.ts" import type { ChatInputCommand, CommandData, MessageCommand } from 'commandkit'; export const command: CommandData = { name: 'ping', description: 'Pong!', }; export const chatInput: ChatInputCommand = async ({ interaction }) => { await interaction.reply('Pong!'); }; export const message: MessageCommand = async ({ message }) => { await message.reply('Pong!'); }; ``` ```js title="src/app/commands/ping.js" /** * @typedef {import('commandkit').ChatInputCommand} ChatInputCommand * @typedef {import('commandkit').CommandData} CommandData * @typedef {import('commandkit').MessageCommand} MessageCommand */ /** @type {CommandData} */ export const command = { name: 'ping', description: 'Pong!', }; /** @type {ChatInputCommand} */ export const chatInput = async ({ interaction }) => { await interaction.reply('Pong!'); }; /** @type {MessageCommand} */ export const message = async ({ message }) => { await message.reply('Pong!'); }; ``` This command will reply with "Pong!" when the user runs the `/ping` slash command, or sends `!ping` in the chat. :::tip The prefix for message commands can be changed globally or per-guild. Learn more [here](../02-commands/04-message-commands.mdx#custom-prefixes). ::: ## Adding events To register and handle events emitted by your discord.js client, create a folder inside the `src/app` directory called `events` and create a folder with the name of the discord.js event you'd like to handle (e.g. clientReady, messageCreate, etc). This example will use the `clientReady` event. In the `src/app/events/clientReady` directory, you can create files which will export default functions that will be called when the respective event is emitted by discord.js. Following the `clientReady` event example mentioned above, you may want to log when your bot comes online. The function for that will look like so: ```ts title="src/app/events/clientReady/log.ts" import type { Client } from 'discord.js'; export default function (client: Client) { console.log(`Logged in as ${client.user.username}!`); } ``` ```js title="src/app/events/clientReady/log.js" /** * Event handler for when the bot is ready * @param {import('discord.js').Client} client - The Discord client instance */ export default function (client) { console.log(`Logged in as ${client.user.username}!`); } ``` :::tip You can find a full list of events under the [discord.js Client class](https://discord.js.org/docs/packages/discord.js/main/Client:Class). ::: ## Running the app in development To run your application in development mode, you can use the `commandkit dev` command in your terminal. This will allow you to quickly reload specific parts of your application using Hot Module Replacement (HMR) when you make changes to your code, without having to restart the entire application. ```sh npm2yarn npx commandkit dev ``` :::warning When running in development mode, CommandKit will generate a `.commandkit` folder in the root of your project. This folder contains the compiled files used in development mode. You should not commit this folder to your version control system by making sure you add it to your `.gitignore` file. ::: ## Building for production When you are ready to deploy your bot, you can use `commandkit build` to create a production build of your bot. This will create a `dist` folder in your project directory containing the compiled files. ```sh npx commandkit build ``` :::warning After running your build script, CommandKit will generate a `dist` folder in the root of your project. This folder contains the compiled files used for production. You should not commit this folder to your version control system by making sure you add it to your `.gitignore` file. ::: ## Running the app in production You can use `commandkit start` to start the bot in production mode. This will load the environment variables from the `.env` file in the root of your project directory. ```sh npx commandkit start ``` Alternatively, you can manually start the bot in production mode using a runtime environment of your choice: ```sh node dist/index.js ``` --- ## File Naming Conventions ## app.ts The `src/app.ts` (or `app.js`) file is the entry point for your application. It should define and export your `discord.js` client instance. ```ts title="src/app.ts" import { Client } from 'discord.js'; const client = new Client({/* options */}); client.token = 'YOUR_BOT_TOKEN'; export default client; ``` ## Middleware Files The `src/app/commands` tree supports three middleware filenames: - `+global-middleware.ts`: runs for every command - `+middleware.ts`: runs for leaves defined in the same directory - `+.middleware.ts`: runs for the matching leaf token in the same directory ```ts title="src/app/commands/+middleware.ts" import { MiddlewareContext } from 'commandkit'; export function beforeExecute(context: MiddlewareContext) { console.log('Before command execution'); } export function afterExecute(context: MiddlewareContext) { console.log('After command execution'); } ``` :::info You can learn more about middleware behavior [here](../02-commands/08-middlewares.mdx). ::: ## (category) Directory Use parenthesis directories such as `(Moderation)` for organization only. Categories help structure the repo and category metadata, but they do not change slash command routes. ```text src/app/commands/ └── (Moderation)/ ├── ban.ts ├── kick.ts ├── mute.ts ├── unmute.ts ├── warn.ts ├── warn-list.ts └── warn-remove.ts ``` :::info You can learn more about command categories [here](../02-commands/07-category-directory.mdx). ::: ## Hierarchical Command Directories CommandKit uses distinct tokens for hierarchical command trees: - `[command]` for root command directories - `{group}` for subcommand group directories - `command.ts` for directory-backed command nodes - `group.ts` for group definition files - `*.subcommand.ts` for shorthand leaves - `[leaf]/command.ts` for directory-backed leaves ```text src/app/commands/ └── [settings]/ ├── command.ts ├── profile.subcommand.ts └── {notifications}/ ├── group.ts ├── enable.subcommand.ts └── [disable]/ └── command.ts ``` This transforms into: - `/settings profile` - `/settings notifications enable` - `/settings notifications disable` `[command]/command.ts` may represent a parent command node. If that command directory contains child subcommands or groups, its `command.ts` file must only define metadata or configuration and must not export executable handlers such as `chatInput`, `message`, or `autocomplete`. Place those handlers in leaf command files, such as `profile.subcommand.ts` or `[disable]/command.ts`. Middleware inside these trees follows the same same-directory rule: - `+middleware.ts` applies only to leaves defined in that directory - `+.middleware.ts` applies only to the matching leaf token in that directory - ancestor directories do not automatically contribute middleware to deeper hierarchical leaves :::info You can learn more about configuring tree structures [here](../02-commands/09-subcommand-hierarchy.mdx). ::: --- ## Sharding Your Discord Bot Sharding is a method of splitting your bot into multiple processes, or "shards". This is useful for large bots that have a lot of guilds, as it allows you to distribute the load across multiple processes. Discord actually requires sharding for bots in more than 2,500 guilds, so understanding how to implement it is crucial as your bot grows. Sharding is a built-in feature of discord.js, and CommandKit does not alter the way sharding works. While this guide covers how to shard your bot using CommandKit, you can learn more about sharding in discord.js by checking out the [discord.js guide](https://discordjs.guide/sharding). ## When to use sharding You should consider implementing sharding in the following scenarios: - Your bot is in, or approaching, 2,500 guilds (required by Discord) - You're experiencing memory or performance issues with a single process - You want to distribute your bot's workload across multiple cores/machines - You're considering scaling your bot in the future ## Creating a sharding manager file You can simply create a new file in your source directory named `sharding-manager.ts` (or `sharding-manager.js` if you're using JavaScript) and CommandKit will automatically detect it and use it as the sharding manager. This file will be responsible for creating the shards and managing them. ```ts title="src/sharding-manager.ts" import { ShardingManager } from 'discord.js'; import { join } from 'node:path'; process.loadEnvFile('./.env'); const manager = new ShardingManager(join(import.meta.dirname, 'index.js'), { token: process.env.DISCORD_TOKEN, totalShards: 2, mode: 'worker', }); manager.on('shardCreate', (shard) => console.log(`Launched shard ${shard.id}`)); await manager.spawn(); ``` :::info If you're confused about `index.js` being used, this is an autogenerated entrypoint file that sets up the CommandKit environment. When running `commandkit start` or `commandkit dev`, CommandKit automatically detects the entrypoint file (either `sharding-manager.js` or `index.js`) and loads it. ::: --- ## Migrating From v0 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; :::warning **Minimum Requirements**: CommandKit v1 requires [Node.js](https://nodejs.org) version 24 or higher. Please ensure your environment meets this requirement before proceeding. **Migration Focus**: This guide specifically covers converting existing v0 code to v1. For information about new v1 features and capabilities, please refer to the rest of the documentation after completing your migration. ::: :::info This guide uses TypeScript examples, but all concepts apply to JavaScript projects as well. Simply use the corresponding JavaScript file extensions (e.g., `app.js`, `commandkit.config.js`). ::: This comprehensive guide will walk you through migrating your Discord bot from CommandKit v0 to v1. CommandKit v1 introduces significant architectural improvements, including a framework-based approach with enhanced developer experience features. ## Updating CommandKit Begin your migration by updating to the latest version of CommandKit: ```bash npm2yarn npm install commandkit ``` This command will install CommandKit v1 and update your `package.json` with the latest version. ## Project structure migration CommandKit v1 adopts a framework-based approach with a structured `app` directory that serves as the primary location for your bot's functionality. This new structure provides better organization and enables advanced features like automatic route discovery. ``` . ├── src/ │ ├── app/ │ │ ├── commands/ │ │ │ └── ping.ts │ │ └── events/ │ │ └── clientReady/ │ │ └── log.ts │ └── app.ts ├── .env ├── commandkit.config.ts ├── package.json └── tsconfig.json ``` ``` . ├── src/ │ ├── commands/ │ │ └── ping.ts │ ├── events/ │ │ └── clientReady/ │ │ └── log.ts │ └── index.ts ├── .env ├── commandkit.config.mjs ├── package.json └── tsconfig.json ``` ## Configuration file updates CommandKit v1 significantly simplifies configuration by automatically detecting your project structure and entry points. The framework now supports a standardized set of configuration file names for consistency. **Supported configuration files:** - `commandkit.config.js` - `commandkit.config.mjs` - `commandkit.config.cjs` - `commandkit.config.ts` ```ts title='commandkit.config.ts' import { defineConfig } from 'commandkit'; export default defineConfig({ // Configuration is now optional for most use cases // CommandKit automatically detects your app structure }); ``` ```ts title='commandkit.config.mjs' import { defineConfig } from 'commandkit'; export default defineConfig({ src: 'src', main: 'index.mjs', // Manual configuration was required }); ``` :::info The CommandKit CLI commands (`commandkit dev`, `commandkit build`, `commandkit start`) continue to work seamlessly with the new configuration system. ::: ## Entry point transformation CommandKit v1 introduces a dramatically simplified entry point system. Instead of manually managing the CommandKit instance, bot login, and path configurations, you now simply export a configured Discord.js client. ```ts title='src/app.ts' import { Client } from 'discord.js'; const client = new Client({ intents: [ 'Guilds', 'GuildMessages', 'MessageContent', ], }); // Optional: Override the default DISCORD_TOKEN env variable client.token = 'CUSTOM_BOT_TOKEN'; export default client; // CommandKit handles the rest automatically ``` ```ts title='src/index.ts' import { Client, GatewayIntentBits } from 'discord.js'; import { CommandKit } from 'commandkit'; import path from 'path'; const client = new Client({ intents: [ 'Guilds', 'GuildMessages', 'MessageContent', ], }); new CommandKit({ client, commandsPath: path.join(__dirname, 'commands'), eventsPath: path.join(__dirname, 'events'), validationsPath: path.join(__dirname, 'validations'), skipBuiltInValidations: true, bulkRegister: true, }); client.login('YOUR_TOKEN_HERE'); ``` ## Command file structure CommandKit v1 modernizes the command API with more intuitive naming and cleaner type definitions. The new structure separates command metadata from execution logic more clearly, while introducing room for new APIs such as message (legacy) commands. ```ts title='src/app/commands/ping.ts' import type { CommandData, ChatInputCommandContext } from 'commandkit'; export const command: CommandData = { name: 'ping', description: 'Pong!', }; export function chatInput({ interaction }: ChatInputCommandContext) { interaction.reply('Pong!'); } ``` ```ts title='src/commands/ping.ts' import type { CommandData, SlashCommandProps } from 'commandkit'; export const data: CommandData = { name: 'ping', description: 'Pong!', }; export function run({ interaction }: SlashCommandProps) { interaction.reply('Pong!'); } ``` ## Middleware system (formerly validations) CommandKit v1 renames and enhances the validation system to "middleware," which better reflects its purpose and capabilities. Middleware can now run in various contexts and provides more granular control over command execution. ```ts title='src/app/commands/+global-middleware.ts' import type { MiddlewareContext, stopMiddlewares } from 'commandkit'; export function beforeExecute(ctx: MiddlewareContext) { // Example: Block command execution based on conditions if (ctx.interaction.isRepliable()) { ctx.interaction.reply('Access denied: Command execution blocked.'); } stopMiddlewares(); // Prevents command execution } ``` ```ts title='src/validations/block-execution.ts' import type { ValidationProps } from 'commandkit'; export default function (ctx: ValidationProps) { if (ctx.interaction.isRepliable()) { ctx.interaction.reply('You are blocked from running the command!'); } return true; // command will not be executed }; ``` For comprehensive middleware documentation, including command and directory-specific middlewares, refer to the [middleware documentation](../02-commands/08-middlewares.mdx). ## Development environment CommandKit v1 introduces advanced development features that require using the CommandKit CLI. The development server provides Hot Module Replacement (HMR) for rapid iteration and supports modern features like JSX components. ### Starting development server ```bash npm2yarn npx commandkit dev ``` :::warning **Generated Files**: The development server creates a `.commandkit` directory for temporary files. Add this to your `.gitignore` to prevent committing generated files: ```gitignore .commandkit/ ``` ::: ## Production deployment ### Building your application ```bash npm2yarn npx commandkit build ``` ### Starting production application Option 1 - Using CommandKit CLI (recommended): ```bash npm2yarn npx commandkit start ``` Option 2 - Direct Node.js execution: ```bash node dist/index.js ``` :::warning **Generated Build Files**: The build process creates a `dist` directory. Add this to your `.gitignore` to prevent committing build artifacts: ```gitignore dist/ ``` ::: --- ## Using CLI API import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; The `commandkit/cli` module provides programmatic access to CommandKit's CLI functionality. This API is **not recommended for normal use cases** but can be useful in specific situations where you need to integrate CommandKit's build and development processes into custom tooling or scripts. :::warning Advanced Usage Only This API is intended for advanced users who need to integrate CommandKit into custom build systems or development workflows. For most users, the standard CLI commands (`commandkit dev`, `commandkit build`, etc.) should be sufficient. ::: ## Installation The CLI API is available as part of the main CommandKit package: ```sh npm2yarn npm install commandkit ``` ## Available Modules The CLI API is organized into several modules, each providing specific functionality: ### Development Module The `development` module provides functions for running CommandKit in development mode with hot module reloading. ```ts import { development } from 'commandkit/cli'; // Bootstrap development server const devServer = await development.bootstrapDevelopmentServer( './commandkit.config.ts', ); // Access the development server instance const watcher = devServer.watcher; const process = devServer.getProcess(); ``` **Key Functions:** - `bootstrapDevelopmentServer(configPath?: string)` - Starts the development server with file watching and HMR ### Production Module The `production` module handles production builds and server startup. ```ts import { production } from 'commandkit/cli'; // Create a production build await production.createProductionBuild('./commandkit.config.ts'); // Start production server const server = await production.bootstrapProductionServer( './commandkit.config.ts', ); ``` **Key Functions:** - `createProductionBuild(configPath?: string)` - Creates an optimized production build - `bootstrapProductionServer(configPath?: string)` - Starts the production server ### Environment Module The `env` module provides environment configuration utilities. ```ts import { env } from 'commandkit/cli'; // Get development environment variables const devEnv = env.DevEnv(); // Get production environment variables const prodEnv = env.ProdEnv(); // Set CLI environment flag env.setCLIEnv(); ``` **Key Functions:** - `DevEnv(static?: boolean)` - Returns development environment variables - `ProdEnv(static?: boolean)` - Returns production environment variables - `setCLIEnv()` - Sets the CLI environment flag ### CLI Module The `cli` module provides the main CLI bootstrap functionality. ```ts import { cli } from 'commandkit/cli'; // Bootstrap the CLI with custom arguments await cli.bootstrapCommandkitCLI(['dev', '--config', './custom.config.ts']); ``` **Key Functions:** - `bootstrapCommandkitCLI(argv: string[], options?: ParseOptions)` - Bootstraps the CLI with custom arguments ### Compiler Module The `compiler` module handles application building and compilation. ```ts import { compiler } from 'commandkit/cli'; // Build application with custom options await compiler.buildApplication({ configPath: './commandkit.config.ts', isDev: false, plugins: [], rolldownPlugins: [], }); ``` **Key Functions:** - `buildApplication(options: ApplicationBuildOptions)` - Builds the application with custom options ### App Process Module The `appProcess` module manages child processes for running applications. ```ts import { appProcess } from 'commandkit/cli'; // Create a new app process const process = appProcess.createAppProcess( './dist/index.js', process.cwd(), false, // isDev ); ``` **Key Functions:** - `createAppProcess(fileName: string, cwd: string, isDev: boolean)` - Creates a new application process ### Type Checker Module The `typeChecker` module provides TypeScript type checking functionality. ```ts import { typeChecker } from 'commandkit/cli'; // Perform type checking await typeChecker.performTypeCheck('./src'); ``` **Key Functions:** - `performTypeCheck(path: string)` - Performs TypeScript type checking on the specified path ### Utilities Module The `utilities` module provides common utility functions used throughout the CLI. ```ts import { utilities } from 'commandkit/cli'; // Find entrypoint file const entrypoint = utilities.findEntrypoint('./dist'); // Write to stdout utilities.write('Hello, World!'); // Panic with error message utilities.panic('Something went wrong!'); ``` **Key Functions:** - `findEntrypoint(dir: string)` - Finds the entrypoint file in a directory - `write(message: any)` - Writes a message to stdout - `panic(message: any)` - Exits the process with an error message - `loadTypeScript(errorMessage?: string)` - Loads TypeScript dynamically - `loadConfigFileFromPath(target: string)` - Loads configuration from a specific path ## Example: Custom Build Script Here's an example of how you might use the CLI API to create a custom build script: ```ts import { production, compiler, typeChecker } from 'commandkit/cli'; async function customBuild() { const configPath = './commandkit.config.ts'; try { // Perform type checking first await typeChecker.performTypeCheck('./src'); // Create production build await production.createProductionBuild(configPath); // Additional custom build steps console.log('Custom build completed successfully!'); } catch (error) { console.error('Build failed:', error); process.exit(1); } } customBuild(); ``` ## Example: Custom Development Server Here's an example of creating a custom development server with additional functionality: ```ts import { development, env } from 'commandkit/cli'; async function customDevServer() { // Set custom environment variables process.env.CUSTOM_DEV_FLAG = 'true'; // Bootstrap development server const devServer = await development.bootstrapDevelopmentServer( './commandkit.config.ts', ); // Add custom file watchers devServer.watcher.on('change', (path) => { console.log(`File changed: ${path}`); // Custom logic here }); // Access the running process const process = devServer.getProcess(); return devServer; } customDevServer(); ``` ## Important Notes 1. **Internal API**: These functions are marked as `@private` and `@internal`, meaning they may change without notice in future versions. 2. **Error Handling**: Always wrap CLI API calls in try-catch blocks as they may throw errors. 3. **Process Management**: When using `createAppProcess`, make sure to properly manage the returned child process. 4. **Configuration**: Most functions accept an optional `configPath` parameter. If not provided, they will look for configuration files in the current working directory. 5. **Environment Variables**: The environment modules provide pre-configured environment variables for development and production modes. ## When to Use the CLI API Consider using the CLI API when you need to: - Integrate CommandKit into existing build systems - Create custom development workflows - Build tooling that wraps CommandKit functionality - Implement custom file watching or hot reloading (though we recommend using the plugins api for this) - Create automated deployment scripts For most use cases, the standard CLI commands should be sufficient and are the recommended approach. --- ## Feature Flags Feature flags are a powerful tool for controlling the visibility of features in your application. They allow you to enable or disable features for specific users or groups, making it easier to test and roll out new functionality. This is particularly useful for A/B testing, gradual rollouts, and canary releases. CommandKit natively offers a feature flag system that allows you to define flags in your configuration and use them in your code. This system is designed to be simple and flexible, allowing you to easily manage feature flags across your application. ## Use cases Feature flags can be used in various scenarios to improve your application's development and deployment process: - **Maintenance Mode**: Enable or disable features during maintenance without affecting the user experience - **A/B Testing**: Test different versions of a feature with different user groups (e.g., show one version to 50% of users) - **Gradual Rollouts**: Gradually roll out new features to a small percentage of users before full deployment - **Canary Releases**: Release features to a small group of users to test in production before wider rollout - **Feature Toggles**: Enable or disable features without deploying new code - **User Segmentation**: Customize the user experience based on user preferences or behavior - **Testing and Debugging**: Isolate features for testing without affecting the entire application ## Defining feature flags Feature flags can be defined in any file in your project (except `app.ts`). The recommended place to define them is in the `src/flags` directory. ```ts title="src/flags/embedColorFlag.ts" import { flag } from 'commandkit'; import murmurhash from 'murmurhash'; export const embedColorFlag = flag({ name: 'embed-color-flag', description: 'Show red color instead of blue in embeds', identify(ctx) { const command = ctx.command; const id = command?.interaction?.user.id ?? command?.message?.author.id; return { id: id ?? null }; }, decide({ entities }) { if (!entities.id) { // if no ID is provided, we cannot determine the feature flag // so fall back to the default behavior return false; } // use murmurhash to hash the ID and determine the bucket // this allows us to have a consistent way to determine the feature flag // based on the user ID, so that the same user will always see the same feature flag // this is useful for A/B testing, where you want to show the same feature to the same user // without changing it every time they use the command // you can use any other hashing function or logic here as needed const hash = murmurhash.v3(entities.id); const bucket = hash % 100; // show red color instead of blue in embeds // to 50% of users, based on their ID // this is a simple A/B test // you can use any other logic here return bucket < 50; }, }); ``` ## Using feature flags Once you have defined a feature flag, you can use it in your code to control the visibility of features. You can use the function returned by the `flag` function to check if a feature flag is enabled or not. ```ts title="src/commands/hello.ts" import { ChatInputCommand, CommandData } from 'commandkit'; import { embedColorFlag } from '../flags/embedColorFlag'; export const command: CommandData = { name: 'hello', description: 'Hello world command', }; export const chatInput: ChatInputCommand = async (ctx) => { const showRedColor = await embedColorFlag(); await ctx.interaction.reply({ embeds: [ { title: 'Hello world', description: 'This is a hello world command', color: showRedColor ? 0xff0000 : 0x0000ff, }, ], }); }; ``` Now there's a 50% chance that the embed will be red instead of blue. You can use this feature flag to test the new color scheme with a subset of users before rolling it out to everyone. ## Context identification Sometimes you may want to identify specific users or groups for feature flags. For example, you may want to show a secret feature to server boosters only. In order to identify the context of the flag, you can add the `identify` method to the flag definition. ```ts title="src/flags/embedColorFlag.ts" import { flag } from 'commandkit'; import { GuildMember } from 'discord.js'; interface Entity { isBooster: boolean; } export const embedColorFlag = flag({ key: 'embed-color-flag', description: 'Show red color instead of blue in embeds', identify(ctx) { let member: GuildMember | null = null; if (ctx.command) { member = (ctx.command.interaction || ctx.command.message) ?.member as GuildMember; } else if (ctx.event) { // handle event specific context if (ctx.event.event === 'guildMemberUpdate') { const [_oldMember, newMember] = ctx.event.argumentsAs('guildMemberUpdate'); member = newMember as GuildMember; } } return { isBooster: member?.premiumSince !== null }; }, decide({ entities }) { return entities.isBooster; }, }); ``` :::info Understanding Flag Context Feature flags are **context-aware**, meaning they automatically detect and provide the necessary context (like user information, guild data, etc.) when used within CommandKit's execution environment. **Automatic Context Detection:** - ✅ **Commands** - Flags automatically access interaction/message data - ✅ **Events** - Flags automatically access event-specific data - ✅ **Middlewares** - Flags automatically access the current execution context **Manual Context Required:** If you need to use flags outside of these contexts (e.g., in utility functions, background jobs, or external API handlers), you must manually provide the identification data: ```ts // Option 1: Pass values directly const result = await myFlag.run({ identify: { isBooster: true } }); // Option 2: Pass as a function (useful for dynamic values) const result = await myFlag.run({ identify: () => ({ isBooster: user.premiumSince !== null }), }); ``` **Best Practice:** Keep flag usage within commands, events, and middlewares whenever possible. Manual context passing should only be used when absolutely necessary, as it bypasses the automatic context detection which causes flags to be less efficient and more error-prone. ::: ## Analytics integration You may want to see how many users are using a specific feature flag. You can do this by using the `analytics` plugin of CommandKit, which automatically collects the analytics data in the background whenever the feature flags are used. See [Analytics in CommandKit](../05-official-plugins/02-commandkit-analytics.mdx) for more information on how to set it up. ## Custom providers While CommandKit's built-in feature flag system is powerful for local flag management, you may want to integrate with external feature flag services like LaunchDarkly, Split, Unleash, or your own custom backend. CommandKit supports this through the provider pattern. ### What are flag providers? Flag providers are adapters that allow CommandKit to fetch feature flag configurations from external systems. When a provider is configured, your local `decide` function receives additional context from the external system, giving you the flexibility to combine local logic with remote configuration. ### Setting up a provider #### Step 1: Implement the FlagProvider interface ```ts title="src/providers/LaunchDarklyProvider.ts" import { FlagProvider, FlagConfiguration } from 'commandkit'; export class LaunchDarklyProvider implements FlagProvider { private client: any; // LaunchDarkly SDK client constructor(private apiKey: string) {} async initialize(): Promise { // Initialize LaunchDarkly client // this.client = LaunchDarkly.initialize(this.apiKey); // await this.client.waitUntilReady(); console.log('LaunchDarkly provider initialized'); } async getFlag(key: string, context?: any): Promise { try { // Fetch flag from LaunchDarkly // const variation = await this.client.variation(key, context, false); // const flagDetail = await this.client.variationDetail(key, context, false); // Return provider configuration return { enabled: true, // Whether the flag is enabled percentage: 75, // Optional: percentage rollout config: { // Custom configuration from LaunchDarkly colors: ['#ff0000', '#00ff00'], feature: 'enhanced-ui', }, }; } catch (error) { console.error(`LaunchDarkly flag evaluation failed for ${key}:`, error); return null; } } async hasFlag(key: string): Promise { // Check if flag exists in LaunchDarkly return true; } async destroy(): Promise { // Clean up LaunchDarkly client // await this.client.close(); } } ``` #### Step 2: Configure the global provider ```ts title="src/app.ts" import { setFlagProvider } from 'commandkit'; import { LaunchDarklyProvider } from './providers/LaunchDarklyProvider'; const provider = new LaunchDarklyProvider(process.env.LAUNCHDARKLY_API_KEY!); // Initialize and set the global provider await provider.initialize(); setFlagProvider(provider); ``` #### Step 3: Use provider data in your flags ```ts title="src/flags/embedColorFlag.ts" import { flag } from 'commandkit'; import murmurhash from 'murmurhash'; export const embedColorFlag = flag({ key: 'embed-color-flag', description: 'Show different embed colors based on external configuration', identify(ctx) { const command = ctx.command; const id = command?.interaction?.user.id ?? command?.message?.author.id; return { id: id ?? null }; }, decide({ entities, provider }) { // Local fallback when provider is unavailable if (!provider) { // Use local hashing logic const hash = murmurhash.v3(entities.id || 'anonymous'); return hash % 100 < 50; } // Provider-based logic if (!provider.enabled) { return false; // Provider disabled this flag } // Use provider's percentage rollout if (provider.percentage) { const hash = murmurhash.v3(entities.id || 'anonymous'); const bucket = hash % 100; return bucket < provider.percentage; } // Use provider's custom config if (provider.config?.colors) { return provider.config.colors[0]; // Return specific color } return provider.enabled; }, }); ``` ### Built-in JSON provider CommandKit includes a simple JSON-based provider for basic external configuration: ```ts title="src/providers/config.ts" import { JsonFlagProvider, setFlagProvider } from 'commandkit'; const provider = new JsonFlagProvider({ 'embed-color-flag': { enabled: true, percentage: 75, config: { colors: ['#ff0000', '#00ff00', '#0000ff'], theme: 'dark', }, }, 'premium-features': { enabled: true, targeting: { segments: ['premium_users', 'beta_testers'], }, }, }); await provider.initialize(); setFlagProvider(provider); ``` ### Advanced provider examples #### Database-backed provider ```ts title="src/providers/DatabaseProvider.ts" import { FlagProvider, FlagConfiguration } from 'commandkit'; export class DatabaseProvider implements FlagProvider { constructor(private db: any) {} async getFlag(key: string): Promise { const result = await this.db.query( 'SELECT * FROM feature_flags WHERE key = ?', [key], ); if (!result.length) return null; const flag = result[0]; return { enabled: flag.enabled, percentage: flag.rollout_percentage, config: JSON.parse(flag.config || '{}'), targeting: JSON.parse(flag.targeting || '{}'), }; } async hasFlag(key: string): Promise { const result = await this.db.query( 'SELECT 1 FROM feature_flags WHERE key = ?', [key], ); return result.length > 0; } } ``` #### Multi-source provider ```ts title="src/providers/MultiProvider.ts" import { FlagProvider, FlagConfiguration } from 'commandkit'; export class MultiProvider implements FlagProvider { constructor(private providers: FlagProvider[]) {} async getFlag(key: string, context?: any): Promise { // Try providers in order, return first successful result for (const provider of this.providers) { try { const result = await provider.getFlag(key, context); if (result) return result; } catch (error) { console.warn(`Provider failed for ${key}:`, error); continue; } } return null; } async hasFlag(key: string): Promise { for (const provider of this.providers) { if (await provider.hasFlag(key)) return true; } return false; } } ``` ## Best practices ### Always provide fallbacks Your `decide` function should handle cases where the provider is unavailable: ```ts decide({ entities, provider }) { // Always check if provider data is available if (!provider) { // Local fallback logic return defaultBehavior(entities); } // Provider-based logic return providerBasedLogic(entities, provider); } ``` ### Handle provider errors gracefully Providers may fail due to network issues, API limits, or configuration problems. CommandKit automatically catches these errors and continues with local evaluation. ### Use provider configuration wisely The provider's `config` object can contain any data structure your external system provides: ```ts decide({ entities, provider }) { if (provider?.config?.experimentConfig) { const experiment = provider.config.experimentConfig; return evaluateExperiment(entities, experiment); } return defaultLogic(entities); } ``` ### Disable analytics when needed For high-frequency flags or privacy-sensitive scenarios, you can disable analytics: ```ts export const highFrequencyFlag = flag({ key: 'rate-limiting-flag', description: 'High frequency flag for rate limiting', disableAnalytics: true, // Skip analytics tracking decide({ entities }) { return entities.requestCount > 100; }, }); ``` --- ## commandkit/ratelimit CommandKit provides a rate limiter utility that controls the frequency of operations such as user actions or API requests. This helps prevent system overload by enforcing configurable request limits. ## Basic usage The simplest way to use rate limiting is with the default settings: ```typescript import { ratelimit } from 'commandkit/ratelimit'; // Check if this user can make another request const allowed = await ratelimit('user:123'); if (allowed) { // Process the request console.log('Request processed successfully'); } else { // User has made too many requests console.log('Please wait before making another request'); } ``` ## Custom configuration You can create a custom rate limiter with specific settings: ```typescript import { createRateLimiter } from 'commandkit/ratelimit'; // Create a stricter rate limiter for API endpoints const apiLimiter = createRateLimiter({ maxRequests: 5, // Allow 5 requests interval: 30000, // Per 30 seconds }); const allowed = await apiLimiter.limit('api:endpoint'); ``` ## Advanced usage ### Checking remaining requests You can check how many requests a user has left and when the limit resets: ```typescript import { getRemainingRequests, getResetTime } from 'commandkit/ratelimit'; const remaining = await getRemainingRequests('user:123'); const resetTime = await getResetTime('user:123'); console.log(`${remaining} requests remaining`); console.log(`Limit resets in ${Math.round(resetTime / 1000)} seconds`); ``` ### Manual reset You can manually reset a user's rate limit when needed: ```typescript import { resetRateLimit } from 'commandkit/ratelimit'; // Give the user a fresh start await resetRateLimit('user:123'); ``` ### Using external storage Rate limiters store data in memory by default. For multi-server deployments, use external storage like Redis to share rate limit data across all servers: ```typescript import { RateLimiter, RateLimitStorage } from 'commandkit/ratelimit'; import { RedisRateLimitStorage } from '@commandkit/redis'; import { Redis } from 'ioredis'; // Create Redis client const redis = new Redis(); // Use Redis-based rate limit storage const limiter = new RateLimiter(10, 60000, new RedisRateLimitStorage(redis)); ``` You can also use the convenience function: ```typescript import { createRateLimiter } from 'commandkit/ratelimit'; import { RedisRateLimitStorage } from '@commandkit/redis'; const limiter = createRateLimiter({ maxRequests: 10, interval: 60000, storage: new RedisRateLimitStorage(redis), }); ``` ## Default settings - **Max Requests**: 10 requests - **Time Window**: 60 seconds (1 minute) - **Storage**: In-memory (works for single-server applications) --- ## commandkit/mutex A mutex (mutual exclusion) ensures that only one operation can access a shared resource at a time. This prevents race conditions and data corruption when multiple operations attempt to modify the same resource simultaneously. ## Basic usage The easiest way to use a mutex is with the `withMutex` function, which automatically handles locking and unlocking: ```typescript import { withMutex } from 'commandkit/mutex'; // This ensures only one operation can update the shared resource at a time const result = await withMutex('shared-resource', async () => { // This code runs with exclusive access return await updateSharedResource(); }); ``` ## Custom configuration You can create a custom mutex with different timeout settings: ```typescript import { createMutex } from 'commandkit/mutex'; const mutex = createMutex({ timeout: 60000, // 60 second timeout }); const result = await mutex.withLock('resource', async () => { return await criticalOperation(); }); ``` ## Advanced usage ### Manual lock management Sometimes you need more control over when locks are acquired and released: ```typescript import { acquireLock, releaseLock, isLocked } from 'commandkit/mutex'; // Acquire lock manually const acquired = await acquireLock('resource', 30000); if (acquired) { try { // Perform critical operation await criticalOperation(); } finally { // Always release the lock, even if an error occurs await releaseLock('resource'); } } // Check if resource is currently locked const locked = await isLocked('resource'); console.log(`Resource is ${locked ? 'locked' : 'available'}`); ``` ### Cancelling operations You can cancel a mutex operation using an AbortSignal: ```typescript import { withMutex } from 'commandkit/mutex'; // Create a timeout that cancels after 5 seconds const signal = AbortSignal.timeout(5000); try { const result = await withMutex( 'resource', async () => { return await longRunningOperation(); }, 30000, signal, ); } catch (error) { if (error.message.includes('aborted')) { console.log('Lock acquisition was cancelled'); } } ``` ### Using external storage Mutexes store lock information in memory by default. For multi-server deployments, use external storage like Redis: ```typescript import { Mutex, MutexStorage } from 'commandkit/mutex'; import { RedisMutexStorage } from '@commandkit/redis'; import { Redis } from 'ioredis'; // Create Redis client const redis = new Redis(); // Use Redis-based mutex storage const mutex = new Mutex({ timeout: 30000, storage: new RedisMutexStorage(redis), }); ``` You can also use the convenience function: ```typescript import { createMutex } from 'commandkit/mutex'; import { RedisMutexStorage } from '@commandkit/redis'; const mutex = createMutex({ timeout: 60000, storage: new RedisMutexStorage(redis), }); ``` ## Default settings - **Timeout**: 30 seconds (30000ms) - **Storage**: In-memory (works for single-server applications) --- ## commandkit/async-queue An async queue manages task execution by processing them sequentially or with controlled concurrency. Tasks are queued and executed in order, with configurable limits on how many can run simultaneously. ## Basic usage The simplest way to use a queue is with the default sequential processing: ```typescript import { createAsyncQueue } from 'commandkit/async-queue'; // Create a sequential queue (default) const queue = createAsyncQueue(); // Add tasks to the queue const result1 = await queue.add(async () => { return await processTask1(); }); const result2 = await queue.add(async () => { return await processTask2(); }); ``` ## Custom configuration You can create a queue that processes multiple tasks at the same time: ```typescript import { createAsyncQueue } from 'commandkit/async-queue'; // Allow 3 concurrent tasks const queue = createAsyncQueue({ concurrency: 3, onDrain: () => { console.log('All tasks completed'); }, }); // Tasks will run with up to 3 concurrent executions const promises = [ queue.add(async () => await task1()), queue.add(async () => await task2()), queue.add(async () => await task3()), queue.add(async () => await task4()), ]; const results = await Promise.all(promises); ``` ## Advanced usage ### Queue control You can pause and resume the queue, and check its status: ```typescript import { createAsyncQueue } from 'commandkit/async-queue'; const queue = createAsyncQueue({ concurrency: 2 }); // Pause the queue queue.pause(); // Add tasks (they won't execute until resumed) const promise1 = queue.add(async () => await task1()); const promise2 = queue.add(async () => await task2()); // Resume the queue queue.resume(); // Check queue status console.log(`Running: ${queue.getRunning()}`); console.log(`Pending: ${queue.getPending()}`); console.log(`Paused: ${queue.isPaused()}`); ``` ### Cancelling operations You can cancel queue operations using an AbortSignal: ```typescript import { createAsyncQueue } from 'commandkit/async-queue'; // Create a timeout that cancels after 5 seconds const signal = AbortSignal.timeout(5000); const queue = createAsyncQueue({ concurrency: 2, signal, }); // Add tasks with individual timeouts const task1Promise = queue.add( async () => await longRunningTask1(), AbortSignal.timeout(3000), ); const task2Promise = queue.add( async () => await longRunningTask2(), AbortSignal.timeout(3000), ); try { await Promise.all([task1Promise, task2Promise]); } catch (error) { if (error.message.includes('aborted')) { console.log('Queue was aborted'); } } ``` ### Batch processing Queues are great for processing large batches of items efficiently: ```typescript import { createAsyncQueue } from 'commandkit/async-queue'; const queue = createAsyncQueue({ concurrency: 5, onDrain: () => console.log('Batch processing complete'), }); // Process a large batch of items const items = Array.from({ length: 100 }, (_, i) => i); const processItem = async (item: number) => { await new Promise((resolve) => setTimeout(resolve, 100)); return `Processed item ${item}`; }; // Add all items to queue const promises = items.map((item) => queue.add(() => processItem(item))); // Wait for all to complete const results = await Promise.all(promises); ``` ### Error handling Queues handle errors gracefully without stopping other tasks: ```typescript import { createAsyncQueue } from 'commandkit/async-queue'; const queue = createAsyncQueue({ concurrency: 2 }); // Handle individual task errors const results = await Promise.allSettled([ queue.add(async () => { throw new Error('Task failed'); }), queue.add(async () => { return 'Task succeeded'; }), queue.add(async () => { return 'Another success'; }), ]); results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`Task ${index}: ${result.value}`); } else { console.log(`Task ${index}: ${result.reason.message}`); } }); ``` ## Default settings - **Concurrency**: 1 (sequential processing) - **Storage**: In-memory - **Abort Support**: Yes --- ## commandkit/kv CommandKit provides a built-in key-value store that offers simple, persistent storage using SQLite. It supports storing any JSON-serializable data types directly, including objects, arrays, dates, maps, sets, and more. ## Basic usage The simplest way to use the key-value store is to create an instance and start storing data: ```typescript import { KV } from 'commandkit/kv'; // Create a new KV store const kv = new KV('data.db'); // Store data directly kv.set('user:123', { name: 'John', age: 30 }); kv.set('counter', 42); kv.set('active', true); // Retrieve data const user = kv.get('user:123'); // { name: 'John', age: 30 } const counter = kv.get('counter'); // 42 ``` ## Configuration options You can customize the key-value store with various options: ```typescript import { openKV } from 'commandkit/kv'; // Create with custom database file const kv = openKV('my-bot-data.db'); // In-memory store for testing const testKv = openKV(':memory:'); // Create with specific namespace const userKv = openKV('data.db', { namespace: 'users', }); ``` ## Supported data types The KV store supports storing and retrieving these data types: - **Primitives**: `string`, `number`, `boolean`, `bigint`, `null`, `undefined` - **Objects**: Plain objects, nested objects - **Arrays**: Any array of supported types - **Dates**: JavaScript Date objects - **Collections**: `Map`, `Set` - **Buffers**: Node.js Buffer objects - **Regular Expressions**: RegExp objects - **Functions**: Function objects (serialized as strings) ```typescript // Store different data types kv.set('user', { name: 'John', preferences: { theme: 'dark' } }); kv.set('tags', ['javascript', 'typescript', 'discord']); kv.set('created_at', new Date()); kv.set( 'permissions', new Map([ ['admin', true], ['user', false], ]), ); kv.set('unique_ids', new Set([1, 2, 3])); ``` ## Dot notation for nested access Access and modify nested properties using dot notation: ```typescript // Store an object kv.set('user:123', { name: 'John Doe', settings: { theme: 'dark', notifications: { email: true, push: false }, }, }); // Access nested properties const theme = kv.get('user:123.settings.theme'); // 'dark' const emailNotifications = kv.get('user:123.settings.notifications.email'); // true // Set nested properties kv.set('user:123.settings.theme', 'light'); kv.set('user:123.settings.notifications.push', true); ``` ## Namespaces Organize your data into logical groups using namespaces: ```typescript const kv = openKV('bot-data.db'); // Create namespace instances const userKv = kv.namespace('users'); const configKv = kv.namespace('config'); const guildKv = kv.namespace('guilds'); // Store data in different namespaces userKv.set('123', { name: 'John', level: 5 }); configKv.set('theme', 'dark'); guildKv.set('456', { name: 'My Server', premium: true }); // Same key in different namespaces userKv.set('settings', { theme: 'light' }); configKv.set('settings', { maintenance: false }); ``` ### Namespace operations Each namespace has its own isolated operations: ```typescript // Check current namespace console.log(userKv.getCurrentNamespace()); // 'users' // List all namespaces const namespaces = kv.namespaces(); console.log('Available namespaces:', namespaces); // Namespace-specific counts and data console.log(`Users: ${userKv.count()}`); console.log('User keys:', userKv.keys()); console.log('All users:', userKv.all()); ``` ## Expiration support Set automatic expiration for temporary data: ```typescript // Set data with expiration (1 hour) kv.setex('session:123', { userId: 123, token: 'abc123' }, 60 * 60 * 1000); // Set expiration for existing keys kv.set('user:123', { name: 'John' }); kv.expire('user:123', 30 * 60 * 1000); // 30 minutes // Check time to live const ttl = kv.ttl('user:123'); if (ttl > 0) { console.log(`Expires in ${Math.floor(ttl / 1000)} seconds`); } ``` ## Transactions Execute multiple operations atomically: ```typescript // Basic transaction kv.transaction(() => { kv.set('user:123', { balance: 100 }); kv.set('user:456', { balance: 200 }); // If any operation fails, all changes are rolled back }); // Async transactions await kv.transaction(async () => { const user = kv.get('user:123') || { balance: 0 }; user.balance += 50; kv.set('user:123', user); // Log the transaction kv.set(`transaction:${Date.now()}`, { type: 'deposit', amount: 50, timestamp: new Date(), }); }); ``` ## Bulk operations Work with multiple entries efficiently: ```typescript // Get all data const allData = kv.all(); const keys = kv.keys(); const values = kv.values(); const count = kv.count(); // Iterate over entries for (const [key, value] of kv) { console.log(`${key}:`, value); } // Filter entries const userEntries = [...kv].filter(([key]) => key.startsWith('user:')); // Check existence if (kv.has('user:123')) { console.log('User exists'); } // Delete data kv.delete('user:123'); kv.clear(); // Clear all data in current namespace ``` ## Resource management Use proper resource management to ensure database connections are closed: ```typescript // Automatic cleanup with using statement { using kv = openKV('data.db'); kv.set('key', 'value'); // kv is automatically closed when the block ends } // Manual cleanup const kv = openKV('data.db'); try { kv.set('key', 'value'); } finally { kv.close(); } ``` --- ## commandkit/semaphore A semaphore controls the number of operations that can access a resource concurrently. It maintains a set number of permits, allowing only that many operations to proceed simultaneously while others wait in queue. ## Basic usage The easiest way to use a semaphore is with the `withPermit` function, which automatically handles getting and releasing permits: ```typescript import { withPermit } from 'commandkit/semaphore'; // This ensures only a limited number of operations can run at once const result = await withPermit('database-connection', async () => { // This code runs with a permit from the semaphore return await executeDatabaseQuery(); }); ``` ## Custom configuration You can create a semaphore with specific limits and timeout settings: ```typescript import { createSemaphore } from 'commandkit/semaphore'; const semaphore = createSemaphore({ permits: 5, // Allow 5 concurrent operations timeout: 60000, // 60 second timeout }); const result = await semaphore.withPermit('api-endpoint', async () => { return await apiCall(); }); ``` ## Advanced usage ### Manual permit management Sometimes you need more control over when permits are acquired and released: ```typescript import { acquirePermit, releasePermit, getAvailablePermits, } from 'commandkit/semaphore'; // Acquire permit manually const acquired = await acquirePermit('resource', 30000); if (acquired) { try { // Perform operation with limited concurrency await limitedOperation(); } finally { // Always release the permit, even if an error occurs await releasePermit('resource'); } } // Check how many permits are available const available = await getAvailablePermits('resource'); console.log(`${available} permits available`); ``` ### Cancelling operations You can cancel a semaphore operation using an AbortSignal: ```typescript import { withPermit } from 'commandkit/semaphore'; // Create a timeout that cancels after 5 seconds const signal = AbortSignal.timeout(5000); try { const result = await withPermit( 'resource', async () => { return await longRunningOperation(); }, 30000, signal, ); } catch (error) { if (error.message.includes('aborted')) { console.log('Permit acquisition was cancelled'); } } ``` ### Monitoring semaphore state You can check how many permits are being used and how many are available: ```typescript import { getAvailablePermits, getAcquiredPermits } from 'commandkit/semaphore'; const available = await getAvailablePermits('database'); const acquired = await getAcquiredPermits('database'); console.log(`Database connections: ${acquired} active, ${available} available`); ``` ### Using external storage Semaphores store permit information in memory by default. For multi-server deployments, use external storage like Redis: ```typescript import { Semaphore, SemaphoreStorage } from 'commandkit/semaphore'; import { RedisSemaphoreStorage } from '@commandkit/redis'; import { Redis } from 'ioredis'; // Create Redis client const redis = new Redis(); // Use Redis-based semaphore storage const semaphore = new Semaphore({ permits: 10, timeout: 30000, storage: new RedisSemaphoreStorage(redis), }); ``` You can also use the convenience function: ```typescript import { createSemaphore } from 'commandkit/semaphore'; import { RedisSemaphoreStorage } from '@commandkit/redis'; const semaphore = createSemaphore({ permits: 5, timeout: 60000, storage: new RedisSemaphoreStorage(redis), }); ``` ## Default settings - **Permits**: 1 (sequential access) - **Timeout**: 30 seconds (30000ms) - **Storage**: In-memory (works for single-server applications) --- ## Experimental Features CommandKit includes a small set of opt-in experimental features for improving startup performance and local developer ergonomics. :::warning Experimental features are disabled by default and may change in future minor releases. ::: ## Enable Experimental Flags Set these flags in `commandkit.config.ts`: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit/config'; export default defineConfig({ experimental: { pregenerateCommands: true, incrementalRouter: true, }, }); ``` ## `experimental.pregenerateCommands` When enabled, `commandkit build` pre-generates a serialized command and event router artifact and writes it into build output: - `dist/.commandkit/router-tree.json` At runtime in production, CommandKit attempts to hydrate routers from this artifact instead of performing full filesystem traversal. If the artifact is missing, stale, or invalid (for example version mismatch), CommandKit automatically falls back to dynamic runtime resolution. ### Why Use It - Keeps production cold starts predictable as command/event trees grow. - Avoids repeated full tree construction work on each startup. ## `experimental.incrementalRouter` When enabled, development HMR updates only the subtree affected by a file change instead of reconciling the full router tree. This applies to command and event routers during `commandkit dev`, including file add/change/delete operations. ### Why Use It - Reduces reload lag in larger bots. - Preserves unchanged router state across updates. ## Type Generation (`commandkit typegen`) CommandKit can generate command route types for editor autocomplete. Run: ```sh commandkit typegen ``` This writes: - `./.commandkit/types.ts` (generated command route registry) - `./commandkit-env.d.ts` (type reference file) `commandkit-env.d.ts` references the generated file: ```ts title="commandkit-env.d.ts" /// ``` During development, CommandKit also refreshes generated command route types after command reloads. ### Where You See It APIs that accept resolvable command names (for example `ctx.forwardCommand(...)`) get route-aware autocomplete when generated types are present. ---