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

terminal
npm install @shiver/core @shiver/storage @shiver/ai

Peer dependencies: discord.js ^14.14, node ^20.

Requirements

RequirementMinimumRecommended
Node.js20.022 LTS
discord.js14.1414.16
SQLitebuilt-inbetter-sqlite3
Postgres15+16

Quick start

bot.ts
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

commands/ping.ts
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

PropertyTypeDefaultDescription
tokenstringrequiredDiscord bot token
clientIdstringrequiredApplication client ID
prefixstring'!'Default prefix for message commands
storageAdapterMemoryAdapterStorage backend
commandsstring'./commands'Command directory path
middlewarestring'./middleware'Middleware directory path
guardsGuardConfig{}Guard configuration
healthHealthConfig{ port: 3000 }Health check server
devModebooleanfalseEnable dev mode

Client init flow

On bot.start() the framework executes this sequence:

  1. Load and validate config.
  2. Initialize storage adapter, run pending migrations.
  3. Scan command/middleware directories and build registries.
  4. Register slash commands with Discord API (if syncCommands: true).
  5. Start health check HTTP server.
  6. Connect WebSocket, bind event handlers.
  7. Emit ready event internally.
Configuration

Config reference

shiver.config.ts
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 config
guards: {
  ownerOnly: { ids: ['1234567890', '0987654321'], fallback: 'You lack permission.' },
  guildOnly: { enabled: true, fallback: 'This command only works in servers.' },
  channelWhitelist: ['1111111111'],
}

Storage

storage config
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 config
health: {
  port: 3000,
  path: '/health',
  auth: process.env.HEALTH_TOKEN,
  checks: { storage: true, uptime: true, memory: true },
}

Slash sync

slash sync
{ slashSync: true }
{ slashSync: { guildId: '1234567890' } }
{ slashSync: false }
Command system

Registry API

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

commands/kick.ts
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

context normalization
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
}

Context menus

commands/user-info.ts
export default {
  name: 'User Info',
  type: 'USER',
  async run(ctx) {
    const user = ctx.interaction.targetUser;
    await ctx.reply({ content: `User: ${user.tag}`, ephemeral: true });
  },
};

Autocomplete

autocomplete handler
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

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

Middleware chain

middleware/rate-limit.ts
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

