spearkit

API reference

Every symbol spearkit exports, in addition to the entire re-exported discord.js surface. Import any of these from "spearkit".

import { SpearClient, command, option, event, button, modal, row /* … */ } from "spearkit";

Client

class SpearClient extends Client

A discord.js Client with registries and interaction routing wired up.

new SpearClient(options?: SpearClientOptions)
MemberTypeDescription
commandsCommandRegistrySlash command registry + dispatcher.
eventsEventRegistryEvent listener registry.
componentsComponentRegistryButton/select/modal router.
loggerLoggerStructured logger (client.logger.child(scope) for sub-scopes).
cooldownsCooldownManagerShared cooldown manager (also used by prefix commands).
schedulerTaskSchedulerCron / interval task scheduler.
prefixPrefixRegistryPrefix (text) command registry.
usageUsageTrackerUsage tracker — records who used what.
embedsEmbedsPreset embed factory behind ctx.success/error/....
contextMenusContextMenuRegistryUser / message context-menu registry.
register(...items: Registerable[])thisRoute each item to the matching registry.
use(...plugins: SpearPlugin[])Promise<this>Run each plugin's setup.
load(dir: string, options?: LoadOptions)Promise<number>Import a directory and register its exports. Returns count.
start(token?: string)Promise<this>Log in (falls back to DISCORD_TOKEN).
deployCommands(options?: { guildId?: string })Promise<DeployResult>Push commands using the client's REST. Call after ready.
deployAllCommands(options?)Promise<DeployResult | { skipped: true; reason; body }>Deploy slash + context menus together; supports dryRun and strategy: "diff".
schedule(config: TaskConfig)ScheduledTaskDefine and register a scheduled task in one call.
enableGracefulShutdown(options?: GracefulShutdownOptions)() => voidTear down cleanly on SIGINT/SIGTERM; returns a disposer.

Inherits everything from discord.js Client (on, once, login, ws, rest, application, user, …).

type SpearClientOptions = Partial<ClientOptions> & SpearOptions

discord.js ClientOptions (with intents optional — it defaults to Intents.default) intersected with spearkit's own options (SpearOptions):

OptionTypeConfigures
loggerLogger | LoggerOptionsThe client.logger.
dotenvboolean | LoadEnvOptionsAuto-load .env on start() (default true).
cooldownCooldownInputDefault cooldown applied to every command.
prefixstring | readonly string[] | PrefixOptionsEnable prefix commands.
usageUsageOptionsUsage-tracking store and/or channel.
embedsEmbeds | EmbedsOptionsPreset embed factory.
guardsreadonly Guard[]Default guards run before every handler.
autoDeferAutoDeferInputDefault auto-defer for slash + context-menu handlers.

const Intents

Ready-made intent presets (arrays of GatewayIntentBits).

KeyContents
Intents.none[]
Intents.default[Guilds]
Intents.guilds[Guilds, GuildMembers]
Intents.messages[Guilds, GuildMessages, MessageContent]
Intents.allEvery intent (includes privileged).

type Registerable = SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand | ContextMenuCommand

The union accepted by SpearClient.register.


Commands

function command<O, R>(config): SlashCommand

Define a leaf slash command.

interface CommandConfig<O extends OptionMap, R> {
  name: string;
  description: string;
  options?: O;
  defaultMemberPermissions?: PermissionResolvable | null;
  nsfw?: boolean;
  guildOnly?: boolean;
  nameLocalizations?: LocalizationMap;
  descriptionLocalizations?: LocalizationMap;
  cooldown?: CooldownInput;
  guards?: readonly Guard[];
  autoDefer?: AutoDeferInput;
  run: (ctx: CommandContext<O>) => Awaitable<R>;
}

function commandGroup(config: CommandGroupConfig): SlashCommand

Define a command that routes to subcommands and/or subcommand groups.

interface CommandGroupConfig {
  name: string;
  description: string;
  subcommands?: Record<string, Subcommand>;
  groups?: Record<string, SubcommandGroup>;
  defaultMemberPermissions?: PermissionResolvable | null;
  nsfw?: boolean;
  guildOnly?: boolean;
  nameLocalizations?: LocalizationMap;
  descriptionLocalizations?: LocalizationMap;
  cooldown?: CooldownInput;
  guards?: readonly Guard[];
  autoDefer?: AutoDeferInput;
}

function subcommand<O, R>(config): Subcommand

