@julanzw/ttoolbox-discordjs-framework 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +675 -0
  2. package/README.md +77 -0
  3. package/dist/classes/Command.class.d.ts +169 -0
  4. package/dist/classes/Command.class.js +156 -0
  5. package/dist/classes/CommandManager.class.d.ts +69 -0
  6. package/dist/classes/CommandManager.class.js +149 -0
  7. package/dist/classes/DiscordHandler.class.d.ts +241 -0
  8. package/dist/classes/DiscordHandler.class.js +222 -0
  9. package/dist/classes/InteractionError.class.d.ts +8 -0
  10. package/dist/classes/InteractionError.class.js +11 -0
  11. package/dist/classes/ModalManager.class.d.ts +154 -0
  12. package/dist/classes/ModalManager.class.js +205 -0
  13. package/dist/classes/SubcommandGroup.class.d.ts +127 -0
  14. package/dist/classes/SubcommandGroup.class.js +156 -0
  15. package/dist/index.d.ts +18 -0
  16. package/dist/index.js +17 -0
  17. package/dist/types/button.d.ts +2 -0
  18. package/dist/types/button.js +1 -0
  19. package/dist/types/channel.d.ts +2 -0
  20. package/dist/types/channel.js +1 -0
  21. package/dist/types/logger.d.ts +37 -0
  22. package/dist/types/logger.js +1 -0
  23. package/dist/types/modal.d.ts +77 -0
  24. package/dist/types/modal.js +1 -0
  25. package/dist/types/permission.d.ts +2 -0
  26. package/dist/types/permission.js +1 -0
  27. package/dist/utils/PaginatedEmbed.class.d.ts +25 -0
  28. package/dist/utils/PaginatedEmbed.class.js +102 -0
  29. package/dist/utils/TToolboxLogger.class.d.ts +176 -0
  30. package/dist/utils/TToolboxLogger.class.js +252 -0
  31. package/dist/utils/cooldown.d.ts +13 -0
  32. package/dist/utils/cooldown.js +43 -0
  33. package/dist/utils/editAndReply.d.ts +37 -0
  34. package/dist/utils/editAndReply.js +85 -0
  35. package/dist/utils/embeds.d.ts +55 -0
  36. package/dist/utils/embeds.js +94 -0
  37. package/dist/utils/formatting.d.ts +44 -0
  38. package/dist/utils/formatting.js +87 -0
  39. package/dist/utils/miliseconds.d.ts +10 -0
  40. package/dist/utils/miliseconds.js +11 -0
  41. package/dist/utils/permissions.d.ts +8 -0
  42. package/dist/utils/permissions.js +24 -0
  43. package/dist/utils/slashCommandOptions.d.ts +8 -0
  44. package/dist/utils/slashCommandOptions.js +11 -0
  45. package/package.json +50 -0
