spearkit 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/spearkit/SKILL.md +236 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +218 -0
- package/AGENTS.md +164 -0
- package/README.md +13 -0
- package/docs/README.md +63 -0
- package/docs/api-reference.md +589 -0
- package/docs/client.md +207 -0
- package/docs/commands.md +198 -0
- package/docs/components.md +274 -0
- package/docs/context.md +201 -0
- package/docs/cooldown.md +124 -0
- package/docs/env.md +130 -0
- package/docs/events.md +152 -0
- package/docs/getting-started.md +147 -0
- package/docs/loading.md +142 -0
- package/docs/logging.md +195 -0
- package/docs/migration.md +160 -0
- package/docs/options.md +163 -0
- package/docs/plugins.md +116 -0
- package/docs/prefix.md +180 -0
- package/docs/scheduler.md +87 -0
- package/docs/usage.md +178 -0
- package/llms-full.txt +3367 -0
- package/llms.txt +39 -0
- package/package.json +9 -3
package/docs/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# spearkit documentation
|
|
2
|
+
|
|
3
|
+
**discord.js++** — a developer-experience-first layer over discord.js. spearkit
|
|
4
|
+
re-exports all of discord.js (so it's a drop-in replacement) and adds an
|
|
5
|
+
ergonomic, fully type-safe API for events, slash commands and interactive
|
|
6
|
+
components.
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
1. [Getting started](./getting-started.md) — install, first bot, project layout.
|
|
11
|
+
2. [Client](./client.md) — `SpearClient`, intents, `register`, `start`, deployment.
|
|
12
|
+
3. [Commands](./commands.md) — slash commands, subcommands, permissions, deployment.
|
|
13
|
+
4. [Options](./options.md) — typed option builders, choices, autocomplete.
|
|
14
|
+
5. [Components](./components.md) — buttons, selects, modals, custom-id routing.
|
|
15
|
+
6. [Events](./events.md) — the `event()` helper and the event registry.
|
|
16
|
+
7. [Contexts](./context.md) — reply helpers shared by every handler.
|
|
17
|
+
8. [Cooldowns](./cooldown.md) — per-user/role/guild rate limiting.
|
|
18
|
+
9. [Scheduled tasks](./scheduler.md) — cron and interval jobs.
|
|
19
|
+
10. [Prefix commands](./prefix.md) — classic `!text` commands.
|
|
20
|
+
11. [Logging](./logging.md) — structured, leveled, scoped logging.
|
|
21
|
+
12. [Usage tracking](./usage.md) — record who used what (store + Discord channel).
|
|
22
|
+
13. [Environment & dotenv](./env.md) — load `.env` and read typed env vars.
|
|
23
|
+
14. [Plugins](./plugins.md) — bundling features into reusable units.
|
|
24
|
+
15. [File-based loading](./loading.md) — one file per command/event/component.
|
|
25
|
+
16. [Migrating from discord.js](./migration.md) — the drop-in path.
|
|
26
|
+
17. [API reference](./api-reference.md) — every exported symbol.
|
|
27
|
+
|
|
28
|
+
## Why spearkit
|
|
29
|
+
|
|
30
|
+
- **Drop-in.** `import { Client, EmbedBuilder } from "spearkit"` — every discord.js
|
|
31
|
+
export is available, so you can migrate one file at a time.
|
|
32
|
+
- **Fully type-safe.** No `any` or `unknown` leaks into your handlers. Option
|
|
33
|
+
values, custom-id params and modal fields are all inferred from your
|
|
34
|
+
definitions.
|
|
35
|
+
- **Co-located.** A command's options and handler, a button's appearance and
|
|
36
|
+
click logic, a modal's fields and submit logic — each lives in one place.
|
|
37
|
+
- **No boilerplate.** No `interactionCreate` switch statements; spearkit routes
|
|
38
|
+
commands, autocomplete, buttons, selects and modals for you.
|
|
39
|
+
|
|
40
|
+
## Thirty-second tour
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { SpearClient, Intents, command, option, button, row, event } from "spearkit";
|
|
44
|
+
|
|
45
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
46
|
+
|
|
47
|
+
const greet = command({
|
|
48
|
+
name: "greet",
|
|
49
|
+
description: "Greet someone",
|
|
50
|
+
options: { who: option.user({ description: "Who", required: true }) },
|
|
51
|
+
run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const ping = button({
|
|
55
|
+
id: "ping:{n}",
|
|
56
|
+
label: "Ping",
|
|
57
|
+
run: (ctx) => ctx.update(`pong #${ctx.params.n}`), // n: string
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
client.register(greet, ping, event("clientReady", (c) => console.log(c.user.tag)));
|
|
61
|
+
await client.start(process.env.DISCORD_TOKEN);
|
|
62
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID });
|
|
63
|
+
```
|
|
@@ -0,0 +1,589 @@
|
|
|
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
|
+
| `register(...items: Registerable[])` | `this` | Route each item to the matching registry. |
|
|
28
|
+
| `use(...plugins: SpearPlugin[])` | `Promise<this>` | Run each plugin's `setup`. |
|
|
29
|
+
| `load(dir: string, options?: LoadOptions)` | `Promise<number>` | Import a directory and register its exports. Returns count. |
|
|
30
|
+
| `start(token?: string)` | `Promise<this>` | Log in (falls back to `DISCORD_TOKEN`). |
|
|
31
|
+
| `deployCommands(options?: { guildId?: string })` | `Promise<DeployResult>` | Push commands using the client's REST. Call after ready. |
|
|
32
|
+
|
|
33
|
+
Inherits everything from discord.js `Client` (`on`, `once`, `login`, `ws`, `rest`, `application`, `user`, …).
|
|
34
|
+
|
|
35
|
+
### `type SpearClientOptions = Partial<ClientOptions>`
|
|
36
|
+
|
|
37
|
+
Same as discord.js `ClientOptions`, but `intents` may be omitted (defaults to `Intents.default`).
|
|
38
|
+
|
|
39
|
+
### `const Intents`
|
|
40
|
+
|
|
41
|
+
Ready-made intent presets (arrays of `GatewayIntentBits`).
|
|
42
|
+
|
|
43
|
+
| Key | Contents |
|
|
44
|
+
| --- | -------- |
|
|
45
|
+
| `Intents.none` | `[]` |
|
|
46
|
+
| `Intents.default` | `[Guilds]` |
|
|
47
|
+
| `Intents.guilds` | `[Guilds, GuildMembers]` |
|
|
48
|
+
| `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
|
|
49
|
+
| `Intents.all` | Every intent (includes privileged). |
|
|
50
|
+
|
|
51
|
+
### `type Registerable = SlashCommand | EventDef | ComponentDef`
|
|
52
|
+
|
|
53
|
+
The union accepted by `SpearClient.register`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
### `function command<O, R>(config): SlashCommand`
|
|
60
|
+
|
|
61
|
+
Define a leaf slash command.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
interface CommandConfig<O extends OptionMap, R> {
|
|
65
|
+
name: string;
|
|
66
|
+
description: string;
|
|
67
|
+
options?: O;
|
|
68
|
+
defaultMemberPermissions?: PermissionResolvable | null;
|
|
69
|
+
nsfw?: boolean;
|
|
70
|
+
guildOnly?: boolean;
|
|
71
|
+
nameLocalizations?: LocalizationMap;
|
|
72
|
+
descriptionLocalizations?: LocalizationMap;
|
|
73
|
+
run: (ctx: CommandContext<O>) => Awaitable<R>;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `function commandGroup(config: CommandGroupConfig): SlashCommand`
|
|
78
|
+
|
|
79
|
+
Define a command that routes to subcommands and/or subcommand groups.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
interface CommandGroupConfig {
|
|
83
|
+
name: string;
|
|
84
|
+
description: string;
|
|
85
|
+
subcommands?: Record<string, Subcommand>;
|
|
86
|
+
groups?: Record<string, SubcommandGroup>;
|
|
87
|
+
defaultMemberPermissions?: PermissionResolvable | null;
|
|
88
|
+
nsfw?: boolean;
|
|
89
|
+
guildOnly?: boolean;
|
|
90
|
+
nameLocalizations?: LocalizationMap;
|
|
91
|
+
descriptionLocalizations?: LocalizationMap;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `function subcommand<O, R>(config): Subcommand`
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
interface SubcommandConfig<O extends OptionMap, R> {
|
|
99
|
+
description: string;
|
|
100
|
+
options?: O;
|
|
101
|
+
nameLocalizations?: LocalizationMap;
|
|
102
|
+
descriptionLocalizations?: LocalizationMap;
|
|
103
|
+
run: (ctx: CommandContext<O>) => Awaitable<R>;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `function subcommandGroup(config: SubcommandGroupConfig): SubcommandGroup`
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
interface SubcommandGroupConfig {
|
|
111
|
+
description: string;
|
|
112
|
+
subcommands: Record<string, Subcommand>;
|
|
113
|
+
nameLocalizations?: LocalizationMap;
|
|
114
|
+
descriptionLocalizations?: LocalizationMap;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `class SlashCommand`
|
|
119
|
+
|
|
120
|
+
| Member | Type | Description |
|
|
121
|
+
| ------ | ---- | ----------- |
|
|
122
|
+
| `name` | `string` | Top-level command name. |
|
|
123
|
+
| `hasAutocomplete` | `boolean` | True if any option declares autocomplete. |
|
|
124
|
+
| `toJSON()` | `RESTPostAPIChatInputApplicationCommandsJSONBody` | REST payload. |
|
|
125
|
+
| `execute(interaction)` | `Promise<void>` | Run for a chat-input interaction. |
|
|
126
|
+
| `autocomplete(interaction)` | `Promise<void>` | Run autocomplete for the focused option. |
|
|
127
|
+
|
|
128
|
+
### `class CommandContext<O> extends BaseContext<ChatInputCommandInteraction>`
|
|
129
|
+
|
|
130
|
+
| Member | Type | Description |
|
|
131
|
+
| ------ | ---- | ----------- |
|
|
132
|
+
| `options` | `ResolvedOptions<O>` | Resolved, fully-typed option values. |
|
|
133
|
+
| `commandName` | `string` | Invoked command name. |
|
|
134
|
+
| `subcommand` | `string \| null` | Invoked subcommand, if any. |
|
|
135
|
+
| `showModal(modal)` | `Promise<void>` | Present a modal. |
|
|
136
|
+
|
|
137
|
+
Plus all `BaseContext` members.
|
|
138
|
+
|
|
139
|
+
### `class CommandRegistry`
|
|
140
|
+
|
|
141
|
+
| Member | Type | Description |
|
|
142
|
+
| ------ | ---- | ----------- |
|
|
143
|
+
| `add(...commands: SlashCommand[])` | `this` | Register commands (override by name). |
|
|
144
|
+
| `remove(name: string)` | `boolean` | Remove a command. |
|
|
145
|
+
| `get(name: string)` | `SlashCommand \| undefined` | Look up a command. |
|
|
146
|
+
| `all()` | `SlashCommand[]` | All commands. |
|
|
147
|
+
| `names` | `string[]` | All command names. |
|
|
148
|
+
| `size` | `number` | Count. |
|
|
149
|
+
| `onError(handler: CommandErrorHandler)` | `this` | Set the error handler. |
|
|
150
|
+
| `toJSON()` | `RESTPostAPIApplicationCommandsJSONBody[]` | Serialise all commands. |
|
|
151
|
+
| `handle(interaction)` | `Promise<void>` | Dispatch a chat-input interaction. |
|
|
152
|
+
| `handleAutocomplete(interaction)` | `Promise<void>` | Dispatch an autocomplete interaction. |
|
|
153
|
+
| `deploy(options: DeployOptions)` | `Promise<DeployResult>` | Push commands to discord. |
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
type CommandErrorHandler = (error: Error, interaction: ChatInputCommandInteraction) => Awaitable<void>;
|
|
157
|
+
interface DeployOptions { token?: string; applicationId: string; guildId?: string; rest?: REST; }
|
|
158
|
+
type DeployResult = RESTPutAPIApplicationCommandsResult | RESTPutAPIApplicationGuildCommandsResult;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Options
|
|
164
|
+
|
|
165
|
+
### `const option`
|
|
166
|
+
|
|
167
|
+
Type-safe option builders. Each returns an `OptionDef` whose resolved value type
|
|
168
|
+
is inferred (required → value, optional → `value | undefined`, `choices` →
|
|
169
|
+
literal union).
|
|
170
|
+
|
|
171
|
+
| Builder | Resolved type | Extra config |
|
|
172
|
+
| ------- | ------------- | ------------ |
|
|
173
|
+
| `option.string(config)` | `string` | `choices?`, `minLength?`, `maxLength?`, `autocomplete?` |
|
|
174
|
+
| `option.integer(config)` | `number` | `choices?`, `minValue?`, `maxValue?`, `autocomplete?` |
|
|
175
|
+
| `option.number(config)` | `number` | `choices?`, `minValue?`, `maxValue?`, `autocomplete?` |
|
|
176
|
+
| `option.boolean(config)` | `boolean` | — |
|
|
177
|
+
| `option.user(config)` | `User` | — |
|
|
178
|
+
| `option.channel(config)` | channel union | `channelTypes?` |
|
|
179
|
+
| `option.role(config)` | `Role \| APIRole` | — |
|
|
180
|
+
| `option.mentionable(config)` | user/role/member | — |
|
|
181
|
+
| `option.attachment(config)` | `Attachment` | — |
|
|
182
|
+
|
|
183
|
+
Common config (`BaseConfig`):
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
{
|
|
187
|
+
description: string;
|
|
188
|
+
required?: boolean; // default false
|
|
189
|
+
nameLocalizations?: LocalizationMap;
|
|
190
|
+
descriptionLocalizations?: LocalizationMap;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`choices` items are `OptionChoice<V>`:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
interface OptionChoice<V extends string | number = string | number> {
|
|
198
|
+
name: string;
|
|
199
|
+
value: V;
|
|
200
|
+
nameLocalizations?: LocalizationMap;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`autocomplete`:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
type AutocompleteHandler<V extends string | number> =
|
|
208
|
+
(ctx: AutocompleteContext) => Awaitable<OptionChoice<V>[]>;
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Option types
|
|
212
|
+
|
|
213
|
+
| Symbol | Description |
|
|
214
|
+
| ------ | ----------- |
|
|
215
|
+
| `interface OptionDef<TValue, TRequired>` | A described option (phantom-typed for inference). |
|
|
216
|
+
| `type AnyOptionDef` | `OptionDef<OptionValue, boolean>`. |
|
|
217
|
+
| `type OptionMap` | `Record<string, AnyOptionDef>`. |
|
|
218
|
+
| `type ResolvedOption<O>` | The handler value for one option. |
|
|
219
|
+
| `type ResolvedOptions<O>` | The handler's `options` object. |
|
|
220
|
+
| `type OptionValue` | Union of all possible resolved values. |
|
|
221
|
+
| `type AllowedChannelType` | Channel types valid for a channel option. |
|
|
222
|
+
| `function toAPIOption(name, def)` | Serialise one option to REST. |
|
|
223
|
+
| `function readOption(resolver, name, def)` | Read a resolved value (null → undefined). |
|
|
224
|
+
| `function optionsHaveAutocomplete(options)` | True if any option has autocomplete. |
|
|
225
|
+
|
|
226
|
+
### `class AutocompleteContext`
|
|
227
|
+
|
|
228
|
+
| Member | Type | Description |
|
|
229
|
+
| ------ | ---- | ----------- |
|
|
230
|
+
| `interaction` | `AutocompleteInteraction` | Raw interaction. |
|
|
231
|
+
| `client` / `user` / `guild` / `guildId` | — | Convenience accessors. |
|
|
232
|
+
| `commandName` | `string` | Command being completed. |
|
|
233
|
+
| `focusedName` | `string` | Name of the focused option. |
|
|
234
|
+
| `value` | `string` | Current partial value typed by the user. |
|
|
235
|
+
| `respond(choices: OptionChoice[])` | `Promise<void>` | Send up to 25 suggestions. |
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Events
|
|
240
|
+
|
|
241
|
+
### `function event(name, run): EventDef` / `function event(config): EventDef`
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
type EventHandler<E extends keyof ClientEvents> = (...args: ClientEvents[E]) => Awaitable<void>;
|
|
245
|
+
interface EventConfig<E extends keyof ClientEvents> { name: E; once?: boolean; run: EventHandler<E>; }
|
|
246
|
+
interface EventDef { name: keyof ClientEvents; once: boolean; attach(client: Client): void; detach(client: Client): void; }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Thrown errors and rejected promises are routed to the client's `error` event.
|
|
250
|
+
|
|
251
|
+
### `class EventRegistry`
|
|
252
|
+
|
|
253
|
+
| Member | Type | Description |
|
|
254
|
+
| ------ | ---- | ----------- |
|
|
255
|
+
| `add(...defs: EventDef[])` | `this` | Register listeners. |
|
|
256
|
+
| `size` | `number` | Count. |
|
|
257
|
+
| `attachAll(client: Client)` | `void` | Attach every listener. |
|
|
258
|
+
| `detachAll(client: Client)` | `void` | Detach every listener. |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Components
|
|
263
|
+
|
|
264
|
+
### Builders
|
|
265
|
+
|
|
266
|
+
| Function | Returns | Notes |
|
|
267
|
+
| -------- | ------- | ----- |
|
|
268
|
+
| `button(config)` | `Button<P>` | Interactive button. |
|
|
269
|
+
| `linkButton(config)` | `ButtonBuilder` | URL button, no handler. |
|
|
270
|
+
| `stringSelect(config)` | `StringSelect<P>` | String select; takes `options`. |
|
|
271
|
+
| `userSelect(config)` | `UserSelect<P>` | User select. |
|
|
272
|
+
| `roleSelect(config)` | `RoleSelect<P>` | Role select. |
|
|
273
|
+
| `channelSelect(config)` | `ChannelSelect<P>` | Channel select; takes `channelTypes?`. |
|
|
274
|
+
| `mentionableSelect(config)` | `MentionableSelect<P>` | User + role select. |
|
|
275
|
+
| `modal(config)` | `Modal<P>` | Modal with `fields`. |
|
|
276
|
+
| `textInput(config)` | `TextInputDef` | A modal text-input field. |
|
|
277
|
+
| `row(...components)` | `ActionRowBuilder<C>` | Wrap components in a row. |
|
|
278
|
+
|
|
279
|
+
Each registrable component (`Button`, `StringSelect`, …, `Modal`) extends its
|
|
280
|
+
routing interface and adds `build(...args: BuildArgs<P>)`, which returns the
|
|
281
|
+
discord.js builder. `build` requires exactly the params declared in the id
|
|
282
|
+
pattern.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
interface ButtonConfig<P extends string, R> {
|
|
286
|
+
id: P; // pattern: "name" or "name:{param}"
|
|
287
|
+
label?: string;
|
|
288
|
+
style?: ButtonStyleInput; // "Primary" | "Secondary" | "Success" | "Danger" | ButtonStyle.*
|
|
289
|
+
emoji?: ComponentEmojiResolvable;
|
|
290
|
+
disabled?: boolean;
|
|
291
|
+
run: (ctx: ButtonContext<Params<P>>) => Awaitable<R>;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface LinkButtonConfig { url: string; label?: string; emoji?: ComponentEmojiResolvable; disabled?: boolean; }
|
|
295
|
+
|
|
296
|
+
interface StringSelectConfig<P extends string, R> {
|
|
297
|
+
id: P;
|
|
298
|
+
options: readonly SelectMenuComponentOptionData[];
|
|
299
|
+
placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
|
|
300
|
+
run: (ctx: StringSelectContext<Params<P>>) => Awaitable<R>;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
interface EntitySelectConfig<P extends string> {
|
|
304
|
+
id: P; placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
|
|
305
|
+
}
|
|
306
|
+
// user/role/mentionable selects take EntitySelectConfig & { run };
|
|
307
|
+
// channelSelect additionally takes { channelTypes?: readonly ChannelType[] }.
|
|
308
|
+
|
|
309
|
+
function textInput(config: {
|
|
310
|
+
label: string;
|
|
311
|
+
style?: TextInputStyleInput; // "Short" | "Paragraph" | TextInputStyle
|
|
312
|
+
placeholder?: string; required?: boolean; minLength?: number; maxLength?: number; value?: string;
|
|
313
|
+
}): TextInputDef;
|
|
314
|
+
|
|
315
|
+
interface ModalConfig<P extends string, F extends Record<string, TextInputDef>, R> {
|
|
316
|
+
id: P;
|
|
317
|
+
title: string;
|
|
318
|
+
fields: F;
|
|
319
|
+
run: (ctx: ModalContext<Params<P>, keyof F & string>) => Awaitable<R>;
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Component contexts
|
|
324
|
+
|
|
325
|
+
| Class | Extra members |
|
|
326
|
+
| ----- | ------------- |
|
|
327
|
+
| `MessageComponentContext<P, I>` | `params`, `customId`, `message`, `update(input)`, `deferUpdate()`, `showModal(modal)` (+ BaseContext) |
|
|
328
|
+
| `ButtonContext<P>` | — |
|
|
329
|
+
| `StringSelectContext<P>` | `values: string[]`, `value: string \| undefined` |
|
|
330
|
+
| `UserSelectContext<P>` | `values`, `users`, `members` |
|
|
331
|
+
| `RoleSelectContext<P>` | `values`, `roles` |
|
|
332
|
+
| `ChannelSelectContext<P>` | `values`, `channels` |
|
|
333
|
+
| `MentionableSelectContext<P>` | `values`, `users`, `roles`, `members` |
|
|
334
|
+
| `ModalContext<P, F>` | `params`, `fields: Record<F, string>`, `customId` (+ BaseContext) |
|
|
335
|
+
|
|
336
|
+
### `class ComponentRegistry`
|
|
337
|
+
|
|
338
|
+
| Member | Type | Description |
|
|
339
|
+
| ------ | ---- | ----------- |
|
|
340
|
+
| `add(...defs: ComponentDef[])` | `this` | Register components (override by namespace). |
|
|
341
|
+
| `onError(handler: ComponentErrorHandler)` | `this` | Set the error handler. |
|
|
342
|
+
| `size` | `number` | Count. |
|
|
343
|
+
| `handle(interaction: Interaction)` | `Promise<boolean>` | Route an interaction; `true` if matched. |
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
type ComponentErrorHandler = (error: Error, interaction: RepliableInteraction) => Awaitable<void>;
|
|
347
|
+
type ComponentDef = ButtonRoute | StringSelectRoute | UserSelectRoute | RoleSelectRoute
|
|
348
|
+
| ChannelSelectRoute | MentionableSelectRoute | ModalRoute;
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Custom-id codec
|
|
352
|
+
|
|
353
|
+
| Symbol | Description |
|
|
354
|
+
| ------ | ----------- |
|
|
355
|
+
| `type ParamNames<S>` | Union of `{param}` names in a pattern. |
|
|
356
|
+
| `type Params<S>` | The params object a pattern resolves to. |
|
|
357
|
+
| `type BuildArgs<S>` | `build()` args (none when no params). |
|
|
358
|
+
| `const MAX_CUSTOM_ID_LENGTH` | `100`. |
|
|
359
|
+
| `function compilePattern(pattern)` | → `CompiledPattern { pattern, namespace, paramNames }`. |
|
|
360
|
+
| `function buildCustomId(compiled, params)` | Encode a concrete id. |
|
|
361
|
+
| `function parseCustomId(customId)` | → `ParsedCustomId { namespace, values }`. |
|
|
362
|
+
| `function paramsFromValues(paramNames, values)` | Map values onto names. |
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Contexts (shared)
|
|
367
|
+
|
|
368
|
+
### `abstract class BaseContext<I>`
|
|
369
|
+
|
|
370
|
+
The base for every interaction context.
|
|
371
|
+
|
|
372
|
+
| Member | Type | Description |
|
|
373
|
+
| ------ | ---- | ----------- |
|
|
374
|
+
| `interaction` | `I` | Raw discord.js interaction. |
|
|
375
|
+
| `client` / `user` / `member` / `guild` / `guildId` / `channel` / `channelId` / `locale` | — | Accessors. |
|
|
376
|
+
| `deferred` / `replied` | `boolean` | Interaction state. |
|
|
377
|
+
| `reply(input)` | `Promise<InteractionResponse>` | Initial response. |
|
|
378
|
+
| `replyEphemeral(input)` | `Promise<InteractionResponse>` | Hidden reply. |
|
|
379
|
+
| `defer({ ephemeral? })` | `Promise<InteractionResponse>` | Acknowledge, respond later. |
|
|
380
|
+
| `editReply(input)` | `Promise<Message>` | Edit the response. |
|
|
381
|
+
| `followUp(input)` | `Promise<Message>` | Additional message. |
|
|
382
|
+
| `send(input)` | `Promise<void>` | State-aware reply/edit/followUp. |
|
|
383
|
+
| `error(message)` | `Promise<void>` | State-aware ephemeral error. |
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
type ReplyData = InteractionReplyOptions & { ephemeral?: boolean };
|
|
387
|
+
type ReplyInput = string | ReplyData;
|
|
388
|
+
function normalizeReply(input: ReplyInput): InteractionReplyOptions;
|
|
389
|
+
function asEphemeral(input: ReplyInput): ReplyData;
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Plugins
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
interface SpearPlugin { name: string; setup(client: SpearClient): Awaitable<void>; }
|
|
398
|
+
function definePlugin(plugin: SpearPlugin): SpearPlugin;
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Loading
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
interface LoadOptions { extensions?: readonly string[]; recursive?: boolean; } // defaults: [.js,.mjs,.cjs], true
|
|
407
|
+
function collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>;
|
|
408
|
+
function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>;
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`SpearClient.load(dir, options?)` is the method form of `loadInto`.
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Added in 0.2
|
|
416
|
+
|
|
417
|
+
New subsystems, each with a dedicated guide. The `SpearClient` options
|
|
418
|
+
`{ logger?, dotenv?, cooldown?, prefix?, usage? }` configure them.
|
|
419
|
+
|
|
420
|
+
### Logging — [guide](./logging.md)
|
|
421
|
+
|
|
422
|
+
```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; }
|
|
424
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
425
|
+
type LogThreshold = LogLevel | "silent";
|
|
426
|
+
function consoleSink(entry: LogEntry): void;
|
|
427
|
+
function toError(value: unknown): Error;
|
|
428
|
+
// client.logger is a Logger; new SpearClient({ logger: { level: "debug" } })
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Environment — [guide](./env.md)
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
function parseEnv(content: string): Record<string, string>;
|
|
435
|
+
function loadEnv(options?: { path?: string; override?: boolean }): Record<string, string>;
|
|
436
|
+
const env: { string(k, fallback?); number(k, fallback?); boolean(k, fallback?); require(k): string };
|
|
437
|
+
// client auto-loads .env on start(); disable/configure via the dotenv option
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Cooldowns — [guide](./cooldown.md)
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
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
|
+
class CooldownManager { consume(bucket, input, actor, now?); peek(...); reset(...); clear(); }
|
|
445
|
+
// command({ cooldown: number | CooldownConfig }); new SpearClient({ cooldown }); client.cooldowns
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Scheduled tasks — [guide](./scheduler.md)
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
function task(config: { name: string; cron?: string; interval?: number; runOnStart?: boolean; run: (client: SpearClient) => Awaitable<void> }): ScheduledTask;
|
|
452
|
+
function cron(expression: string): CronExpression; // .next(from?: Date): Date
|
|
453
|
+
class TaskScheduler { add/remove/list/size/active/start/stop }
|
|
454
|
+
// client.register(task(...)); client.schedule(config); client.scheduler
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Prefix commands — [guide](./prefix.md)
|
|
458
|
+
|
|
459
|
+
```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); }
|
|
462
|
+
// new SpearClient({ prefix: "!" | string[] | { prefix, mention?, ignoreBots?, caseInsensitive? } }); client.prefix
|
|
463
|
+
// reading others' content needs the privileged MessageContent intent (Intents.messages)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Usage tracking — [guide](./usage.md)
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
interface UsageEvent { type: "command" | "prefix" | "component" | "event"; name: string; userId?; userTag?; guildId?; channelId?; detail?; timestamp: Date; }
|
|
470
|
+
interface UsageStore { record(event): Awaitable<void>; all(): Awaitable<readonly UsageEvent[]>; }
|
|
471
|
+
class MemoryUsageStore { record; all; size; byUser(id); clear; }
|
|
472
|
+
class JsonFileUsageStore { constructor(path: string); record; all; }
|
|
473
|
+
class UsageTracker { setStore(store); reportTo(channelId, format?); track(event); store; enabled; }
|
|
474
|
+
// new SpearClient({ usage: { store?, channel?, format? } }); client.usage
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Added in 0.3
|
|
480
|
+
|
|
481
|
+
Driven by patterns repeated across long-running production bots: the role/
|
|
482
|
+
permission checks, `.catch(() => null)` fetches, embed factories, pagination
|
|
483
|
+
/confirm flows, mention/duration parsing, locks, config loaders and pluggable
|
|
484
|
+
log/usage transports a real Discord bot ends up writing.
|
|
485
|
+
|
|
486
|
+
### Embeds — preset replies
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
class Embeds { error(input); success(input); info(input); warn(input); build(level, input); }
|
|
490
|
+
function createEmbeds(opts?): Embeds; // alias for new Embeds(opts)
|
|
491
|
+
// SpearClient owns one as `client.embeds`; configure via the `embeds` option.
|
|
492
|
+
// BaseContext gains ctx.success/info/warn/error (state-aware send) + replySuccess/replyInfo/replyWarn/replyError.
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Guards — declarative preconditions
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
type Guard<TCtx extends GuardContext = GuardContext> = (ctx: TCtx) => Awaitable<GuardResult>;
|
|
499
|
+
function denied(reason?: string): GuardResult;
|
|
500
|
+
function guildOnly(reason?: string): Guard;
|
|
501
|
+
function dmOnly(reason?: string): Guard;
|
|
502
|
+
function requireAnyRole(roleIds: readonly string[], reason?: string): Guard;
|
|
503
|
+
function requireAllRoles(roleIds: readonly string[], reason?: string): Guard;
|
|
504
|
+
function requireOwner(ownerIds: readonly string[], reason?: string): Guard;
|
|
505
|
+
function requireUserPermissions(permission: PermissionResolvable, reason?: string): Guard;
|
|
506
|
+
function requireBotPermissions(permission: PermissionResolvable, reason?: string): Guard;
|
|
507
|
+
function guard<TCtx>(predicate: Guard<TCtx>): Guard<TCtx>;
|
|
508
|
+
// per-handler: command({ guards: [...] }), prefixCommand({ guards }), button({ guards }), userCommand({ guards }), ...
|
|
509
|
+
// client-wide: new SpearClient({ guards: [...] })
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Context-menu commands
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
function userCommand({ name, run: (ctx: UserContextMenuContext) => Awaitable<R>, guards?, cooldown? }): UserContextMenu;
|
|
516
|
+
function messageCommand({ name, run: (ctx: MessageContextMenuContext) => Awaitable<R>, guards?, cooldown? }): MessageContextMenu;
|
|
517
|
+
// SpearClient.deployAllCommands deploys slash + context menus in one PUT.
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Prefix typed arguments
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
function prefixArgs(): PrefixArgsBuilder<{}>;
|
|
524
|
+
// builder methods: .string/.integer/.number/.boolean/.snowflake/.duration/.rest
|
|
525
|
+
// prefixCommand<TArgs>({ args: a => a.snowflake("target").duration("dur").rest("reason"), run: ctx => ctx.options }))
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Pagination + Confirmation
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
function paginate<T>(interaction, items, { pageSize, render, user?, timeoutMs?, controls?, ephemeral? }): Promise<void>;
|
|
532
|
+
function buildPaginatorPage<T>(items, page, options): Promise<{ payload; pages }>;
|
|
533
|
+
function confirm(interaction, { title?, body, confirm?, cancel?, user?, timeoutMs?, ephemeral? }): Promise<{ confirmed, reason, interaction? }>;
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Primitives
|
|
537
|
+
|
|
538
|
+
```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
|
|
541
|
+
function withSafeTimeout<T>(p: Promise<T>, ms): Promise<T | null>;
|
|
542
|
+
function formatDuration(ms, { locale?: "en" | "tr" | UnitLabels; largest?; units? }): string;
|
|
543
|
+
function parseDuration(input: string): number | null;
|
|
544
|
+
function discordTimestamp(date, style?: "t"|"T"|"d"|"D"|"f"|"F"|"R"): string;
|
|
545
|
+
function relativeTimestamp(date): string;
|
|
546
|
+
interface CacheStore { get; set; delete; has; increment; rateLimit; clear; }
|
|
547
|
+
class MemoryCache implements CacheStore { /* TTL, counter, fixed-window rate limit */ }
|
|
548
|
+
function loadConfig<T>({ file, parser?, schema?, encoding? }): T;
|
|
549
|
+
function loadConfigAsync<T>(opts): Promise<T>;
|
|
550
|
+
function lookup<K, V>(table, resourceName?): (key: K) => V;
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Logger transports
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
new Logger({ level, transports: [consoleSink, jsonlSink("./logs/bot.jsonl"), webhookSink({ url, minLevel: "error" })] });
|
|
557
|
+
function jsonlSink(path: string, { minLevel? }?): LogSink;
|
|
558
|
+
function webhookSink({ url, minLevel?, username? }): LogSink;
|
|
559
|
+
// Logger.addTransport(sink), setTransports([sinks])
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Scheduler — one-shot + reconcile
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
client.scheduler.delay(name, ms, fn) -> { cancel(): boolean };
|
|
566
|
+
client.scheduler.followUp(name, [10_000, 30_000, 60_000], (i) => ...) -> { cancel(): boolean };
|
|
567
|
+
client.scheduler.reconcile("voice-sessions", async (client) => { /* once on ready */ });
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Deploy diff + dry run
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
client.deployAllCommands({ guildId, dryRun: true }); // returns { skipped, body, reason: "dry-run" }
|
|
574
|
+
client.deployAllCommands({ guildId, strategy: "diff" }); // skips PUT when remote matches
|
|
575
|
+
client.deployAllCommands({ applicationId: "...", strategy: "diff" }); // explicit app id, no ready required
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### Usage outcome + duration
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
interface UsageEvent {
|
|
582
|
+
type; name; userId; userTag; guildId; channelId; detail?;
|
|
583
|
+
outcome?: "success" | "error";
|
|
584
|
+
durationMs?: number;
|
|
585
|
+
errorMessage?: string;
|
|
586
|
+
options?: Record<string, string | number | boolean | null>;
|
|
587
|
+
timestamp: Date;
|
|
588
|
+
}
|
|
589
|
+
```
|