spearkit 0.3.1 → 0.4.0

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.
@@ -62,6 +62,17 @@ await client.start(process.env.DISCORD_TOKEN); // falls back to DIS
62
62
  await client.deployCommands({ guildId: process.env.GUILD_ID }); // omit guildId → global (slow)
63
63
  ```
64
64
 
65
+ ## Pick the right tool
66
+
67
+ - **Slash command** → `command()`; **typed inputs** → `option.*`; **grouped** → `commandGroup` + `subcommand`; **type-ahead** → `option.string({ autocomplete })`.
68
+ - **Right-click on a user/message** → `userCommand` / `messageCommand`; **`!text` command** → `prefixCommand` (+ typed `args`).
69
+ - **Button** → `button`; **URL button** → `linkButton`; **dropdown** → `stringSelect`; **pick user/role/channel/mentionable** → `userSelect` / `roleSelect` / `channelSelect` / `mentionableSelect`; **form** → `modal` + `textInput`; **carry data** → custom-id `{param}`.
70
+ - **Paged list** → `paginate`; **yes/no gate** → `confirm`.
71
+ - **Reply** → `ctx.reply` / `replyEphemeral`; **>3s work** → `ctx.defer()` then `editReply`; **styled embed** → `ctx.success/error/info/warn`.
72
+ - **Gateway events** → `event(...)`; **rate-limit** → `cooldown`; **role/permission/owner gate** → guards; **cron/interval** → `task` / `client.schedule`; **logs** → `client.logger` + sinks; **usage tracking** → `usage`; **typed env / `.env`** → `env.*`.
73
+ - **Reusable bundle** → `definePlugin` + `client.use`; **file-per-handler** → `client.load`; **deploy** → `client.deployCommands` / `deployAllCommands`.
74
+ - **Primitives** — per-key lock → `KeyedLock`; null-safe fetch → `safeFetch.*`; durations → `formatDuration` / `parseDuration`; timestamps → `discordTimestamp`; cache / rate-limit → `MemoryCache`; config files → `loadConfig`.
75
+
65
76
  ## Recipes
66
77
 
67
78
  ### Slash command with typed options
@@ -4,10 +4,81 @@ Condensed map of every spearkit export. All importable from `"spearkit"` alongsi
4
4
  the full discord.js surface. For prose and edge cases, read the package's
5
5
  `llms-full.txt` or `docs/`.
6
6
 
7
+ ## Use cases — reach for
8
+
9
+ **Bot setup & lifecycle**
10
+
11
+ | Want to… | Reach for |
12
+ | --- | --- |
13
+ | Start a bot and connect | `new SpearClient({ intents })` + `await client.start(token)` |
14
+ | Choose gateway intents | `Intents.none / default / guilds / messages / all` |
15
+ | Wire up handlers | `client.register(...)`; one file per handler → `client.load(dir)` |
16
+ | Push commands to Discord | `client.deployCommands({ guildId })`; slash + menus, safe CI → `client.deployAllCommands({ strategy: "diff", dryRun })` |
17
+ | Package a reusable feature | `definePlugin(...)` + `client.use(...)` |
18
+ | Migrate an existing discord.js bot | import from `"spearkit"`, swap `Client` → `SpearClient` |
19
+
20
+ **Commands & input**
21
+
22
+ | Want to… | Reach for |
23
+ | --- | --- |
24
+ | A slash command | `command({ name, description, run })` |
25
+ | Typed inputs to a command | `options: { x: option.string/integer/number/boolean/user/channel/role/mentionable/attachment(...) }` |
26
+ | Group many commands under one name | `commandGroup` + `subcommand` / `subcommandGroup` |
27
+ | Suggest values while the user types | `option.string({ autocomplete })` |
28
+ | A right-click "Apps" action on a user/message | `userCommand` / `messageCommand` |
29
+ | A classic `!text` command | `prefixCommand(...)` + `new SpearClient({ prefix })` |
30
+ | Parse `!cmd` arguments into typed values | `args: (a) => a.snowflake().duration().rest()` → `ctx.options` |
31
+
32
+ **Interactivity (components)**
33
+
34
+ | Want to… | Reach for |
35
+ | --- | --- |
36
+ | A clickable button | `button({ id, run })` → `row(btn.build(...))` |
37
+ | A URL button (no handler) | `linkButton` |
38
+ | A dropdown of fixed options | `stringSelect` |
39
+ | Pick users / roles / channels / mentionables | `userSelect` / `roleSelect` / `channelSelect` / `mentionableSelect` |
40
+ | A form with text fields | `modal` + `textInput` |
41
+ | Carry data through a component | custom-id params `id: "x:{id}"` → `ctx.params.id` |
42
+ | A paged list with next/prev | `paginate(...)` |
43
+ | An "Are you sure?" yes/no gate | `confirm(...)` |
44
+
45
+ **Replies & UX**
46
+
47
+ | Want to… | Reach for |
48
+ | --- | --- |
49
+ | Reply, public or hidden | `ctx.reply(...)` / `ctx.replyEphemeral(...)` |
50
+ | Work that takes >3s | `ctx.defer()` then `ctx.editReply(...)` |
51
+ | A styled success/error/info/warn embed | `ctx.success/error/info/warn(...)` |
52
+ | "Reply, edit, or follow-up — whichever fits" | `ctx.send(...)` |
53
+
54
+ **Cross-cutting concerns**
55
+
56
+ | Want to… | Reach for |
57
+ | --- | --- |
58
+ | React to gateway events | `event(name, run)`; once on startup → `event("clientReady", ...)` |
59
+ | Rate-limit a command/handler | `cooldown` (per-command or client-wide) |
60
+ | Restrict by role / permission / owner / guild | guards: `requireAnyRole` / `requireUserPermissions` / `requireOwner` / `guildOnly` |
61
+ | Run jobs on cron or interval | `task({ cron \| interval })` / `client.schedule(...)` |
62
+ | Delay once / staged follow-ups / recover on restart | `client.scheduler.delay` / `followUp` / `reconcile` |
63
+ | Structured logs to file/webhook | `client.logger` + `consoleSink` / `jsonlSink` / `webhookSink` |
64
+ | Track who used what | `new SpearClient({ usage })` + `MemoryUsageStore` / `JsonFileUsageStore` |
65
+ | Read typed env / load `.env` | `env.string/number/boolean/require` (auto-loaded on `start()`) |
66
+
67
+ **Utilities (primitives)**
68
+
69
+ | Want to… | Reach for |
70
+ | --- | --- |
71
+ | Stop concurrent runs per key (e.g. per user) | `KeyedLock` |
72
+ | Fetch that returns `null` instead of throwing | `safeFetch.{member,channel,message,user,guild,role}` |
73
+ | Format/parse `"1h30m"` durations | `formatDuration` / `parseDuration` |
74
+ | Render `<t:…>` Discord timestamps | `discordTimestamp` / `relativeTimestamp` |
75
+ | In-memory cache / counters / rate-limit window | `MemoryCache` |
76
+ | Load JSON/JSON5/YAML config | `loadConfig` |
77
+
7
78
  ## Client
8
79
 
9
80
  ```ts
