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
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.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Permissions & hierarchy
|
|
2
|
+
|
|
3
|
+
Moderation commands fail in two predictable ways: the bot lacks a permission in
|
|
4
|
+
the channel (`Missing Permissions`, 50013), or the target sits above the bot (or
|
|
5
|
+
the moderator) in the role list. Both are checkable *before* you act, so you can
|
|
6
|
+
bail out with a clear message instead of a half-finished action and an exception.
|
|
7
|
+
|
|
8
|
+
## Did the bot/user get the permissions? (zero-fetch)
|
|
9
|
+
|
|
10
|
+
Every interaction carries the bot's and the invoker's resolved permissions for
|
|
11
|
+
the current channel. `ctx.botMissing(...)` / `ctx.userMissing(...)` read them
|
|
12
|
+
with no API calls and return the **missing** flag names:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { PermissionFlagsBits, command, formatPermissions } from "spearkit";
|
|
16
|
+
|
|
17
|
+
export const slowmode = command({
|
|
18
|
+
name: "slowmode",
|
|
19
|
+
description: "Set channel slowmode",
|
|
20
|
+
run: async (ctx) => {
|
|
21
|
+
const missing = ctx.botMissing(PermissionFlagsBits.ManageChannels);
|
|
22
|
+
if (missing.length > 0) return ctx.error(`I need: ${formatPermissions(missing)}`);
|
|
23
|
+
// …
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`formatPermissions(...)` renders flag names as a friendly list
|
|
29
|
+
(`"Manage Channels, Ban Members"`).
|
|
30
|
+
|
|
31
|
+
## Permissions in another channel
|
|
32
|
+
|
|
33
|
+
For a channel other than the current one, use the standalone helpers:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { botMissingPermissions, hasPermissions, missingPermissions } from "spearkit";
|
|
37
|
+
|
|
38
|
+
const missing = botMissingPermissions(targetChannel, [PermissionFlagsBits.SendMessages]);
|
|
39
|
+
if (missing.length > 0) return ctx.error("I can't post there.");
|
|
40
|
+
|
|
41
|
+
// or for a specific member/role:
|
|
42
|
+
hasPermissions(targetChannel, member, PermissionFlagsBits.ViewChannel); // boolean
|
|
43
|
+
missingPermissions(targetChannel, role, [PermissionFlagsBits.Connect]); // PermissionsString[]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Role hierarchy
|
|
47
|
+
|
|
48
|
+
`moderationCheck(...)` validates both the moderator and the bot against a target,
|
|
49
|
+
returning a ready-to-show reason on the first failing rule (self, server owner,
|
|
50
|
+
moderator hierarchy, bot hierarchy):
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { moderationCheck } from "spearkit";
|
|
54
|
+
|
|
55
|
+
const moderator = await ctx.guild!.members.fetch(ctx.user.id);
|
|
56
|
+
const check = moderationCheck({ moderator, target, action: "ban" });
|
|
57
|
+
if (!check.ok) return ctx.error(check.reason);
|
|
58
|
+
await target.ban();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The `me` (bot) member defaults to `target.guild.members.me`; pass `me: null` to
|
|
62
|
+
skip the bot check. `action` is the verb used in messages (default `"moderate"`).
|
|
63
|
+
|
|
64
|
+
Lower-level primitives are exported too:
|
|
65
|
+
|
|
66
|
+
- `canActOn(actor, target)` — boolean: not self, target isn't the owner, actor is
|
|
67
|
+
the owner or outranks the target.
|
|
68
|
+
- `compareRoles(a, b)` — highest-role position comparison (`>0`, `<0`, `0`).
|
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,234 @@
|
|
|
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
|
+
### Dynamic (per-guild) prefixes
|
|
35
|
+
|
|
36
|
+
Pass `dynamic` to resolve extra prefix(es) per message — for example a custom
|
|
37
|
+
per-guild prefix from a database or [`createSettings`](./api-reference.md#persistent-storage).
|
|
38
|
+
Dynamic prefixes are tried in addition to any static `prefix`; return
|
|
39
|
+
`null`/`undefined` for none. It runs on every candidate message, so keep it fast
|
|
40
|
+
(and cached).
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
new SpearClient({
|
|
44
|
+
intents: Intents.messages,
|
|
45
|
+
prefix: {
|
|
46
|
+
prefix: "!", // static fallback
|
|
47
|
+
dynamic: async (message) =>
|
|
48
|
+
message.guildId ? await settings.get(message.guildId).then((s) => s.prefix) : null,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## You need the MessageContent intent
|
|
54
|
+
|
|
55
|
+
Reading the text of other users' messages is a **privileged** gateway intent.
|
|
56
|
+
Without `MessageContent` your bot still receives `messageCreate`, but
|
|
57
|
+
`message.content` arrives empty for messages it was not mentioned in or did not
|
|
58
|
+
author — so no prefix command will ever match.
|
|
59
|
+
|
|
60
|
+
Use the `Intents.messages` preset, which includes `Guilds`, `GuildMessages`, and
|
|
61
|
+
the privileged `MessageContent` bit:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { Intents, SpearClient } from "spearkit";
|
|
65
|
+
|
|
66
|
+
const client = new SpearClient({ intents: Intents.messages, prefix: "!" });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You must also toggle **Message Content Intent** on for your application in the
|
|
70
|
+
Discord Developer Portal, or the gateway will reject the connection.
|
|
71
|
+
|
|
72
|
+
## Defining a command
|
|
73
|
+
|
|
74
|
+
`prefixCommand` takes the command name, the handler, and a few optional fields:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { prefixCommand } from "spearkit";
|
|
78
|
+
|
|
79
|
+
export const ping = prefixCommand({
|
|
80
|
+
name: "ping",
|
|
81
|
+
description: "Check that the bot is alive",
|
|
82
|
+
run: (ctx) => ctx.reply("Pong!"),
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Register it like anything else, with `client.register(...)`:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { SpearClient } from "spearkit";
|
|
90
|
+
|
|
91
|
+
const client = new SpearClient({ prefix: "!" });
|
|
92
|
+
client.register(ping);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Field | Type | Effect |
|
|
96
|
+
| ----- | ---- | ------ |
|
|
97
|
+
| `name` | `string` | The word after the prefix that triggers the command. |
|
|
98
|
+
| `aliases` | `string[]` | Extra names that also trigger it. |
|
|
99
|
+
| `description` | `string` | Human description, for your own help command. |
|
|
100
|
+
| `cooldown` | `number \| CooldownConfig` | Per-user rate limit (a number is milliseconds). |
|
|
101
|
+
| `guards` | `readonly Guard[]` | Preconditions run before the handler. See [Guards](./guards.md). |
|
|
102
|
+
| `args` | `(a) => PrefixArgsBuilder` | Typed argument schema; shapes `ctx.options`. See [Typed arguments](#typed-arguments). |
|
|
103
|
+
| `run` | `(ctx: PrefixContext) => void \| Promise<void>` | The handler. |
|
|
104
|
+
|
|
105
|
+
## The prefix context
|
|
106
|
+
|
|
107
|
+
The handler receives a `PrefixContext`. It wraps the triggering `Message` and
|
|
108
|
+
adds the parsed arguments plus reply helpers.
|
|
109
|
+
|
|
110
|
+
| Member | Description |
|
|
111
|
+
| ------ | ----------- |
|
|
112
|
+
| `ctx.message` | The triggering discord.js `Message`. |
|
|
113
|
+
| `ctx.commandName` | The matched name as the user typed it (an alias if they used one). |
|
|
114
|
+
| `ctx.args` | Whitespace-split arguments after the command name (`string[]`). |
|
|
115
|
+
| `ctx.rest` | The raw text after the command name (unsplit). |
|
|
116
|
+
| `ctx.options` | Typed parsed arguments from the `args` schema (`{}` when none). |
|
|
117
|
+
| `ctx.author` / `ctx.member` / `ctx.guild` / `ctx.guildId` / `ctx.channel` / `ctx.channelId` | Actor and location accessors. |
|
|
118
|
+
| `ctx.reply(content)` | Reply to the triggering message. |
|
|
119
|
+
| `ctx.send(content)` | Send a message to the same channel without a reply reference. |
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { prefixCommand } from "spearkit";
|
|
123
|
+
|
|
124
|
+
export const echo = prefixCommand({
|
|
125
|
+
name: "echo",
|
|
126
|
+
description: "Repeat what you said",
|
|
127
|
+
run: (ctx) => {
|
|
128
|
+
if (ctx.args.length === 0) return ctx.reply("Give me something to echo.");
|
|
129
|
+
// `args` is split on whitespace; `rest` is the untouched remainder.
|
|
130
|
+
return ctx.reply(ctx.rest);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`ctx.args` and `ctx.rest` are two views of the same input: `!say hello world`
|
|
136
|
+
gives `args === ["hello", "world"]` and `rest === "hello world"`.
|
|
137
|
+
|
|
138
|
+
## Typed arguments
|
|
139
|
+
|
|
140
|
+
Pass an `args` schema to parse positional arguments into typed values. Chain
|
|
141
|
+
builder methods — first token → first arg, second → second, and so on — and read
|
|
142
|
+
the result from `ctx.options`. Each method requires a name and takes optional
|
|
143
|
+
settings (`required`, `default`, and per-type bounds).
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { prefixCommand } from "spearkit";
|
|
147
|
+
|
|
148
|
+
export const mute = prefixCommand({
|
|
149
|
+
name: "mute",
|
|
150
|
+
description: "Mute a member",
|
|
151
|
+
args: (a) =>
|
|
152
|
+
a
|
|
153
|
+
.snowflake("target", { required: true }) // raw id or <@mention> → string
|
|
154
|
+
.duration("duration", { required: true }) // "1h30m" → number (ms)
|
|
155
|
+
.rest("reason", { default: "No reason given" }), // remaining text → string
|
|
156
|
+
run: (ctx) => {
|
|
157
|
+
ctx.options.target; // string
|
|
158
|
+
ctx.options.duration; // number
|
|
159
|
+
ctx.options.reason; // string
|
|
160
|
+
return ctx.reply(`Muted <@${ctx.options.target}> for ${ctx.options.duration}ms.`);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Builder methods: `.string`, `.integer`, `.number`, `.boolean`, `.snowflake`,
|
|
166
|
+
`.duration`, `.rest`. A missing required argument — or a value that fails to
|
|
167
|
+
parse — makes spearkit reply with an error and skip the handler. Without an `args`
|
|
168
|
+
schema, `ctx.options` is `{}`; use `ctx.args` / `ctx.rest` for raw access.
|
|
169
|
+
|
|
170
|
+
## Aliases
|
|
171
|
+
|
|
172
|
+
List alternative names in `aliases`; any of them triggers the command, and
|
|
173
|
+
`ctx.commandName` reports whichever the user typed:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { prefixCommand } from "spearkit";
|
|
177
|
+
|
|
178
|
+
export const help = prefixCommand({
|
|
179
|
+
name: "help",
|
|
180
|
+
aliases: ["h", "commands"],
|
|
181
|
+
run: (ctx) => ctx.reply(`You used "${ctx.commandName}".`),
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Cooldowns
|
|
186
|
+
|
|
187
|
+
Prefix commands share the client's cooldown manager (`client.cooldowns`) with
|
|
188
|
+
slash commands, so the API is identical. Pass `cooldown` as a number of
|
|
189
|
+
milliseconds or a full `CooldownConfig`:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { prefixCommand } from "spearkit";
|
|
193
|
+
|
|
194
|
+
export const daily = prefixCommand({
|
|
195
|
+
name: "daily",
|
|
196
|
+
description: "Claim your daily reward",
|
|
197
|
+
cooldown: 5_000, // one use per user per 5s
|
|
198
|
+
run: (ctx) => ctx.reply("Reward claimed! Come back soon."),
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
When a user is on cooldown, spearkit replies with the remaining time and does not
|
|
203
|
+
run the handler. A per-command `cooldown` overrides the client-wide `cooldown`
|
|
204
|
+
default. See [Cooldowns](./cooldown.md) for scopes and configuration.
|
|
205
|
+
|
|
206
|
+
## The prefix registry
|
|
207
|
+
|
|
208
|
+
`client.prefix` is a `PrefixRegistry`. The client wires it to `messageCreate`,
|
|
209
|
+
the logger, and the cooldown manager for you, so you rarely call it directly. It
|
|
210
|
+
is available for introspection and advanced control:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
client.prefix.get("ping"); // PrefixCommand | undefined (also resolves aliases)
|
|
214
|
+
client.prefix.list(); // PrefixCommand[] (excludes aliases)
|
|
215
|
+
client.prefix.size; // number of commands
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Error handling
|
|
219
|
+
|
|
220
|
+
If a handler throws, spearkit catches it, logs it, and calls your error hook if you
|
|
221
|
+
set one — the process never crashes:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
client.prefix.onError((error, message) => {
|
|
225
|
+
console.error(`prefix command failed in #${message.channelId}`, error);
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## See also
|
|
230
|
+
|
|
231
|
+
- [Commands](./commands.md) — slash commands.
|
|
232
|
+
- [Cooldowns](./cooldown.md) — the shared rate limiter.
|
|
233
|
+
- [Usage tracking](./usage.md) — record who runs which prefix commands.
|
|
234
|
+
- [Client](./client.md) — the `prefix` option and intent presets.
|
|
@@ -0,0 +1,111 @@
|
|
|
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` (if both are set, the interval is used):
|
|
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`/`@annually`, `@monthly`, `@weekly`, `@daily`/`@midnight`, `@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
|
+
## One-shot jobs, follow-ups and on-ready recovery
|
|
75
|
+
|
|
76
|
+
Beyond recurring tasks, the scheduler runs one-shot timers (they `unref()`
|
|
77
|
+
themselves, so they never keep the process alive) and a once-on-ready reconciler.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// Run once after a delay; returns a cancel handle.
|
|
81
|
+
const handle = client.scheduler.delay("remind", 10 * 60_000, async () => {
|
|
82
|
+
// …remind the moderator if nothing happened…
|
|
83
|
+
});
|
|
84
|
+
handle.cancel(); // true if it was still pending
|
|
85
|
+
|
|
86
|
+
// A series of fires measured from "now"; the callback gets the fire index.
|
|
87
|
+
client.scheduler.followUp("escalate", [10_000, 30_000, 60_000], (i) => {
|
|
88
|
+
// i = 0, then 1, then 2
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Run once the first time the scheduler starts (typically on clientReady) and
|
|
92
|
+
// never again — ideal for restart recovery.
|
|
93
|
+
client.scheduler.reconcile("voice-sessions", async (client) => {
|
|
94
|
+
// …close orphaned voice sessions, reapply cached state…
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## The scheduler
|
|
99
|
+
|
|
100
|
+
`client.scheduler` is the `TaskScheduler`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
client.scheduler.size; // number of tasks
|
|
104
|
+
client.scheduler.active; // started?
|
|
105
|
+
client.scheduler.list(); // every task
|
|
106
|
+
client.scheduler.remove("heartbeat"); // cancel + forget
|
|
107
|
+
client.scheduler.stop(); // cancel all timers
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Task errors are caught and logged through `client.logger` (scope `scheduler`),
|
|
111
|
+
so a throwing task never crashes the process or stops future runs.
|