spearkit
Guides

Context-menu commands

Context-menu commands are the right-click "Apps" actions Discord shows on a user or a message. spearkit makes them first-class: define one with userCommand or messageCommand…

import { userCommand } from "spearkit";

export const whois = userCommand({
  name: "Who is this?",
  run: (ctx) => ctx.replyEphemeral(`That's ${ctx.targetUser.tag}.`),
});

name is the label shown in the Apps menu (no description — Discord does not show one for context-menu commands).

User vs message commands

BuilderAppears onTarget context
userCommanda user (right-click → Apps)ctx.targetUser, ctx.targetMember
messageCommanda message (right-click → Apps)ctx.targetMessage
import { messageCommand } from "spearkit";

export const report = messageCommand({
  name: "Report message",
  run: (ctx) =>
    ctx.replyEphemeral(`Reported message ${ctx.targetMessage.id}.`),
});

Both handler contexts extend the shared BaseContext, so ctx.reply, ctx.replyEphemeral, ctx.defer, ctx.success/error/... and the usual accessors are all available.

Metadata, cooldowns and guards

Both builders accept the same metadata, plus a cooldown and guards:

FieldTypeEffect
namestringThe Apps-menu label.
defaultMemberPermissionsPermissionResolvable | nullDefault permission gate.
nsfwbooleanMarks the command age-restricted.
guildOnlybooleanRestricts it to guild contexts.
nameLocalizationsLocalizationMapPer-locale label.
cooldownnumber | CooldownConfigRate limit (shares client.cooldowns).
guardsreadonly Guard[]Preconditions run before the handler.
autoDeferboolean | { ephemeral?, delayMs? }Auto-deferReply() if the handler is slow, preventing Unknown interaction.
import { userCommand, guildOnly, requireUserPermissions, PermissionFlagsBits } from "spearkit";

export const warn = userCommand({
  name: "Warn user",
  guildOnly: true,
  cooldown: 5_000,
  guards: [requireUserPermissions(PermissionFlagsBits.ModerateMembers)],
  run: (ctx) => ctx.replyEphemeral(`Warned ${ctx.targetUser.tag}.`),
});

See Cooldowns and Guards for the shared options.

Registering and deploying

Register context-menu commands like everything else with client.register(...). They route automatically — spearkit dispatches user- and message-context-menu interactions to the matching command.

client.register(whois, report, warn);

Because context menus and slash commands deploy to the same Discord endpoint, push them together with deployAllCommands once you mix the two — it sends both in a single request. (deployCommands is slash-only.)

await client.start(process.env.DISCORD_TOKEN);
await client.deployAllCommands({ guildId: process.env.GUILD_ID }); // slash + menus

deployAllCommands also supports a dryRun flag and a strategy: "diff" that skips the PUT when the remote set already matches — handy in CI:

// Preview without touching Discord:
const result = await client.deployAllCommands({ guildId, dryRun: true });
// → { skipped: true, reason: "dry-run", body: [...] }

// Only deploy when something changed:
await client.deployAllCommands({ guildId, strategy: "diff" });

The registry

client.contextMenus is a ContextMenuRegistry. The client wires it to the logger and cooldown manager and routes interactions for you, so you rarely touch it directly:

client.contextMenus.size;   // number registered
client.contextMenus.all();  // ContextMenuCommand[]
client.contextMenus.toJSON(); // REST payloads (also included by deployAllCommands)

Note: context-menu commands are not picked up by client.load(...) directory loading — register them explicitly with client.register(...).

See also

On this page