spearkit 0.2.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.cts CHANGED
@@ -1,135 +1,498 @@
1
1
  import * as discord_js from 'discord.js';
2
- import { RepliableInteraction, InteractionReplyOptions, InteractionResponse, Message, APIApplicationCommandChannelOption, CommandInteractionOption, Attachment, ApplicationCommandOptionType, LocalizationMap, Awaitable, ChatInputCommandInteraction, APIApplicationCommandBasicOption, AutocompleteInteraction, JSONEncodable, APIModalInteractionResponseCallbackData, ModalComponentData, ModalBuilder, PermissionResolvable, RESTPostAPIChatInputApplicationCommandsJSONBody, RESTPostAPIApplicationCommandsJSONBody, REST, RESTPutAPIApplicationCommandsResult, RESTPutAPIApplicationGuildCommandsResult, ClientEvents, Client, ButtonInteraction, ChannelSelectMenuInteraction, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, MentionableSelectMenuInteraction, ModalSubmitInteraction, Interaction, MessagePayload, MessageReplyOptions, MessageCreateOptions, GatewayIntentBits, ClientOptions, InteractionUpdateOptions, ButtonBuilder, ButtonStyle, ComponentEmojiResolvable, ChannelSelectMenuBuilder, MentionableSelectMenuBuilder, TextInputStyle, RoleSelectMenuBuilder, StringSelectMenuBuilder, SelectMenuComponentOptionData, UserSelectMenuBuilder, ChannelType, MessageActionRowComponentBuilder, ActionRowBuilder } 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
5
  /**
6
- * A small, dependency-free structured logger used across spearkit so every
7
- * problem (command/component/event failures, gateway errors, your own code)
8
- * lands in one consistent, debuggable place.
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.
9
58
  *
10
- * It is intentionally tiny: levels, scopes, structured data and a pluggable
11
- * sink. No `any`/`unknown` leaks into your code — log metadata is constrained
12
- * to primitive {@link LogValue}s and an optional {@link Error}.
59
+ * @example
60
+ * ```ts
61
+ * const embeds = new Embeds({ colors: { success: 0x00ff88 } });
62
+ * await channel.send({ embeds: [embeds.success("Saved.")] });
63
+ * ```
13
64
  */
14
- /** Severity of a log entry, lowest to highest. */
15
- type LogLevel = "debug" | "info" | "warn" | "error";
16
- /** A minimum severity to emit, or `"silent"` to suppress everything. */
17
- type LogThreshold = LogLevel | "silent";
18
- /** A primitive metadata value attached to a log entry. */
19
- type LogValue = string | number | boolean | bigint | null | undefined;
20
- /** Extra context passed alongside a log message. */
21
- interface LogOptions {
22
- /** An error to attach; the default sink renders its stack. */
23
- error?: Error;
24
- /** Structured key/value metadata. */
25
- data?: Record<string, LogValue>;
26
- }
27
- /** A fully-resolved record handed to a {@link LogSink}. */
28
- interface LogEntry {
29
- readonly level: LogLevel;
30
- readonly message: string;
31
- readonly scope?: string;
32
- readonly timestamp: Date;
33
- readonly error?: Error;
34
- readonly data?: Readonly<Record<string, LogValue>>;
35
- }
36
- /** Receives every entry at or above the configured threshold. */
37
- type LogSink = (entry: LogEntry) => void;
38
- /** Construction options for a {@link Logger}. */
39
- interface LoggerOptions {
40
- /** Minimum level to emit. Default `"info"`. */
41
- level?: LogThreshold;
42
- /** Where entries go. Default {@link consoleSink}. */
43
- sink?: LogSink;
44
- /** A scope prefix for every entry (e.g. `"commands"`). */
45
- scope?: string;
46
- }
47
- /** Default sink: human-readable lines to the console (stderr for warn/error). */
48
- declare function consoleSink(entry: LogEntry): void;
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
+
49
85
  /**
50
- * A leveled, scoped logger. Create one directly or read `client.logger`.
51
- * {@link child} loggers share the parent's threshold and sink, so calling
52
- * {@link setLevel} on any of them affects the whole tree.
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.
53
104
  *
54
105
  * @example
55
106
  * ```ts
56
- * const log = new Logger({ level: "debug" });
57
- * log.info("ready", { data: { shard: 0 } });
58
- * log.child("commands").error("handler failed", { error });
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" });
59
112
  * ```
60
113
  */
