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
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:
| Property | Type | Default | Description |
|---|---|---|---|
name | string | Required | Command name in lowercase alphanumeric characters without spaces. |
description | string | Required | Explanation of command purpose displayed in Discord's slash command picker. |
category | string | "General" | Organizational group used in dashboard menus and help listings. |
baselinePermission | string | "member" | Default permission rank required: "member", "moderator", or "admin". |
dependency | string | "" | Optional module dependency required for the command to register. |
options | Array<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 String | Discord.js Enum Value | Description |
|---|---|---|
"subcommand" | ApplicationCommandOptionType.Subcommand | Subcommand nesting branch. |
"subcommandGroup" | ApplicationCommandOptionType.SubcommandGroup | Group containing multiple subcommands. |
"string" | ApplicationCommandOptionType.String | Text input option. |
"integer" | ApplicationCommandOptionType.Integer | Whole number input option. |
"number" | ApplicationCommandOptionType.Number | Floating point or decimal number option. |
"boolean" | ApplicationCommandOptionType.Boolean | True or false toggle option. |
"user" | ApplicationCommandOptionType.User | Discord member or user mention selector. |
"channel" | ApplicationCommandOptionType.Channel | Discord channel selector. |
"role" | ApplicationCommandOptionType.Role | Discord role selector. |
"mentionable" | ApplicationCommandOptionType.Mentionable | User, member, or role mention selector. |
"attachment" | ApplicationCommandOptionType.Attachment | File 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
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Lowercase option name (1-32 characters). |
description | string | Yes | Option description displayed in Discord UI. |
type | string | Yes | Type identifier from the supported option types table. |
required | boolean | No | Whether Discord requires the user to specify this option before submitting. |
choices | Array<object> | No | Static choices array containing objects with name and value (string or number). |
autocomplete | boolean | No | Enables dynamic autocomplete handled by autocomplete(interaction). |
options | Array<object> | No | Nested 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>| Segment | Valid Values | Description |
|---|---|---|
prefix | medusa | Medusa system namespace identifier. |
component | button, select_menu, modal | Type of interactive Discord component. |
module | Module name (e.g. template, tickets) | Target module responsible for handling the interaction. |
name | Action identifier (e.g. primary, category, submit) | Action verb or target handler method. |
describer | Context 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);
