Skip to main content
CommandKit’s plugin system allows you to extend functionality and integrate custom features into your bot. Create runtime plugins for bot lifecycle hooks or compiler plugins for code transformations.

Plugin Types

CommandKit supports two types of plugins:

Runtime Plugins

Run during bot execution. Hook into command execution, events, and lifecycle methods.

Compiler Plugins

Transform code at compile time. Process files before they’re executed.

Runtime Plugins

Runtime plugins extend your bot’s behavior during execution.

Creating a Runtime Plugin

1

Create plugin class

Extend the RuntimePlugin class from CommandKit:
src/plugins/logger.ts
import { RuntimePlugin, CommandKitPluginRuntime } from 'commandkit/plugin';

export class LoggerPlugin extends RuntimePlugin {
  public readonly name = 'LoggerPlugin';

  public async activate(ctx: CommandKitPluginRuntime): Promise<void> {
    console.log('LoggerPlugin activated!');
  }

  public async deactivate(ctx: CommandKitPluginRuntime): Promise<void> {
    console.log('LoggerPlugin deactivated!');
  }
}
2

Register the plugin

Add your plugin to CommandKit:
src/index.ts
import { CommandKit } from 'commandkit';
import { LoggerPlugin } from './plugins/logger';

new CommandKit({
  client,
  plugins: [new LoggerPlugin({})],
});

Runtime Plugin Hooks

Runtime plugins provide numerous lifecycle hooks:
export class MyPlugin extends RuntimePlugin {
  public async activate(ctx: CommandKitPluginRuntime): Promise<void> {
    // Called when plugin is activated
    console.log('Plugin starting...');
  }

  public async deactivate(ctx: CommandKitPluginRuntime): Promise<void> {
    // Called when plugin is deactivated
    console.log('Plugin stopping...');
  }
}

Example: Command Logger Plugin

src/plugins/command-logger.ts
import { RuntimePlugin, CommandKitPluginRuntime } from 'commandkit/plugin';
import type { CommandKitEnvironment } from 'commandkit';
import type { Interaction, Message } from 'discord.js';

export class CommandLoggerPlugin extends RuntimePlugin {
  public readonly name = 'CommandLogger';
  private commandCounts = new Map<string, number>();

  public async executeCommand(
    ctx: CommandKitPluginRuntime,
    env: CommandKitEnvironment,
    source: Interaction | Message,
    command: any,
  ): Promise<boolean> {
    const commandName = command.name;
    const count = this.commandCounts.get(commandName) || 0;
    this.commandCounts.set(commandName, count + 1);

    console.log(
      `[${new Date().toISOString()}] Command "${commandName}" executed (${count + 1} times)`
    );

    return false; // Don't prevent execution
  }

  public async deactivate(): Promise<void> {
    console.log('Command usage statistics:');
    for (const [command, count] of this.commandCounts) {
      console.log(`  ${command}: ${count}`);
    }
  }
}

Example: Rate Limiter Plugin

src/plugins/rate-limiter.ts
import { RuntimePlugin, CommandKitPluginRuntime } from 'commandkit/plugin';
import type { CommandKitEnvironment } from 'commandkit';
import type { Interaction, Message } from 'discord.js';

interface RateLimitOptions {
  maxCommands: number;
  windowMs: number;
}

export class RateLimiterPlugin extends RuntimePlugin<RateLimitOptions> {
  public readonly name = 'RateLimiter';
  private userCommands = new Map<string, number[]>();

  public async executeCommand(
    ctx: CommandKitPluginRuntime,
    env: CommandKitEnvironment,
    source: Interaction | Message,
  ): Promise<boolean> {
    const userId = 'user' in source ? source.user.id : source.author.id;
    const now = Date.now();
    const windowStart = now - this.options.windowMs;

    // Get user's recent commands
    const userTimes = this.userCommands.get(userId) || [];
    const recentTimes = userTimes.filter((time) => time > windowStart);

    if (recentTimes.length >= this.options.maxCommands) {
      const reply = 'You\'re doing that too fast! Please slow down.';
      
      if ('reply' in source && typeof source.reply === 'function') {
        await source.reply({ content: reply, ephemeral: true });
      }
      
      return true; // Prevent command execution
    }

    // Record this command
    recentTimes.push(now);
    this.userCommands.set(userId, recentTimes);

    return false;
  }
}

// Usage:
export function rateLimit(options: RateLimitOptions) {
  return new RateLimiterPlugin(options);
}
Use it:
src/index.ts
import { rateLimit } from './plugins/rate-limiter';

new CommandKit({
  client,
  plugins: [
    rateLimit({
      maxCommands: 5,
      windowMs: 60000, // 1 minute
    }),
  ],
});

Compiler Plugins

Compiler plugins transform code before execution.

Creating a Compiler Plugin

src/plugins/add-header.ts
import { CompilerPlugin } from 'commandkit/plugin';
import type { PluginTransformParameters, TransformedResult } from 'commandkit/plugin';

export class AddHeaderPlugin extends CompilerPlugin {
  public readonly name = 'AddHeaderPlugin';

  public async transform(
    params: PluginTransformParameters,
  ): Promise<TransformedResult> {
    const { code, id } = params;

    // Only transform command files
    if (!id.includes('/commands/')) {
      return null;
    }

    // Add header comment
    const header = `// Auto-generated header\n// File: ${id}\n\n`;
    
    return {
      code: header + code,
      map: null,
    };
  }
}

