spearkit 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms-full.txt ADDED
@@ -0,0 +1,3367 @@
1
+ # spearkit v0.3.1 — full documentation for LLMs
2
+
3
+ > discord.js++ — a developer-experience-first Discord library. Drop-in compatible with discord.js, with ergonomic events, slash commands, and interactive components.
4
+
5
+ This file concatenates every spearkit guide and the complete API reference. It is generated from docs/*.md (the single source of truth). Install: `npm install spearkit discord.js`. Import every symbol — spearkit additions and the entire re-exported discord.js surface — from `spearkit`.
6
+
7
+ ---
8
+
9
+ # spearkit documentation
10
+
11
+ **discord.js++** — a developer-experience-first layer over discord.js. spearkit
12
+ re-exports all of discord.js (so it's a drop-in replacement) and adds an
13
+ ergonomic, fully type-safe API for events, slash commands and interactive
14
+ components.
15
+
16
+ ## Contents
17
+
18
+ 1. [Getting started](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/getting-started.md) — install, first bot, project layout.
19
+ 2. [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — `SpearClient`, intents, `register`, `start`, deployment.
20
+ 3. [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — slash commands, subcommands, permissions, deployment.
21
+ 4. [Options](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/options.md) — typed option builders, choices, autocomplete.
22
+ 5. [Components](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/components.md) — buttons, selects, modals, custom-id routing.
23
+ 6. [Events](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/events.md) — the `event()` helper and the event registry.
24
+ 7. [Contexts](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md) — reply helpers shared by every handler.
25
+ 8. [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md) — per-user/role/guild rate limiting.
26
+ 9. [Scheduled tasks](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/scheduler.md) — cron and interval jobs.
27
+ 10. [Prefix commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/prefix.md) — classic `!text` commands.
28
+ 11. [Logging](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/logging.md) — structured, leveled, scoped logging.
29
+ 12. [Usage tracking](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/usage.md) — record who used what (store + Discord channel).
30
+ 13. [Environment & dotenv](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/env.md) — load `.env` and read typed env vars.
31
+ 14. [Plugins](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/plugins.md) — bundling features into reusable units.
32
+ 15. [File-based loading](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/loading.md) — one file per command/event/component.
33
+ 16. [Migrating from discord.js](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/migration.md) — the drop-in path.
34
+ 17. [API reference](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/api-reference.md) — every exported symbol.
35
+
36
+ ## Why spearkit
37
+
38
+ - **Drop-in.** `import { Client, EmbedBuilder } from "spearkit"` — every discord.js
39
+ export is available, so you can migrate one file at a time.
40
+ - **Fully type-safe.** No `any` or `unknown` leaks into your handlers. Option
41
+ values, custom-id params and modal fields are all inferred from your
42
+ definitions.
43
+ - **Co-located.** A command's options and handler, a button's appearance and
44
+ click logic, a modal's fields and submit logic — each lives in one place.
45
+ - **No boilerplate.** No `interactionCreate` switch statements; spearkit routes
46
+ commands, autocomplete, buttons, selects and modals for you.
47
+
48
+ ## Thirty-second tour
49
+
50
+ ```ts
51
+ import { SpearClient, Intents, command, option, button, row, event } from "spearkit";
52
+
53
+ const client = new SpearClient({ intents: Intents.default });
54
+
55
+ const greet = command({
56
+ name: "greet",
57
+ description: "Greet someone",
58
+ options: { who: option.user({ description: "Who", required: true }) },
59
+ run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
60
+ });
61
+
62
+ const ping = button({
63
+ id: "ping:{n}",
64
+ label: "Ping",
65
+ run: (ctx) => ctx.update(`pong #${ctx.params.n}`), // n: string
66
+ });
67
+
68
+ client.register(greet, ping, event("clientReady", (c) => console.log(c.user.tag)));
69
+ await client.start(process.env.DISCORD_TOKEN);
70
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
71
+ ```
72
+
73
+ ---
74
+
75
+ # Getting started
76
+
77
+ spearkit is **discord.js++**: it re-exports the entire discord.js surface and adds a
78
+ fully type-safe layer for events, slash commands and interactive components. This
79
+ page takes you from an empty folder to a running bot that responds to a slash
80
+ command.
81
+
82
+ ## Install
83
+
84
+ spearkit sits alongside discord.js, so install both:
85
+
86
+ ```bash
87
+ npm install spearkit discord.js
88
+ ```
89
+
90
+ Everything in your code imports from `"spearkit"` — including the plain discord.js
91
+ symbols, which spearkit re-exports unchanged.
92
+
93
+ ## Credentials you need
94
+
95
+ Create an application in the [Discord Developer Portal](https://discord.com/developers/applications)
96
+ and collect three values:
97
+
98
+ | Value | Where to find it | Used for |
99
+ | ----- | ---------------- | -------- |
100
+ | Bot token | Application → **Bot** → *Reset Token* | `client.start(token)` |
101
+ | Application id | Application → **General Information** → *Application ID* | command deployment (spearkit reads it from the client once ready) |
102
+ | Test guild id | Right-click your server in Discord (with Developer Mode on) → *Copy Server ID* | guild-scoped deploy |
103
+
104
+ Keep the token secret. The examples below read these from the environment
105
+ (`DISCORD_TOKEN`, `GUILD_ID`).
106
+
107
+ ## Your first bot
108
+
109
+ ```ts
110
+ import { SpearClient, Intents, command, option, event } from "spearkit";
111
+
112
+ const client = new SpearClient({ intents: Intents.default });
113
+
114
+ const greet = command({
115
+ name: "greet",
116
+ description: "Greet someone",
117
+ options: {
118
+ who: option.user({ description: "Who to greet", required: true }),
119
+ },
120
+ run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // ctx.options.who: User
121
+ });
122
+
123
+ const ready = event("clientReady", (c) => console.log(`Online as ${c.user.tag}`));
124
+
125
+ client.register(greet, ready);
126
+
127
+ await client.start(process.env.DISCORD_TOKEN);
128
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
129
+ ```
130
+
131
+ What each step does:
132
+
133
+ 1. **`new SpearClient({ intents })`** — a discord.js `Client` with command, event
134
+ and component routing wired up. `Intents.default` is `[Guilds]`, enough for
135
+ slash commands and interactions.
136
+ 2. **`command({ ... })`** — defines a leaf slash command. Required options resolve
137
+ to their value type (`who` is a `User`); optional options would resolve to
138
+ `value | undefined`.
139
+ 3. **`client.register(...)`** — routes each item to the matching registry
140
+ (commands, events, components) by its kind.
141
+ 4. **`client.start(token)`** — logs in. With no argument it falls back to the
142
+ `DISCORD_TOKEN` environment variable.
143
+ 5. **`client.deployCommands({ guildId })`** — pushes your command definitions to
144
+ Discord over the client's own authenticated REST connection. Must run after the
145
+ client is ready (i.e. after `start`).
146
+
147
+ ### Guild vs global deploy
148
+
149
+ `deployCommands` takes an optional `guildId`:
150
+
151
+ - **Guild deploy** (`{ guildId }`) registers commands in a single server. Changes
152
+ appear **instantly** — ideal while developing.
153
+ - **Global deploy** (omit `guildId`) registers commands across every server the
154
+ bot is in. Propagation can take up to an hour.
155
+
156
+ ```ts
157
+ await client.deployCommands({ guildId: process.env.GUILD_ID }); // instant, one guild
158
+ await client.deployCommands(); // global, slow to propagate
159
+ ```
160
+
161
+ You only need to deploy when your command *definitions* change (names,
162
+ descriptions, options) — not on every restart.
163
+
164
+ ## Suggested project layout
165
+
166
+ As a bot grows, give each command, event and component its own file:
167
+
168
+ ```
169
+ my-bot/
170
+ src/
171
+ index.ts # construct the client, register/load, start, deploy
172
+ commands/
173
+ greet.ts
174
+ ping.ts
175
+ events/
176
+ ready.ts
177
+ components/
178
+ vote.ts
179
+ package.json
180
+ tsconfig.json
181
+ ```
182
+
183
+ A module exports a command, event or component as a default or named export:
184
+
185
+ ```ts
186
+ // src/commands/ping.ts
187
+ import { command } from "spearkit";
188
+
189
+ export default command({
190
+ name: "ping",
191
+ description: "Check that the bot is alive",
192
+ run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
193
+ });
194
+ ```
195
+
196
+ You can wire the pieces up explicitly with `register`, or let spearkit discover them
197
+ with `client.load` (see [File-based loading](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/loading.md)).
198
+
199
+ ## Running it
200
+
201
+ **With tsx** (run TypeScript directly, great for development):
202
+
203
+ ```bash
204
+ npx tsx src/index.ts
205
+ ```
206
+
207
+ **Compiled JavaScript** (for production):
208
+
209
+ ```bash
210
+ npx tsc # emit JS into dist/ per your tsconfig
211
+ node dist/index.js
212
+ ```
213
+
214
+ Note that `client.load` imports **compiled JavaScript**, so if you use file-based
215
+ loading you must build before running the compiled output. Explicit `register`
216
+ calls work the same under `tsx` or `node`.
217
+
218
+ ## See also
219
+
220
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — `SpearClient`, intents, `register`, `start`, deployment.
221
+ - [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — slash commands, subcommands, options, deployment.
222
+
223
+ ---
224
+
225
+ # Migrating from discord.js
226
+
227
+ spearkit re-exports the entire discord.js surface, so adopting it is not a rewrite —
228
+ it is a one-line import change followed by *optional*, incremental cleanup. You can
229
+ move to spearkit today and start using its ergonomic helpers whenever you like.
230
+
231
+ ## The drop-in story
232
+
233
+ Change `from "discord.js"` to `from "spearkit"`. Nothing else has to change: every
234
+ discord.js export is available under the same name with the same types.
235
+
236
+ ```ts
237
+ // before
238
+ import { Client, EmbedBuilder, GatewayIntentBits } from "discord.js";
239
+
240
+ // after — identical behaviour
241
+ import { Client, EmbedBuilder, GatewayIntentBits } from "spearkit";
242
+ ```
243
+
244
+ The full classic surface is there — builders, enums, the REST client, route
245
+ helpers, the `Events` map, and so on:
246
+
247
+ ```ts
248
+ import {
249
+ ActionRowBuilder,
250
+ ButtonBuilder,
251
+ ButtonStyle,
252
+ Client,
253
+ EmbedBuilder,
254
+ Events,
255
+ GatewayIntentBits,
256
+ REST,
257
+ Routes,
258
+ SlashCommandBuilder,
259
+ } from "spearkit";
260
+
261
+ const client = new Client({ intents: [GatewayIntentBits.Guilds] });
262
+
263
+ const pingCommand = new SlashCommandBuilder()
264
+ .setName("ping")
265
+ .setDescription("Replies with an embed and a button");
266
+
267
+ client.once(Events.ClientReady, (c) => {
268
+ console.log(`Ready as ${c.user.tag}`);
269
+ });
270
+
271
+ client.on(Events.InteractionCreate, async (interaction) => {
272
+ if (!interaction.isChatInputCommand()) return;
273
+ if (interaction.commandName !== "ping") return;
274
+
275
+ const embed = new EmbedBuilder().setTitle("Pong!").setDescription(`Latency: ${client.ws.ping}ms`);
276
+ const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
277
+ new ButtonBuilder().setCustomId("again").setLabel("Again").setStyle(ButtonStyle.Primary),
278
+ );
279
+ await interaction.reply({ embeds: [embed], components: [buttons] });
280
+ });
281
+
282
+ async function deploy(token: string, appId: string): Promise<void> {
283
+ const rest = new REST().setToken(token);
284
+ await rest.put(Routes.applicationCommands(appId), { body: [pingCommand.toJSON()] });
285
+ }
286
+ ```
287
+
288
+ This file is 100% classic discord.js — only the import source changed. It keeps
289
+ working exactly as before.
290
+
291
+ ## Incremental adoption
292
+
293
+ Once your imports point at spearkit, you can convert pieces one at a time. There is no
294
+ big-bang migration; old and new styles coexist.
295
+
296
+ 1. **Swap the client.** Replace `new Client(...)` with `new SpearClient(...)`. It
297
+ *is* a discord.js `Client` (it extends it), so your existing `client.on`,
298
+ `client.once`, `client.ws`, `client.rest` code is unchanged — but now it also
299
+ routes interactions to spearkit's registries.
300
+
301
+ ```ts
302
+ import { SpearClient, Intents } from "spearkit";
303
+
304
+ const client = new SpearClient({ intents: Intents.default });
305
+ ```
306
+
307
+ 2. **Move commands to `command()`.** Replace a hand-written `SlashCommandBuilder`
308
+ plus its branch of the `interactionCreate` switch with a single co-located
309
+ definition. Option values become fully typed.
310
+ 3. **Move events to `event()`.** Replace `client.on(Events.X, ...)` listeners with
311
+ `event("x", ...)` definitions and register them.
312
+ 4. **Move components to spearkit builders.** Replace manual `ButtonBuilder` +
313
+ custom-id parsing with `button()`, `stringSelect()`, `modal()`, etc. — spearkit
314
+ routes them by custom-id namespace and decodes `{param}`s for you.
315
+
316
+ Convert at whatever pace suits you; un-migrated handlers keep running through your
317
+ existing `interactionCreate` listener.
318
+
319
+ ## Before and after
320
+
321
+ The classic approach hand-routes every interaction through one big switch and
322
+ parses custom ids by hand:
323
+
324
+ ```ts
325
+ // discord.js: one listener routes everything by hand
326
+ import { Client, Events, GatewayIntentBits } from "discord.js";
327
+
328
+ const client = new Client({ intents: [GatewayIntentBits.Guilds] });
329
+
330
+ client.on(Events.InteractionCreate, async (interaction) => {
331
+ if (interaction.isChatInputCommand()) {
332
+ if (interaction.commandName === "greet") {
333
+ const who = interaction.options.getUser("who", true);
334
+ await interaction.reply(`Hello ${who}!`);
335
+ }
336
+ } else if (interaction.isButton()) {
337
+ const [name, choice] = interaction.customId.split(":"); // manual parsing
338
+ if (name === "vote") {
339
+ await interaction.update({ content: `You chose ${choice}` });
340
+ }
341
+ }
342
+ });
343
+ ```
344
+
345
+ spearkit co-locates each command and component with its handler, and routes
346
+ interactions for you — no switch, no manual id parsing:
347
+
348
+ ```ts
349
+ // spearkit: each handler owns its definition; routing is automatic
350
+ import { SpearClient, Intents, command, option, button, row } from "spearkit";
351
+
352
+ const client = new SpearClient({ intents: Intents.default });
353
+
354
+ const greet = command({
355
+ name: "greet",
356
+ description: "Greet someone",
357
+ options: { who: option.user({ description: "Who to greet", required: true }) },
358
+ run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
359
+ });
360
+
361
+ const vote = button({
362
+ id: "vote:{choice}", // {choice} is a typed param
363
+ label: "Yes",
364
+ style: "Success",
365
+ run: (ctx) => ctx.update(`You chose ${ctx.params.choice}`), // choice: string
366
+ });
367
+
368
+ client.register(greet, vote);
369
+ await client.start(process.env.DISCORD_TOKEN);
370
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
371
+
372
+ // build() requires exactly the params the id pattern declares:
373
+ await channel.send({ content: "Vote:", components: [row(vote.build({ choice: "yes" }))] });
374
+ ```
375
+
376
+ The option value (`who`) and the custom-id param (`choice`) are inferred from the
377
+ definitions — no casts, no `getUser`/`split` boilerplate, and no `interactionCreate`
378
+ switch to maintain.
379
+
380
+ ## See also
381
+
382
+ - [Getting started](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/getting-started.md) — install spearkit and build a first bot.
383
+ - [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — define slash commands with typed options.
384
+ - [Components](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/components.md) — buttons, selects, modals and custom-id routing.
385
+
386
+ ---
387
+
388
+ # Client
389
+
390
+ `SpearClient` is a discord.js `Client` with command, event and component
391
+ registries — plus interaction routing — wired up for you. You construct it the
392
+ same way you construct a discord.js client, register your handlers, log in, and
393
+ (optionally) push your slash commands to Discord.
394
+
395
+ ```ts
396
+ import { SpearClient, Intents } from "spearkit";
397
+
398
+ const client = new SpearClient({ intents: Intents.default });
399
+ ```
400
+
401
+ ## Constructing a client
402
+
403
+ `new SpearClient(options?)` takes the same options as discord.js'
404
+ `ClientOptions`, except `intents` may be omitted: it defaults to
405
+ `Intents.default` (just the `Guilds` intent, enough for slash commands and
406
+ interactions).
407
+
408
+ ```ts
409
+ import { SpearClient, Intents } from "spearkit";
410
+
411
+ // Explicit preset.
412
+ const a = new SpearClient({ intents: Intents.messages });
413
+
414
+ // Omitted — falls back to Intents.default.
415
+ const b = new SpearClient();
416
+ ```
417
+
418
+ The options type is exported as `SpearClientOptions` (`Partial<ClientOptions>`),
419
+ so every other discord.js option (`partials`, `presence`, `sweepers`, …) is
420
+ available.
421
+
422
+ ### Intents presets
423
+
424
+ `Intents` is a set of ready-made arrays of `GatewayIntentBits`. Pass one as
425
+ `intents`, or compose your own array of `GatewayIntentBits` if you need
426
+ something in between.
427
+
428
+ | Preset | Contents |
429
+ | ------ | -------- |
430
+ | `Intents.none` | `[]` |
431
+ | `Intents.default` | `[Guilds]` |
432
+ | `Intents.guilds` | `[Guilds, GuildMembers]` |
433
+ | `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
434
+ | `Intents.all` | Every intent, including privileged ones. |
435
+
436
+ `Intents.messages` includes `MessageContent`, and `Intents.guilds` includes
437
+ `GuildMembers` — both are **privileged intents**. You must enable them in the
438
+ Discord developer portal for your application, otherwise the gateway will reject
439
+ the connection. `Intents.all` includes every privileged intent for the same
440
+ reason.
441
+
442
+ ```ts
443
+ import { SpearClient, GatewayIntentBits } from "spearkit";
444
+
445
+ // A custom intent set, mixing a preset idea with explicit bits.
446
+ const client = new SpearClient({
447
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
448
+ });
449
+ ```
450
+
451
+ ## The three registries
452
+
453
+ Every client owns three registries, each populated by `register` (or `load`):
454
+
455
+ | Registry | Property | Holds |
456
+ | -------- | -------- | ----- |
457
+ | `CommandRegistry` | `client.commands` | Slash commands; dispatches chat-input and autocomplete interactions. |
458
+ | `EventRegistry` | `client.events` | Event listeners; attached to the client automatically. |
459
+ | `ComponentRegistry` | `client.components` | Buttons, selects and modals; routes component interactions by custom id. |
460
+
461
+ You rarely touch them directly — `register` routes items into the right one —
462
+ but they are public if you need to inspect or manipulate them (e.g.
463
+ `client.commands.size`, `client.commands.toJSON()`).
464
+
465
+ ## Registering handlers
466
+
467
+ `client.register(...items)` accepts commands, events and components in a single
468
+ call and routes each to its registry by kind. The accepted union is exported as
469
+ `Registerable` (`SlashCommand | EventDef | ComponentDef`). It returns the client
470
+ for chaining.
471
+
472
+ ```ts
473
+ import { SpearClient, command, event, button, option } from "spearkit";
474
+
475
+ const client = new SpearClient();
476
+
477
+ const greet = command({
478
+ name: "greet",
479
+ description: "Greet someone",
480
+ options: { who: option.user({ description: "Who", required: true }) },
481
+ run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
482
+ });
483
+
484
+ const ready = event("clientReady", (c) => {
485
+ console.log(`Logged in as ${c.user.tag}`); // c: Client<true>
486
+ });
487
+
488
+ const ping = button({
489
+ id: "ping:{n}",
490
+ label: "Ping",
491
+ run: (ctx) => ctx.reply(`pong #${ctx.params.n}`), // n: string
492
+ });
493
+
494
+ // Commands, events and components in one call.
495
+ client.register(greet, ready, ping);
496
+ ```
497
+
498
+ ## Plugins
499
+
500
+ `client.use(...plugins)` installs one or more plugins, awaiting each plugin's
501
+ `setup`. It is async and returns the client.
502
+
503
+ ```ts
504
+ import { SpearClient } from "spearkit";
505
+ import { statsPlugin } from "./plugins/stats.js";
506
+
507
+ const client = new SpearClient();
508
+ await client.use(statsPlugin);
509
+ ```
510
+
511
+ See [Plugins](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/plugins.md) for authoring `SpearPlugin`s.
512
+
513
+ ## File-based loading
514
+
515
+ `client.load(dir, options?)` recursively imports a directory and registers every
516
+ command, event and component it exports. It returns the number of items
517
+ registered.
518
+
519
+ ```ts
520
+ import { SpearClient } from "spearkit";
521
+
522
+ const client = new SpearClient();
523
+ const count = await client.load("./src/commands");
524
+ console.log(`Loaded ${count} handlers`);
525
+ ```
526
+
527
+ See [File-based loading](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/loading.md) for the layout and `LoadOptions`.
528
+
529
+ ## Starting and deploying
530
+
531
+ `client.start(token?)` logs in. If you omit the token it falls back to the
532
+ `DISCORD_TOKEN` environment variable, and throws if neither is present.
533
+
534
+ ```ts
535
+ import { SpearClient } from "spearkit";
536
+
537
+ const client = new SpearClient();
538
+
539
+ // Pass a token explicitly…
540
+ await client.start("your-token");
541
+
542
+ // …or set DISCORD_TOKEN and call start() with no argument.
543
+ await client.start();
544
+ ```
545
+
546
+ `client.deployCommands({ guildId })` pushes the registered slash commands to
547
+ Discord using the client's own authenticated REST connection — there is no
548
+ separate token or application id to supply. Because it reads the application id
549
+ from the logged-in client, it **must run after the client is ready**. Pass a
550
+ `guildId` to deploy instantly to a single guild (ideal for development); omit it
551
+ to deploy globally.
552
+
553
+ ```ts
554
+ import { SpearClient, Intents } from "spearkit";
555
+
556
+ const client = new SpearClient({ intents: Intents.default });
557
+ // …register commands…
558
+
559
+ await client.start(); // uses DISCORD_TOKEN
560
+
561
+ // Deploy once the client is ready.
562
+ client.once("clientReady", async () => {
563
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
564
+ });
565
+ ```
566
+
567
+ ## Everything discord.js still works
568
+
569
+ `SpearClient` extends discord.js `Client`, so the full client surface is
570
+ available unchanged. spearkit adds registries on top — it never hides what is
571
+ underneath:
572
+
573
+ ```ts
574
+ import { SpearClient } from "spearkit";
575
+
576
+ const client = new SpearClient();
577
+
578
+ client.on("guildCreate", (guild) => console.log(`Joined ${guild.name}`));
579
+ client.ws.on("VOICE_SERVER_UPDATE", () => {});
580
+
581
+ await client.start();
582
+
583
+ console.log(client.application?.id); // application
584
+ console.log(client.user?.tag); // user
585
+ console.log(client.rest); // REST manager (used by deployCommands)
586
+
587
+ await client.destroy(); // graceful shutdown
588
+ ```
589
+
590
+ ## See also
591
+
592
+ - [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — defining slash commands you register here.
593
+ - [Plugins](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/plugins.md) — bundling features for `client.use`.
594
+ - [File-based loading](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/loading.md) — populating the client from a directory.
595
+
596
+ ---
597
+
598
+ # Commands
599
+
600
+ Slash commands in spearkit are defined as a single object: the metadata, the typed
601
+ options, and the handler all live together. spearkit serialises them for discord
602
+ and routes incoming interactions to the right handler for you.
603
+
604
+ ## A first command
605
+
606
+ ```ts
607
+ import { command } from "spearkit";
608
+
609
+ export const ping = command({
610
+ name: "ping",
611
+ description: "Check latency",
612
+ run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
613
+ });
614
+ ```
615
+
616
+ Register it on a client (`client.register(ping)`) and deploy it (see
617
+ [Deployment](#deployment)). That's the whole loop.
618
+
619
+ ## The command context
620
+
621
+ The handler receives a `CommandContext`. It wraps the discord.js
622
+ `ChatInputCommandInteraction` and adds ergonomic accessors and reply helpers.
623
+
624
+ | Member | Description |
625
+ | ------ | ----------- |
626
+ | `ctx.options` | Resolved, fully-typed option values (see [Options](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/options.md)). |
627
+ | `ctx.commandName` | The invoked command name. |
628
+ | `ctx.subcommand` | The invoked subcommand name, or `null`. |
629
+ | `ctx.showModal(modal)` | Present a modal in response. |
630
+ | `ctx.user` / `ctx.member` / `ctx.guild` / `ctx.guildId` / `ctx.channel` / `ctx.channelId` / `ctx.locale` | Actor and location accessors. |
631
+ | `ctx.reply` / `ctx.replyEphemeral` / `ctx.defer` / `ctx.editReply` / `ctx.followUp` / `ctx.send` / `ctx.error` | Reply helpers (see [Contexts](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md)). |
632
+ | `ctx.interaction` | The raw discord.js interaction, for anything not wrapped. |
633
+
634
+ ```ts
635
+ import { command, option } from "spearkit";
636
+
637
+ export const echo = command({
638
+ name: "echo",
639
+ description: "Repeat a message",
640
+ options: {
641
+ text: option.string({ description: "What to say", required: true }),
642
+ times: option.integer({ description: "Repeat count", minValue: 1, maxValue: 5 }),
643
+ },
644
+ run: (ctx) => {
645
+ ctx.options.text; // string
646
+ ctx.options.times; // number | undefined
647
+ return ctx.reply({
648
+ content: ctx.options.text.repeat(ctx.options.times ?? 1),
649
+ ephemeral: true,
650
+ });
651
+ },
652
+ });
653
+ ```
654
+
655
+ Options are covered in depth in [Options](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/options.md).
656
+
657
+ ## Command metadata
658
+
659
+ ```ts
660
+ import { command, PermissionFlagsBits } from "spearkit";
661
+
662
+ export const purge = command({
663
+ name: "purge",
664
+ description: "Delete recent messages",
665
+ guildOnly: true, // only usable in guilds
666
+ nsfw: false, // age-restricted command
667
+ defaultMemberPermissions: PermissionFlagsBits.ManageMessages, // who sees it by default
668
+ nameLocalizations: { tr: "temizle" }, // localized name
669
+ descriptionLocalizations: { tr: "Mesajları sil" },
670
+ run: (ctx) => ctx.reply("…"),
671
+ });
672
+ ```
673
+
674
+ | Field | Type | Effect |
675
+ | ----- | ---- | ------ |
676
+ | `guildOnly` | `boolean` | Restricts the command to guild contexts. |
677
+ | `nsfw` | `boolean` | Marks the command age-restricted. |
678
+ | `defaultMemberPermissions` | `PermissionResolvable \| null` | Default permission gate (members without it don't see the command). |
679
+ | `nameLocalizations` / `descriptionLocalizations` | `LocalizationMap` | Per-locale name/description. |
680
+
681
+ ## Subcommands and groups
682
+
683
+ For commands with subcommands, use `commandGroup` together with `subcommand`
684
+ and (optionally) `subcommandGroup`. Each subcommand has its own typed options
685
+ and handler; spearkit routes to the right one automatically.
686
+
687
+ ```ts
688
+ import { commandGroup, subcommand, subcommandGroup, option } from "spearkit";
689
+
690
+ export const admin = commandGroup({
691
+ name: "admin",
692
+ description: "Administration",
693
+ guildOnly: true,
694
+ // Direct subcommands: /admin say
695
+ subcommands: {
696
+ say: subcommand({
697
+ description: "Make the bot say something",
698
+ options: { message: option.string({ description: "Message", required: true }) },
699
+ run: (ctx) => ctx.reply(ctx.options.message),
700
+ }),
701
+ },
702
+ // Grouped subcommands: /admin users ban
703
+ groups: {
704
+ users: subcommandGroup({
705
+ description: "Manage users",
706
+ subcommands: {
707
+ ban: subcommand({
708
+ description: "Ban a member",
709
+ options: {
710
+ target: option.user({ description: "Member", required: true }),
711
+ reason: option.string({ description: "Reason" }),
712
+ },
713
+ run: (ctx) =>
714
+ ctx.reply(`Banned ${ctx.options.target.tag}: ${ctx.options.reason ?? "no reason"}`),
715
+ }),
716
+ },
717
+ }),
718
+ },
719
+ });
720
+ ```
721
+
722
+ Inside a subcommand handler, `ctx.options` is typed from *that subcommand's*
723
+ options. There is no `switch (subcommand)` to write — spearkit dispatches by the
724
+ invoked subcommand group/name.
725
+
726
+ ## The command registry
727
+
728
+ `client.commands` is a `CommandRegistry`. You usually feed it through
729
+ `client.register(...)`, but you can use it directly:
730
+
731
+ ```ts
732
+ import { CommandRegistry } from "spearkit";
733
+
734
+ const registry = new CommandRegistry();
735
+ registry.add(ping, echo, admin);
736
+
737
+ registry.get("ping"); // SlashCommand | undefined
738
+ registry.names; // string[]
739
+ registry.size; // number
740
+ registry.remove("ping"); // boolean
741
+ registry.toJSON(); // REST payloads for all commands
742
+ ```
743
+
744
+ `SpearClient` calls `registry.handle(interaction)` and
745
+ `registry.handleAutocomplete(interaction)` for you on every interaction.
746
+
747
+ ### Error handling
748
+
749
+ If a handler throws, spearkit catches it. By default it emits the client's `error`
750
+ event and replies with an ephemeral "something went wrong" message. Override
751
+ that:
752
+
753
+ ```ts
754
+ client.commands.onError((error, interaction) => {
755
+ console.error(`/${interaction.commandName} failed`, error);
756
+ if (!interaction.replied && !interaction.deferred) {
757
+ return interaction.reply({ content: "Command failed.", ephemeral: true });
758
+ }
759
+ });
760
+ ```
761
+
762
+ ## Deployment
763
+
764
+ Commands must be registered with discord before they appear. spearkit gives you two
765
+ ways.
766
+
767
+ **From the client** (uses the client's authenticated REST; call after ready):
768
+
769
+ ```ts
770
+ await client.start(process.env.DISCORD_TOKEN);
771
+ await client.deployCommands({ guildId: process.env.GUILD_ID }); // omit guildId for global
772
+ ```
773
+
774
+ **Standalone** (a separate deploy script, no running client needed):
775
+
776
+ ```ts
777
+ import { CommandRegistry } from "spearkit";
778
+
779
+ const registry = new CommandRegistry().add(ping, echo, admin);
780
+ await registry.deploy({
781
+ token: process.env.DISCORD_TOKEN,
782
+ applicationId: process.env.DISCORD_APP_ID,
783
+ guildId: process.env.GUILD_ID, // optional
784
+ });
785
+ ```
786
+
787
+ Guild deploys apply **instantly** and are ideal during development. Global
788
+ deploys (no `guildId`) can take up to an hour to propagate.
789
+
790
+ ## See also
791
+
792
+ - [Options](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/options.md) — typed option builders, choices, autocomplete.
793
+ - [Components](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/components.md) — buttons, selects, modals.
794
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — registering and deploying from the client.
795
+ - [Contexts](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md) — the reply helpers every handler shares.
796
+
797
+ ---
798
+
799
+ # Options
800
+
801
+ Slash command options are declared as a map of name → builder. spearkit infers the
802
+ exact value type each option resolves to, so your handler's `ctx.options` is
803
+ fully typed — no casts, no `any`, no manual `getString` calls.
804
+
805
+ ```ts
806
+ import { command, option } from "spearkit";
807
+
808
+ command({
809
+ name: "profile",
810
+ description: "Show a profile",
811
+ options: {
812
+ user: option.user({ description: "Whose profile", required: true }),
813
+ detailed: option.boolean({ description: "Show extra detail" }),
814
+ },
815
+ run: (ctx) => {
816
+ ctx.options.user; // User
817
+ ctx.options.detailed; // boolean | undefined
818
+ },
819
+ });
820
+ ```
821
+
822
+ ## Builders and resolved types
823
+
824
+ | Builder | Resolved type | Type-specific config |
825
+ | ------- | ------------- | -------------------- |
826
+ | `option.string(config)` | `string` | `choices`, `minLength`, `maxLength`, `autocomplete` |
827
+ | `option.integer(config)` | `number` | `choices`, `minValue`, `maxValue`, `autocomplete` |
828
+ | `option.number(config)` | `number` | `choices`, `minValue`, `maxValue`, `autocomplete` |
829
+ | `option.boolean(config)` | `boolean` | — |
830
+ | `option.user(config)` | `User` | — |
831
+ | `option.channel(config)` | channel union | `channelTypes` |
832
+ | `option.role(config)` | `Role \| APIRole` | — |
833
+ | `option.mentionable(config)` | user / role / member | — |
834
+ | `option.attachment(config)` | `Attachment` | — |
835
+
836
+ Every builder accepts the common config:
837
+
838
+ ```ts
839
+ {
840
+ description: string; // required
841
+ required?: boolean; // default: false
842
+ nameLocalizations?: LocalizationMap;
843
+ descriptionLocalizations?: LocalizationMap;
844
+ }
845
+ ```
846
+
847
+ ## Inference rules
848
+
849
+ spearkit narrows the resolved type from your declaration:
850
+
851
+ ```ts
852
+ options: {
853
+ // required → the value type, never undefined
854
+ name: option.string({ description: "Name", required: true }), // string
855
+
856
+ // optional (default) → value | undefined
857
+ age: option.integer({ description: "Age" }), // number | undefined
858
+
859
+ // choices → a literal union of the choice values
860
+ size: option.string({
861
+ description: "Size",
862
+ choices: [
863
+ { name: "Small", value: "sm" },
864
+ { name: "Large", value: "lg" },
865
+ ],
866
+ }), // "sm" | "lg" | undefined
867
+ }
868
+ ```
869
+
870
+ - **Required** options resolve to the value type.
871
+ - **Optional** options resolve to `value | undefined` (spearkit converts discord's
872
+ absent value to `undefined`, never `null`).
873
+ - **`choices`** narrow string/integer/number options to a literal union of the
874
+ declared `value`s.
875
+
876
+ ```ts
877
+ run: (ctx) => {
878
+ const name: string = ctx.options.name;
879
+ const age: number | undefined = ctx.options.age;
880
+ const size: "sm" | "lg" | undefined = ctx.options.size;
881
+ };
882
+ ```
883
+
884
+ ## Numeric and length constraints
885
+
886
+ ```ts
887
+ options: {
888
+ count: option.integer({ description: "How many", minValue: 1, maxValue: 100 }),
889
+ code: option.string({ description: "Code", minLength: 4, maxLength: 8 }),
890
+ }
891
+ ```
892
+
893
+ ## Channel options
894
+
895
+ Restrict the selectable channel types with `channelTypes` (from discord.js
896
+ `ChannelType`):
897
+
898
+ ```ts
899
+ import { option, ChannelType } from "spearkit";
900
+
901
+ options: {
902
+ target: option.channel({
903
+ description: "A text or announcement channel",
904
+ channelTypes: [ChannelType.GuildText, ChannelType.GuildAnnouncement],
905
+ }),
906
+ }
907
+ ```
908
+
909
+ ## Choices
910
+
911
+ `choices` are `{ name, value }` pairs. `name` is shown to the user; `value` is
912
+ what your handler receives (and what spearkit narrows the type to).
913
+
914
+ ```ts
915
+ option.integer({
916
+ description: "Priority",
917
+ choices: [
918
+ { name: "Low", value: 1 },
919
+ { name: "High", value: 2 },
920
+ ],
921
+ // optional per-choice localization:
922
+ // choices: [{ name: "Low", value: 1, nameLocalizations: { tr: "Düşük" } }],
923
+ }); // 1 | 2 | undefined
924
+ ```
925
+
926
+ ## Autocomplete
927
+
928
+ Provide an `autocomplete` handler instead of fixed `choices` to suggest values
929
+ as the user types. spearkit marks the option as autocompletable, routes the
930
+ autocomplete interaction, and (for subcommands) finds the right option.
931
+
932
+ ```ts
933
+ const fruits = ["apple", "apricot", "banana", "cherry"];
934
+
935
+ option.string({
936
+ description: "Fruit",
937
+ required: true,
938
+ autocomplete: (ctx) =>
939
+ fruits
940
+ .filter((f) => f.startsWith(ctx.value))
941
+ .map((f) => ({ name: f, value: f })),
942
+ });
943
+ ```
944
+
945
+ The autocomplete handler receives an `AutocompleteContext`:
946
+
947
+ | Member | Description |
948
+ | ------ | ----------- |
949
+ | `ctx.value` | The current partial value typed by the user. |
950
+ | `ctx.focusedName` | The name of the option being completed. |
951
+ | `ctx.commandName` | The command being completed. |
952
+ | `ctx.client` / `ctx.user` / `ctx.guild` / `ctx.guildId` | Accessors. |
953
+ | `ctx.respond(choices)` | Send suggestions (capped at discord's 25). |
954
+
955
+ Returning the choices array (as above) is enough — spearkit calls `respond` for
956
+ you. Returning `[]` shows no suggestions.
957
+
958
+ ## See also
959
+
960
+ - [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — using options inside commands and subcommands.
961
+ - [Components](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/components.md) — buttons, selects and modals.
962
+
963
+ ---
964
+
965
+ # Components
966
+
967
+ Buttons, select menus and modals in spearkit follow one pattern: define the
968
+ appearance, the **custom-id pattern**, and the handler in one place; register
969
+ it; then `build()` the discord.js component to put in a message. spearkit decodes
970
+ incoming interactions and routes them to your handler — no `interactionCreate`
971
+ switch statements, no manual custom-id parsing.
972
+
973
+ ```ts
974
+ import { button, row } from "spearkit";
975
+
976
+ const vote = button({
977
+ id: "vote:{choice}",
978
+ label: "Yes",
979
+ style: "Success",
980
+ run: (ctx) => ctx.update(`You chose ${ctx.params.choice}`), // ctx.params.choice: string
981
+ });
982
+
983
+ client.register(vote); // or client.components.add(vote)
984
+
985
+ await channel.send({
986
+ content: "Cast your vote:",
987
+ components: [row(vote.build({ choice: "yes" }))], // build() requires { choice }
988
+ });
989
+ ```
990
+
991
+ ## Custom-id patterns
992
+
993
+ The `id` is a pattern with the grammar `name` or `name:{param}` or
994
+ `name:{a}:{b}`. The leading `name` is the routing **namespace**; each `{param}`
995
+ becomes a positional value carried in the custom-id.
996
+
997
+ - In the handler, params are available as a typed object: `ctx.params.choice`.
998
+ - `build(params)` requires **exactly** those params and encodes them into the
999
+ custom-id.
1000
+
1001
+ ```ts
1002
+ const page = button({
1003
+ id: "page:{id}:{dir}",
1004
+ label: "Next",
1005
+ run: (ctx) => ctx.update(`item ${ctx.params.id}, going ${ctx.params.dir}`),
1006
+ });
1007
+
1008
+ page.build({ id: "42", dir: "next" }); // custom-id "page:42:next"
1009
+ ```
1010
+
1011
+ spearkit percent-escapes param values, so they may safely contain `:`. Custom-ids
1012
+ are limited to 100 characters (`MAX_CUSTOM_ID_LENGTH`); `build()` throws if you
1013
+ exceed it.
1014
+
1015
+ For advanced use, the codec is exported directly: `compilePattern`,
1016
+ `buildCustomId`, `parseCustomId`, and `paramsFromValues`.
1017
+
1018
+ ## Buttons
1019
+
1020
+ ```ts
1021
+ import { button, linkButton, ButtonStyle } from "spearkit";
1022
+
1023
+ const confirm = button({
1024
+ id: "confirm:{action}",
1025
+ label: "Confirm",
1026
+ style: ButtonStyle.Danger, // or the string "Danger"
1027
+ emoji: "⚠️",
1028
+ disabled: false,
1029
+ run: (ctx) => ctx.update(`Confirmed: ${ctx.params.action}`),
1030
+ });
1031
+
1032
+ // Link buttons have no handler and no custom-id:
1033
+ const docs = linkButton({ url: "https://example.com", label: "Docs" });
1034
+ ```
1035
+
1036
+ `style` accepts the string names `"Primary"`, `"Secondary"`, `"Success"`,
1037
+ `"Danger"`, or the `ButtonStyle` enum. It defaults to `"Secondary"`.
1038
+
1039
+ The `ButtonContext` adds, on top of the shared [reply helpers](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md):
1040
+
1041
+ | Member | Description |
1042
+ | ------ | ----------- |
1043
+ | `ctx.params` | Decoded custom-id params. |
1044
+ | `ctx.update(input)` | Edit the message the button is on. |
1045
+ | `ctx.deferUpdate()` | Acknowledge without editing yet. |
1046
+ | `ctx.showModal(modal)` | Open a modal in response. |
1047
+ | `ctx.message` | The message the button belongs to. |
1048
+ | `ctx.customId` | The raw custom-id. |
1049
+
1050
+ ## Select menus
1051
+
1052
+ There are five select builders. All share `placeholder`, `minValues`,
1053
+ `maxValues`, and `disabled`; the string select additionally takes `options`,
1054
+ and the channel select takes `channelTypes`.
1055
+
1056
+ ```ts
1057
+ import { stringSelect, channelSelect, ChannelType } from "spearkit";
1058
+
1059
+ const colour = stringSelect({
1060
+ id: "colour",
1061
+ placeholder: "Pick a colour",
1062
+ minValues: 1,
1063
+ maxValues: 1,
1064
+ options: [
1065
+ { label: "Red", value: "red" },
1066
+ { label: "Green", value: "green", description: "the calm one" },
1067
+ { label: "Blue", value: "blue", default: true },
1068
+ ],
1069
+ run: (ctx) => ctx.reply({ content: `You picked ${ctx.values.join(", ")}`, ephemeral: true }),
1070
+ });
1071
+
1072
+ const pickChannel = channelSelect({
1073
+ id: "pick-channel",
1074
+ channelTypes: [ChannelType.GuildText],
1075
+ run: (ctx) => ctx.reply({ content: `${ctx.values.length} channel(s)`, ephemeral: true }),
1076
+ });
1077
+ ```
1078
+
1079
+ Each select context exposes the relevant resolved data:
1080
+
1081
+ | Builder | Context | Extra accessors |
1082
+ | ------- | ------- | --------------- |
1083
+ | `stringSelect` | `StringSelectContext` | `values: string[]`, `value: string \| undefined` |
1084
+ | `userSelect` | `UserSelectContext` | `values`, `users`, `members` |
1085
+ | `roleSelect` | `RoleSelectContext` | `values`, `roles` |
1086
+ | `channelSelect` | `ChannelSelectContext` | `values`, `channels` |
1087
+ | `mentionableSelect` | `MentionableSelectContext` | `values`, `users`, `roles`, `members` |
1088
+
1089
+ Select contexts also have `ctx.params`, `ctx.update`, `ctx.deferUpdate`,
1090
+ `ctx.showModal`, and the shared reply helpers.
1091
+
1092
+ ## Modals
1093
+
1094
+ A modal declares its `fields` as a map of name → `textInput`. The submit handler
1095
+ receives the submitted values in `ctx.fields`, keyed (and typed) by those names,
1096
+ plus any custom-id params in `ctx.params`.
1097
+
1098
+ ```ts
1099
+ import { modal, textInput } from "spearkit";
1100
+
1101
+ const feedback = modal({
1102
+ id: "feedback:{ticket}",
1103
+ title: "Feedback",
1104
+ fields: {
1105
+ summary: textInput({ label: "Summary", required: true }),
1106
+ detail: textInput({ label: "Details", style: "Paragraph", maxLength: 2000 }),
1107
+ },
1108
+ run: (ctx) =>
1109
+ ctx.reply({
1110
+ // ctx.params.ticket: string, ctx.fields.summary / ctx.fields.detail: string
1111
+ content: `#${ctx.params.ticket}: ${ctx.fields.summary}`,
1112
+ ephemeral: true,
1113
+ }),
1114
+ });
1115
+ ```
1116
+
1117
+ `textInput` config: `label` (required), `style` (`"Short"` default, or
1118
+ `"Paragraph"`, or a `TextInputStyle`), `placeholder`, `required`, `minLength`,
1119
+ `maxLength`, `value`.
1120
+
1121
+ Open a modal from a command or a component handler with `showModal` — modals
1122
+ cannot be the *response* to another modal, but they can follow a command or a
1123
+ button/select:
1124
+
1125
+ ```ts
1126
+ import { command } from "spearkit";
1127
+
1128
+ const ask = command({
1129
+ name: "ask",
1130
+ description: "Open the feedback form",
1131
+ run: (ctx) => ctx.showModal(feedback.build({ ticket: "1234" })),
1132
+ });
1133
+ ```
1134
+
1135
+ ## Action rows
1136
+
1137
+ `row(...components)` wraps builders in an `ActionRowBuilder`. A row holds up to
1138
+ five buttons, or exactly one select menu.
1139
+
1140
+ ```ts
1141
+ import { row } from "spearkit";
1142
+
1143
+ const components = [
1144
+ row(confirm.build({ action: "delete" }), docs),
1145
+ row(colour.build()),
1146
+ ];
1147
+ await channel.send({ content: "Choose:", components });
1148
+ ```
1149
+
1150
+ ## Registering and routing
1151
+
1152
+ Register components like anything else:
1153
+
1154
+ ```ts
1155
+ client.register(vote, colour, feedback);
1156
+ // equivalently:
1157
+ client.components.add(vote, colour, feedback);
1158
+ ```
1159
+
1160
+ `SpearClient` routes every button, select and modal interaction to the matching
1161
+ namespace automatically. The `ComponentRegistry` API:
1162
+
1163
+ | Member | Description |
1164
+ | ------ | ----------- |
1165
+ | `add(...defs)` | Register components (override by namespace). |
1166
+ | `size` | Number registered. |
1167
+ | `onError(handler)` | Set the error handler. |
1168
+ | `handle(interaction)` | Route an interaction; returns `true` if matched. |
1169
+
1170
+ ### Error handling
1171
+
1172
+ By default a throwing handler emits the client `error` event and replies with an
1173
+ ephemeral message. Customise it:
1174
+
1175
+ ```ts
1176
+ client.components.onError((error, interaction) => {
1177
+ console.error("component failed", error);
1178
+ });
1179
+ ```
1180
+
1181
+ ## End-to-end example
1182
+
1183
+ ```ts
1184
+ import {
1185
+ SpearClient,
1186
+ Intents,
1187
+ command,
1188
+ button,
1189
+ stringSelect,
1190
+ modal,
1191
+ textInput,
1192
+ row,
1193
+ } from "spearkit";
1194
+
1195
+ const client = new SpearClient({ intents: Intents.default });
1196
+
1197
+ const open = button({
1198
+ id: "open-form:{topic}",
1199
+ label: "Open form",
1200
+ style: "Primary",
1201
+ run: (ctx) => ctx.showModal(form.build({ topic: ctx.params.topic })),
1202
+ });
1203
+
1204
+ const rating = stringSelect({
1205
+ id: "rating",
1206
+ placeholder: "Rate us",
1207
+ options: [
1208
+ { label: "Good", value: "good" },
1209
+ { label: "Bad", value: "bad" },
1210
+ ],
1211
+ run: (ctx) => ctx.reply({ content: `Thanks: ${ctx.value}`, ephemeral: true }),
1212
+ });
1213
+
1214
+ const form = modal({
1215
+ id: "form:{topic}",
1216
+ title: "Tell us more",
1217
+ fields: { body: textInput({ label: "Message", style: "Paragraph", required: true }) },
1218
+ run: (ctx) => ctx.reply({ content: `[${ctx.params.topic}] ${ctx.fields.body}`, ephemeral: true }),
1219
+ });
1220
+
1221
+ const panel = command({
1222
+ name: "panel",
1223
+ description: "Show the panel",
1224
+ run: (ctx) =>
1225
+ ctx.reply({
1226
+ content: "How was it?",
1227
+ components: [row(open.build({ topic: "support" })), row(rating.build())],
1228
+ }),
1229
+ });
1230
+
1231
+ client.register(panel, open, rating, form);
1232
+ ```
1233
+
1234
+ ## See also
1235
+
1236
+ - [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — opening components from commands.
1237
+ - [Contexts](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md) — the reply/update helpers contexts share.
1238
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — registration and routing.
1239
+
1240
+ ---
1241
+
1242
+ # Events
1243
+
1244
+ `event()` defines a reusable, loadable discord.js event listener with a
1245
+ fully-typed handler. The handler's arguments are inferred from discord.js'
1246
+ `ClientEvents`, so you never annotate them by hand. Register an event with the
1247
+ client and spearkit attaches the listener for you.
1248
+
1249
+ ```ts
1250
+ import { event } from "spearkit";
1251
+
1252
+ export default event("messageCreate", (message) => {
1253
+ if (message.author.bot) return;
1254
+ // message is fully typed as Message
1255
+ });
1256
+ ```
1257
+
1258
+ ## Defining an event
1259
+
1260
+ `event` has two forms. The positional form takes the event name and handler:
1261
+
1262
+ ```ts
1263
+ import { event } from "spearkit";
1264
+
1265
+ const onMessage = event("messageCreate", (message) => {
1266
+ // message: Message
1267
+ console.log(message.content);
1268
+ });
1269
+
1270
+ const onReady = event("clientReady", (client) => {
1271
+ // client: Client<true> — the ready client
1272
+ console.log(`Logged in as ${client.user.tag}`);
1273
+ });
1274
+ ```
1275
+
1276
+ The object form (`EventConfig`) additionally accepts `once`, which runs the
1277
+ handler at most once and then auto-detaches:
1278
+
1279
+ ```ts
1280
+ import { event } from "spearkit";
1281
+
1282
+ const onceReady = event({
1283
+ name: "clientReady",
1284
+ once: true,
1285
+ run: (client) => {
1286
+ // client: Client<true>
1287
+ console.log(`Ready as ${client.user.tag}`);
1288
+ },
1289
+ });
1290
+ ```
1291
+
1292
+ Both forms return an `EventDef` — a type-erased, ready-to-attach listener
1293
+ (`{ name, once, attach, detach }`). Register it like anything else:
1294
+
1295
+ ```ts
1296
+ import { SpearClient } from "spearkit";
1297
+
1298
+ const client = new SpearClient();
1299
+ client.register(onMessage, onReady);
1300
+ ```
1301
+
1302
+ ### Handlers are fully typed from `ClientEvents`
1303
+
1304
+ The event name drives the parameter types. There is nothing to import or
1305
+ annotate — picking `"messageCreate"` types the argument as `Message`, picking
1306
+ `"guildMemberAdd"` types it as `GuildMember`, and so on. The handler type is
1307
+ exported as `EventHandler<E>` (`(...args: ClientEvents[E]) => Awaitable<void>`).
1308
+
1309
+ ```ts
1310
+ import { event } from "spearkit";
1311
+
1312
+ const onJoin = event("guildMemberAdd", (member) => {
1313
+ // member: GuildMember
1314
+ void member.roles.add("123456789012345678");
1315
+ });
1316
+
1317
+ const onReaction = event("messageReactionAdd", (reaction, user) => {
1318
+ // reaction: MessageReaction | PartialMessageReaction
1319
+ // user: User | PartialUser
1320
+ console.log(`${user.id} reacted with ${reaction.emoji.name}`);
1321
+ });
1322
+ ```
1323
+
1324
+ ### Intents are required
1325
+
1326
+ An event only fires if the client connected with the matching gateway intents.
1327
+ For example, `messageCreate` with message content needs `Intents.messages` (or
1328
+ at least the `GuildMessages` / `MessageContent` bits); `guildMemberAdd` needs
1329
+ `GuildMembers`. See [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) for the intent presets.
1330
+
1331
+ ## Errors are routed, not fatal
1332
+
1333
+ If a handler throws synchronously or rejects a returned promise, spearkit catches
1334
+ it and emits it on the client's `error` event instead of crashing the process.
1335
+ Listen for `error` to log or report failures centrally:
1336
+
1337
+ ```ts
1338
+ import { SpearClient } from "spearkit";
1339
+
1340
+ const client = new SpearClient();
1341
+
1342
+ client.on("error", (err) => {
1343
+ console.error("A handler failed:", err);
1344
+ });
1345
+ ```
1346
+
1347
+ ## Inline listeners still work
1348
+
1349
+ Because spearkit re-exports discord.js, the plain `client.on(...)` / `client.once(...)`
1350
+ listeners work exactly as before — they are the same methods. Reach for them for
1351
+ quick, inline, client-local listeners:
1352
+
1353
+ ```ts
1354
+ import { SpearClient } from "spearkit";
1355
+
1356
+ const client = new SpearClient();
1357
+ client.on("guildCreate", (guild) => console.log(`Joined ${guild.name}`));
1358
+ ```
1359
+
1360
+ Use `event()` when you want a listener that is **reusable and loadable** — a
1361
+ self-contained module you can export, register from anywhere, or pick up via
1362
+ `client.load(...)`. Note that inline `client.on` listeners do **not** get the
1363
+ automatic error-routing that `event()` handlers do.
1364
+
1365
+ ## The `EventRegistry`
1366
+
1367
+ `client.events` is an `EventRegistry`. The client attaches it automatically at
1368
+ construction and again when you `register` an event, so you usually never call
1369
+ its methods directly. They are available for advanced control:
1370
+
1371
+ | Member | Type | Description |
1372
+ | ------ | ---- | ----------- |
1373
+ | `add(...defs)` | `this` | Register one or more `EventDef`s (and attach them to already-attached clients). |
1374
+ | `size` | `number` | Number of registered listeners. |
1375
+ | `attachAll(client)` | `void` | Attach every registered listener to a client. |
1376
+ | `detachAll(client)` | `void` | Detach every registered listener from a client. |
1377
+
1378
+ ```ts
1379
+ import { SpearClient, event } from "spearkit";
1380
+
1381
+ const client = new SpearClient();
1382
+ client.events.add(event("warn", (info) => console.warn(info)));
1383
+
1384
+ console.log(client.events.size); // 1
1385
+
1386
+ // Detach all spearkit-managed listeners (e.g. before a hot reload).
1387
+ client.events.detachAll(client);
1388
+ ```
1389
+
1390
+ ## See also
1391
+
1392
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — registering events and the required intents.
1393
+ - [File-based loading](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/loading.md) — one event per file, auto-registered.
1394
+
1395
+ ---
1396
+
1397
+ # Contexts
1398
+
1399
+ Every spearkit handler — command, button, select, modal — receives a context
1400
+ object. They all share `BaseContext`, which smooths over discord.js'
1401
+ reply/defer/edit/follow-up state machine and exposes the common
1402
+ actor/location accessors. Learn it once and it applies everywhere.
1403
+
1404
+ ```ts
1405
+ import { command, option } from "spearkit";
1406
+
1407
+ export default command({
1408
+ name: "hello",
1409
+ description: "Say hello",
1410
+ options: { name: option.string({ description: "Name", required: true }) },
1411
+ run: (ctx) => ctx.reply(`Hi, ${ctx.options.name}!`),
1412
+ });
1413
+ ```
1414
+
1415
+ `CommandContext`, `ButtonContext`, `StringSelectContext`, modal contexts and the
1416
+ rest extend `BaseContext`, adding their own specifics (e.g. `ctx.options`,
1417
+ `ctx.params`, `ctx.fields`) on top of everything below.
1418
+
1419
+ ## Reply helpers
1420
+
1421
+ | Method | Returns | Behaviour |
1422
+ | ------ | ------- | --------- |
1423
+ | `reply(input)` | `Promise<InteractionResponse>` | Send the initial response. |
1424
+ | `replyEphemeral(input)` | `Promise<InteractionResponse>` | Reply, hidden to everyone but the invoking user. |
1425
+ | `defer({ ephemeral })` | `Promise<InteractionResponse>` | Acknowledge now, respond later via `editReply`. |
1426
+ | `editReply(input)` | `Promise<Message>` | Edit the original (or deferred) response. |
1427
+ | `followUp(input)` | `Promise<Message>` | Add a message after the initial response. |
1428
+ | `send(input)` | `Promise<void>` | State-aware: replies, edits, or follows up automatically. |
1429
+ | `error(message)` | `Promise<void>` | State-aware ephemeral message. |
1430
+
1431
+ ```ts
1432
+ import { command } from "spearkit";
1433
+
1434
+ export default command({
1435
+ name: "demo",
1436
+ description: "Reply helpers",
1437
+ run: async (ctx) => {
1438
+ await ctx.reply("Working on it…");
1439
+ await ctx.followUp("…almost done.");
1440
+ },
1441
+ });
1442
+ ```
1443
+
1444
+ ### `send` is the one most handlers need
1445
+
1446
+ `send` inspects the interaction state and does the right thing:
1447
+
1448
+ - not yet answered → `reply`
1449
+ - already deferred → `editReply`
1450
+ - already replied → `followUp`
1451
+
1452
+ This means you can call `send` without tracking whether you deferred, which is
1453
+ ideal for shared helpers that may run before or after a `defer`.
1454
+
1455
+ ```ts
1456
+ import { command } from "spearkit";
1457
+
1458
+ export default command({
1459
+ name: "report",
1460
+ description: "Generate a report",
1461
+ run: async (ctx) => {
1462
+ await ctx.defer(); // acknowledge while we do slow work
1463
+ const data = await buildReport();
1464
+ await ctx.send(data); // sees the deferred state → edits the reply
1465
+ },
1466
+ });
1467
+ ```
1468
+
1469
+ ### `error` for ephemeral failures
1470
+
1471
+ `error(message)` sends a state-aware, always-ephemeral message — perfect for
1472
+ validation failures that only the invoking user should see.
1473
+
1474
+ ```ts
1475
+ import { command, option } from "spearkit";
1476
+
1477
+ export default command({
1478
+ name: "kick",
1479
+ description: "Kick a member",
1480
+ options: { who: option.user({ description: "Member", required: true }) },
1481
+ run: async (ctx) => {
1482
+ if (!ctx.guild) return ctx.error("This command only works in a server.");
1483
+ await ctx.reply(`Kicked ${ctx.options.who}.`);
1484
+ },
1485
+ });
1486
+ ```
1487
+
1488
+ ## The `{ ephemeral: true }` shortcut
1489
+
1490
+ discord.js represents an ephemeral reply with `flags: MessageFlags.Ephemeral`.
1491
+ spearkit lets you write the more obvious `{ ephemeral: true }` on any reply payload
1492
+ and maps it to that flag for you. The input type is `ReplyInput`
1493
+ (`string | ReplyData`), where `ReplyData` is discord.js'
1494
+ `InteractionReplyOptions` plus the optional `ephemeral` boolean.
1495
+
1496
+ ```ts
1497
+ import { command, EmbedBuilder } from "spearkit";
1498
+
1499
+ export default command({
1500
+ name: "secret",
1501
+ description: "Only you can see this",
1502
+ run: (ctx) =>
1503
+ ctx.reply({
1504
+ embeds: [new EmbedBuilder().setTitle("Just for you")],
1505
+ ephemeral: true, // mapped to MessageFlags.Ephemeral
1506
+ }),
1507
+ });
1508
+ ```
1509
+
1510
+ `replyEphemeral(input)` is sugar for the same thing, accepting either a string
1511
+ or a payload:
1512
+
1513
+ ```ts
1514
+ await ctx.replyEphemeral("Saved.");
1515
+ await ctx.replyEphemeral({ embeds: [embed] });
1516
+ ```
1517
+
1518
+ If you set `flags` yourself, spearkit preserves them and adds the ephemeral flag
1519
+ rather than overwriting it.
1520
+
1521
+ ### Exported helpers
1522
+
1523
+ spearkit exports the two functions it uses internally, so you can normalise reply
1524
+ input yourself (e.g. in a plugin or shared utility):
1525
+
1526
+ - `normalizeReply(input: ReplyInput): InteractionReplyOptions` — converts a
1527
+ string or `ReplyData` into a discord.js reply payload, applying the ephemeral
1528
+ flag mapping.
1529
+ - `asEphemeral(input: ReplyInput): ReplyData` — marks any input ephemeral,
1530
+ regardless of how it was passed.
1531
+
1532
+ ```ts
1533
+ import { normalizeReply, asEphemeral } from "spearkit";
1534
+
1535
+ normalizeReply("hi");
1536
+ // → { content: "hi" }
1537
+
1538
+ normalizeReply({ content: "hi", ephemeral: true });
1539
+ // → { content: "hi", flags: MessageFlags.Ephemeral }
1540
+
1541
+ asEphemeral("hidden");
1542
+ // → { content: "hidden", ephemeral: true }
1543
+ ```
1544
+
1545
+ ## Accessors
1546
+
1547
+ `BaseContext` forwards the common interaction fields so you do not reach through
1548
+ `ctx.interaction` for everyday data:
1549
+
1550
+ | Accessor | Description |
1551
+ | -------- | ----------- |
1552
+ | `interaction` | The raw discord.js interaction. |
1553
+ | `client` | The `SpearClient` (typed as the interaction's client). |
1554
+ | `user` | The invoking `User`. |
1555
+ | `member` | The invoking guild member (or `null` outside a guild). |
1556
+ | `guild` | The `Guild`, or `null` in DMs. |
1557
+ | `guildId` | The guild id, or `null`. |
1558
+ | `channel` | The channel the interaction came from. |
1559
+ | `channelId` | The channel id. |
1560
+ | `locale` | The user's locale. |
1561
+ | `deferred` | Whether the interaction is already deferred. |
1562
+ | `replied` | Whether the interaction already received an initial response. |
1563
+
1564
+ ```ts
1565
+ import { command } from "spearkit";
1566
+
1567
+ export default command({
1568
+ name: "whereami",
1569
+ description: "Report context",
1570
+ run: (ctx) =>
1571
+ ctx.reply(
1572
+ ctx.guild
1573
+ ? `In ${ctx.guild.name} (#${ctx.channelId}), locale ${ctx.locale}.`
1574
+ : "We're in a DM.",
1575
+ ),
1576
+ });
1577
+ ```
1578
+
1579
+ `deferred` and `replied` let you branch when you are not using `send`:
1580
+
1581
+ ```ts
1582
+ import { button } from "spearkit";
1583
+
1584
+ export default button({
1585
+ id: "refresh",
1586
+ label: "Refresh",
1587
+ run: async (ctx) => {
1588
+ if (ctx.replied || ctx.deferred) await ctx.followUp("Refreshed.");
1589
+ else await ctx.reply("Refreshed.");
1590
+ },
1591
+ });
1592
+ ```
1593
+
1594
+ ## See also
1595
+
1596
+ - [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — `CommandContext`, options and `showModal`.
1597
+ - [Components](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/components.md) — button, select and modal contexts.
1598
+
1599
+ ---
1600
+
1601
+ # Cooldowns
1602
+
1603
+ Rate-limit commands per user, per role, per guild, per channel or globally.
1604
+ Cooldowns are enforced automatically by command dispatch: when an actor is
1605
+ still on cooldown, spearkit replies (ephemerally) with a message and the
1606
+ handler does not run.
1607
+
1608
+ ## Per-command
1609
+
1610
+ Pass a number (milliseconds) or a full config to any command:
1611
+
1612
+ ```ts
1613
+ import { command } from "spearkit";
1614
+
1615
+ export const daily = command({
1616
+ name: "daily",
1617
+ description: "Claim your daily reward",
1618
+ cooldown: 86_400_000, // once per day, per user
1619
+ run: (ctx) => ctx.reply("Reward claimed!"),
1620
+ });
1621
+ ```
1622
+
1623
+ ## Client-wide default
1624
+
1625
+ A default applies to every command; a command's own `cooldown` overrides it.
1626
+
1627
+ ```ts
1628
+ import { SpearClient } from "spearkit";
1629
+
1630
+ const client = new SpearClient({ cooldown: { duration: 3000 } });
1631
+ ```
1632
+
1633
+ ## Scope
1634
+
1635
+ `scope` controls what the cooldown is keyed on. Default `"user"`.
1636
+
1637
+ ```ts
1638
+ command({
1639
+ name: "announce",
1640
+ description: "Post an announcement",
1641
+ cooldown: { duration: 60_000, scope: "guild" }, // one per guild per minute
1642
+ run: (ctx) => ctx.reply("Announced."),
1643
+ });
1644
+ ```
1645
+
1646
+ | Scope | Keyed on |
1647
+ | --- | --- |
1648
+ | `user` | the invoking user (default) |
1649
+ | `guild` | the guild |
1650
+ | `channel` | the channel |
1651
+ | `global` | everyone shares one bucket |
1652
+
1653
+ ## Exemptions — who waits and who doesn't
1654
+
1655
+ `exempt` lists users and roles that bypass the cooldown entirely.
1656
+
1657
+ ```ts
1658
+ command({
1659
+ name: "purge",
1660
+ description: "Bulk delete",
1661
+ cooldown: {
1662
+ duration: 10_000,
1663
+ exempt: { roles: ["111111111111111111"], users: ["222222222222222222"] },
1664
+ },
1665
+ run: (ctx) => ctx.reply("Purged."),
1666
+ });
1667
+ ```
1668
+
1669
+ ## Per-role / per-user overrides
1670
+
1671
+ `overrides` gives specific roles or users a different duration (milliseconds).
1672
+ A user override beats role overrides; among matching roles the most lenient
1673
+ (shortest) duration wins. Use `0` to effectively disable the wait for them.
1674
+
1675
+ ```ts
1676
+ command({
1677
+ name: "search",
1678
+ description: "Search the archive",
1679
+ cooldown: {
1680
+ duration: 10_000, // everyone else
1681
+ overrides: {
1682
+ roles: { "333333333333333333": 2_000 }, // VIP role: 2s
1683
+ users: { "444444444444444444": 0 }, // this user: no wait
1684
+ },
1685
+ },
1686
+ run: (ctx) => ctx.reply("Searching…"),
1687
+ });
1688
+ ```
1689
+
1690
+ ## The message
1691
+
1692
+ `message` customises what blocked users see — a string, or a function of the
1693
+ remaining milliseconds.
1694
+
1695
+ ```ts
1696
+ command({
1697
+ name: "spin",
1698
+ description: "Spin the wheel",
1699
+ cooldown: {
1700
+ duration: 5_000,
1701
+ message: (ms) => `Hold on — ${Math.ceil(ms / 1000)}s to go.`,
1702
+ },
1703
+ run: (ctx) => ctx.reply("🎡"),
1704
+ });
1705
+ ```
1706
+
1707
+ ## The manager
1708
+
1709
+ `client.cooldowns` is the shared `CooldownManager` (also used by
1710
+ [prefix commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/prefix.md)). Use it directly for custom flows:
1711
+
1712
+ ```ts
1713
+ const result = client.cooldowns.consume("vote", 5_000, {
1714
+ userId: "1",
1715
+ roleIds: [],
1716
+ guildId: null,
1717
+ channelId: null,
1718
+ });
1719
+ if (!result.allowed) console.log(`wait ${result.remaining}ms`);
1720
+ ```
1721
+
1722
+ `consume` records the use and returns `{ allowed: true }` or
1723
+ `{ allowed: false, remaining }`. `peek` checks without recording; `reset` and
1724
+ `clear` drop tracked cooldowns.
1725
+
1726
+ ---
1727
+
1728
+ # Scheduled tasks
1729
+
1730
+ Run work on a cron schedule or a fixed interval. The client starts the
1731
+ scheduler when it becomes ready and stops it on `destroy()`, so timers never
1732
+ outlive your bot.
1733
+
1734
+ ## Define a task
1735
+
1736
+ Provide exactly one of `cron` or `interval`:
1737
+
1738
+ ```ts
1739
+ import { task } from "spearkit";
1740
+
1741
+ export const heartbeat = task({
1742
+ name: "heartbeat",
1743
+ interval: 60_000, // every minute
1744
+ runOnStart: true, // also run once on startup
1745
+ run: (client) => client.logger.info("still alive"),
1746
+ });
1747
+ ```
1748
+
1749
+ Register it like anything else:
1750
+
1751
+ ```ts
1752
+ client.register(heartbeat);
1753
+ ```
1754
+
1755
+ Or define and register in one call:
1756
+
1757
+ ```ts
1758
+ client.schedule({
1759
+ name: "cleanup",
1760
+ cron: "0 3 * * *", // 03:00 local time, every day
1761
+ run: async (client) => {
1762
+ // …purge expired records…
1763
+ },
1764
+ });
1765
+ ```
1766
+
1767
+ ## Cron syntax
1768
+
1769
+ Standard 5-field expressions, evaluated in the host's **local** time:
1770
+
1771
+ ```
1772
+ ┌─ minute (0-59)
1773
+ │ ┌─ hour (0-23)
1774
+ │ │ ┌─ day of month (1-31)
1775
+ │ │ │ ┌─ month (1-12)
1776
+ │ │ │ │ ┌─ day of week (0-6, Sunday = 0)
1777
+ │ │ │ │ │
1778
+ * * * * *
1779
+ ```
1780
+
1781
+ Each field supports `*`, ranges (`1-5`), lists (`1,3,5`) and steps (`*/15`).
1782
+ When both day-of-month and day-of-week are restricted, a date matches if
1783
+ **either** does (standard cron behaviour).
1784
+
1785
+ Aliases: `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`.
1786
+
1787
+ ```ts
1788
+ task({ name: "report", cron: "@daily", run: () => {} });
1789
+ task({ name: "poll", cron: "*/5 * * * *", run: () => {} }); // every 5 minutes
1790
+ task({ name: "mondays", cron: "0 9 * * 1", run: () => {} }); // Mon 09:00
1791
+ ```
1792
+
1793
+ Compute the next run yourself with `cron`:
1794
+
1795
+ ```ts
1796
+ import { cron } from "spearkit";
1797
+
1798
+ const next = cron("*/15 * * * *").next(new Date());
1799
+ ```
1800
+
1801
+ ## The scheduler
1802
+
1803
+ `client.scheduler` is the `TaskScheduler`:
1804
+
1805
+ ```ts
1806
+ client.scheduler.size; // number of tasks
1807
+ client.scheduler.active; // started?
1808
+ client.scheduler.list(); // every task
1809
+ client.scheduler.remove("heartbeat"); // cancel + forget
1810
+ client.scheduler.stop(); // cancel all timers
1811
+ ```
1812
+
1813
+ Task errors are caught and logged through `client.logger` (scope `scheduler`),
1814
+ so a throwing task never crashes the process or stops future runs.
1815
+
1816
+ ---
1817
+
1818
+ # Prefix commands
1819
+
1820
+ Alongside slash commands, spearkit can dispatch classic text/prefix commands like
1821
+ `!ping`. You define them with `prefixCommand`, enable them with the client's
1822
+ `prefix` option, and spearkit parses each `messageCreate` for you — matching the
1823
+ prefix, splitting arguments, and routing to the right handler.
1824
+
1825
+ ## Enabling prefix commands
1826
+
1827
+ Prefix commands are off until you set the `prefix` option on the client. It
1828
+ accepts a string, an array of strings, or a `PrefixOptions` object:
1829
+
1830
+ ```ts
1831
+ import { Intents, SpearClient } from "spearkit";
1832
+
1833
+ // A single prefix.
1834
+ new SpearClient({ intents: Intents.messages, prefix: "!" });
1835
+
1836
+ // Several prefixes.
1837
+ new SpearClient({ intents: Intents.messages, prefix: ["!", "?"] });
1838
+
1839
+ // Full control.
1840
+ new SpearClient({
1841
+ intents: Intents.messages,
1842
+ prefix: {
1843
+ prefix: "!",
1844
+ mention: true, // also trigger on a leading @bot mention (default true)
1845
+ ignoreBots: true, // skip messages authored by bots (default true)
1846
+ caseInsensitive: true, // match command names ignoring case (default true)
1847
+ },
1848
+ });
1849
+ ```
1850
+
1851
+ ## You need the MessageContent intent
1852
+
1853
+ Reading the text of other users' messages is a **privileged** gateway intent.
1854
+ Without `MessageContent` your bot still receives `messageCreate`, but
1855
+ `message.content` arrives empty for messages it was not mentioned in or did not
1856
+ author — so no prefix command will ever match.
1857
+
1858
+ Use the `Intents.messages` preset, which includes `Guilds`, `GuildMessages`, and
1859
+ the privileged `MessageContent` bit:
1860
+
1861
+ ```ts
1862
+ import { Intents, SpearClient } from "spearkit";
1863
+
1864
+ const client = new SpearClient({ intents: Intents.messages, prefix: "!" });
1865
+ ```
1866
+
1867
+ You must also toggle **Message Content Intent** on for your application in the
1868
+ Discord Developer Portal, or the gateway will reject the connection.
1869
+
1870
+ ## Defining a command
1871
+
1872
+ `prefixCommand` takes the command name, the handler, and a few optional fields:
1873
+
1874
+ ```ts
1875
+ import { prefixCommand } from "spearkit";
1876
+
1877
+ export const ping = prefixCommand({
1878
+ name: "ping",
1879
+ description: "Check that the bot is alive",
1880
+ run: (ctx) => ctx.reply("Pong!"),
1881
+ });
1882
+ ```
1883
+
1884
+ Register it like anything else, with `client.register(...)`:
1885
+
1886
+ ```ts
1887
+ import { SpearClient } from "spearkit";
1888
+
1889
+ const client = new SpearClient({ prefix: "!" });
1890
+ client.register(ping);
1891
+ ```
1892
+
1893
+ | Field | Type | Effect |
1894
+ | ----- | ---- | ------ |
1895
+ | `name` | `string` | The word after the prefix that triggers the command. |
1896
+ | `aliases` | `string[]` | Extra names that also trigger it. |
1897
+ | `description` | `string` | Human description, for your own help command. |
1898
+ | `cooldown` | `number \| CooldownConfig` | Per-user rate limit (a number is milliseconds). |
1899
+ | `run` | `(ctx: PrefixContext) => void \| Promise<void>` | The handler. |
1900
+
1901
+ ## The prefix context
1902
+
1903
+ The handler receives a `PrefixContext`. It wraps the triggering `Message` and
1904
+ adds the parsed arguments plus reply helpers.
1905
+
1906
+ | Member | Description |
1907
+ | ------ | ----------- |
1908
+ | `ctx.message` | The triggering discord.js `Message`. |
1909
+ | `ctx.commandName` | The matched name as the user typed it (an alias if they used one). |
1910
+ | `ctx.args` | Whitespace-split arguments after the command name (`string[]`). |
1911
+ | `ctx.rest` | The raw text after the command name (unsplit). |
1912
+ | `ctx.author` / `ctx.member` / `ctx.guild` / `ctx.guildId` / `ctx.channel` / `ctx.channelId` | Actor and location accessors. |
1913
+ | `ctx.reply(content)` | Reply to the triggering message. |
1914
+ | `ctx.send(content)` | Send a message to the same channel without a reply reference. |
1915
+
1916
+ ```ts
1917
+ import { prefixCommand } from "spearkit";
1918
+
1919
+ export const echo = prefixCommand({
1920
+ name: "echo",
1921
+ description: "Repeat what you said",
1922
+ run: (ctx) => {
1923
+ if (ctx.args.length === 0) return ctx.reply("Give me something to echo.");
1924
+ // `args` is split on whitespace; `rest` is the untouched remainder.
1925
+ return ctx.reply(ctx.rest);
1926
+ },
1927
+ });
1928
+ ```
1929
+
1930
+ `ctx.args` and `ctx.rest` are two views of the same input: `!say hello world`
1931
+ gives `args === ["hello", "world"]` and `rest === "hello world"`.
1932
+
1933
+ ## Aliases
1934
+
1935
+ List alternative names in `aliases`; any of them triggers the command, and
1936
+ `ctx.commandName` reports whichever the user typed:
1937
+
1938
+ ```ts
1939
+ import { prefixCommand } from "spearkit";
1940
+
1941
+ export const help = prefixCommand({
1942
+ name: "help",
1943
+ aliases: ["h", "commands"],
1944
+ run: (ctx) => ctx.reply(`You used "${ctx.commandName}".`),
1945
+ });
1946
+ ```
1947
+
1948
+ ## Cooldowns
1949
+
1950
+ Prefix commands share the client's cooldown manager (`client.cooldowns`) with
1951
+ slash commands, so the API is identical. Pass `cooldown` as a number of
1952
+ milliseconds or a full `CooldownConfig`:
1953
+
1954
+ ```ts
1955
+ import { prefixCommand } from "spearkit";
1956
+
1957
+ export const daily = prefixCommand({
1958
+ name: "daily",
1959
+ description: "Claim your daily reward",
1960
+ cooldown: 5_000, // one use per user per 5s
1961
+ run: (ctx) => ctx.reply("Reward claimed! Come back soon."),
1962
+ });
1963
+ ```
1964
+
1965
+ When a user is on cooldown, spearkit replies with the remaining time and does not
1966
+ run the handler. A per-command `cooldown` overrides the client-wide `cooldown`
1967
+ default. See [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md) for scopes and configuration.
1968
+
1969
+ ## The prefix registry
1970
+
1971
+ `client.prefix` is a `PrefixRegistry`. The client wires it to `messageCreate`,
1972
+ the logger, and the cooldown manager for you, so you rarely call it directly. It
1973
+ is available for introspection and advanced control:
1974
+
1975
+ ```ts
1976
+ client.prefix.get("ping"); // PrefixCommand | undefined (also resolves aliases)
1977
+ client.prefix.list(); // PrefixCommand[] (excludes aliases)
1978
+ client.prefix.size; // number of commands
1979
+ ```
1980
+
1981
+ ### Error handling
1982
+
1983
+ If a handler throws, spearkit catches it, logs it, and calls your error hook if you
1984
+ set one — the process never crashes:
1985
+
1986
+ ```ts
1987
+ client.prefix.onError((error, message) => {
1988
+ console.error(`prefix command failed in #${message.channelId}`, error);
1989
+ });
1990
+ ```
1991
+
1992
+ ## See also
1993
+
1994
+ - [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — slash commands.
1995
+ - [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md) — the shared rate limiter.
1996
+ - [Usage tracking](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/usage.md) — record who runs which prefix commands.
1997
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — the `prefix` option and intent presets.
1998
+
1999
+ ---
2000
+
2001
+ # Logging
2002
+
2003
+ spearkit ships a small, dependency-free structured logger. Every client owns one
2004
+ at `client.logger`, and spearkit routes its own command, component, event, and
2005
+ gateway errors through it. You can use the same logger for your code, or build a
2006
+ standalone one.
2007
+
2008
+ ## A first logger
2009
+
2010
+ ```ts
2011
+ import { Logger } from "spearkit";
2012
+
2013
+ const log = new Logger(); // level "info", logs to the console
2014
+ log.info("bot starting");
2015
+ log.error("connection lost", { error: new Error("ECONNRESET") });
2016
+ ```
2017
+
2018
+ A logger is constructed from `LoggerOptions`:
2019
+
2020
+ ```ts
2021
+ import { Logger } from "spearkit";
2022
+
2023
+ const log = new Logger({
2024
+ level: "debug", // minimum severity to emit; default "info"
2025
+ scope: "worker", // a prefix attached to every entry
2026
+ // sink: consoleSink (the default)
2027
+ });
2028
+ ```
2029
+
2030
+ ## Levels
2031
+
2032
+ There are four levels, lowest to highest: `debug`, `info`, `warn`, `error`. The
2033
+ logger emits an entry only if its level is at or above the configured threshold.
2034
+ The default threshold is **`info`**, so `debug` entries are suppressed until you
2035
+ lower it. A fifth threshold, `"silent"`, suppresses everything.
2036
+
2037
+ ```ts
2038
+ import { Logger } from "spearkit";
2039
+
2040
+ const log = new Logger(); // threshold "info"
2041
+ log.debug("only visible at debug"); // suppressed by default
2042
+ log.info("visible");
2043
+
2044
+ log.setLevel("debug"); // now debug entries are emitted too
2045
+ log.enabled("debug"); // true
2046
+ log.setLevel("silent"); // suppress everything
2047
+ ```
2048
+
2049
+ | Method | Level | Use for |
2050
+ | ------ | ----- | ------- |
2051
+ | `log.debug(msg, opts?)` | `debug` | Verbose diagnostics, off by default. |
2052
+ | `log.info(msg, opts?)` | `info` | Normal operational messages. |
2053
+ | `log.warn(msg, opts?)` | `warn` | Recoverable problems worth attention. |
2054
+ | `log.error(msg, opts?)` | `error` | Failures; attach the cause via `{ error }`. |
2055
+
2056
+ `log.level` reads the current threshold, `log.setLevel(level)` changes it, and
2057
+ `log.enabled(level)` reports whether an entry of that level would be emitted —
2058
+ handy to guard expensive message construction.
2059
+
2060
+ ## Scopes and child loggers
2061
+
2062
+ `log.child("scope")` returns a child logger whose entries carry an extra scope
2063
+ segment. A child **shares its parent's threshold and sink**, so changing the
2064
+ level on any logger in the tree affects them all.
2065
+
2066
+ ```ts
2067
+ import { Logger } from "spearkit";
2068
+
2069
+ const log = new Logger({ scope: "app" });
2070
+ const db = log.child("db"); // scope "app:db"
2071
+ const cache = db.child("cache"); // scope "app:db:cache"
2072
+
2073
+ db.info("connected");
2074
+ log.setLevel("debug"); // affects log, db, and cache
2075
+ cache.debug("warm"); // now emitted
2076
+ ```
2077
+
2078
+ spearkit uses this internally: the client creates `commands`, `components`,
2079
+ `events`, `scheduler`, `prefix`, and `usage` children off `client.logger`, so
2080
+ every subsystem's output is scoped and a single `setLevel` controls them all.
2081
+
2082
+ ## Structured `data` and `error`
2083
+
2084
+ Both arguments live in the optional second parameter (`LogOptions`):
2085
+
2086
+ ```ts
2087
+ import { Logger } from "spearkit";
2088
+
2089
+ const log = new Logger();
2090
+
2091
+ log.info("command finished", {
2092
+ data: { command: "ping", ms: 12, cached: true },
2093
+ });
2094
+
2095
+ try {
2096
+ throw new Error("kaboom");
2097
+ } catch (cause) {
2098
+ log.error("handler failed", {
2099
+ error: cause instanceof Error ? cause : new Error(String(cause)),
2100
+ data: { command: "purge", guildId: "123" },
2101
+ });
2102
+ }
2103
+ ```
2104
+
2105
+ `data` is a flat record of primitives (`string | number | boolean | bigint |
2106
+ null | undefined`). `error` is an `Error`; the default sink renders its stack.
2107
+
2108
+ ### Coercing unknown throws
2109
+
2110
+ A `catch` binding is `unknown`. `toError(value)` turns any thrown value into an
2111
+ `Error` so it fits `{ error }`:
2112
+
2113
+ ```ts
2114
+ import { Logger, toError } from "spearkit";
2115
+
2116
+ const log = new Logger();
2117
+ try {
2118
+ JSON.parse("{");
2119
+ } catch (cause) {
2120
+ log.error("parse failed", { error: toError(cause) });
2121
+ }
2122
+ ```
2123
+
2124
+ ## Custom sinks
2125
+
2126
+ A sink is `(entry: LogEntry) => void`. The default is `consoleSink`, which writes
2127
+ human-readable lines to the console (stderr for `warn`/`error`). Pass your own to
2128
+ route entries anywhere — JSON lines, a file, an aggregator:
2129
+
2130
+ ```ts
2131
+ import { Logger, type LogEntry } from "spearkit";
2132
+
2133
+ const log = new Logger({
2134
+ level: "debug",
2135
+ sink: (entry: LogEntry) => {
2136
+ process.stdout.write(
2137
+ JSON.stringify({
2138
+ level: entry.level,
2139
+ message: entry.message,
2140
+ scope: entry.scope,
2141
+ at: entry.timestamp.toISOString(),
2142
+ data: entry.data,
2143
+ error: entry.error?.message,
2144
+ }) + "\n",
2145
+ );
2146
+ },
2147
+ });
2148
+
2149
+ log.child("commands").info("dispatched", { data: { name: "ping" } });
2150
+ ```
2151
+
2152
+ A `LogEntry` is the fully-resolved record handed to the sink:
2153
+
2154
+ | Field | Type | Notes |
2155
+ | ----- | ---- | ----- |
2156
+ | `level` | `LogLevel` | One of `debug`/`info`/`warn`/`error`. |
2157
+ | `message` | `string` | The log message. |
2158
+ | `scope` | `string \| undefined` | The accumulated scope, if any. |
2159
+ | `timestamp` | `Date` | When the entry was created. |
2160
+ | `error` | `Error \| undefined` | The attached error, if any. |
2161
+ | `data` | `Record<string, LogValue> \| undefined` | The structured metadata, if any. |
2162
+
2163
+ ## Configuring via the client
2164
+
2165
+ Pass `logger` to `SpearClient`. Give it `LoggerOptions` to build one, or a
2166
+ `Logger` instance you already have:
2167
+
2168
+ ```ts
2169
+ import { SpearClient, Logger } from "spearkit";
2170
+
2171
+ // Build from options:
2172
+ const a = new SpearClient({ logger: { level: "debug" } });
2173
+
2174
+ // Or reuse an instance (e.g. one shared with non-Discord code):
2175
+ const shared = new Logger({ level: "info", scope: "svc" });
2176
+ const b = new SpearClient({ logger: shared });
2177
+
2178
+ a.logger.info("ready");
2179
+ ```
2180
+
2181
+ The client logs all command, component, and event handler errors plus gateway
2182
+ errors through `client.logger`. Set `level: "debug"` to see dispatch traces from
2183
+ every subsystem:
2184
+
2185
+ ```ts
2186
+ import { SpearClient } from "spearkit";
2187
+
2188
+ const client = new SpearClient({ logger: { level: "debug" } });
2189
+ // client.logger.child("commands"), ".child('events')", etc. all log at debug now
2190
+ ```
2191
+
2192
+ ## See also
2193
+
2194
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — the `logger` and other construction options.
2195
+ - [Environment & dotenv](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/env.md) — load configuration before you start.
2196
+
2197
+ ---
2198
+
2199
+ # Usage tracking
2200
+
2201
+ Usage tracking records **who used what**: every successful command, component,
2202
+ and prefix-command invocation becomes a `UsageEvent` that spearkit can persist to a
2203
+ store and/or mirror into a Discord channel. Turn it on with the client's `usage`
2204
+ option.
2205
+
2206
+ ## Usage tracking vs the logger
2207
+
2208
+ These look similar but answer different questions, and they are completely
2209
+ independent sinks:
2210
+
2211
+ | | Logger | Usage tracking |
2212
+ | --- | --- | --- |
2213
+ | Question | *What is the bot doing?* (diagnostics) | *Who used which feature?* (audit) |
2214
+ | Content | Free-form messages, levels, errors, internals | Structured `UsageEvent`s for successful uses |
2215
+ | Sinks | Console / your log pipeline | A database store and/or a Discord channel |
2216
+ | Configured by | the `logger` option | the `usage` option |
2217
+
2218
+ A failed or errored command shows up in your **logs**; it is not recorded as a
2219
+ usage event. Reach for the [logger](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/logging.md) for debugging, and usage
2220
+ tracking for analytics, audit trails, and "top commands" dashboards.
2221
+
2222
+ ## Enabling it
2223
+
2224
+ Set the `usage` option. Provide a `store` (a database), a `channel` (a Discord
2225
+ channel id to mirror events into), or both:
2226
+
2227
+ ```ts
2228
+ import { MemoryUsageStore, SpearClient } from "spearkit";
2229
+
2230
+ const client = new SpearClient({
2231
+ usage: {
2232
+ store: new MemoryUsageStore(),
2233
+ channel: "123456789012345678", // optional: also post each event here
2234
+ },
2235
+ });
2236
+ ```
2237
+
2238
+ Once enabled, spearkit auto-tracks every successful command, component, and prefix
2239
+ command — you write no tracking code in your handlers.
2240
+
2241
+ ## The usage event
2242
+
2243
+ Each tracked use is a `UsageEvent`:
2244
+
2245
+ ```ts
2246
+ interface UsageEvent {
2247
+ type: "command" | "prefix" | "component" | "event";
2248
+ name: string; // command/component/event name
2249
+ userId?: string;
2250
+ userTag?: string;
2251
+ guildId?: string | null;
2252
+ channelId?: string | null;
2253
+ detail?: string; // free-form extra detail
2254
+ timestamp: Date;
2255
+ }
2256
+ ```
2257
+
2258
+ ## Stores (the database)
2259
+
2260
+ A store is any object implementing `UsageStore`:
2261
+
2262
+ ```ts
2263
+ interface UsageStore {
2264
+ record(event: UsageEvent): void | Promise<void>;
2265
+ all(): UsageEvent[] | Promise<readonly UsageEvent[]>;
2266
+ }
2267
+ ```
2268
+
2269
+ spearkit ships two.
2270
+
2271
+ ### MemoryUsageStore
2272
+
2273
+ In-memory and synchronous — ideal for tests, prototypes, and live dashboards.
2274
+ Pass an optional cap to keep only the most recent N events:
2275
+
2276
+ ```ts
2277
+ import { MemoryUsageStore } from "spearkit";
2278
+
2279
+ const store = new MemoryUsageStore(1_000); // keep the last 1,000 events
2280
+
2281
+ store.all(); // readonly UsageEvent[]
2282
+ store.size; // number of events held
2283
+ store.byUser("123..."); // UsageEvent[] for one user id
2284
+ store.clear(); // forget everything
2285
+ ```
2286
+
2287
+ ### JsonFileUsageStore
2288
+
2289
+ Durable and dependency-free: appends one event per line as newline-delimited
2290
+ JSON (`.jsonl`). `all()` reads the file back and parses it, so it behaves like a
2291
+ small file-backed database:
2292
+
2293
+ ```ts
2294
+ import { JsonFileUsageStore } from "spearkit";
2295
+
2296
+ const store = new JsonFileUsageStore("./usage.jsonl");
2297
+
2298
+ await store.record({ type: "command", name: "ping", timestamp: new Date() });
2299
+ const events = await store.all(); // readonly UsageEvent[]
2300
+ ```
2301
+
2302
+ The directory is created on demand. Because `all()` is async here (it reads from
2303
+ disk), always `await` it.
2304
+
2305
+ ## Querying the store
2306
+
2307
+ `client.usage.store` is the store you configured — query it directly. Note that
2308
+ `all()` may be synchronous (`MemoryUsageStore`) or asynchronous
2309
+ (`JsonFileUsageStore`); awaiting works for both:
2310
+
2311
+ ```ts
2312
+ const store = client.usage.store;
2313
+ if (store !== undefined) {
2314
+ const events = await store.all();
2315
+ const topCommand = events.filter((e) => e.type === "command").length;
2316
+ console.log(`${topCommand} command uses recorded`);
2317
+ }
2318
+ ```
2319
+
2320
+ ## The Discord channel reporter
2321
+
2322
+ Besides (or instead of) a store, you can mirror each event into a Discord
2323
+ channel. Pass `channel` (a channel id) in the `usage` option; spearkit posts one
2324
+ line per event using `formatUsage`:
2325
+
2326
+ ```ts
2327
+ import { SpearClient } from "spearkit";
2328
+
2329
+ new SpearClient({ usage: { channel: "123456789012345678" } });
2330
+ ```
2331
+
2332
+ `formatUsage(event)` is the default renderer (e.g. `` `command` **ping** by
2333
+ user#0001 in <#…> ``). Override it with `format` to control the line:
2334
+
2335
+ ```ts
2336
+ import { SpearClient, type UsageEvent } from "spearkit";
2337
+
2338
+ new SpearClient({
2339
+ usage: {
2340
+ channel: "123456789012345678",
2341
+ format: (event: UsageEvent) => `${event.userTag ?? "someone"} used ${event.name}`,
2342
+ },
2343
+ });
2344
+ ```
2345
+
2346
+ ## The usage tracker
2347
+
2348
+ `client.usage` is a `UsageTracker`. The client configures it from the `usage`
2349
+ option, but you can drive it directly:
2350
+
2351
+ | Member | Description |
2352
+ | ------ | ----------- |
2353
+ | `setStore(store)` | Set (or swap) the persistence store. |
2354
+ | `reportTo(channelId, format?)` | Mirror events into a channel, optionally with a custom formatter. |
2355
+ | `track(event)` | Record a use. Returns immediately; storing/reporting run in the background. |
2356
+ | `store` | The configured store, for querying. |
2357
+ | `enabled` | `true` if a store or channel is configured. |
2358
+
2359
+ ```ts
2360
+ import { JsonFileUsageStore } from "spearkit";
2361
+
2362
+ client.usage.setStore(new JsonFileUsageStore("./usage.jsonl"));
2363
+ client.usage.reportTo("123456789012345678");
2364
+
2365
+ // Record a custom event yourself (e.g. a non-command action).
2366
+ client.usage.track({ type: "event", name: "signup", timestamp: new Date() });
2367
+ ```
2368
+
2369
+ Tracking is fire-and-forget: a slow store or channel never blocks command
2370
+ handling, and any failure is logged rather than thrown.
2371
+
2372
+ ## See also
2373
+
2374
+ - [Logging](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/logging.md) — diagnostics, the other sink.
2375
+ - [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) / [Prefix commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/prefix.md) / [Components](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/components.md) — what gets tracked.
2376
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — the `usage` option.
2377
+
2378
+ ---
2379
+
2380
+ # Environment & dotenv
2381
+
2382
+ spearkit includes a tiny, dependency-free `.env` loader and a typed reader over
2383
+ `process.env`, so a bot needs no extra dotenv dependency. The client auto-loads
2384
+ `.env` on `start()`, and the same helpers are exported for your own use.
2385
+
2386
+ ## Loading a `.env` file
2387
+
2388
+ `loadEnv(options?)` reads a `.env` file and merges it into `process.env`. By
2389
+ default it reads `.env` from the current working directory. Variables already
2390
+ present in `process.env` win unless you pass `override: true`. A missing file is
2391
+ ignored — it simply returns `{}` — so it is safe to call unconditionally.
2392
+
2393
+ ```ts
2394
+ import { loadEnv } from "spearkit";
2395
+
2396
+ const parsed = loadEnv(); // reads ./.env
2397
+ loadEnv({ path: ".env.local" }); // a different file
2398
+ loadEnv({ override: true }); // let the file win over existing vars
2399
+ ```
2400
+
2401
+ `loadEnv` returns the parsed key/value pairs it read from the file:
2402
+
2403
+ ```ts
2404
+ import { loadEnv } from "spearkit";
2405
+
2406
+ const parsed = loadEnv(); // ParsedEnv = Record<string, string>
2407
+ console.log(Object.keys(parsed));
2408
+ ```
2409
+
2410
+ ## Parsing without touching `process.env`
2411
+
2412
+ `parseEnv(text)` parses `.env`-formatted text into a flat object and never
2413
+ mutates `process.env`. It understands single/double quotes, a leading `export `,
2414
+ `#` comments, and `\n`/`\r`/`\t` escapes inside double quotes.
2415
+
2416
+ ```ts
2417
+ import { parseEnv } from "spearkit";
2418
+
2419
+ const vars = parseEnv(`
2420
+ # a comment
2421
+ export TOKEN="abc#notacomment"
2422
+ GREETING="line one\nline two"
2423
+ RAW='no $escapes here'
2424
+ `);
2425
+
2426
+ vars.TOKEN; // "abc#notacomment"
2427
+ vars.GREETING; // "line one\nline two" (real newline)
2428
+ vars.RAW; // "no $escapes here"
2429
+ ```
2430
+
2431
+ ## The typed `env` reader
2432
+
2433
+ `env` reads from `process.env` with coercion and optional fallbacks. Empty
2434
+ strings count as missing.
2435
+
2436
+ ```ts
2437
+ import { env } from "spearkit";
2438
+
2439
+ env.string("REGION"); // string | undefined
2440
+ env.string("REGION", "eu"); // string (fallback when missing)
2441
+
2442
+ env.number("PORT"); // number | undefined
2443
+ env.number("PORT", 3000); // number (fallback when missing or non-numeric)
2444
+
2445
+ env.boolean("DEBUG"); // boolean | undefined
2446
+ env.boolean("DEBUG", false); // boolean
2447
+
2448
+ env.require("DISCORD_TOKEN"); // string, throws if missing or empty
2449
+ ```
2450
+
2451
+ `env.boolean` treats `true`/`1`/`yes`/`on` as `true` and `false`/`0`/`no`/`off`
2452
+ as `false` (case-insensitive); anything else yields the fallback. `env.require`
2453
+ throws a descriptive error when the variable is missing or empty — use it for
2454
+ values your bot cannot run without.
2455
+
2456
+ ```ts
2457
+ import { loadEnv, env } from "spearkit";
2458
+
2459
+ loadEnv();
2460
+ const token = env.require("DISCORD_TOKEN"); // guaranteed string
2461
+ const port = env.number("PORT", 8080); // number
2462
+ const verbose = env.boolean("VERBOSE", false);
2463
+ ```
2464
+
2465
+ ## Auto-loading on the client
2466
+
2467
+ `SpearClient` calls `loadEnv()` for you inside `client.start()`, so `.env` is
2468
+ picked up before login. That means `await client.start()` finds
2469
+ `DISCORD_TOKEN` from `.env` without any extra wiring:
2470
+
2471
+ ```ts
2472
+ import { SpearClient } from "spearkit";
2473
+
2474
+ const client = new SpearClient();
2475
+
2476
+ async function main(): Promise<void> {
2477
+ await client.start(); // loads .env, then reads DISCORD_TOKEN
2478
+ }
2479
+
2480
+ void main();
2481
+ ```
2482
+
2483
+ ### The `dotenv` option
2484
+
2485
+ Control the auto-load with the `dotenv` construction option:
2486
+
2487
+ ```ts
2488
+ import { SpearClient } from "spearkit";
2489
+
2490
+ // Default: load ./.env on start.
2491
+ new SpearClient({ dotenv: true });
2492
+
2493
+ // Disable auto-loading entirely (e.g. env is provided by the platform).
2494
+ new SpearClient({ dotenv: false });
2495
+
2496
+ // Customize: same shape as loadEnv's options.
2497
+ new SpearClient({ dotenv: { path: ".env.production", override: true } });
2498
+ ```
2499
+
2500
+ | `dotenv` value | Effect |
2501
+ | -------------- | ------ |
2502
+ | `true` / omitted | Load `.env` from the cwd on `start()`. |
2503
+ | `false` | Skip auto-loading; `process.env` is used as-is. |
2504
+ | `{ path?, override? }` | Load with those `loadEnv` options. |
2505
+
2506
+ ## See also
2507
+
2508
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — the `dotenv` and other construction options.
2509
+ - [Logging](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/logging.md) — structured logging that pairs with `env`-driven config.
2510
+
2511
+ ---
2512
+
2513
+ # Plugins
2514
+
2515
+ A plugin is a named, reusable bundle of commands, events and components. It lets
2516
+ you package a feature once and install it into any `SpearClient` with a single
2517
+ call — useful for sharing functionality across bots or splitting a large bot into
2518
+ self-contained features.
2519
+
2520
+ ## Defining a plugin
2521
+
2522
+ `definePlugin` is an identity helper: it returns the object you pass it, but gives
2523
+ it the `SpearPlugin` type and editor hints.
2524
+
2525
+ ```ts
2526
+ interface SpearPlugin {
2527
+ name: string;
2528
+ setup(client: SpearClient): Awaitable<void>;
2529
+ }
2530
+ ```
2531
+
2532
+ A plugin has a `name` and a `setup` function. `setup` receives the client and
2533
+ registers whatever the feature needs — commands, events, components — typically
2534
+ via `client.register`.
2535
+
2536
+ ```ts
2537
+ import { definePlugin, button, command, event, option, row } from "spearkit";
2538
+
2539
+ export const moderation = definePlugin({
2540
+ name: "moderation",
2541
+ setup(client) {
2542
+ const confirmKick = button({
2543
+ id: "kick:{userId}",
2544
+ label: "Confirm kick",
2545
+ style: "Danger",
2546
+ run: (ctx) => ctx.update(`Kicked <@${ctx.params.userId}> (demo).`), // userId: string
2547
+ });
2548
+
2549
+ const warn = command({
2550
+ name: "warn",
2551
+ description: "Warn a member",
2552
+ options: {
2553
+ member: option.user({ description: "Member", required: true }),
2554
+ reason: option.string({ description: "Reason" }),
2555
+ },
2556
+ run: (ctx) =>
2557
+ ctx.reply({
2558
+ // member: User, reason: string | undefined
2559
+ content: `Warning ${ctx.options.member.tag}: ${ctx.options.reason ?? "no reason given"}`,
2560
+ components: [row(confirmKick.build({ userId: ctx.options.member.id }))],
2561
+ ephemeral: true,
2562
+ }),
2563
+ });
2564
+
2565
+ const ready = event("clientReady", (c) => console.log(`[moderation] ready on ${c.user.tag}`));
2566
+
2567
+ client.register(warn, confirmKick, ready);
2568
+ },
2569
+ });
2570
+ ```
2571
+
2572
+ Everything declared inside `setup` is local to the plugin; only what you pass to
2573
+ `client.register` becomes active on the client.
2574
+
2575
+ ## Installing a plugin
2576
+
2577
+ Install one or more plugins with `client.use`. It runs each plugin's `setup` in
2578
+ order and resolves to the client, so you can chain it with the rest of startup.
2579
+
2580
+ ```ts
2581
+ import { SpearClient, Intents } from "spearkit";
2582
+ import { moderation } from "./plugins/moderation.js";
2583
+
2584
+ const client = new SpearClient({ intents: Intents.default });
2585
+
2586
+ await client.use(moderation);
2587
+
2588
+ await client.start(process.env.DISCORD_TOKEN);
2589
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
2590
+ ```
2591
+
2592
+ `use` accepts several plugins at once:
2593
+
2594
+ ```ts
2595
+ await client.use(moderation, welcome, tickets);
2596
+ ```
2597
+
2598
+ ## Asynchronous setup
2599
+
2600
+ `setup` may be async — `client.use` awaits each one before moving to the next. Use
2601
+ this to load data, connect to a database, or fetch remote config before
2602
+ registering handlers.
2603
+
2604
+ ```ts
2605
+ export const tags = definePlugin({
2606
+ name: "tags",
2607
+ async setup(client) {
2608
+ const store = await openTagStore(); // await anything you need first
2609
+
2610
+ client.register(
2611
+ command({
2612
+ name: "tag",
2613
+ description: "Show a saved tag",
2614
+ options: { name: option.string({ description: "Tag name", required: true }) },
2615
+ run: (ctx) => ctx.reply(store.get(ctx.options.name) ?? "No such tag."),
2616
+ }),
2617
+ );
2618
+ },
2619
+ });
2620
+ ```
2621
+
2622
+ Because `use` awaits `setup`, every plugin is fully installed before
2623
+ `client.start` runs.
2624
+
2625
+ ## See also
2626
+
2627
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — `register`, `use`, `start`, and the registries plugins write to.
2628
+ - [File-based loading](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/loading.md) — discover commands, events and components from a directory instead of bundling them by hand.
2629
+
2630
+ ---
2631
+
2632
+ # File-based loading
2633
+
2634
+ Instead of importing and registering every handler by hand, you can keep one
2635
+ command, event or component per file and let spearkit discover them. The loader
2636
+ imports a directory, inspects each module's exports, and registers everything that
2637
+ is a command, event or component.
2638
+
2639
+ ## `client.load`
2640
+
2641
+ ```ts
2642
+ load(dir: string, options?: LoadOptions): Promise<number>
2643
+ ```
2644
+
2645
+ `client.load` imports `dir` and registers every spearkit-registrable export it finds,
2646
+ resolving to the number of items registered.
2647
+
2648
+ ```ts
2649
+ import { fileURLToPath } from "node:url";
2650
+ import { SpearClient, Intents } from "spearkit";
2651
+
2652
+ const here = fileURLToPath(new URL(".", import.meta.url));
2653
+
2654
+ const client = new SpearClient({ intents: Intents.default });
2655
+
2656
+ const loaded =
2657
+ (await client.load(`${here}commands`)) +
2658
+ (await client.load(`${here}events`)) +
2659
+ (await client.load(`${here}components`));
2660
+ console.log(`Loaded ${loaded} modules.`);
2661
+
2662
+ await client.start(process.env.DISCORD_TOKEN);
2663
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
2664
+ ```
2665
+
2666
+ ### What gets registered
2667
+
2668
+ For every imported file, spearkit walks **all** of its exports — default *and*
2669
+ named — and registers each value that is a command (`command`, `commandGroup`),
2670
+ an event (`event`), or a component (`button`, `stringSelect`, `modal`, …). Other
2671
+ exports (helpers, constants, types) are ignored. So both of these are picked up:
2672
+
2673
+ ```ts
2674
+ // default export
2675
+ export default command({ name: "ping", description: "…", run: (ctx) => ctx.reply("pong") });
2676
+ ```
2677
+
2678
+ ```ts
2679
+ // named export
2680
+ export const vote = button({ id: "vote:{choice}", label: "Vote", run: (ctx) => ctx.update(ctx.params.choice) });
2681
+ ```
2682
+
2683
+ ### Options
2684
+
2685
+ ```ts
2686
+ interface LoadOptions {
2687
+ extensions?: readonly string[]; // default: [".js", ".mjs", ".cjs"]
2688
+ recursive?: boolean; // default: true
2689
+ }
2690
+ ```
2691
+
2692
+ - **`extensions`** — which file extensions to import. By default the loader reads
2693
+ `.js`, `.mjs` and `.cjs` — i.e. **compiled JavaScript**, not `.ts` source.
2694
+ - **`recursive`** — by default the loader descends into subdirectories. Pass
2695
+ `recursive: false` to load only the top level.
2696
+
2697
+ ```ts
2698
+ await client.load(`${here}features`, { recursive: false });
2699
+ ```
2700
+
2701
+ > The loader imports compiled JavaScript. **Build your TypeScript first**, then run
2702
+ > (and load) the emitted output — `npx tsc && node dist/index.js`. Loading a
2703
+ > directory of `.ts` source files will not match the default extensions.
2704
+
2705
+ ## Standalone helpers
2706
+
2707
+ `client.load` is the method form of `loadInto`. Both helpers are exported if you
2708
+ want to collect or register modules separately.
2709
+
2710
+ ```ts
2711
+ collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>
2712
+ loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>
2713
+ ```
2714
+
2715
+ - **`collectModules`** imports a directory and returns the registrable exports it
2716
+ found, without touching any client. Use it to inspect, filter, or combine
2717
+ modules before registering.
2718
+ - **`loadInto`** calls `collectModules` and then `client.register(...)` for you,
2719
+ returning the count.
2720
+
2721
+ ```ts
2722
+ import { collectModules, loadInto } from "spearkit";
2723
+
2724
+ // Inspect before registering:
2725
+ const items = await collectModules(`${here}commands`);
2726
+ console.log(`Found ${items.length} modules`);
2727
+ client.register(...items);
2728
+
2729
+ // Or do both in one step:
2730
+ const count = await loadInto(client, `${here}events`);
2731
+ ```
2732
+
2733
+ ## Example layout
2734
+
2735
+ The `examples/file-based-loading` project keeps each handler in its own file:
2736
+
2737
+ ```
2738
+ file-based-loading/
2739
+ index.ts # construct client, load each folder, start, deploy
2740
+ commands/
2741
+ ping.ts # export default command({ ... })
2742
+ echo.ts # export default command({ ... })
2743
+ events/
2744
+ ready.ts # export default event("clientReady", ...)
2745
+ components/
2746
+ vote.ts # export const vote = button({ ... })
2747
+ ```
2748
+
2749
+ A command file looks like this:
2750
+
2751
+ ```ts
2752
+ // commands/echo.ts
2753
+ import { command, option } from "spearkit";
2754
+
2755
+ export default command({
2756
+ name: "echo",
2757
+ description: "Repeat a message",
2758
+ options: {
2759
+ text: option.string({ description: "What to say", required: true }),
2760
+ loud: option.boolean({ description: "Shout it" }),
2761
+ },
2762
+ // text: string, loud: boolean | undefined
2763
+ run: (ctx) => ctx.reply(ctx.options.loud ? ctx.options.text.toUpperCase() : ctx.options.text),
2764
+ });
2765
+ ```
2766
+
2767
+ Build the project, then run the compiled `index.js`; `client.load` will import the
2768
+ emitted `.js` files and register `ping`, `echo`, `ready` and `vote` for you.
2769
+
2770
+ ## See also
2771
+
2772
+ - [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — `load`, `register`, and the registries the loader writes to.
2773
+ - [Events](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/events.md) — the `event()` helper that event modules export.
2774
+
2775
+ ---
2776
+
2777
+ # API reference
2778
+
2779
+ Every symbol spearkit exports, in addition to the entire re-exported discord.js
2780
+ surface. Import any of these from `"spearkit"`.
2781
+
2782
+ ```ts
2783
+ import { SpearClient, command, option, event, button, modal, row /* … */ } from "spearkit";
2784
+ ```
2785
+
2786
+ ---
2787
+
2788
+ ## Client
2789
+
2790
+ ### `class SpearClient extends Client`
2791
+
2792
+ A discord.js `Client` with registries and interaction routing wired up.
2793
+
2794
+ ```ts
2795
+ new SpearClient(options?: SpearClientOptions)
2796
+ ```
2797
+
2798
+ | Member | Type | Description |
2799
+ | ------ | ---- | ----------- |
2800
+ | `commands` | `CommandRegistry` | Slash command registry + dispatcher. |
2801
+ | `events` | `EventRegistry` | Event listener registry. |
2802
+ | `components` | `ComponentRegistry` | Button/select/modal router. |
2803
+ | `register(...items: Registerable[])` | `this` | Route each item to the matching registry. |
2804
+ | `use(...plugins: SpearPlugin[])` | `Promise<this>` | Run each plugin's `setup`. |
2805
+ | `load(dir: string, options?: LoadOptions)` | `Promise<number>` | Import a directory and register its exports. Returns count. |
2806
+ | `start(token?: string)` | `Promise<this>` | Log in (falls back to `DISCORD_TOKEN`). |
2807
+ | `deployCommands(options?: { guildId?: string })` | `Promise<DeployResult>` | Push commands using the client's REST. Call after ready. |
2808
+
2809
+ Inherits everything from discord.js `Client` (`on`, `once`, `login`, `ws`, `rest`, `application`, `user`, …).
2810
+
2811
+ ### `type SpearClientOptions = Partial<ClientOptions>`
2812
+
2813
+ Same as discord.js `ClientOptions`, but `intents` may be omitted (defaults to `Intents.default`).
2814
+
2815
+ ### `const Intents`
2816
+
2817
+ Ready-made intent presets (arrays of `GatewayIntentBits`).
2818
+
2819
+ | Key | Contents |
2820
+ | --- | -------- |
2821
+ | `Intents.none` | `[]` |
2822
+ | `Intents.default` | `[Guilds]` |
2823
+ | `Intents.guilds` | `[Guilds, GuildMembers]` |
2824
+ | `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
2825
+ | `Intents.all` | Every intent (includes privileged). |
2826
+
2827
+ ### `type Registerable = SlashCommand | EventDef | ComponentDef`
2828
+
2829
+ The union accepted by `SpearClient.register`.
2830
+
2831
+ ---
2832
+
2833
+ ## Commands
2834
+
2835
+ ### `function command<O, R>(config): SlashCommand`
2836
+
2837
+ Define a leaf slash command.
2838
+
2839
+ ```ts
2840
+ interface CommandConfig<O extends OptionMap, R> {
2841
+ name: string;
2842
+ description: string;
2843
+ options?: O;
2844
+ defaultMemberPermissions?: PermissionResolvable | null;
2845
+ nsfw?: boolean;
2846
+ guildOnly?: boolean;
2847
+ nameLocalizations?: LocalizationMap;
2848
+ descriptionLocalizations?: LocalizationMap;
2849
+ run: (ctx: CommandContext<O>) => Awaitable<R>;
2850
+ }
2851
+ ```
2852
+
2853
+ ### `function commandGroup(config: CommandGroupConfig): SlashCommand`
2854
+
2855
+ Define a command that routes to subcommands and/or subcommand groups.
2856
+
2857
+ ```ts
2858
+ interface CommandGroupConfig {
2859
+ name: string;
2860
+ description: string;
2861
+ subcommands?: Record<string, Subcommand>;
2862
+ groups?: Record<string, SubcommandGroup>;
2863
+ defaultMemberPermissions?: PermissionResolvable | null;
2864
+ nsfw?: boolean;
2865
+ guildOnly?: boolean;
2866
+ nameLocalizations?: LocalizationMap;
2867
+ descriptionLocalizations?: LocalizationMap;
2868
+ }
2869
+ ```
2870
+
2871
+ ### `function subcommand<O, R>(config): Subcommand`
2872
+
2873
+ ```ts
2874
+ interface SubcommandConfig<O extends OptionMap, R> {
2875
+ description: string;
2876
+ options?: O;
2877
+ nameLocalizations?: LocalizationMap;
2878
+ descriptionLocalizations?: LocalizationMap;
2879
+ run: (ctx: CommandContext<O>) => Awaitable<R>;
2880
+ }
2881
+ ```
2882
+
2883
+ ### `function subcommandGroup(config: SubcommandGroupConfig): SubcommandGroup`
2884
+
2885
+ ```ts
2886
+ interface SubcommandGroupConfig {
2887
+ description: string;
2888
+ subcommands: Record<string, Subcommand>;
2889
+ nameLocalizations?: LocalizationMap;
2890
+ descriptionLocalizations?: LocalizationMap;
2891
+ }
2892
+ ```
2893
+
2894
+ ### `class SlashCommand`
2895
+
2896
+ | Member | Type | Description |
2897
+ | ------ | ---- | ----------- |
2898
+ | `name` | `string` | Top-level command name. |
2899
+ | `hasAutocomplete` | `boolean` | True if any option declares autocomplete. |
2900
+ | `toJSON()` | `RESTPostAPIChatInputApplicationCommandsJSONBody` | REST payload. |
2901
+ | `execute(interaction)` | `Promise<void>` | Run for a chat-input interaction. |
2902
+ | `autocomplete(interaction)` | `Promise<void>` | Run autocomplete for the focused option. |
2903
+
2904
+ ### `class CommandContext<O> extends BaseContext<ChatInputCommandInteraction>`
2905
+
2906
+ | Member | Type | Description |
2907
+ | ------ | ---- | ----------- |
2908
+ | `options` | `ResolvedOptions<O>` | Resolved, fully-typed option values. |
2909
+ | `commandName` | `string` | Invoked command name. |
2910
+ | `subcommand` | `string \| null` | Invoked subcommand, if any. |
2911
+ | `showModal(modal)` | `Promise<void>` | Present a modal. |
2912
+
2913
+ Plus all `BaseContext` members.
2914
+
2915
+ ### `class CommandRegistry`
2916
+
2917
+ | Member | Type | Description |
2918
+ | ------ | ---- | ----------- |
2919
+ | `add(...commands: SlashCommand[])` | `this` | Register commands (override by name). |
2920
+ | `remove(name: string)` | `boolean` | Remove a command. |
2921
+ | `get(name: string)` | `SlashCommand \| undefined` | Look up a command. |
2922
+ | `all()` | `SlashCommand[]` | All commands. |
2923
+ | `names` | `string[]` | All command names. |
2924
+ | `size` | `number` | Count. |
2925
+ | `onError(handler: CommandErrorHandler)` | `this` | Set the error handler. |
2926
+ | `toJSON()` | `RESTPostAPIApplicationCommandsJSONBody[]` | Serialise all commands. |
2927
+ | `handle(interaction)` | `Promise<void>` | Dispatch a chat-input interaction. |
2928
+ | `handleAutocomplete(interaction)` | `Promise<void>` | Dispatch an autocomplete interaction. |
2929
+ | `deploy(options: DeployOptions)` | `Promise<DeployResult>` | Push commands to discord. |
2930
+
2931
+ ```ts
2932
+ type CommandErrorHandler = (error: Error, interaction: ChatInputCommandInteraction) => Awaitable<void>;
2933
+ interface DeployOptions { token?: string; applicationId: string; guildId?: string; rest?: REST; }
2934
+ type DeployResult = RESTPutAPIApplicationCommandsResult | RESTPutAPIApplicationGuildCommandsResult;
2935
+ ```
2936
+
2937
+ ---
2938
+
2939
+ ## Options
2940
+
2941
+ ### `const option`
2942
+
2943
+ Type-safe option builders. Each returns an `OptionDef` whose resolved value type
2944
+ is inferred (required → value, optional → `value | undefined`, `choices` →
2945
+ literal union).
2946
+
2947
+ | Builder | Resolved type | Extra config |
2948
+ | ------- | ------------- | ------------ |
2949
+ | `option.string(config)` | `string` | `choices?`, `minLength?`, `maxLength?`, `autocomplete?` |
2950
+ | `option.integer(config)` | `number` | `choices?`, `minValue?`, `maxValue?`, `autocomplete?` |
2951
+ | `option.number(config)` | `number` | `choices?`, `minValue?`, `maxValue?`, `autocomplete?` |
2952
+ | `option.boolean(config)` | `boolean` | — |
2953
+ | `option.user(config)` | `User` | — |
2954
+ | `option.channel(config)` | channel union | `channelTypes?` |
2955
+ | `option.role(config)` | `Role \| APIRole` | — |
2956
+ | `option.mentionable(config)` | user/role/member | — |
2957
+ | `option.attachment(config)` | `Attachment` | — |
2958
+
2959
+ Common config (`BaseConfig`):
2960
+
2961
+ ```ts
2962
+ {
2963
+ description: string;
2964
+ required?: boolean; // default false
2965
+ nameLocalizations?: LocalizationMap;
2966
+ descriptionLocalizations?: LocalizationMap;
2967
+ }
2968
+ ```
2969
+
2970
+ `choices` items are `OptionChoice<V>`:
2971
+
2972
+ ```ts
2973
+ interface OptionChoice<V extends string | number = string | number> {
2974
+ name: string;
2975
+ value: V;
2976
+ nameLocalizations?: LocalizationMap;
2977
+ }
2978
+ ```
2979
+
2980
+ `autocomplete`:
2981
+
2982
+ ```ts
2983
+ type AutocompleteHandler<V extends string | number> =
2984
+ (ctx: AutocompleteContext) => Awaitable<OptionChoice<V>[]>;
2985
+ ```
2986
+
2987
+ ### Option types
2988
+
2989
+ | Symbol | Description |
2990
+ | ------ | ----------- |
2991
+ | `interface OptionDef<TValue, TRequired>` | A described option (phantom-typed for inference). |
2992
+ | `type AnyOptionDef` | `OptionDef<OptionValue, boolean>`. |
2993
+ | `type OptionMap` | `Record<string, AnyOptionDef>`. |
2994
+ | `type ResolvedOption<O>` | The handler value for one option. |
2995
+ | `type ResolvedOptions<O>` | The handler's `options` object. |
2996
+ | `type OptionValue` | Union of all possible resolved values. |
2997
+ | `type AllowedChannelType` | Channel types valid for a channel option. |
2998
+ | `function toAPIOption(name, def)` | Serialise one option to REST. |
2999
+ | `function readOption(resolver, name, def)` | Read a resolved value (null → undefined). |
3000
+ | `function optionsHaveAutocomplete(options)` | True if any option has autocomplete. |
3001
+
3002
+ ### `class AutocompleteContext`
3003
+
3004
+ | Member | Type | Description |
3005
+ | ------ | ---- | ----------- |
3006
+ | `interaction` | `AutocompleteInteraction` | Raw interaction. |
3007
+ | `client` / `user` / `guild` / `guildId` | — | Convenience accessors. |
3008
+ | `commandName` | `string` | Command being completed. |
3009
+ | `focusedName` | `string` | Name of the focused option. |
3010
+ | `value` | `string` | Current partial value typed by the user. |
3011
+ | `respond(choices: OptionChoice[])` | `Promise<void>` | Send up to 25 suggestions. |
3012
+
3013
+ ---
3014
+
3015
+ ## Events
3016
+
3017
+ ### `function event(name, run): EventDef` / `function event(config): EventDef`
3018
+
3019
+ ```ts
3020
+ type EventHandler<E extends keyof ClientEvents> = (...args: ClientEvents[E]) => Awaitable<void>;
3021
+ interface EventConfig<E extends keyof ClientEvents> { name: E; once?: boolean; run: EventHandler<E>; }
3022
+ interface EventDef { name: keyof ClientEvents; once: boolean; attach(client: Client): void; detach(client: Client): void; }
3023
+ ```
3024
+
3025
+ Thrown errors and rejected promises are routed to the client's `error` event.
3026
+
3027
+ ### `class EventRegistry`
3028
+
3029
+ | Member | Type | Description |
3030
+ | ------ | ---- | ----------- |
3031
+ | `add(...defs: EventDef[])` | `this` | Register listeners. |
3032
+ | `size` | `number` | Count. |
3033
+ | `attachAll(client: Client)` | `void` | Attach every listener. |
3034
+ | `detachAll(client: Client)` | `void` | Detach every listener. |
3035
+
3036
+ ---
3037
+
3038
+ ## Components
3039
+
3040
+ ### Builders
3041
+
3042
+ | Function | Returns | Notes |
3043
+ | -------- | ------- | ----- |
3044
+ | `button(config)` | `Button<P>` | Interactive button. |
3045
+ | `linkButton(config)` | `ButtonBuilder` | URL button, no handler. |
3046
+ | `stringSelect(config)` | `StringSelect<P>` | String select; takes `options`. |
3047
+ | `userSelect(config)` | `UserSelect<P>` | User select. |
3048
+ | `roleSelect(config)` | `RoleSelect<P>` | Role select. |
3049
+ | `channelSelect(config)` | `ChannelSelect<P>` | Channel select; takes `channelTypes?`. |
3050
+ | `mentionableSelect(config)` | `MentionableSelect<P>` | User + role select. |
3051
+ | `modal(config)` | `Modal<P>` | Modal with `fields`. |
3052
+ | `textInput(config)` | `TextInputDef` | A modal text-input field. |
3053
+ | `row(...components)` | `ActionRowBuilder<C>` | Wrap components in a row. |
3054
+
3055
+ Each registrable component (`Button`, `StringSelect`, …, `Modal`) extends its
3056
+ routing interface and adds `build(...args: BuildArgs<P>)`, which returns the
3057
+ discord.js builder. `build` requires exactly the params declared in the id
3058
+ pattern.
3059
+
3060
+ ```ts
3061
+ interface ButtonConfig<P extends string, R> {
3062
+ id: P; // pattern: "name" or "name:{param}"
3063
+ label?: string;
3064
+ style?: ButtonStyleInput; // "Primary" | "Secondary" | "Success" | "Danger" | ButtonStyle.*
3065
+ emoji?: ComponentEmojiResolvable;
3066
+ disabled?: boolean;
3067
+ run: (ctx: ButtonContext<Params<P>>) => Awaitable<R>;
3068
+ }
3069
+
3070
+ interface LinkButtonConfig { url: string; label?: string; emoji?: ComponentEmojiResolvable; disabled?: boolean; }
3071
+
3072
+ interface StringSelectConfig<P extends string, R> {
3073
+ id: P;
3074
+ options: readonly SelectMenuComponentOptionData[];
3075
+ placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
3076
+ run: (ctx: StringSelectContext<Params<P>>) => Awaitable<R>;
3077
+ }
3078
+
3079
+ interface EntitySelectConfig<P extends string> {
3080
+ id: P; placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
3081
+ }
3082
+ // user/role/mentionable selects take EntitySelectConfig & { run };
3083
+ // channelSelect additionally takes { channelTypes?: readonly ChannelType[] }.
3084
+
3085
+ function textInput(config: {
3086
+ label: string;
3087
+ style?: TextInputStyleInput; // "Short" | "Paragraph" | TextInputStyle
3088
+ placeholder?: string; required?: boolean; minLength?: number; maxLength?: number; value?: string;
3089
+ }): TextInputDef;
3090
+
3091
+ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>, R> {
3092
+ id: P;
3093
+ title: string;
3094
+ fields: F;
3095
+ run: (ctx: ModalContext<Params<P>, keyof F & string>) => Awaitable<R>;
3096
+ }
3097
+ ```
3098
+
3099
+ ### Component contexts
3100
+
3101
+ | Class | Extra members |
3102
+ | ----- | ------------- |
3103
+ | `MessageComponentContext<P, I>` | `params`, `customId`, `message`, `update(input)`, `deferUpdate()`, `showModal(modal)` (+ BaseContext) |
3104
+ | `ButtonContext<P>` | — |
3105
+ | `StringSelectContext<P>` | `values: string[]`, `value: string \| undefined` |
3106
+ | `UserSelectContext<P>` | `values`, `users`, `members` |
3107
+ | `RoleSelectContext<P>` | `values`, `roles` |
3108
+ | `ChannelSelectContext<P>` | `values`, `channels` |
3109
+ | `MentionableSelectContext<P>` | `values`, `users`, `roles`, `members` |
3110
+ | `ModalContext<P, F>` | `params`, `fields: Record<F, string>`, `customId` (+ BaseContext) |
3111
+
3112
+ ### `class ComponentRegistry`
3113
+
3114
+ | Member | Type | Description |
3115
+ | ------ | ---- | ----------- |
3116
+ | `add(...defs: ComponentDef[])` | `this` | Register components (override by namespace). |
3117
+ | `onError(handler: ComponentErrorHandler)` | `this` | Set the error handler. |
3118
+ | `size` | `number` | Count. |
3119
+ | `handle(interaction: Interaction)` | `Promise<boolean>` | Route an interaction; `true` if matched. |
3120
+
3121
+ ```ts
3122
+ type ComponentErrorHandler = (error: Error, interaction: RepliableInteraction) => Awaitable<void>;
3123
+ type ComponentDef = ButtonRoute | StringSelectRoute | UserSelectRoute | RoleSelectRoute
3124
+ | ChannelSelectRoute | MentionableSelectRoute | ModalRoute;
3125
+ ```
3126
+
3127
+ ### Custom-id codec
3128
+
3129
+ | Symbol | Description |
3130
+ | ------ | ----------- |
3131
+ | `type ParamNames<S>` | Union of `{param}` names in a pattern. |
3132
+ | `type Params<S>` | The params object a pattern resolves to. |
3133
+ | `type BuildArgs<S>` | `build()` args (none when no params). |
3134
+ | `const MAX_CUSTOM_ID_LENGTH` | `100`. |
3135
+ | `function compilePattern(pattern)` | → `CompiledPattern { pattern, namespace, paramNames }`. |
3136
+ | `function buildCustomId(compiled, params)` | Encode a concrete id. |
3137
+ | `function parseCustomId(customId)` | → `ParsedCustomId { namespace, values }`. |
3138
+ | `function paramsFromValues(paramNames, values)` | Map values onto names. |
3139
+
3140
+ ---
3141
+
3142
+ ## Contexts (shared)
3143
+
3144
+ ### `abstract class BaseContext<I>`
3145
+
3146
+ The base for every interaction context.
3147
+
3148
+ | Member | Type | Description |
3149
+ | ------ | ---- | ----------- |
3150
+ | `interaction` | `I` | Raw discord.js interaction. |
3151
+ | `client` / `user` / `member` / `guild` / `guildId` / `channel` / `channelId` / `locale` | — | Accessors. |
3152
+ | `deferred` / `replied` | `boolean` | Interaction state. |
3153
+ | `reply(input)` | `Promise<InteractionResponse>` | Initial response. |
3154
+ | `replyEphemeral(input)` | `Promise<InteractionResponse>` | Hidden reply. |
3155
+ | `defer({ ephemeral? })` | `Promise<InteractionResponse>` | Acknowledge, respond later. |
3156
+ | `editReply(input)` | `Promise<Message>` | Edit the response. |
3157
+ | `followUp(input)` | `Promise<Message>` | Additional message. |
3158
+ | `send(input)` | `Promise<void>` | State-aware reply/edit/followUp. |
3159
+ | `error(message)` | `Promise<void>` | State-aware ephemeral error. |
3160
+
3161
+ ```ts
3162
+ type ReplyData = InteractionReplyOptions & { ephemeral?: boolean };
3163
+ type ReplyInput = string | ReplyData;
3164
+ function normalizeReply(input: ReplyInput): InteractionReplyOptions;
3165
+ function asEphemeral(input: ReplyInput): ReplyData;
3166
+ ```
3167
+
3168
+ ---
3169
+
3170
+ ## Plugins
3171
+
3172
+ ```ts
3173
+ interface SpearPlugin { name: string; setup(client: SpearClient): Awaitable<void>; }
3174
+ function definePlugin(plugin: SpearPlugin): SpearPlugin;
3175
+ ```
3176
+
3177
+ ---
3178
+
3179
+ ## Loading
3180
+
3181
+ ```ts
3182
+ interface LoadOptions { extensions?: readonly string[]; recursive?: boolean; } // defaults: [.js,.mjs,.cjs], true
3183
+ function collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>;
3184
+ function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>;
3185
+ ```
3186
+
3187
+ `SpearClient.load(dir, options?)` is the method form of `loadInto`.
3188
+
3189
+ ---
3190
+
3191
+ ## Added in 0.2
3192
+
3193
+ New subsystems, each with a dedicated guide. The `SpearClient` options
3194
+ `{ logger?, dotenv?, cooldown?, prefix?, usage? }` configure them.
3195
+
3196
+ ### Logging — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/logging.md)
3197
+
3198
+ ```ts
3199
+ class Logger { 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; }
3200
+ type LogLevel = "debug" | "info" | "warn" | "error";
3201
+ type LogThreshold = LogLevel | "silent";
3202
+ function consoleSink(entry: LogEntry): void;
3203
+ function toError(value: unknown): Error;
3204
+ // client.logger is a Logger; new SpearClient({ logger: { level: "debug" } })
3205
+ ```
3206
+
3207
+ ### Environment — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/env.md)
3208
+
3209
+ ```ts
3210
+ function parseEnv(content: string): Record<string, string>;
3211
+ function loadEnv(options?: { path?: string; override?: boolean }): Record<string, string>;
3212
+ const env: { string(k, fallback?); number(k, fallback?); boolean(k, fallback?); require(k): string };
3213
+ // client auto-loads .env on start(); disable/configure via the dotenv option
3214
+ ```
3215
+
3216
+ ### Cooldowns — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md)
3217
+
3218
+ ```ts
3219
+ 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); }
3220
+ class CooldownManager { consume(bucket, input, actor, now?); peek(...); reset(...); clear(); }
3221
+ // command({ cooldown: number | CooldownConfig }); new SpearClient({ cooldown }); client.cooldowns
3222
+ ```
3223
+
3224
+ ### Scheduled tasks — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/scheduler.md)
3225
+
3226
+ ```ts
3227
+ function task(config: { name: string; cron?: string; interval?: number; runOnStart?: boolean; run: (client: SpearClient) => Awaitable<void> }): ScheduledTask;
3228
+ function cron(expression: string): CronExpression; // .next(from?: Date): Date
3229
+ class TaskScheduler { add/remove/list/size/active/start/stop }
3230
+ // client.register(task(...)); client.schedule(config); client.scheduler
3231
+ ```
3232
+
3233
+ ### Prefix commands — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/prefix.md)
3234
+
3235
+ ```ts
3236
+ function prefixCommand(config: { name: string; aliases?: string[]; description?: string; cooldown?: CooldownInput; run: (ctx: PrefixContext) => Awaitable<R> }): PrefixCommand;
3237
+ class PrefixContext { message; commandName; args: string[]; rest: string; reply(content); send(content); }
3238
+ // new SpearClient({ prefix: "!" | string[] | { prefix, mention?, ignoreBots?, caseInsensitive? } }); client.prefix
3239
+ // reading others' content needs the privileged MessageContent intent (Intents.messages)
3240
+ ```
3241
+
3242
+ ### Usage tracking — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/usage.md)
3243
+
3244
+ ```ts
3245
+ interface UsageEvent { type: "command" | "prefix" | "component" | "event"; name: string; userId?; userTag?; guildId?; channelId?; detail?; timestamp: Date; }
3246
+ interface UsageStore { record(event): Awaitable<void>; all(): Awaitable<readonly UsageEvent[]>; }
3247
+ class MemoryUsageStore { record; all; size; byUser(id); clear; }
3248
+ class JsonFileUsageStore { constructor(path: string); record; all; }
3249
+ class UsageTracker { setStore(store); reportTo(channelId, format?); track(event); store; enabled; }
3250
+ // new SpearClient({ usage: { store?, channel?, format? } }); client.usage
3251
+ ```
3252
+
3253
+ ---
3254
+
3255
+ ## Added in 0.3
3256
+
3257
+ Driven by patterns repeated across long-running production bots: the role/
3258
+ permission checks, `.catch(() => null)` fetches, embed factories, pagination
3259
+ /confirm flows, mention/duration parsing, locks, config loaders and pluggable
3260
+ log/usage transports a real Discord bot ends up writing.
3261
+
3262
+ ### Embeds — preset replies
3263
+
3264
+ ```ts
3265
+ class Embeds { error(input); success(input); info(input); warn(input); build(level, input); }
3266
+ function createEmbeds(opts?): Embeds; // alias for new Embeds(opts)
3267
+ // SpearClient owns one as `client.embeds`; configure via the `embeds` option.
3268
+ // BaseContext gains ctx.success/info/warn/error (state-aware send) + replySuccess/replyInfo/replyWarn/replyError.
3269
+ ```
3270
+
3271
+ ### Guards — declarative preconditions
3272
+
3273
+ ```ts
3274
+ type Guard<TCtx extends GuardContext = GuardContext> = (ctx: TCtx) => Awaitable<GuardResult>;
3275
+ function denied(reason?: string): GuardResult;
3276
+ function guildOnly(reason?: string): Guard;
3277
+ function dmOnly(reason?: string): Guard;
3278
+ function requireAnyRole(roleIds: readonly string[], reason?: string): Guard;
3279
+ function requireAllRoles(roleIds: readonly string[], reason?: string): Guard;
3280
+ function requireOwner(ownerIds: readonly string[], reason?: string): Guard;
3281
+ function requireUserPermissions(permission: PermissionResolvable, reason?: string): Guard;
3282
+ function requireBotPermissions(permission: PermissionResolvable, reason?: string): Guard;
3283
+ function guard<TCtx>(predicate: Guard<TCtx>): Guard<TCtx>;
3284
+ // per-handler: command({ guards: [...] }), prefixCommand({ guards }), button({ guards }), userCommand({ guards }), ...
3285
+ // client-wide: new SpearClient({ guards: [...] })
3286
+ ```
3287
+
3288
+ ### Context-menu commands
3289
+
3290
+ ```ts
3291
+ function userCommand({ name, run: (ctx: UserContextMenuContext) => Awaitable<R>, guards?, cooldown? }): UserContextMenu;
3292
+ function messageCommand({ name, run: (ctx: MessageContextMenuContext) => Awaitable<R>, guards?, cooldown? }): MessageContextMenu;
3293
+ // SpearClient.deployAllCommands deploys slash + context menus in one PUT.
3294
+ ```
3295
+
3296
+ ### Prefix typed arguments
3297
+
3298
+ ```ts
3299
+ function prefixArgs(): PrefixArgsBuilder<{}>;
3300
+ // builder methods: .string/.integer/.number/.boolean/.snowflake/.duration/.rest
3301
+ // prefixCommand<TArgs>({ args: a => a.snowflake("target").duration("dur").rest("reason"), run: ctx => ctx.options }))
3302
+ ```
3303
+
3304
+ ### Pagination + Confirmation
3305
+
3306
+ ```ts
3307
+ function paginate<T>(interaction, items, { pageSize, render, user?, timeoutMs?, controls?, ephemeral? }): Promise<void>;
3308
+ function buildPaginatorPage<T>(items, page, options): Promise<{ payload; pages }>;
3309
+ function confirm(interaction, { title?, body, confirm?, cancel?, user?, timeoutMs?, ephemeral? }): Promise<{ confirmed, reason, interaction? }>;
3310
+ ```
3311
+
3312
+ ### Primitives
3313
+
3314
+ ```ts
3315
+ class KeyedLock { tryAcquire(key, ttl?); run(key, fn, { onBusy?, ttl? }); isHeld(key); forget(key); dispose(); }
3316
+ const safeFetch = { member, channel, message, user, guild, role, try }; // each returns T | null
3317
+ function withSafeTimeout<T>(p: Promise<T>, ms): Promise<T | null>;
3318
+ function formatDuration(ms, { locale?: "en" | "tr" | UnitLabels; largest?; units? }): string;
3319
+ function parseDuration(input: string): number | null;
3320
+ function discordTimestamp(date, style?: "t"|"T"|"d"|"D"|"f"|"F"|"R"): string;
3321
+ function relativeTimestamp(date): string;
3322
+ interface CacheStore { get; set; delete; has; increment; rateLimit; clear; }
3323
+ class MemoryCache implements CacheStore { /* TTL, counter, fixed-window rate limit */ }
3324
+ function loadConfig<T>({ file, parser?, schema?, encoding? }): T;
3325
+ function loadConfigAsync<T>(opts): Promise<T>;
3326
+ function lookup<K, V>(table, resourceName?): (key: K) => V;
3327
+ ```
3328
+
3329
+ ### Logger transports
3330
+
3331
+ ```ts
3332
+ new Logger({ level, transports: [consoleSink, jsonlSink("./logs/bot.jsonl"), webhookSink({ url, minLevel: "error" })] });
3333
+ function jsonlSink(path: string, { minLevel? }?): LogSink;
3334
+ function webhookSink({ url, minLevel?, username? }): LogSink;
3335
+ // Logger.addTransport(sink), setTransports([sinks])
3336
+ ```
3337
+
3338
+ ### Scheduler — one-shot + reconcile
3339
+
3340
+ ```ts
3341
+ client.scheduler.delay(name, ms, fn) -> { cancel(): boolean };
3342
+ client.scheduler.followUp(name, [10_000, 30_000, 60_000], (i) => ...) -> { cancel(): boolean };
3343
+ client.scheduler.reconcile("voice-sessions", async (client) => { /* once on ready */ });
3344
+ ```
3345
+
3346
+ ### Deploy diff + dry run
3347
+
3348
+ ```ts
3349
+ client.deployAllCommands({ guildId, dryRun: true }); // returns { skipped, body, reason: "dry-run" }
3350
+ client.deployAllCommands({ guildId, strategy: "diff" }); // skips PUT when remote matches
3351
+ client.deployAllCommands({ applicationId: "...", strategy: "diff" }); // explicit app id, no ready required
3352
+ ```
3353
+
3354
+ ### Usage outcome + duration
3355
+
3356
+ ```ts
3357
+ interface UsageEvent {
3358
+ type; name; userId; userTag; guildId; channelId; detail?;
3359
+ outcome?: "success" | "error";
3360
+ durationMs?: number;
3361
+ errorMessage?: string;
3362
+ options?: Record<string, string | number | boolean | null>;
3363
+ timestamp: Date;
3364
+ }
3365
+ ```
3366
+
3367
+ ---