spearkit 0.1.0 → 0.3.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/dist/index.d.ts CHANGED
@@ -1,7 +1,413 @@
1
1
  import * as discord_js from 'discord.js';
2
- import { RepliableInteraction, InteractionReplyOptions, InteractionResponse, Message, AutocompleteInteraction, ChatInputCommandInteraction, JSONEncodable, APIModalInteractionResponseCallbackData, ModalComponentData, ModalBuilder, APIApplicationCommandChannelOption, CommandInteractionOption, Attachment, ApplicationCommandOptionType, LocalizationMap, Awaitable, APIApplicationCommandBasicOption, PermissionResolvable, RESTPostAPIChatInputApplicationCommandsJSONBody, RESTPostAPIApplicationCommandsJSONBody, REST, RESTPutAPIApplicationCommandsResult, RESTPutAPIApplicationGuildCommandsResult, ClientEvents, Client, ButtonInteraction, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, InteractionUpdateOptions, ModalSubmitInteraction, Interaction, ButtonBuilder, ButtonStyle, ComponentEmojiResolvable, ChannelSelectMenuBuilder, MentionableSelectMenuBuilder, TextInputStyle, RoleSelectMenuBuilder, StringSelectMenuBuilder, SelectMenuComponentOptionData, UserSelectMenuBuilder, ChannelType, MessageActionRowComponentBuilder, ActionRowBuilder, GatewayIntentBits, ClientOptions } from 'discord.js';
2
+ import { APIEmbedField, APIEmbedFooter, APIEmbedAuthor, EmbedBuilder, Client, Channel, Guild, GuildMember, MessageManager, Message, Role, User, APIInteractionGuildMember, Awaitable, PermissionResolvable, RepliableInteraction, InteractionReplyOptions, InteractionResponse, APIApplicationCommandChannelOption, CommandInteractionOption, Attachment, ApplicationCommandOptionType, LocalizationMap, ChatInputCommandInteraction, APIApplicationCommandBasicOption, AutocompleteInteraction, JSONEncodable, APIModalInteractionResponseCallbackData, ModalComponentData, ModalBuilder, RESTPostAPIChatInputApplicationCommandsJSONBody, RESTPostAPIApplicationCommandsJSONBody, REST, RESTPutAPIApplicationCommandsResult, RESTPutAPIApplicationGuildCommandsResult, ClientEvents, ButtonInteraction, ChannelSelectMenuInteraction, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, MentionableSelectMenuInteraction, ModalSubmitInteraction, Interaction, MessagePayload, MessageReplyOptions, MessageCreateOptions, GatewayIntentBits, ClientOptions, RESTPostAPIContextMenuApplicationCommandsJSONBody, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, BaseMessageOptions, InteractionUpdateOptions, ButtonBuilder, ButtonStyle, ComponentEmojiResolvable, ChannelSelectMenuBuilder, MentionableSelectMenuBuilder, TextInputStyle, RoleSelectMenuBuilder, StringSelectMenuBuilder, SelectMenuComponentOptionData, UserSelectMenuBuilder, ChannelType, MessageActionRowComponentBuilder, ActionRowBuilder } from 'discord.js';
3
3
  export * from 'discord.js';
4
4
 
5
+ /**
6
+ * Preset embeds — `error`, `success`, `info`, `warn` — with consistent colors
7
+ * and icons so every reply in your bot looks the same. The client owns one as
8
+ * `client.embeds` and the context preset methods (`ctx.success(...)` etc.) use
9
+ * it; you can also call `embeds.error(...)` directly to build an embed for
10
+ * `channel.send({ embeds: [...] })`.
11
+ */
12
+
13
+ /** Color in `0xRRGGBB` form for each preset level. */
14
+ interface EmbedColors {
15
+ error: number;
16
+ success: number;
17
+ info: number;
18
+ warn: number;
19
+ }
20
+ /** Icon glyph prepended to the description of each preset. Pass `""` to drop. */
21
+ interface EmbedIcons {
22
+ error: string;
23
+ success: string;
24
+ info: string;
25
+ warn: string;
26
+ }
27
+ /** Construction options for {@link Embeds}. Missing fields fall back to defaults. */
28
+ interface EmbedsOptions {
29
+ /** Per-level color overrides. */
30
+ colors?: Partial<EmbedColors>;
31
+ /** Per-level icon overrides; pass `""` to drop the prefix for a level. */
32
+ icons?: Partial<EmbedIcons>;
33
+ }
34
+ /** Shape accepted by every preset: a plain string or a structured body. */
35
+ type EmbedPresetInput = string | {
36
+ title?: string;
37
+ description?: string;
38
+ fields?: readonly APIEmbedField[];
39
+ footer?: APIEmbedFooter;
40
+ author?: APIEmbedAuthor;
41
+ url?: string;
42
+ timestamp?: Date | number | string;
43
+ thumbnail?: {
44
+ url: string;
45
+ };
46
+ image?: {
47
+ url: string;
48
+ };
49
+ };
50
+ /** One of the four built-in preset levels. */
51
+ type EmbedLevel = "error" | "success" | "info" | "warn";
52
+ /** Discord-ish defaults: red / green / blue / yellow + ⛔ ✅ ℹ️ ⚠️. */
53
+ declare const DEFAULT_EMBED_COLORS: EmbedColors;
54
+ /** Default icons: warning / check / info / triangle. */
55
+ declare const DEFAULT_EMBED_ICONS: EmbedIcons;
56
+ /**
57
+ * Builds preset embeds with consistent colors and icons.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const embeds = new Embeds({ colors: { success: 0x00ff88 } });
62
+ * await channel.send({ embeds: [embeds.success("Saved.")] });
63
+ * ```
64
+ */
65
+ declare class Embeds {
66
+ /** The resolved colors for every preset. */
67
+ readonly colors: EmbedColors;
68
+ /** The resolved icons for every preset. */
69
+ readonly icons: EmbedIcons;
70
+ constructor(options?: EmbedsOptions);
71
+ /** Red preset — something went wrong. */
72
+ error(input: EmbedPresetInput): EmbedBuilder;
73
+ /** Green preset — something succeeded. */
74
+ success(input: EmbedPresetInput): EmbedBuilder;
75
+ /** Blue preset — neutral information. */
76
+ info(input: EmbedPresetInput): EmbedBuilder;
77
+ /** Yellow preset — caution. */
78
+ warn(input: EmbedPresetInput): EmbedBuilder;
79
+ /** Build an embed at a chosen level. */
80
+ build(level: EmbedLevel, input: EmbedPresetInput): EmbedBuilder;
81
+ }
82
+ /** The shared default factory — used by contexts when the client has none. */
83
+ declare const defaultEmbeds: Embeds;
84
+
85
+ /**
86
+ * Keyed in-memory async lock (mutex) with TTL.
87
+ *
88
+ * Prevents two handlers from mutating the same resource concurrently — e.g.
89
+ * the same ticket id being claimed twice, or a user opening a ticket while
90
+ * another button click is still creating one. Hold leases are auto-expired so
91
+ * a forgotten `release()` cannot deadlock the process forever.
92
+ */
93
+ /** Construction options for {@link KeyedLock}. */
94
+ interface KeyedLockOptions {
95
+ /** Maximum lifetime (ms) of a held lock before it auto-expires. Default `60_000`. */
96
+ ttl?: number;
97
+ /** Sweep interval (ms) for expired-but-not-released locks. `0` disables sweeping. */
98
+ sweep?: number;
99
+ }
100
+ /** Release a previously-acquired lease. Idempotent — safe to call multiple times. */
101
+ type LockRelease = () => void;
102
+ /**
103
+ * Acquire, release and run-while-locked operations keyed on an arbitrary string.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const locks = new KeyedLock();
108
+ * const result = await locks.run(`ticket:${id}:claim`, async () => {
109
+ * // …mutate ticket atomically…
110
+ * return "ok";
111
+ * }, { onBusy: () => "busy" });
112
+ * ```
113
+ */
114
+ declare class KeyedLock {
115
+ private readonly entries;
116
+ private readonly defaultTtl;
117
+ private readonly sweepTimer?;
118
+ constructor(options?: KeyedLockOptions);
119
+ /** Try to acquire `key`. Returns a release function, or `null` if already held. */
120
+ tryAcquire(key: string, ttl?: number): LockRelease | null;
121
+ /** Whether `key` is currently held and not expired. */
122
+ isHeld(key: string): boolean;
123
+ /**
124
+ * Run `fn` while holding `key`. If the key is already held, calls `onBusy`
125
+ * (or returns `undefined`) without ever calling `fn`. Always releases on
126
+ * return or throw.
127
+ */
128
+ run<T>(key: string, fn: () => Promise<T> | T, options?: {
129
+ ttl?: number;
130
+ onBusy?: () => Promise<T> | T;
131
+ }): Promise<T | undefined>;
132
+ /** Number of currently-tracked leases (including expired-but-unswept). */
133
+ get size(): number;
134
+ /** Drop all known leases and stop the sweep timer. */
135
+ dispose(): void;
136
+ /** Manually remove a single key without running anything. */
137
+ forget(key: string): boolean;
138
+ private sweep;
139
+ }
140
+
141
+ /**
142
+ * Cache-first, timeout-bounded fetch helpers that resolve to `T | null` instead
143
+ * of throwing. They replace the `.catch(() => null)` pattern repeated 30+ times
144
+ * in production bots (member/channel/message/user/guild/role lookups that may
145
+ * 404, rate-limit, or block startup if awaited indefinitely).
146
+ */
147
+
148
+ /** Shared options for every safe-fetch helper. */
149
+ interface SafeFetchOptions {
150
+ /** Use the cache when present and not `force`. Default `true`. */
151
+ cache?: boolean;
152
+ /** Bypass the cache and force a REST hit. Default `false`. */
153
+ force?: boolean;
154
+ /** Resolve to `null` if Discord takes longer than this (ms). Default `5000`. */
155
+ timeoutMs?: number;
156
+ }
157
+ /** Resolve a guild member with a cache-hit fast path. Returns `null` on failure. */
158
+ declare function fetchMember(guild: Guild | null | undefined, userId: string | null | undefined, options?: SafeFetchOptions): Promise<GuildMember | null>;
159
+ /** Resolve a channel by id from the client. Returns `null` on failure. */
160
+ declare function fetchChannel(client: Client | null | undefined, channelId: string | null | undefined, options?: SafeFetchOptions): Promise<Channel | null>;
161
+ /** Resolve a message id in a given channel's messages manager. */
162
+ declare function fetchMessage(messages: MessageManager | null | undefined, messageId: string | null | undefined, options?: SafeFetchOptions): Promise<Message | null>;
163
+ /** Resolve a user by id from the client. Returns `null` on failure. */
164
+ declare function fetchUser(client: Client | null | undefined, userId: string | null | undefined, options?: SafeFetchOptions): Promise<User | null>;
165
+ /** Resolve a guild by id from the client. Returns `null` on failure. */
166
+ declare function fetchGuild(client: Client | null | undefined, guildId: string | null | undefined, options?: SafeFetchOptions): Promise<Guild | null>;
167
+ /** Resolve a role id from a guild's roles manager. Returns `null` on failure. */
168
+ declare function fetchRole(guild: Guild | null | undefined, roleId: string | null | undefined, options?: SafeFetchOptions): Promise<Role | null>;
169
+ /**
170
+ * Wrap an arbitrary best-effort operation so a failure resolves to `null`
171
+ * instead of throwing. Useful for sends/deletes whose outcome is non-critical.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * await safeTry(() => message.delete());
176
+ * ```
177
+ */
178
+ declare function safeTry<T>(op: () => Promise<T> | T): Promise<T | null>;
179
+ /** Time-bound an arbitrary promise; resolves to `null` on timeout or rejection. */
180
+ declare function withSafeTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | null>;
181
+ /** Cache-first, timeout-bounded fetch helpers grouped for ergonomic imports. */
182
+ declare const safeFetch: {
183
+ readonly member: typeof fetchMember;
184
+ readonly channel: typeof fetchChannel;
185
+ readonly message: typeof fetchMessage;
186
+ readonly user: typeof fetchUser;
187
+ readonly guild: typeof fetchGuild;
188
+ readonly role: typeof fetchRole;
189
+ readonly try: typeof safeTry;
190
+ };
191
+
192
+ /**
193
+ * Locale-aware duration formatter, duration parser and Discord timestamp
194
+ * helpers. Replaces the dozens of inline `Math.floor(s/86400)`/`X gün Y saat`
195
+ * and `<t:${secs}:R>` snippets in production bots, including the duplicate
196
+ * `formatTimeInTurkish`-style helpers duplicated across many bot command files.
197
+ */
198
+ /** Discord timestamp style: `t/T/d/D/f/F/R`. */
199
+ type DiscordTimestampStyle = "t" | "T" | "d" | "D" | "f" | "F" | "R";
200
+ type DurationUnit = "week" | "day" | "hour" | "minute" | "second";
201
+ interface UnitLabels {
202
+ week: [string, string];
203
+ day: [string, string];
204
+ hour: [string, string];
205
+ minute: [string, string];
206
+ second: [string, string];
207
+ separator: string;
208
+ zero: string;
209
+ }
210
+ /** Options for {@link formatDuration}. */
211
+ interface FormatDurationOptions {
212
+ /** Locale label set: `"en"`, `"en-US"`, `"tr"`, `"tr-TR"`, or a custom set. */
213
+ locale?: string | UnitLabels;
214
+ /** How many non-zero units to keep. Default `2`. Pass `Infinity` to keep all. */
215
+ largest?: number;
216
+ /** Subset of units to consider (in order: week → second). */
217
+ units?: readonly DurationUnit[];
218
+ }
219
+ /**
220
+ * Format a millisecond duration into human-readable text.
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * formatDuration(3_725_000); // "1 hour 2 minutes"
225
+ * formatDuration(3_725_000, { locale: "tr" }); // "1 saat 2 dakika"
226
+ * formatDuration(86_400_000 * 9, { largest: 3 }); // "1 week 2 days"
227
+ * ```
228
+ */
229
+ declare function formatDuration(ms: number, options?: FormatDurationOptions): string;
230
+ /**
231
+ * Parse a human duration like `"1h30m"`, `"2 days"`, `"1 saat 30 dakika"` or
232
+ * `"5000ms"` into milliseconds. Returns `null` if nothing parseable was found.
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * parseDuration("1h30m"); // 5_400_000
237
+ * parseDuration("1 saat"); // 3_600_000
238
+ * parseDuration("oops"); // null
239
+ * ```
240
+ */
241
+ declare function parseDuration(input: string): number | null;
242
+ /**
243
+ * Render a Discord-flavoured timestamp tag (`<t:1234:R>`).
244
+ *
245
+ * Styles: `t` short time, `T` long time, `d` short date, `D` long date,
246
+ * `f` short date/time (default), `F` long date/time, `R` relative.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * discordTimestamp(date); // <t:1234:f>
251
+ * discordTimestamp(date, "R"); // <t:1234:R>
252
+ * discordTimestamp(Date.now() + 60_000, "R"); // <t:..:R>
253
+ * ```
254
+ */
255
+ declare function discordTimestamp(date: Date | number, style?: DiscordTimestampStyle): string;
256
+ /** Short-hand for the relative Discord timestamp (`R` style). */
257
+ declare function relativeTimestamp(date: Date | number): string;
258
+
259
+ /**
260
+ * Pluggable cache abstraction with TTL, counters and rate-limit primitives.
261
+ *
262
+ * The {@link CacheStore} interface lets you swap a {@link MemoryCache} for any
263
+ * external backend (Redis, Memcached, your DB) without changing call sites —
264
+ * production bots commonly start with memory and graduate to Redis as load
265
+ * grows. Counters and fixed-window rate limits live on the same surface so the
266
+ * three patterns repeated in real bots (TTL cache, daily counter, per-user
267
+ * rate limit) share one API.
268
+ */
269
+ /** Result of a fixed-window {@link CacheStore.rateLimit} hit. */
270
+ interface RateLimitResult {
271
+ /** `true` if this hit was within the window's budget. */
272
+ allowed: boolean;
273
+ /** Remaining hits in the current window (`0` once `allowed` is false). */
274
+ remaining: number;
275
+ /** Epoch ms at which the current window resets. */
276
+ resetAt: number;
277
+ }
278
+ /** Options accepted by every write helper. */
279
+ interface CacheSetOptions {
280
+ /** Time-to-live in milliseconds. `undefined` means never expire. */
281
+ ttl?: number;
282
+ }
283
+ /** A swappable cache backend. All operations are async to allow remote stores. */
284
+ interface CacheStore {
285
+ /** Read a previously set value, or `undefined` if missing/expired. */
286
+ get<T = unknown>(key: string): Promise<T | undefined>;
287
+ /** Write a value, optionally with a TTL in ms. */
288
+ set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>;
289
+ /** Remove a key. Resolves to `true` if it existed. */
290
+ delete(key: string): Promise<boolean>;
291
+ /** Whether a non-expired key is present. */
292
+ has(key: string): Promise<boolean>;
293
+ /** Atomically increment a numeric counter. Returns the new value. */
294
+ increment(key: string, delta?: number, options?: CacheSetOptions): Promise<number>;
295
+ /** Fixed-window rate limit hit. Atomic per key. */
296
+ rateLimit(key: string, options: {
297
+ limit: number;
298
+ windowMs: number;
299
+ }): Promise<RateLimitResult>;
300
+ /** Drop every entry. */
301
+ clear(): Promise<void>;
302
+ }
303
+ /** In-memory implementation of {@link CacheStore}. Lazy TTL expiration. */
304
+ declare class MemoryCache implements CacheStore {
305
+ private readonly store;
306
+ /** Total number of stored (possibly expired) entries — primarily for tests. */
307
+ get size(): number;
308
+ get<T = unknown>(key: string): Promise<T | undefined>;
309
+ set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>;
310
+ delete(key: string): Promise<boolean>;
311
+ has(key: string): Promise<boolean>;
312
+ increment(key: string, delta?: number, options?: CacheSetOptions): Promise<number>;
313
+ rateLimit(key: string, options: {
314
+ limit: number;
315
+ windowMs: number;
316
+ }): Promise<RateLimitResult>;
317
+ clear(): Promise<void>;
318
+ }
319
+ /** Convenience factory: returns a default in-memory {@link CacheStore}. */
320
+ declare function createCache(): CacheStore;
321
+
322
+ /** Options accepted by {@link loadConfig} / {@link loadConfigAsync}. */
323
+ interface LoadConfigOptions<T> {
324
+ /** Absolute or cwd-relative path to the config file. */
325
+ file: string;
326
+ /** Custom parser. Default `JSON.parse`. Pass `JSON5.parse` (etc.) for other formats. */
327
+ parser?: (text: string) => unknown;
328
+ /** Validation function — receives the parsed value, returns the typed config. */
329
+ schema?: (value: unknown) => T;
330
+ /** File encoding. Default `"utf8"`. */
331
+ encoding?: BufferEncoding;
332
+ }
333
+ /** Synchronously read + parse + (optionally) validate a config file. */
334
+ declare function loadConfig<T = unknown>(options: LoadConfigOptions<T>): T;
335
+ /** Asynchronous variant of {@link loadConfig}. */
336
+ declare function loadConfigAsync<T = unknown>(options: LoadConfigOptions<T>): Promise<T>;
337
+ /**
338
+ * Build a typed lookup over a `Record<key, value>` table. Throws on missing
339
+ * keys so config typos surface at startup, not at use.
340
+ *
341
+ * @example
342
+ * ```ts
343
+ * const roles = lookup(config.roles, "role");
344
+ * const modId = roles("moderator"); // string; throws if "moderator" is absent
345
+ * ```
346
+ */
347
+ declare function lookup<K extends string, V>(table: Readonly<Record<K, V>>, resourceName?: string): (key: K) => V;
348
+ /** Build a non-throwing lookup that returns `undefined` for missing keys. */
349
+ declare function lookupOptional<K extends string, V>(table: Readonly<Record<K, V>>): (key: K) => V | undefined;
350
+
351
+ /**
352
+ * Declarative preconditions ("guards") that run before a command, component
353
+ * or prefix-command handler. Replaces the role/permission/guild-only/owner
354
+ * checks repeated 127+ times across production bots — `member.roles.cache.some(...)`,
355
+ * `member.permissions.has(...)`, `if (!message.guild) return`, ownership and
356
+ * target-hierarchy checks — with a composable predicate pipeline.
357
+ *
358
+ * Attach guards on a command (`command({ guards: [...] })`), on a component
359
+ * (`button({ guards: [...] })`), on a prefix command, or once on the client
360
+ * for everything (`new SpearClient({ guards: [...] })`). On denial, spearkit
361
+ * replies with an ephemeral error embed and skips the handler.
362
+ */
363
+
364
+ /**
365
+ * Minimal context a guard reads. Every spearkit handler (slash/prefix/component
366
+ * /modal) already exposes these — guards work uniformly across all of them.
367
+ */
368
+ interface GuardContext {
369
+ readonly client: Client;
370
+ readonly user: User;
371
+ readonly member: GuildMember | APIInteractionGuildMember | null;
372
+ readonly guild: Guild | null;
373
+ readonly guildId: string | null;
374
+ readonly channelId: string | null;
375
+ }
376
+ /** A guard's outcome. `true` = pass; `false`/`{ allowed: false, reason? }` = deny. */
377
+ type GuardResult = boolean | {
378
+ allowed: false;
379
+ reason?: string;
380
+ };
381
+ /** A precondition evaluated before a handler runs. */
382
+ type Guard<TCtx extends GuardContext = GuardContext> = (ctx: TCtx) => Awaitable<GuardResult>;
383
+ /** Sugar: build a denial result with an explanation. */
384
+ declare function denied(reason?: string): GuardResult;
385
+ /** The resolved outcome of running a list of guards. */
386
+ type RunGuardsResult = {
387
+ allowed: true;
388
+ } | {
389
+ allowed: false;
390
+ reason: string | undefined;
391
+ };
392
+ /** Run guards in order, short-circuiting on the first denial. */
393
+ declare function runGuards<TCtx extends GuardContext>(ctx: TCtx, guards: readonly Guard<TCtx>[] | undefined): Promise<RunGuardsResult>;
394
+ /** Require the interaction/message to come from inside a guild. */
395
+ declare function guildOnly(reason?: string): Guard;
396
+ /** Require the interaction/message to come from a DM. */
397
+ declare function dmOnly(reason?: string): Guard;
398
+ /** Require the invoking member to hold ANY of these role ids. */
399
+ declare function requireAnyRole(roleIds: readonly string[], reason?: string): Guard;
400
+ /** Require the invoking member to hold EVERY one of these role ids. */
401
+ declare function requireAllRoles(roleIds: readonly string[], reason?: string): Guard;
402
+ /** Require the invoking user to be one of `ownerIds` ("bot owners"). */
403
+ declare function requireOwner(ownerIds: readonly string[], reason?: string): Guard;
404
+ /** Require the invoking member to hold a Discord permission flag. */
405
+ declare function requireUserPermissions(permission: PermissionResolvable, reason?: string): Guard;
406
+ /** Require the BOT's own member to hold a Discord permission flag. */
407
+ declare function requireBotPermissions(permission: PermissionResolvable, reason?: string): Guard;
408
+ /** Inline custom predicate; sugar so a one-off check still types as a Guard. */
409
+ declare function guard<TCtx extends GuardContext = GuardContext>(predicate: Guard<TCtx>): Guard<TCtx>;
410
+
5
411
  /** Reply options with an ergonomic `ephemeral` shortcut (mapped to flags). */