10
- new SpearClient(options?: Partial<ClientOptions>) // intents optional → Intents.default
81
+ new SpearClient(options?: Partial<ClientOptions> & SpearOptions) // intents optional → Intents.default; SpearOptions: logger/dotenv/cooldown/prefix/usage/embeds/guards
11
82
  client.commands // CommandRegistry
12
83
  client.events // EventRegistry
13
84
  client.components // ComponentRegistry
@@ -18,7 +89,9 @@ client.start(token?): Promise<this> // login; falls back to D
18
89
  client.deployCommands({ guildId? }): Promise<DeployResult> // after start()
19
90
  client.deployAllCommands({ guildId?, applicationId?, strategy?: "diff", dryRun? }) // slash + context menus
20
91
  client.schedule(taskConfig); client.scheduler // TaskScheduler
21
- client.cooldowns; client.prefix; client.usage; client.logger; client.embeds
92
+ client.enableGracefulShutdown({ onShutdown?, timeoutMs? }): () => void // clean SIGINT/SIGTERM teardown
93
+ client.cooldowns; client.prefix; client.usage; client.logger; client.embeds; client.contextMenus
94
+ new SpearClient({ autoDefer: true }) // default auto-defer for slash + context-menu handlers
22
95
 
23
96
  const Intents = { none: [], default: [Guilds], guilds: [Guilds, GuildMembers],
24
97
  messages: [Guilds, GuildMessages, MessageContent], all: /* every intent */ }
