spearkit 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/spearkit/SKILL.md +11 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +117 -6
- package/AGENTS.md +98 -1
- package/README.md +10 -3
- package/dist/index.cjs +599 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +524 -2
- package/dist/index.d.ts +524 -2
- package/dist/index.js +576 -19
- package/dist/index.js.map +1 -1
- package/docs/README.md +21 -12
- package/docs/api-reference.md +222 -34
- package/docs/auto-defer.md +74 -0
- package/docs/client.md +60 -22
- package/docs/collectors.md +65 -0
- package/docs/commands.md +5 -0
- package/docs/components.md +7 -0
- package/docs/context-menus.md +121 -0
- package/docs/context.md +94 -2
- package/docs/cooldown.md +2 -1
- package/docs/errors.md +73 -0
- package/docs/guards.md +146 -0
- package/docs/loading.md +7 -5
- package/docs/messages.md +35 -0
- package/docs/permissions.md +68 -0
- package/docs/prefix.md +54 -0
- package/docs/scheduler.md +26 -2
- package/docs/shutdown.md +42 -0
- package/docs/store.md +90 -0
- package/docs/usage.md +20 -10
- package/llms-full.txt +1337 -85
- package/llms.txt +91 -3
- package/package.json +1 -1
package/docs/client.md
CHANGED
|
@@ -28,9 +28,10 @@ const a = new SpearClient({ intents: Intents.messages });
|
|
|
28
28
|
const b = new SpearClient();
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The options type is exported as `SpearClientOptions`
|
|
32
|
-
|
|
33
|
-
available
|
|
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).
|
|
34
35
|
|
|
35
36
|
### Intents presets
|
|
36
37
|
|
|
@@ -61,26 +62,38 @@ const client = new SpearClient({
|
|
|
61
62
|
});
|
|
62
63
|
```
|
|
63
64
|
|
|
64
|
-
##
|
|
65
|
-
|
|
66
|
-
Every client owns
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
`client.
|
|
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).
|
|
77
89
|
|
|
78
90
|
## Registering handlers
|
|
79
91
|
|
|
80
|
-
`client.register(...items)` accepts commands, events
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
84
97
|
|
|
85
98
|
```ts
|
|
86
99
|
import { SpearClient, command, event, button, option } from "spearkit";
|
|
@@ -126,8 +139,8 @@ See [Plugins](./plugins.md) for authoring `SpearPlugin`s.
|
|
|
126
139
|
## File-based loading
|
|
127
140
|
|
|
128
141
|
`client.load(dir, options?)` recursively imports a directory and registers every
|
|
129
|
-
|
|
130
|
-
registered.
|
|
142
|
+
spearkit-registrable export it finds — commands, events, components, scheduled
|
|
143
|
+
tasks and prefix commands. It returns the number of items registered.
|
|
131
144
|
|
|
132
145
|
```ts
|
|
133
146
|
import { SpearClient } from "spearkit";
|
|
@@ -177,6 +190,31 @@ client.once("clientReady", async () => {
|
|
|
177
190
|
});
|
|
178
191
|
```
|
|
179
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
|
+
|
|
180
218
|
## Everything discord.js still works
|
|
181
219
|
|
|
182
220
|
`SpearClient` extends discord.js `Client`, so the full client surface is
|
|
@@ -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
CHANGED
|
@@ -80,6 +80,9 @@ export const purge = command({
|
|
|
80
80
|
| `nsfw` | `boolean` | Marks the command age-restricted. |
|
|
81
81
|
| `defaultMemberPermissions` | `PermissionResolvable \| null` | Default permission gate (members without it don't see the command). |
|
|
82
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`. |
|
|
83
86
|
|
|
84
87
|
## Subcommands and groups
|
|
85
88
|
|
|
@@ -196,3 +199,5 @@ deploys (no `guildId`) can take up to an hour to propagate.
|
|
|
196
199
|
- [Components](./components.md) — buttons, selects, modals.
|
|
197
200
|
- [Client](./client.md) — registering and deploying from the client.
|
|
198
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`.
|
package/docs/components.md
CHANGED
|
@@ -72,6 +72,10 @@ const docs = linkButton({ url: "https://example.com", label: "Docs" });
|
|
|
72
72
|
`style` accepts the string names `"Primary"`, `"Secondary"`, `"Success"`,
|
|
73
73
|
`"Danger"`, or the `ButtonStyle` enum. It defaults to `"Secondary"`.
|
|
74
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
|
+
|
|
75
79
|
The `ButtonContext` adds, on top of the shared [reply helpers](./context.md):
|
|
76
80
|
|
|
77
81
|
| Member | Description |
|
|
@@ -202,6 +206,9 @@ namespace automatically. The `ComponentRegistry` API:
|
|
|
202
206
|
| `size` | Number registered. |
|
|
203
207
|
| `onError(handler)` | Set the error handler. |
|
|
204
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.
|
|
205
212
|
|
|
206
213
|
### Error handling
|
|
207
214
|
|
|
@@ -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
CHANGED
|
@@ -30,7 +30,9 @@ rest extend `BaseContext`, adding their own specifics (e.g. `ctx.options`,
|
|
|
30
30
|
| `editReply(input)` | `Promise<Message>` | Edit the original (or deferred) response. |
|
|
31
31
|
| `followUp(input)` | `Promise<Message>` | Add a message after the initial response. |
|
|
32
32
|
| `send(input)` | `Promise<void>` | State-aware: replies, edits, or follows up automatically. |
|
|
33
|
-
| `error(
|
|
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. |
|
|
34
36
|
|
|
35
37
|
```ts
|
|
36
38
|
import { command } from "spearkit";
|
|
@@ -72,7 +74,8 @@ export default command({
|
|
|
72
74
|
|
|
73
75
|
### `error` for ephemeral failures
|
|
74
76
|
|
|
75
|
-
`error(
|
|
77
|
+
`error(input, options?)` sends a state-aware preset **error embed** — ephemeral
|
|
78
|
+
by default (pass `{ ephemeral: false }` to make it public) — perfect for
|
|
76
79
|
validation failures that only the invoking user should see.
|
|
77
80
|
|
|
78
81
|
```ts
|
|
@@ -89,6 +92,40 @@ export default command({
|
|
|
89
92
|
});
|
|
90
93
|
```
|
|
91
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
|
+
|
|
92
129
|
## The `{ ephemeral: true }` shortcut
|
|
93
130
|
|
|
94
131
|
discord.js represents an ephemeral reply with `flags: MessageFlags.Ephemeral`.
|
|
@@ -164,6 +201,7 @@ asEphemeral("hidden");
|
|
|
164
201
|
| `locale` | The user's locale. |
|
|
165
202
|
| `deferred` | Whether the interaction is already deferred. |
|
|
166
203
|
| `replied` | Whether the interaction already received an initial response. |
|
|
204
|
+
| `botPermissions` | The bot's resolved permissions in the channel (`PermissionsBitField`, zero-fetch). |
|
|
167
205
|
|
|
168
206
|
```ts
|
|
169
207
|
import { command } from "spearkit";
|
|
@@ -195,6 +233,60 @@ export default button({
|
|
|
195
233
|
});
|
|
196
234
|
```
|
|
197
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
|
+
|
|
198
290
|
## See also
|
|
199
291
|
|
|
200
292
|
- [Commands](./commands.md) — `CommandContext`, options and `showModal`.
|
package/docs/cooldown.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Cooldowns
|
|
2
2
|
|
|
3
|
-
Rate-limit commands per user, per
|
|
3
|
+
Rate-limit commands per user, per guild, per channel, or globally — with per-role
|
|
4
|
+
and per-user exemptions and overrides.
|
|
4
5
|
Cooldowns are enforced automatically by command dispatch: when an actor is
|
|
5
6
|
still on cooldown, spearkit replies (ephemerally) with a message and the
|
|
6
7
|
handler does not run.
|
package/docs/errors.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Discord API errors
|
|
2
|
+
|
|
3
|
+
discord.js reports REST failures as `DiscordAPIError` with a numeric `code`
|
|
4
|
+
(`10008` "Unknown Message", `50013` "Missing Permissions", `50007` "Cannot send
|
|
5
|
+
DMs to this user", …). Catching *everything* turns recoverable failures — a
|
|
6
|
+
deleted message, a closed DM — into crashes or scary stack traces. spearkit gives
|
|
7
|
+
you named codes, a type-narrowing predicate, and a friendly explanation.
|
|
8
|
+
|
|
9
|
+
## Recognise and recover
|
|
10
|
+
|
|
11
|
+
`isDiscordError(err, code?)` narrows the throw and optionally matches a code
|
|
12
|
+
(or a list). Perfect for "ignore this one, re-throw the rest":
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { DiscordErrorCode, isDiscordError } from "spearkit";
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await message.delete();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (isDiscordError(err, DiscordErrorCode.UnknownMessage)) return; // already gone
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// match any of several codes
|
|
27
|
+
if (isDiscordError(err, [DiscordErrorCode.UnknownChannel, DiscordErrorCode.MissingAccess])) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Friendly messages
|
|
33
|
+
|
|
34
|
+
`explainDiscordError(err)` returns an end-user-appropriate sentence for a
|
|
35
|
+
recognised failure, or `null` otherwise (fall back to a generic message + log):
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { explainDiscordError } from "spearkit";
|
|
39
|
+
|
|
40
|
+
catch (err) {
|
|
41
|
+
await ctx.error(explainDiscordError(err) ?? "Something went wrong.");
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
spearkit already routes its own command/context-menu errors through
|
|
46
|
+
`explainDiscordError`, so a handler that throws `Missing Permissions` shows the
|
|
47
|
+
user *"I'm missing the permissions needed to do that."* instead of a generic
|
|
48
|
+
error.
|
|
49
|
+
|
|
50
|
+
## Named codes
|
|
51
|
+
|
|
52
|
+
`DiscordErrorCode` is a curated map of the codes bots actually hit:
|
|
53
|
+
|
|
54
|
+
| Name | Code | When |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `UnknownChannel` | 10003 | Channel gone/invisible |
|
|
57
|
+
| `UnknownMessage` | 10008 | Message deleted |
|
|
58
|
+
| `UnknownMember` | 10007 | Member left |
|
|
59
|
+
| `UnknownInteraction` | 10062 | Token expired (the 3s window) |
|
|
60
|
+
| `MissingAccess` | 50001 | No access to the resource |
|
|
61
|
+
| `CannotSendMessagesToThisUser` | 50007 | DMs closed / blocked |
|
|
62
|
+
| `MissingPermissions` | 50013 | Missing a permission |
|
|
63
|
+
| `InteractionHasAlreadyBeenAcknowledged` | 40060 | Double-acked |
|
|
64
|
+
|
|
65
|
+
(See the type for the full set — it mirrors discord.js' `RESTJSONErrorCodes`.)
|
|
66
|
+
|
|
67
|
+
## Transport & rate-limit errors
|
|
68
|
+
|
|
69
|
+
- `isHTTPError(err)` — a transport-level `HTTPError` (timeout, 5xx, aborted): an
|
|
70
|
+
HTTP status with no Discord JSON code.
|
|
71
|
+
- `isRateLimitError(err)` — a `DiscordAPIError` with HTTP status `429`.
|
|
72
|
+
`explainDiscordError` handles this case first, returning a "try again in a
|
|
73
|
+
moment" message.
|