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.
@@ -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,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.