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.
i18nI18n | undefinedRuntime translations behind ctx.t.
ownersreadonly string[]Bot-owner ids for requireBotOwner.
register(...items: Registerable[])thisRoute each item to the matching registry.
inviteUrl(options?)stringOAuth2 invite link (after ready).
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.
onHandlerErrorHandlerErrorHandlerOne error policy for commands, components, context menus, and prefix commands.
i18nI18n | I18nOptionsRuntime catalogs, locale fallback, and optional async resolver.
cooldownStoreCooldownBackend | KeyValueStorePersist cooldowns across restarts and shards.
ownersreadonly string[]Bot-owner ids for requireBotOwner().

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;
  install?: readonly ("guild" | "user")[];
  contexts?: readonly ("guild" | "botDm" | "privateChannel")[];
  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;
  install?: readonly ("guild" | "user")[];
  contexts?: readonly ("guild" | "botDm" | "privateChannel")[];
  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)AttachmentfileTypes

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 / guildId—Convenience 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 (submits string).
radioGroup(config)RadioGroupDefModal radio group; submits the picked option value.
checkboxGroup(config)CheckboxGroupDefModal checkbox group; submits picked values.
checkbox(config)CheckboxDefSingle modal checkbox; submits boolean.
fileUpload(config)FileUploadDefModal file upload; submits Attachment[], takes allowedFileTypes?.
stringSelectField(config)StringSelectFieldDefString select inside a modal; submits values.
userSelectField(config)UserSelectFieldDefUser select inside a modal; submits user ids.
roleSelectField(config)RoleSelectFieldDefRole select inside a modal; submits role ids.
channelSelectField(config)ChannelSelectFieldDefChannel select inside a modal; submits channel ids.
mentionableSelectField(config)MentionableSelectFieldDefMentionable select inside a modal; submits ids.
row(...components)ActionRowBuilder<C>Wrap components in a row.
textDisplay(content)TextDisplayBuilderComponents V2 text block.
separator(config?)SeparatorBuilderComponents V2 spacing/divider.
section(config)SectionBuilderText + one button/thumbnail accessory.
mediaGallery(items)MediaGalleryBuilderGrid of remote images/videos.
file(url, config?)FileBuilderAttached-file block (attachment://…).
thumbnail(config)ThumbnailBuilderInline thumbnail (section accessory).
container(config)ContainerBuilderAccent-coloured card holding V2 children.

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
  description?: string;
  placeholder?: string; required?: boolean; minLength?: number; maxLength?: number; value?: string;
}): TextInputDef;

