spearkit 0.3.0 → 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 +247 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +329 -0
- package/AGENTS.md +261 -0
- package/README.md +23 -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 +72 -0
- package/docs/api-reference.md +777 -0
- package/docs/auto-defer.md +74 -0
- package/docs/client.md +245 -0
- package/docs/collectors.md +65 -0
- package/docs/commands.md +203 -0
- package/docs/components.md +281 -0
- package/docs/context-menus.md +121 -0
- package/docs/context.md +293 -0
- package/docs/cooldown.md +125 -0
- package/docs/env.md +130 -0
- package/docs/errors.md +73 -0
- package/docs/events.md +152 -0
- package/docs/getting-started.md +147 -0
- package/docs/guards.md +146 -0
- package/docs/loading.md +144 -0
- package/docs/logging.md +195 -0
- package/docs/messages.md +35 -0
- package/docs/migration.md +160 -0
- package/docs/options.md +163 -0
- package/docs/permissions.md +68 -0
- package/docs/plugins.md +116 -0
- package/docs/prefix.md +234 -0
- package/docs/scheduler.md +111 -0
- package/docs/shutdown.md +42 -0
- package/docs/store.md +90 -0
- package/docs/usage.md +188 -0
- package/llms-full.txt +4619 -0
- package/llms.txt +127 -0
- package/package.json +9 -3
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
Every symbol spearkit exports, in addition to the entire re-exported discord.js
|
|
4
|
+
surface. Import any of these from `"spearkit"`.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { SpearClient, command, option, event, button, modal, row /* … */ } from "spearkit";
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Client
|
|
13
|
+
|
|
14
|
+
### `class SpearClient extends Client`
|
|
15
|
+
|
|
16
|
+
A discord.js `Client` with registries and interaction routing wired up.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
new SpearClient(options?: SpearClientOptions)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| Member | Type | Description |
|
|
23
|
+
| ------ | ---- | ----------- |
|
|
24
|
+
| `commands` | `CommandRegistry` | Slash command registry + dispatcher. |
|
|
25
|
+
| `events` | `EventRegistry` | Event listener registry. |
|
|
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. |
|
|
34
|
+
| `register(...items: Registerable[])` | `this` | Route each item to the matching registry. |
|
|
35
|
+
| `use(...plugins: SpearPlugin[])` | `Promise<this>` | Run each plugin's `setup`. |
|
|
36
|
+
| `load(dir: string, options?: LoadOptions)` | `Promise<number>` | Import a directory and register its exports. Returns count. |
|
|
37
|
+
| `start(token?: string)` | `Promise<this>` | Log in (falls back to `DISCORD_TOKEN`). |
|
|
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. |
|
|
42
|
+
|
|
43
|
+
Inherits everything from discord.js `Client` (`on`, `once`, `login`, `ws`, `rest`, `application`, `user`, …).
|
|
44
|
+
|
|
45
|
+
### `type SpearClientOptions = Partial<ClientOptions> & SpearOptions`
|
|
46
|
+
|
|
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. |
|
|
60
|
+
|
|
61
|
+
### `const Intents`
|
|
62
|
+
|
|
63
|
+
Ready-made intent presets (arrays of `GatewayIntentBits`).
|
|
64
|
+
|
|
65
|
+
| Key | Contents |
|
|
66
|
+
| --- | -------- |
|
|
67
|
+
| `Intents.none` | `[]` |
|
|
68
|
+
| `Intents.default` | `[Guilds]` |
|
|
69
|
+
| `Intents.guilds` | `[Guilds, GuildMembers]` |
|
|
70
|
+
| `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
|
|
71
|
+
| `Intents.all` | Every intent (includes privileged). |
|
|
72
|
+
|
|
73
|
+
### `type Registerable = SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand | ContextMenuCommand`
|
|
74
|
+
|
|
75
|
+
The union accepted by `SpearClient.register`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Commands
|
|
80
|
+
|
|
81
|
+
### `function command<O, R>(config): SlashCommand`
|
|
82
|
+
|
|
83
|
+
Define a leaf slash command.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
interface CommandConfig<O extends OptionMap, R> {
|
|
87
|
+
name: string;
|
|
88
|
+
description: string;
|
|
89
|
+
options?: O;
|
|
90
|
+
defaultMemberPermissions?: PermissionResolvable | null;
|
|
91
|
+
nsfw?: boolean;
|
|
92
|
+
guildOnly?: boolean;
|
|
93
|
+
nameLocalizations?: LocalizationMap;
|
|
94
|
+
descriptionLocalizations?: LocalizationMap;
|
|
95
|
+
cooldown?: CooldownInput;
|
|
96
|
+
guards?: readonly Guard[];
|
|
97
|
+
autoDefer?: AutoDeferInput;
|
|
98
|
+
run: (ctx: CommandContext<O>) => Awaitable<R>;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `function commandGroup(config: CommandGroupConfig): SlashCommand`
|
|
103
|
+
|
|
104
|
+
Define a command that routes to subcommands and/or subcommand groups.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
interface CommandGroupConfig {
|
|
108
|
+
name: string;
|
|
109
|
+
description: string;
|
|
110
|
+
subcommands?: Record<string, Subcommand>;
|
|
111
|
+
groups?: Record<string, SubcommandGroup>;
|
|
112
|
+
defaultMemberPermissions?: PermissionResolvable | null;
|
|
113
|
+
nsfw?: boolean;
|
|
114
|
+
guildOnly?: boolean;
|
|
115
|
+
nameLocalizations?: LocalizationMap;
|
|
116
|
+
descriptionLocalizations?: LocalizationMap;
|
|
117
|
+
cooldown?: CooldownInput;
|
|
118
|
+
guards?: readonly Guard[];
|
|
119
|
+
autoDefer?: AutoDeferInput;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `function subcommand<O, R>(config): Subcommand`
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
interface SubcommandConfig<O extends OptionMap, R> {
|
|
127
|
+
description: string;
|
|
128
|
+
options?: O;
|
|
129
|
+
nameLocalizations?: LocalizationMap;
|
|
130
|
+
descriptionLocalizations?: LocalizationMap;
|
|
131
|
+
run: (ctx: CommandContext<O>) => Awaitable<R>;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `function subcommandGroup(config: SubcommandGroupConfig): SubcommandGroup`
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
interface SubcommandGroupConfig {
|
|
139
|
+
description: string;
|
|
140
|
+
subcommands: Record<string, Subcommand>;
|
|
141
|
+
nameLocalizations?: LocalizationMap;
|
|
142
|
+
descriptionLocalizations?: LocalizationMap;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `class SlashCommand`
|
|
147
|
+
|
|
148
|
+
| Member | Type | Description |
|
|
149
|
+
| ------ | ---- | ----------- |
|
|
150
|
+
| `name` | `string` | Top-level command name. |
|
|
151
|
+
| `hasAutocomplete` | `boolean` | True if any option declares autocomplete. |
|
|
152
|
+
| `toJSON()` | `RESTPostAPIChatInputApplicationCommandsJSONBody` | REST payload. |
|
|
153
|
+
| `execute(interaction)` | `Promise<void>` | Run for a chat-input interaction. |
|
|
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. |
|
|
158
|
+
|
|
159
|
+
### `class CommandContext<O> extends BaseContext<ChatInputCommandInteraction>`
|
|
160
|
+
|
|
161
|
+
| Member | Type | Description |
|
|
162
|
+
| ------ | ---- | ----------- |
|
|
163
|
+
| `options` | `ResolvedOptions<O>` | Resolved, fully-typed option values. |
|
|
164
|
+
| `commandName` | `string` | Invoked command name. |
|
|
165
|
+
| `subcommand` | `string \| null` | Invoked subcommand, if any. |
|
|
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). |
|
|
168
|
+
|
|
169
|
+
Plus all `BaseContext` members.
|
|
170
|
+
|
|
171
|
+
### `class CommandRegistry`
|
|
172
|
+
|
|
173
|
+
| Member | Type | Description |
|
|
174
|
+
| ------ | ---- | ----------- |
|
|
175
|
+
| `add(...commands: SlashCommand[])` | `this` | Register commands (override by name). |
|
|
176
|
+
| `remove(name: string)` | `boolean` | Remove a command. |
|
|
177
|
+
| `get(name: string)` | `SlashCommand \| undefined` | Look up a command. |
|
|
178
|
+
| `all()` | `SlashCommand[]` | All commands. |
|
|
179
|
+
| `names` | `string[]` | All command names. |
|
|
180
|
+
| `size` | `number` | Count. |
|
|
181
|
+
| `onError(handler: CommandErrorHandler)` | `this` | Set the error handler. |
|
|
182
|
+
| `toJSON()` | `RESTPostAPIApplicationCommandsJSONBody[]` | Serialise all commands. |
|
|
183
|
+
| `handle(interaction)` | `Promise<void>` | Dispatch a chat-input interaction. |
|
|
184
|
+
| `handleAutocomplete(interaction)` | `Promise<void>` | Dispatch an autocomplete interaction. |
|
|
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). |
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
type CommandErrorHandler = (error: Error, interaction: ChatInputCommandInteraction) => Awaitable<void>;
|
|
193
|
+
interface DeployOptions { token?: string; applicationId: string; guildId?: string; rest?: REST; }
|
|
194
|
+
type DeployResult = RESTPutAPIApplicationCommandsResult | RESTPutAPIApplicationGuildCommandsResult;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Options
|
|
200
|
+
|
|
201
|
+
### `const option`
|
|
202
|
+
|
|
203
|
+
Type-safe option builders. Each returns an `OptionDef` whose resolved value type
|
|
204
|
+
is inferred (required → value, optional → `value | undefined`, `choices` →
|
|
205
|
+
literal union).
|
|
206
|
+
|
|
207
|
+
| Builder | Resolved type | Extra config |
|
|
208
|
+
| ------- | ------------- | ------------ |
|
|
209
|
+
| `option.string(config)` | `string` | `choices?`, `minLength?`, `maxLength?`, `autocomplete?` |
|
|
210
|
+
| `option.integer(config)` | `number` | `choices?`, `minValue?`, `maxValue?`, `autocomplete?` |
|
|
211
|
+
| `option.number(config)` | `number` | `choices?`, `minValue?`, `maxValue?`, `autocomplete?` |
|
|
212
|
+
| `option.boolean(config)` | `boolean` | — |
|
|
213
|
+
| `option.user(config)` | `User` | — |
|
|
214
|
+
| `option.channel(config)` | channel union | `channelTypes?` |
|
|
215
|
+
| `option.role(config)` | `Role \| APIRole` | — |
|
|
216
|
+
| `option.mentionable(config)` | user/role/member | — |
|
|
217
|
+
| `option.attachment(config)` | `Attachment` | — |
|
|
218
|
+
|
|
219
|
+
Common config (`BaseConfig`):
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
{
|
|
223
|
+
description: string;
|
|
224
|
+
required?: boolean; // default false
|
|
225
|
+
nameLocalizations?: LocalizationMap;
|
|
226
|
+
descriptionLocalizations?: LocalizationMap;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`choices` items are `OptionChoice<V>`:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
interface OptionChoice<V extends string | number = string | number> {
|
|
234
|
+
name: string;
|
|
235
|
+
value: V;
|
|
236
|
+
nameLocalizations?: LocalizationMap;
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`autocomplete`:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
type AutocompleteHandler<V extends string | number> =
|
|
244
|
+
(ctx: AutocompleteContext) => Awaitable<OptionChoice<V>[]>;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Option types
|
|
248
|
+
|
|
249
|
+
| Symbol | Description |
|
|
250
|
+
| ------ | ----------- |
|
|
251
|
+
| `interface OptionDef<TValue, TRequired>` | A described option (phantom-typed for inference). |
|
|
252
|
+
| `type AnyOptionDef` | `OptionDef<OptionValue, boolean>`. |
|
|
253
|
+
| `type OptionMap` | `Record<string, AnyOptionDef>`. |
|
|
254
|
+
| `type ResolvedOption<O>` | The handler value for one option. |
|
|
255
|
+
| `type ResolvedOptions<O>` | The handler's `options` object. |
|
|
256
|
+
| `type OptionValue` | Union of all possible resolved values. |
|
|
257
|
+
| `type AllowedChannelType` | Channel types valid for a channel option. |
|
|
258
|
+
| `function toAPIOption(name, def)` | Serialise one option to REST. |
|
|
259
|
+
| `function readOption(resolver, name, def)` | Read a resolved value (null → undefined). |
|
|
260
|
+
| `function optionsHaveAutocomplete(options)` | True if any option has autocomplete. |
|
|
261
|
+
|
|
262
|
+
### `class AutocompleteContext`
|
|
263
|
+
|
|
264
|
+
| Member | Type | Description |
|
|
265
|
+
| ------ | ---- | ----------- |
|
|
266
|
+
| `interaction` | `AutocompleteInteraction` | Raw interaction. |
|
|
267
|
+
| `client` / `user` / `guild` / `guildId` | — | Convenience accessors. |
|
|
268
|
+
| `commandName` | `string` | Command being completed. |
|
|
269
|
+
| `focusedName` | `string` | Name of the focused option. |
|
|
270
|
+
| `value` | `string` | Current partial value typed by the user. |
|
|
271
|
+
| `respond(choices: OptionChoice[])` | `Promise<void>` | Send up to 25 suggestions. |
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Events
|
|
276
|
+
|
|
277
|
+
### `function event(name, run): EventDef` / `function event(config): EventDef`
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
type EventHandler<E extends keyof ClientEvents> = (...args: ClientEvents[E]) => Awaitable<void>;
|
|
281
|
+
interface EventConfig<E extends keyof ClientEvents> { name: E; once?: boolean; run: EventHandler<E>; }
|
|
282
|
+
interface EventDef { name: keyof ClientEvents; once: boolean; attach(client: Client): void; detach(client: Client): void; }
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Thrown errors and rejected promises are routed to the client's `error` event.
|
|
286
|
+
|
|
287
|
+
### `class EventRegistry`
|
|
288
|
+
|
|
289
|
+
| Member | Type | Description |
|
|
290
|
+
| ------ | ---- | ----------- |
|
|
291
|
+
| `add(...defs: EventDef[])` | `this` | Register listeners. |
|
|
292
|
+
| `size` | `number` | Count. |
|
|
293
|
+
| `attachAll(client: Client)` | `void` | Attach every listener. |
|
|
294
|
+
| `detachAll(client: Client)` | `void` | Detach every listener. |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Components
|
|
299
|
+
|
|
300
|
+
### Builders
|
|
301
|
+
|
|
302
|
+
| Function | Returns | Notes |
|
|
303
|
+
| -------- | ------- | ----- |
|
|
304
|
+
| `button(config)` | `Button<P>` | Interactive button. |
|
|
305
|
+
| `linkButton(config)` | `ButtonBuilder` | URL button, no handler. |
|
|
306
|
+
| `stringSelect(config)` | `StringSelect<P>` | String select; takes `options`. |
|
|
307
|
+
| `userSelect(config)` | `UserSelect<P>` | User select. |
|
|
308
|
+
| `roleSelect(config)` | `RoleSelect<P>` | Role select. |
|
|
309
|
+
| `channelSelect(config)` | `ChannelSelect<P>` | Channel select; takes `channelTypes?`. |
|
|
310
|
+
| `mentionableSelect(config)` | `MentionableSelect<P>` | User + role select. |
|
|
311
|
+
| `modal(config)` | `Modal<P>` | Modal with `fields`. |
|
|
312
|
+
| `textInput(config)` | `TextInputDef` | A modal text-input field. |
|
|
313
|
+
| `row(...components)` | `ActionRowBuilder<C>` | Wrap components in a row. |
|
|
314
|
+
|
|
315
|
+
Each registrable component (`Button`, `StringSelect`, …, `Modal`) extends its
|
|
316
|
+
routing interface and adds `build(...args: BuildArgs<P>)`, which returns the
|
|
317
|
+
discord.js builder. `build` requires exactly the params declared in the id
|
|
318
|
+
pattern.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
interface ButtonConfig<P extends string, R> {
|
|
322
|
+
id: P; // pattern: "name" or "name:{param}"
|
|
323
|
+
label?: string;
|
|
324
|
+
style?: ButtonStyleInput; // "Primary" | "Secondary" | "Success" | "Danger" | ButtonStyle.*
|
|
325
|
+
emoji?: ComponentEmojiResolvable;
|
|
326
|
+
disabled?: boolean;
|
|
327
|
+
guards?: readonly Guard[];
|
|
328
|
+
run: (ctx: ButtonContext<Params<P>>) => Awaitable<R>;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
interface LinkButtonConfig { url: string; label?: string; emoji?: ComponentEmojiResolvable; disabled?: boolean; }
|
|
332
|
+
|
|
333
|
+
interface StringSelectConfig<P extends string, R> {
|
|
334
|
+
id: P;
|
|
335
|
+
options: readonly SelectMenuComponentOptionData[];
|
|
336
|
+
placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
|
|
337
|
+
guards?: readonly Guard[];
|
|
338
|
+
run: (ctx: StringSelectContext<Params<P>>) => Awaitable<R>;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
interface EntitySelectConfig<P extends string> {
|
|
342
|
+
id: P; placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
|
|
343
|
+
guards?: readonly Guard[];
|
|
344
|
+
}
|
|
345
|
+
// user/role/mentionable selects take EntitySelectConfig & { run };
|
|
346
|
+
// channelSelect additionally takes { channelTypes?: readonly ChannelType[] }.
|
|
347
|
+
|
|
348
|
+
function textInput(config: {
|
|
349
|
+
label: string;
|
|
350
|
+
style?: TextInputStyleInput; // "Short" | "Paragraph" | TextInputStyle
|
|
351
|
+
placeholder?: string; required?: boolean; minLength?: number; maxLength?: number; value?: string;
|
|
352
|
+
}): TextInputDef;
|
|
353
|
+
|
|
354
|
+
interface ModalConfig<P extends string, F extends Record<string, TextInputDef>, R> {
|
|
355
|
+
id: P;
|
|
356
|
+
title: string;
|
|
357
|
+
fields: F;
|
|
358
|
+
guards?: readonly Guard[];
|
|
359
|
+
run: (ctx: ModalContext<Params<P>, keyof F & string>) => Awaitable<R>;
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Component contexts
|
|
364
|
+
|
|
365
|
+
| Class | Extra members |
|
|
366
|
+
| ----- | ------------- |
|
|
367
|
+
| `MessageComponentContext<P, I>` | `params`, `customId`, `message`, `update(input)`, `deferUpdate()`, `showModal(modal)`, `awaitModal(modal, options?)` (+ BaseContext) |
|
|
368
|
+
| `ButtonContext<P>` | — |
|
|
369
|
+
| `StringSelectContext<P>` | `values: string[]`, `value: string \| undefined` |
|
|
370
|
+
| `UserSelectContext<P>` | `values`, `users`, `members` |
|
|
371
|
+
| `RoleSelectContext<P>` | `values`, `roles` |
|
|
372
|
+
| `ChannelSelectContext<P>` | `values`, `channels` |
|
|
373
|
+
| `MentionableSelectContext<P>` | `values`, `users`, `roles`, `members` |
|
|
374
|
+
| `ModalContext<P, F>` | `params`, `fields: Record<F, string>`, `customId` (+ BaseContext) |
|
|
375
|
+
|
|
376
|
+
### `class ComponentRegistry`
|
|
377
|
+
|
|
378
|
+
| Member | Type | Description |
|
|
379
|
+
| ------ | ---- | ----------- |
|
|
380
|
+
| `add(...defs: ComponentDef[])` | `this` | Register components (override by namespace). |
|
|
381
|
+
| `onError(handler: ComponentErrorHandler)` | `this` | Set the error handler. |
|
|
382
|
+
| `size` | `number` | Count. |
|
|
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. |
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
type ComponentErrorHandler = (error: Error, interaction: RepliableInteraction) => Awaitable<void>;
|
|
390
|
+
type ComponentDef = ButtonRoute | StringSelectRoute | UserSelectRoute | RoleSelectRoute
|
|
391
|
+
| ChannelSelectRoute | MentionableSelectRoute | ModalRoute;
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Custom-id codec
|
|
395
|
+
|
|
396
|
+
| Symbol | Description |
|
|
397
|
+
| ------ | ----------- |
|
|
398
|
+
| `type ParamNames<S>` | Union of `{param}` names in a pattern. |
|
|
399
|
+
| `type Params<S>` | The params object a pattern resolves to. |
|
|
400
|
+
| `type BuildArgs<S>` | `build()` args (none when no params). |
|
|
401
|
+
| `const MAX_CUSTOM_ID_LENGTH` | `100`. |
|
|
402
|
+
| `function compilePattern(pattern)` | → `CompiledPattern { pattern, namespace, paramNames }`. |
|
|
403
|
+
| `function buildCustomId(compiled, params)` | Encode a concrete id. |
|
|
404
|
+
| `function parseCustomId(customId)` | → `ParsedCustomId { namespace, values }`. |
|
|
405
|
+
| `function paramsFromValues(paramNames, values)` | Map values onto names. |
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Contexts (shared)
|
|
410
|
+
|
|
411
|
+
### `abstract class BaseContext<I>`
|
|
412
|
+
|
|
413
|
+
The base for every interaction context.
|
|
414
|
+
|
|
415
|
+
| Member | Type | Description |
|
|
416
|
+
| ------ | ---- | ----------- |
|
|
417
|
+
| `interaction` | `I` | Raw discord.js interaction. |
|
|
418
|
+
| `client` / `user` / `member` / `guild` / `guildId` / `channel` / `channelId` / `locale` | — | Accessors. |
|
|
419
|
+
| `deferred` / `replied` | `boolean` | Interaction state. |
|
|
420
|
+
| `reply(input)` | `Promise<InteractionResponse>` | Initial response. |
|
|
421
|
+
| `replyEphemeral(input)` | `Promise<InteractionResponse>` | Hidden reply. |
|
|
422
|
+
| `defer({ ephemeral? })` | `Promise<InteractionResponse>` | Acknowledge, respond later. |
|
|
423
|
+
| `editReply(input)` | `Promise<Message>` | Edit the response. |
|
|
424
|
+
| `followUp(input)` | `Promise<Message>` | Additional message. |
|
|
425
|
+
| `send(input)` | `Promise<void>` | State-aware reply/edit/followUp. |
|
|
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. |
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
type ReplyData = InteractionReplyOptions & { ephemeral?: boolean };
|
|
437
|
+
type ReplyInput = string | ReplyData;
|
|
438
|
+
function normalizeReply(input: ReplyInput): InteractionReplyOptions;
|
|
439
|
+
function asEphemeral(input: ReplyInput): ReplyData;
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Plugins
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
interface SpearPlugin { name: string; setup(client: SpearClient): Awaitable<void>; }
|
|
448
|
+
function definePlugin(plugin: SpearPlugin): SpearPlugin;
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Loading
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
interface LoadOptions { extensions?: readonly string[]; recursive?: boolean; } // defaults: [.js,.mjs,.cjs], true
|
|
457
|
+
function collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>;
|
|
458
|
+
function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>;
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
`SpearClient.load(dir, options?)` is the method form of `loadInto`.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Added in 0.2
|
|
466
|
+
|
|
467
|
+
New subsystems, each with a dedicated guide. The `SpearClient` options
|
|
468
|
+
`{ logger?, dotenv?, cooldown?, prefix?, usage?, embeds?, guards? }` configure them.
|
|
469
|
+
|
|
470
|
+
### Logging — [guide](./logging.md)
|
|
471
|
+
|
|
472
|
+
```ts
|
|
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; }
|
|
474
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
475
|
+
type LogThreshold = LogLevel | "silent";
|
|
476
|
+
function consoleSink(entry: LogEntry): void;
|
|
477
|
+
function toError(value: unknown): Error;
|
|
478
|
+
// client.logger is a Logger; new SpearClient({ logger: { level: "debug" } })
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Environment — [guide](./env.md)
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
function parseEnv(content: string): Record<string, string>;
|
|
485
|
+
function loadEnv(options?: { path?: string; override?: boolean }): Record<string, string>;
|
|
486
|
+
const env: { string(k, fallback?); number(k, fallback?); boolean(k, fallback?); require(k): string };
|
|
487
|
+
// client auto-loads .env on start(); disable/configure via the dotenv option
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Cooldowns — [guide](./cooldown.md)
|
|
491
|
+
|
|
492
|
+
```ts
|
|
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); }
|
|
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;
|
|
502
|
+
// command({ cooldown: number | CooldownConfig }); new SpearClient({ cooldown }); client.cooldowns
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Scheduled tasks — [guide](./scheduler.md)
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
function task(config: { name: string; cron?: string; interval?: number; runOnStart?: boolean; run: (client: SpearClient) => Awaitable<void> }): ScheduledTask;
|
|
509
|
+
function cron(expression: string): CronExpression; // .next(from?: Date): Date
|
|
510
|
+
class TaskScheduler { add/remove/list/size/active/start/stop/setLogger; delay/followUp/reconcile (see "Scheduler — one-shot + reconcile") }
|
|
511
|
+
// client.register(task(...)); client.schedule(config); client.scheduler
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Prefix commands — [guide](./prefix.md)
|
|
515
|
+
|
|
516
|
+
```ts
|
|
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); }
|
|
519
|
+
// new SpearClient({ prefix: "!" | string[] | { prefix, mention?, ignoreBots?, caseInsensitive? } }); client.prefix
|
|
520
|
+
// reading others' content needs the privileged MessageContent intent (Intents.messages)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Usage tracking — [guide](./usage.md)
|
|
524
|
+
|
|
525
|
+
```ts
|
|
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
|
|
531
|
+
interface UsageStore { record(event): Awaitable<void>; all(): Awaitable<readonly UsageEvent[]>; }
|
|
532
|
+
class MemoryUsageStore { record; all; size; byUser(id); clear; }
|
|
533
|
+
class JsonFileUsageStore { constructor(path: string); record; all; }
|
|
534
|
+
class UsageTracker { setStore(store); reportTo(channelId, format?); track(event); store; enabled; }
|
|
535
|
+
// new SpearClient({ usage: { store?, channel?, format? } }); client.usage
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Added in 0.3
|
|
541
|
+
|
|
542
|
+
Driven by patterns repeated across long-running production bots: the role/
|
|
543
|
+
permission checks, `.catch(() => null)` fetches, embed factories, pagination
|
|
544
|
+
/confirm flows, mention/duration parsing, locks, config loaders and pluggable
|
|
545
|
+
log/usage transports a real Discord bot ends up writing.
|
|
546
|
+
|
|
547
|
+
### Embeds — preset replies
|
|
548
|
+
|
|
549
|
+
```ts
|
|
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; // ⛔ ✅ ℹ️ ⚠️
|
|
554
|
+
// SpearClient owns one as `client.embeds`; configure via the `embeds` option.
|
|
555
|
+
// BaseContext gains ctx.success/info/warn/error (state-aware send) + replySuccess/replyInfo/replyWarn/replyError.
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Guards — declarative preconditions — [guide](./guards.md)
|
|
559
|
+
|
|
560
|
+
```ts
|
|
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>;
|
|
566
|
+
function denied(reason?: string): GuardResult;
|
|
567
|
+
function guildOnly(reason?: string): Guard;
|
|
568
|
+
function dmOnly(reason?: string): Guard;
|
|
569
|
+
function requireAnyRole(roleIds: readonly string[], reason?: string): Guard;
|
|
570
|
+
function requireAllRoles(roleIds: readonly string[], reason?: string): Guard;
|
|
571
|
+
function requireOwner(ownerIds: readonly string[], reason?: string): Guard;
|
|
572
|
+
function requireUserPermissions(permission: PermissionResolvable, reason?: string): Guard;
|
|
573
|
+
function requireBotPermissions(permission: PermissionResolvable, reason?: string): Guard;
|
|
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.
|
|
576
|
+
// per-handler: command({ guards: [...] }), prefixCommand({ guards }), button({ guards }), userCommand({ guards }), ...
|
|
577
|
+
// client-wide: new SpearClient({ guards: [...] })
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Context-menu commands — [guide](./context-menus.md)
|
|
581
|
+
|
|
582
|
+
```ts
|
|
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 }).
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Prefix typed arguments
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
function prefixArgs(): PrefixArgsBuilder<{}>;
|
|
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 });
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Pagination + Confirmation
|
|
607
|
+
|
|
608
|
+
```ts
|
|
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>;
|
|
610
|
+
function buildPaginatorPage<T>(items, page, options): Promise<{ payload; pages }>;
|
|
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"
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Primitives
|
|
615
|
+
|
|
616
|
+
```ts
|
|
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
|
|
619
|
+
function withSafeTimeout<T>(p: Promise<T>, ms): Promise<T | null>;
|
|
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
|
|
621
|
+
function parseDuration(input: string): number | null;
|
|
622
|
+
function discordTimestamp(date, style?: "t"|"T"|"d"|"D"|"f"|"F"|"R"): string;
|
|
623
|
+
function relativeTimestamp(date): string;
|
|
624
|
+
interface CacheStore { get; set; delete; has; increment; rateLimit; clear; }
|
|
625
|
+
class MemoryCache implements CacheStore { /* TTL, counter, fixed-window rate limit */ }
|
|
626
|
+
function createCache(): CacheStore; // default in-memory cache
|
|
627
|
+
function loadConfig<T>({ file, parser?, schema?, encoding? }): T;
|
|
628
|
+
function loadConfigAsync<T>(opts): Promise<T>;
|
|
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
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Logger transports
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
new Logger({ level, transports: [consoleSink, jsonlSink("./logs/bot.jsonl"), webhookSink({ url, minLevel: "error" })] });
|
|
637
|
+
function jsonlSink(path: string, { minLevel? }?): LogSink;
|
|
638
|
+
function webhookSink({ url, minLevel?, username? }): LogSink;
|
|
639
|
+
function consoleSink(entry: LogEntry): void; // default human-readable console transport
|
|
640
|
+
// Logger.addTransport(sink), setTransports([sinks])
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Scheduler — one-shot + reconcile
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
client.scheduler.delay(name, ms, fn) -> { cancel(): boolean };
|
|
647
|
+
client.scheduler.followUp(name, [10_000, 30_000, 60_000], (i) => ...) -> { cancel(): boolean };
|
|
648
|
+
client.scheduler.reconcile("voice-sessions", async (client) => { /* once on ready */ });
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Deploy diff + dry run
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
client.deployAllCommands({ guildId, dryRun: true }); // returns { skipped, body, reason: "dry-run" }
|
|
655
|
+
client.deployAllCommands({ guildId, strategy: "diff" }); // skips PUT when remote matches
|
|
656
|
+
client.deployAllCommands({ applicationId: "...", strategy: "diff" }); // explicit app id, no ready required
|
|
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
|
+
```
|
|
680
|
+
|
|
681
|
+
### Graceful shutdown
|
|
682
|
+
|
|
683
|
+
```ts
|
|
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 };
|
|
690
|
+
}
|
|
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
|
+
```
|