Commands and options
Create commands with command() and place them under src/commands/. Command names are unique across your project and enabled plugins; Popii stops startup when two definitions collide.
Slash and text invocation
Section titled “Slash and text invocation”A chat-input command can be invoked as both a Discord slash command and a guild text command. Both modes use the same handler, options, authorization rules, middleware, cooldowns, quotas, timeout, metrics, localization, and error boundary.
import { ApplicationCommandOptionType } from "discord.js";import { command } from "popii-framework/commands";
export default command({ name: "greet", description: "Greet somebody", slash: true, text: true, aliases: ["hello"], options: [ { name: "user", description: "Who to greet", type: ApplicationCommandOptionType.User, required: true, }, ], async do(pop) { const user = pop.options.getUser("user")!; await pop.reply(`Hello, ${user.username}!`); },});With prefix: "!", this definition accepts:
/greet user:@Cake!greet @Cake!hello @Cakeslash and text default to enabled for ordinary chat-input commands. Set one explicitly to false when a command only makes sense in the other mode:
export default command({ name: "say", description: "Send a message from a text invocation", slash: false, text: true, async do(pop) { await pop.reply("Hello from a text command"); },});Slash registration ignores commands with slash: false. The message handler ignores commands with text: false.
Configure the prefix
Section titled “Configure the prefix”Set a static prefix in popii.config.ts:
import { defineConfig } from "popii-framework";
export default defineConfig({ token: process.env.DISCORD_TOKEN!, prefix: "!",});The prefix may also be a synchronous or asynchronous function. Return one prefix, several accepted prefixes, or null to disable prefix commands for that message:
export default defineConfig({ token: process.env.DISCORD_TOKEN!, async prefix(message) { if (!message.guildId) return null; return message.guildId === process.env.SUPPORT_GUILD_ID ? ["!", "?"] : "!"; },});Mentioning the bot also works as a prefix even when no string prefix matches:
@Popii pingMessage Content Intent
Section titled “Message Content Intent”Prefix commands require Discord’s privileged Message Content Intent. Open the Discord Developer Portal, select the application, go to Bot → Privileged Gateway Intents, and enable Message Content Intent.
Popii requests the corresponding gateway intent, but Discord will reject or withhold message content unless it is enabled for the application. Slash commands do not require this privileged intent.
Text argument parsing
Section titled “Text argument parsing”Text arguments are mapped to the command’s declared option order. The final string option consumes all remaining positional text.
!announce general Maintenance starts in ten minutesSingle and double quotes preserve spaces, and a backslash escapes the following character:
!tag create "release notes" "Cakemix is ready"!tag create 'owner tools' 'Restricted workspace'Named flags use --name=value. A flag without a value is treated as boolean true:
!announce --channel=general --message="Maintenance starts soon" --silentThe text adapter supports string, integer, number, boolean, user, channel, role, mentionable, and attachment getters. Users, roles, and channels may be supplied as Discord mentions or IDs. Attachments are read from the message in declared attachment-option order.
For a required argument that is missing, Popii prompts when the active context provides awaitMessage; otherwise it returns a localized missing-argument response. Permission and middleware checks run before any prompt.
Replies and deferral
Section titled “Replies and deferral”Use pop.reply() in handlers shared by slash and text commands. It maps to the correct Discord response mechanism in either mode.
pop.defer() sends the typing indicator for a text command. pop.respond() is interaction-only and throws when called from a text command.
Do not read raw interaction fields in a dual-mode handler. Prefer pop.options, pop.user, pop.member, pop.guild, pop.channel, and pop.reply().
Subcommands
Section titled “Subcommands”Text invocations resolve declared subcommands and subcommand groups from positional words:
/config moderation enable!config moderation enableNested files still provide the conventional command structure, such as src/commands/config/moderation/enable.ts.
Custom text commands
Section titled “Custom text commands”Guild managers can create custom text and role commands from Dashboard → Server → Manage → Community → Custom commands. They may enable or disable text invocation and choose text response, role add, role remove, or role toggle behavior.
Custom commands are checked when no authored text command matches. They remain guild-scoped and cannot override an authored command with the same resolved name.
Security and restrictions
Section titled “Security and restrictions”Text commands honor the same declarations as slash commands:
guildOnlyandownerOnly- Discord permissions
- Allowed and denied roles
- Allowed channels
- Cooldowns, quotas, and mutexes
- Global and guild command disablement
- User and guild blacklists
- Middleware and input schemas
- Execution timeouts and error handling
Bots are ignored, preventing bot-to-bot command loops.
Synchronization
Section titled “Synchronization”Run popii sync after changing slash-visible names, descriptions, options, or command structure. Text-only changes do not require Discord synchronization, but the running bot must be restarted or reloaded through development mode.
popii syncpopii devSee the complete compiled example under examples/commands/text.ts.