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)| Member | Type | Description |
|---|---|---|
commands | CommandRegistry | Slash command registry + dispatcher. |
events | EventRegistry | Event listener registry. |
components | ComponentRegistry | Button/select/modal router. |
logger | Logger | Structured logger (client.logger.child(scope) for sub-scopes). |
cooldowns | CooldownManager | Shared cooldown manager (also used by prefix commands). |
scheduler | TaskScheduler | Cron / interval task scheduler. |
prefix | PrefixRegistry | Prefix (text) command registry. |
usage | UsageTracker | Usage tracker — records who used what. |
embeds | Embeds | Preset embed factory behind ctx.success/error/.... |
contextMenus | ContextMenuRegistry | User / message context-menu registry. |
register(...items: Registerable[]) | this | Route 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) | ScheduledTask | Define and register a scheduled task in one call. |
enableGracefulShutdown(options?: GracefulShutdownOptions) | () => void | Tear 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):
| Option | Type | Configures |
|---|---|---|
logger | Logger | LoggerOptions | The client.logger. |
dotenv | boolean | LoadEnvOptions | Auto-load .env on start() (default true). |
cooldown | CooldownInput | Default cooldown applied to every command. |
prefix | string | readonly string[] | PrefixOptions | Enable prefix commands. |
usage | UsageOptions | Usage-tracking store and/or channel. |
embeds | Embeds | EmbedsOptions | Preset embed factory. |
guards | readonly Guard[] | Default guards run before every handler. |
autoDefer | AutoDeferInput | Default auto-defer for slash + context-menu handlers. |
const Intents
Ready-made intent presets (arrays of GatewayIntentBits).
| Key | Contents |
|---|---|
Intents.none | [] |
Intents.default | [Guilds] |
Intents.guilds | [Guilds, GuildMembers] |
Intents.messages | [Guilds, GuildMessages, MessageContent] |
Intents.all | Every 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
| Member | Type | Description |
|---|---|---|
name | string | Top-level command name. |
hasAutocomplete | boolean | True if any option declares autocomplete. |
toJSON() | RESTPostAPIChatInputApplicationCommandsJSONBody | REST payload. |
execute(interaction) | Promise<void> | Run for a chat-input interaction. |
autocomplete(interaction) | Promise<void> | Run autocomplete for the focused option. |
cooldown | CooldownConfig | undefined | Resolved cooldown, when set. |
guards | readonly Guard[] | undefined | Guards run before execute. |
autoDefer | AutoDeferConfig | undefined | Resolved auto-defer config, when set. |
class CommandContext<O> extends BaseContext<ChatInputCommandInteraction>
| Member | Type | Description |
|---|---|---|
options | ResolvedOptions<O> | Resolved, fully-typed option values. |
commandName | string | Invoked command name. |
subcommand | string | null | Invoked 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
| Member | Type | Description |
|---|---|---|
add(...commands: SlashCommand[]) | this | Register commands (override by name). |
remove(name: string) | boolean | Remove a command. |
get(name: string) | SlashCommand | undefined | Look up a command. |
all() | SlashCommand[] | All commands. |
names | string[] | All command names. |
size | number | Count. |
onError(handler: CommandErrorHandler) | this | Set 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) | this | Attach a debug logger for dispatch tracing. |
setCooldowns(manager: CooldownManager, default?: CooldownConfig) | this | Wire a shared cooldown manager and optional default. |
setDefaultGuards(guards: readonly Guard[]) | this | Guards run before each command's own guards. |
setUsageHook(hook: (event: UsageEvent) => void) | this | Called 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).
| Builder | Resolved type | Extra config |
|---|---|---|
option.string(config) | string | choices?, minLength?, maxLength?, autocomplete? |
option.integer(config) | number | choices?, minValue?, maxValue?, autocomplete? |
option.number(config) | number | choices?, minValue?, maxValue?, autocomplete? |
option.boolean(config) | boolean | — |
option.user(config) | User | — |
option.channel(config) | channel union | channelTypes? |
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
| Symbol | Description |
|---|---|
interface OptionDef<TValue, TRequired> | A described option (phantom-typed for inference). |
type AnyOptionDef | OptionDef<OptionValue, boolean>. |
type OptionMap | Record<string, AnyOptionDef>. |
type ResolvedOption<O> | The handler value for one option. |
type ResolvedOptions<O> | The handler's options object. |
type OptionValue | Union of all possible resolved values. |
type AllowedChannelType | Channel 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
| Member | Type | Description |
|---|---|---|
interaction | AutocompleteInteraction | Raw interaction. |
client / user / guild / guildId | — | Convenience accessors. |
commandName | string | Command being completed. |
focusedName | string | Name of the focused option. |
value | string | Current 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
| Member | Type | Description |
|---|---|---|
add(...defs: EventDef[]) | this | Register listeners. |
size | number | Count. |
attachAll(client: Client) | void | Attach every listener. |
detachAll(client: Client) | void | Detach every listener. |
Components
Builders
| Function | Returns | Notes |
|---|---|---|
button(config) | Button<P> | Interactive button. |
linkButton(config) | ButtonBuilder | URL 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) | TextInputDef | A 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
| Class | Extra 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
| Member | Type | Description |
|---|---|---|
add(...defs: ComponentDef[]) | this | Register components (override by namespace). |
onError(handler: ComponentErrorHandler) | this | Set the error handler. |
size | number | Count. |
handle(interaction: Interaction) | Promise<boolean> | Route an interaction; true if matched. |
setLogger(logger: Logger) | this | Debug logger for dispatch tracing. |
setUsageHook(hook: (event: UsageEvent) => void) | this | Called after each component run (success or error). |
setDefaultGuards(guards: readonly Guard[]) | this | Guards 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
| Symbol | Description |
|---|---|
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_LENGTH | 100. |
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.
| Member | Type | Description |
|---|---|---|
interaction | I | Raw discord.js interaction. |
client / user / member / guild / guildId / channel / channelId / locale | — | Accessors. |
deferred / replied | boolean | Interaction 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. |
botPermissions | Readonly<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 optionCooldowns — 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.cooldownsScheduled 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.schedulerPrefix 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.usageAdded 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 lookupLogger 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 requiredAdded 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 preflightPersistent 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 boundariesDynamic 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).