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,281 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
Buttons, select menus and modals in spearkit follow one pattern: define the
|
|
4
|
+
appearance, the **custom-id pattern**, and the handler in one place; register
|
|
5
|
+
it; then `build()` the discord.js component to put in a message. spearkit decodes
|
|
6
|
+
incoming interactions and routes them to your handler — no `interactionCreate`
|
|
7
|
+
switch statements, no manual custom-id parsing.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { button, row } from "spearkit";
|
|
11
|
+
|
|
12
|
+
const vote = button({
|
|
13
|
+
id: "vote:{choice}",
|
|
14
|
+
label: "Yes",
|
|
15
|
+
style: "Success",
|
|
16
|
+
run: (ctx) => ctx.update(`You chose ${ctx.params.choice}`), // ctx.params.choice: string
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
client.register(vote); // or client.components.add(vote)
|
|
20
|
+
|
|
21
|
+
await channel.send({
|
|
22
|
+
content: "Cast your vote:",
|
|
23
|
+
components: [row(vote.build({ choice: "yes" }))], // build() requires { choice }
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Custom-id patterns
|
|
28
|
+
|
|
29
|
+
The `id` is a pattern with the grammar `name` or `name:{param}` or
|
|
30
|
+
`name:{a}:{b}`. The leading `name` is the routing **namespace**; each `{param}`
|
|
31
|
+
becomes a positional value carried in the custom-id.
|
|
32
|
+
|
|
33
|
+
- In the handler, params are available as a typed object: `ctx.params.choice`.
|
|
34
|
+
- `build(params)` requires **exactly** those params and encodes them into the
|
|
35
|
+
custom-id.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const page = button({
|
|
39
|
+
id: "page:{id}:{dir}",
|
|
40
|
+
label: "Next",
|
|
41
|
+
run: (ctx) => ctx.update(`item ${ctx.params.id}, going ${ctx.params.dir}`),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
page.build({ id: "42", dir: "next" }); // custom-id "page:42:next"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
spearkit percent-escapes param values, so they may safely contain `:`. Custom-ids
|
|
48
|
+
are limited to 100 characters (`MAX_CUSTOM_ID_LENGTH`); `build()` throws if you
|
|
49
|
+
exceed it.
|
|
50
|
+
|
|
51
|
+
For advanced use, the codec is exported directly: `compilePattern`,
|
|
52
|
+
`buildCustomId`, `parseCustomId`, and `paramsFromValues`.
|
|
53
|
+
|
|
54
|
+
## Buttons
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { button, linkButton, ButtonStyle } from "spearkit";
|
|
58
|
+
|
|
59
|
+
const confirm = button({
|
|
60
|
+
id: "confirm:{action}",
|
|
61
|
+
label: "Confirm",
|
|
62
|
+
style: ButtonStyle.Danger, // or the string "Danger"
|
|
63
|
+
emoji: "⚠️",
|
|
64
|
+
disabled: false,
|
|
65
|
+
run: (ctx) => ctx.update(`Confirmed: ${ctx.params.action}`),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Link buttons have no handler and no custom-id:
|
|
69
|
+
const docs = linkButton({ url: "https://example.com", label: "Docs" });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`style` accepts the string names `"Primary"`, `"Secondary"`, `"Success"`,
|
|
73
|
+
`"Danger"`, or the `ButtonStyle` enum. It defaults to `"Secondary"`.
|
|
74
|
+
|
|
75
|
+
All component builders (`button`, the five selects, and `modal`) also accept
|
|
76
|
+
`guards?: readonly Guard[]` — preconditions evaluated before the handler runs.
|
|
77
|
+
See [Guards](./guards.md).
|
|
78
|
+
|
|
79
|
+
The `ButtonContext` adds, on top of the shared [reply helpers](./context.md):
|
|
80
|
+
|
|
81
|
+
| Member | Description |
|
|
82
|
+
| ------ | ----------- |
|
|
83
|
+
| `ctx.params` | Decoded custom-id params. |
|
|
84
|
+
| `ctx.update(input)` | Edit the message the button is on. |
|
|
85
|
+
| `ctx.deferUpdate()` | Acknowledge without editing yet. |
|
|
86
|
+
| `ctx.showModal(modal)` | Open a modal in response. |
|
|
87
|
+
| `ctx.message` | The message the button belongs to. |
|
|
88
|
+
| `ctx.customId` | The raw custom-id. |
|
|
89
|
+
|
|
90
|
+
## Select menus
|
|
91
|
+
|
|
92
|
+
There are five select builders. All share `placeholder`, `minValues`,
|
|
93
|
+
`maxValues`, and `disabled`; the string select additionally takes `options`,
|
|
94
|
+
and the channel select takes `channelTypes`.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { stringSelect, channelSelect, ChannelType } from "spearkit";
|
|
98
|
+
|
|
99
|
+
const colour = stringSelect({
|
|
100
|
+
id: "colour",
|
|
101
|
+
placeholder: "Pick a colour",
|
|
102
|
+
minValues: 1,
|
|
103
|
+
maxValues: 1,
|
|
104
|
+
options: [
|
|
105
|
+
{ label: "Red", value: "red" },
|
|
106
|
+
{ label: "Green", value: "green", description: "the calm one" },
|
|
107
|
+
{ label: "Blue", value: "blue", default: true },
|
|
108
|
+
],
|
|
109
|
+
run: (ctx) => ctx.reply({ content: `You picked ${ctx.values.join(", ")}`, ephemeral: true }),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const pickChannel = channelSelect({
|
|
113
|
+
id: "pick-channel",
|
|
114
|
+
channelTypes: [ChannelType.GuildText],
|
|
115
|
+
run: (ctx) => ctx.reply({ content: `${ctx.values.length} channel(s)`, ephemeral: true }),
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Each select context exposes the relevant resolved data:
|
|
120
|
+
|
|
121
|
+
| Builder | Context | Extra accessors |
|
|
122
|
+
| ------- | ------- | --------------- |
|
|
123
|
+
| `stringSelect` | `StringSelectContext` | `values: string[]`, `value: string \| undefined` |
|
|
124
|
+
| `userSelect` | `UserSelectContext` | `values`, `users`, `members` |
|
|
125
|
+
| `roleSelect` | `RoleSelectContext` | `values`, `roles` |
|
|
126
|
+
| `channelSelect` | `ChannelSelectContext` | `values`, `channels` |
|
|
127
|
+
| `mentionableSelect` | `MentionableSelectContext` | `values`, `users`, `roles`, `members` |
|
|
128
|
+
|
|
129
|
+
Select contexts also have `ctx.params`, `ctx.update`, `ctx.deferUpdate`,
|
|
130
|
+
`ctx.showModal`, and the shared reply helpers.
|
|
131
|
+
|
|
132
|
+
## Modals
|
|
133
|
+
|
|
134
|
+
A modal declares its `fields` as a map of name → `textInput`. The submit handler
|
|
135
|
+
receives the submitted values in `ctx.fields`, keyed (and typed) by those names,
|
|
136
|
+
plus any custom-id params in `ctx.params`.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { modal, textInput } from "spearkit";
|
|
140
|
+
|
|
141
|
+
const feedback = modal({
|
|
142
|
+
id: "feedback:{ticket}",
|
|
143
|
+
title: "Feedback",
|
|
144
|
+
fields: {
|
|
145
|
+
summary: textInput({ label: "Summary", required: true }),
|
|
146
|
+
detail: textInput({ label: "Details", style: "Paragraph", maxLength: 2000 }),
|
|
147
|
+
},
|
|
148
|
+
run: (ctx) =>
|
|
149
|
+
ctx.reply({
|
|
150
|
+
// ctx.params.ticket: string, ctx.fields.summary / ctx.fields.detail: string
|
|
151
|
+
content: `#${ctx.params.ticket}: ${ctx.fields.summary}`,
|
|
152
|
+
ephemeral: true,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`textInput` config: `label` (required), `style` (`"Short"` default, or
|
|
158
|
+
`"Paragraph"`, or a `TextInputStyle`), `placeholder`, `required`, `minLength`,
|
|
159
|
+
`maxLength`, `value`.
|
|
160
|
+
|
|
161
|
+
Open a modal from a command or a component handler with `showModal` — modals
|
|
162
|
+
cannot be the *response* to another modal, but they can follow a command or a
|
|
163
|
+
button/select:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { command } from "spearkit";
|
|
167
|
+
|
|
168
|
+
const ask = command({
|
|
169
|
+
name: "ask",
|
|
170
|
+
description: "Open the feedback form",
|
|
171
|
+
run: (ctx) => ctx.showModal(feedback.build({ ticket: "1234" })),
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Action rows
|
|
176
|
+
|
|
177
|
+
`row(...components)` wraps builders in an `ActionRowBuilder`. A row holds up to
|
|
178
|
+
five buttons, or exactly one select menu.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { row } from "spearkit";
|
|
182
|
+
|
|
183
|
+
const components = [
|
|
184
|
+
row(confirm.build({ action: "delete" }), docs),
|
|
185
|
+
row(colour.build()),
|
|
186
|
+
];
|
|
187
|
+
await channel.send({ content: "Choose:", components });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Registering and routing
|
|
191
|
+
|
|
192
|
+
Register components like anything else:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
client.register(vote, colour, feedback);
|
|
196
|
+
// equivalently:
|
|
197
|
+
client.components.add(vote, colour, feedback);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`SpearClient` routes every button, select and modal interaction to the matching
|
|
201
|
+
namespace automatically. The `ComponentRegistry` API:
|
|
202
|
+
|
|
203
|
+
| Member | Description |
|
|
204
|
+
| ------ | ----------- |
|
|
205
|
+
| `add(...defs)` | Register components (override by namespace). |
|
|
206
|
+
| `size` | Number registered. |
|
|
207
|
+
| `onError(handler)` | Set the error handler. |
|
|
208
|
+
| `handle(interaction)` | Route an interaction; returns `true` if matched. |
|
|
209
|
+
| `setDefaultGuards(guards)` | Guards run before each component's own guards. |
|
|
210
|
+
|
|
211
|
+
`setLogger` and `setUsageHook` also exist; the client wires all three for you.
|
|
212
|
+
|
|
213
|
+
### Error handling
|
|
214
|
+
|
|
215
|
+
By default a throwing handler emits the client `error` event and replies with an
|
|
216
|
+
ephemeral message. Customise it:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
client.components.onError((error, interaction) => {
|
|
220
|
+
console.error("component failed", error);
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## End-to-end example
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import {
|
|
228
|
+
SpearClient,
|
|
229
|
+
Intents,
|
|
230
|
+
command,
|
|
231
|
+
button,
|
|
232
|
+
stringSelect,
|
|
233
|
+
modal,
|
|
234
|
+
textInput,
|
|
235
|
+
row,
|
|
236
|
+
} from "spearkit";
|
|
237
|
+
|
|
238
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
239
|
+
|
|
240
|
+
const open = button({
|
|
241
|
+
id: "open-form:{topic}",
|
|
242
|
+
label: "Open form",
|
|
243
|
+
style: "Primary",
|
|
244
|
+
run: (ctx) => ctx.showModal(form.build({ topic: ctx.params.topic })),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const rating = stringSelect({
|
|
248
|
+
id: "rating",
|
|
249
|
+
placeholder: "Rate us",
|
|
250
|
+
options: [
|
|
251
|
+
{ label: "Good", value: "good" },
|
|
252
|
+
{ label: "Bad", value: "bad" },
|
|
253
|
+
],
|
|
254
|
+
run: (ctx) => ctx.reply({ content: `Thanks: ${ctx.value}`, ephemeral: true }),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const form = modal({
|
|
258
|
+
id: "form:{topic}",
|
|
259
|
+
title: "Tell us more",
|
|
260
|
+
fields: { body: textInput({ label: "Message", style: "Paragraph", required: true }) },
|
|
261
|
+
run: (ctx) => ctx.reply({ content: `[${ctx.params.topic}] ${ctx.fields.body}`, ephemeral: true }),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const panel = command({
|
|
265
|
+
name: "panel",
|
|
266
|
+
description: "Show the panel",
|
|
267
|
+
run: (ctx) =>
|
|
268
|
+
ctx.reply({
|
|
269
|
+
content: "How was it?",
|
|
270
|
+
components: [row(open.build({ topic: "support" })), row(rating.build())],
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
client.register(panel, open, rating, form);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## See also
|
|
278
|
+
|
|
279
|
+
- [Commands](./commands.md) — opening components from commands.
|
|
280
|
+
- [Contexts](./context.md) — the reply/update helpers contexts share.
|
|
281
|
+
- [Client](./client.md) — registration and routing.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Context-menu commands
|
|
2
|
+
|
|
3
|
+
Context-menu commands are the right-click **"Apps"** actions Discord shows on a
|
|
4
|
+
user or a message. spearkit makes them first-class: define one with `userCommand`
|
|
5
|
+
or `messageCommand`, register it like anything else, and deploy it alongside your
|
|
6
|
+
slash commands. The handler gets a typed `targetUser` or `targetMessage`.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { userCommand } from "spearkit";
|
|
10
|
+
|
|
11
|
+
export const whois = userCommand({
|
|
12
|
+
name: "Who is this?",
|
|
13
|
+
run: (ctx) => ctx.replyEphemeral(`That's ${ctx.targetUser.tag}.`),
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`name` is the label shown in the Apps menu (no description — Discord does not show
|
|
18
|
+
one for context-menu commands).
|
|
19
|
+
|
|
20
|
+
## User vs message commands
|
|
21
|
+
|
|
22
|
+
| Builder | Appears on | Target context |
|
|
23
|
+
| ------- | ---------- | -------------- |
|
|
24
|
+
| `userCommand` | a user (right-click → Apps) | `ctx.targetUser`, `ctx.targetMember` |
|
|
25
|
+
| `messageCommand` | a message (right-click → Apps) | `ctx.targetMessage` |
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { messageCommand } from "spearkit";
|
|
29
|
+
|
|
30
|
+
export const report = messageCommand({
|
|
31
|
+
name: "Report message",
|
|
32
|
+
run: (ctx) =>
|
|
33
|
+
ctx.replyEphemeral(`Reported message ${ctx.targetMessage.id}.`),
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Both handler contexts extend the shared [`BaseContext`](./context.md), so
|
|
38
|
+
`ctx.reply`, `ctx.replyEphemeral`, `ctx.defer`, `ctx.success/error/...` and the
|
|
39
|
+
usual accessors are all available.
|
|
40
|
+
|
|
41
|
+
## Metadata, cooldowns and guards
|
|
42
|
+
|
|
43
|
+
Both builders accept the same metadata, plus a `cooldown` and `guards`:
|
|
44
|
+
|
|
45
|
+
| Field | Type | Effect |
|
|
46
|
+
| ----- | ---- | ------ |
|
|
47
|
+
| `name` | `string` | The Apps-menu label. |
|
|
48
|
+
| `defaultMemberPermissions` | `PermissionResolvable \| null` | Default permission gate. |
|
|
49
|
+
| `nsfw` | `boolean` | Marks the command age-restricted. |
|
|
50
|
+
| `guildOnly` | `boolean` | Restricts it to guild contexts. |
|
|
51
|
+
| `nameLocalizations` | `LocalizationMap` | Per-locale label. |
|
|
52
|
+
| `cooldown` | `number \| CooldownConfig` | Rate limit (shares `client.cooldowns`). |
|
|
53
|
+
| `guards` | `readonly Guard[]` | Preconditions run before the handler. |
|
|
54
|
+
| `autoDefer` | `boolean \| { ephemeral?, delayMs? }` | Auto-`deferReply()` if the handler is slow, preventing `Unknown interaction`. |
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { userCommand, guildOnly, requireUserPermissions, PermissionFlagsBits } from "spearkit";
|
|
58
|
+
|
|
59
|
+
export const warn = userCommand({
|
|
60
|
+
name: "Warn user",
|
|
61
|
+
guildOnly: true,
|
|
62
|
+
cooldown: 5_000,
|
|
63
|
+
guards: [requireUserPermissions(PermissionFlagsBits.ModerateMembers)],
|
|
64
|
+
run: (ctx) => ctx.replyEphemeral(`Warned ${ctx.targetUser.tag}.`),
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
See [Cooldowns](./cooldown.md) and [Guards](./guards.md) for the shared options.
|
|
69
|
+
|
|
70
|
+
## Registering and deploying
|
|
71
|
+
|
|
72
|
+
Register context-menu commands like everything else with `client.register(...)`.
|
|
73
|
+
They route automatically — spearkit dispatches user- and message-context-menu
|
|
74
|
+
interactions to the matching command.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
client.register(whois, report, warn);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Because context menus and slash commands deploy to the same Discord endpoint,
|
|
81
|
+
push them together with `deployAllCommands` once you mix the two — it sends both
|
|
82
|
+
in a single request. (`deployCommands` is slash-only.)
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
await client.start(process.env.DISCORD_TOKEN);
|
|
86
|
+
await client.deployAllCommands({ guildId: process.env.GUILD_ID }); // slash + menus
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`deployAllCommands` also supports a `dryRun` flag and a `strategy: "diff"` that
|
|
90
|
+
skips the PUT when the remote set already matches — handy in CI:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// Preview without touching Discord:
|
|
94
|
+
const result = await client.deployAllCommands({ guildId, dryRun: true });
|
|
95
|
+
// → { skipped: true, reason: "dry-run", body: [...] }
|
|
96
|
+
|
|
97
|
+
// Only deploy when something changed:
|
|
98
|
+
await client.deployAllCommands({ guildId, strategy: "diff" });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## The registry
|
|
102
|
+
|
|
103
|
+
`client.contextMenus` is a `ContextMenuRegistry`. The client wires it to the
|
|
104
|
+
logger and cooldown manager and routes interactions for you, so you rarely touch
|
|
105
|
+
it directly:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
client.contextMenus.size; // number registered
|
|
109
|
+
client.contextMenus.all(); // ContextMenuCommand[]
|
|
110
|
+
client.contextMenus.toJSON(); // REST payloads (also included by deployAllCommands)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Note: context-menu commands are **not** picked up by `client.load(...)`
|
|
114
|
+
directory loading — register them explicitly with `client.register(...)`.
|
|
115
|
+
|
|
116
|
+
## See also
|
|
117
|
+
|
|
118
|
+
- [Commands](./commands.md) — slash commands.
|
|
119
|
+
- [Client](./client.md) — `deployAllCommands` and registration.
|
|
120
|
+
- [Guards](./guards.md) / [Cooldowns](./cooldown.md) — the shared preconditions.
|
|
121
|
+
- [Contexts](./context.md) — the reply helpers every handler shares.
|
package/docs/context.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Contexts
|
|
2
|
+
|
|
3
|
+
Every spearkit handler — command, button, select, modal — receives a context
|
|
4
|
+
object. They all share `BaseContext`, which smooths over discord.js'
|
|
5
|
+
reply/defer/edit/follow-up state machine and exposes the common
|
|
6
|
+
actor/location accessors. Learn it once and it applies everywhere.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { command, option } from "spearkit";
|
|
10
|
+
|
|
11
|
+
export default command({
|
|
12
|
+
name: "hello",
|
|
13
|
+
description: "Say hello",
|
|
14
|
+
options: { name: option.string({ description: "Name", required: true }) },
|
|
15
|
+
run: (ctx) => ctx.reply(`Hi, ${ctx.options.name}!`),
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`CommandContext`, `ButtonContext`, `StringSelectContext`, modal contexts and the
|
|
20
|
+
rest extend `BaseContext`, adding their own specifics (e.g. `ctx.options`,
|
|
21
|
+
`ctx.params`, `ctx.fields`) on top of everything below.
|
|
22
|
+
|
|
23
|
+
## Reply helpers
|
|
24
|
+
|
|
25
|
+
| Method | Returns | Behaviour |
|
|
26
|
+
| ------ | ------- | --------- |
|
|
27
|
+
| `reply(input)` | `Promise<InteractionResponse>` | Send the initial response. |
|
|
28
|
+
| `replyEphemeral(input)` | `Promise<InteractionResponse>` | Reply, hidden to everyone but the invoking user. |
|
|
29
|
+
| `defer({ ephemeral })` | `Promise<InteractionResponse>` | Acknowledge now, respond later via `editReply`. |
|
|
30
|
+
| `editReply(input)` | `Promise<Message>` | Edit the original (or deferred) response. |
|
|
31
|
+
| `followUp(input)` | `Promise<Message>` | Add a message after the initial response. |
|
|
32
|
+
| `send(input)` | `Promise<void>` | State-aware: replies, edits, or follows up automatically. |
|
|
33
|
+
| `error(input, options?)` | `Promise<void>` | State-aware preset error embed; ephemeral by default. |
|
|
34
|
+
| `success` / `info` / `warn` `(input, options?)` | `Promise<void>` | State-aware preset embeds. |
|
|
35
|
+
| `replyError` / `replySuccess` / `replyInfo` / `replyWarn` `(input, options?)` | `Promise<InteractionResponse>` | Initial-reply preset embeds. |
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { command } from "spearkit";
|
|
39
|
+
|
|
40
|
+
export default command({
|
|
41
|
+
name: "demo",
|
|
42
|
+
description: "Reply helpers",
|
|
43
|
+
run: async (ctx) => {
|
|
44
|
+
await ctx.reply("Working on it…");
|
|
45
|
+
await ctx.followUp("…almost done.");
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `send` is the one most handlers need
|
|
51
|
+
|
|
52
|
+
`send` inspects the interaction state and does the right thing:
|
|
53
|
+
|
|
54
|
+
- not yet answered → `reply`
|
|
55
|
+
- already deferred → `editReply`
|
|
56
|
+
- already replied → `followUp`
|
|
57
|
+
|
|
58
|
+
This means you can call `send` without tracking whether you deferred, which is
|
|
59
|
+
ideal for shared helpers that may run before or after a `defer`.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { command } from "spearkit";
|
|
63
|
+
|
|
64
|
+
export default command({
|
|
65
|
+
name: "report",
|
|
66
|
+
description: "Generate a report",
|
|
67
|
+
run: async (ctx) => {
|
|
68
|
+
await ctx.defer(); // acknowledge while we do slow work
|
|
69
|
+
const data = await buildReport();
|
|
70
|
+
await ctx.send(data); // sees the deferred state → edits the reply
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `error` for ephemeral failures
|
|
76
|
+
|
|
77
|
+
`error(input, options?)` sends a state-aware preset **error embed** — ephemeral
|
|
78
|
+
by default (pass `{ ephemeral: false }` to make it public) — perfect for
|
|
79
|
+
validation failures that only the invoking user should see.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { command, option } from "spearkit";
|
|
83
|
+
|
|
84
|
+
export default command({
|
|
85
|
+
name: "kick",
|
|
86
|
+
description: "Kick a member",
|
|
87
|
+
options: { who: option.user({ description: "Member", required: true }) },
|
|
88
|
+
run: async (ctx) => {
|
|
89
|
+
if (!ctx.guild) return ctx.error("This command only works in a server.");
|
|
90
|
+
await ctx.reply(`Kicked ${ctx.options.who}.`);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Preset embeds
|
|
96
|
+
|
|
97
|
+
`BaseContext` builds consistent, colored embeds from `client.embeds` (or a shared
|
|
98
|
+
default). Each takes an `EmbedPresetInput` — a plain string, or a structured body
|
|
99
|
+
(`{ title?, description?, fields?, footer?, ... }`) — and an optional
|
|
100
|
+
`{ ephemeral? }`.
|
|
101
|
+
|
|
102
|
+
| Method | Sends via | Default visibility |
|
|
103
|
+
| ------ | --------- | ------------------ |
|
|
104
|
+
| `success(input, options?)` | `send` (state-aware) | public |
|
|
105
|
+
| `info(input, options?)` | `send` (state-aware) | public |
|
|
106
|
+
| `warn(input, options?)` | `send` (state-aware) | public |
|
|
107
|
+
| `error(input, options?)` | `send` (state-aware) | **ephemeral** |
|
|
108
|
+
| `replySuccess` / `replyInfo` / `replyWarn` `(input, options?)` | `reply` (initial only) | public |
|
|
109
|
+
| `replyError(input, options?)` | `reply` (initial only) | **ephemeral** |
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { command } from "spearkit";
|
|
113
|
+
|
|
114
|
+
export default command({
|
|
115
|
+
name: "save",
|
|
116
|
+
description: "Save settings",
|
|
117
|
+
run: async (ctx) => {
|
|
118
|
+
await ctx.success("Settings saved."); // green embed, public
|
|
119
|
+
await ctx.warn({ title: "Heads up", description: "Quota is almost full." });
|
|
120
|
+
// error defaults to ephemeral; make it public with { ephemeral: false }:
|
|
121
|
+
// await ctx.error("Failed to save.", { ephemeral: false });
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Configure the colors/icons with the client `embeds` option; see the
|
|
127
|
+
[API reference](./api-reference.md#embeds--preset-replies).
|
|
128
|
+
|
|
129
|
+
## The `{ ephemeral: true }` shortcut
|
|
130
|
+
|
|
131
|
+
discord.js represents an ephemeral reply with `flags: MessageFlags.Ephemeral`.
|
|
132
|
+
spearkit lets you write the more obvious `{ ephemeral: true }` on any reply payload
|
|
133
|
+
and maps it to that flag for you. The input type is `ReplyInput`
|
|
134
|
+
(`string | ReplyData`), where `ReplyData` is discord.js'
|
|
135
|
+
`InteractionReplyOptions` plus the optional `ephemeral` boolean.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { command, EmbedBuilder } from "spearkit";
|
|
139
|
+
|
|
140
|
+
export default command({
|
|
141
|
+
name: "secret",
|
|
142
|
+
description: "Only you can see this",
|
|
143
|
+
run: (ctx) =>
|
|
144
|
+
ctx.reply({
|
|
145
|
+
embeds: [new EmbedBuilder().setTitle("Just for you")],
|
|
146
|
+
ephemeral: true, // mapped to MessageFlags.Ephemeral
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`replyEphemeral(input)` is sugar for the same thing, accepting either a string
|
|
152
|
+
or a payload:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
await ctx.replyEphemeral("Saved.");
|
|
156
|
+
await ctx.replyEphemeral({ embeds: [embed] });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
If you set `flags` yourself, spearkit preserves them and adds the ephemeral flag
|
|
160
|
+
rather than overwriting it.
|
|
161
|
+
|
|
162
|
+
### Exported helpers
|
|
163
|
+
|
|
164
|
+
spearkit exports the two functions it uses internally, so you can normalise reply
|
|
165
|
+
input yourself (e.g. in a plugin or shared utility):
|
|
166
|
+
|
|
167
|
+
- `normalizeReply(input: ReplyInput): InteractionReplyOptions` — converts a
|
|
168
|
+
string or `ReplyData` into a discord.js reply payload, applying the ephemeral
|
|
169
|
+
flag mapping.
|
|
170
|
+
- `asEphemeral(input: ReplyInput): ReplyData` — marks any input ephemeral,
|
|
171
|
+
regardless of how it was passed.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { normalizeReply, asEphemeral } from "spearkit";
|
|
175
|
+
|
|
176
|
+
normalizeReply("hi");
|
|
177
|
+
// → { content: "hi" }
|
|
178
|
+
|
|
179
|
+
normalizeReply({ content: "hi", ephemeral: true });
|
|
180
|
+
// → { content: "hi", flags: MessageFlags.Ephemeral }
|
|
181
|
+
|
|
182
|
+
asEphemeral("hidden");
|
|
183
|
+
// → { content: "hidden", ephemeral: true }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Accessors
|
|
187
|
+
|
|
188
|
+
`BaseContext` forwards the common interaction fields so you do not reach through
|
|
189
|
+
`ctx.interaction` for everyday data:
|
|
190
|
+
|
|
191
|
+
| Accessor | Description |
|
|
192
|
+
| -------- | ----------- |
|
|
193
|
+
| `interaction` | The raw discord.js interaction. |
|
|
194
|
+
| `client` | The `SpearClient` (typed as the interaction's client). |
|
|
195
|
+
| `user` | The invoking `User`. |
|
|
196
|
+
| `member` | The invoking guild member (or `null` outside a guild). |
|
|
197
|
+
| `guild` | The `Guild`, or `null` in DMs. |
|
|
198
|
+
| `guildId` | The guild id, or `null`. |
|
|
199
|
+
| `channel` | The channel the interaction came from. |
|
|
200
|
+
| `channelId` | The channel id. |
|
|
201
|
+
| `locale` | The user's locale. |
|
|
202
|
+
| `deferred` | Whether the interaction is already deferred. |
|
|
203
|
+
| `replied` | Whether the interaction already received an initial response. |
|
|
204
|
+
| `botPermissions` | The bot's resolved permissions in the channel (`PermissionsBitField`, zero-fetch). |
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { command } from "spearkit";
|
|
208
|
+
|
|
209
|
+
export default command({
|
|
210
|
+
name: "whereami",
|
|
211
|
+
description: "Report context",
|
|
212
|
+
run: (ctx) =>
|
|
213
|
+
ctx.reply(
|
|
214
|
+
ctx.guild
|
|
215
|
+
? `In ${ctx.guild.name} (#${ctx.channelId}), locale ${ctx.locale}.`
|
|
216
|
+
: "We're in a DM.",
|
|
217
|
+
),
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`deferred` and `replied` let you branch when you are not using `send`:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { button } from "spearkit";
|
|
225
|
+
|
|
226
|
+
export default button({
|
|
227
|
+
id: "refresh",
|
|
228
|
+
label: "Refresh",
|
|
229
|
+
run: async (ctx) => {
|
|
230
|
+
if (ctx.replied || ctx.deferred) await ctx.followUp("Refreshed.");
|
|
231
|
+
else await ctx.reply("Refreshed.");
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Permission preflights
|
|
237
|
+
|
|
238
|
+
`BaseContext` reads the permissions Discord already attached to the interaction —
|
|
239
|
+
no extra fetches — so you can check before attempting a privileged action:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { command, PermissionFlagsBits } from "spearkit";
|
|
243
|
+
|
|
244
|
+
export default command({
|
|
245
|
+
name: "slowmode",
|
|
246
|
+
description: "Set slowmode",
|
|
247
|
+
run: async (ctx) => {
|
|
248
|
+
const missing = ctx.botMissing(PermissionFlagsBits.ManageChannels);
|
|
249
|
+
if (missing.length > 0) return ctx.error(`I'm missing: ${missing.join(", ")}`);
|
|
250
|
+
// …apply slowmode…
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
- `ctx.botPermissions` — the bot's `PermissionsBitField` in the current channel.
|
|
256
|
+
- `ctx.botMissing(required)` — permission names the bot lacks here (`[]` if none).
|
|
257
|
+
- `ctx.userMissing(required)` — permission names the invoking user lacks here.
|
|
258
|
+
|
|
259
|
+
For role-hierarchy and moderation preflights (acting on self/owner, comparing top
|
|
260
|
+
roles) see `moderationCheck` and the permission helpers in the
|
|
261
|
+
[API reference](./api-reference.md#permissions--moderation).
|
|
262
|
+
|
|
263
|
+
## Awaiting input
|
|
264
|
+
|
|
265
|
+
When a flow needs a follow-up message or a modal, the context wraps discord.js
|
|
266
|
+
collectors so you skip the boilerplate. Both resolve to `null` on timeout.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { command, modal, textInput } from "spearkit";
|
|
270
|
+
|
|
271
|
+
const nameModal = modal({ id: "name", title: "Your name", fields: { name: textInput({ label: "Name" }) }, run: () => {} });
|
|
272
|
+
|
|
273
|
+
export default command({
|
|
274
|
+
name: "setup",
|
|
275
|
+
description: "Interactive setup",
|
|
276
|
+
run: async (ctx) => {
|
|
277
|
+
// Wait for the user to type an answer in this channel:
|
|
278
|
+
const reply = await ctx.awaitMessageFrom(ctx.user.id, { time: 30_000 });
|
|
279
|
+
if (reply === null) return ctx.error("Timed out.");
|
|
280
|
+
// Or show a modal and await its submission:
|
|
281
|
+
const submission = await ctx.awaitModal(nameModal);
|
|
282
|
+
if (submission !== null) await submission.reply(`Hi, ${submission.fields.getTextInputValue("name")}!`);
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The standalone `awaitMessage`, `awaitComponent` and `showAndAwaitModal` helpers
|
|
288
|
+
are also exported; see the [API reference](./api-reference.md#collectors).
|
|
289
|
+
|
|
290
|
+
## See also
|
|
291
|
+
|
|
292
|
+
- [Commands](./commands.md) — `CommandContext`, options and `showModal`.
|
|
293
|
+
- [Components](./components.md) — button, select and modal contexts.
|