@@ -28,7 +101,7 @@ const Intents = { none: [], default: [Guilds], guilds: [Guilds, GuildMembers],
28
101
 
29
102
  ```ts
30
103
  command({ name, description, options?, defaultMemberPermissions?, nsfw?, guildOnly?,
31
- nameLocalizations?, descriptionLocalizations?, guards?, cooldown?, run })
104
+ nameLocalizations?, descriptionLocalizations?, guards?, cooldown?, autoDefer?, run })
32
105
  commandGroup({ name, description, subcommands?, groups?, defaultMemberPermissions?, nsfw?, guildOnly?, ... })
33
106
  subcommand({ description, options?, run, ... })
34
107
  subcommandGroup({ description, subcommands, ... })
@@ -85,10 +158,12 @@ event({ name, once?, run })
85
158
 
86
159
  ```ts
87
160
  reply(input) · replyEphemeral(input) · defer({ ephemeral? }) · editReply(input) ·
88
- followUp(input) · send(input) · error(message)
161
+ followUp(input) · send(input)
89
162
  success(input) · info(input) · warn(input) · error(input) // preset embeds, state-aware
90
163
  replySuccess/replyInfo/replyWarn/replyError(input)
91
164
  client · user · member · guild · guildId · channel · channelId · locale · deferred · replied
165
+ botPermissions · botMissing(perm) · userMissing(perm) // permission preflight (zero-fetch)
166
+ awaitMessageFrom(userId?, { time?, filter? }) · awaitModal(modal, { time?, filter? }) // → T | null
92
167
  // ReplyInput = string | (InteractionReplyOptions & { ephemeral?: boolean })
93
168
  ```
94
169
 
@@ -155,7 +230,7 @@ confirm(interaction, { body, title?, confirm?, cancel?, user?, timeoutMs?, ephem
155
230
 
156
231
  ```ts
157
232
  client.embeds.{ error|success|info|warn|build(level, input) }
158
- createEmbeds(opts?) // alias for new Embeds(opts)
233
+ new Embeds(opts?); defaultEmbeds // shared default used when client.embeds is unset
159
234
  new SpearClient({ embeds: { /* colors/icons per level */ } })
160
235
  // BaseContext exposes ctx.success/info/warn/error + replySuccess/Info/Warn/Error
161
236
  ```
@@ -201,7 +276,7 @@ client.load(dir, { extensions?: readonly string[], recursive? }) // defaults [.
201
276
  new KeyedLock(); lock.tryAcquire(key, ttl?); lock.run(key, fn, { onBusy?, ttl? }); lock.isHeld(key); lock.forget(key); lock.dispose()
202
277
  safeFetch.{ member, channel, message, user, guild, role, try } // each returns T | null
203
278
  withSafeTimeout(promise, ms) // T | null
204
- formatDuration(ms, { locale?: "en"|"tr"|UnitLabels, largest?, units? })
279
+ formatDuration(ms, { locale?: string | custom-labels, largest?, units? }) // "en"|"en-US"|"en-GB"|"tr"|"tr-TR" or a custom label set
205
280
  parseDuration(input): number | null
206
281
  discordTimestamp(date, style?: "t"|"T"|"d"|"D"|"f"|"F"|"R"); relativeTimestamp(date)
207
282
  new MemoryCache() // CacheStore: get/set/delete/has/increment/rateLimit/clear (TTL + counters + fixed-window rate limit)
@@ -209,6 +284,42 @@ loadConfig({ file, parser?, schema?, encoding? }); loadConfigAsync(opts) // JS
209
284
  lookup(table, resourceName?) -> (key) => value
210
285
  ```
211
286
 
287
+ ## Reliability, permissions, storage & collectors
288
+
289
+ ```ts
290
+ // Auto-defer — dodge "Unknown interaction" (10062) on slow handlers
291
+ command({ autoDefer: true | { ephemeral?, delayMs? } }); new SpearClient({ autoDefer: true })
292
+ armAutoDefer(interaction, config) -> cancel(); normalizeAutoDefer(input); DEFAULT_AUTO_DEFER_DELAY_MS = 2000
293
+
294
+ // Graceful shutdown
295
+ client.enableGracefulShutdown({ signals?, timeoutMs?, exit?, onShutdown?, logger? }) -> dispose()
296
+ gracefulShutdown(client, options) // standalone variant
297
+
298
+ // Permissions & moderation
299
+ missingPermissions(channel, who, required) -> PermissionsString[]; botMissingPermissions(channel, required)
300
+ hasPermissions(channel, who, required); compareRoles(a, b); canActOn(actor, target); formatPermissions(perm)
301
+ moderationCheck({ moderator, target, me?, action? }) -> { ok: true } | { ok: false, reason }
302
+
303
+ // Persistent storage + per-guild settings
304
+ new MemoryStore(); new JsonStore(path) // KeyValueStore: get/set/has/delete/keys/clear
305
+ namespaced(store, prefix)
306
+ createSettings({ store, defaults, namespace? }) -> { defaults, store, get(id), set(id, patch), reset(id) }
307
+
308
+ // Collectors (all resolve null on timeout)
309
+ awaitMessage(channel, { filter?, time? }); awaitComponent(message, { filter?, time?, componentType? })
310
+ showAndAwaitModal(interaction, modal, { time?, filter? })
311
+
312
+ // Discord errors
313
+ isDiscordError(err, DiscordErrorCode.UnknownMessage?); isHTTPError(err); isRateLimitError(err)
314
+ explainDiscordError(err) -> string | null; DiscordErrorCode.{ UnknownMessage, UnknownInteraction, MissingPermissions, ... }
315
+
316
+ // Message formatting
317
+ truncate(text, max, suffix?); chunkMessage(text, { max? }) -> string[]; MESSAGE_CHARACTER_LIMIT = 2000
318
+
319
+ // Dynamic prefixes
320
+ new SpearClient({ prefix: { prefix?, dynamic: (message) => string | string[] | null } })
321
+ ```
322
+
212
323
  ## Error handling
213
324
 
214
325
  ```ts
package/AGENTS.md CHANGED
@@ -32,6 +32,87 @@ logging, usage tracking and dotenv. Install: `npm install spearkit discord.js`.
32
32
  `T | undefined`, `choices` narrow to a literal union, custom-id `{param}`s and
33
33
  modal field keys are typed. Don't cast; don't annotate handler args.
34
34
 
35
+ ## Use cases — reach for
36
+
37
+ Map the task to the API. Patterns and signatures are below and in `docs/`.
38
+
39
+ **Bot setup & lifecycle**
40
+
41
+ | Want to… | Reach for |
42
+ | --- | --- |
43
+ | Start a bot and connect | `new SpearClient({ intents })` + `await client.start(token)` |
44
+ | Choose gateway intents | `Intents.none / default / guilds / messages / all` |
45
+ | Wire up handlers | `client.register(...)`; one file per handler → `client.load(dir)` |
46
+ | Push commands to Discord | `client.deployCommands({ guildId })`; slash + context menus, safe CI → `client.deployAllCommands({ strategy: "diff", dryRun })` |
47
+ | Package a reusable feature | `definePlugin(...)` + `client.use(...)` |
48
+ | Migrate an existing discord.js bot | import from `"spearkit"`, swap `Client` → `SpearClient` |
49
+
50
+ **Commands & input**
51
+
52
+ | Want to… | Reach for |
53
+ | --- | --- |
54
+ | A slash command | `command({ name, description, run })` |
55
+ | Typed inputs to a command | `options: { x: option.string/integer/number/boolean/user/channel/role/mentionable/attachment(...) }` |
56
+ | Group many commands under one name | `commandGroup` + `subcommand` / `subcommandGroup` |
57
+ | Suggest values while the user types | `option.string({ autocomplete })` |
58
+ | A right-click "Apps" action on a user/message | `userCommand` / `messageCommand` |
59
+ | A classic `!text` command | `prefixCommand(...)` + `new SpearClient({ prefix })` |
60
+ | Parse `!cmd` arguments into typed values | `args: (a) => a.snowflake().duration().rest()` → `ctx.options` |
61
+ | Avoid `Unknown interaction` (10062) on slow work | `command({ autoDefer: true })` / `new SpearClient({ autoDefer: true })` |
62
+
63
+ **Interactivity (components)**
64
+
65
+ | Want to… | Reach for |
66
+ | --- | --- |
67
+ | A clickable button | `button({ id, run })` → `row(btn.build(...))` |
68
+ | A URL button (no handler) | `linkButton` |
69
+ | A dropdown of fixed options | `stringSelect` |
70
+ | Pick users / roles / channels / mentionables | `userSelect` / `roleSelect` / `channelSelect` / `mentionableSelect` |
71
+ | A form with text fields | `modal` + `textInput` |
72
+ | Carry data through a component | custom-id params `id: "x:{id}"` → `ctx.params.id` |
73
+ | A paged list with next/prev | `paginate(...)` |
74
+ | An "Are you sure?" yes/no gate | `confirm(...)` |
75
+
76
+ **Replies & UX**
77
+
78
+ | Want to… | Reach for |
79
+ | --- | --- |
80
+ | Reply, public or hidden | `ctx.reply(...)` / `ctx.replyEphemeral(...)` |
81
+ | Work that takes >3s | `ctx.defer()` then `ctx.editReply(...)` |
82
+ | A styled success/error/info/warn embed | `ctx.success/error/info/warn(...)` |
83
+ | "Reply, edit, or follow-up — whichever fits" | `ctx.send(...)` |
84
+
85
+ **Cross-cutting concerns**
86
+
87
+ | Want to… | Reach for |
88
+ | --- | --- |
89
+ | React to gateway events | `event(name, run)`; once on startup → `event("clientReady", ...)` |
90
+ | Rate-limit a command/handler | `cooldown` (per-command or client-wide) |
91
+ | Restrict by role / permission / owner / guild | guards: `requireAnyRole` / `requireUserPermissions` / `requireOwner` / `guildOnly` |
92
+ | Run jobs on cron or interval | `task({ cron \| interval })` / `client.schedule(...)` |
93
+ | Delay once / staged follow-ups / recover on restart | `client.scheduler.delay` / `followUp` / `reconcile` |
94
+ | Structured logs to file/webhook | `client.logger` + `consoleSink` / `jsonlSink` / `webhookSink` |
95
+ | Track who used what | `new SpearClient({ usage })` + `MemoryUsageStore` / `JsonFileUsageStore` |
96
+ | Read typed env / load `.env` | `env.string/number/boolean/require` (auto-loaded on `start()`) |
97
+ | Shut down cleanly on SIGINT/SIGTERM | `client.enableGracefulShutdown({ onShutdown })` |
98
+ | Permission / role-hierarchy preflight | `moderationCheck(...)`, `missingPermissions(...)`, `canActOn(...)`, `ctx.botMissing(...)` |
99
+ | Wait for a reply / click / modal submission | `ctx.awaitMessageFrom(...)` / `ctx.awaitModal(...)` / `awaitComponent(...)` |
100
+ | Branch on a Discord API error | `isDiscordError(err, DiscordErrorCode.X)` / `explainDiscordError(err)` |
101
+ | Per-guild prefix from a store | `prefix: { dynamic: (message) => ... }` |
102
+
103
+ **Utilities (primitives)**
104
+
105
+ | Want to… | Reach for |
106
+ | --- | --- |
107
+ | Stop concurrent runs per key (e.g. per user) | `KeyedLock` |
108
+ | Fetch that returns `null` instead of throwing | `safeFetch.{member,channel,message,user,guild,role}` |
109
+ | Format/parse `"1h30m"` durations | `formatDuration` / `parseDuration` |
110
+ | Render `<t:…>` Discord timestamps | `discordTimestamp` / `relativeTimestamp` |
111
+ | In-memory cache / counters / rate-limit window | `MemoryCache` |
112
+ | Load JSON/JSON5/YAML config | `loadConfig` |
113
+ | Persist key-value data / per-guild settings | `MemoryStore` / `JsonStore` + `createSettings({ store, defaults })` |
114
+ | Split text to Discord's 2000-char limit | `chunkMessage(text)` / `truncate(text, max)` |
115
+
35
116
  ## Canonical patterns
36
117
 
37
118
  ### Client + lifecycle
@@ -114,7 +195,7 @@ modals add `ctx.fields`.
114
195
  `ctx.deferred/replied`. For hidden replies prefer `ctx.replyEphemeral(...)` or
115
196
  `ctx.reply({ content, ephemeral: true })` — spearkit normalizes it.
116
197
 
117
- ## Subsystems (each has a guide in docs/)
198
+ ## Subsystems (most have a dedicated guide in docs/; all are in the API reference)
118
199
 
119
200
  - **Guards** — `guards: [...]` on `command`/`prefixCommand`/`button`/`userCommand`/
120
201
  `messageCommand`, or client-wide `new SpearClient({ guards })`. Helpers:
@@ -145,6 +226,22 @@ modals add `ctx.fields`.
145
226
  - **Primitives** — `KeyedLock`, `safeFetch.{member,channel,message,user,guild,role,try}`,
146
227
  `formatDuration`/`parseDuration`/`discordTimestamp`/`relativeTimestamp`,
147
228
  `MemoryCache`, `loadConfig`.
229
+ - **Auto-defer** — `command({ autoDefer: true | { ephemeral?, delayMs? } })` or
230
+ `new SpearClient({ autoDefer: true })`; auto-`deferReply()` before Discord's 3s
231
+ window so slow handlers don't 10062. Respond via `ctx.send`/`ctx.editReply`.
232
+ - **Graceful shutdown** — `client.enableGracefulShutdown({ onShutdown, timeoutMs? })`
233
+ (or `gracefulShutdown(client, ...)`); clean `SIGINT`/`SIGTERM` teardown.
234
+ - **Permissions & moderation** — `missingPermissions`, `botMissingPermissions`,
235
+ `hasPermissions`, `compareRoles`, `canActOn`, `moderationCheck`, `formatPermissions`;
236
+ on context: `ctx.botPermissions`, `ctx.botMissing(...)`, `ctx.userMissing(...)`.
237
+ - **Persistent storage** — `MemoryStore`/`JsonStore` (`KeyValueStore`), `namespaced(...)`,
238
+ and typed per-guild `createSettings({ store, defaults, namespace? })`.
239
+ - **Collectors** — `ctx.awaitMessageFrom(...)`, `ctx.awaitModal(...)`, plus standalone
240
+ `awaitMessage`, `awaitComponent`, `showAndAwaitModal` (all resolve `null` on timeout).
241
+ - **Discord errors** — `isDiscordError(err, DiscordErrorCode.X)`, `isHTTPError`,
242
+ `isRateLimitError`, `explainDiscordError`; named `DiscordErrorCode` map.
243
+ - **Message formatting** — `chunkMessage(text)` (2000-char-safe split), `truncate(text, max)`,
244
+ `MESSAGE_CHARACTER_LIMIT`.
148
245
 
149
246
  ## Common mistakes to avoid
150
247
 
package/README.md CHANGED
@@ -15,15 +15,15 @@ npm install spearkit discord.js
15
15
  ## Batteries included
16
16
 
17
17
  - **Type-safe slash commands**, options, subcommands, autocomplete, buttons, selects and modals — no `interactionCreate` switch.
18
- - **Cooldowns** — per user/role/guild/channel, with exemptions and per-role/per-user overrides ([guide](./docs/cooldown.md)).
18
+ - **Cooldowns** — per user/guild/channel/global, with per-role/per-user exemptions and overrides ([guide](./docs/cooldown.md)).
19
19
  - **Scheduled tasks** — cron and interval jobs, started on ready ([guide](./docs/scheduler.md)).
20
20
  - **Prefix commands** — classic `!text` commands that share cooldowns ([guide](./docs/prefix.md)).
21
21
  - **Structured logging** — leveled, scoped, pluggable; every error flows through it ([guide](./docs/logging.md)).
22
22
  - **Usage tracking** — record who used what to a database and/or a Discord channel ([guide](./docs/usage.md)).
23
23
  - **dotenv built in** — auto-load `.env` and read typed env vars ([guide](./docs/env.md)).
24
24
  - **Plugins & file-based loading** for organising larger bots.
25
- - **Guards** — declarative `requireAnyRole`/`requireUserPermissions`/`guildOnly`/`requireOwner` preconditions on commands, components and prefix commands ([API ref](./docs/api-reference.md#guards--declarative-preconditions)).
26
- - **Context-menu commands** — `userCommand` / `messageCommand` with typed `targetUser` / `targetMessage` ([API ref](./docs/api-reference.md#context-menu-commands)).
25
+ - **Guards** — declarative `requireAnyRole`/`requireUserPermissions`/`guildOnly`/`requireOwner` preconditions on commands, components and prefix commands ([guide](./docs/guards.md)).
26
+ - **Context-menu commands** — `userCommand` / `messageCommand` with typed `targetUser` / `targetMessage` ([guide](./docs/context-menus.md)).
27
27
  - **Preset embeds** — `ctx.success/info/warn/error` and `client.embeds` factory with configurable colors/icons ([API ref](./docs/api-reference.md#embeds--preset-replies)).
28
28
  - **Pagination & confirmation** — `paginate(...)` and `confirm(...)` button flows with user-only filter and timeout.
29
29
  - **Typed prefix args** — `prefixCommand({ args: a => a.snowflake("target").duration("d").rest("reason"), run: ctx => ctx.options })`.
@@ -31,6 +31,13 @@ npm install spearkit discord.js
31
31
  - **Logger transports** — multi-sink (`consoleSink`, `jsonlSink`, `webhookSink`); per-level routing.
32
32
  - **Scheduler extras** — `scheduler.delay/followUp/reconcile` for one-shot jobs and on-ready recovery.
33
33
  - **Deploy strategy** — `deployAllCommands({ dryRun, strategy: "diff" })` for safe CI deploys.
34
+ - **Auto-defer** — `command({ autoDefer: true })` / `new SpearClient({ autoDefer: true })` to dodge `Unknown interaction` (10062) on slow handlers ([API ref](./docs/api-reference.md#auto-defer)).
35
+ - **Graceful shutdown** — `client.enableGracefulShutdown({ onShutdown })` for clean `SIGINT`/`SIGTERM` teardown ([API ref](./docs/api-reference.md#graceful-shutdown)).
36
+ - **Permissions & moderation** — `moderationCheck`, `missingPermissions`, `canActOn`, `ctx.botMissing(...)` role-hierarchy/permission preflights ([API ref](./docs/api-reference.md#permissions--moderation)).
37
+ - **Persistent storage** — `MemoryStore`/`JsonStore` key-value stores + typed per-guild `createSettings(...)` ([API ref](./docs/api-reference.md#persistent-storage)).
38
+ - **Collectors** — `ctx.awaitMessageFrom(...)`, `ctx.awaitModal(...)`, `awaitComponent(...)` without hand-rolled collectors ([API ref](./docs/api-reference.md#collectors)).
39
+ - **Discord error helpers** — `isDiscordError(err, DiscordErrorCode.UnknownMessage)`, `explainDiscordError(...)` ([API ref](./docs/api-reference.md#discord-errors)).
40
+ - **Dynamic prefixes** — per-guild prefix resolution via `prefix: { dynamic }` ([guide](./docs/prefix.md#dynamic-per-guild-prefixes)).
34
41
 
35
42
  ## Documentation
36
43