interface SubcommandConfig<O extends OptionMap, R> {
  description: string;
  options?: O;
  nameLocalizations?: LocalizationMap;
  descriptionLocalizations?: LocalizationMap;
  run: (ctx: CommandContext<O>) => Awaitable<R>;
}

function subcommandGroup(config: SubcommandGroupConfig): SubcommandGroup

interface SubcommandGroupConfig {
  description: string;
  subcommands: Record<string, Subcommand>;
  nameLocalizations?: LocalizationMap;
  descriptionLocalizations?: LocalizationMap;
}

class SlashCommand

MemberTypeDescription
namestringTop-level command name.
hasAutocompletebooleanTrue if any option declares autocomplete.
toJSON()RESTPostAPIChatInputApplicationCommandsJSONBodyREST payload.
execute(interaction)Promise<void>Run for a chat-input interaction.
autocomplete(interaction)Promise<void>Run autocomplete for the focused option.
cooldownCooldownConfig | undefinedResolved cooldown, when set.
guardsreadonly Guard[] | undefinedGuards run before execute.
autoDeferAutoDeferConfig | undefinedResolved auto-defer config, when set.

class CommandContext<O> extends BaseContext<ChatInputCommandInteraction>

MemberTypeDescription
optionsResolvedOptions<O>Resolved, fully-typed option values.
commandNamestringInvoked command name.
subcommandstring | nullInvoked subcommand, if any.
showModal(modal)Promise<void>Present a modal.
awaitModal(modal, options?)Promise<ModalSubmitInteraction | null>Show a modal and await its submission (scoped to this user).

Plus all BaseContext members.

class CommandRegistry

MemberTypeDescription
add(...commands: SlashCommand[])thisRegister commands (override by name).
remove(name: string)booleanRemove a command.
get(name: string)SlashCommand | undefinedLook up a command.
all()SlashCommand[]All commands.
namesstring[]All command names.
sizenumberCount.
onError(handler: CommandErrorHandler)thisSet the error handler.
toJSON()RESTPostAPIApplicationCommandsJSONBody[]Serialise all commands.
handle(interaction)Promise<void>Dispatch a chat-input interaction.
handleAutocomplete(interaction)Promise<void>Dispatch an autocomplete interaction.
deploy(options: DeployOptions)Promise<DeployResult>Push commands to discord.
setLogger(logger: Logger)thisAttach a debug logger for dispatch tracing.
setCooldowns(manager: CooldownManager, default?: CooldownConfig)thisWire a shared cooldown manager and optional default.
setDefaultGuards(guards: readonly Guard[])thisGuards run before each command's own guards.
setUsageHook(hook: (event: UsageEvent) => void)thisCalled after each dispatch (success or error).
type CommandErrorHandler = (error: Error, interaction: ChatInputCommandInteraction) => Awaitable<void>;
interface DeployOptions { token?: string; applicationId: string; guildId?: string; rest?: REST; }
type DeployResult = RESTPutAPIApplicationCommandsResult | RESTPutAPIApplicationGuildCommandsResult;

Options

const option

Type-safe option builders. Each returns an OptionDef whose resolved value type is inferred (required → value, optional → value | undefined, choices → literal union).

BuilderResolved typeExtra config
option.string(config)stringchoices?, minLength?, maxLength?, autocomplete?
option.integer(config)numberchoices?, minValue?, maxValue?, autocomplete?
option.number(config)numberchoices?, minValue?, maxValue?, autocomplete?
option.boolean(config)boolean
option.user(config)User
option.channel(config)channel unionchannelTypes?
option.role(config)Role | APIRole
option.mentionable(config)user/role/member
option.attachment(config)Attachment

Common config (BaseConfig):

{
  description: string;
  required?: boolean;            // default false
  nameLocalizations?: LocalizationMap;
  descriptionLocalizations?: LocalizationMap;
}

choices items are OptionChoice<V>:

interface OptionChoice<V extends string | number = string | number> {
  name: string;
  value: V;
  nameLocalizations?: LocalizationMap;
}

autocomplete:

type AutocompleteHandler<V extends string | number> =
  (ctx: AutocompleteContext) => Awaitable<OptionChoice<V>[]>;

Option types