middleware/logger.ts
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`);
  },
};
Handlers

safeRespond

safeRespond usage
import { safeRespond } from '@shiver/core';

async run(ctx) {
  await safeRespond(ctx.interaction, {
    content: 'Here is your result.',
    ephemeral: true,
  });
}

Defer strategies

defer strategies
async run(ctx) {
  await ctx.defer();
  await ctx.defer({ ephemeral: true });
  await ctx.followUp('Still processing...');
  await ctx.editReply('Done!');
}
Storage

Adapter API

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

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

migrations.ts
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

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' };
Infrastructure

Health checks

GET /health
{ "status": "ok", "uptime": 86400, "storage": "connected",
  "memory": { "heapUsed": 45200000, "heapTotal": 67000000 },
  "guilds": 142, "commands": 87 }

Custom routes

custom routes
bot.healthServer.route('/api/stats', (req, res) => {
  res.json({ guilds: bot.guilds.cache.size, users: bot.users.cache.size });
});
Optional systems

Assets

assets API
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

modal example
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

reload commands
bot.registry.reload('kick');
bot.registry.reloadAll();
console.log(bot.registry.all().map(c => c.name));

Stats / health

stats API
const stats = bot.stats;
stats.on('command', (name, duration) => {
  console.log(`${name} took ${duration}ms`);
});

Lifecycle / multi-instance

lifecycle hooks
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

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

function registry
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

ai context
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

conversation context
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

prompt builder
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

natural command router
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

structured output
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,
});
Interaction flows

ComponentRouter

component router
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

wizard session
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

form builder
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

vote manager
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

message collector
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

user session store
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);
Infrastructure modules

Scheduler

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

broadcast manager
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

request deduplicator
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

safe executor
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

feature flags
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

command disabled manager
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.');
}
DX helpers

HelpGenerator

help generator
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

command suggester
import { CommandSuggester } from '@shiver/core';
const suggester = new CommandSuggester(bot.registry);
const suggestions = suggester.suggest('ban');
const correction = suggester.correct('bann');

DiffTracker

diff tracker
import { DiffTracker } from '@shiver/core';
const tracker = new DiffTracker();
tracker.snapshot('config', config);
const diff = tracker.diff('config', newConfig);

AlertManager

alert manager
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

locale sync
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 }));
UI helpers

ProgressBar

progress bar
import { ProgressBar } from '@shiver/core';
const bar = new ProgressBar({ total: 100, filledChar: '#', emptyChar: '-', length: 20 });
bar.render(65);

TableBuilder

table builder
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

list builder
import { ListBuilder } from '@shiver/core';
const list = new ListBuilder()
  .setStyle('bullet')
  .addItem('First item')
  .addItem('Second item')
  .addItem('Third item')
  .build();
Systems

TicketSystem

ticket system
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

giveaway system
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

tag system
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

starboard system
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

Guards are pre-execution checks that can block a command from running. They run before middleware.

Owner

owner guard
guards: { ownerOnly: { ids: ['1234567890'] } }
export default { name: 'shutdown', guards: ['ownerOnly'], async run(ctx) { /* ... */ } };

Guild

guild guard
guards: ['guildOnly']
guards: [{ guildOnly: { guildIds: ['123', '456'] } }]

Channel

channel guard
guards: [{ channelOnly: { ids: ['1111111111'] } }]
guards: [{ channelBlacklist: { ids: ['2222222222'] } }]

Role

role guard
guards: [{ roleRequired: { ids: ['3333333333', '4444444444'] } }]
guards: [{ roleAll: { ids: ['5555555555'] } }]

Time

time guard
guards: [{ timeWindow: { start: '09:00', end: '17:00' } }]
guards: [{ cooldown: 30 }]
guards: [{ cooldownGuild: 10 }]

RateLimit

rate limit guard
guards: [{ rateLimit: { uses: 5, window: 10 } }]
guards: [{ rateLimit: { uses: 10, window: 60, scope: 'guild' } }]
guards: [{ rateLimit: { uses: 3, window: 5, message: 'Whoa, slow down!' } }]
Discord formatting

Text formatting

SyntaxResultNotes
**bold**boldDouble asterisks
*italic*italicSingle asterisks
__underline__underlineDouble underscores
~~strikethrough~~strikethroughDouble tildes
`code`codeInline code
```code block```Code blockTriple backticks
> quoteBlock quoteGreater-than + space
||spoiler||||spoiler||Double pipes

Emoji & mentions

TypeFormatExample
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

container component
{ type: 'CONTAINER', components: [
  { type: 'TEXT_DISPLAY', content: '**Welcome!**' },
  { type: 'SEPARATOR', spacing: 'MEDIUM' },
  { type: 'TEXT_DISPLAY', content: 'Select an option below.' },
], color: 0x5865F2 }

Separator

separator
{ type: 'SEPARATOR', spacing: 'SMALL' }
{ type: 'SEPARATOR', spacing: 'MEDIUM', divider: true }

MediaGallery

media gallery
{ type: 'MEDIA_GALLERY', items: [
  { media: 'https://example.com/image.png', alt: 'Image' },
  { media: 'https://example.com/video.mp4' },
] }

Section

section
{ type: 'SECTION', text: { type: 'TEXT_DISPLAY', content: 'Section content' },
  accessory: { type: 'BUTTON', customId: 'section-btn', label: 'Click', style: 'PRIMARY' } }

Buttons

buttons
{ type: 'BUTTON', customId: 'action:confirm', label: 'Confirm', style: 'PRIMARY', disabled: false }
{ type: 'BUTTON', style: 'LINK', label: 'Visit', url: 'https://example.com' }

Rules

rules component
{ 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)

legacy embed
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] });
Reference

CLI

shiver CLI
npx shiver init
npx shiver dev
npx shiver start
npx shiver sync
npx shiver generate cmd
npx shiver test

Testing

test example
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:

custom ID patterns
'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

full command example
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
# 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

ModulePackageDescription
Shiver@shiver/coreMain framework entry point
Registry@shiver/coreCommand registry
SafeExecutor@shiver/coreError-safe async execution
Scheduler@shiver/coreCron and delayed tasks
BroadcastManager@shiver/coreCross-shard messaging
RequestDeduplicator@shiver/coreDuplicate request prevention
SQLiteAdapter@shiver/storageSQLite storage backend
PostgresAdapter@shiver/storagePostgreSQL storage backend
RedisAdapter@shiver/storageRedis storage backend
FunctionRegistry@shiver/aiAI function registration
AIContext@shiver/aiAI conversation management
PromptBuilder@shiver/aiStructured prompt construction
NaturalCommandRouter@shiver/aiNLP command routing
TicketSystem@shiver/systemsSupport ticket management
GiveawaySystem@shiver/systemsGiveaway creation and management
TagSystem@shiver/systemsCustom command tags
StarboardSystem@shiver/systemsStar-based message curation