6
412
  type ReplyData = InteractionReplyOptions & {
7
413
  ephemeral?: boolean;
@@ -50,45 +456,223 @@ declare abstract class BaseContext<I extends RepliableInteraction = RepliableInt
50
456
  * most handlers ever need.
51
457
  */
52
458
  send(input: ReplyInput): Promise<void>;
53
- /** State-aware ephemeral error message. */
54
- error(message: string): Promise<void>;
459
+ /** Get the configured {@link Embeds} factory — `client.embeds` or the default. */
460
+ protected getEmbeds(): Embeds;
461
+ /** State-aware send of a red error embed. Defaults to ephemeral. */
462
+ error(input: EmbedPresetInput, options?: {
463
+ ephemeral?: boolean;
464
+ }): Promise<void>;
465
+ /** State-aware send of a green success embed. */
466
+ success(input: EmbedPresetInput, options?: {
467
+ ephemeral?: boolean;
468
+ }): Promise<void>;
469
+ /** State-aware send of a blue info embed. */
470
+ info(input: EmbedPresetInput, options?: {
471
+ ephemeral?: boolean;
472
+ }): Promise<void>;
473
+ /** State-aware send of a yellow warn embed. */
474
+ warn(input: EmbedPresetInput, options?: {
475
+ ephemeral?: boolean;
476
+ }): Promise<void>;
477
+ /** Initial-reply variant of {@link error} (always `reply`, never `editReply`/`followUp`). */
478
+ replyError(input: EmbedPresetInput, options?: {
479
+ ephemeral?: boolean;
480
+ }): Promise<InteractionResponse<boolean>>;
481
+ /** Initial-reply variant of {@link success}. */
482
+ replySuccess(input: EmbedPresetInput, options?: {
483
+ ephemeral?: boolean;
484
+ }): Promise<InteractionResponse<boolean>>;
485
+ /** Initial-reply variant of {@link info}. */
486
+ replyInfo(input: EmbedPresetInput, options?: {
487
+ ephemeral?: boolean;
488
+ }): Promise<InteractionResponse<boolean>>;
489
+ /** Initial-reply variant of {@link warn}. */
490
+ replyWarn(input: EmbedPresetInput, options?: {
491
+ ephemeral?: boolean;
492
+ }): Promise<InteractionResponse<boolean>>;
493
+ private sendPreset;
494
+ private replyPreset;
55
495
  }
56
496
 
57
497
  /**
58
- * The handler argument for a slash command. Wraps the discord.js interaction
59
- * and exposes the resolved, fully-typed {@link options}.
498
+ * Rate-limit commands per user, per role, per guild, per channel or globally.
499
+ *
500
+ * A cooldown is described by a {@link CooldownConfig}: a base `duration`, the
501
+ * `scope` it is keyed on, an `exempt` set (users/roles that never wait) and
502
+ * per-user / per-role `overrides` (different durations for specific ids). Set a
503
+ * default on the client (applies to every command) and/or per command.
60
504
  */