SymbolDescription
interface OptionDef<TValue, TRequired>A described option (phantom-typed for inference).
type AnyOptionDefOptionDef<OptionValue, boolean>.
type OptionMapRecord<string, AnyOptionDef>.
type ResolvedOption<O>The handler value for one option.
type ResolvedOptions<O>The handler's options object.
type OptionValueUnion of all possible resolved values.
type AllowedChannelTypeChannel types valid for a channel option.
function toAPIOption(name, def)Serialise one option to REST.
function readOption(resolver, name, def)Read a resolved value (null → undefined).
function optionsHaveAutocomplete(options)True if any option has autocomplete.

class AutocompleteContext

MemberTypeDescription
interactionAutocompleteInteractionRaw interaction.
client / user / guild / guildIdConvenience accessors.
commandNamestringCommand being completed.
focusedNamestringName of the focused option.
valuestringCurrent partial value typed by the user.
respond(choices: OptionChoice[])Promise<void>Send up to 25 suggestions.

Events

function event(name, run): EventDef / function event(config): EventDef

type EventHandler<E extends keyof ClientEvents> = (...args: ClientEvents[E]) => Awaitable<void>;
interface EventConfig<E extends keyof ClientEvents> { name: E; once?: boolean; run: EventHandler<E>; }
interface EventDef { name: keyof ClientEvents; once: boolean; attach(client: Client): void; detach(client: Client): void; }

Thrown errors and rejected promises are routed to the client's error event.

class EventRegistry

MemberTypeDescription
add(...defs: EventDef[])thisRegister listeners.
sizenumberCount.
attachAll(client: Client)voidAttach every listener.
detachAll(client: Client)voidDetach every listener.

Components

Builders

FunctionReturnsNotes
button(config)Button<P>Interactive button.
linkButton(config)ButtonBuilderURL button, no handler.
stringSelect(config)StringSelect<P>String select; takes options.
userSelect(config)UserSelect<P>User select.
roleSelect(config)RoleSelect<P>Role select.
channelSelect(config)ChannelSelect<P>Channel select; takes channelTypes?.
mentionableSelect(config)MentionableSelect<P>User + role select.
modal(config)Modal<P>Modal with fields.
textInput(config)TextInputDefA modal text-input field.
row(...components)ActionRowBuilder<C>Wrap components in a row.

Each registrable component (Button, StringSelect, …, Modal) extends its routing interface and adds build(...args: BuildArgs<P>), which returns the discord.js builder. build requires exactly the params declared in the id pattern.

interface ButtonConfig<P extends string, R> {
  id: P;                       // pattern: "name" or "name:{param}"
  label?: string;
  style?: ButtonStyleInput;    // "Primary" | "Secondary" | "Success" | "Danger" | ButtonStyle.*
  emoji?: ComponentEmojiResolvable;
  disabled?: boolean;
  guards?: readonly Guard[];
  run: (ctx: ButtonContext<Params<P>>) => Awaitable<R>;
}

interface LinkButtonConfig { url: string; label?: string; emoji?: ComponentEmojiResolvable; disabled?: boolean; }

interface StringSelectConfig<P extends string, R> {
  id: P;
  options: readonly SelectMenuComponentOptionData[];
  placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
  guards?: readonly Guard[];
  run: (ctx: StringSelectContext<Params<P>>) => Awaitable<R>;
}

interface EntitySelectConfig<P extends string> {
  id: P; placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
  guards?: readonly Guard[];
}
// user/role/mentionable selects take EntitySelectConfig & { run };
// channelSelect additionally takes { channelTypes?: readonly ChannelType[] }.

function textInput(config: {
  label: string;
  style?: TextInputStyleInput;     // "Short" | "Paragraph" | TextInputStyle
  placeholder?: string; required?: boolean; minLength?: number; maxLength?: number; value?: string;
}): TextInputDef;

interface ModalConfig<P extends string, F extends Record<string, TextInputDef>, R> {
  id: P;
  title: string;
  fields: F;
  guards?: readonly Guard[];
  run: (ctx: ModalContext<Params<P>, keyof F & string>) => Awaitable<R>;
}

Component contexts

ClassExtra members
MessageComponentContext<P, I>params, customId, message, update(input), deferUpdate(), showModal(modal), awaitModal(modal, options?) (+ BaseContext)
ButtonContext<P>
StringSelectContext<P>values: string[], value: string | undefined
UserSelectContext<P>values, users, members
RoleSelectContext<P>values, roles
ChannelSelectContext<P>values, channels
MentionableSelectContext<P>values, users, roles, members
ModalContext<P, F>params, fields: Record<F, string>, customId (+ BaseContext)

class ComponentRegistry