61
- declare class Logger {
62
- private state;
63
- /** The scope prefix applied to every entry, if any. */
64
- readonly scope?: string;
65
- constructor(options?: LoggerOptions);
66
- /** The current minimum threshold. */
67
- get level(): LogThreshold;
68
- /** Change the threshold for this logger and every child sharing its state. */
69
- setLevel(level: LogThreshold): this;
70
- /** Whether an entry of `level` would currently be emitted. */
71
- enabled(level: LogLevel): boolean;
72
- /** A child logger with an extra scope segment, sharing this logger's state. */
73
- child(scope: string): Logger;
74
- /** Emit an entry at an explicit level. */
75
- log(level: LogLevel, message: string, options?: LogOptions): void;
76
- /** Verbose diagnostics, off by default. */
77
- debug(message: string, options?: LogOptions): void;
78
- /** Normal operational messages. */
79
- info(message: string, options?: LogOptions): void;
80
- /** Recoverable problems worth attention. */
81
- warn(message: string, options?: LogOptions): void;
82
- /** Failures. Attach the cause via `{ error }`. */
83
- error(message: string, options?: LogOptions): void;
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;
84
139
  }
85
- /** Coerce an unknown thrown value into an {@link Error}. */
86
- declare function toError(value: unknown): Error;
87
140
 
88
- /** The flat key/value map parsed from a `.env` file. */
89
- type ParsedEnv = Record<string, string>;
90
- /** Options for {@link loadEnv}. */
91
- interface LoadEnvOptions {
92
- /** File to read. Default `.env` in the current working directory. */
93
- path?: string;
94
- /** Overwrite variables already present in `process.env`. Default `false`. */
95
- override?: boolean;
96
- }
97
- /** Parse `.env`-formatted text into a flat object. Does not touch `process.env`. */
98
- declare function parseEnv(content: string): ParsedEnv;
99
141
  /**
100
- * Read a `.env` file and merge it into `process.env`. Existing variables win
101
- * unless `override` is set. Missing files are ignored (returns `{}`), so it is
102
- * safe to call unconditionally.
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.
103
172
  *
104
- * @returns the parsed key/value pairs from the file.
173
+ * @example
174
+ * ```ts
175
+ * await safeTry(() => message.delete());
176
+ * ```
105
177
  */
106
- declare function loadEnv(options?: LoadEnvOptions): ParsedEnv;
107
- /** Typed, ergonomic reader over `process.env`. */
108
- interface EnvReader {
109
- /** A string value (empty strings count as missing), or `undefined`/`fallback`. */
110
- string(key: string): string | undefined;
111
- string(key: string, fallback: string): string;
112
- /** A numeric value, or `undefined`/`fallback` when missing or non-numeric. */
113
- number(key: string): number | undefined;
114
- number(key: string, fallback: number): number;
115
- /** A boolean (`true/1/yes/on` vs `false/0/no/off`), or `undefined`/`fallback`. */
116
- boolean(key: string): boolean | undefined;
117
- boolean(key: string, fallback: boolean): boolean;
118
- /** A string value, throwing if the variable is missing or empty. */
119
- require(key: string): string;
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[];
120
218
  }
121
219
  /**
122
- * Typed accessor over `process.env`.
220
+ * Format a millisecond duration into human-readable text.
123
221
  *
124
222
  * @example
125
223
  * ```ts
126
- * loadEnv();
127
- * const token = env.require("DISCORD_TOKEN");
128
- * const port = env.number("PORT", 3000);
129
- * const debug = env.boolean("DEBUG", false);
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"
130
227
  * ```
131
228
  */
132
- declare const env: EnvReader;
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
+
411
+ /** Reply options with an ergonomic `ephemeral` shortcut (mapped to flags). */
412
+ type ReplyData = InteractionReplyOptions & {
413
+ ephemeral?: boolean;
414
+ };
415
+ /** Either a plain string or full reply options. */
416
+ type ReplyInput = string | ReplyData;
417
+ /** Normalises spearkit reply input into a discord.js reply payload. */
418
+ declare function normalizeReply(input: ReplyInput): InteractionReplyOptions;
419
+ /** Marks an input as ephemeral, regardless of how it was passed. */
420
+ declare function asEphemeral(input: ReplyInput): ReplyData;
421
+ /**
422
+ * Ergonomic base wrapper shared by every interaction context (commands,
423
+ * buttons, selects, modals). Exposes the common actor/location accessors plus
424
+ * reply helpers that smooth over discord.js' state machine.
425
+ */
426
+ declare abstract class BaseContext<I extends RepliableInteraction = RepliableInteraction> {
427
+ readonly interaction: I;
428
+ constructor(interaction: I);
429
+ get client(): I["client"];
430
+ get user(): discord_js.User;
431
+ get member(): discord_js.GuildMember | discord_js.APIInteractionGuildMember | null;
432
+ get guild(): discord_js.Guild | null;
433
+ get guildId(): string | null;
434
+ get channel(): discord_js.TextBasedChannel | null;
435
+ get channelId(): string | null;
436
+ get locale(): discord_js.Locale;
437
+ /** Whether the interaction is already deferred. */
438
+ get deferred(): boolean;
439
+ /** Whether the interaction already received an initial response. */
440
+ get replied(): boolean;
441
+ /** Send the initial response to the interaction. */
442
+ reply(input: ReplyInput): Promise<InteractionResponse<boolean>>;
443
+ /** Reply, but always hidden to everyone except the invoking user. */
444
+ replyEphemeral(input: ReplyInput): Promise<InteractionResponse<boolean>>;
445
+ /** Acknowledge now and respond later via {@link editReply}. */
446
+ defer(options?: {
447
+ ephemeral?: boolean;
448
+ }): Promise<InteractionResponse<boolean>>;
449
+ /** Edit the original (or deferred) response. */
450
+ editReply(input: ReplyInput): Promise<Message>;
451
+ /** Add an additional message after the initial response. */
452
+ followUp(input: ReplyInput): Promise<Message>;
453
+ /**
454
+ * State-aware send: replies, edits a deferred response, or follows up —
455
+ * whichever is valid given the current interaction state. The single method
456
+ * most handlers ever need.
457
+ */
458
+ send(input: ReplyInput): 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;
495
+ }
133
496
 