61
- declare class CommandContext<O extends OptionMap = OptionMap> extends BaseContext<ChatInputCommandInteraction> {
62
- /** Resolved option values, typed from the command's `options` map. */
63
- readonly options: ResolvedOptions<O>;
64
- constructor(interaction: ChatInputCommandInteraction,
65
- /** Resolved option values, typed from the command's `options` map. */
66
- options: ResolvedOptions<O>);
67
- get commandName(): string;
68
- /** The invoked subcommand name, if any. */
69
- get subcommand(): string | null;
70
- /** Present a modal to the user in response to this command. */
71
- showModal(modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalComponentData | ModalBuilder): Promise<void>;
505
+ /** What a cooldown is bucketed against. Default `"user"`. */
506
+ type CooldownScope = "user" | "guild" | "channel" | "global";
507
+ /** Users and roles that bypass a cooldown entirely. */
508
+ interface CooldownExemptions {
509
+ /** User ids that never wait. */
510
+ users?: readonly string[];
511
+ /** Role ids whose members never wait. */
512
+ roles?: readonly string[];
513
+ }
514
+ /** Per-user and per-role duration overrides (milliseconds; `0` disables). */
515
+ interface CooldownOverrides {
516
+ /** `userId -> duration ms`. */
517
+ users?: Readonly<Record<string, number>>;
518
+ /** `roleId -> duration ms`. The most lenient matching role wins. */
519
+ roles?: Readonly<Record<string, number>>;
72
520
  }
521
+ /** Full cooldown description. */
522
+ interface CooldownConfig {
523
+ /** Base cooldown in milliseconds. */
524
+ duration: number;
525
+ /** What the cooldown is keyed on. Default `"user"`. */
526
+ scope?: CooldownScope;
527
+ /** Users/roles that bypass the cooldown. */
528
+ exempt?: CooldownExemptions;
529
+ /** Per-user / per-role duration overrides. */
530
+ overrides?: CooldownOverrides;
531
+ /** Message shown when blocked. A function receives the remaining ms. */
532
+ message?: string | ((remainingMs: number) => string);
533
+ }
534
+ /** A `CooldownConfig`, or a bare duration in milliseconds. */
535
+ type CooldownInput = number | CooldownConfig;
536
+ /** Normalise a {@link CooldownInput} to a full {@link CooldownConfig}. */
537
+ declare function normalizeCooldown(input: CooldownInput): CooldownConfig;
538
+ /** The actor a cooldown is evaluated for. */
539
+ interface CooldownActor {
540
+ userId: string;
541
+ roleIds: readonly string[];
542
+ guildId: string | null;
543
+ channelId: string | null;
544
+ }
545
+ /** Whether an action is allowed now, and if not, how long remains. */
546
+ type CooldownResult = {
547
+ allowed: true;
548
+ } | {
549
+ allowed: false;
550
+ remaining: number;
551
+ };
73
552
  /**
74
- * The handler argument for autocomplete requests. Provides the focused value
75
- * and a typed {@link respond} helper.
553
+ * Resolve the cooldown an actor should serve. `null` means exempt (no
554
+ * cooldown). Otherwise a duration in milliseconds (which may be `0`).
76
555
  */
77
- declare class AutocompleteContext {
78
- readonly interaction: AutocompleteInteraction;
79
- constructor(interaction: AutocompleteInteraction);
80
- get client(): discord_js.Client<true>;
81
- get user(): discord_js.User;
82
- get guild(): discord_js.Guild | null;
83
- get guildId(): string | null;
84
- get commandName(): string;
85
- /** Name of the option currently being completed. */
86
- get focusedName(): string;
87
- /** Current partial value typed by the user. */
88
- get value(): string;
89
- /** Send autocomplete suggestions (capped at the discord limit of 25). */
90
- respond(choices: OptionChoice<string | number>[]): Promise<void>;
556
+ declare function effectiveDuration(config: CooldownConfig, actor: CooldownActor): number | null;
557
+ /**
558
+ * Tracks last-use timestamps and decides whether an action is allowed.
559
+ * Stateful but dependency-free; one instance is shared on `client.cooldowns`.
560
+ */
561
+ declare class CooldownManager {
562
+ private readonly hits;
563
+ /** Number of tracked buckets. */
564
+ get size(): number;
565
+ /**
566
+ * Check whether `actor` may use `bucket`, recording the use when allowed.
567
+ * Exempt actors and non-positive durations are always allowed (no record).
568
+ */
569
+ consume(bucket: string, input: CooldownInput, actor: CooldownActor, now?: number): CooldownResult;
570
+ /** Like {@link consume} but never records — a read-only check. */
571
+ peek(bucket: string, input: CooldownInput, actor: CooldownActor, now?: number): CooldownResult;
572
+ /** Clear a single actor's cooldown for a bucket. Returns whether one existed. */
573
+ reset(bucket: string, actor: CooldownActor, scope?: CooldownScope): boolean;
574
+ /** Drop every tracked cooldown. */
575
+ clear(): void;
576
+ }
577
+ /** Build the user-facing message for a blocked action. */
578
+ declare function formatCooldownMessage(config: CooldownConfig, remainingMs: number): string;
579
+
580
+ /** Severity of a log entry, lowest to highest. */
581
+ type LogLevel = "debug" | "info" | "warn" | "error";
582
+ /** A minimum severity to emit, or `"silent"` to suppress everything. */
583
+ type LogThreshold = LogLevel | "silent";
584
+ /** A primitive metadata value attached to a log entry. */
585
+ type LogValue = string | number | boolean | bigint | null | undefined;
586
+ /** Extra context passed alongside a log message. */
587
+ interface LogOptions {
588
+ /** An error to attach; the default sink renders its stack. */
589
+ error?: Error;
590
+ /** Structured key/value metadata. */
591
+ data?: Record<string, LogValue>;
592
+ }
593
+ /** A fully-resolved record handed to a {@link LogSink}. */
594
+ interface LogEntry {
595
+ readonly level: LogLevel;
596
+ readonly message: string;
597
+ readonly scope?: string;
598
+ readonly timestamp: Date;
599
+ readonly error?: Error;
600
+ readonly data?: Readonly<Record<string, LogValue>>;
601
+ }
602
+ /** Receives every entry at or above the configured threshold. */
603
+ type LogSink = (entry: LogEntry) => void;
604
+ /** Construction options for a {@link Logger}. */
605
+ interface LoggerOptions {
606
+ /** Minimum level to emit. Default `"info"`. */
607
+ level?: LogThreshold;
608
+ /** Single transport — shorthand for `transports: [sink]`. */
609
+ sink?: LogSink;
610
+ /** Multiple transports. If set, takes precedence over `sink`. */
611
+ transports?: readonly LogSink[];
612
+ /** A scope prefix for every entry (e.g. `"commands"`). */
613
+ scope?: string;
614
+ }
615
+ /** Default sink: human-readable lines to the console (stderr for warn/error). */
616
+ declare function consoleSink(entry: LogEntry): void;
617
+ /**
618
+ * JSON-lines sink: appends one JSON object per entry to `path`. Fire-and-forget;
619
+ * filesystem errors are swallowed so logging never crashes the bot.
620
+ */
621
+ declare function jsonlSink(path: string, options?: {
622
+ minLevel?: LogLevel;
623
+ }): LogSink;
624
+ /**
625
+ * Discord-webhook sink: POSTs an embed to a webhook URL for entries at or
626
+ * above `minLevel` (default `"warn"`). Useful for sending errors to a private
627
+ * `#bot-errors` channel.
628
+ */
629
+ declare function webhookSink(options: {
630
+ url: string;
631
+ minLevel?: LogLevel;
632
+ username?: string;
633
+ }): LogSink;
634
+ /**
635
+ * A leveled, scoped logger. Create one directly or read `client.logger`.
636
+ * {@link child} loggers share the parent's threshold and transports, so calling
637
+ * {@link setLevel} on any of them affects the whole tree.
638
+ *
639
+ * @example
640
+ * ```ts
641
+ * const log = new Logger({ level: "debug", transports: [consoleSink, jsonlSink("./logs/bot.jsonl")] });
642
+ * log.info("ready", { data: { shard: 0 } });
643
+ * log.child("commands").error("handler failed", { error });
644
+ * ```
645
+ */
646
+ declare class Logger {
647
+ private state;
648
+ /** The scope prefix applied to every entry, if any. */
649
+ readonly scope?: string;
650
+ constructor(options?: LoggerOptions);
651
+ /** The current minimum threshold. */
652
+ get level(): LogThreshold;
653
+ /** Change the threshold for this logger and every child sharing its state. */
654
+ setLevel(level: LogThreshold): this;
655
+ /** Replace the transport list for this logger and every child sharing its state. */
656
+ setTransports(transports: readonly LogSink[]): this;
657
+ /** Append a transport to the existing list. */
658
+ addTransport(sink: LogSink): this;
659
+ /** Whether an entry of `level` would currently be emitted. */
660
+ enabled(level: LogLevel): boolean;
661
+ /** A child logger with an extra scope segment, sharing this logger's state. */
662
+ child(scope: string): Logger;
663
+ /** Emit an entry at an explicit level. */
664
+ log(level: LogLevel, message: string, options?: LogOptions): void;
665
+ /** Verbose diagnostics, off by default. */
666
+ debug(message: string, options?: LogOptions): void;
667
+ /** Normal operational messages. */
668
+ info(message: string, options?: LogOptions): void;
669
+ /** Recoverable problems worth attention. */
670
+ warn(message: string, options?: LogOptions): void;
671
+ /** Failures. Attach the cause via `{ error }`. */
672
+ error(message: string, options?: LogOptions): void;
91
673
  }
674
+ /** Coerce an unknown thrown value into an {@link Error}. */
675
+ declare function toError(value: unknown): Error;
92
676
 
93
677
  /**
94
678
  * Resolved runtime value types, derived directly from discord.js' option
@@ -202,6 +786,42 @@ declare function readOption(resolver: OptionReader, name: string, def: AnyOption
202
786
  /** True if any option in the map declares an autocomplete handler. */
203
787
  declare function optionsHaveAutocomplete(options: OptionMap): boolean;
204
788
 
789
+ /**
790
+ * The handler argument for a slash command. Wraps the discord.js interaction
791
+ * and exposes the resolved, fully-typed {@link options}.
792
+ */
793
+ declare class CommandContext<O extends OptionMap = OptionMap> extends BaseContext<ChatInputCommandInteraction> {
794
+ /** Resolved option values, typed from the command's `options` map. */
795
+ readonly options: ResolvedOptions<O>;
796
+ constructor(interaction: ChatInputCommandInteraction,
797
+ /** Resolved option values, typed from the command's `options` map. */
798
+ options: ResolvedOptions<O>);
799
+ get commandName(): string;
800
+ /** The invoked subcommand name, if any. */
801
+ get subcommand(): string | null;
802
+ /** Present a modal to the user in response to this command. */
803
+ showModal(modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalComponentData | ModalBuilder): Promise<void>;
804
+ }
805
+ /**
806
+ * The handler argument for autocomplete requests. Provides the focused value
807
+ * and a typed {@link respond} helper.
808
+ */
809
+ declare class AutocompleteContext {
810
+ readonly interaction: AutocompleteInteraction;
811
+ constructor(interaction: AutocompleteInteraction);
812
+ get client(): discord_js.Client<true>;
813
+ get user(): discord_js.User;
814
+ get guild(): discord_js.Guild | null;
815
+ get guildId(): string | null;
816
+ get commandName(): string;
817
+ /** Name of the option currently being completed. */
818
+ get focusedName(): string;
819
+ /** Current partial value typed by the user. */
820
+ get value(): string;
821
+ /** Send autocomplete suggestions (capped at the discord limit of 25). */
822
+ respond(choices: OptionChoice<string | number>[]): Promise<void>;
823
+ }
824
+
205
825
  /** Metadata shared by every kind of command. */
206
826
  interface CommonMeta {
207
827
  /** Permissions a member must have by default to see/use the command. */
@@ -212,6 +832,10 @@ interface CommonMeta {
212
832
  guildOnly?: boolean;
213
833
  nameLocalizations?: LocalizationMap;
214
834
  descriptionLocalizations?: LocalizationMap;
835
+ /** Rate-limit this command. A number is a duration in ms; see {@link CooldownConfig}. */
836
+ cooldown?: CooldownInput;
837
+ /** Preconditions evaluated before the handler runs. */
838
+ guards?: readonly Guard[];
215
839
  }
216
840
  /** Configuration for a leaf (non-subcommand) slash command. */
217
841
  interface CommandConfig<O extends OptionMap, R> extends CommonMeta {
@@ -264,6 +888,8 @@ interface SlashCommandSpec {
264
888
  hasAutocomplete: boolean;
265
889
  executor: (interaction: ChatInputCommandInteraction) => Promise<void>;
266
890
  autocompleter: (interaction: AutocompleteInteraction) => Promise<void>;
891
+ cooldown?: CooldownConfig;
892
+ guards?: readonly Guard[];
267
893
  }
268
894
  /**
269
895
  * A registered slash command. Serialises itself for the discord REST API and
@@ -278,6 +904,10 @@ declare class SlashCommand {
278
904
  private readonly json;
279
905
  private readonly executor;
280
906
  private readonly autocompleter;
907
+ /** Resolved cooldown configuration for this command, if any. */
908
+ readonly cooldown?: CooldownConfig;
909
+ /** Resolved guard list for this command, if any. */
910
+ readonly guards?: readonly Guard[];
281
911
  /** @internal */
282
912
  constructor(spec: SlashCommandSpec);
283
913
  /** Serialise to the discord REST chat-input command payload. */
@@ -327,6 +957,11 @@ type DeployResult = RESTPutAPIApplicationCommandsResult | RESTPutAPIApplicationG
327
957
  declare class CommandRegistry {
328
958
  private readonly commands;
329
959
  private errorHandler?;
960
+ private logger?;
961
+ private cooldowns?;
962
+ private defaultCooldown?;
963
+ private defaultGuards;
964
+ private onUsage?;
330
965
  /** Register one or more commands. Later registrations override by name. */
331
966
  add(...commands: SlashCommand[]): this;
332
967
  /** Remove a command by name. */
@@ -341,6 +976,14 @@ declare class CommandRegistry {
341
976
  get size(): number;
342
977
  /** Set the handler used when a command throws. */
343
978
  onError(handler: CommandErrorHandler): this;
979
+ /** Attach a logger used for dispatch tracing (debug level). */
980
+ setLogger(logger: Logger): this;
981
+ /** Wire a shared cooldown manager and an optional default cooldown for every command. */
982
+ setCooldowns(manager: CooldownManager, defaultCooldown?: CooldownConfig): this;
983
+ /** Guards that run before every command's own guards. */
984
+ setDefaultGuards(guards: readonly Guard[]): this;
985
+ /** Attach a hook called after each successful command execution. */
986
+ setUsageHook(hook: (event: UsageEvent) => void): this;
344
987
  /** Serialise every command to discord REST payloads. */
345
988
  toJSON(): RESTPostAPIApplicationCommandsJSONBody[];
346
989
  /** Dispatch an incoming chat-input interaction to its command. */
@@ -355,6 +998,8 @@ declare class CommandRegistry {
355
998
  */
356
999
  deploy(options: DeployOptions): Promise<DeployResult>;
357
1000
  private defaultErrorReply;
1001
+ private replyCooldown;
1002
+ private replyDenied;
358
1003
  }
359
1004
 
360
1005
  /** A typed handler for a discord.js client event. */
@@ -407,127 +1052,16 @@ declare class EventRegistry {
407
1052
  detachAll(client: Client): void;
408
1053
  }
409
1054
 
410
- /**
411
- * Typed custom-id codec.
412
- *
413
- * Patterns follow the grammar `namespace(:{param})*`, e.g. `"vote"` or
414
- * `"vote:{choice}"` or `"page:{id}:{dir}"`. The `namespace` is the routing key;
415
- * each `{param}` becomes a positional, percent-escaped value in the encoded id.
416
- * Param names are recovered at the type level so handlers get a typed `params`
417
- * object and `build()` requires exactly the right params.
418
- */
419
- /** Names of the `{param}` placeholders inside a pattern. */
420
- type ParamNames<S extends string> = S extends `${string}{${infer Name}}${infer Rest}` ? Name | ParamNames<Rest> : never;
421
- /** The params object a pattern resolves to (every value is a string). */
422
- type Params<S extends string> = {
423
- [K in ParamNames<S>]: string;
424
- };
425
- /** Arguments `build()` accepts: none when the pattern has no params. */
426
- type BuildArgs<S extends string> = [ParamNames<S>] extends [never] ? [] : [params: Params<S>];
427
- /** The discord custom-id length limit. */
428
- declare const MAX_CUSTOM_ID_LENGTH = 100;
429
- /** A compiled pattern: its routing namespace and ordered param names. */
430
- interface CompiledPattern {
431
- readonly pattern: string;
1055
+ /** Shared shape of every routed component. */
1056
+ interface RouteBase {
432
1057
  readonly namespace: string;
433
1058
  readonly paramNames: readonly string[];
1059
+ readonly guards?: readonly Guard[];
434
1060
  }
435
- /** Compile and validate a custom-id pattern. Throws on malformed input. */
436
- declare function compilePattern(pattern: string): CompiledPattern;
437
- /** Build a concrete custom-id from a compiled pattern and its params. */
438
- declare function buildCustomId(compiled: CompiledPattern, params: Readonly<Record<string, string>>): string;
439
- /** The namespace + raw values parsed out of an incoming custom-id. */
440
- interface ParsedCustomId {
441
- readonly namespace: string;
442
- readonly values: readonly string[];
443
- }
444
- /** Parse an incoming custom-id into its namespace and decoded values. */
445
- declare function parseCustomId(customId: string): ParsedCustomId;
446
- /** Map ordered values onto their param names. */
447
- declare function paramsFromValues(paramNames: readonly string[], values: readonly string[]): Record<string, string>;
448
-
449
- type UpdateInput = string | InteractionUpdateOptions;
450
- /** The concrete message-component interaction types (button + every select). */
451
- type AnyComponentInteraction = ButtonInteraction | StringSelectMenuInteraction | UserSelectMenuInteraction | RoleSelectMenuInteraction | ChannelSelectMenuInteraction | MentionableSelectMenuInteraction;
452
- /**
453
- * Base context for message-component interactions (buttons and selects).
454
- * Adds the component-only `update`/`deferUpdate`/`showModal` helpers and the
455
- * routed, typed {@link params}.
456
- */
457
- declare class MessageComponentContext<P, I extends AnyComponentInteraction = AnyComponentInteraction> extends BaseContext<I> {
458
- /** Params extracted from the custom-id pattern. */
459
- readonly params: P;
460
- constructor(interaction: I,
461
- /** Params extracted from the custom-id pattern. */
462
- params: P);
463
- /** The raw custom-id that triggered this interaction. */
464
- get customId(): string;
465
- /** The message the component lives on. */
466
- get message(): discord_js.Message<boolean>;
467
- /** Edit the message this component belongs to. */
468
- update(input: UpdateInput): Promise<void>;
469
- /** Acknowledge the interaction without editing the message yet. */
470
- deferUpdate(): Promise<void>;
471
- /** Open a modal in response to this component. */
472
- showModal(modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalComponentData | ModalBuilder): Promise<void>;
473
- }
474
- /** Context for a button click. */
475
- declare class ButtonContext<P = Record<string, never>> extends MessageComponentContext<P, ButtonInteraction> {
476
- }
477
- /** Context for a string select; exposes the chosen {@link values}. */
478
- declare class StringSelectContext<P = Record<string, never>> extends MessageComponentContext<P, StringSelectMenuInteraction> {
479
- /** All selected values. */
480
- get values(): string[];
481
- /** The first selected value, or `undefined` if none. */
482
- get value(): string | undefined;
483
- }
484
- /** Context for a user select; exposes selected ids, users and members. */
485
- declare class UserSelectContext<P = Record<string, never>> extends MessageComponentContext<P, UserSelectMenuInteraction> {
486
- get values(): string[];
487
- get users(): discord_js.Collection<string, discord_js.User>;
488
- get members(): discord_js.Collection<string, discord_js.GuildMember | discord_js.APIGuildMember>;
489
- }
490
- /** Context for a role select. */
491
- declare class RoleSelectContext<P = Record<string, never>> extends MessageComponentContext<P, RoleSelectMenuInteraction> {
492
- get values(): string[];
493
- get roles(): discord_js.Collection<string, discord_js.Role | discord_js.APIRole>;
494
- }
495
- /** Context for a channel select. */
496
- declare class ChannelSelectContext<P = Record<string, never>> extends MessageComponentContext<P, ChannelSelectMenuInteraction> {
497
- get values(): string[];
498
- get channels(): discord_js.Collection<string, discord_js.Channel | discord_js.APIChannel>;
499
- }
500
- /** Context for a mentionable (user + role) select. */
501
- declare class MentionableSelectContext<P = Record<string, never>> extends MessageComponentContext<P, MentionableSelectMenuInteraction> {
502
- get values(): string[];
503
- get users(): discord_js.Collection<string, discord_js.User>;
504
- get roles(): discord_js.Collection<string, discord_js.Role | discord_js.APIRole>;
505
- get members(): discord_js.Collection<string, discord_js.GuildMember | discord_js.APIGuildMember>;
506
- }
507
- /**
508
- * Context for a submitted modal. Exposes the routed {@link params} plus the
509
- * resolved text-input {@link fields}, keyed by the field names you declared.
510
- */
511
- declare class ModalContext<P, F extends string = string> extends BaseContext<ModalSubmitInteraction> {
512
- readonly params: P;
513
- /** Submitted values, keyed by the field names from your modal definition. */
514
- readonly fields: Record<F, string>;
515
- constructor(interaction: ModalSubmitInteraction, params: P,
516
- /** Submitted values, keyed by the field names from your modal definition. */
517
- fields: Record<F, string>);
518
- /** The raw custom-id that triggered this modal submission. */
519
- get customId(): string;
520
- }
521
-
522
- /** Shared shape of every routed component. */
523
- interface RouteBase {
524
- readonly namespace: string;
525
- readonly paramNames: readonly string[];
526
- }
527
- /** Routing entry for a button. */
528
- interface ButtonRoute extends RouteBase {
529
- readonly kind: "button";
530
- handle(interaction: ButtonInteraction, params: Record<string, string>): Promise<void>;
1061
+ /** Routing entry for a button. */
1062
+ interface ButtonRoute extends RouteBase {
1063
+ readonly kind: "button";
1064
+ handle(interaction: ButtonInteraction, params: Record<string, string>): Promise<void>;
531
1065
  }
532
1066
  /** Routing entry for a string select. */
533
1067
  interface StringSelectRoute extends RouteBase {
@@ -577,10 +1111,19 @@ declare class ComponentRegistry {
577
1111
  private readonly mentionableSelects;
578
1112
  private readonly modals;
579
1113
  private errorHandler?;
1114
+ private logger?;
1115
+ private onUsage?;
1116
+ private defaultGuards;
580
1117
  /** Register one or more components. Later registrations override by namespace. */
581
1118
  add(...defs: ComponentDef[]): this;
582
1119
  /** Set the handler used when a component throws. */
583
1120
  onError(handler: ComponentErrorHandler): this;
1121
+ /** Attach a logger used for dispatch tracing (debug level). */
1122
+ setLogger(logger: Logger): this;
1123
+ /** Attach a hook called after each successful component handler run. */
1124
+ setUsageHook(hook: (event: UsageEvent) => void): this;
1125
+ /** Guards that run before every component's own guards. */
1126
+ setDefaultGuards(guards: readonly Guard[]): this;
584
1127
  /** Total number of registered components. */
585
1128
  get size(): number;
586
1129
  /**
@@ -591,6 +1134,962 @@ declare class ComponentRegistry {
591
1134
  private exec;
592
1135
  }
593
1136
 
1137
+ /**
1138
+ * A spearkit plugin: a named, reusable bundle of commands, events and components.
1139
+ * Its {@link setup} runs once when added to a client via `client.use(plugin)`.
1140
+ */
1141
+ interface SpearPlugin {
1142
+ readonly name: string;
1143
+ setup(client: SpearClient): Awaitable<void>;
1144
+ }
1145
+ /** Identity helper that gives a plugin object its type and editor hints. */
1146
+ declare function definePlugin(plugin: SpearPlugin): SpearPlugin;
1147
+
1148
+ /** Options for the directory loader. */
1149
+ interface LoadOptions {
1150
+ /** File extensions to import. Default: `.js`, `.mjs`, `.cjs`. */
1151
+ extensions?: readonly string[];
1152
+ /** Recurse into subdirectories. Default: `true`. */
1153
+ recursive?: boolean;
1154
+ }
1155
+ /**
1156
+ * Recursively import a directory and collect every spearkit-registrable export
1157
+ * (commands, events, components) found in default or named exports.
1158
+ */
1159
+ declare function collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>;
1160
+ /**
1161
+ * Load a directory and register everything it exports into the client.
1162
+ * Returns the number of items registered.
1163
+ */
1164
+ declare function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>;
1165
+
1166
+ /** The flat key/value map parsed from a `.env` file. */
1167
+ type ParsedEnv = Record<string, string>;
1168
+ /** Options for {@link loadEnv}. */
1169
+ interface LoadEnvOptions {
1170
+ /** File to read. Default `.env` in the current working directory. */
1171
+ path?: string;
1172
+ /** Overwrite variables already present in `process.env`. Default `false`. */
1173
+ override?: boolean;
1174
+ }
1175
+ /** Parse `.env`-formatted text into a flat object. Does not touch `process.env`. */
1176
+ declare function parseEnv(content: string): ParsedEnv;
1177
+ /**
1178
+ * Read a `.env` file and merge it into `process.env`. Existing variables win
1179
+ * unless `override` is set. Missing files are ignored (returns `{}`), so it is
1180
+ * safe to call unconditionally.
1181
+ *
1182
+ * @returns the parsed key/value pairs from the file.
1183
+ */
1184
+ declare function loadEnv(options?: LoadEnvOptions): ParsedEnv;
1185
+ /** Typed, ergonomic reader over `process.env`. */
1186
+ interface EnvReader {
1187
+ /** A string value (empty strings count as missing), or `undefined`/`fallback`. */
1188
+ string(key: string): string | undefined;
1189
+ string(key: string, fallback: string): string;
1190
+ /** A numeric value, or `undefined`/`fallback` when missing or non-numeric. */
1191
+ number(key: string): number | undefined;
1192
+ number(key: string, fallback: number): number;
1193
+ /** A boolean (`true/1/yes/on` vs `false/0/no/off`), or `undefined`/`fallback`. */
1194
+ boolean(key: string): boolean | undefined;
1195
+ boolean(key: string, fallback: boolean): boolean;
1196
+ /** A string value, throwing if the variable is missing or empty. */
1197
+ require(key: string): string;
1198
+ }
1199
+ /**
1200
+ * Typed accessor over `process.env`.
1201
+ *
1202
+ * @example
1203
+ * ```ts
1204
+ * loadEnv();
1205
+ * const token = env.require("DISCORD_TOKEN");
1206
+ * const port = env.number("PORT", 3000);
1207
+ * const debug = env.boolean("DEBUG", false);
1208
+ * ```
1209
+ */
1210
+ declare const env: EnvReader;
1211
+
1212
+ /**
1213
+ * Scheduled tasks: run work on a cron schedule or a fixed interval.
1214
+ *
1215
+ * Dependency-free. Includes a standard 5-field cron parser (`*`, ranges,
1216
+ * lists, steps, `@daily` style aliases) evaluated in local time, plus a
1217
+ * {@link TaskScheduler} that the client starts on ready and stops on destroy.
1218
+ */
1219
+
1220
+ /**
1221
+ * A parsed cron expression. Evaluates in the host's local time.
1222
+ *
1223
+ * @example
1224
+ * ```ts
1225
+ * cron("*\u200b/5 * * * *").next(); // next 5-minute boundary
1226
+ * cron("@daily").next(new Date()); // next midnight
1227
+ * ```
1228
+ */
1229
+ declare class CronExpression {
1230
+ /** The original expression string. */
1231
+ readonly source: string;
1232
+ private readonly minutes;
1233
+ private readonly hours;
1234
+ private readonly daysOfMonth;
1235
+ private readonly months;
1236
+ private readonly daysOfWeek;
1237
+ private readonly domRestricted;
1238
+ private readonly dowRestricted;
1239
+ constructor(expression: string);
1240
+ private dayMatches;
1241
+ /** The next time strictly after `from` (default now) that matches. */
1242
+ next(from?: Date): Date;
1243
+ }
1244
+ /** Compile a cron expression. Throws on malformed input. */
1245
+ declare function cron(expression: string): CronExpression;
1246
+ /** Configuration for a scheduled task. Provide exactly one of `cron`/`interval`. */
1247
+ interface TaskConfig {
1248
+ /** Unique task name. */
1249
+ name: string;
1250
+ /** A cron expression (local time). */
1251
+ cron?: string;
1252
+ /** A fixed interval in milliseconds. */
1253
+ interval?: number;
1254
+ /** Also run once immediately when the scheduler starts. Default `false`. */
1255
+ runOnStart?: boolean;
1256
+ /** The work to perform. */
1257
+ run: (client: SpearClient) => Awaitable<void>;
1258
+ }
1259
+ /** A compiled, registrable scheduled task. Build it with {@link task}. */
1260
+ interface ScheduledTask {
1261
+ readonly kind: "task";
1262
+ readonly name: string;
1263
+ readonly interval?: number;
1264
+ readonly cron?: CronExpression;
1265
+ readonly runOnStart: boolean;
1266
+ readonly run: (client: SpearClient) => Awaitable<void>;
1267
+ }
1268
+ /** Define a scheduled task. Throws if neither `cron` nor `interval` is given. */
1269
+ declare function task(config: TaskConfig): ScheduledTask;
1270
+ /**
1271
+ * Runs {@link ScheduledTask}s. The client owns one as `client.scheduler`,
1272
+ * starts it on `clientReady` and stops it on `destroy`. Tasks added while
1273
+ * running are scheduled immediately.
1274
+ */
1275
+ declare class TaskScheduler {
1276
+ private readonly tasks;
1277
+ private readonly timers;
1278
+ private running;
1279
+ private client?;
1280
+ private logger?;
1281
+ private readonly reconcilers;
1282
+ /** Number of registered tasks. */
1283
+ get size(): number;
1284
+ /** Whether the scheduler is currently running. */
1285
+ get active(): boolean;
1286
+ /** Every registered task. */
1287
+ list(): ScheduledTask[];
1288
+ /** Attach a logger for task error reporting. */
1289
+ setLogger(logger: Logger): this;
1290
+ /** Register one or more tasks. If already running, they are scheduled now. */
1291
+ add(...tasks: ScheduledTask[]): this;
1292
+ /** Remove a task and cancel its timer. */
1293
+ remove(name: string): boolean;
1294
+ /**
1295
+ * Schedule a one-shot job: run `fn` once after `ms` milliseconds, then forget.
1296
+ * Returns a cancel handle. Replaces hand-rolled `setTimeout` calls for things
1297
+ * like "remind the moderator in 10 minutes if no claim happened".
1298
+ */
1299
+ delay(name: string, ms: number, fn: () => Awaitable<void>): {
1300
+ cancel: () => boolean;
1301
+ };
1302
+ /**
1303
+ * Schedule a series of follow-up fires from a single start point. Each
1304
+ * delay is measured from "now"; the callback receives the index of the
1305
+ * fire. Generalises the 10s/30s/60s retry pattern in real bots.
1306
+ */
1307
+ followUp(name: string, delays: readonly number[], fn: (index: number) => Awaitable<void>): {
1308
+ cancel: () => boolean;
1309
+ };
1310
+ /**
1311
+ * Register a once-on-ready reconciler — runs the first time the scheduler
1312
+ * starts (typically when the client becomes ready) and never again. Use
1313
+ * for restart-recovery work like closing orphaned voice sessions or
1314
+ * reapplying cached channel state.
1315
+ */
1316
+ reconcile(name: string, fn: (client: SpearClient) => Awaitable<void>): void;
1317
+ private runReconciler;
1318
+ /** Start every task. Safe to call once; later calls are ignored. */
1319
+ start(client: SpearClient): void;
1320
+ /** Stop the scheduler and cancel every pending timer. */
1321
+ stop(): void;
1322
+ private cancel;
1323
+ private begin;
1324
+ private delayFor;
1325
+ private scheduleNext;
1326
+ private arm;
1327
+ private runTask;
1328
+ }
1329
+
1330
+ type Resolved = {
1331
+ string: string;
1332
+ integer: number;
1333
+ number: number;
1334
+ boolean: boolean;
1335
+ snowflake: string;
1336
+ duration: number;
1337
+ rest: string;
1338
+ };
1339
+ /** A single argument's runtime spec; recorded by {@link PrefixArgsBuilder}. */
1340
+ interface PrefixArgSpec {
1341
+ readonly name: string;
1342
+ readonly kind: keyof Resolved;
1343
+ readonly required: boolean;
1344
+ readonly defaultValue?: string | number | boolean;
1345
+ }
1346
+ /** A failed parse, returned by {@link PrefixArgsParser.parse}. */
1347
+ interface PrefixArgError {
1348
+ readonly ok: false;
1349
+ readonly arg: string;
1350
+ readonly reason: string;
1351
+ }
1352
+ /** A successful parse, returned by {@link PrefixArgsParser.parse}. */
1353
+ interface PrefixArgsOk<T> {
1354
+ readonly ok: true;
1355
+ readonly values: T;
1356
+ }
1357
+ /** The compiled parser produced by {@link PrefixArgsBuilder.compile}. */
1358
+ interface PrefixArgsParser<T> {
1359
+ readonly specs: readonly PrefixArgSpec[];
1360
+ parse(tokens: readonly string[], rest: string): PrefixArgsOk<T> | PrefixArgError;
1361
+ }
1362
+ type AddField<TShape, K extends string, T, Req extends boolean> = Req extends true ? TShape & {
1363
+ [P in K]: T;
1364
+ } : TShape & {
1365
+ [P in K]?: T;
1366
+ };
1367
+ interface BaseOpts {
1368
+ /** Mark the arg required. Default `false`. */
1369
+ required?: boolean;
1370
+ }
1371
+ interface StringOpts extends BaseOpts {
1372
+ minLength?: number;
1373
+ maxLength?: number;
1374
+ /** Default value when the token is missing. Makes the arg effectively optional. */
1375
+ default?: string;
1376
+ }
1377
+ interface NumericOpts extends BaseOpts {
1378
+ minValue?: number;
1379
+ maxValue?: number;
1380
+ default?: number;
1381
+ }
1382
+ interface BooleanOpts extends BaseOpts {
1383
+ default?: boolean;
1384
+ }
1385
+ interface RestOpts extends BaseOpts {
1386
+ default?: string;
1387
+ }
1388
+ /**
1389
+ * Build a typed argument schema for {@link prefixCommand}. Chain calls
1390
+ * positionally — first token → first arg, second → second arg, etc.
1391
+ */
1392
+ declare class PrefixArgsBuilder<TShape extends Record<string, unknown> = {}> {
1393
+ private readonly specs;
1394
+ /** @internal */
1395
+ constructor(specs?: readonly PrefixArgSpec[]);
1396
+ /** A raw string token. */
1397
+ string<K extends string, Req extends boolean = false>(name: K, options?: StringOpts & {
1398
+ required?: Req;
1399
+ }): PrefixArgsBuilder<AddField<TShape, K, string, Req>>;
1400
+ /** A whole integer. */
1401
+ integer<K extends string, Req extends boolean = false>(name: K, options?: NumericOpts & {
1402
+ required?: Req;
1403
+ }): PrefixArgsBuilder<AddField<TShape, K, number, Req>>;
1404
+ /** A floating-point number. */
1405
+ number<K extends string, Req extends boolean = false>(name: K, options?: NumericOpts & {
1406
+ required?: Req;
1407
+ }): PrefixArgsBuilder<AddField<TShape, K, number, Req>>;
1408
+ /** A boolean (`true`/`yes`/`1`/`on` vs `false`/`no`/`0`/`off`). */
1409
+ boolean<K extends string, Req extends boolean = false>(name: K, options?: BooleanOpts & {
1410
+ required?: Req;
1411
+ }): PrefixArgsBuilder<AddField<TShape, K, boolean, Req>>;
1412
+ /** A Discord snowflake id — accepts raw ids and `<@u>` / `<#c>` / `<@&r>` mentions. */
1413
+ snowflake<K extends string, Req extends boolean = false>(name: K, options?: BaseOpts & {
1414
+ required?: Req;
1415
+ default?: string;
1416
+ }): PrefixArgsBuilder<AddField<TShape, K, string, Req>>;
1417
+ /** A duration like `"1h30m"` or `"1 saat"` parsed to milliseconds. */
1418
+ duration<K extends string, Req extends boolean = false>(name: K, options?: BaseOpts & {
1419
+ required?: Req;
1420
+ default?: number;
1421
+ }): PrefixArgsBuilder<AddField<TShape, K, number, Req>>;
1422
+ /** The remainder of the message (everything after previous args). */
1423
+ rest<K extends string, Req extends boolean = false>(name: K, options?: RestOpts & {
1424
+ required?: Req;
1425
+ }): PrefixArgsBuilder<AddField<TShape, K, string, Req>>;
1426
+ private push;
1427
+ /** Compile this builder into a parser. */
1428
+ compile(): PrefixArgsParser<TShape>;
1429
+ }
1430
+ /** Start a fresh args builder. Pass to `prefixCommand({ args })`. */
1431
+ declare function prefixArgs(): PrefixArgsBuilder<{}>;
1432
+
1433
+ /** Options controlling how prefix messages are recognised. */
1434
+ interface PrefixOptions {
1435
+ /** One or more command prefixes (e.g. `"!"` or `["!", "?"]`). */
1436
+ prefix?: string | readonly string[];
1437
+ /** Also accept a leading bot mention as a prefix. Default `true`. */
1438
+ mention?: boolean;
1439
+ /** Ignore messages authored by bots. Default `true`. */
1440
+ ignoreBots?: boolean;
1441
+ /** Match command names case-insensitively. Default `true`. */
1442
+ caseInsensitive?: boolean;
1443
+ }
1444
+ /** Configuration for a prefix command. */
1445
+ interface PrefixCommandConfig<TArgs extends Record<string, unknown> = Record<string, never>, R = void> {
1446
+ /** Primary command name (the word after the prefix). */
1447
+ name: string;
1448
+ /** Alternative names that also trigger the command. */
1449
+ aliases?: readonly string[];
1450
+ /** Human description (for your own help command). */
1451
+ description?: string;
1452
+ /** Rate-limit this command. A number is a duration in ms. */
1453
+ cooldown?: CooldownInput;
1454
+ /** Preconditions evaluated before the handler runs. */
1455
+ guards?: readonly Guard[];
1456
+ /** Typed argument schema; `ctx.options` will be shaped from this. */
1457
+ args?: (builder: PrefixArgsBuilder<{}>) => PrefixArgsBuilder<TArgs>;
1458
+ /** Handler invoked with a {@link PrefixContext} typed by `args`. */
1459
+ run: (ctx: PrefixContext<TArgs>) => Awaitable<R>;
1460
+ }
1461
+ /** A registrable prefix command. Build it with {@link prefixCommand}. */
1462
+ interface PrefixCommand {
1463
+ readonly kind: "prefixCommand";
1464
+ readonly name: string;
1465
+ readonly aliases: readonly string[];
1466
+ readonly description?: string;
1467
+ readonly cooldown?: CooldownConfig;
1468
+ readonly guards?: readonly Guard[];
1469
+ readonly parser?: PrefixArgsParser<Record<string, unknown>>;
1470
+ readonly run: (ctx: PrefixContext) => Promise<void>;
1471
+ }
1472
+ /** Define a prefix command. */
1473
+ declare function prefixCommand<TArgs extends Record<string, unknown> = Record<string, never>, R = void>(config: PrefixCommandConfig<TArgs, R>): PrefixCommand;
1474
+ /** The handler argument for a prefix command: the message plus parsed args. */
1475
+ declare class PrefixContext<TArgs extends Record<string, unknown> = Record<string, never>> {
1476
+ /** The triggering message. */
1477
+ readonly message: Message;
1478
+ /** The matched command name (as typed). */
1479
+ readonly commandName: string;
1480
+ /** Whitespace-split arguments after the command name. */
1481
+ readonly args: string[];
1482
+ /** The raw text after the command name. */
1483
+ readonly rest: string;
1484
+ /** Typed parsed arguments from `args` schema, or `{}` if none. */
1485
+ readonly options: TArgs;
1486
+ constructor(
1487
+ /** The triggering message. */
1488
+ message: Message,
1489
+ /** The matched command name (as typed). */
1490
+ commandName: string,
1491
+ /** Whitespace-split arguments after the command name. */
1492
+ args: string[],
1493
+ /** The raw text after the command name. */
1494
+ rest: string,
1495
+ /** Typed parsed arguments from `args` schema, or `{}` if none. */
1496
+ options?: TArgs);
1497
+ get client(): Message["client"];
1498
+ get author(): discord_js.User;
1499
+ get member(): discord_js.GuildMember | null;
1500
+ get guild(): discord_js.Guild | null;
1501
+ get guildId(): string | null;
1502
+ get channel(): discord_js.DMChannel | discord_js.PrivateThreadChannel | discord_js.PartialDMChannel | discord_js.PartialGroupDMChannel | discord_js.NewsChannel | discord_js.StageChannel | discord_js.TextChannel | discord_js.PublicThreadChannel<boolean> | discord_js.VoiceChannel;
1503
+ get channelId(): string;
1504
+ /** Reply to the triggering message. */
1505
+ reply(content: string | MessagePayload | MessageReplyOptions): Promise<Message>;
1506
+ /** Send a message to the same channel (no reply reference). */
1507
+ send(content: string | MessagePayload | MessageCreateOptions): Promise<Message | undefined>;
1508
+ }
1509
+ /** Error hook invoked when a prefix command handler throws. */
1510
+ type PrefixErrorHandler = (error: Error, message: Message) => Awaitable<void>;
1511
+ /** Holds prefix commands and dispatches matching messages to them. */
1512
+ declare class PrefixRegistry {
1513
+ private readonly commands;
1514
+ private readonly lookup;
1515
+ private options;
1516
+ private logger?;
1517
+ private cooldowns?;
1518
+ private defaultCooldown?;
1519
+ private errorHandler?;
1520
+ private defaultGuards;
1521
+ private onUsage?;
1522
+ /** Configure prefixes and matching behaviour. */
1523
+ setOptions(input: string | readonly string[] | PrefixOptions): this;
1524
+ /** Attach a logger for dispatch tracing and error reporting. */
1525
+ setLogger(logger: Logger): this;
1526
+ /** Attach a hook called after each successful prefix command run. */
1527
+ setUsageHook(hook: (event: UsageEvent) => void): this;
1528
+ /** Share a cooldown manager and an optional default cooldown. */
1529
+ setCooldowns(manager: CooldownManager, defaultCooldown?: CooldownConfig): this;
1530
+ /** Guards that run before every prefix command's own guards. */
1531
+ setDefaultGuards(guards: readonly Guard[]): this;
1532
+ /** Set the handler used when a prefix command throws. */
1533
+ onError(handler: PrefixErrorHandler): this;
1534
+ /** Register one or more prefix commands (and their aliases). */
1535
+ add(...commands: PrefixCommand[]): this;
1536
+ private index;
1537
+ /** Look up a command by name or alias. */
1538
+ get(nameOrAlias: string): PrefixCommand | undefined;
1539
+ /** Number of registered commands (excluding aliases). */
1540
+ get size(): number;
1541
+ /** Every registered command. */
1542
+ list(): PrefixCommand[];
1543
+ /** Strip a matching prefix (or bot mention) from `content`, or return `null`. */
1544
+ private stripPrefix;
1545
+ /**
1546
+ * Parse and dispatch a message. Returns `true` when a command ran (or was
1547
+ * blocked by a cooldown), `false` when the message was not a prefix command.
1548
+ */
1549
+ handle(message: Message): Promise<boolean>;
1550
+ }
1551
+
1552
+ /** Anything that can be handed to {@link SpearClient.register}. */
1553
+ type Registerable = SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand | ContextMenuCommand;
1554
+ /**
1555
+ * Ready-made intent presets. Pass one to {@link SpearClient} as `intents`.
1556
+ * `all` includes privileged intents — enable them in the developer portal.
1557
+ */
1558
+ declare const Intents: {
1559
+ /** No intents. */
1560
+ readonly none: GatewayIntentBits[];
1561
+ /** Just `Guilds` — enough for slash commands and interactions. */
1562
+ readonly default: readonly [GatewayIntentBits.Guilds];
1563
+ /** Guild + member gateway data. */
1564
+ readonly guilds: readonly [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers];
1565
+ /** Read message content (privileged) alongside guild messages. */
1566
+ readonly messages: readonly [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent];
1567
+ /** Every intent, including privileged ones. */
1568
+ readonly all: GatewayIntentBits[];
1569
+ };
1570
+ /** spearkit-specific client options layered on top of discord.js {@link ClientOptions}. */
1571
+ interface SpearOptions {
1572
+ /** A {@link Logger} instance, or options to build one. Exposed as `client.logger`. */
1573
+ logger?: Logger | LoggerOptions;
1574
+ /**
1575
+ * Auto-load a `.env` file into `process.env` on {@link SpearClient.start}.
1576
+ * `true` (default) loads `.env` from the cwd; pass {@link LoadEnvOptions} for
1577
+ * a custom path or override behaviour, or `false` to disable.
1578
+ */
1579
+ dotenv?: boolean | LoadEnvOptions;
1580
+ /** A default cooldown applied to every command. A command's own cooldown overrides it. */
1581
+ cooldown?: CooldownInput;
1582
+ /** Enable prefix (text) commands. A string/array sets prefixes; an object configures matching. */
1583
+ prefix?: string | readonly string[] | PrefixOptions;
1584
+ /** Track command/component/prefix usage to a store and/or a Discord channel. */
1585
+ usage?: UsageOptions;
1586
+ /** Default {@link Embeds} factory for preset replies. Pass an instance or options. */
1587
+ embeds?: Embeds | EmbedsOptions;
1588
+ /** Default guards (preconditions) applied before every command/component/prefix handler. */
1589
+ guards?: readonly Guard[];
1590
+ }
1591
+ /** Options for {@link SpearClient}: discord.js options plus {@link SpearOptions}. `intents` may be omitted. */
1592
+ type SpearClientOptions = Partial<ClientOptions> & SpearOptions;
1593
+ /**
1594
+ * A discord.js {@link Client} with batteries included: command, event,
1595
+ * component, prefix-command, scheduler and context-menu registries plus
1596
+ * interaction routing wired up automatically.
1597
+ *
1598
+ * @example
1599
+ * ```ts
1600
+ * const client = new SpearClient({ intents: Intents.default });
1601
+ * client.register(ping, onReady, voteButton);
1602
+ * await client.start(process.env.TOKEN);
1603
+ * await client.deployAllCommands({ guildId: "123" });
1604
+ * ```
1605
+ */
1606
+ declare class SpearClient extends Client {
1607
+ /** Slash command registry and dispatcher. */
1608
+ readonly commands: CommandRegistry;
1609
+ /** Event listener registry. */
1610
+ readonly events: EventRegistry;
1611
+ /** Button / select / modal registry and router. */
1612
+ readonly components: ComponentRegistry;
1613
+ /** Structured logger shared across spearkit and available to your code. */
1614
+ readonly logger: Logger;
1615
+ /** Shared cooldown manager used by command dispatch; also usable directly. */
1616
+ readonly cooldowns: CooldownManager;
1617
+ /** Cron/interval task scheduler; started on ready and stopped on destroy. */
1618
+ readonly scheduler: TaskScheduler;
1619
+ /** Prefix (text) command registry, dispatched from `messageCreate`. */
1620
+ readonly prefix: PrefixRegistry;
1621
+ /** Usage tracker: records who used what to a store and/or a Discord channel. */
1622
+ readonly usage: UsageTracker;
1623
+ /** Preset embed factory used by `ctx.error/success/info/warn` and available to your code. */
1624
+ readonly embeds: Embeds;
1625
+ /** User- and message-context-menu command registry. */
1626
+ readonly contextMenus: ContextMenuRegistry;
1627
+ private readonly envConfig;
1628
+ constructor(options?: SpearClientOptions);
1629
+ /**
1630
+ * Register commands, events, components, scheduled tasks, prefix commands
1631
+ * and context menus in one call. Each item is routed to its matching registry.
1632
+ */
1633
+ register(...items: Registerable[]): this;
1634
+ /** Install one or more plugins, running each plugin's `setup`. */
1635
+ use(...plugins: SpearPlugin[]): Promise<this>;
1636
+ /**
1637
+ * Recursively load a directory and register every command, event and
1638
+ * component it exports. Returns the number of items registered.
1639
+ */
1640
+ load(dir: string, options?: LoadOptions): Promise<number>;
1641
+ /**
1642
+ * Log in. Falls back to the `DISCORD_TOKEN` environment variable when no
1643
+ * token is passed.
1644
+ */
1645
+ start(token?: string): Promise<this>;
1646
+ /**
1647
+ * Push the registered slash commands to Discord using the client's REST
1648
+ * connection. Slash-only — use {@link deployAllCommands} to include context
1649
+ * menus in the same request.
1650
+ */
1651
+ deployCommands(options?: {
1652
+ guildId?: string;
1653
+ }): Promise<DeployResult>;
1654
+ /**
1655
+ * Deploy slash commands AND context menus together to Discord in a single
1656
+ * PUT. Use this once you mix `userCommand` / `messageCommand` with `command`.
1657
+ */
1658
+ /**
1659
+ * Deploy slash commands AND context menus together. Supports a `diff`
1660
+ * strategy that fetches the remote set first and skips the PUT when
1661
+ * nothing changed, and a `dryRun` flag that returns the body without
1662
+ * touching Discord (perfect for CI deploy gates).
1663
+ *
1664
+ * @returns the API's DeployResult on PUT, or a skipped report when
1665
+ * `dryRun: true` is set OR `strategy: "diff"` finds no changes.
1666
+ */
1667
+ deployAllCommands(options?: {
1668
+ guildId?: string;
1669
+ dryRun?: boolean;
1670
+ strategy?: "always" | "diff";
1671
+ /** Override the application id (default reads from the ready client). */
1672
+ applicationId?: string;
1673
+ }): Promise<DeployResult | {
1674
+ skipped: true;
1675
+ reason: "dry-run" | "no-changes";
1676
+ body: readonly unknown[];
1677
+ }>;
1678
+ /** Define and register a scheduled task in one call. */
1679
+ schedule(config: TaskConfig): ScheduledTask;
1680
+ /** Stop the scheduler, then tear down the discord.js client. */
1681
+ destroy(): Promise<void>;
1682
+ private route;
1683
+ }
1684
+
1685
+ /** What kind of interaction was used. */
1686
+ type UsageType = "command" | "prefix" | "component" | "event";
1687
+ /** Outcome of a tracked use — `"success"` if the handler returned without throwing. */
1688
+ type UsageOutcome = "success" | "error";
1689
+ /** A primitive metadata value attached to a usage event. */
1690
+ type UsageMetaValue = string | number | boolean | null;
1691
+ /** A single recorded use. */
1692
+ interface UsageEvent {
1693
+ readonly type: UsageType;
1694
+ /** Command/component name (or event name). */
1695
+ readonly name: string;
1696
+ readonly userId?: string;
1697
+ readonly userTag?: string;
1698
+ readonly guildId?: string | null;
1699
+ readonly channelId?: string | null;
1700
+ /** Free-form extra detail. */
1701
+ readonly detail?: string;
1702
+ readonly timestamp: Date;
1703
+ /** Outcome of the handler — `"success"` or `"error"`. */
1704
+ readonly outcome?: UsageOutcome;
1705
+ /** Wall-clock duration of the handler in milliseconds. */
1706
+ readonly durationMs?: number;
1707
+ /** Snapshot of the typed options the handler ran with. */
1708
+ readonly options?: Readonly<Record<string, UsageMetaValue>>;
1709
+ /** Error message if `outcome === "error"`. */
1710
+ readonly errorMessage?: string;
1711
+ }
1712
+ /** A pluggable persistence backend for {@link UsageEvent}s. */
1713
+ interface UsageStore {
1714
+ /** Persist one event. */
1715
+ record(event: UsageEvent): Awaitable<void>;
1716
+ /** Read every persisted event. */
1717
+ all(): Awaitable<readonly UsageEvent[]>;
1718
+ }
1719
+ /** In-memory store; great for tests and dashboards. Optionally capped. */
1720
+ declare class MemoryUsageStore implements UsageStore {
1721
+ private readonly limit;
1722
+ private readonly events;
1723
+ constructor(limit?: number);
1724
+ record(event: UsageEvent): void;
1725
+ all(): readonly UsageEvent[];
1726
+ /** Total recorded events. */
1727
+ get size(): number;
1728
+ /** Events recorded for a given user id. */
1729
+ byUser(userId: string): UsageEvent[];
1730
+ /** Forget everything. */
1731
+ clear(): void;
1732
+ }
1733
+ /**
1734
+ * File-backed store using newline-delimited JSON (`.jsonl`). Appends one line
1735
+ * per event — durable, human-inspectable, and dependency-free.
1736
+ */
1737
+ declare class JsonFileUsageStore implements UsageStore {
1738
+ private readonly path;
1739
+ constructor(path: string);
1740
+ record(event: UsageEvent): Promise<void>;
1741
+ all(): Promise<readonly UsageEvent[]>;
1742
+ }
1743
+ /** Default one-line rendering of a usage event for a Discord channel. */
1744
+ declare function formatUsage(event: UsageEvent): string;
1745
+ /** Client-level usage configuration (the `usage` option). */
1746
+ interface UsageOptions {
1747
+ /** Persist events to this store (a database). */
1748
+ store?: UsageStore;
1749
+ /** Mirror events into this Discord channel id. */
1750
+ channel?: string;
1751
+ /** Custom channel-line formatter. */
1752
+ format?: (event: UsageEvent) => string;
1753
+ }
1754
+ /**
1755
+ * Routes each {@link UsageEvent} to a store and/or a Discord channel. The
1756
+ * client owns one as `client.usage`. Tracking is fire-and-forget: a slow store
1757
+ * or channel never blocks command handling, and failures are logged.
1758
+ */
1759
+ declare class UsageTracker {
1760
+ /** The configured store, if any. Directly queryable. */
1761
+ store?: UsageStore;
1762
+ private reporter?;
1763
+ private client?;
1764
+ private logger?;
1765
+ /** Whether anything will happen on {@link track}. */
1766
+ get enabled(): boolean;
1767
+ /** @internal Used by the client to resolve report channels. */
1768
+ setClient(client: SpearClient): this;
1769
+ setLogger(logger: Logger): this;
1770
+ /** Persist events to a store (a database). */
1771
+ setStore(store: UsageStore): this;
1772
+ /** Mirror events into a Discord channel. */
1773
+ reportTo(channelId: string, format?: (event: UsageEvent) => string): this;
1774
+ /** Record a use. Returns immediately; storing/reporting happen in the background. */
1775
+ track(event: UsageEvent): void;
1776
+ private run;
1777
+ }
1778
+
1779
+ /** Metadata accepted by both context-menu kinds. */
1780
+ interface ContextMenuMeta {
1781
+ defaultMemberPermissions?: PermissionResolvable | null;
1782
+ nsfw?: boolean;
1783
+ guildOnly?: boolean;
1784
+ nameLocalizations?: LocalizationMap;
1785
+ cooldown?: CooldownInput;
1786
+ guards?: readonly Guard[];
1787
+ }
1788
+ /** Configuration for {@link userCommand}. */
1789
+ interface UserCommandConfig<R = void> extends ContextMenuMeta {
1790
+ name: string;
1791
+ run: (ctx: UserContextMenuContext) => Awaitable<R>;
1792
+ }
1793
+ /** Configuration for {@link messageCommand}. */
1794
+ interface MessageCommandConfig<R = void> extends ContextMenuMeta {
1795
+ name: string;
1796
+ run: (ctx: MessageContextMenuContext) => Awaitable<R>;
1797
+ }
1798
+ /** Common shape for any context-menu command (user or message). */
1799
+ interface BaseContextMenuCommand {
1800
+ readonly name: string;
1801
+ readonly cooldown?: CooldownConfig;
1802
+ readonly guards?: readonly Guard[];
1803
+ toJSON(): RESTPostAPIContextMenuApplicationCommandsJSONBody;
1804
+ }
1805
+ /** A user-target context-menu command. */
1806
+ interface UserContextMenu extends BaseContextMenuCommand {
1807
+ readonly kind: "userMenu";
1808
+ execute(interaction: UserContextMenuCommandInteraction): Promise<void>;
1809
+ }
1810
+ /** A message-target context-menu command. */
1811
+ interface MessageContextMenu extends BaseContextMenuCommand {
1812
+ readonly kind: "messageMenu";
1813
+ execute(interaction: MessageContextMenuCommandInteraction): Promise<void>;
1814
+ }
1815
+ /** A registered context-menu command — discriminated by `kind`. */
1816
+ type ContextMenuCommand = UserContextMenu | MessageContextMenu;
1817
+ /** Handler context for a user-target context menu. */
1818
+ declare class UserContextMenuContext extends BaseContext<UserContextMenuCommandInteraction> {
1819
+ /** The user the menu was invoked on. */
1820
+ get targetUser(): discord_js.User;
1821
+ /** The member version of the target, if available. */
1822
+ get targetMember(): discord_js.GuildMember | discord_js.APIInteractionGuildMember | null;
1823
+ }
1824
+ /** Handler context for a message-target context menu. */
1825
+ declare class MessageContextMenuContext extends BaseContext<MessageContextMenuCommandInteraction> {
1826
+ /** The message the menu was invoked on. */
1827
+ get targetMessage(): discord_js.Message<boolean>;
1828
+ }
1829
+ /** Define a user-target ("Apps → user") context-menu command. */
1830
+ declare function userCommand<R = void>(config: UserCommandConfig<R>): UserContextMenu;
1831
+ /** Define a message-target ("Apps → message") context-menu command. */
1832
+ declare function messageCommand<R = void>(config: MessageCommandConfig<R>): MessageContextMenu;
1833
+ /** Holds context-menu commands and routes incoming interactions to them. */
1834
+ declare class ContextMenuRegistry {
1835
+ private readonly users;
1836
+ private readonly messages;
1837
+ private logger?;
1838
+ private cooldowns?;
1839
+ private defaultCooldown?;
1840
+ private defaultGuards;
1841
+ private onUsage?;
1842
+ /** Register one or more context-menu commands. */
1843
+ add(...commands: readonly ContextMenuCommand[]): this;
1844
+ /** Total number of registered context-menu commands. */
1845
+ get size(): number;
1846
+ /** Every registered command, both kinds. */
1847
+ all(): ContextMenuCommand[];
1848
+ /** Serialise every command for the REST `applicationCommands` PUT body. */
1849
+ toJSON(): RESTPostAPIContextMenuApplicationCommandsJSONBody[];
1850
+ setLogger(logger: Logger): this;
1851
+ setCooldowns(manager: CooldownManager, defaultCooldown?: CooldownConfig): this;
1852
+ setDefaultGuards(guards: readonly Guard[]): this;
1853
+ setUsageHook(hook: (event: UsageEvent) => void): this;
1854
+ /** Dispatch a user-target interaction. */
1855
+ handleUser(interaction: UserContextMenuCommandInteraction): Promise<void>;
1856
+ /** Dispatch a message-target interaction. */
1857
+ handleMessage(interaction: MessageContextMenuCommandInteraction): Promise<void>;
1858
+ private dispatch;
1859
+ }
1860
+
1861
+ /**
1862
+ * Paginated embed replies driven by buttons.
1863
+ *
1864
+ * Real bots reimplement the same {page state, prev/next buttons, user-only
1865
+ * filter, disable-on-timeout} dance per command. {@link paginate} handles it:
1866
+ * pass an item list and a render function, and you get an interactive,
1867
+ * timeout-aware paginator in one call. {@link buildPaginatorPage} exposes the
1868
+ * same logic without the collector, for tests or custom flows.
1869
+ */
1870
+
1871
+ /** Result of {@link PaginateOptions.render}: a builder OR a full message payload. */
1872
+ type PaginateRender = EmbedBuilder | readonly EmbedBuilder[] | BaseMessageOptions;
1873
+ /** Options for {@link paginate} / {@link buildPaginatorPage}. */
1874
+ interface PaginateOptions<T> {
1875
+ /** Items per page. Default `10`. */
1876
+ pageSize?: number;
1877
+ /** Build the body for one page. */
1878
+ render: (slice: readonly T[], state: {
1879
+ page: number;
1880
+ pages: number;
1881
+ }) => PaginateRender | Promise<PaginateRender>;
1882
+ /** When set, only this user id can click. Defaults to the invoker. */
1883
+ user?: string;
1884
+ /** Time (ms) before buttons are disabled. Default `5 * 60_000`. */
1885
+ timeoutMs?: number;
1886
+ /** Custom-id prefix to avoid clashes with other components. Default `"spk-page"`. */
1887
+ namespace?: string;
1888
+ /** Make the initial reply ephemeral. Default `false`. */
1889
+ ephemeral?: boolean;
1890
+ /** Button labels. Defaults: `‹` Prev / `›` Next / `«` First / `»` Last. */
1891
+ labels?: {
1892
+ first?: string;
1893
+ prev?: string;
1894
+ next?: string;
1895
+ last?: string;
1896
+ };
1897
+ /** Which buttons to show. Default `"prev-next"`. */
1898
+ controls?: "prev-next" | "first-prev-next-last";
1899
+ }
1900
+ /**
1901
+ * Build the payload for a single paginator page (embeds + button row), without
1902
+ * any interaction or collector wiring. Useful for tests, web previews and
1903
+ * custom UIs that want spearkit's slicing/controls but their own send path.
1904
+ */
1905
+ declare function buildPaginatorPage<T>(items: readonly T[], page: number, options: PaginateOptions<T>): Promise<{
1906
+ payload: BaseMessageOptions;
1907
+ pages: number;
1908
+ }>;
1909
+ /**
1910
+ * Send an item list across paginated, button-controlled embeds.
1911
+ *
1912
+ * The first page is replied to {@link interaction} (or `editReply`d when
1913
+ * already deferred), then a button-component collector handles prev/next
1914
+ * clicks until the timeout fires — at which point the buttons are disabled.
1915
+ */
1916
+ declare function paginate<T>(interaction: RepliableInteraction, items: readonly T[], options: PaginateOptions<T>): Promise<void>;
1917
+
1918
+ /**
1919
+ * Yes/no confirmation prompts: a one-line API for the "are you sure?" flow
1920
+ * Yes/no confirmation prompts: a one-line API for the "are you sure?" flow
1921
+ * that bots reimplement every time (reaction-based, button-based, or modal).
1922
+ *
1923
+ * @example
1924
+ * ```ts
1925
+ * const ok = await confirm(interaction, {
1926
+ * body: `Reset **${role.name}** for **${members}** members?`,
1927
+ * confirm: { label: "Reset", style: "Danger" },
1928
+ * cancel: { label: "Cancel" },
1929
+ * timeoutMs: 30_000,
1930
+ * });
1931
+ * if (!ok.confirmed) return ctx.error("Cancelled.");
1932
+ * ```
1933
+ */
1934
+
1935
+ /** Visual style for a confirm/cancel button. */
1936
+ type ConfirmButtonStyle = "Primary" | "Secondary" | "Success" | "Danger";
1937
+ /** One of the two buttons. */
1938
+ interface ConfirmButtonOptions {
1939
+ /** Visible label. */
1940
+ label?: string;
1941
+ /** Visual style. */
1942
+ style?: ConfirmButtonStyle;
1943
+ }
1944
+ /** Options for {@link confirm}. */
1945
+ interface ConfirmOptions {
1946
+ /** Embed title. */
1947
+ title?: string;
1948
+ /** Embed body. */
1949
+ body: string;
1950
+ /** Confirm button config. Defaults: label `"Confirm"`, style `"Success"`. */
1951
+ confirm?: ConfirmButtonOptions;
1952
+ /** Cancel button config. Defaults: label `"Cancel"`, style `"Secondary"`. */
1953
+ cancel?: ConfirmButtonOptions;
1954
+ /** Only this user id can click. Defaults to the invoker. */
1955
+ user?: string;
1956
+ /** Time (ms) before the prompt times out as cancelled. Default `30_000`. */
1957
+ timeoutMs?: number;
1958
+ /** Make the prompt ephemeral. Default `true`. */
1959
+ ephemeral?: boolean;
1960
+ /** Custom-id prefix to avoid clashes. Default `"spk-confirm"`. */
1961
+ namespace?: string;
1962
+ }
1963
+ /** Result of {@link confirm}. */
1964
+ interface ConfirmResult {
1965
+ /** Whether the user confirmed (clicked the confirm button before timeout). */
1966
+ confirmed: boolean;
1967
+ /** How the prompt ended. */
1968
+ reason: "confirm" | "cancel" | "timeout";
1969
+ /** The triggering button interaction when `reason !== "timeout"`. */
1970
+ interaction?: ButtonInteraction;
1971
+ }
1972
+ /**
1973
+ * Show a yes/no confirmation prompt and wait for the user's choice.
1974
+ *
1975
+ * Resolves once a button is clicked or the timeout fires. The clicked button
1976
+ * is automatically acknowledged via `deferUpdate`, and the original message's
1977
+ * buttons are disabled. Returns `{ confirmed, reason, interaction? }`.
1978
+ */
1979
+ declare function confirm(interaction: RepliableInteraction, options: ConfirmOptions): Promise<ConfirmResult>;
1980
+
1981
+ /**
1982
+ * Typed custom-id codec.
1983
+ *
1984
+ * Patterns follow the grammar `namespace(:{param})*`, e.g. `"vote"` or
1985
+ * `"vote:{choice}"` or `"page:{id}:{dir}"`. The `namespace` is the routing key;
1986
+ * each `{param}` becomes a positional, percent-escaped value in the encoded id.
1987
+ * Param names are recovered at the type level so handlers get a typed `params`
1988
+ * object and `build()` requires exactly the right params.
1989
+ */
1990
+ /** Names of the `{param}` placeholders inside a pattern. */
1991
+ type ParamNames<S extends string> = S extends `${string}{${infer Name}}${infer Rest}` ? Name | ParamNames<Rest> : never;
1992
+ /** The params object a pattern resolves to (every value is a string). */
1993
+ type Params<S extends string> = {
1994
+ [K in ParamNames<S>]: string;
1995
+ };
1996
+ /** Arguments `build()` accepts: none when the pattern has no params. */
1997
+ type BuildArgs<S extends string> = [ParamNames<S>] extends [never] ? [] : [params: Params<S>];
1998
+ /** The discord custom-id length limit. */
1999
+ declare const MAX_CUSTOM_ID_LENGTH = 100;
2000
+ /** A compiled pattern: its routing namespace and ordered param names. */
2001
+ interface CompiledPattern {
2002
+ readonly pattern: string;
2003
+ readonly namespace: string;
2004
+ readonly paramNames: readonly string[];
2005
+ }
2006
+ /** Compile and validate a custom-id pattern. Throws on malformed input. */
2007
+ declare function compilePattern(pattern: string): CompiledPattern;
2008
+ /** Build a concrete custom-id from a compiled pattern and its params. */
2009
+ declare function buildCustomId(compiled: CompiledPattern, params: Readonly<Record<string, string>>): string;
2010
+ /** The namespace + raw values parsed out of an incoming custom-id. */
2011
+ interface ParsedCustomId {
2012
+ readonly namespace: string;
2013
+ readonly values: readonly string[];
2014
+ }
2015
+ /** Parse an incoming custom-id into its namespace and decoded values. */
2016
+ declare function parseCustomId(customId: string): ParsedCustomId;
2017
+ /** Map ordered values onto their param names. */
2018
+ declare function paramsFromValues(paramNames: readonly string[], values: readonly string[]): Record<string, string>;
2019
+
2020
+ type UpdateInput = string | InteractionUpdateOptions;
2021
+ /** The concrete message-component interaction types (button + every select). */
2022
+ type AnyComponentInteraction = ButtonInteraction | StringSelectMenuInteraction | UserSelectMenuInteraction | RoleSelectMenuInteraction | ChannelSelectMenuInteraction | MentionableSelectMenuInteraction;
2023
+ /**
2024
+ * Base context for message-component interactions (buttons and selects).
2025
+ * Adds the component-only `update`/`deferUpdate`/`showModal` helpers and the
2026
+ * routed, typed {@link params}.
2027
+ */
2028
+ declare class MessageComponentContext<P, I extends AnyComponentInteraction = AnyComponentInteraction> extends BaseContext<I> {
2029
+ /** Params extracted from the custom-id pattern. */
2030
+ readonly params: P;
2031
+ constructor(interaction: I,
2032
+ /** Params extracted from the custom-id pattern. */
2033
+ params: P);
2034
+ /** The raw custom-id that triggered this interaction. */
2035
+ get customId(): string;
2036
+ /** The message the component lives on. */
2037
+ get message(): discord_js.Message<boolean>;
2038
+ /** Edit the message this component belongs to. */
2039
+ update(input: UpdateInput): Promise<void>;
2040
+ /** Acknowledge the interaction without editing the message yet. */
2041
+ deferUpdate(): Promise<void>;
2042
+ /** Open a modal in response to this component. */
2043
+ showModal(modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalComponentData | ModalBuilder): Promise<void>;
2044
+ }
2045
+ /** Context for a button click. */
2046
+ declare class ButtonContext<P = Record<string, never>> extends MessageComponentContext<P, ButtonInteraction> {
2047
+ }
2048
+ /** Context for a string select; exposes the chosen {@link values}. */
2049
+ declare class StringSelectContext<P = Record<string, never>> extends MessageComponentContext<P, StringSelectMenuInteraction> {
2050
+ /** All selected values. */
2051
+ get values(): string[];
2052
+ /** The first selected value, or `undefined` if none. */
2053
+ get value(): string | undefined;
2054
+ }
2055
+ /** Context for a user select; exposes selected ids, users and members. */
2056
+ declare class UserSelectContext<P = Record<string, never>> extends MessageComponentContext<P, UserSelectMenuInteraction> {
2057
+ get values(): string[];
2058
+ get users(): discord_js.Collection<string, discord_js.User>;
2059
+ get members(): discord_js.Collection<string, discord_js.GuildMember | discord_js.APIGuildMember>;
2060
+ }
2061
+ /** Context for a role select. */
2062
+ declare class RoleSelectContext<P = Record<string, never>> extends MessageComponentContext<P, RoleSelectMenuInteraction> {
2063
+ get values(): string[];
2064
+ get roles(): discord_js.Collection<string, discord_js.Role | discord_js.APIRole>;
2065
+ }
2066
+ /** Context for a channel select. */
2067
+ declare class ChannelSelectContext<P = Record<string, never>> extends MessageComponentContext<P, ChannelSelectMenuInteraction> {
2068
+ get values(): string[];
2069
+ get channels(): discord_js.Collection<string, discord_js.Channel | discord_js.APIChannel>;
2070
+ }
2071
+ /** Context for a mentionable (user + role) select. */
2072
+ declare class MentionableSelectContext<P = Record<string, never>> extends MessageComponentContext<P, MentionableSelectMenuInteraction> {
2073
+ get values(): string[];
2074
+ get users(): discord_js.Collection<string, discord_js.User>;
2075
+ get roles(): discord_js.Collection<string, discord_js.Role | discord_js.APIRole>;
2076
+ get members(): discord_js.Collection<string, discord_js.GuildMember | discord_js.APIGuildMember>;
2077
+ }
2078
+ /**
2079
+ * Context for a submitted modal. Exposes the routed {@link params} plus the
2080
+ * resolved text-input {@link fields}, keyed by the field names you declared.
2081
+ */
2082
+ declare class ModalContext<P, F extends string = string> extends BaseContext<ModalSubmitInteraction> {
2083
+ readonly params: P;
2084
+ /** Submitted values, keyed by the field names from your modal definition. */
2085
+ readonly fields: Record<F, string>;
2086
+ constructor(interaction: ModalSubmitInteraction, params: P,
2087
+ /** Submitted values, keyed by the field names from your modal definition. */
2088
+ fields: Record<F, string>);
2089
+ /** The raw custom-id that triggered this modal submission. */
2090
+ get customId(): string;
2091
+ }
2092
+
594
2093
  /** Accepted button styles for an interactive (custom-id) button. */
595
2094
  type ButtonStyleInput = "Primary" | "Secondary" | "Success" | "Danger" | ButtonStyle.Primary | ButtonStyle.Secondary | ButtonStyle.Success | ButtonStyle.Danger;
596
2095
  /** Config for an interactive button created with {@link button}. */
@@ -601,6 +2100,8 @@ interface ButtonConfig<P extends string, R> {
601
2100
  style?: ButtonStyleInput;
602
2101
  emoji?: ComponentEmojiResolvable;
603
2102
  disabled?: boolean;
2103
+ /** Preconditions evaluated before the handler runs. */
2104
+ guards?: readonly Guard[];
604
2105
  run: (ctx: ButtonContext<Params<P>>) => Awaitable<R>;
605
2106
  }
606
2107
  /** A registrable button with a typed {@link build}. */
@@ -637,6 +2138,8 @@ interface SelectConfigBase {
637
2138
  minValues?: number;
638
2139
  maxValues?: number;
639
2140
  disabled?: boolean;
2141
+ /** Preconditions evaluated before the handler runs. */
2142
+ guards?: readonly Guard[];
640
2143
  }
641
2144
  /** Config for a string select created with {@link stringSelect}. */
642
2145
  interface StringSelectConfig<P extends string, R> extends SelectConfigBase {
@@ -714,6 +2217,8 @@ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>,
714
2217
  id: P;
715
2218
  title: string;
716
2219
  fields: F;
2220
+ /** Preconditions evaluated before the handler runs. */
2221
+ guards?: readonly Guard[];
717
2222
  run: (ctx: ModalContext<Params<P>, keyof F & string>) => Awaitable<R>;
718
2223
  }
719
2224
  /** A registrable modal with a typed {@link build}. */
@@ -750,100 +2255,4 @@ declare function modal<const P extends string, F extends Record<string, TextInpu
750
2255
  */
751
2256
  declare function row<C extends MessageActionRowComponentBuilder>(...components: C[]): ActionRowBuilder<C>;
752
2257
 
753
- /**
754
- * A spearkit plugin: a named, reusable bundle of commands, events and components.
755
- * Its {@link setup} runs once when added to a client via `client.use(plugin)`.
756
- */
757
- interface SpearPlugin {
758
- readonly name: string;
759
- setup(client: SpearClient): Awaitable<void>;
760
- }
761
- /** Identity helper that gives a plugin object its type and editor hints. */
762
- declare function definePlugin(plugin: SpearPlugin): SpearPlugin;
763
-
764
- /** Options for the directory loader. */
765
- interface LoadOptions {
766
- /** File extensions to import. Default: `.js`, `.mjs`, `.cjs`. */
767
- extensions?: readonly string[];
768
- /** Recurse into subdirectories. Default: `true`. */
769
- recursive?: boolean;
770
- }
771
- /**
772
- * Recursively import a directory and collect every spearkit-registrable export
773
- * (commands, events, components) found in default or named exports.
774
- */
775
- declare function collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>;
776
- /**
777
- * Load a directory and register everything it exports into the client.
778
- * Returns the number of items registered.
779
- */
780
- declare function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>;
781
-
782
- /** Anything that can be handed to {@link SpearClient.register}. */
783
- type Registerable = SlashCommand | EventDef | ComponentDef;
784
- /**
785
- * Ready-made intent presets. Pass one to {@link SpearClient} as `intents`.
786
- * `all` includes privileged intents — enable them in the developer portal.
787
- */
788
- declare const Intents: {
789
- /** No intents. */
790
- readonly none: GatewayIntentBits[];
791
- /** Just `Guilds` — enough for slash commands and interactions. */
792
- readonly default: readonly [GatewayIntentBits.Guilds];
793
- /** Guild + member gateway data. */
794
- readonly guilds: readonly [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers];
795
- /** Read message content (privileged) alongside guild messages. */
796
- readonly messages: readonly [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent];
797
- /** Every intent, including privileged ones. */
798
- readonly all: GatewayIntentBits[];
799
- };
800
- /** Options for {@link SpearClient}. Identical to discord.js but `intents` may be omitted. */
801
- type SpearClientOptions = Partial<ClientOptions>;
802
- /**
803
- * A discord.js {@link Client} with batteries included: command, event and
804
- * component registries plus interaction routing wired up automatically.
805
- *
806
- * @example
807
- * ```ts
808
- * const client = new SpearClient({ intents: Intents.default });
809
- * client.register(ping, onReady, voteButton);
810
- * await client.start(process.env.TOKEN);
811
- * await client.deployCommands({ guildId: "123" });
812
- * ```
813
- */
814
- declare class SpearClient extends Client {
815
- /** Slash command registry and dispatcher. */
816
- readonly commands: CommandRegistry;
817
- /** Event listener registry. */
818
- readonly events: EventRegistry;
819
- /** Button / select / modal registry and router. */
820
- readonly components: ComponentRegistry;
821
- constructor(options?: SpearClientOptions);
822
- /**
823
- * Register commands, events and components in one call. Each item is routed
824
- * to the matching registry based on its kind.
825
- */
826
- register(...items: Registerable[]): this;
827
- /** Install one or more plugins, running each plugin's `setup`. */
828
- use(...plugins: SpearPlugin[]): Promise<this>;
829
- /**
830
- * Recursively load a directory and register every command, event and
831
- * component it exports. Returns the number of items registered.
832
- */
833
- load(dir: string, options?: LoadOptions): Promise<number>;
834
- /**
835
- * Log in. Falls back to the `DISCORD_TOKEN` environment variable when no
836
- * token is passed.
837
- */
838
- start(token?: string): Promise<this>;
839
- /**
840
- * Push the registered slash commands to discord using the client's own
841
- * authenticated REST connection. Call after the client is ready.
842
- */
843
- deployCommands(options?: {
844
- guildId?: string;
845
- }): Promise<DeployResult>;
846
- private route;
847
- }
848
-
849
- export { type AllowedChannelType, type AnyComponentInteraction, type AnyOptionDef, AutocompleteContext, type AutocompleteHandler, BaseContext, type BuildArgs, type Button, type ButtonConfig, ButtonContext, type ButtonRoute, type ButtonStyleInput, type ChannelSelect, ChannelSelectContext, type ChannelSelectRoute, type CommandConfig, CommandContext, type CommandErrorHandler, type CommandGroupConfig, CommandRegistry, type CompiledPattern, type ComponentDef, type ComponentErrorHandler, ComponentRegistry, type DeployOptions, type DeployResult, type EntitySelectConfig, type EventConfig, type EventDef, type EventHandler, EventRegistry, Intents, type LinkButtonConfig, type LoadOptions, MAX_CUSTOM_ID_LENGTH, type MentionableSelect, MentionableSelectContext, type MentionableSelectRoute, MessageComponentContext, type Modal, type ModalConfig, ModalContext, type ModalRoute, type OptionChoice, type OptionDef, type OptionMap, type OptionValue, type ParamNames, type Params, type ParsedCustomId, type Registerable, type ReplyData, type ReplyInput, type ResolvedOption, type ResolvedOptions, type RoleSelect, RoleSelectContext, type RoleSelectRoute, SlashCommand, SpearClient, type SpearClientOptions, type SpearPlugin, type StringSelect, type StringSelectConfig, StringSelectContext, type StringSelectRoute, type Subcommand, type SubcommandConfig, type SubcommandGroup, type SubcommandGroupConfig, type TextInputDef, type TextInputStyleInput, type UserSelect, UserSelectContext, type UserSelectRoute, asEphemeral, buildCustomId, button, channelSelect, collectModules, command, commandGroup, compilePattern, definePlugin, event, linkButton, loadInto, mentionableSelect, modal, normalizeReply, option, optionsHaveAutocomplete, paramsFromValues, parseCustomId, readOption, roleSelect, row, stringSelect, subcommand, subcommandGroup, textInput, toAPIOption, userSelect };
2258
+ export { type AllowedChannelType, type AnyComponentInteraction, type AnyOptionDef, AutocompleteContext, type AutocompleteHandler, BaseContext, type BaseContextMenuCommand, type BuildArgs, type Button, type ButtonConfig, ButtonContext, type ButtonRoute, type ButtonStyleInput, type CacheSetOptions, type CacheStore, type ChannelSelect, ChannelSelectContext, type ChannelSelectRoute, type CommandConfig, CommandContext, type CommandErrorHandler, type CommandGroupConfig, CommandRegistry, type CompiledPattern, type ComponentDef, type ComponentErrorHandler, ComponentRegistry, type ConfirmButtonOptions, type ConfirmButtonStyle, type ConfirmOptions, type ConfirmResult, type ContextMenuCommand, ContextMenuRegistry, type CooldownActor, type CooldownConfig, type CooldownExemptions, type CooldownInput, CooldownManager, type CooldownOverrides, type CooldownResult, type CooldownScope, CronExpression, DEFAULT_EMBED_COLORS, DEFAULT_EMBED_ICONS, type DeployOptions, type DeployResult, type DiscordTimestampStyle, type EmbedColors, type EmbedIcons, type EmbedLevel, type EmbedPresetInput, Embeds, type EmbedsOptions, type EntitySelectConfig, type EnvReader, type EventConfig, type EventDef, type EventHandler, EventRegistry, type FormatDurationOptions, type Guard, type GuardContext, type GuardResult, Intents, JsonFileUsageStore, KeyedLock, type KeyedLockOptions, type LinkButtonConfig, type LoadConfigOptions, type LoadEnvOptions, type LoadOptions, type LockRelease, type LogEntry, type LogLevel, type LogOptions, type LogSink, type LogThreshold, type LogValue, Logger, type LoggerOptions, MAX_CUSTOM_ID_LENGTH, MemoryCache, MemoryUsageStore, type MentionableSelect, MentionableSelectContext, type MentionableSelectRoute, type MessageCommandConfig, MessageComponentContext, type MessageContextMenu, MessageContextMenuContext, type Modal, type ModalConfig, ModalContext, type ModalRoute, type OptionChoice, type OptionDef, type OptionMap, type OptionValue, type PaginateOptions, type PaginateRender, type ParamNames, type Params, type ParsedCustomId, type ParsedEnv, type PrefixArgError, type PrefixArgSpec, PrefixArgsBuilder, type PrefixArgsOk, type PrefixArgsParser, type PrefixCommand, type PrefixCommandConfig, PrefixContext, type PrefixErrorHandler, type PrefixOptions, PrefixRegistry, type RateLimitResult, type Registerable, type ReplyData, type ReplyInput, type ResolvedOption, type ResolvedOptions, type RoleSelect, RoleSelectContext, type RoleSelectRoute, type RunGuardsResult, type SafeFetchOptions, type ScheduledTask, SlashCommand, SpearClient, type SpearClientOptions, type SpearOptions, type SpearPlugin, type StringSelect, type StringSelectConfig, StringSelectContext, type StringSelectRoute, type Subcommand, type SubcommandConfig, type SubcommandGroup, type SubcommandGroupConfig, type TaskConfig, TaskScheduler, type TextInputDef, type TextInputStyleInput, type UsageEvent, type UsageMetaValue, type UsageOptions, type UsageOutcome, type UsageStore, UsageTracker, type UsageType, type UserCommandConfig, type UserContextMenu, UserContextMenuContext, type UserSelect, UserSelectContext, type UserSelectRoute, asEphemeral, buildCustomId, buildPaginatorPage, button, channelSelect, collectModules, command, commandGroup, compilePattern, confirm, consoleSink, createCache, cron, defaultEmbeds, definePlugin, denied, discordTimestamp, dmOnly, effectiveDuration, env, event, fetchChannel, fetchGuild, fetchMember, fetchMessage, fetchRole, fetchUser, formatCooldownMessage, formatDuration, formatUsage, guard, guildOnly, jsonlSink, linkButton, loadConfig, loadConfigAsync, loadEnv, loadInto, lookup, lookupOptional, mentionableSelect, messageCommand, modal, normalizeCooldown, normalizeReply, option, optionsHaveAutocomplete, paginate, paramsFromValues, parseCustomId, parseDuration, parseEnv, prefixArgs, prefixCommand, readOption, relativeTimestamp, requireAllRoles, requireAnyRole, requireBotPermissions, requireOwner, requireUserPermissions, roleSelect, row, runGuards, safeFetch, safeTry, stringSelect, subcommand, subcommandGroup, task, textInput, toAPIOption, toError, userCommand, userSelect, webhookSink, withSafeTimeout };