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/docs/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # spearkit documentation
2
+
3
+ **discord.js++** — a developer-experience-first layer over discord.js. spearkit
4
+ re-exports all of discord.js (so it's a drop-in replacement) and adds an
5
+ ergonomic, fully type-safe API for events, slash commands and interactive
6
+ components.
7
+
8
+ ## Contents
9
+
10
+ 1. [Getting started](./getting-started.md) — install, first bot, project layout.
11
+ 2. [Client](./client.md) — `SpearClient`, intents, `register`, `start`, deployment.
12
+ 3. [Commands](./commands.md) — slash commands, subcommands, permissions, deployment.
13
+ 4. [Options](./options.md) — typed option builders, choices, autocomplete.
14
+ 5. [Components](./components.md) — buttons, selects, modals, custom-id routing.
15
+ 6. [Events](./events.md) — the `event()` helper and the event registry.
16
+ 7. [Contexts](./context.md) — reply helpers shared by every handler.
17
+ 8. [Cooldowns](./cooldown.md) — per-user/role/guild rate limiting.
18
+ 9. [Scheduled tasks](./scheduler.md) — cron and interval jobs.
19
+ 10. [Prefix commands](./prefix.md) — classic `!text` commands.
20
+ 11. [Logging](./logging.md) — structured, leveled, scoped logging.
21
+ 12. [Usage tracking](./usage.md) — record who used what (store + Discord channel).
22
+ 13. [Environment & dotenv](./env.md) — load `.env` and read typed env vars.
23
+ 14. [Plugins](./plugins.md) — bundling features into reusable units.
24
+ 15. [File-based loading](./loading.md) — one file per command/event/component.
25
+ 16. [Migrating from discord.js](./migration.md) — the drop-in path.
26
+ 17. [API reference](./api-reference.md) — every exported symbol.
27
+
28
+ ## Why spearkit
29
+
30
+ - **Drop-in.** `import { Client, EmbedBuilder } from "spearkit"` — every discord.js
31
+ export is available, so you can migrate one file at a time.
32
+ - **Fully type-safe.** No `any` or `unknown` leaks into your handlers. Option
33
+ values, custom-id params and modal fields are all inferred from your
34
+ definitions.
35
+ - **Co-located.** A command's options and handler, a button's appearance and
36
+ click logic, a modal's fields and submit logic — each lives in one place.
37
+ - **No boilerplate.** No `interactionCreate` switch statements; spearkit routes
38
+ commands, autocomplete, buttons, selects and modals for you.
39
+
40
+ ## Thirty-second tour
41
+
42
+ ```ts
43
+ import { SpearClient, Intents, command, option, button, row, event } from "spearkit";
44
+
45
+ const client = new SpearClient({ intents: Intents.default });
46
+
47
+ const greet = command({
48
+ name: "greet",
49
+ description: "Greet someone",
50
+ options: { who: option.user({ description: "Who", required: true }) },
51
+ run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
52
+ });
53
+
54
+ const ping = button({
55
+ id: "ping:{n}",
56
+ label: "Ping",
57
+ run: (ctx) => ctx.update(`pong #${ctx.params.n}`), // n: string
58
+ });
59
+
60
+ client.register(greet, ping, event("clientReady", (c) => console.log(c.user.tag)));
61
+ await client.start(process.env.DISCORD_TOKEN);
62
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
63
+ ```
@@ -0,0 +1,589 @@
1
+ # API reference
2
+
3
+ Every symbol spearkit exports, in addition to the entire re-exported discord.js
4
+ surface. Import any of these from `"spearkit"`.
5
+
6
+ ```ts
7
+ import { SpearClient, command, option, event, button, modal, row /* … */ } from "spearkit";
8
+ ```
9
+
10
+ ---
11
+
12
+ ## Client
13
+
14
+ ### `class SpearClient extends Client`
15
+
16
+ A discord.js `Client` with registries and interaction routing wired up.
17
+
18
+ ```ts
19
+ new SpearClient(options?: SpearClientOptions)
20
+ ```
21
+
22
+ | Member | Type | Description |
23
+ | ------ | ---- | ----------- |
24
+ | `commands` | `CommandRegistry` | Slash command registry + dispatcher. |
25
+ | `events` | `EventRegistry` | Event listener registry. |
26
+ | `components` | `ComponentRegistry` | Button/select/modal router. |
27
+ | `register(...items: Registerable[])` | `this` | Route each item to the matching registry. |
28
+ | `use(...plugins: SpearPlugin[])` | `Promise<this>` | Run each plugin's `setup`. |
29
+ | `load(dir: string, options?: LoadOptions)` | `Promise<number>` | Import a directory and register its exports. Returns count. |
30
+ | `start(token?: string)` | `Promise<this>` | Log in (falls back to `DISCORD_TOKEN`). |
31
+ | `deployCommands(options?: { guildId?: string })` | `Promise<DeployResult>` | Push commands using the client's REST. Call after ready. |
32
+
33
+ Inherits everything from discord.js `Client` (`on`, `once`, `login`, `ws`, `rest`, `application`, `user`, …).
34
+
35
+ ### `type SpearClientOptions = Partial<ClientOptions>`
36
+
37
+ Same as discord.js `ClientOptions`, but `intents` may be omitted (defaults to `Intents.default`).
38
+
39
+ ### `const Intents`
40
+
41
+ Ready-made intent presets (arrays of `GatewayIntentBits`).
42
+
43
+ | Key | Contents |
44
+ | --- | -------- |
45
+ | `Intents.none` | `[]` |
46
+ | `Intents.default` | `[Guilds]` |
47
+ | `Intents.guilds` | `[Guilds, GuildMembers]` |
48
+ | `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
49
+ | `Intents.all` | Every intent (includes privileged). |
50
+
51
+ ### `type Registerable = SlashCommand | EventDef | ComponentDef`
52
+
53
+ The union accepted by `SpearClient.register`.
54
+
55
+ ---
56
+
57
+ ## Commands
58
+
59
+ ### `function command<O, R>(config): SlashCommand`
60
+
61
+ Define a leaf slash command.
62
+
63
+ ```ts
64
+ interface CommandConfig<O extends OptionMap, R> {
65
+ name: string;
66
+ description: string;
67
+ options?: O;
68
+ defaultMemberPermissions?: PermissionResolvable | null;
69
+ nsfw?: boolean;
70
+ guildOnly?: boolean;
71
+ nameLocalizations?: LocalizationMap;
72
+ descriptionLocalizations?: LocalizationMap;
73
+ run: (ctx: CommandContext<O>) => Awaitable<R>;
74
+ }
75
+ ```
76
+
77
+ ### `function commandGroup(config: CommandGroupConfig): SlashCommand`
78
+
79
+ Define a command that routes to subcommands and/or subcommand groups.
80
+
81
+ ```ts
82
+ interface CommandGroupConfig {
83
+ name: string;
84
+ description: string;
85
+ subcommands?: Record<string, Subcommand>;
86
+ groups?: Record<string, SubcommandGroup>;
87
+ defaultMemberPermissions?: PermissionResolvable | null;
88
+ nsfw?: boolean;
89
+ guildOnly?: boolean;
90
+ nameLocalizations?: LocalizationMap;
91
+ descriptionLocalizations?: LocalizationMap;
92
+ }
93
+ ```
94
+
95
+ ### `function subcommand<O, R>(config): Subcommand`
96
+
97
+ ```ts
98
+ interface SubcommandConfig<O extends OptionMap, R> {
99
+ description: string;
100
+ options?: O;
101
+ nameLocalizations?: LocalizationMap;
102
+ descriptionLocalizations?: LocalizationMap;
103
+ run: (ctx: CommandContext<O>) => Awaitable<R>;
104
+ }
105
+ ```
106
+
107
+ ### `function subcommandGroup(config: SubcommandGroupConfig): SubcommandGroup`
108
+
109
+ ```ts
110
+ interface SubcommandGroupConfig {
111
+ description: string;
112
+ subcommands: Record<string, Subcommand>;
113
+ nameLocalizations?: LocalizationMap;
114
+ descriptionLocalizations?: LocalizationMap;
115
+ }
116
+ ```
117
+
118
+ ### `class SlashCommand`
119
+
120
+ | Member | Type | Description |
121
+ | ------ | ---- | ----------- |
122
+ | `name` | `string` | Top-level command name. |
123
+ | `hasAutocomplete` | `boolean` | True if any option declares autocomplete. |
124
+ | `toJSON()` | `RESTPostAPIChatInputApplicationCommandsJSONBody` | REST payload. |
125
+ | `execute(interaction)` | `Promise<void>` | Run for a chat-input interaction. |
126
+ | `autocomplete(interaction)` | `Promise<void>` | Run autocomplete for the focused option. |
127
+
128
+ ### `class CommandContext<O> extends BaseContext<ChatInputCommandInteraction>`
129
+
130
+ | Member | Type | Description |
131
+ | ------ | ---- | ----------- |
132
+ | `options` | `ResolvedOptions<O>` | Resolved, fully-typed option values. |
133
+ | `commandName` | `string` | Invoked command name. |
134
+ | `subcommand` | `string \| null` | Invoked subcommand, if any. |
135
+ | `showModal(modal)` | `Promise<void>` | Present a modal. |
136
+
137
+ Plus all `BaseContext` members.
138
+
139
+ ### `class CommandRegistry`
140
+
141
+ | Member | Type | Description |
142
+ | ------ | ---- | ----------- |
143
+ | `add(...commands: SlashCommand[])` | `this` | Register commands (override by name). |
144
+ | `remove(name: string)` | `boolean` | Remove a command. |
145
+ | `get(name: string)` | `SlashCommand \| undefined` | Look up a command. |
146
+ | `all()` | `SlashCommand[]` | All commands. |
147
+ | `names` | `string[]` | All command names. |
148
+ | `size` | `number` | Count. |
149
+ | `onError(handler: CommandErrorHandler)` | `this` | Set the error handler. |
150
+ | `toJSON()` | `RESTPostAPIApplicationCommandsJSONBody[]` | Serialise all commands. |
151
+ | `handle(interaction)` | `Promise<void>` | Dispatch a chat-input interaction. |
152
+ | `handleAutocomplete(interaction)` | `Promise<void>` | Dispatch an autocomplete interaction. |
153
+ | `deploy(options: DeployOptions)` | `Promise<DeployResult>` | Push commands to discord. |
154
+
155
+ ```ts
156
+ type CommandErrorHandler = (error: Error, interaction: ChatInputCommandInteraction) => Awaitable<void>;
157
+ interface DeployOptions { token?: string; applicationId: string; guildId?: string; rest?: REST; }
158
+ type DeployResult = RESTPutAPIApplicationCommandsResult | RESTPutAPIApplicationGuildCommandsResult;
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Options
164
+
165
+ ### `const option`
166
+
167
+ Type-safe option builders. Each returns an `OptionDef` whose resolved value type
168
+ is inferred (required → value, optional → `value | undefined`, `choices` →
169
+ literal union).
170
+
171
+ | Builder | Resolved type | Extra config |
172
+ | ------- | ------------- | ------------ |
173
+ | `option.string(config)` | `string` | `choices?`, `minLength?`, `maxLength?`, `autocomplete?` |
174
+ | `option.integer(config)` | `number` | `choices?`, `minValue?`, `maxValue?`, `autocomplete?` |
175
+ | `option.number(config)` | `number` | `choices?`, `minValue?`, `maxValue?`, `autocomplete?` |
176
+ | `option.boolean(config)` | `boolean` | — |
177
+ | `option.user(config)` | `User` | — |
178
+ | `option.channel(config)` | channel union | `channelTypes?` |
179
+ | `option.role(config)` | `Role \| APIRole` | — |
180
+ | `option.mentionable(config)` | user/role/member | — |
181
+ | `option.attachment(config)` | `Attachment` | — |
182
+
183
+ Common config (`BaseConfig`):
184
+
185
+ ```ts
186
+ {
187
+ description: string;
188
+ required?: boolean; // default false
189
+ nameLocalizations?: LocalizationMap;
190
+ descriptionLocalizations?: LocalizationMap;
191
+ }
192
+ ```
193
+
194
+ `choices` items are `OptionChoice<V>`:
195
+
196
+ ```ts
197
+ interface OptionChoice<V extends string | number = string | number> {
198
+ name: string;
199
+ value: V;
200
+ nameLocalizations?: LocalizationMap;
201
+ }
202
+ ```
203
+
204
+ `autocomplete`:
205
+
206
+ ```ts
207
+ type AutocompleteHandler<V extends string | number> =
208
+ (ctx: AutocompleteContext) => Awaitable<OptionChoice<V>[]>;
209
+ ```
210
+
211
+ ### Option types
212
+
213
+ | Symbol | Description |
214
+ | ------ | ----------- |
215
+ | `interface OptionDef<TValue, TRequired>` | A described option (phantom-typed for inference). |
216
+ | `type AnyOptionDef` | `OptionDef<OptionValue, boolean>`. |
217
+ | `type OptionMap` | `Record<string, AnyOptionDef>`. |
218
+ | `type ResolvedOption<O>` | The handler value for one option. |
219
+ | `type ResolvedOptions<O>` | The handler's `options` object. |
220
+ | `type OptionValue` | Union of all possible resolved values. |
221
+ | `type AllowedChannelType` | Channel types valid for a channel option. |
222
+ | `function toAPIOption(name, def)` | Serialise one option to REST. |
223
+ | `function readOption(resolver, name, def)` | Read a resolved value (null → undefined). |
224
+ | `function optionsHaveAutocomplete(options)` | True if any option has autocomplete. |
225
+
226
+ ### `class AutocompleteContext`
227
+
228
+ | Member | Type | Description |
229
+ | ------ | ---- | ----------- |
230
+ | `interaction` | `AutocompleteInteraction` | Raw interaction. |
231
+ | `client` / `user` / `guild` / `guildId` | — | Convenience accessors. |
232
+ | `commandName` | `string` | Command being completed. |
233
+ | `focusedName` | `string` | Name of the focused option. |
234
+ | `value` | `string` | Current partial value typed by the user. |
235
+ | `respond(choices: OptionChoice[])` | `Promise<void>` | Send up to 25 suggestions. |
236
+
237
+ ---
238
+
239
+ ## Events
240
+
241
+ ### `function event(name, run): EventDef` / `function event(config): EventDef`
242
+
243
+ ```ts
244
+ type EventHandler<E extends keyof ClientEvents> = (...args: ClientEvents[E]) => Awaitable<void>;
245
+ interface EventConfig<E extends keyof ClientEvents> { name: E; once?: boolean; run: EventHandler<E>; }
246
+ interface EventDef { name: keyof ClientEvents; once: boolean; attach(client: Client): void; detach(client: Client): void; }
247
+ ```
248
+
249
+ Thrown errors and rejected promises are routed to the client's `error` event.
250
+
251
+ ### `class EventRegistry`
252
+
253
+ | Member | Type | Description |
254
+ | ------ | ---- | ----------- |
255
+ | `add(...defs: EventDef[])` | `this` | Register listeners. |
256
+ | `size` | `number` | Count. |
257
+ | `attachAll(client: Client)` | `void` | Attach every listener. |
258
+ | `detachAll(client: Client)` | `void` | Detach every listener. |
259
+
260
+ ---
261
+
262
+ ## Components
263
+
264
+ ### Builders
265
+
266
+ | Function | Returns | Notes |
267
+ | -------- | ------- | ----- |
268
+ | `button(config)` | `Button<P>` | Interactive button. |
269
+ | `linkButton(config)` | `ButtonBuilder` | URL button, no handler. |
270
+ | `stringSelect(config)` | `StringSelect<P>` | String select; takes `options`. |
271
+ | `userSelect(config)` | `UserSelect<P>` | User select. |
272
+ | `roleSelect(config)` | `RoleSelect<P>` | Role select. |
273
+ | `channelSelect(config)` | `ChannelSelect<P>` | Channel select; takes `channelTypes?`. |
274
+ | `mentionableSelect(config)` | `MentionableSelect<P>` | User + role select. |
275
+ | `modal(config)` | `Modal<P>` | Modal with `fields`. |
276
+ | `textInput(config)` | `TextInputDef` | A modal text-input field. |
277
+ | `row(...components)` | `ActionRowBuilder<C>` | Wrap components in a row. |
278
+
279
+ Each registrable component (`Button`, `StringSelect`, …, `Modal`) extends its
280
+ routing interface and adds `build(...args: BuildArgs<P>)`, which returns the
281
+ discord.js builder. `build` requires exactly the params declared in the id
282
+ pattern.
283
+
284
+ ```ts
285
+ interface ButtonConfig<P extends string, R> {
286
+ id: P; // pattern: "name" or "name:{param}"
287
+ label?: string;
288
+ style?: ButtonStyleInput; // "Primary" | "Secondary" | "Success" | "Danger" | ButtonStyle.*
289
+ emoji?: ComponentEmojiResolvable;
290
+ disabled?: boolean;
291
+ run: (ctx: ButtonContext<Params<P>>) => Awaitable<R>;
292
+ }
293
+
294
+ interface LinkButtonConfig { url: string; label?: string; emoji?: ComponentEmojiResolvable; disabled?: boolean; }
295
+
296
+ interface StringSelectConfig<P extends string, R> {
297
+ id: P;
298
+ options: readonly SelectMenuComponentOptionData[];
299
+ placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
300
+ run: (ctx: StringSelectContext<Params<P>>) => Awaitable<R>;
301
+ }
302
+
303
+ interface EntitySelectConfig<P extends string> {
304
+ id: P; placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
305
+ }
306
+ // user/role/mentionable selects take EntitySelectConfig & { run };
307
+ // channelSelect additionally takes { channelTypes?: readonly ChannelType[] }.
308
+
309
+ function textInput(config: {
310
+ label: string;
311
+ style?: TextInputStyleInput; // "Short" | "Paragraph" | TextInputStyle
312
+ placeholder?: string; required?: boolean; minLength?: number; maxLength?: number; value?: string;
313
+ }): TextInputDef;
314
+
315
+ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>, R> {
316
+ id: P;
317
+ title: string;
318
+ fields: F;
319
+ run: (ctx: ModalContext<Params<P>, keyof F & string>) => Awaitable<R>;
320
+ }
321
+ ```
322
+
323
+ ### Component contexts
324
+
325
+ | Class | Extra members |
326
+ | ----- | ------------- |
327
+ | `MessageComponentContext<P, I>` | `params`, `customId`, `message`, `update(input)`, `deferUpdate()`, `showModal(modal)` (+ BaseContext) |
328
+ | `ButtonContext<P>` | — |
329
+ | `StringSelectContext<P>` | `values: string[]`, `value: string \| undefined` |
330
+ | `UserSelectContext<P>` | `values`, `users`, `members` |
331
+ | `RoleSelectContext<P>` | `values`, `roles` |
332
+ | `ChannelSelectContext<P>` | `values`, `channels` |
333
+ | `MentionableSelectContext<P>` | `values`, `users`, `roles`, `members` |
334
+ | `ModalContext<P, F>` | `params`, `fields: Record<F, string>`, `customId` (+ BaseContext) |
335
+
336
+ ### `class ComponentRegistry`
337
+
338
+ | Member | Type | Description |
339
+ | ------ | ---- | ----------- |
340
+ | `add(...defs: ComponentDef[])` | `this` | Register components (override by namespace). |
341
+ | `onError(handler: ComponentErrorHandler)` | `this` | Set the error handler. |
342
+ | `size` | `number` | Count. |
343
+ | `handle(interaction: Interaction)` | `Promise<boolean>` | Route an interaction; `true` if matched. |
344
+
345
+ ```ts
346
+ type ComponentErrorHandler = (error: Error, interaction: RepliableInteraction) => Awaitable<void>;
347
+ type ComponentDef = ButtonRoute | StringSelectRoute | UserSelectRoute | RoleSelectRoute
348
+ | ChannelSelectRoute | MentionableSelectRoute | ModalRoute;
349
+ ```
350
+
351
+ ### Custom-id codec
352
+
353
+ | Symbol | Description |
354
+ | ------ | ----------- |
355
+ | `type ParamNames<S>` | Union of `{param}` names in a pattern. |
356
+ | `type Params<S>` | The params object a pattern resolves to. |
357
+ | `type BuildArgs<S>` | `build()` args (none when no params). |
358
+ | `const MAX_CUSTOM_ID_LENGTH` | `100`. |
359
+ | `function compilePattern(pattern)` | → `CompiledPattern { pattern, namespace, paramNames }`. |
360
+ | `function buildCustomId(compiled, params)` | Encode a concrete id. |
361
+ | `function parseCustomId(customId)` | → `ParsedCustomId { namespace, values }`. |
362
+ | `function paramsFromValues(paramNames, values)` | Map values onto names. |
363
+
364
+ ---
365
+
366
+ ## Contexts (shared)
367
+
368
+ ### `abstract class BaseContext<I>`
369
+
370
+ The base for every interaction context.
371
+
372
+ | Member | Type | Description |
373
+ | ------ | ---- | ----------- |
374
+ | `interaction` | `I` | Raw discord.js interaction. |
375
+ | `client` / `user` / `member` / `guild` / `guildId` / `channel` / `channelId` / `locale` | — | Accessors. |
376
+ | `deferred` / `replied` | `boolean` | Interaction state. |
377
+ | `reply(input)` | `Promise<InteractionResponse>` | Initial response. |
378
+ | `replyEphemeral(input)` | `Promise<InteractionResponse>` | Hidden reply. |
379
+ | `defer({ ephemeral? })` | `Promise<InteractionResponse>` | Acknowledge, respond later. |
380
+ | `editReply(input)` | `Promise<Message>` | Edit the response. |
381
+ | `followUp(input)` | `Promise<Message>` | Additional message. |
382
+ | `send(input)` | `Promise<void>` | State-aware reply/edit/followUp. |
383
+ | `error(message)` | `Promise<void>` | State-aware ephemeral error. |
384
+
385
+ ```ts
386
+ type ReplyData = InteractionReplyOptions & { ephemeral?: boolean };
387
+ type ReplyInput = string | ReplyData;
388
+ function normalizeReply(input: ReplyInput): InteractionReplyOptions;
389
+ function asEphemeral(input: ReplyInput): ReplyData;
390
+ ```
391
+
392
+ ---
393
+
394
+ ## Plugins
395
+
396
+ ```ts
397
+ interface SpearPlugin { name: string; setup(client: SpearClient): Awaitable<void>; }
398
+ function definePlugin(plugin: SpearPlugin): SpearPlugin;
399
+ ```
400
+
401
+ ---
402
+
403
+ ## Loading
404
+
405
+ ```ts
406
+ interface LoadOptions { extensions?: readonly string[]; recursive?: boolean; } // defaults: [.js,.mjs,.cjs], true
407
+ function collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>;
408
+ function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>;
409
+ ```
410
+
411
+ `SpearClient.load(dir, options?)` is the method form of `loadInto`.
412
+
413
+ ---
414
+
415
+ ## Added in 0.2
416
+
417
+ New subsystems, each with a dedicated guide. The `SpearClient` options
418
+ `{ logger?, dotenv?, cooldown?, prefix?, usage? }` configure them.
419
+
420
+ ### Logging — [guide](./logging.md)
421
+
422
+ ```ts
423
+ class Logger { debug/info/warn/error(message: string, options?: { error?: Error; data?: Record<string, LogValue> }): void; child(scope: string): Logger; setLevel(level: LogThreshold): this; enabled(level: LogLevel): boolean; }
424
+ type LogLevel = "debug" | "info" | "warn" | "error";
425
+ type LogThreshold = LogLevel | "silent";
426
+ function consoleSink(entry: LogEntry): void;
427
+ function toError(value: unknown): Error;
428
+ // client.logger is a Logger; new SpearClient({ logger: { level: "debug" } })
429
+ ```
430
+
431
+ ### Environment — [guide](./env.md)
432
+
433
+ ```ts
434
+ function parseEnv(content: string): Record<string, string>;
435
+ function loadEnv(options?: { path?: string; override?: boolean }): Record<string, string>;
436
+ const env: { string(k, fallback?); number(k, fallback?); boolean(k, fallback?); require(k): string };
437
+ // client auto-loads .env on start(); disable/configure via the dotenv option
438
+ ```
439
+
440
+ ### Cooldowns — [guide](./cooldown.md)
441
+
442
+ ```ts
443
+ interface CooldownConfig { duration: number; scope?: "user" | "guild" | "channel" | "global"; exempt?: { users?: string[]; roles?: string[] }; overrides?: { users?: Record<string, number>; roles?: Record<string, number> }; message?: string | ((remainingMs: number) => string); }
444
+ class CooldownManager { consume(bucket, input, actor, now?); peek(...); reset(...); clear(); }
445
+ // command({ cooldown: number | CooldownConfig }); new SpearClient({ cooldown }); client.cooldowns
446
+ ```
447
+
448
+ ### Scheduled tasks — [guide](./scheduler.md)
449
+
450
+ ```ts
451
+ function task(config: { name: string; cron?: string; interval?: number; runOnStart?: boolean; run: (client: SpearClient) => Awaitable<void> }): ScheduledTask;
452
+ function cron(expression: string): CronExpression; // .next(from?: Date): Date
453
+ class TaskScheduler { add/remove/list/size/active/start/stop }
454
+ // client.register(task(...)); client.schedule(config); client.scheduler
455
+ ```
456
+
457
+ ### Prefix commands — [guide](./prefix.md)
458
+
459
+ ```ts
460
+ function prefixCommand(config: { name: string; aliases?: string[]; description?: string; cooldown?: CooldownInput; run: (ctx: PrefixContext) => Awaitable<R> }): PrefixCommand;
461
+ class PrefixContext { message; commandName; args: string[]; rest: string; reply(content); send(content); }
462
+ // new SpearClient({ prefix: "!" | string[] | { prefix, mention?, ignoreBots?, caseInsensitive? } }); client.prefix
463
+ // reading others' content needs the privileged MessageContent intent (Intents.messages)
464
+ ```
465
+
466
+ ### Usage tracking — [guide](./usage.md)
467
+
468
+ ```ts
469
+ interface UsageEvent { type: "command" | "prefix" | "component" | "event"; name: string; userId?; userTag?; guildId?; channelId?; detail?; timestamp: Date; }
470
+ interface UsageStore { record(event): Awaitable<void>; all(): Awaitable<readonly UsageEvent[]>; }
471
+ class MemoryUsageStore { record; all; size; byUser(id); clear; }
472
+ class JsonFileUsageStore { constructor(path: string); record; all; }
473
+ class UsageTracker { setStore(store); reportTo(channelId, format?); track(event); store; enabled; }
474
+ // new SpearClient({ usage: { store?, channel?, format? } }); client.usage
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Added in 0.3
480
+
481
+ Driven by patterns repeated across long-running production bots: the role/
482
+ permission checks, `.catch(() => null)` fetches, embed factories, pagination
483
+ /confirm flows, mention/duration parsing, locks, config loaders and pluggable
484
+ log/usage transports a real Discord bot ends up writing.
485
+
486
+ ### Embeds — preset replies
487
+
488
+ ```ts
489
+ class Embeds { error(input); success(input); info(input); warn(input); build(level, input); }
490
+ function createEmbeds(opts?): Embeds; // alias for new Embeds(opts)
491
+ // SpearClient owns one as `client.embeds`; configure via the `embeds` option.
492
+ // BaseContext gains ctx.success/info/warn/error (state-aware send) + replySuccess/replyInfo/replyWarn/replyError.
493
+ ```
494
+
495
+ ### Guards — declarative preconditions
496
+
497
+ ```ts
498
+ type Guard<TCtx extends GuardContext = GuardContext> = (ctx: TCtx) => Awaitable<GuardResult>;
499
+ function denied(reason?: string): GuardResult;
500
+ function guildOnly(reason?: string): Guard;
501
+ function dmOnly(reason?: string): Guard;
502
+ function requireAnyRole(roleIds: readonly string[], reason?: string): Guard;
503
+ function requireAllRoles(roleIds: readonly string[], reason?: string): Guard;
504
+ function requireOwner(ownerIds: readonly string[], reason?: string): Guard;
505
+ function requireUserPermissions(permission: PermissionResolvable, reason?: string): Guard;
506
+ function requireBotPermissions(permission: PermissionResolvable, reason?: string): Guard;
507
+ function guard<TCtx>(predicate: Guard<TCtx>): Guard<TCtx>;
508
+ // per-handler: command({ guards: [...] }), prefixCommand({ guards }), button({ guards }), userCommand({ guards }), ...
509
+ // client-wide: new SpearClient({ guards: [...] })
510
+ ```
511
+
512
+ ### Context-menu commands
513
+
514
+ ```ts
515
+ function userCommand({ name, run: (ctx: UserContextMenuContext) => Awaitable<R>, guards?, cooldown? }): UserContextMenu;
516
+ function messageCommand({ name, run: (ctx: MessageContextMenuContext) => Awaitable<R>, guards?, cooldown? }): MessageContextMenu;
517
+ // SpearClient.deployAllCommands deploys slash + context menus in one PUT.
518
+ ```
519
+
520
+ ### Prefix typed arguments
521
+
522
+ ```ts
523
+ function prefixArgs(): PrefixArgsBuilder<{}>;
524
+ // builder methods: .string/.integer/.number/.boolean/.snowflake/.duration/.rest
525
+ // prefixCommand<TArgs>({ args: a => a.snowflake("target").duration("dur").rest("reason"), run: ctx => ctx.options }))
526
+ ```
527
+
528
+ ### Pagination + Confirmation
529
+
530
+ ```ts
531
+ function paginate<T>(interaction, items, { pageSize, render, user?, timeoutMs?, controls?, ephemeral? }): Promise<void>;
532
+ function buildPaginatorPage<T>(items, page, options): Promise<{ payload; pages }>;
533
+ function confirm(interaction, { title?, body, confirm?, cancel?, user?, timeoutMs?, ephemeral? }): Promise<{ confirmed, reason, interaction? }>;
534
+ ```
535
+
536
+ ### Primitives
537
+
538
+ ```ts
539
+ class KeyedLock { tryAcquire(key, ttl?); run(key, fn, { onBusy?, ttl? }); isHeld(key); forget(key); dispose(); }
540
+ const safeFetch = { member, channel, message, user, guild, role, try }; // each returns T | null
541
+ function withSafeTimeout<T>(p: Promise<T>, ms): Promise<T | null>;
542
+ function formatDuration(ms, { locale?: "en" | "tr" | UnitLabels; largest?; units? }): string;
543
+ function parseDuration(input: string): number | null;
544
+ function discordTimestamp(date, style?: "t"|"T"|"d"|"D"|"f"|"F"|"R"): string;
545
+ function relativeTimestamp(date): string;
546
+ interface CacheStore { get; set; delete; has; increment; rateLimit; clear; }
547
+ class MemoryCache implements CacheStore { /* TTL, counter, fixed-window rate limit */ }
548
+ function loadConfig<T>({ file, parser?, schema?, encoding? }): T;
549
+ function loadConfigAsync<T>(opts): Promise<T>;
550
+ function lookup<K, V>(table, resourceName?): (key: K) => V;
551
+ ```
552
+
553
+ ### Logger transports
554
+
555
+ ```ts
556
+ new Logger({ level, transports: [consoleSink, jsonlSink("./logs/bot.jsonl"), webhookSink({ url, minLevel: "error" })] });
557
+ function jsonlSink(path: string, { minLevel? }?): LogSink;
558
+ function webhookSink({ url, minLevel?, username? }): LogSink;
559
+ // Logger.addTransport(sink), setTransports([sinks])
560
+ ```
561
+
562
+ ### Scheduler — one-shot + reconcile
563
+
564
+ ```ts
565
+ client.scheduler.delay(name, ms, fn) -> { cancel(): boolean };
566
+ client.scheduler.followUp(name, [10_000, 30_000, 60_000], (i) => ...) -> { cancel(): boolean };
567
+ client.scheduler.reconcile("voice-sessions", async (client) => { /* once on ready */ });
568
+ ```
569
+
570
+ ### Deploy diff + dry run
571
+
572
+ ```ts
573
+ client.deployAllCommands({ guildId, dryRun: true }); // returns { skipped, body, reason: "dry-run" }
574
+ client.deployAllCommands({ guildId, strategy: "diff" }); // skips PUT when remote matches
575
+ client.deployAllCommands({ applicationId: "...", strategy: "diff" }); // explicit app id, no ready required
576
+ ```
577
+
578
+ ### Usage outcome + duration
579
+
580
+ ```ts
581
+ interface UsageEvent {
582
+ type; name; userId; userTag; guildId; channelId; detail?;
583
+ outcome?: "success" | "error";
584
+ durationMs?: number;
585
+ errorMessage?: string;
586
+ options?: Record<string, string | number | boolean | null>;
587
+ timestamp: Date;
588
+ }
589
+ ```