CommandKit Documentation

website·Indexed 30 days ago

https://commandkit.js.org

A framework for streamlined Discord command and event handling using a convention-over-configuration approach. Features include JSX components for UI code, built-in component handlers, command middlewares, and unified support for both prefix and slash commands.

Tokens
38.4K
Snippets
210
Records
235
Agent score
56%

What's inside CommandKit

  1. CommandKit Framework Overview

    CommandKit is a meta-framework for discord.js that manages the entire development lifecycle (bundling, TypeScript/JSX compilation, and command registration). Key features include:

    • JSX Components: Declarative UI code for Discord components instead of builder chains.
    • Built-in Component Handlers: Direct onClick, onSelect, and onSubmit handlers for buttons, menus, and modals, removing the need for manual interaction collectors.
    • Command Middlewares: A layer to run logic (e.g., permission checks, cooldowns) before or after command execution.
    • Unified Commands: Support for slash commands, context menus, and prefix commands within a single file.
    • Extensibility: A plugin system for adding behaviors, custom events, i18n, analytics, and AI-powered natural language command execution.
  2. General API constraints and consistency

    Discord's API is eventually consistent. Clients should implement idempotent behavior because:

    • Actions may be executed in any order or not at all.
    • Events may be sent zero, one, or multiple times (up to N times) per client.
    • All HTTP and WebSocket services use TLS 1.2.
  3. Implement localization in Discord event handlers

    To localize event handlers, use the locale function from @commandkit/i18n. Since events (like messageCreate) don't have a built-in interaction locale, you must manually determine the locale using guild preferences, user settings, or a custom detection function. It is recommended to wrap translation calls in a try-catch block to provide a fallback language (e.g., English) if translation fails.
    import { locale } from '@commandkit/i18n';
    import type { Message } from 'discord.js';
    
    export default async function onMessageCreate(message: Message) {
      try {
        // Determine locale (e.g., from guild preferred locale)
        const userLocale = message.guild?.preferredLocale || 'en-US';
        const { t } = locale(userLocale);
    
        await message.channel.send({
          content: t('moderation.auto_warning', { user: message.author.displayName }),
        });
      } catch (error) {
        console.error('Translation error:', error);
        await message.channel.send(`⚠️ ${message.author.displayName}, please follow guidelines.`);
      }
    }
  4. Implement command middleware in CommandKit

    CommandKit allows running logic before or after command execution using middleware files. Middleware execution order is deterministic: global middlewares run first, followed by current-directory middlewares, and finally command-scoped middlewares. Hierarchical leaves do not inherit +middleware.ts from ancestor directories; they only use middleware in their immediate directory.
    // Example of a directory-level middleware (+middleware.ts)
    import type { MiddlewareContext } from 'commandkit';
    
    export function beforeExecute(ctx: MiddlewareContext) {
      if (!ctx.interaction.member.permissions.has('KickMembers')) {
        throw new Error('You need moderation permissions to use this command');
      }
    }
  5. Customizing command execution with executeCommand

    The executeCommand hook allows you to take full control over how a command is executed. If the method returns true, CommandKit will assume the command was handled and will not perform the default execution. This is ideal for implementing custom rate limiting or permission checks.
    async executeCommand(
      ctx: CommandKitPluginRuntime,
      env: CommandKitEnvironment,
      source: Interaction | Message,
      command: PreparedAppCommandExecution,
      execute: () => Promise<any>
    ): Promise<boolean> {
      if (this.isRateLimited(command.name, source.user.id)) {
        if (source instanceof Interaction && source.isRepliable()) {
          await source.reply('You are being rate limited!');
        }
        return true; // Stop CommandKit from executing the command
      }
      return false; // Let CommandKit handle it normally
    }
  6. Create a Discord event handler

    To create an event handler, create a folder inside the src/app/events directory that matches the name of the Discord.js event (e.g., clientReady). CommandKit automatically detects any file within that folder and registers it as a handler for that event. Handlers are executed in alphabetical order by filename.
    // 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;
  7. Intercept interactions and messages using Runtime hooks

    CommandKit provides hooks to run logic before interactions or message commands are processed. onBeforeInteraction is used for logging or analytics on Discord.js Interaction objects, while onBeforeMessageCommand is used for filtering or logging Discord.js Message objects.
    async onBeforeInteraction(
      ctx: CommandKitPluginRuntime,
      interaction: Interaction
    ): Promise<void> {
      if (interaction.isCommand()) {
        console.log(`Command used: ${interaction.commandName}`);
      }
    }
    
    async onBeforeMessageCommand(
      ctx: CommandKitPluginRuntime,
      message: Message
    ): Promise<void> {
      console.log(`Potential message command: ${message.content}`);
    }
  8. Ensure workflow step idempotency

    Because steps can be retried automatically, they must be idempotent. Before performing a side effect (like assigning a role), check if the action has already been completed to avoid duplicate operations.
    export async function assignRole(userId: string, roleId: string) {
      'use step';
      const client = useClient<true>();
      const member = await client.guilds.cache.get('guild-id')?.members.fetch(userId);
    
      if (!member) throw new FatalError('Member not found');
    
      // Idempotency check
      if (member.roles.cache.has(roleId)) {
        return { alreadyAssigned: true };
      }
    
      await member.roles.add(roleId);
      return { assigned: true };
    }
  9. Create a Message Context Menu command

    Message context menu commands allow users to trigger a command by right-clicking a message. To create one, define a command file in your commands directory and export a messageContextMenu function. CommandKit automatically sets the correct command type for the Discord API based on this export, so you do not need to manually specify the type in the command object. Use interaction.targetMessage to access the message being referenced.
    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;
    
      await interaction.reply({
        content: `You have reported the following message: ${content}`,
        flags: MessageFlags.Ephemeral,
      });
    };
  10. Access nested KV data using dot notation

    You can read and modify nested properties of stored objects using dot notation in the key string.
    // Store an object
    kv.set('user:123', {
      name: 'John Doe',
      settings: { theme: 'dark', notifications: { email: true } },
    });
    
    // Access nested properties
    const theme = kv.get('user:123.settings.theme'); // 'dark'
    
    // Set nested properties
    kv.set('user:123.settings.theme', 'light');
  11. Manage Semaphore permits manually

    For advanced control, use acquirePermit and releasePermit. Always wrap the operation in a try-finally block to ensure the permit is released.
    import { acquirePermit, releasePermit, getAvailablePermits } from 'commandkit/semaphore';
    
    const acquired = await acquirePermit('resource', 30000);
    if (acquired) {
      try {
        await limitedOperation();
      } finally {
        await releasePermit('resource');
      }
    }
    
    const available = await getAvailablePermits('resource');
  12. Implement a custom analytics provider in CommandKit

    To create a custom analytics provider, implement the AnalyticsProvider interface. This requires defining a name and implementing track and identify methods to send data to your external analytics service.
    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<void> {
        const { name, data } = event;
        await this.analytics.track({
          name,
          data: {
            ...data,
            sessionId: this.generateSessionId(),
            timestamp: Date.now(),
          },
        });
      }
    
      async identify(engine: AnalyticsEngine, event: IdentifyEvent): Promise<void> {
        await this.analytics.identify({
          sessionId: this.generateSessionId(),
          timestamp: Date.now(),
          properties: {
            environment: process.env.NODE_ENV,
            version: '1.0.0',
          },
        });
      }
    
      private generateSessionId(): string {
        return 'session_' + Math.random().toString(36).substring(7);
      }
    }