Docs
Developer Documentation

Commands

Declare Discord slash and prefix commands, options, autocomplete, and permission controls.

Commands declare interactions exposed to Discord users. Medusa automatically registers slash commands with the Discord Application API, handles permissions, parses subcommands and options, and allows server administrators to customize command prefixes or disable commands through the dashboard.

Commands reside in src/commands/ and load from every *.js file located directly in that directory.

Base Class Definition

modules/example/src/commands/manage.js
import { Commands } from "#medusa/modules";
import { MessageFlags } from "discord.js";

export class ManageCommand extends Commands {
    constructor(medusa) {
        super(medusa, {
            name: "manage",
            description: "Manage system items and configurations.",
            category: "General",
            baselinePermission: "moderator",
            options: [
                {
                    name: "list",
                    description: "List all items in the current server.",
                    type: "subcommand"
                },
                {
                    name: "create",
                    description: "Create a new item.",
                    type: "subcommand",
                    options: [
                        {
                            name: "identifier",
                            description: "Unique item identifier slug.",
                            type: "string",
                            required: true
                        },
                        {
                            name: "name",
                            description: "Display name of the item.",
                            type: "string",
                            required: true
                        },
                        {
                            name: "quantity",
                            description: "Initial stock count.",
                            type: "integer",
                            required: false
                        },
                        {
                            name: "channel",
                            description: "Notification channel for item alerts.",
                            type: "channel",
                            required: false
                        }
                    ]
                }
            ]
        });
    }

    async run(interaction) {
        const allowed = await this.medusa.permissions.guard(interaction, this.getName());
        if (!allowed) return;

        await interaction.deferReply({ flags: MessageFlags.Ephemeral });
        const subcommand = interaction.options.getSubcommand();
        const handler = this.medusa.modules.handlers.get(this.module, "items");

        switch (subcommand) {
            case "list": {
                const items = await handler.listForGuild(interaction.guild.id);
                if (items.length === 0) {
                    return interaction.editReply({
                        embeds: [this.medusa.embeds.info("info", "No items have been created yet.")]
                    });
                }
                return interaction.editReply({
                    embeds: [
                        this.medusa.embeds.default({
                            title: "Configured Items",
                            description: items.map(item => `• **${item.name}** (\`${item.identifier}\`) - Quantity: ${item.quantity}`).join("\n")
                        })
                    ]
                });
            }
            case "create": {
                const id = interaction.options.getString("identifier", true);
                const name = interaction.options.getString("name", true);
                const quantity = interaction.options.getInteger("quantity") !== null ? interaction.options.getInteger("quantity") : 1;

                try {
                    await handler.create(interaction.guild.id, interaction.user.id, id, name, { quantity });
                    return interaction.editReply({
                        embeds: [this.medusa.embeds.success(`Item **${name}** created successfully.`)]
                    });
                } catch (error) {
                    return interaction.editReply({
                        embeds: [this.medusa.embeds.error(error.message)]
                    });
                }
            }
            default:
                return interaction.editReply({
                    embeds: [this.medusa.embeds.error("Unknown subcommand.")]
                });
        }
    }
}

Constructor Options

The configuration object passed to super(medusa, meta) accepts the following properties:

PropertyTypeDefaultDescription
namestringRequiredCommand name in lowercase alphanumeric characters without spaces.
descriptionstringRequiredExplanation of command purpose displayed in Discord's slash command picker.
categorystring"General"Organizational group used in dashboard menus and help listings.
baselinePermissionstring"member"Default permission rank required: "member", "moderator", or "admin".
dependencystring""Optional module dependency required for the command to register.
optionsArray<object>[]List of Discord option definitions, subcommands, or subcommand groups.

Supported Option Types

Medusa supports clean lowercase type strings that resolve automatically to Discord.js ApplicationCommandOptionType values:

Type StringDiscord.js Enum ValueDescription
"subcommand"ApplicationCommandOptionType.SubcommandSubcommand nesting branch.
"subcommandGroup"ApplicationCommandOptionType.SubcommandGroupGroup containing multiple subcommands.
"string"ApplicationCommandOptionType.StringText input option.
"integer"ApplicationCommandOptionType.IntegerWhole number input option.
"number"ApplicationCommandOptionType.NumberFloating point or decimal number option.
"boolean"ApplicationCommandOptionType.BooleanTrue or false toggle option.
"user"ApplicationCommandOptionType.UserDiscord member or user mention selector.
"channel"ApplicationCommandOptionType.ChannelDiscord channel selector.
"role"ApplicationCommandOptionType.RoleDiscord role selector.
"mentionable"ApplicationCommandOptionType.MentionableUser, member, or role mention selector.
"attachment"ApplicationCommandOptionType.AttachmentFile upload option.

