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,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.
|
package/docs/client.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Client
|
|
2
|
+
|
|
3
|
+
`SpearClient` is a discord.js `Client` with command, event and component
|
|
4
|
+
registries — plus interaction routing — wired up for you. You construct it the
|
|
5
|
+
same way you construct a discord.js client, register your handlers, log in, and
|
|
6
|
+
(optionally) push your slash commands to Discord.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { SpearClient, Intents } from "spearkit";
|
|
10
|
+
|
|
11
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Constructing a client
|
|
15
|
+
|
|
16
|
+
`new SpearClient(options?)` takes the same options as discord.js'
|
|
17
|
+
`ClientOptions`, except `intents` may be omitted: it defaults to
|
|
18
|
+
`Intents.default` (just the `Guilds` intent, enough for slash commands and
|
|
19
|
+
interactions).
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { SpearClient, Intents } from "spearkit";
|
|
23
|
+
|
|
24
|
+
// Explicit preset.
|
|
25
|
+
const a = new SpearClient({ intents: Intents.messages });
|
|
26
|
+
|
|
27
|
+
// Omitted — falls back to Intents.default.
|
|
28
|
+
const b = new SpearClient();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The options type is exported as `SpearClientOptions` — `Partial<ClientOptions> &
|
|
32
|
+
SpearOptions`. Every discord.js option (`partials`, `presence`, `sweepers`, …) is
|
|
33
|
+
available, plus spearkit's own: `logger`, `dotenv`, `cooldown`, `prefix`, `usage`,
|
|
34
|
+
`embeds`, `guards` and `autoDefer` (each covered in its own guide).
|
|
35
|
+
|
|
36
|
+
### Intents presets
|
|
37
|
+
|
|
38
|
+
`Intents` is a set of ready-made arrays of `GatewayIntentBits`. Pass one as
|
|
39
|
+
`intents`, or compose your own array of `GatewayIntentBits` if you need
|
|
40
|
+
something in between.
|
|
41
|
+
|
|
42
|
+
| Preset | Contents |
|
|
43
|
+
| ------ | -------- |
|
|
44
|
+
| `Intents.none` | `[]` |
|
|
45
|
+
| `Intents.default` | `[Guilds]` |
|
|
46
|
+
| `Intents.guilds` | `[Guilds, GuildMembers]` |
|
|
47
|
+
| `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
|
|
48
|
+
| `Intents.all` | Every intent, including privileged ones. |
|
|
49
|
+
|
|
50
|
+
`Intents.messages` includes `MessageContent`, and `Intents.guilds` includes
|
|
51
|
+
`GuildMembers` — both are **privileged intents**. You must enable them in the
|
|
52
|
+
Discord developer portal for your application, otherwise the gateway will reject
|
|
53
|
+
the connection. `Intents.all` includes every privileged intent for the same
|
|
54
|
+
reason.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { SpearClient, GatewayIntentBits } from "spearkit";
|
|
58
|
+
|
|
59
|
+
// A custom intent set, mixing a preset idea with explicit bits.
|
|
60
|
+
const client = new SpearClient({
|
|
61
|
+
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Registries and subsystems
|
|
66
|
+
|
|
67
|
+
Every client owns a set of registries and subsystems, each populated by
|
|
68
|
+
`register` (or `load`) or configured by an option:
|
|
69
|
+
|
|
70
|
+
| Member | Type | Holds / does |
|
|
71
|
+
| ------ | ---- | ------------ |
|
|
72
|
+
| `client.commands` | `CommandRegistry` | Slash commands; dispatches chat-input and autocomplete interactions. |
|
|
73
|
+
| `client.events` | `EventRegistry` | Event listeners; attached to the client automatically. |
|
|
74
|
+
| `client.components` | `ComponentRegistry` | Buttons, selects and modals; routed by custom-id namespace. |
|
|
75
|
+
| `client.contextMenus` | `ContextMenuRegistry` | User / message context-menu ("Apps") commands. |
|
|
76
|
+
| `client.prefix` | `PrefixRegistry` | Prefix (text) commands, dispatched from `messageCreate`. |
|
|
77
|
+
| `client.scheduler` | `TaskScheduler` | Cron / interval tasks; started on ready, stopped on `destroy`. |
|
|
78
|
+
| `client.cooldowns` | `CooldownManager` | Shared rate-limit state across commands and prefix commands. |
|
|
79
|
+
| `client.usage` | `UsageTracker` | Records who used what to a store and/or channel. |
|
|
80
|
+
| `client.logger` | `Logger` | Structured, scoped logger used across spearkit. |
|
|
81
|
+
| `client.embeds` | `Embeds` | Preset embed factory behind `ctx.success/error/...`. |
|
|
82
|
+
|
|
83
|
+
You rarely touch the registries directly — `register` routes items into the right
|
|
84
|
+
one — but they are public for inspection and advanced control (e.g.
|
|
85
|
+
`client.commands.size`, `client.commands.toJSON()`). Each subsystem has its own
|
|
86
|
+
guide: [Cooldowns](./cooldown.md), [Scheduled tasks](./scheduler.md),
|
|
87
|
+
[Prefix commands](./prefix.md), [Context menus](./context-menus.md),
|
|
88
|
+
[Logging](./logging.md), [Usage tracking](./usage.md), [Guards](./guards.md).
|
|
89
|
+
|
|
90
|
+
## Registering handlers
|
|
91
|
+
|
|
92
|
+
`client.register(...items)` accepts commands, events, components, context-menu
|
|
93
|
+
commands, prefix commands and scheduled tasks in a single call and routes each to
|
|
94
|
+
its registry by kind. The accepted union is exported as `Registerable`
|
|
95
|
+
(`SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand |
|
|
96
|
+
ContextMenuCommand`). It returns the client for chaining.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { SpearClient, command, event, button, option } from "spearkit";
|
|
100
|
+
|
|
101
|
+
const client = new SpearClient();
|
|
102
|
+
|
|
103
|
+
const greet = command({
|
|
104
|
+
name: "greet",
|
|
105
|
+
description: "Greet someone",
|
|
106
|
+
options: { who: option.user({ description: "Who", required: true }) },
|
|
107
|
+
run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const ready = event("clientReady", (c) => {
|
|
111
|
+
console.log(`Logged in as ${c.user.tag}`); // c: Client<true>
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const ping = button({
|
|
115
|
+
id: "ping:{n}",
|
|
116
|
+
label: "Ping",
|
|
117
|
+
run: (ctx) => ctx.reply(`pong #${ctx.params.n}`), // n: string
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Commands, events and components in one call.
|
|
121
|
+
client.register(greet, ready, ping);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Plugins
|
|
125
|
+
|
|
126
|
+
`client.use(...plugins)` installs one or more plugins, awaiting each plugin's
|
|
127
|
+
`setup`. It is async and returns the client.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { SpearClient } from "spearkit";
|
|
131
|
+
import { statsPlugin } from "./plugins/stats.js";
|
|
132
|
+
|
|
133
|
+
const client = new SpearClient();
|
|
134
|
+
await client.use(statsPlugin);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
See [Plugins](./plugins.md) for authoring `SpearPlugin`s.
|
|
138
|
+
|
|
139
|
+
## File-based loading
|
|
140
|
+
|
|
141
|
+
`client.load(dir, options?)` recursively imports a directory and registers every
|
|
142
|
+
spearkit-registrable export it finds — commands, events, components, scheduled
|
|
143
|
+
tasks and prefix commands. It returns the number of items registered.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { SpearClient } from "spearkit";
|
|
147
|
+
|
|
148
|
+
const client = new SpearClient();
|
|
149
|
+
const count = await client.load("./src/commands");
|
|
150
|
+
console.log(`Loaded ${count} handlers`);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
See [File-based loading](./loading.md) for the layout and `LoadOptions`.
|
|
154
|
+
|
|
155
|
+
## Starting and deploying
|
|
156
|
+
|
|
157
|
+
`client.start(token?)` logs in. If you omit the token it falls back to the
|
|
158
|
+
`DISCORD_TOKEN` environment variable, and throws if neither is present.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { SpearClient } from "spearkit";
|
|
162
|
+
|
|
163
|
+
const client = new SpearClient();
|
|
164
|
+
|
|
165
|
+
// Pass a token explicitly…
|
|
166
|
+
await client.start("your-token");
|
|
167
|
+
|
|
168
|
+
// …or set DISCORD_TOKEN and call start() with no argument.
|
|
169
|
+
await client.start();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`client.deployCommands({ guildId })` pushes the registered slash commands to
|
|
173
|
+
Discord using the client's own authenticated REST connection — there is no
|
|
174
|
+
separate token or application id to supply. Because it reads the application id
|
|
175
|
+
from the logged-in client, it **must run after the client is ready**. Pass a
|
|
176
|
+
`guildId` to deploy instantly to a single guild (ideal for development); omit it
|
|
177
|
+
to deploy globally.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { SpearClient, Intents } from "spearkit";
|
|
181
|
+
|
|
182
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
183
|
+
// …register commands…
|
|
184
|
+
|
|
185
|
+
await client.start(); // uses DISCORD_TOKEN
|
|
186
|
+
|
|
187
|
+
// Deploy once the client is ready.
|
|
188
|
+
client.once("clientReady", async () => {
|
|
189
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID });
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Reliability: auto-defer and graceful shutdown
|
|
194
|
+
|
|
195
|
+
A slow handler that doesn't respond within Discord's 3-second window dies with
|
|
196
|
+
`Unknown interaction` (10062). Set `autoDefer` to have spearkit `deferReply()`
|
|
197
|
+
automatically just before that window closes — per handler (`command({ autoDefer:
|
|
198
|
+
true })`, `userCommand`/`messageCommand`) or for every slash + context-menu
|
|
199
|
+
handler at once:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const client = new SpearClient({ autoDefer: true });
|
|
203
|
+
// or { ephemeral: true, delayMs: 1500 } for a hidden defer / earlier fire.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
With auto-defer on, respond via `ctx.send(...)` or `ctx.editReply(...)` — the
|
|
207
|
+
initial reply slot may already be taken by the safety defer.
|
|
208
|
+
|
|
209
|
+
`client.enableGracefulShutdown(options?)` closes the bot cleanly on `SIGINT` /
|
|
210
|
+
`SIGTERM`: it runs an optional `onShutdown` hook, calls `destroy()` (stopping the
|
|
211
|
+
scheduler and gateway), and exits, with a hard timeout so a wedged shutdown can't
|
|
212
|
+
hang. It returns a disposer that removes the signal handlers.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
client.enableGracefulShutdown({ onShutdown: () => db.close() });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Everything discord.js still works
|
|
219
|
+
|
|
220
|
+
`SpearClient` extends discord.js `Client`, so the full client surface is
|
|
221
|
+
available unchanged. spearkit adds registries on top — it never hides what is
|
|
222
|
+
underneath:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { SpearClient } from "spearkit";
|
|
226
|
+
|
|
227
|
+
const client = new SpearClient();
|
|
228
|
+
|
|
229
|
+
client.on("guildCreate", (guild) => console.log(`Joined ${guild.name}`));
|
|
230
|
+
client.ws.on("VOICE_SERVER_UPDATE", () => {});
|
|
231
|
+
|
|
232
|
+
await client.start();
|
|
233
|
+
|
|
234
|
+
console.log(client.application?.id); // application
|
|
235
|
+
console.log(client.user?.tag); // user
|
|
236
|
+
console.log(client.rest); // REST manager (used by deployCommands)
|
|
237
|
+
|
|
238
|
+
await client.destroy(); // graceful shutdown
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## See also
|
|
242
|
+
|
|
243
|
+
- [Commands](./commands.md) — defining slash commands you register here.
|
|
244
|
+
- [Plugins](./plugins.md) — bundling features for `client.use`.
|
|
245
|
+
- [File-based loading](./loading.md) — populating the client from a directory.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Collectors
|
|
2
|
+
|
|
3
|
+
discord.js collectors are powerful but fiddly: you wire an event emitter, set a
|
|
4
|
+
`time`, write a `filter`, remember that dismissed modals need their own timeout,
|
|
5
|
+
and translate the "timed out" rejection into something you can branch on.
|
|
6
|
+
spearkit collapses the common cases to a single `await` that resolves to the
|
|
7
|
+
result — or `null` on timeout.
|
|
8
|
+
|
|
9
|
+
Beyond these, see the [pagination and confirm helpers](./api-reference.md#pagination--confirmation)
|
|
10
|
+
for ready-made paged lists and yes/no gates.
|
|
11
|
+
|
|
12
|
+
## Wait for a message ("type your answer")
|
|
13
|
+
|
|
14
|
+
`ctx.awaitMessageFrom(userId?, options?)` waits for the next message in the
|
|
15
|
+
current channel from a user (defaults to the invoking user):
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
await ctx.reply("What's your favourite colour?");
|
|
19
|
+
const answer = await ctx.awaitMessageFrom(ctx.user.id, { time: 30_000 });
|
|
20
|
+
if (answer === null) return ctx.followUp("Timed out.");
|
|
21
|
+
await ctx.followUp(`Nice — ${answer.content}!`);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The standalone `awaitMessage(channel, options)` does the same for any text
|
|
25
|
+
channel; `options` takes `{ filter, time }` (default `time` 60s).
|
|
26
|
+
|
|
27
|
+
## Wait for a modal submission
|
|
28
|
+
|
|
29
|
+
`ctx.awaitModal(modal, options?)` (on command and component contexts) shows a
|
|
30
|
+
modal and waits for the submission — scoped to the same user and that modal's
|
|
31
|
+
custom-id, always bounded — sidestepping the "Unknown interaction after a
|
|
32
|
+
cancelled modal" trap:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { modal, textInput } from "spearkit";
|
|
36
|
+
|
|
37
|
+
const form = modal({
|
|
38
|
+
id: "feedback",
|
|
39
|
+
title: "Feedback",
|
|
40
|
+
fields: { text: textInput({ label: "Your feedback", required: true }) },
|
|
41
|
+
run: (ctx) => ctx.replyEphemeral("thanks"), // routed fallback
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const submitted = await ctx.awaitModal(form.build(), { time: 120_000 });
|
|
45
|
+
if (submitted === null) return; // dismissed or timed out
|
|
46
|
+
await submitted.reply({ content: submitted.fields.getTextInputValue("text"), ephemeral: true });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This is the inline alternative to registering a separate modal handler and
|
|
50
|
+
threading state through its custom-id.
|
|
51
|
+
|
|
52
|
+
## Wait for a component click
|
|
53
|
+
|
|
54
|
+
`awaitComponent(message, options)` waits for the next button/select interaction
|
|
55
|
+
on a message. `options` takes `{ filter, time, componentType }`. You must still
|
|
56
|
+
acknowledge the returned interaction (`update`/`deferUpdate`/`reply`):
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { awaitComponent } from "spearkit";
|
|
60
|
+
|
|
61
|
+
const sent = await ctx.channel!.send({ content: "Pick one", components: [row] });
|
|
62
|
+
const click = await awaitComponent(sent, { time: 15_000 });
|
|
63
|
+
if (click === null) return;
|
|
64
|
+
await click.update("Got it!");
|
|
65
|
+
```
|
package/docs/commands.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Commands
|
|
2
|
+
|
|
3
|
+
Slash commands in spearkit are defined as a single object: the metadata, the typed
|
|
4
|
+
options, and the handler all live together. spearkit serialises them for discord
|
|
5
|
+
and routes incoming interactions to the right handler for you.
|
|
6
|
+
|
|
7
|
+
## A first command
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { command } from "spearkit";
|
|
11
|
+
|
|
12
|
+
export const ping = command({
|
|
13
|
+
name: "ping",
|
|
14
|
+
description: "Check latency",
|
|
15
|
+
run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Register it on a client (`client.register(ping)`) and deploy it (see
|
|
20
|
+
[Deployment](#deployment)). That's the whole loop.
|
|
21
|
+
|
|
22
|
+
## The command context
|
|
23
|
+
|
|
24
|
+
The handler receives a `CommandContext`. It wraps the discord.js
|
|
25
|
+
`ChatInputCommandInteraction` and adds ergonomic accessors and reply helpers.
|
|
26
|
+
|
|
27
|
+
| Member | Description |
|
|
28
|
+
| ------ | ----------- |
|
|
29
|
+
| `ctx.options` | Resolved, fully-typed option values (see [Options](./options.md)). |
|
|
30
|
+
| `ctx.commandName` | The invoked command name. |
|
|
31
|
+
| `ctx.subcommand` | The invoked subcommand name, or `null`. |
|
|
32
|
+
| `ctx.showModal(modal)` | Present a modal in response. |
|
|
33
|
+
| `ctx.user` / `ctx.member` / `ctx.guild` / `ctx.guildId` / `ctx.channel` / `ctx.channelId` / `ctx.locale` | Actor and location accessors. |
|
|
34
|
+
| `ctx.reply` / `ctx.replyEphemeral` / `ctx.defer` / `ctx.editReply` / `ctx.followUp` / `ctx.send` / `ctx.error` | Reply helpers (see [Contexts](./context.md)). |
|
|
35
|
+
| `ctx.interaction` | The raw discord.js interaction, for anything not wrapped. |
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { command, option } from "spearkit";
|
|
39
|
+
|
|
40
|
+
export const echo = command({
|
|
41
|
+
name: "echo",
|
|
42
|
+
description: "Repeat a message",
|
|
43
|
+
options: {
|
|
44
|
+
text: option.string({ description: "What to say", required: true }),
|
|
45
|
+
times: option.integer({ description: "Repeat count", minValue: 1, maxValue: 5 }),
|
|
46
|
+
},
|
|
47
|
+
run: (ctx) => {
|
|
48
|
+
ctx.options.text; // string
|
|
49
|
+
ctx.options.times; // number | undefined
|
|
50
|
+
return ctx.reply({
|
|
51
|
+
content: ctx.options.text.repeat(ctx.options.times ?? 1),
|
|
52
|
+
ephemeral: true,
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Options are covered in depth in [Options](./options.md).
|
|
59
|
+
|
|
60
|
+
## Command metadata
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { command, PermissionFlagsBits } from "spearkit";
|
|
64
|
+
|
|
65
|
+
export const purge = command({
|
|
66
|
+
name: "purge",
|
|
67
|
+
description: "Delete recent messages",
|
|
68
|
+
guildOnly: true, // only usable in guilds
|
|
69
|
+
nsfw: false, // age-restricted command
|
|
70
|
+
defaultMemberPermissions: PermissionFlagsBits.ManageMessages, // who sees it by default
|
|
71
|
+
nameLocalizations: { tr: "temizle" }, // localized name
|
|
72
|
+
descriptionLocalizations: { tr: "Mesajları sil" },
|
|
73
|
+
run: (ctx) => ctx.reply("…"),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Field | Type | Effect |
|
|
78
|
+
| ----- | ---- | ------ |
|
|
79
|
+
| `guildOnly` | `boolean` | Restricts the command to guild contexts. |
|
|
80
|
+
| `nsfw` | `boolean` | Marks the command age-restricted. |
|
|
81
|
+
| `defaultMemberPermissions` | `PermissionResolvable \| null` | Default permission gate (members without it don't see the command). |
|
|
82
|
+
| `nameLocalizations` / `descriptionLocalizations` | `LocalizationMap` | Per-locale name/description. |
|
|
83
|
+
| `cooldown` | `number \| CooldownConfig` | Rate-limit the command (a number is milliseconds). See [Cooldowns](./cooldown.md). |
|
|
84
|
+
| `guards` | `readonly Guard[]` | Preconditions run before the handler. See [Guards](./guards.md). |
|
|
85
|
+
| `autoDefer` | `boolean \| { ephemeral?, delayMs? }` | Auto-`deferReply()` if the handler is slow (>~2s), preventing `Unknown interaction`. Respond via `ctx.send`/`ctx.editReply`. |
|
|
86
|
+
|
|
87
|
+
## Subcommands and groups
|
|
88
|
+
|
|
89
|
+
For commands with subcommands, use `commandGroup` together with `subcommand`
|
|
90
|
+
and (optionally) `subcommandGroup`. Each subcommand has its own typed options
|
|
91
|
+
and handler; spearkit routes to the right one automatically.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { commandGroup, subcommand, subcommandGroup, option } from "spearkit";
|
|
95
|
+
|
|
96
|
+
export const admin = commandGroup({
|
|
97
|
+
name: "admin",
|
|
98
|
+
description: "Administration",
|
|
99
|
+
guildOnly: true,
|
|
100
|
+
// Direct subcommands: /admin say
|
|
101
|
+
subcommands: {
|
|
102
|
+
say: subcommand({
|
|
103
|
+
description: "Make the bot say something",
|
|
104
|
+
options: { message: option.string({ description: "Message", required: true }) },
|
|
105
|
+
run: (ctx) => ctx.reply(ctx.options.message),
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
// Grouped subcommands: /admin users ban
|
|
109
|
+
groups: {
|
|
110
|
+
users: subcommandGroup({
|
|
111
|
+
description: "Manage users",
|
|
112
|
+
subcommands: {
|
|
113
|
+
ban: subcommand({
|
|
114
|
+
description: "Ban a member",
|
|
115
|
+
options: {
|
|
116
|
+
target: option.user({ description: "Member", required: true }),
|
|
117
|
+
reason: option.string({ description: "Reason" }),
|
|
118
|
+
},
|
|
119
|
+
run: (ctx) =>
|
|
120
|
+
ctx.reply(`Banned ${ctx.options.target.tag}: ${ctx.options.reason ?? "no reason"}`),
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Inside a subcommand handler, `ctx.options` is typed from *that subcommand's*
|
|
129
|
+
options. There is no `switch (subcommand)` to write — spearkit dispatches by the
|
|
130
|
+
invoked subcommand group/name.
|
|
131
|
+
|
|
132
|
+
## The command registry
|
|
133
|
+
|
|
134
|
+
`client.commands` is a `CommandRegistry`. You usually feed it through
|
|
135
|
+
`client.register(...)`, but you can use it directly:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { CommandRegistry } from "spearkit";
|
|
139
|
+
|
|
140
|
+
const registry = new CommandRegistry();
|
|
141
|
+
registry.add(ping, echo, admin);
|
|
142
|
+
|
|
143
|
+
registry.get("ping"); // SlashCommand | undefined
|
|
144
|
+
registry.names; // string[]
|
|
145
|
+
registry.size; // number
|
|
146
|
+
registry.remove("ping"); // boolean
|
|
147
|
+
registry.toJSON(); // REST payloads for all commands
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`SpearClient` calls `registry.handle(interaction)` and
|
|
151
|
+
`registry.handleAutocomplete(interaction)` for you on every interaction.
|
|
152
|
+
|
|
153
|
+
### Error handling
|
|
154
|
+
|
|
155
|
+
If a handler throws, spearkit catches it. By default it emits the client's `error`
|
|
156
|
+
event and replies with an ephemeral "something went wrong" message. Override
|
|
157
|
+
that:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
client.commands.onError((error, interaction) => {
|
|
161
|
+
console.error(`/${interaction.commandName} failed`, error);
|
|
162
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
163
|
+
return interaction.reply({ content: "Command failed.", ephemeral: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Deployment
|
|
169
|
+
|
|
170
|
+
Commands must be registered with discord before they appear. spearkit gives you two
|
|
171
|
+
ways.
|
|
172
|
+
|
|
173
|
+
**From the client** (uses the client's authenticated REST; call after ready):
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
await client.start(process.env.DISCORD_TOKEN);
|
|
177
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID }); // omit guildId for global
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Standalone** (a separate deploy script, no running client needed):
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { CommandRegistry } from "spearkit";
|
|
184
|
+
|
|
185
|
+
const registry = new CommandRegistry().add(ping, echo, admin);
|
|
186
|
+
await registry.deploy({
|
|
187
|
+
token: process.env.DISCORD_TOKEN,
|
|
188
|
+
applicationId: process.env.DISCORD_APP_ID,
|
|
189
|
+
guildId: process.env.GUILD_ID, // optional
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Guild deploys apply **instantly** and are ideal during development. Global
|
|
194
|
+
deploys (no `guildId`) can take up to an hour to propagate.
|
|
195
|
+
|
|
196
|
+
## See also
|
|
197
|
+
|
|
198
|
+
- [Options](./options.md) — typed option builders, choices, autocomplete.
|
|
199
|
+
- [Components](./components.md) — buttons, selects, modals.
|
|
200
|
+
- [Client](./client.md) — registering and deploying from the client.
|
|
201
|
+
- [Contexts](./context.md) — the reply helpers every handler shares.
|
|
202
|
+
- [Cooldowns](./cooldown.md) — rate-limit a command with `cooldown`.
|
|
203
|
+
- [Guards](./guards.md) — gate a command with `guards`.
|