MemberTypeDescription
add(...defs: ComponentDef[])thisRegister components (override by namespace).
onError(handler: ComponentErrorHandler)thisSet the error handler.
sizenumberCount.
handle(interaction: Interaction)Promise<boolean>Route an interaction; true if matched.
setLogger(logger: Logger)thisDebug logger for dispatch tracing.
setUsageHook(hook: (event: UsageEvent) => void)thisCalled after each component run (success or error).
setDefaultGuards(guards: readonly Guard[])thisGuards run before each component's own guards.
type ComponentErrorHandler = (error: Error, interaction: RepliableInteraction) => Awaitable<void>;
type ComponentDef = ButtonRoute | StringSelectRoute | UserSelectRoute | RoleSelectRoute
  | ChannelSelectRoute | MentionableSelectRoute | ModalRoute;

Custom-id codec

SymbolDescription
type ParamNames<S>Union of {param} names in a pattern.
type Params<S>The params object a pattern resolves to.
type BuildArgs<S>build() args (none when no params).
const MAX_CUSTOM_ID_LENGTH100.
function compilePattern(pattern)CompiledPattern { pattern, namespace, paramNames }.
function buildCustomId(compiled, params)Encode a concrete id.
function parseCustomId(customId)ParsedCustomId { namespace, values }.
function paramsFromValues(paramNames, values)Map values onto names.

Contexts (shared)

abstract class BaseContext<I>

The base for every interaction context.

MemberTypeDescription
interactionIRaw discord.js interaction.
client / user / member / guild / guildId / channel / channelId / localeAccessors.
deferred / repliedbooleanInteraction state.
reply(input)Promise<InteractionResponse>Initial response.
replyEphemeral(input)Promise<InteractionResponse>Hidden reply.
defer({ ephemeral? })Promise<InteractionResponse>Acknowledge, respond later.
editReply(input)Promise<Message>Edit the response.
followUp(input)Promise<Message>Additional message.
send(input)Promise<void>State-aware reply/edit/followUp.
error(input, options?)Promise<void>State-aware preset error embed; defaults to ephemeral (pass { ephemeral: false } to override).
success / info / warn (input, options?)Promise<void>State-aware preset embeds (green / blue / yellow).
replyError(input, options?)Promise<InteractionResponse>Initial-reply error embed; defaults to ephemeral.
replySuccess / replyInfo / replyWarn (input, options?)Promise<InteractionResponse>Initial-reply preset embeds.
botPermissionsReadonly<PermissionsBitField>The bot's resolved permissions in the channel (zero-fetch).
botMissing(required)PermissionsString[]Permission names the bot is missing here.
userMissing(required)PermissionsString[]Permission names the invoking user is missing here.
awaitMessageFrom(userId?, options?)Promise<Message | null>Wait for the next message from a user in this channel.
type ReplyData = InteractionReplyOptions & { ephemeral?: boolean };
type ReplyInput = string | ReplyData;
function normalizeReply(input: ReplyInput): InteractionReplyOptions;
function asEphemeral(input: ReplyInput): ReplyData;

Plugins

interface SpearPlugin { name: string; setup(client: SpearClient): Awaitable<void>; }
function definePlugin(plugin: SpearPlugin): SpearPlugin;

Loading

interface LoadOptions { extensions?: readonly string[]; recursive?: boolean; } // defaults: [.js,.mjs,.cjs], true
function collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>;
function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>;

SpearClient.load(dir, options?) is the method form of loadInto.


Added in 0.2

New subsystems, each with a dedicated guide. The SpearClient options { logger?, dotenv?, cooldown?, prefix?, usage?, embeds?, guards? } configure them.

Logging — guide

class Logger { log(level, message, options?): void; debug/info/warn/error(message: string, options?: { error?: Error; data?: Record<string, LogValue> }): void; child(scope: string): Logger; setLevel(level: LogThreshold): this; enabled(level: LogLevel): boolean; addTransport(sink): this; setTransports(sinks): this; }
type LogLevel = "debug" | "info" | "warn" | "error";
type LogThreshold = LogLevel | "silent";
function consoleSink(entry: LogEntry): void;
function toError(value: unknown): Error;
// client.logger is a Logger; new SpearClient({ logger: { level: "debug" } })

Environment — guide

function parseEnv(content: string): Record<string, string>;
function loadEnv(options?: { path?: string; override?: boolean }): Record<string, string>;
const env: { string(k, fallback?); number(k, fallback?); boolean(k, fallback?); require(k): string };
// client auto-loads .env on start(); disable/configure via the dotenv option

