Commands
Slash commands in spearkit are defined as a single object: the metadata, the typed options, and the handler all live together. spearkit serialises them for discord and routes…
A first command
import { command } from "spearkit";
export const ping = command({
name: "ping",
description: "Check latency",
run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
});Register it on a client (client.register(ping)) and deploy it (see
Deployment). That's the whole loop.
The command context
The handler receives a CommandContext. It wraps the discord.js
ChatInputCommandInteraction and adds ergonomic accessors and reply helpers.
| Member | Description |
|---|---|
ctx.options | Resolved, fully-typed option values (see Options). |
ctx.commandName | The invoked command name. |
ctx.subcommand | The invoked subcommand name, or null. |
ctx.showModal(modal) | Present a modal in response. |
ctx.user / ctx.member / ctx.guild / ctx.guildId / ctx.channel / ctx.channelId / ctx.locale | Actor and location accessors. |
ctx.reply / ctx.replyEphemeral / ctx.defer / ctx.editReply / ctx.followUp / ctx.send / ctx.error | Reply helpers (see Contexts). |
ctx.interaction | The raw discord.js interaction, for anything not wrapped. |
import { command, option } from "spearkit";
export const echo = command({
name: "echo",
description: "Repeat a message",
options: {
text: option.string({ description: "What to say", required: true }),
times: option.integer({ description: "Repeat count", minValue: 1, maxValue: 5 }),
},
run: (ctx) => {
ctx.options.text; // string
ctx.options.times; // number | undefined
return ctx.reply({
content: ctx.options.text.repeat(ctx.options.times ?? 1),
ephemeral: true,
});
},
});Options are covered in depth in Options.
Command metadata
import { command, PermissionFlagsBits } from "spearkit";
export const purge = command({
name: "purge",
description: "Delete recent messages",
guildOnly: true, // only usable in guilds
nsfw: false, // age-restricted command
defaultMemberPermissions: PermissionFlagsBits.ManageMessages, // who sees it by default
nameLocalizations: { tr: "temizle" }, // localized name
descriptionLocalizations: { tr: "Mesajları sil" },
run: (ctx) => ctx.reply("…"),
});| Field | Type | Effect |
|---|---|---|
guildOnly | boolean | Restricts the command to guild contexts (alias for contexts: ["guild"]). |
install | ("guild" | "user")[] | App installation targets → integration_types. ["user"] makes the command user-installable. |
contexts | ("guild" | "botDm" | "privateChannel")[] | Where the command may run → contexts. |
nsfw | boolean | Marks the command age-restricted. |
defaultMemberPermissions | PermissionResolvable | null | Default permission gate (members without it don't see the command). |
nameLocalizations / descriptionLocalizations | LocalizationMap | Per-locale name/description. |
cooldown | number | CooldownConfig | Rate-limit the command (a number is milliseconds). See Cooldowns. |
guards | readonly Guard[] | Preconditions run before the handler. See Guards. |
autoDefer | boolean | { ephemeral?, delayMs? } | Auto-deferReply() if the handler is slow (>~2s), preventing Unknown interaction. Respond via ctx.send/ctx.editReply. |
User-installable apps and contexts
Commands expose two orthogonal Discord axes:
install— where your app is installed: guild install ("guild") or per-user install ("user"), serialised tointegration_types.contexts— where a command may run:"guild","botDm"or"privateChannel"(private channels are only reachable via user install).
import { command } from "spearkit";
export const remind = command({
name: "remind",
description: "Set a reminder from anywhere",
install: ["user"], // integration_types: [UserInstall]
contexts: ["guild", "botDm", "privateChannel"],
run: (ctx) => ctx.reply(`Reminder! ${ctx.interaction.context ?? ""}`),
});guildOnly: true keeps working as an alias for contexts: ["guild"]; passing
both it and an explicit contexts list throws at definition time. The app
itself must allow user installs in the Developer Portal for
integration_types: [UserInstall] to be accepted.
Subcommands and groups
For commands with subcommands, use commandGroup together with subcommand
and (optionally) subcommandGroup. Each subcommand has its own typed options
and handler; spearkit routes to the right one automatically.
import { commandGroup, subcommand, subcommandGroup, option } from "spearkit";
export const admin = commandGroup({
name: "admin",
description: "Administration",
guildOnly: true,
// Direct subcommands: /admin say
subcommands: {
say: subcommand({
description: "Make the bot say something",
options: { message: option.string({ description: "Message", required: true }) },
run: (ctx) => ctx.reply(ctx.options.message),
}),
},
// Grouped subcommands: /admin users ban
groups: {
users: subcommandGroup({
description: "Manage users",
subcommands: {
ban: subcommand({
description: "Ban a member",
options: {
target: option.user({ description: "Member", required: true }),
reason: option.string({ description: "Reason" }),
},
run: (ctx) =>
ctx.reply(`Banned ${ctx.options.target.tag}: ${ctx.options.reason ?? "no reason"}`),
}),
},
}),
},
});Inside a subcommand handler, ctx.options is typed from that subcommand's
options. There is no switch (subcommand) to write — spearkit dispatches by the
invoked subcommand group/name.
The command registry
client.commands is a CommandRegistry. You usually feed it through
client.register(...), but you can use it directly:
import { CommandRegistry } from "spearkit";
const registry = new CommandRegistry();
registry.add(ping, echo, admin);
registry.get("ping"); // SlashCommand | undefined
registry.names; // string[]
registry.size; // number
registry.remove("ping"); // boolean
registry.toJSON(); // REST payloads for all commandsSpearClient calls registry.handle(interaction) and
registry.handleAutocomplete(interaction) for you on every interaction.
Error handling
If a routed handler throws, spearkit logs it and sends a safe response. Configure one policy for slash commands, components, context menus, and prefix commands:
const client = new SpearClient({
onHandlerError: ({ source, name, error }) => {
report(error, { source, name });
return "That action failed. Try again later."; // override safe reply
// return false; // suppress automatic reply
},
});Individual registries still expose .onError(...) for advanced overrides.
Automatic help
helpCommand() reads the live slash, prefix, and context-menu registries and
renders a user-scoped paginator:
import { helpCommand } from "spearkit";
client.register(
ping,
settings,
helpCommand({
title: "Bot commands",
pageSize: 8,
// transform: (entries) => entries.filter(canShow),
}),
);The help command excludes itself. Set includePrefix: false or
includeContextMenus: false to limit surfaces. transform can filter or reorder
entries per invocation.
Deployment
Commands must be registered with discord before they appear. spearkit gives you two ways.
From the client (uses the client's authenticated REST; call after ready):
await client.start(process.env.DISCORD_TOKEN);
await client.deployCommands({ guildId: process.env.GUILD_ID }); // omit guildId for globalStandalone (a separate deploy script, no running client needed):
import { CommandRegistry } from "spearkit";
const registry = new CommandRegistry().add(ping, echo, admin);
await registry.deploy({
token: process.env.DISCORD_TOKEN,
applicationId: process.env.DISCORD_APP_ID,
guildId: process.env.GUILD_ID, // optional
});Guild deploys apply instantly and are ideal during development. Global
deploys (no guildId) can take up to an hour to propagate.
See also
Getting started
spearkit is discord.js++: it re-exports the entire discord.js surface and adds a fully type-safe layer for events, slash commands and interactive components. This page takes you…
Options
Slash command options are declared as a map of name → builder. spearkit infers the exact value type each option resolves to, so your handler's ctx.options is fully typed — no…