// Every field definition carries a phantom value type, so `ctx.fields` is
// inferred per field: textInput -> string, radioGroup -> option-value union,
// checkboxGroup -> value[], checkbox -> boolean, fileUpload -> Attachment[],
// *SelectField -> string[].
interface ModalConfig<P extends string, F extends ModalFieldMap, R> {
  id: P;
  title: string;
  fields: F;
  guards?: readonly Guard[];
  run: (ctx: ModalContext<Params<P>, ResolvedModalFields<F>>) => 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, Fields>params, fields (typed per field definition), 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 / locale—Accessors.
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[]; // default [.js,.mjs,.cjs]
  typescript?: boolean;           // also load .ts/.mts (tsx, bun, or Node type stripping)
  recursive?: boolean;            // default 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

Runtime i18n — guide

type TranslationParam = string | number | boolean | Date | null | undefined;
type TranslationParams = Readonly<Record<string, TranslationParam>>;
type TranslationValue = string | ((params: TranslationParams) => string);
interface LocaleTarget { locale?; guildLocale?; guildId?; userId: string; }
interface I18nOptions {
  messages: Record<string, Record<string, TranslationValue>>;
  defaultLocale: string;
  fallbackLocale?: string;
  resolveLocale?: (target: LocaleTarget) => Awaitable<string | null | undefined>;
  missing?: (key: string, locale: string) => string;
}
class I18n<K extends string = string> {
  t(locale: string, key: K, params?: TranslationParams): string;
  localeFor(target: LocaleTarget): Promise<string>;
  translateFor(target: LocaleTarget, key: K, params?: TranslationParams): Promise<string>;
}
function createI18n<const M>(options: I18nOptions<M>): I18n<MessageKey<M>>;
// new SpearClient({ i18n }); await ctx.t("key", params)

Everyday helpers — guide

function choices(map: Record<string, string | number>): OptionChoice[];
function choices(...values: string[]): OptionChoice[];
function filterChoices(items, query, options?: { limit?: number }): OptionChoice[];
function inviteUrl(options: { clientId; permissions?; scopes?; guildId?; disableGuildSelect? }): string;
function parseSnowflake/parseUserId/parseRoleId/parseChannelId(input: string): string | null;
function parseCustomEmoji(input: string): { name; id; animated } | null;
function slashMention(name, commandId, subcommand?): string;
// ctx.sendLong / ctx.dm / ctx.withTyping / ctx.progress / ctx.suggest
// command({ enabled: false }); new SpearClient({ owners }); requireBotOwner()

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 { constructor(backend?: CooldownStoreInput); setBackend(backend); consume(bucket, input, actor, now?); peek(...); reset(...); clear(); }
class MemoryCooldownBackend implements CooldownBackend {}
interface CooldownBackend { hit(key, durationMs, now); peek(key, durationMs, now); delete(key); clear(); size?(); }
function keyValueCooldownBackend(store: KeyValueStore): CooldownBackend;
function redisCooldownBackend(client: RedisCommands, options?: { prefix?: string }): CooldownBackend;
type CooldownStoreInput = CooldownBackend | KeyValueStore;
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 }); new SpearClient({ cooldown, cooldownStore }); client.cooldowns

Scaling — guide

interface StartShardsOptions extends Omit<ShardingManagerOptions, "token"> {
  token?: string;
  spawn?: MultipleShardSpawnOptions;
  onShardCreate?: (shard: Shard) => void;
}
function startShards(file: string, options?: StartShardsOptions): Promise<ShardingManager>;
function shardIdForGuild(guildId: string, totalShards: number): number;
function shardListForWorker(totalShards: number, workerIndex: number, workerCount: number): number[];
function shardOptionsFromEnv(env?): Pick<ClientOptions, "shards" | "shardCount">;
function fetchShardStats(client: Client): Promise<ShardStatsReport>;

class WorkQueue {
  constructor(options?: { concurrency?: number; maxQueued?: number });
  readonly active: number; readonly queued: number; readonly pending: number;
  run<T>(job: () => Awaitable<T>): Promise<T>;
  onIdle(): Promise<void>;
  close(error?: Error): void;
}
class QueueFullError extends Error {}

function startHealthServer(options?: {
  port?: number;
  host?: string;
  client?: Client;
  checks?: Record<string, () => Awaitable<boolean>>;
}): Promise<HealthServerHandle>;
// GET /healthz · /readyz · /stats

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)

Hybrid commands — guide

