@stonyx/discord 0.1.1-beta.13 → 0.1.1-beta.131

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/README.md CHANGED
@@ -68,20 +68,13 @@ export default class MessageCreateHandler extends EventHandler {
68
68
 
69
69
  ### 4. Start the bot
70
70
 
71
- With Stonyx auto-initialization (recommended):
71
+ Stonyx auto-initializes the bot when your app boots — no manual wiring needed:
72
72
 
73
73
  ```bash
74
74
  stonyx serve
75
75
  ```
76
76
 
77
- Or manually:
78
-
79
- ```javascript
80
- import { DiscordBot } from '@stonyx/discord';
81
-
82
- const bot = new DiscordBot();
83
- await bot.init();
84
- ```
77
+ `Discord.init()` (called by the Stonyx module loader) awaits `new DiscordBot().init()`, which discovers commands/events, derives intents, and connects to the gateway. The lazy-init guards still apply: if `DISCORD_TOKEN` is unset or no commands/events exist, the bot skips login.
85
78
 
86
79
  ## Command Architecture
87
80
 
@@ -185,7 +178,16 @@ All methods are on the `DiscordBot` instance (accessible via `this._bot` in comm
185
178
 
186
179
  ### sendMessage(content, channelId, imagePath?)
187
180
 
188
- Send a text message to a channel. Optionally attach an image file.
181
+ Send a text message to a channel with auto-chunking. Messages over 2000 characters are split at newline/space boundaries and sent sequentially. Optionally attach an image file (attached to the first chunk only).
182
+
183
+ Messages are delivered reliably across connection state transitions:
184
+ - If the bot is **ready**, the message is sent immediately.
185
+ - If the bot is **connecting or disconnected**, the message is queued and auto-delivered when the connection resumes.
186
+ - **Transient failures** (5xx, network errors) are retried up to 3 times with exponential backoff.
187
+ - Queued messages expire after **5 minutes** (TTL) — stale messages are rejected rather than sent late.
188
+ - The queue holds up to **1000** messages; excess messages are rejected with a "queue full" error.
189
+
190
+ The returned promise resolves when the message is actually delivered or rejects on permanent failure.
189
191
 
190
192
  ```javascript
191
193
  await bot.sendMessage('Hello world', '123456789');
@@ -248,10 +250,10 @@ const guild = await bot.getGuild();
248
250
 
249
251
  ### clearChannelMessages(channelId)
250
252
 
251
- Delete all fetched messages in a channel (up to `maxMessagesPerRequest`).
253
+ Delete all fetched messages in a channel (up to `maxMessagesPerRequest`). Awaits every delete via `Promise.allSettled` and resolves with a structured `{ deleted, failed, errors }` so callers can act on partial failure. Never throws on individual delete failures; emits a single `log.warn` summary when any deletes fail.
252
254
 
253
255
  ```javascript
254
- await bot.clearChannelMessages('123456789');
256
+ const { deleted, failed, errors } = await bot.clearChannelMessages('123456789');
255
257
  ```
256
258
 
257
259
  ### giveRole(memberId, roleId)
@@ -296,18 +298,18 @@ Configuration is read from `stonyx/config` under `discord`:
296
298
  | `commands` | `Object` — registered command instances keyed by name |
297
299
  | `eventHandlers` | `Array` — registered event handler instances |
298
300
  | `client` | Discord.js `Client` instance (after init) |
299
- | `ready` | `Promise` — resolves when the bot is connected and ready |
300
- | `sendMessage(content, channelId, imagePath?)` | Send a message to a channel |
301
+ | `ready` | `Promise` — resolves when the bot is connected and ready; re-pends on gateway disconnects |
302
+ | `sendMessage(content, channelId, imagePath?)` | Send a message to a channel with auto-chunking (queues during disconnections, retries transient failures) |
301
303
  | `sendFile(file, messageObject)` | Replace a message with a file attachment |
302
304
  | `reply(interaction, content)` | Reply with auto-chunking |
303
305
  | `updateStatus(name, type?)` | Set bot presence |
304
306
  | `getChannelMessages(channelId, options?)` | Fetch channel messages |
305
307
  | `getChannelMessage(channelId, messageId)` | Fetch a single message |
306
308
  | `getGuild(guildId?)` | Fetch a guild |
307
- | `clearChannelMessages(channelId)` | Delete messages in a channel |
309
+ | `clearChannelMessages(channelId)` | Delete messages in a channel; returns `{ deleted, failed, errors }` and never throws on partial failure |
308
310
  | `giveRole(memberId, roleId)` | Add a role to a member |
309
- | `close()` | Destroy the Discord client and clear singleton |
310
- | `reset()` | Close + clear all commands and handlers |
311
+ | `close()` | Destroy the Discord client, reject pending queue, clear singleton |
312
+ | `reset()` | Close + clear all commands, handlers, and connection state |
311
313
 
312
314
  ### Command
313
315
 
package/dist/base.d.ts CHANGED
@@ -1 +1 @@
1
- export { SlashCommandBuilder, MessageFlags, PermissionFlagsBits, GatewayIntentBits, AttachmentBuilder, Client, Partials } from 'discord.js';
1
+ export { SlashCommandBuilder, MessageFlags, PermissionFlagsBits, GatewayIntentBits, AttachmentBuilder, Client, Partials, REST, Routes } from 'discord.js';
package/dist/base.js CHANGED
@@ -1 +1 @@
1
- export { SlashCommandBuilder, MessageFlags, PermissionFlagsBits, GatewayIntentBits, AttachmentBuilder, Client, Partials } from 'discord.js';
1
+ export { SlashCommandBuilder, MessageFlags, PermissionFlagsBits, GatewayIntentBits, AttachmentBuilder, Client, Partials, REST, Routes } from 'discord.js';
package/dist/bot.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Client } from 'discord.js';
2
2
  import type { ChatInputCommandInteraction, Message } from 'discord.js';
3
+ export interface ClearChannelMessagesResult {
4
+ deleted: number;
5
+ failed: number;
6
+ errors: Error[];
7
+ }
3
8
  interface CommandInstance {
4
9
  data?: {
5
10
  name: string;
@@ -24,19 +29,30 @@ export default class DiscordBot {
24
29
  resolveReady: () => void;
25
30
  ready: Promise<void>;
26
31
  client: Client | null;
32
+ private _connectionState;
33
+ private _queue;
27
34
  constructor();
28
35
  init(): Promise<void>;
36
+ private _resetReady;
29
37
  registerClientEvents(): void;
30
38
  discoverCommands(): Promise<void>;
31
39
  discoverEvents(): Promise<void>;
40
+ private _doSend;
32
41
  sendMessage(content: string, channelId: string, imagePath?: string | null): Promise<Message>;
42
+ private _enqueue;
43
+ private _enqueueForRetry;
44
+ private _scheduleRetry;
45
+ private _processEntry;
46
+ private _flushQueue;
47
+ private _isRetryable;
48
+ private _ensureReady;
33
49
  sendFile(file: string, messageObject: Message): Promise<Message>;
34
50
  reply(interaction: ChatInputCommandInteraction, content: string): Promise<void>;
35
51
  updateStatus(name: string, type?: number): Promise<void>;
36
52
  getChannelMessages(channelId: string, options?: Record<string, unknown>): Promise<unknown>;
37
53
  getChannelMessage(channelId: string, messageId: string): Promise<unknown>;
38
54
  getGuild(guildId?: string): Promise<unknown>;
39
- clearChannelMessages(channelId: string): Promise<unknown[]>;
55
+ clearChannelMessages(channelId: string): Promise<ClearChannelMessagesResult>;
40
56
  giveRole(memberId: string, roleId: string): Promise<void>;
41
57
  close(): void;
42
58
  reset(): void;
package/dist/bot.js CHANGED
@@ -4,6 +4,11 @@ import { Client, GatewayIntentBits, AttachmentBuilder, MessageFlags } from 'disc
4
4
  import { forEachFileImport } from '@stonyx/utils/file';
5
5
  import { deriveIntents, derivePartials } from './intents.js';
6
6
  import { chunkMessage } from './message.js';
7
+ const MAX_RETRIES = 3;
8
+ const MAX_QUEUE_SIZE = 1000;
9
+ const QUEUE_TTL = 300_000; // 5 minutes
10
+ const READY_TIMEOUT = 30_000; // 30 seconds
11
+ const RETRYABLE_NETWORK_CODES = new Set(['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED']);
7
12
  export default class DiscordBot {
8
13
  static instance;
9
14
  commands = {};
@@ -11,29 +16,35 @@ export default class DiscordBot {
11
16
  resolveReady;
12
17
  ready = new Promise(resolve => { this.resolveReady = resolve; });
13
18
  client = null;
19
+ _connectionState = 'connecting';
20
+ _queue = [];
14
21
  constructor() {
15
22
  if (DiscordBot.instance)
16
23
  return DiscordBot.instance;
17
24
  DiscordBot.instance = this;
18
25
  }
19
26
  async init() {
27
+ // Self-register so log.discord works even when @stonyx/discord is in the
28
+ // consumer's `dependencies` (stonyx loader only merges devDependencies).
29
+ const { logColor = '#7289da', logMethod = 'discord' } = config.discord;
30
+ log.defineType(logMethod, logColor);
31
+ if (this.client) {
32
+ // Already initialized — singleton reuse via Discord.init() or a direct
33
+ // new DiscordBot().init() call. Wait for the existing ready promise
34
+ // rather than re-running discovery + login.
35
+ await this.ready;
36
+ return;
37
+ }
20
38
  const { token } = config.discord;
21
39
  if (!token) {
22
- log.discord('No DISCORD_TOKEN configured — bot will not start');
40
+ log.discord?.('No DISCORD_TOKEN configured — bot will not start');
23
41
  this.resolveReady();
24
42
  return;
25
43
  }
26
44
  await this.discoverCommands();
27
45
  await this.discoverEvents();
28
- const hasWork = Object.keys(this.commands).length > 0 || this.eventHandlers.length > 0;
29
- if (!hasWork) {
30
- log.discord('No discord commands or event handlers found — skipping bot initialization');
31
- this.resolveReady();
32
- return;
33
- }
34
- const discordConfig = config.discord;
35
- const intents = deriveIntents(this.eventHandlers, discordConfig.additionalIntents);
36
- const partials = derivePartials(intents, discordConfig.additionalPartials);
46
+ const intents = deriveIntents(this.eventHandlers, config.discord.additionalIntents);
47
+ const partials = derivePartials(intents, config.discord.additionalPartials);
37
48
  if (Object.keys(this.commands).length > 0) {
38
49
  intents.push(GatewayIntentBits.Guilds);
39
50
  }
@@ -42,13 +53,43 @@ export default class DiscordBot {
42
53
  this.client.login(token);
43
54
  await this.ready;
44
55
  }
56
+ _resetReady() {
57
+ this.ready = new Promise(resolve => { this.resolveReady = resolve; });
58
+ }
45
59
  registerClientEvents() {
46
60
  const { client } = this;
47
61
  if (!client)
48
62
  return;
49
63
  client.on('ready', () => {
64
+ this._connectionState = 'ready';
50
65
  this.resolveReady();
51
- log.discord('Discord Bot is Ready!');
66
+ this._flushQueue();
67
+ log.discord?.('Discord Bot is Ready!');
68
+ });
69
+ client.on('shardDisconnect', () => {
70
+ this._resetReady();
71
+ this._connectionState = 'disconnected';
72
+ log.discord?.('Shard disconnected');
73
+ });
74
+ client.on('shardReconnecting', () => {
75
+ this._resetReady();
76
+ this._connectionState = 'connecting';
77
+ log.discord?.('Shard reconnecting');
78
+ });
79
+ client.on('shardResume', () => {
80
+ this._connectionState = 'ready';
81
+ this.resolveReady();
82
+ this._flushQueue();
83
+ log.discord?.('Shard resumed');
84
+ });
85
+ client.on('invalidated', () => {
86
+ log.error('Discord session invalidated — rejecting all queued messages');
87
+ const entries = this._queue.splice(0);
88
+ for (const entry of entries) {
89
+ if (entry.timerId !== null)
90
+ clearTimeout(entry.timerId);
91
+ entry.reject(new Error('Discord session invalidated'));
92
+ }
52
93
  });
53
94
  client.on('interactionCreate', async (interaction) => {
54
95
  if (!interaction.isChatInputCommand())
@@ -88,12 +129,12 @@ export default class DiscordBot {
88
129
  const CommandClass = CommandClassUntyped;
89
130
  const instance = new CommandClass();
90
131
  if (!instance.data || typeof instance.execute !== 'function') {
91
- log.discord(`Command "${name}" is missing data or execute — skipping`);
132
+ log.discord?.(`Command "${name}" is missing data or execute — skipping`);
92
133
  return;
93
134
  }
94
135
  instance._bot = this;
95
136
  this.commands[instance.data.name] = instance;
96
- log.discord(`Loaded command: /${instance.data.name}`);
137
+ log.discord?.(`Loaded command: /${instance.data.name}`);
97
138
  }, { ignoreAccessFailure: true });
98
139
  }
99
140
  async discoverEvents() {
@@ -103,31 +144,184 @@ export default class DiscordBot {
103
144
  await forEachFileImport(eventDir, (EventHandlerClassUntyped, { name }) => {
104
145
  const EventHandlerClass = EventHandlerClassUntyped;
105
146
  if (!EventHandlerClass.event) {
106
- log.discord(`Event handler "${name}" is missing static event property — skipping`);
147
+ log.discord?.(`Event handler "${name}" is missing static event property — skipping`);
107
148
  return;
108
149
  }
109
150
  const instance = new EventHandlerClass();
110
151
  instance._bot = this;
111
152
  this.eventHandlers.push(instance);
112
- log.discord(`Loaded event handler: ${EventHandlerClass.event} (${name})`);
153
+ log.discord?.(`Loaded event handler: ${EventHandlerClass.event} (${name})`);
113
154
  }, { ignoreAccessFailure: true });
114
155
  }
115
- async sendMessage(content, channelId, imagePath = null) {
156
+ async _doSend(content, channelId, imagePath) {
157
+ if (!this.client)
158
+ throw new Error('Discord bot is not initialized');
116
159
  const channel = await this.client.channels.fetch(channelId);
117
160
  if (!channel)
118
161
  throw new Error('Invalid Channel ID');
162
+ const sendable = channel;
163
+ if (content.length > 2000) {
164
+ const chunks = chunkMessage('', content);
165
+ let firstMessage;
166
+ for (let i = 0; i < chunks.length; i++) {
167
+ const options = { content: chunks[i] };
168
+ if (i === 0 && imagePath) {
169
+ options.files = [new AttachmentBuilder(imagePath)];
170
+ }
171
+ const msg = await sendable.send(options);
172
+ if (i === 0)
173
+ firstMessage = msg;
174
+ }
175
+ return firstMessage;
176
+ }
119
177
  const options = { content };
120
178
  if (imagePath) {
121
179
  options.files = [new AttachmentBuilder(imagePath)];
122
180
  }
123
- return await channel.send(options);
181
+ return await sendable.send(options);
182
+ }
183
+ async sendMessage(content, channelId, imagePath = null) {
184
+ if (this._connectionState === 'ready') {
185
+ try {
186
+ return await this._doSend(content, channelId, imagePath);
187
+ }
188
+ catch (error) {
189
+ if (this._isRetryable(error)) {
190
+ return this._enqueueForRetry(content, channelId, imagePath, 1);
191
+ }
192
+ throw error;
193
+ }
194
+ }
195
+ return this._enqueue(content, channelId, imagePath);
196
+ }
197
+ _enqueue(content, channelId, imagePath) {
198
+ if (this._queue.length >= MAX_QUEUE_SIZE) {
199
+ return Promise.reject(new Error('Message queue full'));
200
+ }
201
+ return new Promise((resolve, reject) => {
202
+ this._queue.push({
203
+ content,
204
+ channelId,
205
+ imagePath,
206
+ resolve,
207
+ reject,
208
+ retries: 0,
209
+ enqueuedAt: Date.now(),
210
+ timerId: null,
211
+ });
212
+ });
213
+ }
214
+ _enqueueForRetry(content, channelId, imagePath, retries) {
215
+ if (this._queue.length >= MAX_QUEUE_SIZE) {
216
+ return Promise.reject(new Error('Message queue full'));
217
+ }
218
+ return new Promise((resolve, reject) => {
219
+ const entry = {
220
+ content,
221
+ channelId,
222
+ imagePath,
223
+ resolve,
224
+ reject,
225
+ retries,
226
+ enqueuedAt: Date.now(),
227
+ timerId: null,
228
+ };
229
+ this._queue.push(entry);
230
+ this._scheduleRetry(entry);
231
+ });
232
+ }
233
+ _scheduleRetry(entry) {
234
+ const backoff = Math.min(1000 * Math.pow(2, entry.retries) + Math.floor(Math.random() * 500), 30000);
235
+ entry.timerId = setTimeout(() => {
236
+ entry.timerId = null;
237
+ // Only process if the entry is still in the queue (wasn't rejected by close/invalidated)
238
+ const idx = this._queue.indexOf(entry);
239
+ if (idx === -1)
240
+ return;
241
+ if (this._connectionState === 'ready') {
242
+ this._processEntry(entry, idx);
243
+ }
244
+ // If not ready, entry stays in queue for _flushQueue on reconnect
245
+ }, backoff);
246
+ }
247
+ async _processEntry(entry, idx) {
248
+ // Remove from queue before attempting
249
+ const currentIdx = this._queue.indexOf(entry);
250
+ if (currentIdx === -1)
251
+ return;
252
+ this._queue.splice(currentIdx, 1);
253
+ // TTL check
254
+ if (Date.now() - entry.enqueuedAt > QUEUE_TTL) {
255
+ entry.reject(new Error('Message expired: stale TTL exceeded'));
256
+ return;
257
+ }
258
+ try {
259
+ const msg = await this._doSend(entry.content, entry.channelId, entry.imagePath);
260
+ entry.resolve(msg);
261
+ }
262
+ catch (error) {
263
+ if (this._isRetryable(error) && entry.retries < MAX_RETRIES) {
264
+ entry.retries++;
265
+ this._queue.push(entry);
266
+ this._scheduleRetry(entry);
267
+ }
268
+ else {
269
+ const message = entry.retries >= MAX_RETRIES
270
+ ? `Message send failed: retries exhausted (${entry.retries}/${MAX_RETRIES})`
271
+ : (error instanceof Error ? error.message : String(error));
272
+ entry.reject(new Error(message));
273
+ }
274
+ }
275
+ }
276
+ async _flushQueue() {
277
+ // Take a snapshot of entries to process
278
+ const entries = this._queue.splice(0);
279
+ for (const entry of entries) {
280
+ // TTL check
281
+ if (Date.now() - entry.enqueuedAt > QUEUE_TTL) {
282
+ entry.reject(new Error('Message expired: stale TTL exceeded'));
283
+ continue;
284
+ }
285
+ try {
286
+ const msg = await this._doSend(entry.content, entry.channelId, entry.imagePath);
287
+ entry.resolve(msg);
288
+ }
289
+ catch (error) {
290
+ if (this._isRetryable(error) && entry.retries < MAX_RETRIES) {
291
+ entry.retries++;
292
+ this._queue.push(entry);
293
+ this._scheduleRetry(entry);
294
+ }
295
+ else {
296
+ entry.reject(error instanceof Error ? error : new Error(String(error)));
297
+ }
298
+ }
299
+ }
300
+ }
301
+ _isRetryable(error) {
302
+ if (error == null || typeof error !== 'object')
303
+ return false;
304
+ const err = error;
305
+ // HTTP 5xx errors
306
+ if (typeof err.status === 'number' && err.status >= 500 && err.status <= 599) {
307
+ return true;
308
+ }
309
+ // Network error codes
310
+ if (typeof err.code === 'string' && RETRYABLE_NETWORK_CODES.has(err.code)) {
311
+ return true;
312
+ }
313
+ return false;
314
+ }
315
+ async _ensureReady() {
316
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timed out waiting for Discord bot to be ready')), READY_TIMEOUT));
317
+ await Promise.race([this.ready, timeout]);
124
318
  }
125
319
  async sendFile(file, messageObject) {
126
320
  return await messageObject.edit({
127
321
  content: '',
128
322
  files: [{
129
323
  attachment: file,
130
- name: file.split('/').pop()
324
+ name: file.split('/').pop() ?? file
131
325
  }]
132
326
  });
133
327
  }
@@ -152,32 +346,52 @@ export default class DiscordBot {
152
346
  }
153
347
  }
154
348
  async updateStatus(name, type = 0) {
349
+ await this._ensureReady();
350
+ if (!this.client?.user)
351
+ throw new Error('Discord bot is not initialized');
155
352
  await this.client.user.setPresence({
156
353
  activities: [{ name, type }],
157
354
  status: 'online'
158
355
  });
159
356
  }
160
357
  async getChannelMessages(channelId, options = {}) {
358
+ await this._ensureReady();
359
+ if (!this.client)
360
+ throw new Error('Discord bot is not initialized');
161
361
  const channel = await this.client.channels.fetch(channelId);
162
- const discordConfig = config.discord;
163
362
  return await channel.messages.fetch({
164
- limit: discordConfig.maxMessagesPerRequest,
363
+ limit: config.discord.maxMessagesPerRequest,
165
364
  ...options
166
365
  });
167
366
  }
168
367
  async getChannelMessage(channelId, messageId) {
368
+ await this._ensureReady();
369
+ if (!this.client)
370
+ throw new Error('Discord bot is not initialized');
169
371
  const channel = await this.client.channels.fetch(channelId);
170
372
  return await channel.messages.fetch(messageId);
171
373
  }
172
374
  async getGuild(guildId) {
173
- const discordConfig = config.discord;
174
- return await this.client.guilds.fetch(guildId || discordConfig.serverId);
375
+ await this._ensureReady();
376
+ if (!this.client)
377
+ throw new Error('Discord bot is not initialized');
378
+ return await this.client.guilds.fetch(guildId || config.discord.serverId || '');
175
379
  }
176
380
  async clearChannelMessages(channelId) {
177
- const promises = [];
178
381
  const messages = await this.getChannelMessages(channelId);
179
- messages.forEach(message => promises.push(message.delete()));
180
- return Promise.all(promises);
382
+ const settled = await Promise.allSettled([...messages.values()].map(m => m.delete()));
383
+ const errors = settled
384
+ .filter((s) => s.status === 'rejected')
385
+ .map(s => s.reason instanceof Error ? s.reason : new Error(String(s.reason)));
386
+ const result = {
387
+ deleted: settled.length - errors.length,
388
+ failed: errors.length,
389
+ errors,
390
+ };
391
+ if (result.failed > 0) {
392
+ log.warn(`clearChannelMessages partial failure on channel ${channelId}: deleted=${result.deleted} failed=${result.failed} errors=${errors.map(String).join('; ')}`);
393
+ }
394
+ return result;
181
395
  }
182
396
  async giveRole(memberId, roleId) {
183
397
  const guild = await this.getGuild();
@@ -186,6 +400,13 @@ export default class DiscordBot {
186
400
  await member.roles.add(role);
187
401
  }
188
402
  close() {
403
+ // Cancel all pending retry timers and reject all queue entries
404
+ const entries = this._queue.splice(0);
405
+ for (const entry of entries) {
406
+ if (entry.timerId !== null)
407
+ clearTimeout(entry.timerId);
408
+ entry.reject(new Error('Discord bot closed'));
409
+ }
189
410
  if (this.client) {
190
411
  this.client.destroy();
191
412
  this.client = null;
@@ -196,5 +417,7 @@ export default class DiscordBot {
196
417
  this.close();
197
418
  this.commands = {};
198
419
  this.eventHandlers = [];
420
+ this._connectionState = 'connecting';
421
+ this._resetReady();
199
422
  }
200
423
  }
package/dist/main.js CHANGED
@@ -1,3 +1,4 @@
1
+ import DiscordBot from './bot.js';
1
2
  export default class Discord {
2
3
  static instance;
3
4
  constructor() {
@@ -6,8 +7,7 @@ export default class Discord {
6
7
  Discord.instance = this;
7
8
  }
8
9
  async init() {
9
- // Bot initialization is deferred to DiscordBot.init()
10
- // This entry point satisfies Stonyx module auto-initialization
10
+ await new DiscordBot().init();
11
11
  }
12
12
  reset() {
13
13
  Discord.instance = null;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.1.1-beta.13",
7
+ "version": "0.1.1-beta.131",
8
8
  "description": "Discord bot module for the Stonyx framework",
9
9
  "main": "dist/main.js",
10
10
  "types": "dist/main.d.ts",
@@ -64,16 +64,19 @@
64
64
  "stonyx": ">=0.2.3-beta.4"
65
65
  },
66
66
  "devDependencies": {
67
- "@stonyx/utils": "0.2.3-beta.7",
67
+ "@stonyx/utils": "0.2.3-beta.27",
68
68
  "@types/node": "^25.5.2",
69
+ "@types/qunit": "^2.19.13",
70
+ "@types/sinon": "^21.0.1",
69
71
  "qunit": "^2.24.1",
70
72
  "sinon": "^21.0.0",
71
- "stonyx": "0.2.3-beta.12",
73
+ "stonyx": "0.2.3-beta.98",
74
+ "tsx": "^4.21.0",
72
75
  "typescript": "^5.8.3"
73
76
  },
74
77
  "scripts": {
75
78
  "build": "tsc",
76
- "build:test": "tsc -p tsconfig.test.json",
77
- "test": "pnpm build && pnpm build:test && stonyx test 'dist-test/test/**/*-test.js'"
79
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
80
+ "test": "pnpm build && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'"
78
81
  }
79
82
  }