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
package/docs/README.md
CHANGED
|
@@ -12,18 +12,27 @@ components.
|
|
|
12
12
|
3. [Commands](./commands.md) — slash commands, subcommands, permissions, deployment.
|
|
13
13
|
4. [Options](./options.md) — typed option builders, choices, autocomplete.
|
|
14
14
|
5. [Components](./components.md) — buttons, selects, modals, custom-id routing.
|
|
15
|
-
6. [
|
|
16
|
-
7. [
|
|
17
|
-
8. [
|
|
18
|
-
9. [
|
|
19
|
-
10. [
|
|
20
|
-
11. [
|
|
21
|
-
12. [
|
|
22
|
-
13. [
|
|
23
|
-
14. [
|
|
24
|
-
15. [
|
|
25
|
-
16. [
|
|
26
|
-
17. [
|
|
15
|
+
6. [Context menus](./context-menus.md) — user and message "Apps" commands.
|
|
16
|
+
7. [Events](./events.md) — the `event()` helper and the event registry.
|
|
17
|
+
8. [Contexts](./context.md) — reply helpers shared by every handler.
|
|
18
|
+
9. [Guards](./guards.md) — role/permission/owner/guild preconditions.
|
|
19
|
+
10. [Auto-defer](./auto-defer.md) — beat the 3-second `Unknown interaction` error.
|
|
20
|
+
11. [Permissions & hierarchy](./permissions.md) — moderation preflight checks.
|
|
21
|
+
12. [Discord API errors](./errors.md) — recognise and recover from `DiscordAPIError`.
|
|
22
|
+
13. [Cooldowns](./cooldown.md) — per-user/role/guild rate limiting.
|
|
23
|
+
14. [Scheduled tasks](./scheduler.md) — cron and interval jobs.
|
|
24
|
+
15. [Prefix commands](./prefix.md) — classic `!text` commands.
|
|
25
|
+
16. [Collectors](./collectors.md) — await messages, modals and component clicks.
|
|
26
|
+
17. [Key-value store & settings](./store.md) — persist per-guild config + dynamic prefix.
|
|
27
|
+
18. [Messages & limits](./messages.md) — split long output, truncate text.
|
|
28
|
+
19. [Logging](./logging.md) — structured, leveled, scoped logging.
|
|
29
|
+
20. [Usage tracking](./usage.md) — record who used what (store + Discord channel).
|
|
30
|
+
21. [Environment & dotenv](./env.md) — load `.env` and read typed env vars.
|
|
31
|
+
22. [Graceful shutdown](./shutdown.md) — close cleanly on `SIGINT`/`SIGTERM`.
|
|
32
|
+
23. [Plugins](./plugins.md) — bundling features into reusable units.
|
|
33
|
+
24. [File-based loading](./loading.md) — one file per command/event/component.
|
|
34
|
+
25. [Migrating from discord.js](./migration.md) — the drop-in path.
|
|
35
|
+
26. [API reference](./api-reference.md) — every exported symbol.
|
|
27
36
|
|
|
28
37
|
## Why spearkit
|
|
29
38
|
|
package/docs/api-reference.md
CHANGED
|
@@ -24,17 +24,39 @@ new SpearClient(options?: SpearClientOptions)
|
|
|
24
24
|
| `commands` | `CommandRegistry` | Slash command registry + dispatcher. |
|
|
25
25
|
| `events` | `EventRegistry` | Event listener registry. |
|
|
26
26
|
| `components` | `ComponentRegistry` | Button/select/modal router. |
|
|
27
|
+
| `logger` | `Logger` | Structured logger (`client.logger.child(scope)` for sub-scopes). |
|
|
28
|
+
| `cooldowns` | `CooldownManager` | Shared cooldown manager (also used by prefix commands). |
|
|
29
|
+
| `scheduler` | `TaskScheduler` | Cron / interval task scheduler. |
|
|
30
|
+
| `prefix` | `PrefixRegistry` | Prefix (text) command registry. |
|
|
31
|
+
| `usage` | `UsageTracker` | Usage tracker — records who used what. |
|
|
32
|
+
| `embeds` | `Embeds` | Preset embed factory behind `ctx.success/error/...`. |
|
|
33
|
+
| `contextMenus` | `ContextMenuRegistry` | User / message context-menu registry. |
|
|
27
34
|
| `register(...items: Registerable[])` | `this` | Route each item to the matching registry. |
|
|
28
35
|
| `use(...plugins: SpearPlugin[])` | `Promise<this>` | Run each plugin's `setup`. |
|
|
29
36
|
| `load(dir: string, options?: LoadOptions)` | `Promise<number>` | Import a directory and register its exports. Returns count. |
|
|
30
37
|
| `start(token?: string)` | `Promise<this>` | Log in (falls back to `DISCORD_TOKEN`). |
|
|
31
38
|
| `deployCommands(options?: { guildId?: string })` | `Promise<DeployResult>` | Push commands using the client's REST. Call after ready. |
|
|
39
|
+
| `deployAllCommands(options?)` | `Promise<DeployResult \| { skipped: true; reason; body }>` | Deploy slash + context menus together; supports `dryRun` and `strategy: "diff"`. |
|
|
40
|
+
| `schedule(config: TaskConfig)` | `ScheduledTask` | Define and register a scheduled task in one call. |
|
|
41
|
+
| `enableGracefulShutdown(options?: GracefulShutdownOptions)` | `() => void` | Tear down cleanly on `SIGINT`/`SIGTERM`; returns a disposer. |
|
|
32
42
|
|
|
33
43
|
Inherits everything from discord.js `Client` (`on`, `once`, `login`, `ws`, `rest`, `application`, `user`, …).
|
|
34
44
|
|
|
35
|
-
### `type SpearClientOptions = Partial<ClientOptions
|
|
45
|
+
### `type SpearClientOptions = Partial<ClientOptions> & SpearOptions`
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
discord.js `ClientOptions` (with `intents` optional — it defaults to
|
|
48
|
+
`Intents.default`) intersected with spearkit's own options (`SpearOptions`):
|
|
49
|
+
|
|
50
|
+
| Option | Type | Configures |
|
|
51
|
+
| ------ | ---- | ---------- |
|
|
52
|
+
| `logger` | `Logger \| LoggerOptions` | The `client.logger`. |
|
|
53
|
+
| `dotenv` | `boolean \| LoadEnvOptions` | Auto-load `.env` on `start()` (default `true`). |
|
|
54
|
+
| `cooldown` | `CooldownInput` | Default cooldown applied to every command. |
|
|
55
|
+
| `prefix` | `string \| readonly string[] \| PrefixOptions` | Enable prefix commands. |
|
|
56
|
+
| `usage` | `UsageOptions` | Usage-tracking store and/or channel. |
|
|
57
|
+
| `embeds` | `Embeds \| EmbedsOptions` | Preset embed factory. |
|
|
58
|
+
| `guards` | `readonly Guard[]` | Default guards run before every handler. |
|
|
59
|
+
| `autoDefer` | `AutoDeferInput` | Default auto-defer for slash + context-menu handlers. |
|
|
38
60
|
|
|
39
61
|
### `const Intents`
|
|
40
62
|
|
|
@@ -48,7 +70,7 @@ Ready-made intent presets (arrays of `GatewayIntentBits`).
|
|
|
48
70
|
| `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
|
|
49
71
|
| `Intents.all` | Every intent (includes privileged). |
|
|
50
72
|
|
|
51
|
-
### `type Registerable = SlashCommand | EventDef | ComponentDef`
|
|
73
|
+
### `type Registerable = SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand | ContextMenuCommand`
|
|
52
74
|
|
|
53
75
|
The union accepted by `SpearClient.register`.
|
|
54
76
|
|
|
@@ -70,6 +92,9 @@ interface CommandConfig<O extends OptionMap, R> {
|
|
|
70
92
|
guildOnly?: boolean;
|
|
71
93
|
nameLocalizations?: LocalizationMap;
|
|
72
94
|
descriptionLocalizations?: LocalizationMap;
|
|
95
|
+
cooldown?: CooldownInput;
|
|
96
|
+
guards?: readonly Guard[];
|
|
97
|
+
autoDefer?: AutoDeferInput;
|
|
73
98
|
run: (ctx: CommandContext<O>) => Awaitable<R>;
|
|
74
99
|
}
|
|
75
100
|
```
|
|
@@ -89,6 +114,9 @@ interface CommandGroupConfig {
|
|
|
89
114
|
guildOnly?: boolean;
|
|
90
115
|
nameLocalizations?: LocalizationMap;
|
|
91
116
|
descriptionLocalizations?: LocalizationMap;
|
|
117
|
+
cooldown?: CooldownInput;
|
|
118
|
+
guards?: readonly Guard[];
|
|
119
|
+
autoDefer?: AutoDeferInput;
|
|
92
120
|
}
|
|
93
121
|
```
|
|
94
122
|
|
|
@@ -124,6 +152,9 @@ interface SubcommandGroupConfig {
|
|
|
124
152
|
| `toJSON()` | `RESTPostAPIChatInputApplicationCommandsJSONBody` | REST payload. |
|
|
125
153
|
| `execute(interaction)` | `Promise<void>` | Run for a chat-input interaction. |
|
|
126
154
|
| `autocomplete(interaction)` | `Promise<void>` | Run autocomplete for the focused option. |
|
|
155
|
+
| `cooldown` | `CooldownConfig \| undefined` | Resolved cooldown, when set. |
|
|
156
|
+
| `guards` | `readonly Guard[] \| undefined` | Guards run before `execute`. |
|
|
157
|
+
| `autoDefer` | `AutoDeferConfig \| undefined` | Resolved auto-defer config, when set. |
|
|
127
158
|
|
|
128
159
|
### `class CommandContext<O> extends BaseContext<ChatInputCommandInteraction>`
|
|
129
160
|
|
|
@@ -133,6 +164,7 @@ interface SubcommandGroupConfig {
|
|
|
133
164
|
| `commandName` | `string` | Invoked command name. |
|
|
134
165
|
| `subcommand` | `string \| null` | Invoked subcommand, if any. |
|
|
135
166
|
| `showModal(modal)` | `Promise<void>` | Present a modal. |
|
|
167
|
+
| `awaitModal(modal, options?)` | `Promise<ModalSubmitInteraction \| null>` | Show a modal and await its submission (scoped to this user). |
|
|
136
168
|
|
|
137
169
|
Plus all `BaseContext` members.
|
|
138
170
|
|
|
@@ -151,6 +183,10 @@ Plus all `BaseContext` members.
|
|
|
151
183
|
| `handle(interaction)` | `Promise<void>` | Dispatch a chat-input interaction. |
|
|
152
184
|
| `handleAutocomplete(interaction)` | `Promise<void>` | Dispatch an autocomplete interaction. |
|
|
153
185
|
| `deploy(options: DeployOptions)` | `Promise<DeployResult>` | Push commands to discord. |
|
|
186
|
+
| `setLogger(logger: Logger)` | `this` | Attach a debug logger for dispatch tracing. |
|
|
187
|
+
| `setCooldowns(manager: CooldownManager, default?: CooldownConfig)` | `this` | Wire a shared cooldown manager and optional default. |
|
|
188
|
+
| `setDefaultGuards(guards: readonly Guard[])` | `this` | Guards run before each command's own guards. |
|
|
189
|
+
| `setUsageHook(hook: (event: UsageEvent) => void)` | `this` | Called after each dispatch (success or error). |
|
|
154
190
|
|
|
155
191
|
```ts
|
|
156
192
|
type CommandErrorHandler = (error: Error, interaction: ChatInputCommandInteraction) => Awaitable<void>;
|
|
@@ -288,6 +324,7 @@ interface ButtonConfig<P extends string, R> {
|
|
|
288
324
|
style?: ButtonStyleInput; // "Primary" | "Secondary" | "Success" | "Danger" | ButtonStyle.*
|
|
289
325
|
emoji?: ComponentEmojiResolvable;
|
|
290
326
|
disabled?: boolean;
|
|
327
|
+
guards?: readonly Guard[];
|
|
291
328
|
run: (ctx: ButtonContext<Params<P>>) => Awaitable<R>;
|
|
292
329
|
}
|
|
293
330
|
|
|
@@ -297,11 +334,13 @@ interface StringSelectConfig<P extends string, R> {
|
|
|
297
334
|
id: P;
|
|
298
335
|
options: readonly SelectMenuComponentOptionData[];
|
|
299
336
|
placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
|
|
337
|
+
guards?: readonly Guard[];
|
|
300
338
|
run: (ctx: StringSelectContext<Params<P>>) => Awaitable<R>;
|
|
301
339
|
}
|
|
302
340
|
|
|
303
341
|
interface EntitySelectConfig<P extends string> {
|
|
304
342
|
id: P; placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
|
|
343
|
+
guards?: readonly Guard[];
|
|
305
344
|
}
|
|
306
345
|
// user/role/mentionable selects take EntitySelectConfig & { run };
|
|
307
346
|
// channelSelect additionally takes { channelTypes?: readonly ChannelType[] }.
|
|
@@ -316,6 +355,7 @@ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>,
|
|
|
316
355
|
id: P;
|
|
317
356
|
title: string;
|
|
318
357
|
fields: F;
|
|
358
|
+
guards?: readonly Guard[];
|
|
319
359
|
run: (ctx: ModalContext<Params<P>, keyof F & string>) => Awaitable<R>;
|
|
320
360
|
}
|
|
321
361
|
```
|
|
@@ -324,7 +364,7 @@ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>,
|
|
|
324
364
|
|
|
325
365
|
| Class | Extra members |
|
|
326
366
|
| ----- | ------------- |
|
|
327
|
-
| `MessageComponentContext<P, I>` | `params`, `customId`, `message`, `update(input)`, `deferUpdate()`, `showModal(modal)` (+ BaseContext) |
|
|
367
|
+
| `MessageComponentContext<P, I>` | `params`, `customId`, `message`, `update(input)`, `deferUpdate()`, `showModal(modal)`, `awaitModal(modal, options?)` (+ BaseContext) |
|
|
328
368
|
| `ButtonContext<P>` | — |
|
|
329
369
|
| `StringSelectContext<P>` | `values: string[]`, `value: string \| undefined` |
|
|
330
370
|
| `UserSelectContext<P>` | `values`, `users`, `members` |
|
|
@@ -341,6 +381,9 @@ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>,
|
|
|
341
381
|
| `onError(handler: ComponentErrorHandler)` | `this` | Set the error handler. |
|
|
342
382
|
| `size` | `number` | Count. |
|
|
343
383
|
| `handle(interaction: Interaction)` | `Promise<boolean>` | Route an interaction; `true` if matched. |
|
|
384
|
+
| `setLogger(logger: Logger)` | `this` | Debug logger for dispatch tracing. |
|
|
385
|
+
| `setUsageHook(hook: (event: UsageEvent) => void)` | `this` | Called after each component run (success or error). |
|
|
386
|
+
| `setDefaultGuards(guards: readonly Guard[])` | `this` | Guards run before each component's own guards. |
|
|
344
387
|
|
|
345
388
|
```ts
|
|
346
389
|
type ComponentErrorHandler = (error: Error, interaction: RepliableInteraction) => Awaitable<void>;
|
|
@@ -380,7 +423,14 @@ The base for every interaction context.
|
|
|
380
423
|
| `editReply(input)` | `Promise<Message>` | Edit the response. |
|
|
381
424
|
| `followUp(input)` | `Promise<Message>` | Additional message. |
|
|
382
425
|
| `send(input)` | `Promise<void>` | State-aware reply/edit/followUp. |
|
|
383
|
-
| `error(
|
|
426
|
+
| `error(input, options?)` | `Promise<void>` | State-aware preset error embed; defaults to ephemeral (pass `{ ephemeral: false }` to override). |
|
|
427
|
+
| `success` / `info` / `warn` `(input, options?)` | `Promise<void>` | State-aware preset embeds (green / blue / yellow). |
|
|
428
|
+
| `replyError(input, options?)` | `Promise<InteractionResponse>` | Initial-reply error embed; defaults to ephemeral. |
|
|
429
|
+
| `replySuccess` / `replyInfo` / `replyWarn` `(input, options?)` | `Promise<InteractionResponse>` | Initial-reply preset embeds. |
|
|
430
|
+
| `botPermissions` | `Readonly<PermissionsBitField>` | The bot's resolved permissions in the channel (zero-fetch). |
|
|
431
|
+
| `botMissing(required)` | `PermissionsString[]` | Permission names the bot is missing here. |
|
|
432
|
+
| `userMissing(required)` | `PermissionsString[]` | Permission names the invoking user is missing here. |
|
|
433
|
+
| `awaitMessageFrom(userId?, options?)` | `Promise<Message \| null>` | Wait for the next message from a user in this channel. |
|
|
384
434
|
|
|
385
435
|
```ts
|
|
386
436
|
type ReplyData = InteractionReplyOptions & { ephemeral?: boolean };
|
|
@@ -415,12 +465,12 @@ function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Prom
|
|
|
415
465
|
## Added in 0.2
|
|
416
466
|
|
|
417
467
|
New subsystems, each with a dedicated guide. The `SpearClient` options
|
|
418
|
-
`{ logger?, dotenv?, cooldown?, prefix?, usage? }` configure them.
|
|
468
|
+
`{ logger?, dotenv?, cooldown?, prefix?, usage?, embeds?, guards? }` configure them.
|
|
419
469
|
|
|
420
470
|
### Logging — [guide](./logging.md)
|
|
421
471
|
|
|
422
472
|
```ts
|
|
423
|
-
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; }
|
|
473
|
+
class Logger { log(level, message, options?): void; debug/info/warn/error(message: string, options?: { error?: Error; data?: Record<string, LogValue> }): void; child(scope: string): Logger; setLevel(level: LogThreshold): this; enabled(level: LogLevel): boolean; addTransport(sink): this; setTransports(sinks): this; }
|
|
424
474
|
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
425
475
|
type LogThreshold = LogLevel | "silent";
|
|
426
476
|
function consoleSink(entry: LogEntry): void;
|
|
@@ -442,6 +492,13 @@ const env: { string(k, fallback?); number(k, fallback?); boolean(k, fallback?);
|
|
|
442
492
|
```ts
|
|
443
493
|
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); }
|
|
444
494
|
class CooldownManager { consume(bucket, input, actor, now?); peek(...); reset(...); clear(); }
|
|
495
|
+
type CooldownInput = number | CooldownConfig; // a bare ms duration, or a full config
|
|
496
|
+
type CooldownScope = "user" | "guild" | "channel" | "global";
|
|
497
|
+
type CooldownResult = { allowed: true } | { allowed: false; remaining: number };
|
|
498
|
+
interface CooldownActor { userId; roleIds; guildId; channelId; } // also: CooldownExemptions, CooldownOverrides
|
|
499
|
+
function normalizeCooldown(input: CooldownInput): CooldownConfig;
|
|
500
|
+
function effectiveDuration(config: CooldownConfig, actor: CooldownActor): number | null; // null = exempt
|
|
501
|
+
function formatCooldownMessage(config: CooldownConfig, remainingMs: number): string;
|
|
445
502
|
// command({ cooldown: number | CooldownConfig }); new SpearClient({ cooldown }); client.cooldowns
|
|
446
503
|
```
|
|
447
504
|
|
|
@@ -450,15 +507,15 @@ class CooldownManager { consume(bucket, input, actor, now?); peek(...); reset(..
|
|
|
450
507
|
```ts
|
|
451
508
|
function task(config: { name: string; cron?: string; interval?: number; runOnStart?: boolean; run: (client: SpearClient) => Awaitable<void> }): ScheduledTask;
|
|
452
509
|
function cron(expression: string): CronExpression; // .next(from?: Date): Date
|
|
453
|
-
class TaskScheduler { add/remove/list/size/active/start/stop }
|
|
510
|
+
class TaskScheduler { add/remove/list/size/active/start/stop/setLogger; delay/followUp/reconcile (see "Scheduler — one-shot + reconcile") }
|
|
454
511
|
// client.register(task(...)); client.schedule(config); client.scheduler
|
|
455
512
|
```
|
|
456
513
|
|
|
457
514
|
### Prefix commands — [guide](./prefix.md)
|
|
458
515
|
|
|
459
516
|
```ts
|
|
460
|
-
function prefixCommand(config: { name: string; aliases?: string[]; description?: string; cooldown?: CooldownInput; run: (ctx: PrefixContext) => Awaitable<R> }): PrefixCommand;
|
|
461
|
-
class PrefixContext { message; commandName; args: string[]; rest: string; reply(content); send(content); }
|
|
517
|
+
function prefixCommand<TArgs, R>(config: { name: string; aliases?: readonly string[]; description?: string; cooldown?: CooldownInput; guards?: readonly Guard[]; args?: (a: PrefixArgsBuilder<{}>) => PrefixArgsBuilder<TArgs>; run: (ctx: PrefixContext<TArgs>) => Awaitable<R> }): PrefixCommand;
|
|
518
|
+
class PrefixContext<TArgs> { message; commandName; args: string[]; rest: string; options: TArgs; client; author; member; guild; guildId; channel; channelId; reply(content); send(content); }
|
|
462
519
|
// new SpearClient({ prefix: "!" | string[] | { prefix, mention?, ignoreBots?, caseInsensitive? } }); client.prefix
|
|
463
520
|
// reading others' content needs the privileged MessageContent intent (Intents.messages)
|
|
464
521
|
```
|
|
@@ -466,7 +523,11 @@ class PrefixContext { message; commandName; args: string[]; rest: string; reply(
|
|
|
466
523
|
### Usage tracking — [guide](./usage.md)
|
|
467
524
|
|
|
468
525
|
```ts
|
|
469
|
-
interface UsageEvent { type:
|
|
526
|
+
interface UsageEvent { type: UsageType; name: string; userId?; userTag?; guildId?; channelId?; detail?; outcome?: UsageOutcome; durationMs?: number; options?: Readonly<Record<string, UsageMetaValue>>; errorMessage?: string; timestamp: Date; }
|
|
527
|
+
type UsageType = "command" | "prefix" | "component" | "event";
|
|
528
|
+
type UsageOutcome = "success" | "error";
|
|
529
|
+
type UsageMetaValue = string | number | boolean | null;
|
|
530
|
+
function formatUsage(event: UsageEvent): string; // default channel-line renderer
|
|
470
531
|
interface UsageStore { record(event): Awaitable<void>; all(): Awaitable<readonly UsageEvent[]>; }
|
|
471
532
|
class MemoryUsageStore { record; all; size; byUser(id); clear; }
|
|
472
533
|
class JsonFileUsageStore { constructor(path: string); record; all; }
|
|
@@ -486,16 +547,22 @@ log/usage transports a real Discord bot ends up writing.
|
|
|
486
547
|
### Embeds — preset replies
|
|
487
548
|
|
|
488
549
|
```ts
|
|
489
|
-
class Embeds { error(input); success(input); info(input); warn(input); build(level, input); }
|
|
490
|
-
|
|
550
|
+
class Embeds { constructor(options?: EmbedsOptions); error(input); success(input); info(input); warn(input); build(level, input); readonly colors: EmbedColors; readonly icons: EmbedIcons; }
|
|
551
|
+
const defaultEmbeds: Embeds; // shared default used when `client.embeds` is unset
|
|
552
|
+
const DEFAULT_EMBED_COLORS: EmbedColors; // red / green / blue / yellow
|
|
553
|
+
const DEFAULT_EMBED_ICONS: EmbedIcons; // ⛔ ✅ ℹ️ ⚠️
|
|
491
554
|
// SpearClient owns one as `client.embeds`; configure via the `embeds` option.
|
|
492
555
|
// BaseContext gains ctx.success/info/warn/error (state-aware send) + replySuccess/replyInfo/replyWarn/replyError.
|
|
493
556
|
```
|
|
494
557
|
|
|
495
|
-
### Guards — declarative preconditions
|
|
558
|
+
### Guards — declarative preconditions — [guide](./guards.md)
|
|
496
559
|
|
|
497
560
|
```ts
|
|
498
561
|
type Guard<TCtx extends GuardContext = GuardContext> = (ctx: TCtx) => Awaitable<GuardResult>;
|
|
562
|
+
interface GuardContext { client; user; member; guild; guildId; channelId; }
|
|
563
|
+
type GuardResult = boolean | { allowed: false; reason?: string };
|
|
564
|
+
type RunGuardsResult = { allowed: true } | { allowed: false; reason: string | undefined };
|
|
565
|
+
function runGuards<TCtx extends GuardContext>(ctx: TCtx, guards?: readonly Guard<TCtx>[]): Promise<RunGuardsResult>;
|
|
499
566
|
function denied(reason?: string): GuardResult;
|
|
500
567
|
function guildOnly(reason?: string): Guard;
|
|
501
568
|
function dmOnly(reason?: string): Guard;
|
|
@@ -505,49 +572,62 @@ function requireOwner(ownerIds: readonly string[], reason?: string): Guard;
|
|
|
505
572
|
function requireUserPermissions(permission: PermissionResolvable, reason?: string): Guard;
|
|
506
573
|
function requireBotPermissions(permission: PermissionResolvable, reason?: string): Guard;
|
|
507
574
|
function guard<TCtx>(predicate: Guard<TCtx>): Guard<TCtx>;
|
|
575
|
+
// every built-in guard takes an optional custom `reason`; each has a sensible default message.
|
|
508
576
|
// per-handler: command({ guards: [...] }), prefixCommand({ guards }), button({ guards }), userCommand({ guards }), ...
|
|
509
577
|
// client-wide: new SpearClient({ guards: [...] })
|
|
510
578
|
```
|
|
511
579
|
|
|
512
|
-
### Context-menu commands
|
|
580
|
+
### Context-menu commands — [guide](./context-menus.md)
|
|
513
581
|
|
|
514
582
|
```ts
|
|
515
|
-
|
|
516
|
-
function
|
|
517
|
-
|
|
583
|
+
interface ContextMenuMeta { defaultMemberPermissions?: PermissionResolvable | null; nsfw?: boolean; guildOnly?: boolean; nameLocalizations?: LocalizationMap; cooldown?: CooldownInput; guards?: readonly Guard[]; autoDefer?: AutoDeferInput; }
|
|
584
|
+
function userCommand<R>(config: ContextMenuMeta & { name: string; run: (ctx: UserContextMenuContext) => Awaitable<R> }): UserContextMenu;
|
|
585
|
+
function messageCommand<R>(config: ContextMenuMeta & { name: string; run: (ctx: MessageContextMenuContext) => Awaitable<R> }): MessageContextMenu;
|
|
586
|
+
// UserContextMenuContext adds ctx.targetUser, ctx.targetMember; MessageContextMenuContext adds ctx.targetMessage (+ BaseContext).
|
|
587
|
+
// ContextMenuCommand = UserContextMenu | MessageContextMenu; client.contextMenus is a ContextMenuRegistry.
|
|
588
|
+
// Deploy slash commands + menus together with client.deployAllCommands({ guildId }).
|
|
518
589
|
```
|
|
519
590
|
|
|
520
591
|
### Prefix typed arguments
|
|
521
592
|
|
|
522
593
|
```ts
|
|
523
594
|
function prefixArgs(): PrefixArgsBuilder<{}>;
|
|
524
|
-
// builder methods:
|
|
525
|
-
//
|
|
595
|
+
// builder methods — each requires a `name` and takes an optional options object:
|
|
596
|
+
// .string(name, { required?, minLength?, maxLength?, default? }) -> string
|
|
597
|
+
// .integer(name, { required?, minValue?, maxValue?, default? }) -> number
|
|
598
|
+
// .number(name, { required?, minValue?, maxValue?, default? }) -> number
|
|
599
|
+
// .boolean(name, { required?, default? }) -> boolean
|
|
600
|
+
// .snowflake(name, { required?, default? }) -> string (accepts raw ids and <@u>/<#c>/<@&r> mentions)
|
|
601
|
+
// .duration(name, { required?, default? }) -> number ("1h30m" parsed to ms)
|
|
602
|
+
// .rest(name, { required?, default? }) -> string (remaining text)
|
|
603
|
+
// prefixCommand({ args: (a) => a.snowflake("target", { required: true }).duration("dur").rest("reason", { default: "No reason" }), run: (ctx) => ctx.options });
|
|
526
604
|
```
|
|
527
605
|
|
|
528
606
|
### Pagination + Confirmation
|
|
529
607
|
|
|
530
608
|
```ts
|
|
531
|
-
function paginate<T>(interaction, items, {
|
|
609
|
+
function paginate<T>(interaction, items, { render, pageSize?, user?, timeoutMs?, controls?: "prev-next" | "first-prev-next-last", ephemeral?, namespace?, labels?: { first?; prev?; next?; last? } }): Promise<void>;
|
|
532
610
|
function buildPaginatorPage<T>(items, page, options): Promise<{ payload; pages }>;
|
|
533
|
-
function confirm(interaction, { title?,
|
|
611
|
+
function confirm(interaction, { body, title?, confirm?: { label?; style? }, cancel?: { label?; style? }, user?, timeoutMs?, ephemeral?, namespace? }): Promise<{ confirmed: boolean; reason: "confirm" | "cancel" | "timeout"; interaction? }>; // style: "Primary" | "Secondary" | "Success" | "Danger"
|
|
534
612
|
```
|
|
535
613
|
|
|
536
614
|
### Primitives
|
|
537
615
|
|
|
538
616
|
```ts
|
|
539
|
-
class KeyedLock { tryAcquire(key, ttl?); run(key, fn, { onBusy?, ttl? }); isHeld(key); forget(key); dispose(); }
|
|
540
|
-
const safeFetch = { member, channel, message, user, guild, role, try }; // each returns T | null
|
|
617
|
+
class KeyedLock { constructor(options?: { ttl?: number; sweep?: number }); tryAcquire(key, ttl?); run(key, fn, { onBusy?, ttl? }); isHeld(key); forget(key); dispose(); readonly size: number; }
|
|
618
|
+
const safeFetch = { member, channel, message, user, guild, role, try }; // each returns T | null; also exported standalone as fetchMember/fetchChannel/fetchMessage/fetchUser/fetchGuild/fetchRole/safeTry
|
|
541
619
|
function withSafeTimeout<T>(p: Promise<T>, ms): Promise<T | null>;
|
|
542
|
-
function formatDuration(ms, { locale?:
|
|
620
|
+
function formatDuration(ms, opts?: { locale?: string | UnitLabels; largest?: number; units?: readonly DurationUnit[] }): string; // locale: "en"|"en-US"|"en-GB"|"tr"|"tr-TR" or a custom label set; unknown locales fall back to en
|
|
543
621
|
function parseDuration(input: string): number | null;
|
|
544
622
|
function discordTimestamp(date, style?: "t"|"T"|"d"|"D"|"f"|"F"|"R"): string;
|
|
545
623
|
function relativeTimestamp(date): string;
|
|
546
624
|
interface CacheStore { get; set; delete; has; increment; rateLimit; clear; }
|
|
547
625
|
class MemoryCache implements CacheStore { /* TTL, counter, fixed-window rate limit */ }
|
|
626
|
+
function createCache(): CacheStore; // default in-memory cache
|
|
548
627
|
function loadConfig<T>({ file, parser?, schema?, encoding? }): T;
|
|
549
628
|
function loadConfigAsync<T>(opts): Promise<T>;
|
|
550
629
|
function lookup<K, V>(table, resourceName?): (key: K) => V;
|
|
630
|
+
function lookupOptional<K, V>(table): (key: K) => V | undefined; // non-throwing variant of lookup
|
|
551
631
|
```
|
|
552
632
|
|
|
553
633
|
### Logger transports
|
|
@@ -556,6 +636,7 @@ function lookup<K, V>(table, resourceName?): (key: K) => V;
|
|
|
556
636
|
new Logger({ level, transports: [consoleSink, jsonlSink("./logs/bot.jsonl"), webhookSink({ url, minLevel: "error" })] });
|
|
557
637
|
function jsonlSink(path: string, { minLevel? }?): LogSink;
|
|
558
638
|
function webhookSink({ url, minLevel?, username? }): LogSink;
|
|
639
|
+
function consoleSink(entry: LogEntry): void; // default human-readable console transport
|
|
559
640
|
// Logger.addTransport(sink), setTransports([sinks])
|
|
560
641
|
```
|
|
561
642
|
|
|
@@ -574,16 +655,123 @@ client.deployAllCommands({ guildId, dryRun: true }); // returns { ski
|
|
|
574
655
|
client.deployAllCommands({ guildId, strategy: "diff" }); // skips PUT when remote matches
|
|
575
656
|
client.deployAllCommands({ applicationId: "...", strategy: "diff" }); // explicit app id, no ready required
|
|
576
657
|
```
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## Added in 0.4
|
|
661
|
+
|
|
662
|
+
Reliability and moderation helpers distilled from production bots: never lose an
|
|
663
|
+
interaction to the 3-second window, shut down cleanly, run permission/hierarchy
|
|
664
|
+
preflights, persist per-guild settings, and await replies without hand-rolled
|
|
665
|
+
collectors.
|
|
666
|
+
|
|
667
|
+
### Auto-defer — [guide](./auto-defer.md)
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
type AutoDeferInput = boolean | { ephemeral?: boolean; delayMs?: number };
|
|
671
|
+
interface AutoDeferConfig { ephemeral: boolean; delayMs: number; }
|
|
672
|
+
const DEFAULT_AUTO_DEFER_DELAY_MS = 2000;
|
|
673
|
+
function normalizeAutoDefer(input?: AutoDeferInput): AutoDeferConfig | undefined;
|
|
674
|
+
function armAutoDefer(interaction, config: AutoDeferConfig): () => void; // returns a cancel fn
|
|
675
|
+
type AutoDeferrableInteraction = ChatInputCommandInteraction | UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction;
|
|
676
|
+
// Enable per handler: command({ autoDefer: true }), userCommand({ autoDefer }), messageCommand({ autoDefer })
|
|
677
|
+
// Or globally: new SpearClient({ autoDefer: true }). With it on, respond via ctx.send / ctx.editReply.
|
|
678
|
+
// Arms a timer when the handler starts; defers if it hasn't responded by ~2s, preventing "Unknown interaction" (10062).
|
|
679
|
+
```
|
|
577
680
|
|
|
578
|
-
###
|
|
681
|
+
### Graceful shutdown
|
|
579
682
|
|
|
580
683
|
```ts
|
|
581
|
-
interface
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
timestamp: Date;
|
|
684
|
+
interface GracefulShutdownOptions {
|
|
685
|
+
signals?: readonly NodeJS.Signals[]; // default ["SIGINT", "SIGTERM"]
|
|
686
|
+
timeoutMs?: number; // force-exit after this; default 10000
|
|
687
|
+
exit?: boolean; // call process.exit when done; default true
|
|
688
|
+
onShutdown?: (signal: NodeJS.Signals) => Awaitable<void>; // runs before client.destroy()
|
|
689
|
+
logger?: { info?(msg): void; error?(msg, meta?): void };
|
|
588
690
|
}
|
|
589
|
-
|
|
691
|
+
interface Destroyable { destroy(): Awaitable<void>; } // a discord.js Client qualifies
|
|
692
|
+
interface ShutdownLogger { info?(message: string): void; error?(message: string, meta?: unknown): void; }
|
|
693
|
+
function gracefulShutdown(client: Destroyable, options?: GracefulShutdownOptions): () => void;
|
|
694
|
+
// SpearClient.enableGracefulShutdown(options?) wires it with client.logger and returns a disposer.
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### Permissions & moderation — [guide](./permissions.md)
|
|
698
|
+
|
|
699
|
+
```ts
|
|
700
|
+
type PermissionHolder = GuildMember | Role;
|
|
701
|
+
function missingPermissions(channel: GuildBasedChannel, who: PermissionHolder, required: PermissionResolvable): PermissionsString[];
|
|
702
|
+
function botMissingPermissions(channel: GuildBasedChannel, required: PermissionResolvable): PermissionsString[];
|
|
703
|
+
function hasPermissions(channel: GuildBasedChannel, who: PermissionHolder, required: PermissionResolvable): boolean;
|
|
704
|
+
function compareRoles(a: GuildMember, b: GuildMember): number; // by highest-role position
|
|
705
|
+
function canActOn(actor: GuildMember, target: GuildMember): boolean;
|
|
706
|
+
function formatPermissions(permissions: PermissionResolvable): string; // human, comma-separated
|
|
707
|
+
|
|
708
|
+
type ModerationCheckResult = { ok: true } | { ok: false; reason: string };
|
|
709
|
+
interface ModerationCheckOptions { moderator: GuildMember; target: GuildMember; me?: GuildMember | null; action?: string; }
|
|
710
|
+
function moderationCheck(options: ModerationCheckOptions): ModerationCheckResult; // self / owner / role-hierarchy preflight
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### Persistent storage
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
interface KeyValueStore {
|
|
717
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
718
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
719
|
+
has(key: string): Promise<boolean>;
|
|
720
|
+
delete(key: string): Promise<boolean>;
|
|
721
|
+
keys(): Promise<string[]>;
|
|
722
|
+
clear(): Promise<void>;
|
|
723
|
+
}
|
|
724
|
+
class MemoryStore implements KeyValueStore { /* deep-cloned in-memory */ }
|
|
725
|
+
class JsonStore implements KeyValueStore { constructor(path: string); /* atomic JSON file */ }
|
|
726
|
+
function namespaced(store: KeyValueStore, prefix: string): KeyValueStore;
|
|
727
|
+
|
|
728
|
+
interface SettingsManager<T> { readonly defaults: T; readonly store: KeyValueStore; get(id): Promise<T>; set(id, patch: Partial<T>): Promise<T>; reset(id): Promise<void>; }
|
|
729
|
+
interface CreateSettingsOptions<T> { store: KeyValueStore; defaults: T; namespace?: string; } // namespace default "settings"
|
|
730
|
+
function createSettings<T extends Record<string, unknown>>(options: CreateSettingsOptions<T>): SettingsManager<T>;
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Collectors
|
|
734
|
+
|
|
735
|
+
```ts
|
|
736
|
+
interface AwaitMessageOptions { filter?: (m: Message) => boolean; time?: number; } // time default 60000
|
|
737
|
+
function awaitMessage(channel: CollectableChannel, options?: AwaitMessageOptions): Promise<Message | null>;
|
|
738
|
+
interface AwaitComponentOptions { filter?; time?; componentType?: ComponentType; } // time default 60000
|
|
739
|
+
function awaitComponent(message: Message, options?: AwaitComponentOptions): Promise<MessageComponentInteraction | null>;
|
|
740
|
+
interface AwaitModalOptions { time?: number; filter?: (i: ModalSubmitInteraction) => boolean; } // time default 120000
|
|
741
|
+
function showAndAwaitModal(interaction: ModalShowingInteraction, modal: ModalLike, options?: AwaitModalOptions): Promise<ModalSubmitInteraction | null>;
|
|
742
|
+
// Context sugar: ctx.awaitMessageFrom(userId?, options?) and ctx.awaitModal(modal, options?) (command + component contexts).
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Discord errors — [guide](./errors.md)
|
|
746
|
+
|
|
747
|
+
```ts
|
|
748
|
+
const DiscordErrorCode = { UnknownChannel, UnknownGuild, UnknownMember, UnknownMessage, UnknownUser,
|
|
749
|
+
UnknownInteraction, MissingAccess, CannotExecuteActionOnDMChannel, CannotSendMessagesToThisUser,
|
|
750
|
+
MissingPermissions, InvalidFormBodyOrContentType, InteractionHasAlreadyBeenAcknowledged,
|
|
751
|
+
MaximumNumberOfGuildsReached, MaximumNumberOfReactionsReached } as const; // named RESTJSONErrorCodes
|
|
752
|
+
type DiscordErrorCodeValue = (typeof DiscordErrorCode)[keyof typeof DiscordErrorCode];
|
|
753
|
+
function isDiscordError(error: unknown, code?: number | string | readonly (number | string)[]): error is DiscordAPIError;
|
|
754
|
+
function isHTTPError(error: unknown): error is HTTPError;
|
|
755
|
+
function isRateLimitError(error: unknown): boolean; // HTTP 429
|
|
756
|
+
function explainDiscordError(error: unknown): string | null; // end-user-friendly sentence, or null
|
|
757
|
+
// The default command/component error reply uses explainDiscordError(...) when it can.
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### Message formatting
|
|
761
|
+
|
|
762
|
+
```ts
|
|
763
|
+
const MESSAGE_CHARACTER_LIMIT = 2000;
|
|
764
|
+
function truncate(text: string, max: number, suffix?: string): string; // suffix default "…"
|
|
765
|
+
interface ChunkOptions { max?: number; } // default MESSAGE_CHARACTER_LIMIT
|
|
766
|
+
function chunkMessage(text: string, options?: ChunkOptions): string[]; // splits on line/word boundaries
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### Dynamic prefixes
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
// PrefixOptions gains a per-message resolver (e.g. a per-guild prefix from a store):
|
|
773
|
+
interface PrefixOptions { /* …prefix, mention, ignoreBots, caseInsensitive… */
|
|
774
|
+
dynamic?: (message: Message) => Awaitable<string | readonly string[] | null | undefined>;
|
|
775
|
+
}
|
|
776
|
+
// Dynamic prefixes are tried in addition to any static prefix. Keep the resolver fast (cache it).
|
|
777
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Auto-defer
|
|
2
|
+
|
|
3
|
+
The single most common discord.js error is
|
|
4
|
+
`DiscordAPIError[10062]: Unknown interaction`. An interaction token is valid for
|
|
5
|
+
only **3 seconds** before your first response; any handler that awaits a database
|
|
6
|
+
query or an HTTP call risks blowing past that window, after which the interaction
|
|
7
|
+
is dead and every reply throws.
|
|
8
|
+
|
|
9
|
+
Auto-defer removes the footgun: spearkit arms a timer when your handler starts
|
|
10
|
+
and, if you haven't responded in time, calls `deferReply()` for you. The timer is
|
|
11
|
+
cancelled the instant your handler replies or defers itself.
|
|
12
|
+
|
|
13
|
+
## Per command
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { command, option } from "spearkit";
|
|
17
|
+
|
|
18
|
+
export const weather = command({
|
|
19
|
+
name: "weather",
|
|
20
|
+
description: "Look up the weather",
|
|
21
|
+
autoDefer: true, // defers automatically if the handler takes too long
|
|
22
|
+
options: { city: option.string({ description: "City", required: true }) },
|
|
23
|
+
run: async (ctx) => {
|
|
24
|
+
const report = await fetchWeather(ctx.options.city); // slow
|
|
25
|
+
await ctx.send(`Weather in ${ctx.options.city}: ${report}`);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> With auto-defer on, respond via `ctx.send(...)` or `ctx.editReply(...)`, not
|
|
31
|
+
> `ctx.reply(...)` — the initial reply slot may already be taken by the
|
|
32
|
+
> auto-defer. `ctx.send` is state-aware and always does the right thing.
|
|
33
|
+
|
|
34
|
+
## Options
|
|
35
|
+
|
|
36
|
+
`autoDefer` accepts `true` (defaults) or an object:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
command({
|
|
40
|
+
name: "report",
|
|
41
|
+
description: "Generate a report",
|
|
42
|
+
autoDefer: { ephemeral: true, delayMs: 1500 },
|
|
43
|
+
run: async (ctx) => ctx.send("…"),
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Field | Default | Meaning |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `ephemeral` | `false` | Defer as a hidden ("thinking…") response. |
|
|
50
|
+
| `delayMs` | `2000` | How long to wait before the safety defer fires. Kept under the 3s cutoff. |
|
|
51
|
+
|
|
52
|
+
## Client-wide default
|
|
53
|
+
|
|
54
|
+
Apply auto-defer to **every** slash command and context menu; each handler can
|
|
55
|
+
still override with its own `autoDefer`.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { SpearClient } from "spearkit";
|
|
59
|
+
|
|
60
|
+
const client = new SpearClient({ autoDefer: true });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Scope
|
|
64
|
+
|
|
65
|
+
Auto-defer covers slash commands and context menus (answered with `deferReply`).
|
|
66
|
+
Component handlers (buttons/selects) usually respond instantly with `update`, so
|
|
67
|
+
they're not auto-deferred — call `ctx.deferUpdate()` yourself if a component
|
|
68
|
+
handler does slow work.
|
|
69
|
+
|
|
70
|
+
## Lower-level helpers
|
|
71
|
+
|
|
72
|
+
`normalizeAutoDefer(input)` resolves `true`/object/`undefined` into an
|
|
73
|
+
`AutoDeferConfig`; `armAutoDefer(interaction, config)` arms the timer and returns
|
|
74
|
+
a cancel function. Both are exported for custom dispatch.
|