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.
- package/.claude/skills/spearkit/SKILL.md +11 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +117 -6
- package/AGENTS.md +98 -1
- package/README.md +10 -3
- package/dist/index.cjs +599 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +524 -2
- package/dist/index.d.ts +524 -2
- package/dist/index.js +576 -19
- package/dist/index.js.map +1 -1
- package/docs/README.md +21 -12
- package/docs/api-reference.md +222 -34
- package/docs/auto-defer.md +74 -0
- package/docs/client.md +60 -22
- package/docs/collectors.md +65 -0
- package/docs/commands.md +5 -0
- package/docs/components.md +7 -0
- package/docs/context-menus.md +121 -0
- package/docs/context.md +94 -2
- package/docs/cooldown.md +2 -1
- package/docs/errors.md +73 -0
- package/docs/guards.md +146 -0
- package/docs/loading.md +7 -5
- package/docs/messages.md +35 -0
- package/docs/permissions.md +68 -0
- package/docs/prefix.md +54 -0
- package/docs/scheduler.md +26 -2
- package/docs/shutdown.md +42 -0
- package/docs/store.md +90 -0
- package/docs/usage.md +20 -10
- package/llms-full.txt +1337 -85
- package/llms.txt +91 -3
- package/package.json +1 -1
|
@@ -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.
|
|
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)
|
|
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
|
-
|
|
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"|
|
|
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 (
|
|
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/
|
|
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 ([
|
|
26
|
-
- **Context-menu commands** — `userCommand` / `messageCommand` with typed `targetUser` / `targetMessage` ([
|
|
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
|
|