function hybridCommand(config: { name; description; options?; args?; run; aliases?; ...slash meta }): HybridCommand;
interface HybridCommand { name: string; slash: SlashCommand; prefix: PrefixCommand; }
interface HybridContext { kind: "slash" | "prefix"; options; raw; reply; client; user; member; guild; guildId; channel; channelId; }
// client.register(hybridCommand(...)) registers both halves

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[]>; }
interface BatchUsageStore extends UsageStore { recordMany(events): Awaitable<void>; }
class BufferedUsageStore { constructor(downstream, { batchSize?, flushIntervalMs?, maxBuffered?, onDrop?, onError? }); record; all; flush; close; size; dropped; }
class MemoryUsageStore implements BatchUsageStore { record; recordMany; all; size; byUser(id); clear; }
class JsonFileUsageStore implements BatchUsageStore { constructor(path: string); record; recordMany; 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 requireBotOwner(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; install?: readonly ("guild" | "user")[]; contexts?: readonly ("guild" | "botDm" | "privateChannel")[]; 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"

Automatic help

type HelpSurface = "slash" | "prefix" | "userMenu" | "messageMenu";
interface HelpEntry { name: string; description: string; surface: HelpSurface; }
interface HelpCommandOptions {
  name?: string; description?: string; title?: string; pageSize?: number;
  includePrefix?: boolean; includeContextMenus?: boolean; ephemeral?: boolean;
  transform?: (entries: readonly HelpEntry[], ctx: CommandContext) => Awaitable<readonly HelpEntry[]>;
}
function helpCommand(options?: HelpCommandOptions): SlashCommand;
function buildHelpEntries(client, options?): HelpEntry[];

Polls — guide

type PollAnswerInput = string | { text: string; emoji?: EmojiIdentifierResolvable };
interface PollConfig {
  question: string;
  answers: readonly PollAnswerInput[];
  durationHours?: number; // 1–768; default 24
  multiselect?: boolean;  // default false
}
function poll(config: PollConfig): PollData;

Client-wide handler errors

type HandlerErrorEvent =
  | { source: "command" | "component" | "contextMenu"; name: string; error: Error; interaction: RepliableInteraction }
  | { source: "prefix"; name: string; error: Error; message: Message };
type HandlerErrorHandler =
  (event: HandlerErrorEvent) => Awaitable<string | false | void>;
// string = override safe user reply; false = suppress reply; void = safe default

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 */ }
class SqliteStore implements KeyValueStore { constructor(path: string | { path: string; table?: string }); close(): void; }
class RedisStore implements KeyValueStore { constructor(client: RedisCommands | { client: RedisCommands; prefix?: string }); }
interface RedisCommands { get(key): Promise<string | null>; set(key, value, options?: { NX?: boolean; PX?: number }); del(key | keys); keys(pattern); pttl?(key); }
function namespaced(store: KeyValueStore, prefix: string): KeyValueStore;

interface PayloadStore<T> { put(value: T): Promise<string>; get(token: string): Promise<T | undefined>; delete(token: string): Promise<boolean>; }
interface CreatePayloadStoreOptions { store: KeyValueStore; namespace?: string; ttlMs?: number; }
function createPayloadStore<T>(options: CreatePayloadStoreOptions): PayloadStore<T>;

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

Clientclass SpearClient extends Clienttype SpearClientOptions = Partial<ClientOptions> & SpearOptionsconst Intentstype Registerable = SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand | ContextMenuCommandCommandsfunction command<O, R>(config): SlashCommandfunction commandGroup(config: CommandGroupConfig): SlashCommandfunction subcommand<O, R>(config): Subcommandfunction subcommandGroup(config: SubcommandGroupConfig): SubcommandGroupclass SlashCommandclass CommandContext<O> extends BaseContext<ChatInputCommandInteraction>class CommandRegistryOptionsconst optionOption typesclass AutocompleteContextEventsfunction event(name, run): EventDef / function event(config): EventDefclass EventRegistryComponentsBuildersComponent contextsclass ComponentRegistryCustom-id codecContexts (shared)abstract class BaseContext<I>PluginsLoadingAdded in 0.2Logging — guideEnvironment — guideRuntime i18n — guideEveryday helpers — guideCooldowns — guideScaling — guideScheduled tasks — guidePrefix commands — guideHybrid commands — guideUsage tracking — guideAdded in 0.3Embeds — preset repliesGuards — declarative preconditions — guideContext-menu commands — guidePrefix typed argumentsPagination + ConfirmationAutomatic helpPolls — guideClient-wide handler errorsPrimitivesLogger transportsScheduler — one-shot + reconcileDeploy diff + dry runAdded in 0.4Auto-defer — guideGraceful shutdownPermissions & moderation — guidePersistent storageCollectorsDiscord errors — guideMessage formattingDynamic prefixes