Cooldowns — guide

interface CooldownConfig { duration: number; scope?: "user" | "guild" | "channel" | "global"; exempt?: { users?: string[]; roles?: string[] }; overrides?: { users?: Record<string, number>; roles?: Record<string, number> }; message?: string | ((remainingMs: number) => string); }
class CooldownManager { consume(bucket, input, actor, now?); peek(...); reset(...); clear(); }
type CooldownInput = number | CooldownConfig;       // a bare ms duration, or a full config
type CooldownScope = "user" | "guild" | "channel" | "global";
type CooldownResult = { allowed: true } | { allowed: false; remaining: number };
interface CooldownActor { userId; roleIds; guildId; channelId; }   // also: CooldownExemptions, CooldownOverrides
function normalizeCooldown(input: CooldownInput): CooldownConfig;
function effectiveDuration(config: CooldownConfig, actor: CooldownActor): number | null; // null = exempt
function formatCooldownMessage(config: CooldownConfig, remainingMs: number): string;
// command({ cooldown: number | CooldownConfig }); new SpearClient({ cooldown }); client.cooldowns

Scheduled tasks — guide

function task(config: { name: string; cron?: string; interval?: number; runOnStart?: boolean; run: (client: SpearClient) => Awaitable<void> }): ScheduledTask;
function cron(expression: string): CronExpression; // .next(from?: Date): Date
class TaskScheduler { add/remove/list/size/active/start/stop/setLogger; delay/followUp/reconcile (see "Scheduler — one-shot + reconcile") }
// client.register(task(...)); client.schedule(config); client.scheduler

Prefix commands — guide

function prefixCommand<TArgs, R>(config: { name: string; aliases?: readonly string[]; description?: string; cooldown?: CooldownInput; guards?: readonly Guard[]; args?: (a: PrefixArgsBuilder<{}>) => PrefixArgsBuilder<TArgs>; run: (ctx: PrefixContext<TArgs>) => Awaitable<R> }): PrefixCommand;
class PrefixContext<TArgs> { message; commandName; args: string[]; rest: string; options: TArgs; client; author; member; guild; guildId; channel; channelId; reply(content); send(content); }
// new SpearClient({ prefix: "!" | string[] | { prefix, mention?, ignoreBots?, caseInsensitive? } }); client.prefix
// reading others' content needs the privileged MessageContent intent (Intents.messages)

Usage tracking — guide

interface UsageEvent { type: UsageType; name: string; userId?; userTag?; guildId?; channelId?; detail?; outcome?: UsageOutcome; durationMs?: number; options?: Readonly<Record<string, UsageMetaValue>>; errorMessage?: string; timestamp: Date; }
type UsageType = "command" | "prefix" | "component" | "event";
type UsageOutcome = "success" | "error";
type UsageMetaValue = string | number | boolean | null;
function formatUsage(event: UsageEvent): string; // default channel-line renderer
interface UsageStore { record(event): Awaitable<void>; all(): Awaitable<readonly UsageEvent[]>; }
class MemoryUsageStore { record; all; size; byUser(id); clear; }
class JsonFileUsageStore { constructor(path: string); record; all; }
class UsageTracker { setStore(store); reportTo(channelId, format?); track(event); store; enabled; }
// new SpearClient({ usage: { store?, channel?, format? } }); client.usage

Added in 0.3

Driven by patterns repeated across long-running production bots: the role/ permission checks, .catch(() => null) fetches, embed factories, pagination /confirm flows, mention/duration parsing, locks, config loaders and pluggable log/usage transports a real Discord bot ends up writing.

Embeds — preset replies

class Embeds { constructor(options?: EmbedsOptions); error(input); success(input); info(input); warn(input); build(level, input); readonly colors: EmbedColors; readonly icons: EmbedIcons; }
const defaultEmbeds: Embeds;                 // shared default used when `client.embeds` is unset
const DEFAULT_EMBED_COLORS: EmbedColors;     // red / green / blue / yellow
const DEFAULT_EMBED_ICONS: EmbedIcons;       // ⛔ ✅ ℹ️ ⚠️
// SpearClient owns one as `client.embeds`; configure via the `embeds` option.
// BaseContext gains ctx.success/info/warn/error (state-aware send) + replySuccess/replyInfo/replyWarn/replyError.

