Contexts
Every spearkit handler — command, button, select, modal — receives a context object. They all share BaseContext, which smooths over discord.js' reply/defer/edit/follow-up state…
import { command, option } from "spearkit";
export default command({
name: "hello",
description: "Say hello",
options: { name: option.string({ description: "Name", required: true }) },
run: (ctx) => ctx.reply(`Hi, ${ctx.options.name}!`),
});CommandContext, ButtonContext, StringSelectContext, modal contexts and the
rest extend BaseContext, adding their own specifics (e.g. ctx.options,
ctx.params, ctx.fields) on top of everything below.
Reply helpers
| Method | Returns | Behaviour |
|---|---|---|
reply(input) | Promise<InteractionResponse> | Send the initial response. |
replyEphemeral(input) | Promise<InteractionResponse> | Reply, hidden to everyone but the invoking user. |
defer({ ephemeral }) | Promise<InteractionResponse> | Acknowledge now, respond later via editReply. |
editReply(input) | Promise<Message> | Edit the original (or deferred) response. |
followUp(input) | Promise<Message> | Add a message after the initial response. |
send(input) | Promise<void> | State-aware: replies, edits, or follows up automatically. |
error(input, options?) | Promise<void> | State-aware preset error embed; ephemeral by default. |
success / info / warn (input, options?) | Promise<void> | State-aware preset embeds. |
replyError / replySuccess / replyInfo / replyWarn (input, options?) | Promise<InteractionResponse> | Initial-reply preset embeds. |
import { command } from "spearkit";
export default command({
name: "demo",
description: "Reply helpers",
run: async (ctx) => {
await ctx.reply("Working on it…");
await ctx.followUp("…almost done.");
},
});send is the one most handlers need
send inspects the interaction state and does the right thing:
- not yet answered →
reply - already deferred →
editReply - already replied →
followUp
This means you can call send without tracking whether you deferred, which is
ideal for shared helpers that may run before or after a defer.
import { command } from "spearkit";
export default command({
name: "report",
description: "Generate a report",
run: async (ctx) => {
await ctx.defer(); // acknowledge while we do slow work
const data = await buildReport();
await ctx.send(data); // sees the deferred state → edits the reply
},
});error for ephemeral failures
error(input, options?) sends a state-aware preset error embed — ephemeral
by default (pass { ephemeral: false } to make it public) — perfect for
validation failures that only the invoking user should see.
import { command, option } from "spearkit";
export default command({
name: "kick",
description: "Kick a member",
options: { who: option.user({ description: "Member", required: true }) },
run: async (ctx) => {
if (!ctx.guild) return ctx.error("This command only works in a server.");
await ctx.reply(`Kicked ${ctx.options.who}.`);
},
});Preset embeds
BaseContext builds consistent, colored embeds from client.embeds (or a shared
default). Each takes an EmbedPresetInput — a plain string, or a structured body
({ title?, description?, fields?, footer?, ... }) — and an optional
{ ephemeral? }.
| Method | Sends via | Default visibility |
|---|---|---|
success(input, options?) | send (state-aware) | public |
info(input, options?) | send (state-aware) | public |
warn(input, options?) | send (state-aware) | public |
error(input, options?) | send (state-aware) | ephemeral |
replySuccess / replyInfo / replyWarn (input, options?) | reply (initial only) | public |
replyError(input, options?) | reply (initial only) | ephemeral |
import { command } from "spearkit";
export default command({
name: "save",
description: "Save settings",
run: async (ctx) => {
await ctx.success("Settings saved."); // green embed, public
await ctx.warn({ title: "Heads up", description: "Quota is almost full." });
// error defaults to ephemeral; make it public with { ephemeral: false }:
// await ctx.error("Failed to save.", { ephemeral: false });
},
});Configure the colors/icons with the client embeds option; see the
API reference.
The { ephemeral: true } shortcut
discord.js represents an ephemeral reply with flags: MessageFlags.Ephemeral.
spearkit lets you write the more obvious { ephemeral: true } on any reply payload
and maps it to that flag for you. The input type is ReplyInput
(string | ReplyData), where ReplyData is discord.js'
InteractionReplyOptions plus the optional ephemeral boolean.
import { command, EmbedBuilder } from "spearkit";
export default command({
name: "secret",
description: "Only you can see this",
run: (ctx) =>
ctx.reply({
embeds: [new EmbedBuilder().setTitle("Just for you")],
ephemeral: true, // mapped to MessageFlags.Ephemeral
}),
});replyEphemeral(input) is sugar for the same thing, accepting either a string
or a payload:
await ctx.replyEphemeral("Saved.");
await ctx.replyEphemeral({ embeds: [embed] });If you set flags yourself, spearkit preserves them and adds the ephemeral flag
rather than overwriting it.
Exported helpers
spearkit exports the two functions it uses internally, so you can normalise reply input yourself (e.g. in a plugin or shared utility):
normalizeReply(input: ReplyInput): InteractionReplyOptions— converts a string orReplyDatainto a discord.js reply payload, applying the ephemeral flag mapping.asEphemeral(input: ReplyInput): ReplyData— marks any input ephemeral, regardless of how it was passed.
import { normalizeReply, asEphemeral } from "spearkit";
normalizeReply("hi");
// → { content: "hi" }
normalizeReply({ content: "hi", ephemeral: true });
// → { content: "hi", flags: MessageFlags.Ephemeral }
asEphemeral("hidden");
// → { content: "hidden", ephemeral: true }Accessors
BaseContext forwards the common interaction fields so you do not reach through
ctx.interaction for everyday data:
| Accessor | Description |
|---|---|
interaction | The raw discord.js interaction. |
client | The SpearClient (typed as the interaction's client). |
user | The invoking User. |
member | The invoking guild member (or null outside a guild). |
guild | The Guild, or null in DMs. |
guildId | The guild id, or null. |
channel | The channel the interaction came from. |
channelId | The channel id. |
locale | The user's locale. |
deferred | Whether the interaction is already deferred. |
replied | Whether the interaction already received an initial response. |
botPermissions | The bot's resolved permissions in the channel (PermissionsBitField, zero-fetch). |
import { command } from "spearkit";
export default command({
name: "whereami",
description: "Report context",
run: (ctx) =>
ctx.reply(
ctx.guild
? `In ${ctx.guild.name} (#${ctx.channelId}), locale ${ctx.locale}.`
: "We're in a DM.",
),
});deferred and replied let you branch when you are not using send:
import { button } from "spearkit";
export default button({
id: "refresh",
label: "Refresh",
run: async (ctx) => {
if (ctx.replied || ctx.deferred) await ctx.followUp("Refreshed.");
else await ctx.reply("Refreshed.");
},
});Permission preflights
BaseContext reads the permissions Discord already attached to the interaction —
no extra fetches — so you can check before attempting a privileged action:
import { command, PermissionFlagsBits } from "spearkit";
export default command({
name: "slowmode",
description: "Set slowmode",
run: async (ctx) => {
const missing = ctx.botMissing(PermissionFlagsBits.ManageChannels);
if (missing.length > 0) return ctx.error(`I'm missing: ${missing.join(", ")}`);
// …apply slowmode…
},
});ctx.botPermissions— the bot'sPermissionsBitFieldin the current channel.ctx.botMissing(required)— permission names the bot lacks here ([]if none).ctx.userMissing(required)— permission names the invoking user lacks here.
For role-hierarchy and moderation preflights (acting on self/owner, comparing top
roles) see moderationCheck and the permission helpers in the
API reference.
Awaiting input
When a flow needs a follow-up message or a modal, the context wraps discord.js
collectors so you skip the boilerplate. Both resolve to null on timeout.
import { command, modal, textInput } from "spearkit";
const nameModal = modal({ id: "name", title: "Your name", fields: { name: textInput({ label: "Name" }) }, run: () => {} });
export default command({
name: "setup",
description: "Interactive setup",
run: async (ctx) => {
// Wait for the user to type an answer in this channel:
const reply = await ctx.awaitMessageFrom(ctx.user.id, { time: 30_000 });
if (reply === null) return ctx.error("Timed out.");
// Or show a modal and await its submission:
const submission = await ctx.awaitModal(nameModal);
if (submission !== null) await submission.reply(`Hi, ${submission.fields.getTextInputValue("name")}!`);
},
});The standalone awaitMessage, awaitComponent and showAndAwaitModal helpers
are also exported; see the API reference.
See also
- Commands —
CommandContext, options andshowModal. - Components — button, select and modal contexts.
Events
event() defines a reusable, loadable discord.js event listener with a fully-typed handler. The handler's arguments are inferred from discord.js' ClientEvents, so you never…
Collectors
discord.js collectors are powerful but fiddly: you wire an event emitter, set a time, write a filter, remember that dismissed modals need their own timeout, and translate the…