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/options.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Options
|
|
2
|
+
|
|
3
|
+
Slash command options are declared as a map of name → builder. spearkit infers the
|
|
4
|
+
exact value type each option resolves to, so your handler's `ctx.options` is
|
|
5
|
+
fully typed — no casts, no `any`, no manual `getString` calls.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { command, option } from "spearkit";
|
|
9
|
+
|
|
10
|
+
command({
|
|
11
|
+
name: "profile",
|
|
12
|
+
description: "Show a profile",
|
|
13
|
+
options: {
|
|
14
|
+
user: option.user({ description: "Whose profile", required: true }),
|
|
15
|
+
detailed: option.boolean({ description: "Show extra detail" }),
|
|
16
|
+
},
|
|
17
|
+
run: (ctx) => {
|
|
18
|
+
ctx.options.user; // User
|
|
19
|
+
ctx.options.detailed; // boolean | undefined
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Builders and resolved types
|
|
25
|
+
|
|
26
|
+
| Builder | Resolved type | Type-specific config |
|
|
27
|
+
| ------- | ------------- | -------------------- |
|
|
28
|
+
| `option.string(config)` | `string` | `choices`, `minLength`, `maxLength`, `autocomplete` |
|
|
29
|
+
| `option.integer(config)` | `number` | `choices`, `minValue`, `maxValue`, `autocomplete` |
|
|
30
|
+
| `option.number(config)` | `number` | `choices`, `minValue`, `maxValue`, `autocomplete` |
|
|
31
|
+
| `option.boolean(config)` | `boolean` | — |
|
|
32
|
+
| `option.user(config)` | `User` | — |
|
|
33
|
+
| `option.channel(config)` | channel union | `channelTypes` |
|
|
34
|
+
| `option.role(config)` | `Role \| APIRole` | — |
|
|
35
|
+
| `option.mentionable(config)` | user / role / member | — |
|
|
36
|
+
| `option.attachment(config)` | `Attachment` | — |
|
|
37
|
+
|
|
38
|
+
Every builder accepts the common config:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
{
|
|
42
|
+
description: string; // required
|
|
43
|
+
required?: boolean; // default: false
|
|
44
|
+
nameLocalizations?: LocalizationMap;
|
|
45
|
+
descriptionLocalizations?: LocalizationMap;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Inference rules
|
|
50
|
+
|
|
51
|
+
spearkit narrows the resolved type from your declaration:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
options: {
|
|
55
|
+
// required → the value type, never undefined
|
|
56
|
+
name: option.string({ description: "Name", required: true }), // string
|
|
57
|
+
|
|
58
|
+
// optional (default) → value | undefined
|
|
59
|
+
age: option.integer({ description: "Age" }), // number | undefined
|
|
60
|
+
|
|
61
|
+
// choices → a literal union of the choice values
|
|
62
|
+
size: option.string({
|
|
63
|
+
description: "Size",
|
|
64
|
+
choices: [
|
|
65
|
+
{ name: "Small", value: "sm" },
|
|
66
|
+
{ name: "Large", value: "lg" },
|
|
67
|
+
],
|
|
68
|
+
}), // "sm" | "lg" | undefined
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- **Required** options resolve to the value type.
|
|
73
|
+
- **Optional** options resolve to `value | undefined` (spearkit converts discord's
|
|
74
|
+
absent value to `undefined`, never `null`).
|
|
75
|
+
- **`choices`** narrow string/integer/number options to a literal union of the
|
|
76
|
+
declared `value`s.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
run: (ctx) => {
|
|
80
|
+
const name: string = ctx.options.name;
|
|
81
|
+
const age: number | undefined = ctx.options.age;
|
|
82
|
+
const size: "sm" | "lg" | undefined = ctx.options.size;
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Numeric and length constraints
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
options: {
|
|
90
|
+
count: option.integer({ description: "How many", minValue: 1, maxValue: 100 }),
|
|
91
|
+
code: option.string({ description: "Code", minLength: 4, maxLength: 8 }),
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Channel options
|
|
96
|
+
|
|
97
|
+
Restrict the selectable channel types with `channelTypes` (from discord.js
|
|
98
|
+
`ChannelType`):
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { option, ChannelType } from "spearkit";
|
|
102
|
+
|
|
103
|
+
options: {
|
|
104
|
+
target: option.channel({
|
|
105
|
+
description: "A text or announcement channel",
|
|
106
|
+
channelTypes: [ChannelType.GuildText, ChannelType.GuildAnnouncement],
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Choices
|
|
112
|
+
|
|
113
|
+
`choices` are `{ name, value }` pairs. `name` is shown to the user; `value` is
|
|
114
|
+
what your handler receives (and what spearkit narrows the type to).
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
option.integer({
|
|
118
|
+
description: "Priority",
|
|
119
|
+
choices: [
|
|
120
|
+
{ name: "Low", value: 1 },
|
|
121
|
+
{ name: "High", value: 2 },
|
|
122
|
+
],
|
|
123
|
+
// optional per-choice localization:
|
|
124
|
+
// choices: [{ name: "Low", value: 1, nameLocalizations: { tr: "Düşük" } }],
|
|
125
|
+
}); // 1 | 2 | undefined
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Autocomplete
|
|
129
|
+
|
|
130
|
+
Provide an `autocomplete` handler instead of fixed `choices` to suggest values
|
|
131
|
+
as the user types. spearkit marks the option as autocompletable, routes the
|
|
132
|
+
autocomplete interaction, and (for subcommands) finds the right option.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const fruits = ["apple", "apricot", "banana", "cherry"];
|
|
136
|
+
|
|
137
|
+
option.string({
|
|
138
|
+
description: "Fruit",
|
|
139
|
+
required: true,
|
|
140
|
+
autocomplete: (ctx) =>
|
|
141
|
+
fruits
|
|
142
|
+
.filter((f) => f.startsWith(ctx.value))
|
|
143
|
+
.map((f) => ({ name: f, value: f })),
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The autocomplete handler receives an `AutocompleteContext`:
|
|
148
|
+
|
|
149
|
+
| Member | Description |
|
|
150
|
+
| ------ | ----------- |
|
|
151
|
+
| `ctx.value` | The current partial value typed by the user. |
|
|
152
|
+
| `ctx.focusedName` | The name of the option being completed. |
|
|
153
|
+
| `ctx.commandName` | The command being completed. |
|
|
154
|
+
| `ctx.client` / `ctx.user` / `ctx.guild` / `ctx.guildId` | Accessors. |
|
|
155
|
+
| `ctx.respond(choices)` | Send suggestions (capped at discord's 25). |
|
|
156
|
+
|
|
157
|
+
Returning the choices array (as above) is enough — spearkit calls `respond` for
|
|
158
|
+
you. Returning `[]` shows no suggestions.
|
|
159
|
+
|
|
160
|
+
## See also
|
|
161
|
+
|
|
162
|
+
- [Commands](./commands.md) — using options inside commands and subcommands.
|
|
163
|
+
- [Components](./components.md) — buttons, selects and modals.
|
package/docs/plugins.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Plugins
|
|
2
|
+
|
|
3
|
+
A plugin is a named, reusable bundle of commands, events and components. It lets
|
|
4
|
+
you package a feature once and install it into any `SpearClient` with a single
|
|
5
|
+
call — useful for sharing functionality across bots or splitting a large bot into
|
|
6
|
+
self-contained features.
|
|
7
|
+
|
|
8
|
+
## Defining a plugin
|
|
9
|
+
|
|
10
|
+
`definePlugin` is an identity helper: it returns the object you pass it, but gives
|
|
11
|
+
it the `SpearPlugin` type and editor hints.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
interface SpearPlugin {
|
|
15
|
+
name: string;
|
|
16
|
+
setup(client: SpearClient): Awaitable<void>;
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A plugin has a `name` and a `setup` function. `setup` receives the client and
|
|
21
|
+
registers whatever the feature needs — commands, events, components — typically
|
|
22
|
+
via `client.register`.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { definePlugin, button, command, event, option, row } from "spearkit";
|
|
26
|
+
|
|
27
|
+
export const moderation = definePlugin({
|
|
28
|
+
name: "moderation",
|
|
29
|
+
setup(client) {
|
|
30
|
+
const confirmKick = button({
|
|
31
|
+
id: "kick:{userId}",
|
|
32
|
+
label: "Confirm kick",
|
|
33
|
+
style: "Danger",
|
|
34
|
+
run: (ctx) => ctx.update(`Kicked <@${ctx.params.userId}> (demo).`), // userId: string
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const warn = command({
|
|
38
|
+
name: "warn",
|
|
39
|
+
description: "Warn a member",
|
|
40
|
+
options: {
|
|
41
|
+
member: option.user({ description: "Member", required: true }),
|
|
42
|
+
reason: option.string({ description: "Reason" }),
|
|
43
|
+
},
|
|
44
|
+
run: (ctx) =>
|
|
45
|
+
ctx.reply({
|
|
46
|
+
// member: User, reason: string | undefined
|
|
47
|
+
content: `Warning ${ctx.options.member.tag}: ${ctx.options.reason ?? "no reason given"}`,
|
|
48
|
+
components: [row(confirmKick.build({ userId: ctx.options.member.id }))],
|
|
49
|
+
ephemeral: true,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const ready = event("clientReady", (c) => console.log(`[moderation] ready on ${c.user.tag}`));
|
|
54
|
+
|
|
55
|
+
client.register(warn, confirmKick, ready);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Everything declared inside `setup` is local to the plugin; only what you pass to
|
|
61
|
+
`client.register` becomes active on the client.
|
|
62
|
+
|
|
63
|
+
## Installing a plugin
|
|
64
|
+
|
|
65
|
+
Install one or more plugins with `client.use`. It runs each plugin's `setup` in
|
|
66
|
+
order and resolves to the client, so you can chain it with the rest of startup.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { SpearClient, Intents } from "spearkit";
|
|
70
|
+
import { moderation } from "./plugins/moderation.js";
|
|
71
|
+
|
|
72
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
73
|
+
|
|
74
|
+
await client.use(moderation);
|
|
75
|
+
|
|
76
|
+
await client.start(process.env.DISCORD_TOKEN);
|
|
77
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`use` accepts several plugins at once:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
await client.use(moderation, welcome, tickets);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Asynchronous setup
|
|
87
|
+
|
|
88
|
+
`setup` may be async — `client.use` awaits each one before moving to the next. Use
|
|
89
|
+
this to load data, connect to a database, or fetch remote config before
|
|
90
|
+
registering handlers.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
export const tags = definePlugin({
|
|
94
|
+
name: "tags",
|
|
95
|
+
async setup(client) {
|
|
96
|
+
const store = await openTagStore(); // await anything you need first
|
|
97
|
+
|
|
98
|
+
client.register(
|
|
99
|
+
command({
|
|
100
|
+
name: "tag",
|
|
101
|
+
description: "Show a saved tag",
|
|
102
|
+
options: { name: option.string({ description: "Tag name", required: true }) },
|
|
103
|
+
run: (ctx) => ctx.reply(store.get(ctx.options.name) ?? "No such tag."),
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Because `use` awaits `setup`, every plugin is fully installed before
|
|
111
|
+
`client.start` runs.
|
|
112
|
+
|
|
113
|
+
## See also
|
|
114
|
+
|
|
115
|
+
- [Client](./client.md) — `register`, `use`, `start`, and the registries plugins write to.
|
|
116
|
+
- [File-based loading](./loading.md) — discover commands, events and components from a directory instead of bundling them by hand.
|
package/docs/prefix.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Prefix commands
|
|
2
|
+
|
|
3
|
+
Alongside slash commands, spearkit can dispatch classic text/prefix commands like
|
|
4
|
+
`!ping`. You define them with `prefixCommand`, enable them with the client's
|
|
5
|
+
`prefix` option, and spearkit parses each `messageCreate` for you — matching the
|
|
6
|
+
prefix, splitting arguments, and routing to the right handler.
|
|
7
|
+
|
|
8
|
+
## Enabling prefix commands
|
|
9
|
+
|
|
10
|
+
Prefix commands are off until you set the `prefix` option on the client. It
|
|
11
|
+
accepts a string, an array of strings, or a `PrefixOptions` object:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Intents, SpearClient } from "spearkit";
|
|
15
|
+
|
|
16
|
+
// A single prefix.
|
|
17
|
+
new SpearClient({ intents: Intents.messages, prefix: "!" });
|
|
18
|
+
|
|
19
|
+
// Several prefixes.
|
|
20
|
+
new SpearClient({ intents: Intents.messages, prefix: ["!", "?"] });
|
|
21
|
+
|
|
22
|
+
// Full control.
|
|
23
|
+
new SpearClient({
|
|
24
|
+
intents: Intents.messages,
|
|
25
|
+
prefix: {
|
|
26
|
+
prefix: "!",
|
|
27
|
+
mention: true, // also trigger on a leading @bot mention (default true)
|
|
28
|
+
ignoreBots: true, // skip messages authored by bots (default true)
|
|
29
|
+
caseInsensitive: true, // match command names ignoring case (default true)
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## You need the MessageContent intent
|
|
35
|
+
|
|
36
|
+
Reading the text of other users' messages is a **privileged** gateway intent.
|
|
37
|
+
Without `MessageContent` your bot still receives `messageCreate`, but
|
|
38
|
+
`message.content` arrives empty for messages it was not mentioned in or did not
|
|
39
|
+
author — so no prefix command will ever match.
|
|
40
|
+
|
|
41
|
+
Use the `Intents.messages` preset, which includes `Guilds`, `GuildMessages`, and
|
|
42
|
+
the privileged `MessageContent` bit:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Intents, SpearClient } from "spearkit";
|
|
46
|
+
|
|
47
|
+
const client = new SpearClient({ intents: Intents.messages, prefix: "!" });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You must also toggle **Message Content Intent** on for your application in the
|
|
51
|
+
Discord Developer Portal, or the gateway will reject the connection.
|
|
52
|
+
|
|
53
|
+
## Defining a command
|
|
54
|
+
|
|
55
|
+
`prefixCommand` takes the command name, the handler, and a few optional fields:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { prefixCommand } from "spearkit";
|
|
59
|
+
|
|
60
|
+
export const ping = prefixCommand({
|
|
61
|
+
name: "ping",
|
|
62
|
+
description: "Check that the bot is alive",
|
|
63
|
+
run: (ctx) => ctx.reply("Pong!"),
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Register it like anything else, with `client.register(...)`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { SpearClient } from "spearkit";
|
|
71
|
+
|
|
72
|
+
const client = new SpearClient({ prefix: "!" });
|
|
73
|
+
client.register(ping);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Field | Type | Effect |
|
|
77
|
+
| ----- | ---- | ------ |
|
|
78
|
+
| `name` | `string` | The word after the prefix that triggers the command. |
|
|
79
|
+
| `aliases` | `string[]` | Extra names that also trigger it. |
|
|
80
|
+
| `description` | `string` | Human description, for your own help command. |
|
|
81
|
+
| `cooldown` | `number \| CooldownConfig` | Per-user rate limit (a number is milliseconds). |
|
|
82
|
+
| `run` | `(ctx: PrefixContext) => void \| Promise<void>` | The handler. |
|
|
83
|
+
|
|
84
|
+
## The prefix context
|
|
85
|
+
|
|
86
|
+
The handler receives a `PrefixContext`. It wraps the triggering `Message` and
|
|
87
|
+
adds the parsed arguments plus reply helpers.
|
|
88
|
+
|
|
89
|
+
| Member | Description |
|
|
90
|
+
| ------ | ----------- |
|
|
91
|
+
| `ctx.message` | The triggering discord.js `Message`. |
|
|
92
|
+
| `ctx.commandName` | The matched name as the user typed it (an alias if they used one). |
|
|
93
|
+
| `ctx.args` | Whitespace-split arguments after the command name (`string[]`). |
|
|
94
|
+
| `ctx.rest` | The raw text after the command name (unsplit). |
|
|
95
|
+
| `ctx.author` / `ctx.member` / `ctx.guild` / `ctx.guildId` / `ctx.channel` / `ctx.channelId` | Actor and location accessors. |
|
|
96
|
+
| `ctx.reply(content)` | Reply to the triggering message. |
|
|
97
|
+
| `ctx.send(content)` | Send a message to the same channel without a reply reference. |
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { prefixCommand } from "spearkit";
|
|
101
|
+
|
|
102
|
+
export const echo = prefixCommand({
|
|
103
|
+
name: "echo",
|
|
104
|
+
description: "Repeat what you said",
|
|
105
|
+
run: (ctx) => {
|
|
106
|
+
if (ctx.args.length === 0) return ctx.reply("Give me something to echo.");
|
|
107
|
+
// `args` is split on whitespace; `rest` is the untouched remainder.
|
|
108
|
+
return ctx.reply(ctx.rest);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`ctx.args` and `ctx.rest` are two views of the same input: `!say hello world`
|
|
114
|
+
gives `args === ["hello", "world"]` and `rest === "hello world"`.
|
|
115
|
+
|
|
116
|
+
## Aliases
|
|
117
|
+
|
|
118
|
+
List alternative names in `aliases`; any of them triggers the command, and
|
|
119
|
+
`ctx.commandName` reports whichever the user typed:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { prefixCommand } from "spearkit";
|
|
123
|
+
|
|
124
|
+
export const help = prefixCommand({
|
|
125
|
+
name: "help",
|
|
126
|
+
aliases: ["h", "commands"],
|
|
127
|
+
run: (ctx) => ctx.reply(`You used "${ctx.commandName}".`),
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Cooldowns
|
|
132
|
+
|
|
133
|
+
Prefix commands share the client's cooldown manager (`client.cooldowns`) with
|
|
134
|
+
slash commands, so the API is identical. Pass `cooldown` as a number of
|
|
135
|
+
milliseconds or a full `CooldownConfig`:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { prefixCommand } from "spearkit";
|
|
139
|
+
|
|
140
|
+
export const daily = prefixCommand({
|
|
141
|
+
name: "daily",
|
|
142
|
+
description: "Claim your daily reward",
|
|
143
|
+
cooldown: 5_000, // one use per user per 5s
|
|
144
|
+
run: (ctx) => ctx.reply("Reward claimed! Come back soon."),
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
When a user is on cooldown, spearkit replies with the remaining time and does not
|
|
149
|
+
run the handler. A per-command `cooldown` overrides the client-wide `cooldown`
|
|
150
|
+
default. See [Cooldowns](./cooldown.md) for scopes and configuration.
|
|
151
|
+
|
|
152
|
+
## The prefix registry
|
|
153
|
+
|
|
154
|
+
`client.prefix` is a `PrefixRegistry`. The client wires it to `messageCreate`,
|
|
155
|
+
the logger, and the cooldown manager for you, so you rarely call it directly. It
|
|
156
|
+
is available for introspection and advanced control:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
client.prefix.get("ping"); // PrefixCommand | undefined (also resolves aliases)
|
|
160
|
+
client.prefix.list(); // PrefixCommand[] (excludes aliases)
|
|
161
|
+
client.prefix.size; // number of commands
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Error handling
|
|
165
|
+
|
|
166
|
+
If a handler throws, spearkit catches it, logs it, and calls your error hook if you
|
|
167
|
+
set one — the process never crashes:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
client.prefix.onError((error, message) => {
|
|
171
|
+
console.error(`prefix command failed in #${message.channelId}`, error);
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## See also
|
|
176
|
+
|
|
177
|
+
- [Commands](./commands.md) — slash commands.
|
|
178
|
+
- [Cooldowns](./cooldown.md) — the shared rate limiter.
|
|
179
|
+
- [Usage tracking](./usage.md) — record who runs which prefix commands.
|
|
180
|
+
- [Client](./client.md) — the `prefix` option and intent presets.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Scheduled tasks
|
|
2
|
+
|
|
3
|
+
Run work on a cron schedule or a fixed interval. The client starts the
|
|
4
|
+
scheduler when it becomes ready and stops it on `destroy()`, so timers never
|
|
5
|
+
outlive your bot.
|
|
6
|
+
|
|
7
|
+
## Define a task
|
|
8
|
+
|
|
9
|
+
Provide exactly one of `cron` or `interval`:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { task } from "spearkit";
|
|
13
|
+
|
|
14
|
+
export const heartbeat = task({
|
|
15
|
+
name: "heartbeat",
|
|
16
|
+
interval: 60_000, // every minute
|
|
17
|
+
runOnStart: true, // also run once on startup
|
|
18
|
+
run: (client) => client.logger.info("still alive"),
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Register it like anything else:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
client.register(heartbeat);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or define and register in one call:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
client.schedule({
|
|
32
|
+
name: "cleanup",
|
|
33
|
+
cron: "0 3 * * *", // 03:00 local time, every day
|
|
34
|
+
run: async (client) => {
|
|
35
|
+
// …purge expired records…
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Cron syntax
|
|
41
|
+
|
|
42
|
+
Standard 5-field expressions, evaluated in the host's **local** time:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
┌─ minute (0-59)
|
|
46
|
+
│ ┌─ hour (0-23)
|
|
47
|
+
│ │ ┌─ day of month (1-31)
|
|
48
|
+
│ │ │ ┌─ month (1-12)
|
|
49
|
+
│ │ │ │ ┌─ day of week (0-6, Sunday = 0)
|
|
50
|
+
│ │ │ │ │
|
|
51
|
+
* * * * *
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Each field supports `*`, ranges (`1-5`), lists (`1,3,5`) and steps (`*/15`).
|
|
55
|
+
When both day-of-month and day-of-week are restricted, a date matches if
|
|
56
|
+
**either** does (standard cron behaviour).
|
|
57
|
+
|
|
58
|
+
Aliases: `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
task({ name: "report", cron: "@daily", run: () => {} });
|
|
62
|
+
task({ name: "poll", cron: "*/5 * * * *", run: () => {} }); // every 5 minutes
|
|
63
|
+
task({ name: "mondays", cron: "0 9 * * 1", run: () => {} }); // Mon 09:00
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Compute the next run yourself with `cron`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { cron } from "spearkit";
|
|
70
|
+
|
|
71
|
+
const next = cron("*/15 * * * *").next(new Date());
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## The scheduler
|
|
75
|
+
|
|
76
|
+
`client.scheduler` is the `TaskScheduler`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
client.scheduler.size; // number of tasks
|
|
80
|
+
client.scheduler.active; // started?
|
|
81
|
+
client.scheduler.list(); // every task
|
|
82
|
+
client.scheduler.remove("heartbeat"); // cancel + forget
|
|
83
|
+
client.scheduler.stop(); // cancel all timers
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Task errors are caught and logged through `client.logger` (scope `scheduler`),
|
|
87
|
+
so a throwing task never crashes the process or stops future runs.
|