Guards — declarative preconditions — guide

type Guard<TCtx extends GuardContext = GuardContext> = (ctx: TCtx) => Awaitable<GuardResult>;
interface GuardContext { client; user; member; guild; guildId; channelId; }
type GuardResult = boolean | { allowed: false; reason?: string };
type RunGuardsResult = { allowed: true } | { allowed: false; reason: string | undefined };
function runGuards<TCtx extends GuardContext>(ctx: TCtx, guards?: readonly Guard<TCtx>[]): Promise<RunGuardsResult>;
function denied(reason?: string): GuardResult;
function guildOnly(reason?: string): Guard;
function dmOnly(reason?: string): Guard;
function requireAnyRole(roleIds: readonly string[], reason?: string): Guard;
function requireAllRoles(roleIds: readonly string[], reason?: string): Guard;
function requireOwner(ownerIds: readonly string[], reason?: string): Guard;
function requireUserPermissions(permission: PermissionResolvable, reason?: string): Guard;
function requireBotPermissions(permission: PermissionResolvable, reason?: string): Guard;
function guard<TCtx>(predicate: Guard<TCtx>): Guard<TCtx>;
// every built-in guard takes an optional custom `reason`; each has a sensible default message.
// per-handler: command({ guards: [...] }), prefixCommand({ guards }), button({ guards }), userCommand({ guards }), ...
// client-wide: new SpearClient({ guards: [...] })

Context-menu commands — guide

interface ContextMenuMeta { defaultMemberPermissions?: PermissionResolvable | null; nsfw?: boolean; guildOnly?: boolean; nameLocalizations?: LocalizationMap; cooldown?: CooldownInput; guards?: readonly Guard[]; autoDefer?: AutoDeferInput; }
function userCommand<R>(config: ContextMenuMeta & { name: string; run: (ctx: UserContextMenuContext) => Awaitable<R> }): UserContextMenu;
function messageCommand<R>(config: ContextMenuMeta & { name: string; run: (ctx: MessageContextMenuContext) => Awaitable<R> }): MessageContextMenu;
// UserContextMenuContext adds ctx.targetUser, ctx.targetMember; MessageContextMenuContext adds ctx.targetMessage (+ BaseContext).
// ContextMenuCommand = UserContextMenu | MessageContextMenu; client.contextMenus is a ContextMenuRegistry.
// Deploy slash commands + menus together with client.deployAllCommands({ guildId }).

Prefix typed arguments

function prefixArgs(): PrefixArgsBuilder<{}>;
// builder methods — each requires a `name` and takes an optional options object:
//   .string(name, { required?, minLength?, maxLength?, default? })   -> string
//   .integer(name, { required?, minValue?, maxValue?, default? })    -> number
//   .number(name,  { required?, minValue?, maxValue?, default? })    -> number
//   .boolean(name, { required?, default? })                          -> boolean
//   .snowflake(name, { required?, default? })   -> string (accepts raw ids and <@u>/<#c>/<@&r> mentions)
//   .duration(name, { required?, default? })    -> number ("1h30m" parsed to ms)
//   .rest(name, { required?, default? })        -> string (remaining text)
// prefixCommand({ args: (a) => a.snowflake("target", { required: true }).duration("dur").rest("reason", { default: "No reason" }), run: (ctx) => ctx.options });

Pagination + Confirmation

function paginate<T>(interaction, items, { render, pageSize?, user?, timeoutMs?, controls?: "prev-next" | "first-prev-next-last", ephemeral?, namespace?, labels?: { first?; prev?; next?; last? } }): Promise<void>;
function buildPaginatorPage<T>(items, page, options): Promise<{ payload; pages }>;
function confirm(interaction, { body, title?, confirm?: { label?; style? }, cancel?: { label?; style? }, user?, timeoutMs?, ephemeral?, namespace? }): Promise<{ confirmed: boolean; reason: "confirm" | "cancel" | "timeout"; interaction? }>; // style: "Primary" | "Secondary" | "Success" | "Danger"

Primitives

