Framework Reference
Complete API documentation for the Shiver Discord bot framework. Commands, middleware, storage, AI, and every module in the SDK.
Overview
Shiver is a batteries-included Discord bot framework built on discord.js v14. It provides a typed command registry, middleware pipeline, storage adapters, health checks, AI tooling, and production-ready infrastructure out of the box.
Every module exposes a consistent init() / destroy() lifecycle. Import only what you need.
Why Shiver
- Zero-config slash commands — register from file structure, sync on boot.
- Middleware pipeline — guard, rate-limit, and transform before any handler runs.
- Storage adapters — swap SQLite, Postgres, or Redis with one line.
- AI-native — built-in function registry, prompt builder, and conversation context.
- Production health — HTTP health endpoint, graceful shutdown, multi-instance leader election.
- Developer ergonomics — hot-reload, component routing, diff tracking, and auto-generated help.
Download & install
npm install @shiver/core @shiver/storage @shiver/ai Peer dependencies: discord.js ^14.14, node ^20.
Requirements
| Requirement | Minimum | Recommended |
|---|---|---|
| Node.js | 20.0 | 22 LTS |
| discord.js | 14.14 | 14.16 |
| SQLite | built-in | better-sqlite3 |
| Postgres | 15+ | 16 |
Quick start
import { Shiver } from '@shiver/core';
import { SQLiteAdapter } from '@shiver/storage';
const bot = new Shiver({
token: process.env.DISCORD_TOKEN,
clientId: '1234567890',
prefix: '!',
storage: new SQLiteAdapter({ file: './data/bot.db' }),
commands: './commands',
middleware: './middleware',
});
bot.start(); First command
export default {
name: 'ping',
description: 'Pong!',
slash: true,
prefix: true,
async run(ctx) {
const sent = await ctx.reply('Pinging...');
const latency = sent.createdTimestamp - ctx.interaction.createdTimestamp;
await sent.edit(`Pong! ${latency}ms`);
},
}; Framework properties
| Property | Type | Default | Description |
|---|---|---|---|
token | string | required | Discord bot token |
clientId | string | required | Application client ID |
prefix | string | '!' | Default prefix for message commands |
storage | Adapter | MemoryAdapter | Storage backend |
commands | string | './commands' | Command directory path |
middleware | string | './middleware' | Middleware directory path |
guards | GuardConfig | {} | Guard configuration |
health | HealthConfig | { port: 3000 } | Health check server |
devMode | boolean | false | Enable dev mode |
Client init flow
On bot.start() the framework executes this sequence:
- Load and validate config.
- Initialize storage adapter, run pending migrations.
- Scan command/middleware directories and build registries.
- Register slash commands with Discord API (if
syncCommands: true). - Start health check HTTP server.
- Connect WebSocket, bind event handlers.
- Emit
readyevent internally.
Config reference
export default {
token: process.env.DISCORD_TOKEN,
clientId: process.env.CLIENT_ID,
prefix: '!',
devMode: process.env.NODE_ENV !== 'production',
storage: { adapter: 'sqlite', file: './data/bot.db' },
commands: './commands',
middleware: './middleware',
guards: {
ownerOnly: { ids: ['1234567890'] },
guildOnly: true,
},
health: { port: 3000, path: '/health' },
slashSync: true,
}; Guards
guards: {
ownerOnly: { ids: ['1234567890', '0987654321'], fallback: 'You lack permission.' },
guildOnly: { enabled: true, fallback: 'This command only works in servers.' },
channelWhitelist: ['1111111111'],
} Storage
storage: {
adapter: 'sqlite', // 'sqlite' | 'postgres' | 'redis' | 'memory'
file: './data/bot.db', // SQLite only
host: 'localhost', // Postgres/Redis
port: 5432, // Postgres/Redis
password: process.env.DB_PASS,
prefix: 'shiver:', // Redis key prefix
} Health
health: {
port: 3000,
path: '/health',
auth: process.env.HEALTH_TOKEN,
checks: { storage: true, uptime: true, memory: true },
} Slash sync
{ slashSync: true }
{ slashSync: { guildId: '1234567890' } }
{ slashSync: false } Registry API
const registry = bot.registry;
registry.get('ping');
registry.has('ping');
registry.all();
registry.byCategory('moderation');
registry.reload('ping');
registry.register(cmd); Command file shape
export default {
name: 'kick',
description: 'Kick a member',
category: 'moderation',
slash: true,
prefix: true,
aliases: ['k'],
cooldown: 5,
guards: ['guildOnly', 'ownerOnly'],
options: [
{ name: 'user', type: 'USER', description: 'Member to kick', required: true },
{ name: 'reason', type: 'STRING', description: 'Reason', required: false },
],
async run(ctx) {
const user = ctx.options.getUser('user');
const reason = ctx.options.getString('reason', 'No reason');
await ctx.guild.members.kick(user.id, reason);
await ctx.reply(`Kicked ${user.tag}: ${reason}`);
},
}; Slash & prefix flow
async run(ctx) {
const target = ctx.options.getUser('user');
// ctx.interaction — ChatInputCommandInteraction OR Message
// ctx.isSlash — boolean
// ctx.isPrefix — boolean
// ctx.reply() — auto-defers, works for both
} Autocomplete
export default {
name: 'search',
options: [{ name: 'query', type: 'STRING', autocomplete: true }],
async autocomplete(ctx) {
const focused = ctx.interaction.options.getFocused();
const results = items.filter(i =>
i.name.toLowerCase().includes(focused.toLowerCase())
);
await ctx.interaction.respond(
results.slice(0, 25).map(i => ({ name: i.name, value: i.id }))
);
},
async run(ctx) { /* ... */ },
}; Component handlers
export default {
name: 'confirm',
async run(ctx) {
await ctx.reply({
content: 'Are you sure?',
components: [{
type: 'ACTION_ROW',
components: [{ type: 'BUTTON', customId: 'confirm:yes', label: 'Yes', style: 'DANGER' }],
}],
});
},
components: {
'confirm:yes': async (ctx) => {
await ctx.reply('Confirmed!');
},
},
}; Middleware chain
export default {
name: 'rate-limit',
priority: 1,
async run(ctx, next) {
const key = `${ctx.author.id}:${ctx.command.name}`;
const last = await ctx.storage.get(`rl:${key}`);
if (last && Date.now() - last < 5000) {
return ctx.reply('Slow down! Wait 5 seconds.');
}
await ctx.storage.set(`rl:${key}`, Date.now());
await next();
},
}; Custom middleware
export default {
name: 'logger',
priority: 0,
async run(ctx, next) {
const start = Date.now();
console.log(`[${ctx.command.name}] ${ctx.author.tag}`);
await next();
console.log(`[${ctx.command.name}] ${Date.now() - start}ms`);
},
}; safeRespond
import { safeRespond } from '@shiver/core';
async run(ctx) {
await safeRespond(ctx.interaction, {
content: 'Here is your result.',
ephemeral: true,
});
} Defer strategies
async run(ctx) {
await ctx.defer();
await ctx.defer({ ephemeral: true });
await ctx.followUp('Still processing...');
await ctx.editReply('Done!');
} Adapter API
const db = bot.storage;
await db.get('user:123:name');
await db.set('user:123:name', 'Alice');
await db.delete('user:123:name');
await db.has('user:123:name');
await db.all();
await db.all('user:123:*');
await db.increment('counter:visits');
await db.push('list:items', 'a');
await db.pull('list:items', 'a');
await db.transaction(async (tx) => {
await tx.set('a', 1);
await tx.set('b', 2);
}); Dynamic prefix
const guildPrefix = await bot.storage.get(`guild:${ctx.guild?.id}:prefix`);
const prefix = guildPrefix || bot.config.prefix;
if (message.content.startsWith(prefix)) {
const args = message.content.slice(prefix.length).trim().split(/\s+/);
const command = args.shift();
bot.registry.get(command)?.run(ctx);
} Migrations
export const migrations = [
{ version: 1, up: async (db) => { await db.set('schema:version', 1); } },
{ version: 2, up: async (db) => {
const data = await db.all('user:*');
for (const [key, value] of Object.entries(data)) {
await db.set(`${key}:profile`, value);
await db.delete(key);
}
}},
]; Embed colors
await bot.storage.set(`guild:${guildId}:embedColor`, '#5865F2');
const color = await bot.storage.get(`guild:${guildId}:embedColor`);
const decimalColor = parseInt(color.replace('#', ''), 16);
const embed = { color: decimalColor, title: 'Hello' }; Health checks
{ "status": "ok", "uptime": 86400, "storage": "connected",
"memory": { "heapUsed": 45200000, "heapTotal": 67000000 },
"guilds": 142, "commands": 87 } Custom routes
bot.healthServer.route('/api/stats', (req, res) => {
res.json({ guilds: bot.guilds.cache.size, users: bot.users.cache.size });
}); Assets
await bot.assets.upload('logo.png', buffer);
const file = await bot.assets.get('logo.png');
await bot.assets.delete('logo.png');
const list = await bot.assets.list(); Components v2 / modals
import { ModalBuilder, TextInputBuilder, ActionRowBuilder } from 'discord.js';
async run(ctx) {
const modal = new ModalBuilder()
.setCustomId('feedback-modal')
.setTitle('Feedback');
const input = new TextInputBuilder()
.setCustomId('feedback-text')
.setLabel('Your feedback')
.setStyle('PARAGRAPH');
modal.addComponents(new ActionRowBuilder().addComponents(input));
await ctx.interaction.showModal(modal);
}
components: {
'feedback-modal': async (ctx) => {
const text = ctx.interaction.fields.getTextInputValue('feedback-text');
await ctx.reply({ content: 'Thanks!', ephemeral: true });
},
} Reload / debug
bot.registry.reload('kick');
bot.registry.reloadAll();
console.log(bot.registry.all().map(c => c.name)); Stats / health
const stats = bot.stats;
stats.on('command', (name, duration) => {
console.log(`${name} took ${duration}ms`);
}); Lifecycle / multi-instance
bot.on('ready', () => console.log('Bot ready'));
bot.on('shardReady', (shardId) => console.log(`Shard ${shardId}`));
bot.on('error', (err) => console.error(err));
bot.on('warn', (msg) => console.warn(msg));
process.on('SIGINT', () => bot.destroy()); AI features
Shiver ships with first-class AI tooling. These modules let you register callable functions, build prompts, route natural-language commands, and maintain conversation state.
FunctionRegistry
import { FunctionRegistry } from '@shiver/ai';
const registry = new FunctionRegistry();
registry.register({
name: 'get_weather',
description: 'Get current weather for a city',
parameters: {
type: 'object',
properties: { city: { type: 'string', description: 'City name' } },
required: ['city'],
},
execute: async ({ city }) => {
const data = await fetchWeather(city);
return { temp: data.temp, condition: data.condition };
},
});
const result = await registry.call('get_weather', { city: 'London' }); AIContext
import { AIContext } from '@shiver/ai';
const ctx = new AIContext({
model: 'gpt-4o',
functions: registry,
systemPrompt: 'You are a helpful Discord bot assistant.',
});
const response = await ctx.chat({
userMessage: 'What is the weather in Tokyo?',
userId: '1234567890',
guildId: '0987654321',
}); ConversationContext
import { ConversationContext } from '@shiver/ai';
const conv = new ConversationContext({ storage: bot.storage, maxMessages: 20, ttl: 3600 });
const history = await conv.get(userId);
await conv.add(userId, { role: 'user', content: 'Hello' });
await conv.add(userId, { role: 'assistant', content: 'Hi there!' });
await conv.clear(userId); PromptBuilder
import { PromptBuilder } from '@shiver/ai';
const prompt = new PromptBuilder()
.system('You are a moderation assistant.')
.context(`Server: ${guild.name}`)
.context(`User roles: ${roles.join(', ')}`)
.history(conversationHistory)
.user(message)
.build(); NaturalCommandRouter
import { NaturalCommandRouter } from '@shiver/ai';
const router = new NaturalCommandRouter({ ai: aiContext, commands: bot.registry, confidence: 0.7 });
const match = await router.match('kick that guy for spamming');
if (match) {
await bot.registry.get(match.command).run(ctx);
} else {
await ctx.reply('I could not understand that command.');
} StructuredOutput
import { StructuredOutput } from '@shiver/ai';
const schema = {
type: 'object',
properties: {
action: { type: 'string', enum: ['kick', 'ban', 'warn', 'timeout'] },
target: { type: 'string' },
reason: { type: 'string' },
},
required: ['action', 'target'],
};
const result = await StructuredOutput.parse({
ai: aiContext,
prompt: 'Ban user123 for breaking rules',
schema,
}); ComponentRouter
import { ComponentRouter } from '@shiver/core';
const router = new ComponentRouter();
router.register('settings:theme', async (ctx) => {
const theme = ctx.interaction.values[0];
await bot.storage.set(`guild:${ctx.guildId}:theme`, theme);
await ctx.reply({ content: `Theme set to ${theme}`, ephemeral: true });
});
bot.client.addComponentRouter(router); WizardSession
import { WizardSession } from '@shiver/core';
const wizard = new WizardSession({
steps: [
{ id: 'name', question: 'What is your server name?' },
{ id: 'theme', question: 'Pick a theme:', type: 'SELECT', options: ['dark', 'light'] },
{ id: 'confirm', question: 'Confirm setup?', type: 'CONFIRM' },
],
onComplete: async (data) => { await bot.storage.set(`guild:${guildId}:config`, data); },
onCancel: async () => { /* handle cancel */ },
});
await wizard.start(ctx); FormBuilder
import { FormBuilder } from '@shiver/core';
const form = new FormBuilder()
.addTextInput({ id: 'title', label: 'Ticket Title', required: true })
.addTextInput({ id: 'description', label: 'Description', style: 'PARAGRAPH' })
.addSelectMenu({ id: 'category', label: 'Category',
options: [{ label: 'Support', value: 'support' }, { label: 'Billing', value: 'billing' }],
})
.onSubmit(async (data, ctx) => { await ctx.reply(`Ticket created: ${data.title}`); }); VoteManager
import { VoteManager } from '@shiver/core';
const votes = new VoteManager({ storage: bot.storage });
await votes.create({ id: 'poll-1', question: 'Should we add a new channel?',
options: ['yes', 'no'], duration: 3600, channelId: '1234567890' });
await votes.vote('poll-1', userId, 'yes');
const results = await votes.getResults('poll-1'); MessageCollector
const collector = ctx.channel.createMessageCollector({
filter: (m) => m.author.id === ctx.author.id,
time: 60000,
max: 5,
});
collector.on('collect', (msg) => { console.log(`Collected: ${msg.content}`); });
collector.on('end', (collected) => { console.log(`Done. ${collected.size} collected.`); }); UserSessionStore
import { UserSessionStore } from '@shiver/core';
const sessions = new UserSessionStore({ storage: bot.storage, ttl: 1800 });
await sessions.set(userId, { step: 1, data: {} });
const session = await sessions.get(userId);
await sessions.delete(userId); Scheduler
import { Scheduler } from '@shiver/core';
const scheduler = new Scheduler({ storage: bot.storage });
scheduler.every('daily-reset', '0 0 * * *', async () => {
await bot.storage.set('daily:votes', {});
});
scheduler.after('welcome', 5000, async () => { /* one-shot */ });
scheduler.cron('cleanup', '*/30 * * * *', async () => { await cleanupExpiredData(); });
scheduler.cancel('daily-reset'); BroadcastManager
import { BroadcastManager } from '@shiver/core';
const broadcast = new BroadcastManager(bot.client);
broadcast.on('config-change', (data) => { console.log('Config changed:', data); });
broadcast.emit('config-change', { guildId, key, value });
broadcast.shard('reload-commands'); RequestDeduplicator
import { RequestDeduplicator } from '@shiver/core';
const dedup = new RequestDeduplicator({ ttl: 3000 });
const result = await dedup.run('user-fetch', userId, async () => {
return await client.users.fetch(userId);
}); SafeExecutor
import { SafeExecutor } from '@shiver/core';
const executor = new SafeExecutor({
retries: 3,
backoff: (attempt) => Math.pow(2, attempt) * 1000,
onError: (err) => console.error('Executor error:', err),
});
const result = await executor.run(async () => { return await riskyApiCall(); }); FeatureFlagManager
import { FeatureFlagManager } from '@shiver/core';
const flags = new FeatureFlagManager({
storage: bot.storage,
defaults: { 'ai-commands': false, 'ticket-system': true, 'beta-features': false },
});
if (flags.isEnabled('ai-commands')) { /* ... */ }
await flags.setGuild(guildId, 'ai-commands', true);
await flags.setRollout('new-ui', { percentage: 10 }); CommandDisabledManager
import { CommandDisabledManager } from '@shiver/core';
const disabled = new CommandDisabledManager({ storage: bot.storage });
await disabled.disable('kick', guildId, 'Maintenance mode');
await disabled.enable('kick', guildId);
const isDisabled = await disabled.isDisabled('kick', guildId);
if (await disabled.isDisabled(ctx.command.name, ctx.guildId)) {
return ctx.reply('This command is currently disabled.');
} HelpGenerator
import { HelpGenerator } from '@shiver/core';
const help = new HelpGenerator(bot.registry);
const embed = help.generate();
const categoryEmbed = help.generate('moderation');
const commandHelp = help.generateCommand('kick'); CommandSuggester
import { CommandSuggester } from '@shiver/core';
const suggester = new CommandSuggester(bot.registry);
const suggestions = suggester.suggest('ban');
const correction = suggester.correct('bann'); DiffTracker
import { DiffTracker } from '@shiver/core';
const tracker = new DiffTracker();
tracker.snapshot('config', config);
const diff = tracker.diff('config', newConfig); AlertManager
import { AlertManager } from '@shiver/core';
const alerts = new AlertManager({ storage: bot.storage });
await alerts.broadcast({ title: 'Scheduled Maintenance',
message: 'The bot will be offline at 2 AM UTC.',
severity: 'warning', guildIds: ['all'] });
const active = await alerts.getActive();
await alerts.dismiss(alertId); LocaleSync
import { LocaleSync } from '@shiver/core';
const locale = new LocaleSync({ defaultLocale: 'en', localesDir: './locales' });
const t = locale.get(guildLocale);
await ctx.reply(t('commands.kick.success', { user: user.tag })); ProgressBar
import { ProgressBar } from '@shiver/core';
const bar = new ProgressBar({ total: 100, filledChar: '#', emptyChar: '-', length: 20 });
bar.render(65); TableBuilder
import { TableBuilder } from '@shiver/core';
const table = new TableBuilder()
.setHeaders(['Command', 'Category', 'Cooldown'])
.addRow(['kick', 'moderation', '5s'])
.addRow(['ban', 'moderation', '10s'])
.addRow(['ping', 'utility', '2s'])
.render(); ListBuilder
import { ListBuilder } from '@shiver/core';
const list = new ListBuilder()
.setStyle('bullet')
.addItem('First item')
.addItem('Second item')
.addItem('Third item')
.build(); TicketSystem
import { TicketSystem } from '@shiver/systems';
const tickets = new TicketSystem({ storage: bot.storage,
supportRoleId: '1234567890', categoryId: '0987654321' });
await tickets.open({ userId: ctx.author.id, guildId: ctx.guildId,
category: 'support', title: 'Cannot access my account' });
const ticket = await tickets.get(ticketId);
await tickets.close(ticketId, 'Resolved'); GiveawaySystem
import { GiveawaySystem } from '@shiver/systems';
const giveaways = new GiveawaySystem({ storage: bot.storage });
await giveaways.create({ prize: 'Discord Nitro', winners: 3,
duration: 86400, channelId: '1234567890',
requirements: { minRoles: ['Subscriber'] } }); TagSystem
import { TagSystem } from '@shiver/systems';
const tags = new TagSystem({ storage: bot.storage });
await tags.create({ name: 'rules',
content: '1. Be respectful\n2. No spam\n3. Have fun',
author: ctx.author.id, guildId: ctx.guildId });
const tag = await tags.get('rules', ctx.guildId);
await tags.search('rule', ctx.guildId); StarboardSystem
import { StarboardSystem } from '@shiver/systems';
const starboard = new StarboardSystem({ storage: bot.storage,
channelId: '1234567890', threshold: 5, emoji: '*' });
bot.client.on('messageReactionAdd', async (reaction) => {
await starboard.onReaction(reaction);
});
await starboard.configure(guildId, { channelId: '9999999999', threshold: 10 }); Guards
Guards are pre-execution checks that can block a command from running. They run before middleware.
Owner
guards: { ownerOnly: { ids: ['1234567890'] } }
export default { name: 'shutdown', guards: ['ownerOnly'], async run(ctx) { /* ... */ } }; Guild
guards: ['guildOnly']
guards: [{ guildOnly: { guildIds: ['123', '456'] } }] Channel
guards: [{ channelOnly: { ids: ['1111111111'] } }]
guards: [{ channelBlacklist: { ids: ['2222222222'] } }] Role
guards: [{ roleRequired: { ids: ['3333333333', '4444444444'] } }]
guards: [{ roleAll: { ids: ['5555555555'] } }] Time
guards: [{ timeWindow: { start: '09:00', end: '17:00' } }]
guards: [{ cooldown: 30 }]
guards: [{ cooldownGuild: 10 }] RateLimit
guards: [{ rateLimit: { uses: 5, window: 10 } }]
guards: [{ rateLimit: { uses: 10, window: 60, scope: 'guild' } }]
guards: [{ rateLimit: { uses: 3, window: 5, message: 'Whoa, slow down!' } }] Text formatting
| Syntax | Result | Notes |
|---|---|---|
**bold** | bold | Double asterisks |
*italic* | italic | Single asterisks |
__underline__ | underline | Double underscores |
~~strikethrough~~ | Double tildes | |
`code` | code | Inline code |
```code block``` | Code block | Triple backticks |
> quote | Block quote | Greater-than + space |
||spoiler|| | ||spoiler|| | Double pipes |
Emoji & mentions
| Type | Format | Example |
|---|---|---|
| Unicode emoji | :name: | :wave: |
| Custom emoji | <:name:id> | <:shiver:123456> |
| User mention | <@id> | <@123456789> |
| Channel mention | <#id> | <#987654321> |
| Role mention | <@&id> | <@&111222333> |
Components v2 guide
Discord Components v2 introduces new layout primitives:
Container
{ type: 'CONTAINER', components: [
{ type: 'TEXT_DISPLAY', content: '**Welcome!**' },
{ type: 'SEPARATOR', spacing: 'MEDIUM' },
{ type: 'TEXT_DISPLAY', content: 'Select an option below.' },
], color: 0x5865F2 } Separator
{ type: 'SEPARATOR', spacing: 'SMALL' }
{ type: 'SEPARATOR', spacing: 'MEDIUM', divider: true } MediaGallery
{ type: 'MEDIA_GALLERY', items: [
{ media: 'https://example.com/image.png', alt: 'Image' },
{ media: 'https://example.com/video.mp4' },
] } Section
{ type: 'SECTION', text: { type: 'TEXT_DISPLAY', content: 'Section content' },
accessory: { type: 'BUTTON', customId: 'section-btn', label: 'Click', style: 'PRIMARY' } } Buttons
{ type: 'BUTTON', customId: 'action:confirm', label: 'Confirm', style: 'PRIMARY', disabled: false }
{ type: 'BUTTON', style: 'LINK', label: 'Visit', url: 'https://example.com' } Rules
{ type: 'RULES', items: [
{ label: 'Be respectful', description: 'Treat everyone with respect.' },
{ label: 'No spam', description: 'Do not spam channels.' },
{ label: 'English only', description: 'Use English in main channels.' },
] } Embeds (legacy)
const embed = {
title: 'Server Rules', description: 'Please follow these rules.',
color: 0x5865F2,
thumbnail: { url: 'https://example.com/thumb.png' },
image: { url: 'https://example.com/banner.png' },
fields: [
{ name: 'Rule 1', value: 'Be respectful', inline: true },
{ name: 'Rule 2', value: 'No spam', inline: true },
],
footer: { text: 'Server Rules v2' },
timestamp: new Date().toISOString(),
};
await ctx.reply({ embeds: [embed] }); CLI
npx shiver init
npx shiver dev
npx shiver start
npx shiver sync
npx shiver generate cmd
npx shiver test Testing
import { createMockContext } from '@shiver/testing';
describe('ping command', () => {
it('should reply with pong', async () => {
const ctx = createMockContext({
author: { id: '123', tag: 'Test#0001' },
command: { name: 'ping' },
});
await pingCommand.run(ctx);
expect(ctx.reply).toHaveBeenCalledWith(expect.stringContaining('Pong'));
});
}); Custom IDs
Use the format namespace:action:variant for custom IDs:
'confirm:yes'
'confirm:no'
'settings:theme:dark'
'settings:theme:light'
'role-select:assign'
'feedback-modal:submit'
router.register('confirm:*', async (ctx) => {
const action = ctx.customId.split(':')[1];
}); Examples
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder } from 'discord.js';
export default {
name: 'setup', description: 'Initial server setup wizard',
category: 'admin', slash: true,
guards: ['ownerOnly', 'guildOnly'], cooldown: 30,
async run(ctx) {
const embed = new EmbedBuilder()
.setTitle('Server Setup')
.setDescription('Click a button to configure.')
.setColor(0x5865F2);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId('setup:welcome').setLabel('Welcome Channel').setStyle(1),
new ButtonBuilder().setCustomId('setup:roles').setLabel('Auto Roles').setStyle(2),
);
await ctx.reply({ embeds: [embed], components: [row] });
},
components: {
'setup:welcome': async (ctx) => {
await ctx.reply({ content: 'Select the welcome channel:', ephemeral: true });
},
'setup:roles': async (ctx) => {
await ctx.reply({ content: 'Select auto-assign roles:', ephemeral: true });
},
},
}; AI rules (AGENTS.md)
Place an AGENTS.md in your project root for AI assistants:
# AGENTS.md
## Project: Shiver Discord Bot
### Conventions
- Use TypeScript strict mode
- Commands export a default object
- Use ctx.reply() for all responses
- Use bot.storage for persistence
### File structure
- commands/ - command files
- middleware/ - middleware files
- events/ - event listeners
- lib/ - shared utilities
### Testing
- Run tests with: npm test
- Use createMockContext for unit tests Module index
| Module | Package | Description |
|---|---|---|
Shiver | @shiver/core | Main framework entry point |
Registry | @shiver/core | Command registry |
SafeExecutor | @shiver/core | Error-safe async execution |
Scheduler | @shiver/core | Cron and delayed tasks |
BroadcastManager | @shiver/core | Cross-shard messaging |
RequestDeduplicator | @shiver/core | Duplicate request prevention |
SQLiteAdapter | @shiver/storage | SQLite storage backend |
PostgresAdapter | @shiver/storage | PostgreSQL storage backend |
RedisAdapter | @shiver/storage | Redis storage backend |
FunctionRegistry | @shiver/ai | AI function registration |
AIContext | @shiver/ai | AI conversation management |
PromptBuilder | @shiver/ai | Structured prompt construction |
NaturalCommandRouter | @shiver/ai | NLP command routing |
TicketSystem | @shiver/systems | Support ticket management |
GiveawaySystem | @shiver/systems | Giveaway creation and management |
TagSystem | @shiver/systems | Custom command tags |
StarboardSystem | @shiver/systems | Star-based message curation |