Client
SpearClient is a discord.js Client with command, event and component registries — plus interaction routing — wired up for you. You construct it the same way you construct a…
import { SpearClient, Intents } from "spearkit";
const client = new SpearClient({ intents: Intents.default });Constructing a client
new SpearClient(options?) takes the same options as discord.js'
ClientOptions, except intents may be omitted: it defaults to
Intents.default (just the Guilds intent, enough for slash commands and
interactions).
import { SpearClient, Intents } from "spearkit";
// Explicit preset.
const a = new SpearClient({ intents: Intents.messages });
// Omitted — falls back to Intents.default.
const b = new SpearClient();The options type is exported as SpearClientOptions — Partial<ClientOptions> & SpearOptions. Every discord.js option (partials, presence, sweepers, …) is
available, plus spearkit's own: logger, dotenv, cooldown, prefix, usage,
embeds, guards and autoDefer (each covered in its own guide).
Intents presets
Intents is a set of ready-made arrays of GatewayIntentBits. Pass one as
intents, or compose your own array of GatewayIntentBits if you need
something in between.
| Preset | Contents |
|---|---|
Intents.none | [] |
Intents.default | [Guilds] |
Intents.guilds | [Guilds, GuildMembers] |
Intents.messages | [Guilds, GuildMessages, MessageContent] |
Intents.all | Every intent, including privileged ones. |
Intents.messages includes MessageContent, and Intents.guilds includes
GuildMembers — both are privileged intents. You must enable them in the
Discord developer portal for your application, otherwise the gateway will reject
the connection. Intents.all includes every privileged intent for the same
reason.
import { SpearClient, GatewayIntentBits } from "spearkit";
// A custom intent set, mixing a preset idea with explicit bits.
const client = new SpearClient({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
});Registries and subsystems
Every client owns a set of registries and subsystems, each populated by
register (or load) or configured by an option:
| Member | Type | Holds / does |
|---|---|---|
client.commands | CommandRegistry | Slash commands; dispatches chat-input and autocomplete interactions. |
client.events | EventRegistry | Event listeners; attached to the client automatically. |
client.components | ComponentRegistry | Buttons, selects and modals; routed by custom-id namespace. |
client.contextMenus | ContextMenuRegistry | User / message context-menu ("Apps") commands. |
client.prefix | PrefixRegistry | Prefix (text) commands, dispatched from messageCreate. |
client.scheduler | TaskScheduler | Cron / interval tasks; started on ready, stopped on destroy. |
client.cooldowns | CooldownManager | Shared rate-limit state across commands and prefix commands. |
client.usage | UsageTracker | Records who used what to a store and/or channel. |
client.logger | Logger | Structured, scoped logger used across spearkit. |
client.embeds | Embeds | Preset embed factory behind ctx.success/error/.... |
You rarely touch the registries directly — register routes items into the right
one — but they are public for inspection and advanced control (e.g.
client.commands.size, client.commands.toJSON()). Each subsystem has its own
guide: Cooldowns, Scheduled tasks,
Prefix commands, Context menus,
Logging, Usage tracking, Guards.
Registering handlers
client.register(...items) accepts commands, events, components, context-menu
commands, prefix commands and scheduled tasks in a single call and routes each to
its registry by kind. The accepted union is exported as Registerable
(SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand | ContextMenuCommand). It returns the client for chaining.
import { SpearClient, command, event, button, option } from "spearkit";
const client = new SpearClient();
const greet = command({
name: "greet",
description: "Greet someone",
options: { who: option.user({ description: "Who", required: true }) },
run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
});
const ready = event("clientReady", (c) => {
console.log(`Logged in as ${c.user.tag}`); // c: Client<true>
});
const ping = button({
id: "ping:{n}",
label: "Ping",
run: (ctx) => ctx.reply(`pong #${ctx.params.n}`), // n: string
});
// Commands, events and components in one call.
client.register(greet, ready, ping);Plugins
client.use(...plugins) installs one or more plugins, awaiting each plugin's
setup. It is async and returns the client.
import { SpearClient } from "spearkit";
import { statsPlugin } from "./plugins/stats.js";
const client = new SpearClient();
await client.use(statsPlugin);See Plugins for authoring SpearPlugins.
File-based loading
client.load(dir, options?) recursively imports a directory and registers every
spearkit-registrable export it finds — commands, events, components, scheduled
tasks and prefix commands. It returns the number of items registered.
import { SpearClient } from "spearkit";
const client = new SpearClient();
const count = await client.load("./src/commands");
console.log(`Loaded ${count} handlers`);See File-based loading for the layout and LoadOptions.
Starting and deploying
client.start(token?) logs in. If you omit the token it falls back to the
DISCORD_TOKEN environment variable, and throws if neither is present.
import { SpearClient } from "spearkit";
const client = new SpearClient();
// Pass a token explicitly…
await client.start("your-token");
// …or set DISCORD_TOKEN and call start() with no argument.
await client.start();client.deployCommands({ guildId }) pushes the registered slash commands to
Discord using the client's own authenticated REST connection — there is no
separate token or application id to supply. Because it reads the application id
from the logged-in client, it must run after the client is ready. Pass a
guildId to deploy instantly to a single guild (ideal for development); omit it
to deploy globally.
import { SpearClient, Intents } from "spearkit";
const client = new SpearClient({ intents: Intents.default });
// …register commands…
await client.start(); // uses DISCORD_TOKEN
// Deploy once the client is ready.
client.once("clientReady", async () => {
await client.deployCommands({ guildId: process.env.GUILD_ID });
});Reliability: auto-defer and graceful shutdown
A slow handler that doesn't respond within Discord's 3-second window dies with
Unknown interaction (10062). Set autoDefer to have spearkit deferReply()
automatically just before that window closes — per handler (command({ autoDefer: true }), userCommand/messageCommand) or for every slash + context-menu
handler at once:
const client = new SpearClient({ autoDefer: true });
// or { ephemeral: true, delayMs: 1500 } for a hidden defer / earlier fire.With auto-defer on, respond via ctx.send(...) or ctx.editReply(...) — the
initial reply slot may already be taken by the safety defer.
client.enableGracefulShutdown(options?) closes the bot cleanly on SIGINT /
SIGTERM: it runs an optional onShutdown hook, calls destroy() (stopping the
scheduler and gateway), and exits, with a hard timeout so a wedged shutdown can't
hang. It returns a disposer that removes the signal handlers.
client.enableGracefulShutdown({ onShutdown: () => db.close() });Everything discord.js still works
SpearClient extends discord.js Client, so the full client surface is
available unchanged. spearkit adds registries on top — it never hides what is
underneath:
import { SpearClient } from "spearkit";
const client = new SpearClient();
client.on("guildCreate", (guild) => console.log(`Joined ${guild.name}`));
client.ws.on("VOICE_SERVER_UPDATE", () => {});
await client.start();
console.log(client.application?.id); // application
console.log(client.user?.tag); // user
console.log(client.rest); // REST manager (used by deployCommands)
await client.destroy(); // graceful shutdownSee also
- Commands — defining slash commands you register here.
- Plugins — bundling features for
client.use. - File-based loading — populating the client from a directory.
Auto-defer
The single most common discord.js error is DiscordAPIError[10062]: Unknown interaction. An interaction token is valid for only 3 seconds before your first response; any handler…
Cooldowns
Rate-limit commands per user, per guild, per channel, or globally — with per-role and per-user exemptions and overrides. Cooldowns are enforced automatically by command dispatch…