134
497
  /**
135
498
  * Rate-limit commands per user, per role, per guild, per channel or globally.
@@ -214,57 +577,102 @@ declare class CooldownManager {
214
577
  /** Build the user-facing message for a blocked action. */
215
578
  declare function formatCooldownMessage(config: CooldownConfig, remainingMs: number): string;
216
579
 
217
- /** Reply options with an ergonomic `ephemeral` shortcut (mapped to flags). */
218
- type ReplyData = InteractionReplyOptions & {
219
- ephemeral?: boolean;
220
- };
221
- /** Either a plain string or full reply options. */
222
- type ReplyInput = string | ReplyData;
223
- /** Normalises spearkit reply input into a discord.js reply payload. */
224
- declare function normalizeReply(input: ReplyInput): InteractionReplyOptions;
225
- /** Marks an input as ephemeral, regardless of how it was passed. */
226
- declare function asEphemeral(input: ReplyInput): ReplyData;
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;
227
617
  /**
228
- * Ergonomic base wrapper shared by every interaction context (commands,
229
- * buttons, selects, modals). Exposes the common actor/location accessors plus
230
- * reply helpers that smooth over discord.js' state machine.
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.
231
620
  */
232
- declare abstract class BaseContext<I extends RepliableInteraction = RepliableInteraction> {
233
- readonly interaction: I;
234
- constructor(interaction: I);
235
- get client(): I["client"];
236
- get user(): discord_js.User;
237
- get member(): discord_js.GuildMember | discord_js.APIInteractionGuildMember | null;
238
- get guild(): discord_js.Guild | null;
239
- get guildId(): string | null;
240
- get channel(): discord_js.TextBasedChannel | null;
241
- get channelId(): string | null;
242
- get locale(): discord_js.Locale;
243
- /** Whether the interaction is already deferred. */
244
- get deferred(): boolean;
245
- /** Whether the interaction already received an initial response. */
246
- get replied(): boolean;
247
- /** Send the initial response to the interaction. */
248
- reply(input: ReplyInput): Promise<InteractionResponse<boolean>>;
249
- /** Reply, but always hidden to everyone except the invoking user. */
250
- replyEphemeral(input: ReplyInput): Promise<InteractionResponse<boolean>>;
251
- /** Acknowledge now and respond later via {@link editReply}. */
252
- defer(options?: {
253
- ephemeral?: boolean;
254
- }): Promise<InteractionResponse<boolean>>;
255
- /** Edit the original (or deferred) response. */
256
- editReply(input: ReplyInput): Promise<Message>;
257
- /** Add an additional message after the initial response. */
258
- followUp(input: ReplyInput): Promise<Message>;
259
- /**
260
- * State-aware send: replies, edits a deferred response, or follows up —
261
- * whichever is valid given the current interaction state. The single method
262
- * most handlers ever need.
263
- */
264
- send(input: ReplyInput): Promise<void>;
265
- /** State-aware ephemeral error message. */
266
- error(message: string): Promise<void>;
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;
267
673
  }
674
+ /** Coerce an unknown thrown value into an {@link Error}. */
675
+ declare function toError(value: unknown): Error;
268
676
 
269
677
  /**
270
678
  * Resolved runtime value types, derived directly from discord.js' option
@@ -426,6 +834,8 @@ interface CommonMeta {
426
834
  descriptionLocalizations?: LocalizationMap;
427
835
  /** Rate-limit this command. A number is a duration in ms; see {@link CooldownConfig}. */
428
836
  cooldown?: CooldownInput;
837
+ /** Preconditions evaluated before the handler runs. */
838
+ guards?: readonly Guard[];
429
839
  }
430
840
  /** Configuration for a leaf (non-subcommand) slash command. */