@@ -0,0 +1,85 @@
1
+ import { MessageFlags, } from 'discord.js';
2
+ import { InteractionError } from '../classes/InteractionError.class.js';
3
+ /**
4
+ * Safely replies to an interaction, handling deferred/replied states.
5
+ *
6
+ * @param interaction - The interaction to reply to
7
+ * @param content - The message content
8
+ * @param ephemeral - Whether the reply should be ephemeral
9
+ * @param embeds - Optional embeds to include
10
+ * @param components - Optional components to include
11
+ * @param files - Optional files to attach
12
+ * @returns The message that was sent
13
+ * @throws {InteractionError} If the interaction is too old or fails
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * try {
18
+ * await safeReply(interaction, 'Hello!', true);
19
+ * } catch (err) {
20
+ * if (err instanceof InteractionError && err.reason === 'expired') {
21
+ * this.logger?.warn('Interaction expired', 'command');
22
+ * }
23
+ * }
24
+ * ```
25
+ */
26
+ export async function safeReply(interaction, content, ephemeral = false, embeds, components, files) {
27
+ // Check if interaction is too old
28
+ const now = Date.now();
29
+ const threeMinutes = 3 * 60 * 1000;
30
+ if (now - interaction.createdTimestamp > threeMinutes) {
31
+ throw new InteractionError('Interaction is older than 3 minutes', interaction.id, 'expired');
32
+ }
33
+ const payload = {
34
+ ...(content ? { content } : {}),
35
+ ...(ephemeral ? { flags: MessageFlags.Ephemeral } : {}),
36
+ ...(embeds ? { embeds } : {}),
37
+ ...(components ? { components } : {}),
38
+ ...(files ? { files } : {}),
39
+ };
40
+ try {
41
+ if (!interaction.replied && !interaction.deferred) {
42
+ await interaction.reply(payload);
43
+ return await interaction.fetchReply();
44
+ }
45
+ else {
46
+ return await interaction.followUp(payload);
47
+ }
48
+ }
49
+ catch (err) {
50
+ throw new InteractionError(
51
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
52
+ `Failed to reply to interaction: ${err.message}`, interaction.id, 'failed');
53
+ }
54
+ }
55
+ /**
56
+ * Safely edits an interaction reply.
57
+ *
58
+ * @param interaction - The interaction to edit
59
+ * @param content - The new message content
60
+ * @param embeds - Optional embeds to include
61
+ * @param components - Optional components to include
62
+ * @returns The edited message
63
+ * @throws {InteractionError} If the interaction is too old or fails
64
+ */
65
+ export async function safeEdit(interaction, content, embeds, components) {
66
+ // Check if interaction is too old
67
+ const now = Date.now();
68
+ const threeMinutes = 3 * 60 * 1000;
69
+ if (now - interaction.createdTimestamp > threeMinutes) {
70
+ throw new InteractionError('Interaction is older than 3 minutes', interaction.id, 'expired');
71
+ }
72
+ const editPayload = {
73
+ ...(content ? { content } : {}),
74
+ ...(embeds ? { embeds } : {}),
75
+ ...(components ? { components } : {}),
76
+ };
77
+ try {
78
+ return await interaction.editReply(editPayload);
79
+ }
80
+ catch (err) {
81
+ throw new InteractionError(
82
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
83
+ `Failed to edit interaction: ${err.message}`, interaction.id, 'failed');
84
+ }
85
+ }
@@ -0,0 +1,55 @@
1
+ import { APIEmbedField, ColorResolvable, EmbedBuilder, ButtonStyle, ButtonBuilder, ActionRowBuilder } from 'discord.js';
2
+ import { PaginationButtonLocation } from '../types/button.js';
3
+ /**
4
+ * Util function for building an embed
5
+ *
6
+ * @param title - The title of the embed
7
+ * @param fields - The fields of the embed
8
+ * @param description - The description of the embed, optional
9
+ * @param footer - The footer of the embed, optional
10
+ * @param timestamp - If the embed should have a timestamp, defaults to false
11
+ * @param color - The color of the embed, defaults to STANDARD_COLOR
12
+ * @param customize - A function to customize the embed further, defaults to no customization
13
+ *
14
+ * @returns An EmbedBuilder instance with the specified properties
15
+ */
16
+ export declare function embedBuilder({ title, fields, description, footer, timestamp, color, customize, }: {
17
+ title: string;
18
+ fields?: APIEmbedField[];
19
+ description?: string;
20
+ footer?: string;
21
+ timestamp?: boolean;
22
+ color?: ColorResolvable;
23
+ customize?: (embed: EmbedBuilder) => EmbedBuilder;
24
+ }): EmbedBuilder;
25
+ /**
26
+ * Creates a single button based on its type and config.
27
+ *
28
+ * @param type - The button type of the button (prev, next, edit, delete, etc)
29
+ * @param actionId - The base action ID for the button
30
+ * @param disabled - Whether the button should be disabled, defaults to false
31
+ * @param label - Optional label for the button, defaults to type-based label
32
+ * @param style - Optional style for the button, defaults to secondary
33
+ *
34
+ * @return A ButtonBuilder instance configured with the specified properties
35
+ */
36
+ export declare function createButton({ type, disabled, label, style, customId, }: {
37
+ type: string;
38
+ disabled?: boolean;
39
+ label?: string;
40
+ style?: ButtonStyle;
41
+ customId?: string;
42
+ }): ButtonBuilder;
43
+ /**
44
+ * Creates an `ButtonBuilder[]` with a prev and next button (in that order).
45
+ *
46
+ * @param index - The current index of the item being paginated
47
+ * @param total - The total number of pages
48
+ *
49
+ * @returns An `ButtonBuilder[]` containing the buttons
50
+ */
51
+ export declare function createPaginationButtons(index: number, total: number): ButtonBuilder[];
52
+ export declare function createButtonsRow(normalButtons: ButtonBuilder[], pagination?: {
53
+ buttons: ButtonBuilder[];
54
+ location: PaginationButtonLocation;
55
+ }): ActionRowBuilder<ButtonBuilder>;
@@ -0,0 +1,94 @@
1
+ import { EmbedBuilder, ButtonStyle, ButtonBuilder, ActionRowBuilder, } from 'discord.js';
2
+ /**
3
+ * Util function for building an embed
4
+ *
5
+ * @param title - The title of the embed
6
+ * @param fields - The fields of the embed
7
+ * @param description - The description of the embed, optional
8
+ * @param footer - The footer of the embed, optional
9
+ * @param timestamp - If the embed should have a timestamp, defaults to false
10
+ * @param color - The color of the embed, defaults to STANDARD_COLOR
11
+ * @param customize - A function to customize the embed further, defaults to no customization
12
+ *
13
+ * @returns An EmbedBuilder instance with the specified properties
14
+ */
15
+ export function embedBuilder({ title, fields, description, footer, timestamp = false, color = '#3F48CC', customize = (e) => e, }) {
16
+ let embed = new EmbedBuilder().setTitle(title).setColor(color);
17
+ if (fields && fields.length > 0)
18
+ embed = embed.setFields(fields);
19
+ if (description)
20
+ embed = embed.setDescription(description);
21
+ if (footer)
22
+ embed = embed.setFooter({ text: footer });
23
+ if (timestamp)
24
+ embed = embed.setTimestamp();
25
+ return customize(embed);
26
+ }
27
+ /**
28
+ * Creates a single button based on its type and config.
29
+ *
30
+ * @param type - The button type of the button (prev, next, edit, delete, etc)
31
+ * @param actionId - The base action ID for the button
32
+ * @param disabled - Whether the button should be disabled, defaults to false
33
+ * @param label - Optional label for the button, defaults to type-based label
34
+ * @param style - Optional style for the button, defaults to secondary
35
+ *
36
+ * @return A ButtonBuilder instance configured with the specified properties
37
+ */
38
+ export function createButton({ type, disabled = false, label, style, customId, }) {
39
+ const button = new ButtonBuilder()
40
+ .setCustomId(customId ?? `${type}`)
41
+ .setDisabled(disabled);
42
+ switch (type) {
43
+ case 'prev':
44
+ return button
45
+ .setLabel(label ?? 'Previous')
46
+ .setStyle(ButtonStyle.Secondary);
47
+ case 'next':
48
+ return button.setLabel(label ?? 'Next').setStyle(ButtonStyle.Secondary);
49
+ case 'edit':
50
+ return button.setLabel(label ?? 'Edit').setStyle(ButtonStyle.Primary);
51
+ case 'delete':
52
+ return button.setLabel(label ?? 'Delete').setStyle(ButtonStyle.Danger);
53
+ default:
54
+ return button
55
+ .setLabel(label ?? 'Unknown')
56
+ .setStyle(style ?? ButtonStyle.Secondary);
57
+ }
58
+ }
59
+ /**
60
+ * Creates an `ButtonBuilder[]` with a prev and next button (in that order).
61
+ *
62
+ * @param index - The current index of the item being paginated
63
+ * @param total - The total number of pages
64
+ *
65
+ * @returns An `ButtonBuilder[]` containing the buttons
66
+ */
67
+ export function createPaginationButtons(index, total) {
68
+ const buttons = [
69
+ createButton({
70
+ type: 'prev',
71
+ disabled: index === 0,
72
+ }),
73
+ createButton({
74
+ type: 'next',
75
+ disabled: index === total - 1,
76
+ }),
77
+ ];
78
+ return buttons;
79
+ }
80
+ export function createButtonsRow(normalButtons, pagination) {
81
+ if (pagination && pagination.buttons.length === 2) {
82
+ switch (pagination.location) {
83
+ case 'embrace':
84
+ return new ActionRowBuilder().addComponents(pagination.buttons[0], ...normalButtons, pagination.buttons[1]);
85
+ case 'start':
86
+ return new ActionRowBuilder().addComponents(...pagination.buttons, ...normalButtons);
87
+ case 'end':
88
+ return new ActionRowBuilder().addComponents(...normalButtons, ...pagination.buttons);
89
+ }
90
+ }
91
+ else {
92
+ return new ActionRowBuilder().addComponents(...normalButtons);
93
+ }
94
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Util function for formatting a `Date` like 2000-01-01 into January 1st
3
+ *
4
+ * @param date - the date that needs formatting
5
+ * @returns the formatted date
6
+ */
7
+ export declare function formatDateToString(date: Date): string;
8
+ /**
9
+ * Util function to get the suffix of a number, e.g. 1st, 2nd, 3rd, 4th, etc.
10
+ *
11
+ * @param number - the number you want the suffix of
12
+ * @returns the suffix of the number
13
+ */
14
+ export declare function getDaySuffix(number: number): "th" | "st" | "nd" | "rd";
15
+ /**
16
+ * Util function to format a date into a string with the DD-MM-YYYY format
17
+ *
18
+ * @param date - The date to format
19
+ * @returns A formatted string in the format DD-MM-YYYY
20
+ */
21
+ export declare function formatDateToDDMMYYYY(date: Date): string;
22
+ /**
23
+ * Util function to format a date into a string with the YYYY-MM-DD HH:MM:SS format
24
+ * @param date - The date to format
25
+ * @returns A formatted string in YYYY-MM-DD HH:MM:SS format
26
+ */
27
+ export declare function formatDateToYYYYMMDDHHMMSS(date: Date): string;
28
+ /**
29
+ * Util function to format the first letter of a string
30
+ * @param str - input string
31
+ * @returns The formatted string
32
+ */
33
+ export declare function capitalizeFirst(input: string): string;
34
+ /**
35
+ * Formats a duration in milliseconds into a human-readable string.
36
+ * Examples:
37
+ * - 4200 -> "4s"
38
+ * - 65000 -> "1m 5s"
39
+ * - 3723000 -> "1h 2m 3s"
40
+ *
41
+ * @param ms Duration in milliseconds
42
+ * @returns A formatted string
43
+ */
44
+ export declare function formatDuration(ms: number): string;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Util function for formatting a `Date` like 2000-01-01 into January 1st
3
+ *
4
+ * @param date - the date that needs formatting
5
+ * @returns the formatted date
6
+ */
7
+ export function formatDateToString(date) {
8
+ const daySuffix = getDaySuffix(date.getDate());
9
+ return (date.toLocaleDateString('en-US', { month: 'long', day: 'numeric' }) +
10
+ daySuffix);
11
+ }
12
+ /**
13
+ * Util function to get the suffix of a number, e.g. 1st, 2nd, 3rd, 4th, etc.
14
+ *
15
+ * @param number - the number you want the suffix of
16
+ * @returns the suffix of the number
17
+ */
18
+ export function getDaySuffix(number) {
19
+ if (number > 3 && number < 21)
20
+ return 'th';
21
+ switch (number % 10) {
22
+ case 1:
23
+ return 'st';
24
+ case 2:
25
+ return 'nd';
26
+ case 3:
27
+ return 'rd';
28
+ default:
29
+ return 'th';
30
+ }
31
+ }
32
+ /**
33
+ * Util function to format a date into a string with the DD-MM-YYYY format
34
+ *
35
+ * @param date - The date to format
36
+ * @returns A formatted string in the format DD-MM-YYYY
37
+ */
38
+ export function formatDateToDDMMYYYY(date) {
39
+ const day = String(date.getDate()).padStart(2, '0');
40
+ const month = String(date.getMonth() + 1).padStart(2, '0');
41
+ const year = date.getFullYear();
42
+ return `${day}-${month}-${year}`;
43
+ }
44
+ /**
45
+ * Util function to format a date into a string with the YYYY-MM-DD HH:MM:SS format
46
+ * @param date - The date to format
47
+ * @returns A formatted string in YYYY-MM-DD HH:MM:SS format
48
+ */
49
+ export function formatDateToYYYYMMDDHHMMSS(date) {
50
+ return date.toISOString().replace('T', ' ').slice(0, 19);
51
+ }
52
+ /**
53
+ * Util function to format the first letter of a string
54
+ * @param str - input string
55
+ * @returns The formatted string
56
+ */
57
+ export function capitalizeFirst(input) {
58
+ if (!input)
59
+ return '';
60
+ const lower = input.toLowerCase();
61
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
62
+ }
63
+ /**
64
+ * Formats a duration in milliseconds into a human-readable string.
65
+ * Examples:
66
+ * - 4200 -> "4s"
67
+ * - 65000 -> "1m 5s"
68
+ * - 3723000 -> "1h 2m 3s"
69
+ *
70
+ * @param ms Duration in milliseconds
71
+ * @returns A formatted string
72
+ */
73
+ export function formatDuration(ms) {
74
+ let seconds = Math.floor(ms / 1000);
75
+ const hours = Math.floor(seconds / 3600);
76
+ seconds %= 3600;
77
+ const minutes = Math.floor(seconds / 60);
78
+ seconds %= 60;
79
+ const parts = [];
80
+ if (hours > 0)
81
+ parts.push(`${hours}h`);
82
+ if (minutes > 0)
83
+ parts.push(`${minutes}m`);
84
+ if (seconds > 0 || parts.length === 0)
85
+ parts.push(`${seconds}s`);
86
+ return parts.join(' ');
87
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Common time durations in milliseconds.
3
+ */
4
+ export declare enum TIMES_MILISECONDS {
5
+ SECOND = 1000,
6
+ MINUTE = 60000,
7
+ TEN_MINUTES = 600000,
8
+ HOUR = 6000000,
9
+ DAY = 14400000
10
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Common time durations in milliseconds.
3
+ */
4
+ export var TIMES_MILISECONDS;
5
+ (function (TIMES_MILISECONDS) {
6
+ TIMES_MILISECONDS[TIMES_MILISECONDS["SECOND"] = 1000] = "SECOND";
7
+ TIMES_MILISECONDS[TIMES_MILISECONDS["MINUTE"] = 60000] = "MINUTE";
8
+ TIMES_MILISECONDS[TIMES_MILISECONDS["TEN_MINUTES"] = 600000] = "TEN_MINUTES";
9
+ TIMES_MILISECONDS[TIMES_MILISECONDS["HOUR"] = 6000000] = "HOUR";
10
+ TIMES_MILISECONDS[TIMES_MILISECONDS["DAY"] = 14400000] = "DAY";
11
+ })(TIMES_MILISECONDS || (TIMES_MILISECONDS = {}));
@@ -0,0 +1,8 @@
1
+ import { PermissionLevel } from '../types/permission.js';
2
+ /**
3
+ * Utility function to return the proper permission bits
4
+ *
5
+ * @param level - the PermissionLevel the bits need to be returned of. Can be an array
6
+ * @returns the permission's bit value or null for unrestricted
7
+ */
8
+ export declare function getPermissionsForLevel(level: PermissionLevel): bigint | null;
@@ -0,0 +1,24 @@
1
+ import { PermissionFlagsBits, PermissionsBitField } from 'discord.js';
2
+ /**
3
+ * Utility function to return the proper permission bits
4
+ *
5
+ * @param level - the PermissionLevel the bits need to be returned of. Can be an array
6
+ * @returns the permission's bit value or null for unrestricted
7
+ */
8
+ export function getPermissionsForLevel(level) {
9
+ if (level === 'admin') {
10
+ return PermissionFlagsBits.Administrator;
11
+ }
12
+ if (level === 'owner' || level === 'disabled') {
13
+ // disable the command by default
14
+ return BigInt(0);
15
+ }
16
+ if (typeof level === 'bigint' || typeof level === 'number') {
17
+ return BigInt(level);
18
+ }
19
+ if (Array.isArray(level)) {
20
+ return new PermissionsBitField(level).bitfield;
21
+ }
22
+ // unrestricted
23
+ return null;
24
+ }
@@ -0,0 +1,8 @@
1
+ import { SlashCommandBooleanOption, SlashCommandChannelOption, SlashCommandIntegerOption, SlashCommandRoleOption, SlashCommandStringOption, SlashCommandUserOption } from 'discord.js';
2
+ import { AllowedChannelTypeChannelOption } from '../types/channel.js';
3
+ export declare const userOption: (name: string, desc: string, required?: boolean) => (opt: SlashCommandUserOption) => SlashCommandUserOption;
4
+ export declare const integerOption: (name: string, desc: string, required?: boolean) => (opt: SlashCommandIntegerOption) => SlashCommandIntegerOption;
5
+ export declare const stringOption: (name: string, desc: string, required?: boolean) => (opt: SlashCommandStringOption) => SlashCommandStringOption;
6
+ export declare const channelOption: (name: string, desc: string, required?: boolean, channelType?: AllowedChannelTypeChannelOption | AllowedChannelTypeChannelOption[]) => (opt: SlashCommandChannelOption) => SlashCommandChannelOption;
7
+ export declare const roleOption: (name: string, desc: string, required?: boolean) => (opt: SlashCommandRoleOption) => SlashCommandRoleOption;
8
+ export declare const booleanOption: (name: string, desc: string, required?: boolean) => (opt: SlashCommandBooleanOption) => SlashCommandBooleanOption;
@@ -0,0 +1,11 @@
1
+ import { ChannelType, } from 'discord.js';
2
+ export const userOption = (name, desc, required = true) => (opt) => opt.setName(name).setDescription(desc).setRequired(required);
3
+ export const integerOption = (name, desc, required = true) => (opt) => opt.setName(name).setDescription(desc).setRequired(required);
4
+ export const stringOption = (name, desc, required = true) => (opt) => opt.setName(name).setDescription(desc).setRequired(required);
5
+ export const channelOption = (name, desc, required = true, channelType = [ChannelType.GuildText]) => (opt) => opt
6
+ .setName(name)
7
+ .setDescription(desc)
8
+ .setRequired(required)
9
+ .addChannelTypes(...(Array.isArray(channelType) ? channelType : [channelType]));
10
+ export const roleOption = (name, desc, required = true) => (opt) => opt.setName(name).setDescription(desc).setRequired(required);
11
+ export const booleanOption = (name, desc, required = true) => (opt) => opt.setName(name).setDescription(desc).setRequired(required);
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@julanzw/ttoolbox-discordjs-framework",
3
+ "version": "1.0.1",
4
+ "description": "A Discord.js command framework with built-in handlers and utilities",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build",
16
+ "dev": "tsc --watch"
17
+ },
18
+ "keywords": [
19
+ "discord",
20
+ "discord.js",
21
+ "bot",
22
+ "framework",
23
+ "commands",
24
+ "typescript"
25
+ ],
26
+ "author": "JulanZw",
27
+ "license": "AGPL-3.0",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/JulanZw/ttoolbox-discordjs-framework.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/JulanZw/ttoolbox-discordjs-framework/issues"
34
+ },
35
+ "homepage": "https://github.com/JulanZw/ttoolbox-discordjs-framework#readme",
36
+ "peerDependencies": {
37
+ "discord.js": "^14.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@prisma/client": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@prisma/client": "^6.11.1",
46
+ "@types/node": "^24.0.14",
47
+ "discord.js": "^14.21.0",
48
+ "typescript": "^5.8.3"
49
+ }
50
+ }