# Commands

URL: https://zerodev.ca/docs/medusa/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

```js title="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.modules.lang.embed(this.module, "messages", "itemList", {
                            items: 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"`  | The rank the command starts in: `"member"`, `"staff"`, `"moderator"`, `"admin"` or `"owner"`. Any other value matches no rank, which leaves the command to the license owner only. Server owners can move commands between ranks on the **Permissions** page.                                    |
| `dependency`         | `string`        | `""`        | Optional module dependency required for the command to register.                                                                                                                                                                                                                                 |
| `guild`              | `string`        | `""`        | Optional server id. When set, the command only syncs, resolves, and lists in that server. Used for runtime commands such as automation flow commands.                                                                                                                                            |
| `bot`                | `string`        | `""`        | Optional bot key (`"primary"` or an extra bot's user id). When set and that bot exists, the command syncs to and is handled by that bot instead of the bot that owns the module. Falls back to the module owner when the bot was removed. Automation flows use it to run a flow on a chosen bot. |
| `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:

```js
{
    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"`.                                                |
| `protected`    | `string`        | No       | Addon name that locks a `"subcommand"` on while the license lacks that addon. See Addon-Protected Subcommands. |

Discord requires required options to precede optional options. Medusa validates option ordering during command registration and throws an error at startup if a required option appears after an optional option.

## Command & Option Localization

Medusa synchronizes localized command, subcommand, and option descriptions with Discord Application Commands using `description_localizations`.

Translations load from each module's `commands` language schema (`resources/lang/en/commands.js` and `translations.json` or `resources/lang/<locale>/common.json`).

### Localization Key Mapping

Medusa maps commands, subcommands, and options to language keys using camelCase naming:

| Target Element           | Path Pattern                    | Example Key                         |
| ------------------------ | ------------------------------- | ----------------------------------- |
| Top-level command        | `<command>`                     | `commands.interact.description`     |
| Top-level option         | `<command><Option>`             | `commands.interactUser.description` |
| Subcommand               | `<command><Subcommand>`         | `commands.repGive.description`      |
| Subcommand option        | `<command><Subcommand><Option>` | `commands.repGiveUser.description`  |
| Shared subcommand option | `<command><Option>` (fallback)  | `commands.repUser.description`      |

Declare the base English phrases matching these keys in `resources/lang/en/commands.js`:

```js title="modules/engagement/resources/lang/en/commands.js"
import { Langs } from "#medusa/modules";

export class CommandsLang extends Langs {
    constructor(medusa) {
        super(medusa, {
            name: "commands",
            label: "Commands",
            description: "Command description localizations."
        });
    }

    schema() {
        return {
            interact: { type: "phrase", default: "Express social interactions with other members", label: "Interact" },
            interactUser: { type: "phrase", default: "Target member", label: "Interact user" },
            interactNote: { type: "phrase", default: "Optional message or context", label: "Interact note" }
        };
    }
}
```

Translations defined under the `commands` dictionary across all 16 supported languages populate Discord's localized description maps during command registration. Discord enforces a 100-character maximum on all command and option descriptions.

## Addon-Protected Subcommands

Any `"subcommand"` option may declare `protected: "<Addon>"`. While the active license does not include that addon, the subcommand stays force-enabled: dashboard toggles that would disable it or hide its slash entry are ignored at both registration and dispatch, so the subcommand always registers and always runs. Once the license includes the addon, the subcommand toggles normally.

This is the mechanism that keeps branding-bound commands reachable on unlicensed installs. The core `/info bot` subcommand uses it so the credits block stays available until the Watermark add-on removes default branding.

```js title="modules/core/src/commands/info.js"
{
    name: "bot",
    description: "View information about the bot",
    type: "subcommand",
    protected: "Watermark"
}
```

<Callout type="info">
  `protected` is schema metadata, not a Discord field. Medusa strips it from the option before registering the command, so it never appears in the slash command definition sent to Discord. Addon checks route through `hasAddon(name)` from the Internal API.
</Callout>

## 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)`:

```js
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
            }))
        );
    }
}
```

### The `suggest` Helper

`suggest(interaction, entries)` does the filtering for you: it matches the focused value against each entry's `name` and `value`, filters by `owner` when specified, keeps the first 25, trims names to Discord's 100 character limit and responds. Pass the full list:

```js
async autocomplete(interaction) {
    const reminders = await this.medusa.modules.handlers.get(this.module, "reminders").active(interaction.guildId, interaction.user.id);
    return this.suggest(interaction, reminders.map(reminder => ({ name: `#${reminder.id} · ${reminder.text}`, value: reminder.id })));
}
```

<Callout type="info">
  Prefer autocomplete over free-typed ids, and `type: "user"` over user id strings. The user selector still accepts a pasted id for members who have left the server, such as when unbanning.
</Callout>

### Autocomplete Scope

Every module gets a `<module>:autocomplete` permission node in the Events list of the Permissions page. Medusa only gates autocomplete on the command's own permission node. The `<module>:autocomplete` node is for scoping: members without it see only their own records, members with it see everything.

Entries passed to `this.suggest(interaction, entries)` can include an optional `owner: string` (Discord user ID):

* If `owner` is provided, members without `<module>:autocomplete` only see entries where `owner === interaction.user.id`.
* Members with `<module>:autocomplete` or administrator permissions see all entries.
* If `owner` is undefined, the entry is visible to everyone who can use the command.

```js
async autocomplete(interaction) {
    const licenses = await handler.list();
    return this.suggest(interaction, licenses.map(license => ({
        name: license.key,
        value: license.key,
        owner: license.discordId
    })));
}
```

## Permission Verification

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

```js
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`.

## Aliases and Prefix Configuration

Commands and subcommands support primary names, aliases, and custom aliases across slash and prefix triggers:

* `alias`: Declared in command metadata or subcommand options (retrieved via `command.getAlias()`).
* `preferAlias`: When enabled in configuration, registers the alias or custom alias as the Discord slash command name.
* `customAlias`: Overrides the slash command name when `preferAlias` is enabled.
* `customPrefixAlias`: Allows specifying a dedicated text prefix alias for invoking commands or subcommands in chat.

Subcommand handlers always receive the resolved canonical subcommand name regardless of whether an alias was invoked.

## Slash Command Limit Protection

Discord limits each application to 100 top-level application commands per guild or globally. Subcommands nested under a parent command do not consume top-level slots.

When more than 100 top-level commands are enabled across installed modules:

* Medusa registers the first 100 commands safely without crashing.
* A warning is emitted to the console: `"Some commands were not enabled due to Discord's slash command limit. View more on the dashboard"`.
* The dashboard displays a live tracker with current command counts and an alert banner when the limit is exceeded.

## 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:

```text
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

```js
await interaction.editReply(this.medusa.components.payload([
    this.medusa.components.container({
        components: [
            this.medusa.components.text("### Interactive Panel"),
            this.medusa.components.row([
                this.medusa.components.button({ style: "secondary", label: "Confirm", custom_id: "medusa:button:example:confirm:action" }),
                this.medusa.components.button({ style: "secondary", label: "Cancel", custom_id: "medusa:button:example:cancel:action" })
            ]),
            this.medusa.components.row([
                this.medusa.components.select({
                    custom_id: "medusa:select_menu:example:category:picker",
                    placeholder: "Select a category",
                    options: [
                        { label: "Bundles", value: "bundles" },
                        { label: "Membership", value: "membership" }
                    ]
                })
            ])
        ]
    })
]));
```

Buttons use string styles (`"primary"`, `"secondary"`, `"success"`, `"danger"`, `"link"`). See [Components](/docs/medusa/developer-documentation/components) for every builder method.

### Launching Discord Modals

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

```js
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);
```

## Context Menu Commands

Medusa supports Discord Context Menu commands (User and Message commands) that appear in the Discord client under the Apps right-click menu.

To declare a context menu command, set the `type` property in the command definition to the string literal `"user"` or `"message"`, never the `ApplicationCommandType` enum.

### Rules and Limitations

* Discord rejects context menu commands that define descriptions or options. Medusa strips these fields automatically when syncing context menu commands with the Discord API.
* Discord permits a maximum of 5 User commands and 5 Message commands per application.
* To prevent hitting the 5-command limit, set `contextMenuDefault: false` on commands that should remain optional. Administrators can enable up to 5 User and 5 Message context commands per guild in the dashboard.
* Context menu command names can contain uppercase letters and spaces.

### User Context Menu Example

```js title="modules/moderation/src/commands/banMember.js"
import { Commands } from "#medusa/modules";
import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from "discord.js";

export class BanMemberCommand extends Commands {
    constructor(medusa) {
        super(medusa, {
            name: "Ban Member",
            type: "user",
            category: "Moderation",
            baselinePermission: "moderator"
        });
    }

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

        const targetUser = interaction.targetUser;
        if (!targetUser) return;

        await interaction.showModal(
            new ModalBuilder()
                .setCustomId(`medusa:modal:moderation:ban:${targetUser.id}`)
                .setTitle(`Ban ${targetUser.username.slice(0, 20)}`)
                .addComponents(
                    new ActionRowBuilder().addComponents(
                        new TextInputBuilder()
                            .setCustomId("reason")
                            .setLabel("Reason")
                            .setStyle(TextInputStyle.Paragraph)
                            .setRequired(false)
                    )
                )
        );
    }
}
```

### Message Context Menu Example

```js title="modules/example/src/commands/bookmarkMessage.js"
import { Commands } from "#medusa/modules";
import { MessageFlags } from "discord.js";

export class BookmarkMessageCommand extends Commands {
    constructor(medusa) {
        super(medusa, {
            name: "Bookmark",
            type: "message",
            category: "General"
        });
    }

    async run(interaction) {
        await interaction.deferReply({ flags: MessageFlags.Ephemeral });
        const targetMessage = interaction.targetMessage;
        return interaction.editReply({
            embeds: [
                this.medusa.embeds.success(`Bookmarked message from <@${targetMessage.author.id}>: ${targetMessage.url}`)
            ]
        });
    }
}
```

## Primary Entry Point Commands

Medusa supports Discord Primary Entry Point commands (`type: "primary_entry_point"` or `"entry_point"`), which are used for Discord Activities and Embedded Applications.

### Rules and Architecture

* Discord restricts Primary Entry Point commands to the **global** application scope. They cannot be registered as guild-specific commands.
* An application can have at most one Primary Entry Point command globally.
* Medusa automatically syncs global Primary Entry Point commands on startup and preserves them during command cleanup.
* Supported handlers:
  * `handler: 2` (`DiscordLaunchActivity`, default): Discord's client launches the activity directly without needing a bot interaction.
  * `handler: 1` (`AppHandler`): Discord delivers an interaction to the bot, which responds by calling `await interaction.launchActivity()`.

### Example

```js title="modules/music/src/commands/activity.js"
import { Commands } from "#medusa/modules";
import { MessageFlags } from "discord.js";

export class ActivityCommand extends Commands {
    constructor(medusa) {
        super(medusa, {
            name: "activity",
            description: "Launch the voice channel Music Activity",
            type: "primary_entry_point",
            handler: 2,
            category: "Music",
            baselinePermission: "member"
        });
    }

    async run(interaction) {
        if (!interaction.guild || !interaction.member) return;
        if (interaction.launchActivity) {
            try {
                return await interaction.launchActivity();
            } catch (error) {
                this.medusa.console.error(error, "Activity", "Failed to launch activity.");
            }
        }
    }
}
```