431
841
  interface CommandConfig<O extends OptionMap, R> extends CommonMeta {
@@ -479,6 +889,7 @@ interface SlashCommandSpec {
479
889
  executor: (interaction: ChatInputCommandInteraction) => Promise<void>;
480
890
  autocompleter: (interaction: AutocompleteInteraction) => Promise<void>;
481
891
  cooldown?: CooldownConfig;
892
+ guards?: readonly Guard[];
482
893
  }
483
894
  /**
484
895
  * A registered slash command. Serialises itself for the discord REST API and
@@ -495,6 +906,8 @@ declare class SlashCommand {
495
906
  private readonly autocompleter;
496
907
  /** Resolved cooldown configuration for this command, if any. */
497
908
  readonly cooldown?: CooldownConfig;
909
+ /** Resolved guard list for this command, if any. */
910
+ readonly guards?: readonly Guard[];
498
911
  /** @internal */
499
912
  constructor(spec: SlashCommandSpec);
500
913
  /** Serialise to the discord REST chat-input command payload. */
@@ -525,88 +938,6 @@ declare function subcommandGroup(config: SubcommandGroupConfig): SubcommandGroup
525
938
  /** Define a command that routes to subcommands and/or subcommand groups. */
526
939
  declare function commandGroup(config: CommandGroupConfig): SlashCommand;
527
940
 
528
- /** What kind of interaction was used. */
529
- type UsageType = "command" | "prefix" | "component" | "event";
530
- /** A single recorded use. */
531
- interface UsageEvent {
532
- readonly type: UsageType;
533
- /** Command/component name (or event name). */
534
- readonly name: string;
535
- readonly userId?: string;
536
- readonly userTag?: string;
537
- readonly guildId?: string | null;
538
- readonly channelId?: string | null;
539
- /** Free-form extra detail. */
540
- readonly detail?: string;
541
- readonly timestamp: Date;
542
- }
543
- /** A pluggable persistence backend for {@link UsageEvent}s. */
544
- interface UsageStore {
545
- /** Persist one event. */
546
- record(event: UsageEvent): Awaitable<void>;
547
- /** Read every persisted event. */
548
- all(): Awaitable<readonly UsageEvent[]>;
549
- }
550
- /** In-memory store; great for tests and dashboards. Optionally capped. */
551
- declare class MemoryUsageStore implements UsageStore {
552
- private readonly limit;
553
- private readonly events;
554
- constructor(limit?: number);
555
- record(event: UsageEvent): void;
556
- all(): readonly UsageEvent[];
557
- /** Total recorded events. */
558
- get size(): number;
559
- /** Events recorded for a given user id. */
560
- byUser(userId: string): UsageEvent[];
561
- /** Forget everything. */
562
- clear(): void;
563
- }
564
- /**
565
- * File-backed store using newline-delimited JSON (`.jsonl`). Appends one line
566
- * per event — durable, human-inspectable, and dependency-free.
567
- */
568
- declare class JsonFileUsageStore implements UsageStore {
569
- private readonly path;
570
- constructor(path: string);
571
- record(event: UsageEvent): Promise<void>;
572
- all(): Promise<readonly UsageEvent[]>;
573
- }
574
- /** Default one-line rendering of a usage event for a Discord channel. */
575
- declare function formatUsage(event: UsageEvent): string;
576
- /** Client-level usage configuration (the `usage` option). */
577
- interface UsageOptions {
578
- /** Persist events to this store (a database). */
579
- store?: UsageStore;
580
- /** Mirror events into this Discord channel id. */
581
- channel?: string;
582
- /** Custom channel-line formatter. */
583
- format?: (event: UsageEvent) => string;
584
- }
585
- /**
586
- * Routes each {@link UsageEvent} to a store and/or a Discord channel. The
587
- * client owns one as `client.usage`. Tracking is fire-and-forget: a slow store
588
- * or channel never blocks command handling, and failures are logged.
589
- */
590
- declare class UsageTracker {
591
- /** The configured store, if any. Directly queryable. */
592
- store?: UsageStore;
593
- private reporter?;
594
- private client?;
595
- private logger?;
596
- /** Whether anything will happen on {@link track}. */
597
- get enabled(): boolean;
598
- /** @internal Used by the client to resolve report channels. */
599
- setClient(client: SpearClient): this;
600
- setLogger(logger: Logger): this;
601
- /** Persist events to a store (a database). */
602
- setStore(store: UsageStore): this;
603
- /** Mirror events into a Discord channel. */
604
- reportTo(channelId: string, format?: (event: UsageEvent) => string): this;
605
- /** Record a use. Returns immediately; storing/reporting happen in the background. */
606
- track(event: UsageEvent): void;
607
- private run;
608
- }
609
-
610
941
  /** Error hook invoked when a command handler throws. */
611
942
  type CommandErrorHandler = (error: Error, interaction: ChatInputCommandInteraction) => Awaitable<void>;
612
943
  /** Options for pushing commands to discord. */
@@ -629,6 +960,7 @@ declare class CommandRegistry {
629
960
  private logger?;
630
961
  private cooldowns?;
631
962
  private defaultCooldown?;
963
+ private defaultGuards;
632
964
  private onUsage?;
633
965
  /** Register one or more commands. Later registrations override by name. */
634
966
  add(...commands: SlashCommand[]): this;
@@ -648,6 +980,8 @@ declare class CommandRegistry {
648
980
  setLogger(logger: Logger): this;
649
981
  /** Wire a shared cooldown manager and an optional default cooldown for every command. */
650
982
  setCooldowns(manager: CooldownManager, defaultCooldown?: CooldownConfig): this;
983
+ /** Guards that run before every command's own guards. */
984
+ setDefaultGuards(guards: readonly Guard[]): this;
651
985
  /** Attach a hook called after each successful command execution. */
652
986
  setUsageHook(hook: (event: UsageEvent) => void): this;
653
987
  /** Serialise every command to discord REST payloads. */
@@ -665,6 +999,7 @@ declare class CommandRegistry {
665
999
  deploy(options: DeployOptions): Promise<DeployResult>;
666
1000
  private defaultErrorReply;
667
1001
  private replyCooldown;
1002
+ private replyDenied;
668
1003
  }
669
1004
 
670
1005
  /** A typed handler for a discord.js client event. */
@@ -721,6 +1056,7 @@ declare class EventRegistry {
721
1056
  interface RouteBase {
722
1057
  readonly namespace: string;
723
1058
  readonly paramNames: readonly string[];
1059
+ readonly guards?: readonly Guard[];
724
1060
  }
725
1061
  /** Routing entry for a button. */
726
1062
  interface ButtonRoute extends RouteBase {
@@ -777,6 +1113,7 @@ declare class ComponentRegistry {
777
1113
  private errorHandler?;
778
1114
  private logger?;
779
1115
  private onUsage?;
1116
+ private defaultGuards;
780
1117
  /** Register one or more components. Later registrations override by namespace. */
781
1118
  add(...defs: ComponentDef[]): this;
782
1119
  /** Set the handler used when a component throws. */
@@ -785,44 +1122,313 @@ declare class ComponentRegistry {
785
1122
  setLogger(logger: Logger): this;
786
1123
  /** Attach a hook called after each successful component handler run. */
787
1124
  setUsageHook(hook: (event: UsageEvent) => void): this;
1125
+ /** Guards that run before every component's own guards. */
1126
+ setDefaultGuards(guards: readonly Guard[]): this;
788
1127
  /** Total number of registered components. */
789
1128
  get size(): number;
790
1129
  /**
791
- * Dispatch an interaction to its component handler. Returns `true` if a
792
- * handler matched and ran, `false` otherwise.
1130
+ * Dispatch an interaction to its component handler. Returns `true` if a
1131
+ * handler matched and ran, `false` otherwise.
1132
+ */
1133
+ handle(interaction: Interaction): Promise<boolean>;
1134
+ private exec;
1135
+ }
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".
793
1298
  */
794
- handle(interaction: Interaction): Promise<boolean>;
795
- private exec;
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;
796
1328
  }
797
1329
 
798
- /**
799
- * A spearkit plugin: a named, reusable bundle of commands, events and components.
800
- * Its {@link setup} runs once when added to a client via `client.use(plugin)`.
801
- */
802
- interface SpearPlugin {
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 {
803
1341
  readonly name: string;
804
- setup(client: SpearClient): Awaitable<void>;
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;
805
1370
  }
806
- /** Identity helper that gives a plugin object its type and editor hints. */
807
- declare function definePlugin(plugin: SpearPlugin): SpearPlugin;
808
-
809
- /** Options for the directory loader. */
810
- interface LoadOptions {
811
- /** File extensions to import. Default: `.js`, `.mjs`, `.cjs`. */
812
- extensions?: readonly string[];
813
- /** Recurse into subdirectories. Default: `true`. */
814
- recursive?: boolean;
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;
815
1387
  }
816
1388
  /**
817
- * Recursively import a directory and collect every spearkit-registrable export
818
- * (commands, events, components) found in default or named exports.
819
- */
820
- declare function collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>;
821
- /**
822
- * Load a directory and register everything it exports into the client.
823
- * Returns the number of items registered.
1389
+ * Build a typed argument schema for {@link prefixCommand}. Chain calls
1390
+ * positionally first token first arg, second → second arg, etc.
824
1391
  */
825
- declare function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>;
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<{}>;
826
1432
 
827
1433
  /** Options controlling how prefix messages are recognised. */
828
1434
  interface PrefixOptions {
@@ -836,7 +1442,7 @@ interface PrefixOptions {
836
1442
  caseInsensitive?: boolean;
837
1443
  }
838
1444
  /** Configuration for a prefix command. */
839
- interface PrefixCommandConfig<R = void> {
1445
+ interface PrefixCommandConfig<TArgs extends Record<string, unknown> = Record<string, never>, R = void> {
840
1446
  /** Primary command name (the word after the prefix). */
841
1447
  name: string;
842
1448
  /** Alternative names that also trigger the command. */
@@ -845,8 +1451,12 @@ interface PrefixCommandConfig<R = void> {
845
1451
  description?: string;
846
1452
  /** Rate-limit this command. A number is a duration in ms. */
847
1453
  cooldown?: CooldownInput;
848
- /** Handler invoked with a {@link PrefixContext}. */
849
- run: (ctx: PrefixContext) => Awaitable<R>;
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>;
850
1460
  }
851
1461
  /** A registrable prefix command. Build it with {@link prefixCommand}. */
852
1462
  interface PrefixCommand {
@@ -855,12 +1465,14 @@ interface PrefixCommand {
855
1465
  readonly aliases: readonly string[];
856
1466
  readonly description?: string;
857
1467
  readonly cooldown?: CooldownConfig;
1468
+ readonly guards?: readonly Guard[];
1469
+ readonly parser?: PrefixArgsParser<Record<string, unknown>>;
858
1470
  readonly run: (ctx: PrefixContext) => Promise<void>;
859
1471
  }
860
1472
  /** Define a prefix command. */
861
- declare function prefixCommand<R = void>(config: PrefixCommandConfig<R>): PrefixCommand;
1473
+ declare function prefixCommand<TArgs extends Record<string, unknown> = Record<string, never>, R = void>(config: PrefixCommandConfig<TArgs, R>): PrefixCommand;
862
1474
  /** The handler argument for a prefix command: the message plus parsed args. */
863
- declare class PrefixContext {
1475
+ declare class PrefixContext<TArgs extends Record<string, unknown> = Record<string, never>> {
864
1476
  /** The triggering message. */
865
1477
  readonly message: Message;
866
1478
  /** The matched command name (as typed). */
@@ -869,6 +1481,8 @@ declare class PrefixContext {
869
1481
  readonly args: string[];
870
1482
  /** The raw text after the command name. */
871
1483
  readonly rest: string;
1484
+ /** Typed parsed arguments from `args` schema, or `{}` if none. */
1485
+ readonly options: TArgs;
872
1486
  constructor(
873
1487
  /** The triggering message. */
874
1488
  message: Message,
@@ -877,13 +1491,15 @@ declare class PrefixContext {
877
1491
  /** Whitespace-split arguments after the command name. */
878
1492
  args: string[],
879
1493
  /** The raw text after the command name. */
880
- rest: string);
1494
+ rest: string,
1495
+ /** Typed parsed arguments from `args` schema, or `{}` if none. */
1496
+ options?: TArgs);
881
1497
  get client(): Message["client"];
882
1498
  get author(): discord_js.User;
883
1499
  get member(): discord_js.GuildMember | null;
884
1500
  get guild(): discord_js.Guild | null;
885
1501
  get guildId(): string | null;
886
- get channel(): discord_js.DMChannel | discord_js.PartialDMChannel | discord_js.PartialGroupDMChannel | discord_js.NewsChannel | discord_js.StageChannel | discord_js.TextChannel | discord_js.PublicThreadChannel<boolean> | discord_js.PrivateThreadChannel | discord_js.VoiceChannel;
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;
887
1503
  get channelId(): string;
888
1504
  /** Reply to the triggering message. */
889
1505
  reply(content: string | MessagePayload | MessageReplyOptions): Promise<Message>;
@@ -901,6 +1517,7 @@ declare class PrefixRegistry {
901
1517
  private cooldowns?;
902
1518
  private defaultCooldown?;
903
1519
  private errorHandler?;
1520
+ private defaultGuards;
904
1521
  private onUsage?;
905
1522
  /** Configure prefixes and matching behaviour. */
906
1523
  setOptions(input: string | readonly string[] | PrefixOptions): this;
@@ -910,6 +1527,8 @@ declare class PrefixRegistry {
910
1527
  setUsageHook(hook: (event: UsageEvent) => void): this;
911
1528
  /** Share a cooldown manager and an optional default cooldown. */
912
1529
  setCooldowns(manager: CooldownManager, defaultCooldown?: CooldownConfig): this;
1530
+ /** Guards that run before every prefix command's own guards. */
1531
+ setDefaultGuards(guards: readonly Guard[]): this;
913
1532
  /** Set the handler used when a prefix command throws. */
914
1533
  onError(handler: PrefixErrorHandler): this;
915
1534
  /** Register one or more prefix commands (and their aliases). */
@@ -931,7 +1550,7 @@ declare class PrefixRegistry {
931
1550
  }
932
1551
 
933
1552
  /** Anything that can be handed to {@link SpearClient.register}. */
934
- type Registerable = SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand;
1553
+ type Registerable = SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand | ContextMenuCommand;
935
1554
  /**
936
1555
  * Ready-made intent presets. Pass one to {@link SpearClient} as `intents`.
937
1556
  * `all` includes privileged intents — enable them in the developer portal.
@@ -964,19 +1583,24 @@ interface SpearOptions {
964
1583
  prefix?: string | readonly string[] | PrefixOptions;
965
1584
  /** Track command/component/prefix usage to a store and/or a Discord channel. */
966
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[];
967
1590
  }
968
1591
  /** Options for {@link SpearClient}: discord.js options plus {@link SpearOptions}. `intents` may be omitted. */
969
1592
  type SpearClientOptions = Partial<ClientOptions> & SpearOptions;
970
1593
  /**
971
- * A discord.js {@link Client} with batteries included: command, event and
972
- * component registries plus interaction routing wired up automatically.
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.
973
1597
  *
974
1598
  * @example
975
1599
  * ```ts
976
1600
  * const client = new SpearClient({ intents: Intents.default });
977
1601
  * client.register(ping, onReady, voteButton);
978
1602
  * await client.start(process.env.TOKEN);
979
- * await client.deployCommands({ guildId: "123" });
1603
+ * await client.deployAllCommands({ guildId: "123" });
980
1604
  * ```
981
1605
  */
982
1606
  declare class SpearClient extends Client {
@@ -996,11 +1620,15 @@ declare class SpearClient extends Client {
996
1620
  readonly prefix: PrefixRegistry;
997
1621
  /** Usage tracker: records who used what to a store and/or a Discord channel. */
998
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;
999
1627
  private readonly envConfig;
1000
1628
  constructor(options?: SpearClientOptions);
1001
1629
  /**
1002
- * Register commands, events and components in one call. Each item is routed
1003
- * to the matching registry based on its kind.
1630
+ * Register commands, events, components, scheduled tasks, prefix commands
1631
+ * and context menus in one call. Each item is routed to its matching registry.
1004
1632
  */
1005
1633
  register(...items: Registerable[]): this;
1006
1634
  /** Install one or more plugins, running each plugin's `setup`. */
@@ -1016,111 +1644,339 @@ declare class SpearClient extends Client {
1016
1644
  */
1017
1645
  start(token?: string): Promise<this>;
1018
1646
  /**
1019
- * Push the registered slash commands to discord using the client's own
1020
- * authenticated REST connection. Call after the client is ready.
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.
1021
1650
  */
1022
1651
  deployCommands(options?: {
1023
1652
  guildId?: string;
1024
1653
  }): Promise<DeployResult>;
1025
- private route;
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
+ }>;
1026
1678
  /** Define and register a scheduled task in one call. */
1027
1679
  schedule(config: TaskConfig): ScheduledTask;
1028
1680
  /** Stop the scheduler, then tear down the discord.js client. */
1029
1681
  destroy(): Promise<void>;
1682
+ private route;
1030
1683
  }
1031
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
+ }
1032
1733
  /**
1033
- * Scheduled tasks: run work on a cron schedule or a fixed interval.
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.
1034
1863
  *
1035
- * Dependency-free. Includes a standard 5-field cron parser (`*`, ranges,
1036
- * lists, steps, `@daily` style aliases) evaluated in local time, plus a
1037
- * {@link TaskScheduler} that the client starts on ready and stops on destroy.
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.
1038
1869
  */
1039
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
+ }
1040
1900
  /**
1041
- * A parsed cron expression. Evaluates in the host's local time.
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).
1042
1922
  *
1043
1923
  * @example
1044
1924
  * ```ts
1045
- * cron("*\u200b/5 * * * *").next(); // next 5-minute boundary
1046
- * cron("@daily").next(new Date()); // next midnight
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.");
1047
1932
  * ```
1048
1933
  */
1049
- declare class CronExpression {
1050
- /** The original expression string. */
1051
- readonly source: string;
1052
- private readonly minutes;
1053
- private readonly hours;
1054
- private readonly daysOfMonth;
1055
- private readonly months;
1056
- private readonly daysOfWeek;
1057
- private readonly domRestricted;
1058
- private readonly dowRestricted;
1059
- constructor(expression: string);
1060
- private dayMatches;
1061
- /** The next time strictly after `from` (default now) that matches. */
1062
- next(from?: Date): Date;
1063
- }
1064
- /** Compile a cron expression. Throws on malformed input. */
1065
- declare function cron(expression: string): CronExpression;
1066
- /** Configuration for a scheduled task. Provide exactly one of `cron`/`interval`. */
1067
- interface TaskConfig {
1068
- /** Unique task name. */
1069
- name: string;
1070
- /** A cron expression (local time). */
1071
- cron?: string;
1072
- /** A fixed interval in milliseconds. */
1073
- interval?: number;
1074
- /** Also run once immediately when the scheduler starts. Default `false`. */
1075
- runOnStart?: boolean;
1076
- /** The work to perform. */
1077
- run: (client: SpearClient) => Awaitable<void>;
1078
- }
1079
- /** A compiled, registrable scheduled task. Build it with {@link task}. */
1080
- interface ScheduledTask {
1081
- readonly kind: "task";
1082
- readonly name: string;
1083
- readonly interval?: number;
1084
- readonly cron?: CronExpression;
1085
- readonly runOnStart: boolean;
1086
- readonly run: (client: SpearClient) => Awaitable<void>;
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;
1087
1971
  }
1088
- /** Define a scheduled task. Throws if neither `cron` nor `interval` is given. */
1089
- declare function task(config: TaskConfig): ScheduledTask;
1090
1972
  /**
1091
- * Runs {@link ScheduledTask}s. The client owns one as `client.scheduler`,
1092
- * starts it on `clientReady` and stops it on `destroy`. Tasks added while
1093
- * running are scheduled immediately.
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? }`.
1094
1978
  */
1095
- declare class TaskScheduler {
1096
- private readonly tasks;
1097
- private readonly timers;
1098
- private running;
1099
- private client?;
1100
- private logger?;
1101
- /** Number of registered tasks. */
1102
- get size(): number;
1103
- /** Whether the scheduler is currently running. */
1104
- get active(): boolean;
1105
- /** Every registered task. */
1106
- list(): ScheduledTask[];
1107
- /** Attach a logger for task error reporting. */
1108
- setLogger(logger: Logger): this;
1109
- /** Register one or more tasks. If already running, they are scheduled now. */
1110
- add(...tasks: ScheduledTask[]): this;
1111
- /** Remove a task and cancel its timer. */
1112
- remove(name: string): boolean;
1113
- /** Start every task. Safe to call once; later calls are ignored. */
1114
- start(client: SpearClient): void;
1115
- /** Stop the scheduler and cancel every pending timer. */
1116
- stop(): void;
1117
- private cancel;
1118
- private begin;
1119
- private delayFor;
1120
- private scheduleNext;
1121
- private arm;
1122
- private runTask;
1123
- }
1979
+ declare function confirm(interaction: RepliableInteraction, options: ConfirmOptions): Promise<ConfirmResult>;
1124
1980
 
1125
1981
  /**
1126
1982
  * Typed custom-id codec.
@@ -1244,6 +2100,8 @@ interface ButtonConfig<P extends string, R> {
1244
2100
  style?: ButtonStyleInput;
1245
2101
  emoji?: ComponentEmojiResolvable;
1246
2102
  disabled?: boolean;
2103
+ /** Preconditions evaluated before the handler runs. */
2104
+ guards?: readonly Guard[];
1247
2105
  run: (ctx: ButtonContext<Params<P>>) => Awaitable<R>;
1248
2106
  }
1249
2107
  /** A registrable button with a typed {@link build}. */
@@ -1280,6 +2138,8 @@ interface SelectConfigBase {
1280
2138
  minValues?: number;
1281
2139
  maxValues?: number;
1282
2140
  disabled?: boolean;
2141
+ /** Preconditions evaluated before the handler runs. */
2142
+ guards?: readonly Guard[];
1283
2143
  }
1284
2144
  /** Config for a string select created with {@link stringSelect}. */
1285
2145
  interface StringSelectConfig<P extends string, R> extends SelectConfigBase {
@@ -1357,6 +2217,8 @@ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>,
1357
2217
  id: P;
1358
2218
  title: string;
1359
2219
  fields: F;
2220
+ /** Preconditions evaluated before the handler runs. */
2221
+ guards?: readonly Guard[];
1360
2222
  run: (ctx: ModalContext<Params<P>, keyof F & string>) => Awaitable<R>;
1361
2223
  }
1362
2224
  /** A registrable modal with a typed {@link build}. */
@@ -1393,4 +2255,4 @@ declare function modal<const P extends string, F extends Record<string, TextInpu
1393
2255
  */
1394
2256
  declare function row<C extends MessageActionRowComponentBuilder>(...components: C[]): ActionRowBuilder<C>;
1395
2257
 
1396
- 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 CooldownActor, type CooldownConfig, type CooldownExemptions, type CooldownInput, CooldownManager, type CooldownOverrides, type CooldownResult, type CooldownScope, CronExpression, type DeployOptions, type DeployResult, type EntitySelectConfig, type EnvReader, type EventConfig, type EventDef, type EventHandler, EventRegistry, Intents, JsonFileUsageStore, type LinkButtonConfig, type LoadEnvOptions, type LoadOptions, type LogEntry, type LogLevel, type LogOptions, type LogSink, type LogThreshold, type LogValue, Logger, type LoggerOptions, MAX_CUSTOM_ID_LENGTH, MemoryUsageStore, 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 ParsedEnv, type PrefixCommand, type PrefixCommandConfig, PrefixContext, type PrefixErrorHandler, type PrefixOptions, PrefixRegistry, type Registerable, type ReplyData, type ReplyInput, type ResolvedOption, type ResolvedOptions, type RoleSelect, RoleSelectContext, type RoleSelectRoute, 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 UsageOptions, type UsageStore, UsageTracker, type UsageType, type UserSelect, UserSelectContext, type UserSelectRoute, asEphemeral, buildCustomId, button, channelSelect, collectModules, command, commandGroup, compilePattern, consoleSink, cron, definePlugin, effectiveDuration, env, event, formatCooldownMessage, formatUsage, linkButton, loadEnv, loadInto, mentionableSelect, modal, normalizeCooldown, normalizeReply, option, optionsHaveAutocomplete, paramsFromValues, parseCustomId, parseEnv, prefixCommand, readOption, roleSelect, row, stringSelect, subcommand, subcommandGroup, task, textInput, toAPIOption, toError, 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 };