Option Attributes

Each entry inside options accepts standard Discord command option properties:

{
    name: "category",
    description: "Item category filter.",
    type: "string",
    required: true,
    choices: [
        { name: "Weapons", value: "weapons" },
        { name: "Armor", value: "armor" },
        { name: "Potions", value: "potions" }
    ],
    autocomplete: false
}

Option Schema Properties

PropertyTypeRequiredDescription
namestringYesLowercase option name (1-32 characters).
descriptionstringYesOption description displayed in Discord UI.
typestringYesType identifier from the supported option types table.
requiredbooleanNoWhether Discord requires the user to specify this option before submitting.
choicesArray<object>NoStatic choices array containing objects with name and value (string or number).
autocompletebooleanNoEnables dynamic autocomplete handled by autocomplete(interaction).
optionsArray<object>NoNested options array for "subcommand" or "subcommandGroup".

Autocomplete Interactions

To provide dynamic search recommendations in real time as the user types, set autocomplete: true on a string or number option and implement autocomplete(interaction):

async autocomplete(interaction) {
    const focused = interaction.options.getFocused(true);
    if (focused.name === "identifier") {
        const handler = this.medusa.modules.handlers.get(this.module, "items");
        const items = await handler.listForGuild(interaction.guild.id);
        const query = focused.value.toLowerCase();

        const filtered = items
            .filter(item => item.name.toLowerCase().includes(query) || item.identifier.toLowerCase().includes(query))
            .slice(0, 25);

        await interaction.respond(
            filtered.map(item => ({
                name: `${item.name} (${item.identifier})`,
                value: item.identifier
            }))
        );
    }
}

Permission Verification

Medusa provides a unified permission system covering Discord roles, server owners, administrators, and dashboard ranks. Verify permissions at the start of run():

const allowed = await this.medusa.permissions.guard(interaction, this.getName());
if (!allowed) return;

guard() automatically responds to unauthorized users with a localized error embed using MessageFlags.Ephemeral and returns false.

Discord UI Message Components & Modals

Commands can attach interactive buttons, select menus, and modals to replies using Discord.js builder classes.

Custom ID Format Rule

Medusa enforces a strict 5-part custom ID specification on all interactive message components:

medusa:<component>:<module>:<name>:<describer>
SegmentValid ValuesDescription
prefixmedusaMedusa system namespace identifier.
componentbutton, select_menu, modalType of interactive Discord component.
moduleModule name (e.g. template, tickets)Target module responsible for handling the interaction.
nameAction identifier (e.g. primary, category, submit)Action verb or target handler method.
describerContext identifier (e.g. click, filter, form)Contextual payload, item ID, or sub-action state.

Any component interaction with a custom ID not matching this format will be dropped and logged as a malformed ID.

Sending Buttons and Select Menus

import { ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder } from "discord.js";

const buttonRow = new ActionRowBuilder().addComponents(
    new ButtonBuilder()
        .setCustomId("medusa:button:example:confirm:action")
        .setLabel("Confirm")
        .setStyle(ButtonStyle.Success),
    new ButtonBuilder()
        .setCustomId("medusa:button:example:cancel:action")
        .setLabel("Cancel")
        .setStyle(ButtonStyle.Danger)
);

const selectRow = new ActionRowBuilder().addComponents(
    new StringSelectMenuBuilder()
        .setCustomId("medusa:select_menu:example:category:picker")
        .setPlaceholder("Select a category")
        .addOptions([
            { label: "Bundles", value: "bundles" },
            { label: "Membership", value: "membership" }
        ])
);

await interaction.editReply({
    embeds: [this.medusa.embeds.default({ title: "Interactive Panel" })],
    components: [buttonRow, selectRow]
});

Launching Discord Modals

To present an interactive modal form, call interaction.showModal(modal) before deferring the interaction:

import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from "discord.js";

const modal = new ModalBuilder()
    .setCustomId("medusa:modal:example:submit:form")
    .setTitle("Register Entry");

const input = new TextInputBuilder()
    .setCustomId("title")
    .setLabel("Title")
    .setStyle(TextInputStyle.Short)
    .setRequired(true);

modal.addComponents(new ActionRowBuilder().addComponents(input));
await interaction.showModal(modal);

On this page