Example: Environment Variable Injector

src/plugins/env-injector.ts
import { CompilerPlugin } from 'commandkit/plugin';
import type { PluginTransformParameters, TransformedResult } from 'commandkit/plugin';

export class EnvInjectorPlugin extends CompilerPlugin {
  public readonly name = 'EnvInjector';

  public async transform(
    params: PluginTransformParameters,
  ): Promise<TransformedResult> {
    let { code } = params;

    // Replace process.env.VAR_NAME with actual values
    code = code.replace(
      /process\.env\.([A-Z_]+)/g,
      (match, envVar) => {
        const value = process.env[envVar];
        return value ? JSON.stringify(value) : match;
      }
    );

    return { code, map: null };
  }
}

Plugin Options

Plugins can accept configuration options:
src/plugins/configurable.ts
import { RuntimePlugin } from 'commandkit/plugin';

interface MyPluginOptions {
  enableLogging: boolean;
  logLevel: 'info' | 'warn' | 'error';
}

export class ConfigurablePlugin extends RuntimePlugin<MyPluginOptions> {
  public readonly name = 'ConfigurablePlugin';

  public async activate(): Promise<void> {
    if (this.options.enableLogging) {
      console.log(`Plugin activated with log level: ${this.options.logLevel}`);
    }
  }
}

// Usage:
new ConfigurablePlugin({
  enableLogging: true,
  logLevel: 'info',
});

Preloading Files

Runtime plugins can preload JavaScript files:
export class MyPlugin extends RuntimePlugin {
  public constructor(options: any) {
    super(options);
    this.preload.add('setup.js');
    this.preload.add('config.js');
  }
}

Advanced Examples

Database Integration Plugin

src/plugins/database.ts
import { RuntimePlugin, CommandKitPluginRuntime } from 'commandkit/plugin';
import { PrismaClient } from '@prisma/client';

export class DatabasePlugin extends RuntimePlugin {
  public readonly name = 'DatabasePlugin';
  private prisma: PrismaClient | null = null;

  public async activate(ctx: CommandKitPluginRuntime): Promise<void> {
    this.prisma = new PrismaClient();
    await this.prisma.$connect();
    
    // Store in CommandKit instance
    ctx.commandkit.store.set('db', this.prisma);
    
    console.log('Database connected!');
  }

  public async deactivate(): Promise<void> {
    if (this.prisma) {
      await this.prisma.$disconnect();
      console.log('Database disconnected!');
    }
  }
}

// Access in commands:
export const run: CommandRunOptions = async ({ context }) => {
  const db = context.commandkit.store.get('db') as PrismaClient;
  const users = await db.user.findMany();
};

Command Usage Analytics Plugin

src/plugins/analytics.ts
import { RuntimePlugin } from 'commandkit/plugin';
import type { CommandKitEnvironment } from 'commandkit';
import type { Interaction, Message } from 'discord.js';

export class AnalyticsPlugin extends RuntimePlugin {
  public readonly name = 'Analytics';

  public async executeCommand(
    ctx: any,
    env: CommandKitEnvironment,
    source: Interaction | Message,
    command: any,
  ): Promise<boolean> {
    const userId = 'user' in source ? source.user.id : source.author.id;
    const guildId = source.guildId;

    // Send to analytics service
    await fetch('https://analytics.example.com/track', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        event: 'command_executed',
        command: command.name,
        userId,
        guildId,
        timestamp: Date.now(),
      }),
    });

    return false;
  }
}

Best Practices

Clean Up Resources

Always clean up resources in the deactivate() method.

Error Handling

Handle errors gracefully to prevent plugin failures from crashing the bot.

Type Safety

Use TypeScript and type your plugin options for better DX.

Document Behavior

Document what your plugin does and how to configure it.

Testing Plugins

tests/plugins/logger.test.ts
import { describe, it, expect, vi } from 'vitest';
import { LoggerPlugin } from '../src/plugins/logger';

describe('LoggerPlugin', () => {
  it('should activate successfully', async () => {
    const plugin = new LoggerPlugin({});
    const consoleSpy = vi.spyOn(console, 'log');

    await plugin.activate({} as any);

    expect(consoleSpy).toHaveBeenCalledWith('LoggerPlugin activated!');
  });

  it('should deactivate successfully', async () => {
    const plugin = new LoggerPlugin({});
    const consoleSpy = vi.spyOn(console, 'log');

    await plugin.deactivate({} as any);

    expect(consoleSpy).toHaveBeenCalledWith('LoggerPlugin deactivated!');
  });
});

API Reference

RuntimePlugin<T>

Base class for runtime plugins. Properties:
  • name - Plugin name (required)
  • type - Plugin type (automatic)
  • options - Plugin configuration
  • preload - Set of files to preload
Methods: See Runtime Plugin Hooks

CompilerPlugin<T>

Base class for compiler plugins. Properties:
  • name - Plugin name (required)
  • type - Plugin type (automatic)
Methods:
  • transform(params) - Transform code

CommandKitPluginRuntime

Context passed to runtime plugin hooks. Properties:
  • commandkit - CommandKit instance

PluginTransformParameters

Parameters for compiler transform method. Properties:
  • code - Source code string
  • id - File path

Build docs developers (and LLMs) love