class KeyedLock { constructor(options?: { ttl?: number; sweep?: number }); tryAcquire(key, ttl?); run(key, fn, { onBusy?, ttl? }); isHeld(key); forget(key); dispose(); readonly size: number; }
const safeFetch = { member, channel, message, user, guild, role, try }; // each returns T | null; also exported standalone as fetchMember/fetchChannel/fetchMessage/fetchUser/fetchGuild/fetchRole/safeTry
function withSafeTimeout<T>(p: Promise<T>, ms): Promise<T | null>;
function formatDuration(ms, opts?: { locale?: string | UnitLabels; largest?: number; units?: readonly DurationUnit[] }): string; // locale: "en"|"en-US"|"en-GB"|"tr"|"tr-TR" or a custom label set; unknown locales fall back to en
function parseDuration(input: string): number | null;
function discordTimestamp(date, style?: "t"|"T"|"d"|"D"|"f"|"F"|"R"): string;
function relativeTimestamp(date): string;
interface CacheStore { get; set; delete; has; increment; rateLimit; clear; }
class MemoryCache implements CacheStore { /* TTL, counter, fixed-window rate limit */ }
function createCache(): CacheStore; // default in-memory cache
function loadConfig<T>({ file, parser?, schema?, encoding? }): T;
function loadConfigAsync<T>(opts): Promise<T>;
function lookup<K, V>(table, resourceName?): (key: K) => V;
function lookupOptional<K, V>(table): (key: K) => V | undefined; // non-throwing variant of lookup

Logger transports

new Logger({ level, transports: [consoleSink, jsonlSink("./logs/bot.jsonl"), webhookSink({ url, minLevel: "error" })] });
function jsonlSink(path: string, { minLevel? }?): LogSink;
function webhookSink({ url, minLevel?, username? }): LogSink;
function consoleSink(entry: LogEntry): void; // default human-readable console transport
// Logger.addTransport(sink), setTransports([sinks])

Scheduler — one-shot + reconcile

client.scheduler.delay(name, ms, fn) -> { cancel(): boolean };
client.scheduler.followUp(name, [10_000, 30_000, 60_000], (i) => ...) -> { cancel(): boolean };
client.scheduler.reconcile("voice-sessions", async (client) => { /* once on ready */ });

Deploy diff + dry run

client.deployAllCommands({ guildId, dryRun: true });            // returns { skipped, body, reason: "dry-run" }
client.deployAllCommands({ guildId, strategy: "diff" });        // skips PUT when remote matches
client.deployAllCommands({ applicationId: "...", strategy: "diff" }); // explicit app id, no ready required

Added in 0.4

Reliability and moderation helpers distilled from production bots: never lose an interaction to the 3-second window, shut down cleanly, run permission/hierarchy preflights, persist per-guild settings, and await replies without hand-rolled collectors.

Auto-defer — guide

type AutoDeferInput = boolean | { ephemeral?: boolean; delayMs?: number };
interface AutoDeferConfig { ephemeral: boolean; delayMs: number; }
const DEFAULT_AUTO_DEFER_DELAY_MS = 2000;
function normalizeAutoDefer(input?: AutoDeferInput): AutoDeferConfig | undefined;
function armAutoDefer(interaction, config: AutoDeferConfig): () => void; // returns a cancel fn
type AutoDeferrableInteraction = ChatInputCommandInteraction | UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction;
// Enable per handler: command({ autoDefer: true }), userCommand({ autoDefer }), messageCommand({ autoDefer })
// Or globally: new SpearClient({ autoDefer: true }). With it on, respond via ctx.send / ctx.editReply.
// Arms a timer when the handler starts; defers if it hasn't responded by ~2s, preventing "Unknown interaction" (10062).

Graceful shutdown

interface GracefulShutdownOptions {
  signals?: readonly NodeJS.Signals[];   // default ["SIGINT", "SIGTERM"]
  timeoutMs?: number;                     // force-exit after this; default 10000
  exit?: boolean;                         // call process.exit when done; default true
  onShutdown?: (signal: NodeJS.Signals) => Awaitable<void>; // runs before client.destroy()
  logger?: { info?(msg): void; error?(msg, meta?): void };
}
interface Destroyable { destroy(): Awaitable<void>; } // a discord.js Client qualifies
interface ShutdownLogger { info?(message: string): void; error?(message: string, meta?: unknown): void; }
function gracefulShutdown(client: Destroyable, options?: GracefulShutdownOptions): () => void;
// SpearClient.enableGracefulShutdown(options?) wires it with client.logger and returns a disposer.

Permissions & moderation — guide

type PermissionHolder = GuildMember | Role;
function missingPermissions(channel: GuildBasedChannel, who: PermissionHolder, required: PermissionResolvable): PermissionsString[];
function botMissingPermissions(channel: GuildBasedChannel, required: PermissionResolvable): PermissionsString[];
function hasPermissions(channel: GuildBasedChannel, who: PermissionHolder, required: PermissionResolvable): boolean;
function compareRoles(a: GuildMember, b: GuildMember): number;     // by highest-role position
function canActOn(actor: GuildMember, target: GuildMember): boolean;
function formatPermissions(permissions: PermissionResolvable): string; // human, comma-separated

type ModerationCheckResult = { ok: true } | { ok: false; reason: string };
interface ModerationCheckOptions { moderator: GuildMember; target: GuildMember; me?: GuildMember | null; action?: string; }
function moderationCheck(options: ModerationCheckOptions): ModerationCheckResult; // self / owner / role-hierarchy preflight

Persistent storage

interface KeyValueStore {
  get<T>(key: string): Promise<T | undefined>;
  set<T>(key: string, value: T): Promise<void>;
  has(key: string): Promise<boolean>;
  delete(key: string): Promise<boolean>;
  keys(): Promise<string[]>;
  clear(): Promise<void>;
}
class MemoryStore implements KeyValueStore { /* deep-cloned in-memory */ }
class JsonStore implements KeyValueStore { constructor(path: string); /* atomic JSON file */ }
function namespaced(store: KeyValueStore, prefix: string): KeyValueStore;

interface SettingsManager<T> { readonly defaults: T; readonly store: KeyValueStore; get(id): Promise<T>; set(id, patch: Partial<T>): Promise<T>; reset(id): Promise<void>; }
interface CreateSettingsOptions<T> { store: KeyValueStore; defaults: T; namespace?: string; } // namespace default "settings"
function createSettings<T extends Record<string, unknown>>(options: CreateSettingsOptions<T>): SettingsManager<T>;

Collectors

interface AwaitMessageOptions { filter?: (m: Message) => boolean; time?: number; }   // time default 60000
function awaitMessage(channel: CollectableChannel, options?: AwaitMessageOptions): Promise<Message | null>;
interface AwaitComponentOptions { filter?; time?; componentType?: ComponentType; }   // time default 60000
function awaitComponent(message: Message, options?: AwaitComponentOptions): Promise<MessageComponentInteraction | null>;
interface AwaitModalOptions { time?: number; filter?: (i: ModalSubmitInteraction) => boolean; } // time default 120000
function showAndAwaitModal(interaction: ModalShowingInteraction, modal: ModalLike, options?: AwaitModalOptions): Promise<ModalSubmitInteraction | null>;
// Context sugar: ctx.awaitMessageFrom(userId?, options?) and ctx.awaitModal(modal, options?) (command + component contexts).

Discord errors — guide

const DiscordErrorCode = { UnknownChannel, UnknownGuild, UnknownMember, UnknownMessage, UnknownUser,
  UnknownInteraction, MissingAccess, CannotExecuteActionOnDMChannel, CannotSendMessagesToThisUser,
  MissingPermissions, InvalidFormBodyOrContentType, InteractionHasAlreadyBeenAcknowledged,
  MaximumNumberOfGuildsReached, MaximumNumberOfReactionsReached } as const; // named RESTJSONErrorCodes
type DiscordErrorCodeValue = (typeof DiscordErrorCode)[keyof typeof DiscordErrorCode];
function isDiscordError(error: unknown, code?: number | string | readonly (number | string)[]): error is DiscordAPIError;
function isHTTPError(error: unknown): error is HTTPError;
function isRateLimitError(error: unknown): boolean;        // HTTP 429
function explainDiscordError(error: unknown): string | null; // end-user-friendly sentence, or null
// The default command/component error reply uses explainDiscordError(...) when it can.

Message formatting

const MESSAGE_CHARACTER_LIMIT = 2000;
function truncate(text: string, max: number, suffix?: string): string;       // suffix default "…"
interface ChunkOptions { max?: number; }                                      // default MESSAGE_CHARACTER_LIMIT
function chunkMessage(text: string, options?: ChunkOptions): string[];        // splits on line/word boundaries

Dynamic prefixes

// PrefixOptions gains a per-message resolver (e.g. a per-guild prefix from a store):
interface PrefixOptions { /* …prefix, mention, ignoreBots, caseInsensitive… */
  dynamic?: (message: Message) => Awaitable<string | readonly string[] | null | undefined>;
}
// Dynamic prefixes are tried in addition to any static prefix. Keep the resolver fast (cache it).

On this page