Shiver Framework
High-performance Discord bot framework built on discord.js v14. The same foundation that powers Shiver - built so you can create your own bots with command loading, slash and prefix, middleware, storage, and production-ready defaults.
View on GitHub · Source and README are kept in sync with this page.
v2 - 30+ new modules including AI features, ComponentRouter, Scheduler, Systems, Guards, and more
Overview
Shiver Framework provides the infrastructure for Discord bots: command loading and dispatch, slash and prefix handling, middleware pipelines, storage, settings, health endpoints, and safe response handling.
It does not ship ready-made commands; you implement your bot logic on top of the framework.
- Minimal abstraction - You work with discord.js types and thin wrappers.
- Explicit over implicit - Commands are plain modules with a well-defined shape.
- Performance-first - Event deduplication, optional Redis coordination, efficient caching.
- Pluggable storage - JSON, Supabase, SQLite, or other backends.
Supported runtimes: Node.js 18+. Primary dependency: discord.js ^14.
Why Shiver Framework
Compared to other Discord frameworks, Shiver keeps a thin layer over discord.js so you keep full control. One file per command with name, data, executeSlash, executePrefix - no class boilerplate.
Slash and prefix are first-class with the same middleware and preconditions. Built-in event and prefix deduplication plus optional Redis lock prevent duplicate responses when multiple processes run.
Multi-instance detection warns you with a suggested pkill so you can stop all and restart once. Central safeRespond and defer handling avoid "interaction already replied" and timeout errors. Pluggable storage (JSON, Supabase, etc.) and a plugin system, event bus, and DI container round out extensibility.
| Aspect | Shiver Framework |
|---|---|
| Abstraction | Thin layer; you keep full control of interactions and messages. |
| Command format | Single file; name, data, executeSlash, executePrefix. No class boilerplate. |
| Deduplication | Built-in; optional Redis lock so only one process handles each message. |
| Response safety | Central safeRespond, defer strategies, safe edit/delete. |
| Storage | Pluggable adapter; settings and migrations on top. |
Download & install
The framework is open source and available on GitHub.
Use the link below and follow the commands for your operating system.
Repository
https://github.com/yuwxd/shiver-framework
Clone or download the repository, then install dependencies and use it in your bot project as shown below.
Linux / macOS (terminal)
$ cd shiver-framework
$ npm install
To use the framework in your bot project (from your bot folder):
Windows (PowerShell)
PS> cd shiver-framework
PS> npm install
From your bot project folder:
Windows (Command Prompt)
C:\> cd shiver-framework
C:\> npm install
Requirements
| Requirement | Version |
|---|---|
| Node.js | ≥ 18 |
| discord.js | ^14.25.1 |
Optional (only if you use the feature): redis (multi-instance, prefix dedup), canvas (images), @discordjs/voice + prism-media (voice), @supabase/supabase-js (Supabase storage), better-sqlite3 (SQLite).
Quick start
Create a client, configure the framework, call init(client), then client.login(process.env.DISCORD_TOKEN).
Commands are loaded from commandsPath; slash and prefix listeners are registered automatically.
const { ShiverFramework, ShiverClient } = require('shiver-framework');
const { GatewayIntentBits } = require('discord.js');
const framework = new ShiverFramework({
commandsPath: './src/commands',
prefix: ',',
ownerIds: ['YOUR_DISCORD_USER_ID']
});
const client = framework.createClient({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
async function main() {
await framework.init(client);
await client.login(process.env.DISCORD_TOKEN);
}
main().catch(console.error);
Creating your first command (step by step)
Step 1: Create the commands directory
Ensure your project has a folder for command files, e.g. src/commands. The framework loads all .js files from commandsPath (recursive; files whose name starts with _ are ignored).
Step 2: Create a command file
Create a new file src/commands/ping.js. Each command file must export a single object with at least name, data (for slash), and one or more of executeSlash, executePrefix.
Step 3: Define slash command data
Use SlashCommandBuilder from discord.js. Set name and description (required by Discord for slash commands).
Step 4: Implement executeSlash
Slash commands are deferred by default, so you must use interaction.editReply(...) to send the response. Do not call interaction.reply() unless you did not defer.
Step 5: Implement executePrefix (optional)
For prefix commands, use message.reply(...). The framework passes args (array of arguments after the command name) and commandName (the alias used).
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
name: 'ping',
aliases: ['p'],
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Check bot latency'),
async executeSlash(interaction, client) {
await interaction.editReply({ content: `Pong - ${client.ws.ping}ms` });
},
async executePrefix(message, args, client, commandName) {
await message.reply(`Pong - ${client.ws.ping}ms`);
}
};
Save the file. When you start the bot, the framework loads it automatically.
Sync slash commands to Discord (on first run or when you change slash definitions) with npx shiver-framework sync or by enabling slash sync in options.
Core concepts
After framework.init(client) you have:
| Property | Description |
|---|---|
framework.commands | Command registry: load, get slash/prefix, sync to Discord. |
framework.container | DI container (e.g. container.set('logger', logger)). |
framework.events | Event bus: CommandRun, CommandError, CommandBlocked, afterReady, etc. |
framework.stats | Counters and metrics. |
framework.health | Lifecycle state and optional HTTP health server. |
framework.reload | Reload commands from disk without restart. |
framework.modal | Helper for modals and parsing submissions. |
framework.assets | Load fonts and images from base dir. |
framework.settings | Guild/user settings backed by storage. |
framework.storage | Pluggable key-value storage adapter. |
framework.ping | Gateway and REST latency helper. |
framework.httpPush | Send JSON to external APIs or your website. |
Client and init flow
ShiverClient extends discord.js Client with sensible defaults: cache limits and sweepers, REST timeouts and retries, gateway compression. Use framework.createClient(overrides) or framework.getClientOptions(overrides).
Init flow: (1) init(client) binds the client and loads commands from commandsPath; (2) Registers interactionCreate and messageCreate listeners (deduplication prevents double-handling); (3) Optional multi-instance detector starts if enabled; (4) On ready, slash sync (if configured), afterReady, and health.markReady() run.
Command system
framework.commands is the command registry.
loadFromDirectory(dirPath)- Load all .js commands from a directory. Returns{ loaded, errors }.getSlash(name)- Get command by slash name.getPrefix(name)- Get command by prefix name or alias.getAllSlash()/getAllPrefix()- List all registered commands.syncToDiscord(client, options)- Register slash commands with Discord. Useoptions.guildIdsfor guild-specific sync ornullfor global.getSourcePath(name),removeBySourcePath(path)- For reload.
Slash and prefix flow
Slash: Normalize options → middleware chain (defer, lockdown, blacklist, TOS, premium, rate limit, cooldown, disabled, permissions, preconditions) → executeSlash. Emit CommandRun or CommandError.
Prefix: Skip bot messages; deduplicate by message.id (optional Redis tryAcquirePrefixMessage); resolve prefix via getPrefix(message, framework); parse args (quoted strings supported); same middleware chain → executePrefix.
Context menu and autocomplete
Use ContextMenuCommandBuilder and executeContextMenu for user/message context menus. For autocomplete, set options with setAutocomplete(true) and implement handleAutocomplete; call interaction.respond(choices).
Component handlers
Commands can expose handleButton, handleSelect, handleModalSubmit, etc. InteractionHandler matches customId prefixes to commands. Use framework.buildCustomId(prefix, command, action, userId) for consistent IDs.
Command file shape (reference)
| Field | Required | Description |
|---|---|---|
name | Yes (prefix) | Primary name for prefix dispatch. |
aliases | No | Array of prefix aliases, e.g. ['p']. |
data | Yes (slash) | SlashCommandBuilder or ContextMenuCommandBuilder. |
executeSlash(interaction, client) | No | Called for slash commands. Use editReply if deferred. |
executePrefix(message, args, client, commandName) | No | Called for prefix commands. |
executeContextMenu(interaction, client) | No | For user/message context menu commands. |
handleAutocomplete(interaction, client) | No | Call interaction.respond(choices). |
handleButton, handleSelect, handleModalSubmit | No | Component handlers; match by customId prefix. |
preconditions | No | Array of precondition names or configs. |
cooldown | No | Number or object for cooldown middleware. |
adminOnly | No | If true, command is not registered for prefix. |
deferStrategy, ephemeral | No | Override framework defaults for this command. |
Subcommands: use data.addSubcommand(sub => sub.setName('view').setDescription('...')). In executeSlash, use interaction.options.getSubcommand() and getSubcommandGroup(false) to branch.
Configuration reference
Options are deep-merged with defaults.
Guards and moderation
moderation.checkRoleHierarchy, checkTOS, buildTosReply, isBlacklisted, checkServerBlacklisted, isUserAllowed.
Storage and settings
storage.backend (json | memory | supabase | sqlite | mongodb), storage.path, settings.defaults.guild, settings.defaults.user.
migrationsPath - Directory with migration files (e.g. 001_add_users.js). When set, framework.migrations.run() runs them to evolve storage schema or data safely. If null or path missing, no migrations run.
Health and lifecycle
health.enabled, health.port, health.host, multiInstance (boolean or { groupId?, processMatchPattern? }).
Slash sync and hooks
slashSync.guildIds (null = global), registration.retryOnRateLimit, registration.maxRetries. Hooks: afterReady, afterSlashSync, onCommandRun, onCommandError, onCommandBlocked.
tryAcquirePrefixMessage - Optional async function (messageId) => boolean. Called before running a prefix command; return true to allow execution, false to skip. Use e.g. Redis SET NX so only one process handles each message when multiple bot instances run.
Middleware
Both slash and prefix flows run through a middleware chain. Order (slash): Defer → Lockdown → ServerBlacklist → Blacklist → TOS → RateLimit → Cooldown → Disabled → Permissions → preconditions → command. Prefix skips Defer.
Add custom middleware for every command:
framework.use(async (context, next) => {
const { interaction, message, command, client } = context;
await next();
});
Custom middleware
Use framework.use(fn) so the middleware runs for every slash and prefix command. Read or mutate context; call await next() to continue or omit it to stop the chain (and optionally send a reply).
Handlers and response safety
safeRespond(interaction, payload, options) - Central helper for replying. Uses followUp if already replied, editReply if deferred, otherwise reply. Use framework.followUp(interaction, payload) in your code.
Defer strategies: always (defer immediately), whenSlow (defer after threshold if no reply yet), never. Default is whenSlow with 1500 ms threshold.
Event deduplication: the framework ensures each interaction/message is handled only once; optional Redis lock prevents duplicate handling across multiple processes.
Storage and settings
framework.storage - Key-value adapter. Methods: get(namespace, key), set(namespace, key, value, ttl?), delete, has, keys, entries, etc. Backends: json, memory, supabase, sqlite, mongodb.
framework.settings - Guild and user settings. Methods: getGuild(guildId), setGuild, patchGuild, getUser, setUser, patchUser, getGuildPrefix(guildId, fallback?), setGuildPrefix, resetGuildPrefix.
Dynamic prefix example:
const framework = new ShiverFramework({
commandsPath: './src/commands',
prefix: ',',
async getPrefix(message, fw) {
if (!message.guildId) return ',';
return await fw.settings.getGuildPrefix(message.guildId, null) ?? ',';
}
});
Migrations
framework.migrations runs migration scripts from options.migrationsPath against the storage backend and tracks which have run so you can evolve schema or data safely.
Embed colors and user themes
framework.embedHelper lets you store per-user and per-command embed colors in storage so your bot can offer a /settings-style color picker. Colors are stored under the embed_colors namespace using setCommandEmbedColor(userId, color, commandName?) and read with getCommandEmbedColor(userId, commandName?). Use them when building embeds or Components v2 containers.
To parse user input such as hex, RGB, or named colors, use embedHelper.parseUserColor(input). Supported formats: #fff, #ffffff, 255, 0, 128, rgb(255, 0, 128), and names like default, success, error, warning, info, blurple, red, green, blue, yellow, orange, purple, cyan, magenta, white, black, gray/grey. Combine this with a settings command to let users choose default and per-command colors.
Health and custom API routes
framework.health tracks lifecycle and can run an HTTP server when health.enabled is true. Built-in routes: /health, /ready, /live, /metrics, /status.
Custom routes: Call health.addRoute(method, path, handler) before init.
The handler receives (req, res, url) and can return a value to send as JSON.
framework.health.addRoute('GET', '/api/bot', async (req, res, url) => {
return {
name: 'MyBot',
version: '1.0.0',
commands: framework.commands.getAllSlash().length
};
});
Enable the server with health: { enabled: true, port: 8080 } in framework options.
Assets
framework.assets (AssetLoader) uses options.assets.baseDir (default process.cwd()). Use setBaseDir(path), preload(directory, opts), registerAllFonts(), and getPath(name) for fonts and images in commands or image generation.
Components v2, modals, pagination
Builders (from components/v2/builders.js): textDisplay, separator, mediaGallery, container, buildMessageContainerV2, buildEmbedLikeV2, buildConfirmContainerV2, buildPaginatedContainerV2. Send with flags: MessageFlags.IsComponentsV2. framework.modal helps build and show modals; framework.paginate(interaction, pages, opts) and framework.confirm(interaction, question, opts) for pagination and yes/no flows.
Reload and debug
framework.reload clears require.cache for command files and calls loadFromDirectory(commandsPath) so you can reload without restart. When options.debug === true, framework.debugPanel (LiveDebugPanel) can attach to the client and log command runs and errors. Use syncToDiscord again after reload if slash definitions changed.
Stats and health
framework.stats records metrics (commands run, errors, messages, interactions); use recordCommandRun, recordCommandError, etc. framework.health tracks lifecycle (markStarting, markReady, markShuttingDown) and runs the HTTP server when enabled. Built-in routes: /health, /ready, /live, /metrics, /status.
Lifecycle and multi-instance
Register cleanup with framework.onShutdown(fn). On SIGINT/SIGTERM the framework runs shutdown handlers.
When multiInstance is enabled, a Redis heartbeat runs; if more than one process is detected, the framework logs once with a suggested pkill so you can stop all and restart. Uses REDIS_URL or REDIS_URI.
Voice and code execution
Voice: framework.voice (VoiceManager) for joining channels and playing audio; options: voice.maxBitrateKbps, maxDurationSeconds. Execute: ExecuteRunner for sandboxed code (e.g. Piston or local); options.execute.backend, timeoutMs, maxCodeLength.
Moderation, anti-crash, sharding
framework.moderation provides role hierarchy checks and helpers for kick, ban, timeout. framework.antiCrash listens for uncaughtException and unhandledRejection. When options.sharding.scriptPath is set, framework.sharding (ShardManager) can spawn a child process for the sharding launcher.
Plugins
framework.plugins (PluginManager): plugins.register(name, plugin), plugins.load(nameOrPlugin, options). A plugin has init(framework, options). Built-in plugins include slash-sync, backup-restore, feature-flags, error-reporter, stats-server, scheduled-tasks, i18n, webhook-logger, logger.
Events and container
framework.events emits CommandRun, CommandError, CommandBlocked, afterReady, afterSlashSync, commandReloaded, commandsReloaded. Subscribe with framework.events.on('CommandRun', ...). framework.container is a key-value DI container; the framework sets client, assets, etc.; your bot can container.set('logger', logger) and middleware/commands use container.get('logger').
Security
- Token: Load from
process.env.DISCORD_TOKEN. Never commit or log it. The framework uses log redaction (safeError,redactSecrets) so tokens never appear in console output. - User-facing errors: Show only generic messages (e.g. "This command is currently unavailable"). Log details server-side with
safeError. - HTTP push: Use
framework.httpPush(url, payload, opts)when sending data to external URLs; its error logging is redaction-safe.
Message edit and delete
safeEdit(message, payload, opts?) - Edits a message. Returns the edited message or null. Benign Discord errors (Unknown Message, Cannot edit) are not logged. opts.retryOnce: true retries once on 5xx/429.
safeDelete(message, opts?) - Deletes a message if message.deletable. Returns true/false. opts.reason for audit log.
MessageEditDeleteHelper - For high-frequency updates on the same message, use createMessageEditDeleteHelper({ debounceMs: 120 }). Call helper.edit(message, payload); multiple edits within the window are coalesced into one API call. Use helper.delete(message) to cancel pending edit, helper.flush(key) to send immediately.
CLI
Run from a project that depends on the framework (or from the framework directory):
| Command | Description |
|---|---|
npx shiver-framework validate [commandsDir] | Validate command files (default: src/commands). |
npx shiver-framework sync [token] [commandsDir] | Register slash commands with Discord. Token from args or DISCORD_TOKEN. |
npx shiver-framework generate <type> <name> [dir] | Scaffold command, listener, precondition, or system. type: command, listener, precondition, system. |
Testing
CommandTester (testing/CommandTester.js): run commands in isolation with tester.runSlash(commandName, interactionOverrides) or tester.runPrefix(commandName, messageOverrides, args). Mocks (testing/mocks.js): createMockInteraction, createMockMessage, createMockClient, createMockGuild, etc., plus MockStorage and MockEventBus.
Custom IDs and validation
Use framework.buildCustomId(prefix, command, action, userId) for consistent component customIds; framework.parseCustomId(customId) to parse.
Validation helpers: validateFrameworkConfig, validateCommandDefinition, validateCustomId, validatePayload. Helpers: createGenericErrorPayload, createWarningPayload, createSuccessPayload, createCooldownPayload, and others for consistent user-facing messages.
AI rules and development guidelines
Use async/await everywhere; defer early for slow slash commands. Prefer self-explanatory code; minimal code; single response path per flow.
Generic errors only for users; log details server-side with safeError. Ephemeral for sensitive content; consistent customIds; sensible collector timeouts. Respect Discord limits (25 MB per file, 10 files). Finish work fully; verify before finishing (run the bot and fix startup/runtime errors).
Examples
Ping (gateway and REST latency)
await framework.init(client);
const gatewayMs = framework.ping.getGatewayMs();
const restMs = await framework.ping.getRestMs();
const full = await framework.ping.getFullPing();
HTTP push (send JSON to your site or API)
const result = await framework.httpPush('https://your-site.com/api/stats', {
guilds: client.guilds.cache.size,
commands: framework.commands.getAllSlash().length
}, { method: 'POST', timeoutMs: 5000 });
Safe response (followUp)
const payload = { content: 'Done.', ephemeral: true };
await framework.followUp(interaction, payload);
FunctionRegistry NEW
Export all registered commands as OpenAI or Anthropic function-calling schemas. Lets AI agents know exactly what your bot can do and call it directly.
const { FunctionRegistry } = require('shiver-framework');
const registry = new FunctionRegistry();
registry.registerAllFromCommands(framework.commands.getAllSlash());
const tools = registry.exportOpenAITools();
const response = await openai.chat.completions.create({ model: 'gpt-4o', tools, messages });
const anthropicTools = registry.exportAnthropicTools();
registry.register('ban-user', 'Ban a user from the server', {
type: 'object',
properties: { userId: { type: 'string', description: 'Discord user ID' }, reason: { type: 'string' } },
required: ['userId']
}, async ({ userId, reason }) => banUser(userId, reason));
await registry.call('ban-user', { userId: '123456789', reason: 'Spam' });
| Method | Returns | Description |
|---|---|---|
register(name, desc, params, handler) | this | Register a custom function |
registerFromCommand(command) | this | Auto-register from a command file |
registerAllFromCommands(commands) | this | Register all commands at once |
exportOpenAITools() | Array | OpenAI tools format |
exportAnthropicTools() | Array | Anthropic tools format |
exportSchema() | Array | Raw JSON schema array |
call(name, args) | Promise<any> | Call a registered handler |
toJSON() | string | Formatted JSON schema string |
list() | string[] | All registered function names |
AIContext NEW
Snapshot the full Discord context from any interaction - guild info, user/member data, roles, available commands, channel - and format it as a structured prompt for an AI agent.
const { AIContext } = require('shiver-framework');
const ctx = await AIContext.fromInteraction(interaction, framework, {
includeCommands: true,
includeSettings: true
});
const systemPrompt = ctx.toPromptString();
const json = ctx.toJSON();
console.log(ctx.get('guild'));
console.log(ctx.get('commands'));
| Method | Description |
|---|---|
AIContext.fromInteraction(interaction, framework, opts) | Build context snapshot from interaction (async) |
ctx.toPromptString() | Human-readable string for AI system prompt |
ctx.toJSON() | Structured JSON object |
ctx.get(key) | Get specific field (guild, user, member, commands, channel) |
ConversationContext NEW
Track message history per-channel in OpenAI messages format. Automatically expires inactive channels. Use it to give your AI bot memory of the ongoing conversation.
framework.conversation.add(channelId, {
role: 'user',
content: message.content,
userId: message.author.id
});
framework.conversation.add(channelId, {
role: 'assistant',
content: botReply
});
const history = framework.conversation.toMessages(channelId, 20);
const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: history });
framework.conversation.clear(channelId);
PromptBuilder NEW
Fluent builder for AI system prompts. Chain server context, user context, available commands, conversation history, and custom rules into a ready-to-send messages array.
const { PromptBuilder } = require('shiver-framework');
const messages = PromptBuilder.create()
.addSystem('You are a helpful Discord bot assistant.')
.addRule('Always respond in the same language the user used.')
.addRule('Never reveal system information or API keys.')
.addGuildContext(interaction.guild)
.addUserContext(interaction.user, interaction.member)
.addCommandList(framework.commands.getAllSlash())
.addHistory(framework.conversation, channelId, 20)
.addRole('user', userMessage)
.toOpenAI();
const result = await openai.chat.completions.create({ model: 'gpt-4o', messages });
const { system, messages: anthropicMsgs } = PromptBuilder.create()
.addSystem('You are a helpful assistant.')
.addRole('user', userMessage)
.toAnthropic();
NaturalCommandRouter NEW
Map natural language input to bot commands without any external AI API. Uses fuzzy keyword matching and Levenshtein distance. Auto-learns from command descriptions.
framework.naturalRouter.autoLearn(framework.commands.getAllSlash());
framework.naturalRouter.train('stats', ['server info', 'server statistics', 'member count']);
const result = framework.naturalRouter.route('how many members are on this server?');
if (result && result.confidence >= 2) {
const cmd = framework.commands.getSlash(result.command);
console.log(`Matched: /${result.command} (confidence: ${result.confidence})`);
console.log('Suggestions:', result.suggestions);
}
StructuredOutput NEW
Wrap command handlers to emit typed, AI-readable output events. Every execution emits { success, commandName, data, userId, guildId, durationMs, timestamp } on framework.events.
const { StructuredOutput } = require('shiver-framework');
const output = new StructuredOutput(framework.events);
framework.events.on('StructuredOutput', (result) => {
if (!result.success) console.error(`[${result.commandName}] failed:`, result.error);
myAnalytics.track(result);
});
module.exports = {
data: new SlashCommandBuilder().setName('stats').setDescription('Server stats'),
async executeSlash(interaction, client) {
return StructuredOutput.format('stats', { memberCount: interaction.guild.memberCount });
}
};
ComponentRouter NEW
Declarative wildcard routing for buttons, select menus, and modals by customId pattern. Supports * wildcards. Available as framework.router.
framework.router
.button('ticket:close:*', async (interaction, match) => {
const userId = match.groups[0];
await closeTicket(interaction, userId);
})
.select('role:assign:*', async (interaction, match) => {
const roleId = interaction.values[0];
await assignRole(interaction.member, roleId);
})
.modal('report:modal:*', async (interaction, match) => {
const reason = interaction.fields.getTextInputValue('reason');
await handleReport(interaction, reason);
})
.any('confirm:*', async (interaction, match) => {
await interaction.deferUpdate();
});
client.on('interactionCreate', async interaction => {
if (interaction.isButton()) await framework.router.routeButton(interaction);
if (interaction.isStringSelectMenu()) await framework.router.routeSelect(interaction);
if (interaction.isModalSubmit()) await framework.router.routeModal(interaction);
});
WizardSession NEW
Multi-step guided interaction wizard with built-in state management, step navigation, and automatic timeout cleanup.
const { WizardSession } = require('shiver-framework');
const wizard = new WizardSession(interaction, [
{
name: 'ticket-reason',
async run(wizard, i) {
await i.reply({ content: 'Briefly describe your issue:', ephemeral: true });
const collected = await i.channel.awaitMessages({
filter: m => m.author.id === i.user.id, max: 1, time: 60000
});
wizard.setData('reason', collected.first()?.content ?? '');
}
},
{
name: 'ticket-priority',
async run(wizard, i) {
wizard.setData('priority', 'normal');
}
}
], {
timeoutMs: 120000,
onTimeout: async (wiz) => { /* clean up */ },
onCancel: async (wiz) => { /* handle cancel */ }
});
const data = await wizard.run();
console.log(data.reason, data.priority);
FormBuilder NEW
Fluent builder for Discord modals with built-in validation and field parsing. No manual TextInputBuilder boilerplate.
const { FormBuilder } = require('shiver-framework');
const { TextInputStyle } = require('discord.js');
const form = new FormBuilder('Submit Report', 'report:modal')
.addField('title', 'Report Title', { required: true, maxLength: 100 })
.addField('details', 'What happened?', {
style: TextInputStyle.Paragraph,
required: true,
minLength: 20
})
.addField('link', 'Message link', {
required: false,
regex: /^https:\/\/discord\.com\/channels\//
});
await interaction.showModal(form.build());
const submitted = await interaction.awaitModalSubmit({ time: 120000 });
const { values, errors, ok } = form.parse(submitted);
if (!ok) {
return submitted.reply({ content: errors[0].message, ephemeral: true });
}
await handleReport(values.title, values.details);
VoteManager NEW
Full in-memory poll/voting system. Create polls, accept votes, retrieve results with percentages and automatic winner calculation.
const { VoteManager } = require('shiver-framework');
const votes = new VoteManager();
const pollId = votes.create(channelId, ['Yes', 'No', 'Maybe'], {
title: 'Should we add a music bot?',
timeoutMs: 300000,
anonymous: true
});
votes.cast(pollId, userId, 0);
votes.retract(pollId, userId);
votes.cast(pollId, userId, 1);
const results = votes.getResults(pollId);
console.log(results.options);
console.log(`Winner: ${results.winner.label} with ${results.winner.votes} votes`);
const final = votes.end(pollId);
MessageCollector NEW
High-level helper for prompting a user and collecting their response in one call. Supports validation, multi-question sequences, and auto-cleanup.
const { MessageCollector } = require('shiver-framework');
const { content } = await MessageCollector.prompt(
channel, userId, 'What is your preferred username?',
{
timeoutMs: 30000,
validator: (value) => value.length < 3 ? 'Username must be at least 3 characters.' : true
}
);
const answers = await MessageCollector.sequence(channel, userId, [
{ key: 'name', question: 'What is your name?' },
{ key: 'age', question: 'How old are you?', validator: v => isNaN(v) ? 'Enter a number.' : true }
]);
console.log(answers.name, answers.age);
UserSessionStore NEW
Per-user key-value session storage with TTL and automatic cleanup. Available as framework.sessions.
framework.sessions.set(userId, 'step', 'confirm', 60000);
framework.sessions.set(userId, 'pendingBanTarget', targetUserId);
const step = framework.sessions.get(userId, 'step');
const all = framework.sessions.getAll(userId);
if (framework.sessions.has(userId, 'pendingBanTarget')) {
framework.sessions.touch(userId);
const target = framework.sessions.get(userId, 'pendingBanTarget');
framework.sessions.delete(userId, 'pendingBanTarget');
}
Scheduler NEW
Named interval, timeout, and cron-like tasks. Auto-destroyed on framework.shutdown(). Available as framework.scheduler.
framework.scheduler.every(30000, 'heartbeat', async () => {
console.log('[heartbeat] ping:', framework.client.ws.ping);
});
framework.scheduler.cron('0 9 * * *', 'daily-report', async () => {
await reportChannel.send('Good morning! Daily report:');
});
framework.scheduler.once(5000, 'startup-announce', async () => {
await announceChannel.send('Bot is online!');
});
framework.scheduler.pause('heartbeat');
framework.scheduler.resume('heartbeat');
framework.scheduler.cancel('heartbeat');
console.log(framework.scheduler.list());
BroadcastManager NEW
Rate-safe mass messaging to multiple channels, guilds, or users with configurable delay between sends. Available as framework.broadcast.
const { sent, failed } = await framework.broadcast.send(
['channelId1', 'channelId2', 'channelId3'],
{ content: 'Maintenance in 10 minutes.' }
);
console.log(`Sent: ${sent.length}, Failed: ${failed.length}`);
await framework.broadcast.sendToGuilds(
['guildId1', 'guildId2'],
async (guild) => guild.systemChannel,
{ content: 'Thanks for using our bot!' }
);
await framework.broadcast.dm(
['userId1', 'userId2'],
{ content: 'Your subscription has been renewed.' }
);
RequestDeduplicator NEW
Deduplicate parallel identical async calls. If two commands run simultaneously and both fetch guild settings, only one DB call is made and both get the same result.
const { RequestDeduplicator } = require('shiver-framework');
const dedup = new RequestDeduplicator({ ttlMs: 500 });
async function getGuildSettings(guildId) {
return dedup.run(`settings:${guildId}`, () => db.fetchSettings(guildId));
}
dedup.invalidate(`settings:${guildId}`);
dedup.clear();
SafeExecutor NEW
Three composable wrappers for safe async execution: safeRun (full), withRetry (retry only), withTimeout (timeout only).
const { safeRun, withRetry, withTimeout } = require('shiver-framework');
const data = await safeRun(() => fetchExternalAPI(), {
retries: 3,
timeoutMs: 5000,
delayMs: 500,
onError: (err) => console.error('[API] error:', err.message),
fallback: null
});
const result = await withRetry(() => db.query(), 3, 300);
const val = await withTimeout(() => heavyOperation(), 10000);
FeatureFlagManager NEW
Per-guild, per-user, per-channel, and global feature flags stored in your storage backend. Hierarchy: user override > guild override > global default. Available as framework.flags.
framework.flags
.define('new-leveling', { default: false, description: 'New XP algorithm rollout' })
.define('beta-music', { default: false });
const enabled = await framework.flags.isEnabled('new-leveling', { guildId });
if (enabled) {
await newLevelingLogic(interaction);
} else {
await legacyLevelingLogic(interaction);
}
await framework.flags.enable('beta-music', { guildId: '123456789' });
await framework.flags.disable('beta-music', { userId: specificUserId });
const all = await framework.flags.getAll({ guildId });
CommandDisabledManager NEW
Disable or enable specific commands per-guild at runtime through storage. Works together with EnabledPrecondition.
const { CommandDisabledManager } = require('shiver-framework');
const cmdDisabled = new CommandDisabledManager(framework.storage);
await cmdDisabled.disable('music', guildId);
await cmdDisabled.enable('music', guildId);
const isOff = await cmdDisabled.isDisabled('music', guildId);
const list = await cmdDisabled.getDisabledCommands(guildId);
await cmdDisabled.disableAll(guildId, ['music', 'giveaway']);
HelpGenerator NEW
Auto-generate formatted help text from slash command definitions. No manual documentation needed.
const { HelpGenerator } = require('shiver-framework');
const text = HelpGenerator.generate(framework.commands.getSlash('report'));
const list = HelpGenerator.generateList(framework.commands.getAllSlash());
const modCmds = HelpGenerator.generateCategory(framework.commands.getAllSlash(), 'moderation');
CommandSuggester NEW
Fuzzy matching for prefix command typos using Levenshtein distance. Returns top-N closest matches with distance scores.
const { CommandSuggester } = require('shiver-framework');
const suggester = new CommandSuggester();
const suggestions = suggester.suggest('stast', framework.commands.getAllSlash(), 3);
if (suggestions.length) {
await message.reply(`Did you mean: ${suggestions.map(s => \`\`,\${s.command}\`\`).join(', ')}?`);
}
DiffTracker NEW
Compare two objects and format the diff as Discord-friendly text. Perfect for settings changed confirmations.
const { diff, formatDiff } = require('shiver-framework');
const before = { color: '#5865F2', prefix: ',', language: 'en' };
const after = { color: '#ED4245', prefix: '!', language: 'en', theme: 'dark' };
const changes = diff(before, after);
const text = formatDiff(changes);
// ~ **color**: `#5865F2` to `#ED4245`
// ~ **prefix**: `,` to `!`
// + **theme**: `dark`
AlertManager NEW
Define metric-based alerts with polling, thresholds, comparison operators, and cooldowns. Integrates with StatsManager. Available as framework.alerts.
framework.alerts
.define('high-errors', stats => stats.get('errors') ?? 0, 50, async ({ value }) => {
await alertChannel.send(`High error rate: ${value} errors`);
}, { cooldownMs: 300000, compare: 'gt' })
.define('low-guilds', stats => stats.get('guilds') ?? 0, 5, async ({ value }) => {
console.warn(`Only ${value} guilds left!`);
}, { compare: 'lt' });
framework.alerts.startPolling(60000);
await framework.alerts.checkAll();
framework.alerts.stopPolling();
LocaleSync NEW
Apply i18n localizations to slash command definitions for Discord's built-in localization. Reads from your I18n instance.
const { localizeCommand, localizeAll } = require('shiver-framework');
const locales = ['pl', 'de', 'fr'];
const localizedData = localizeCommand(command.data, framework.i18n, locales);
const allLocalized = localizeAll(framework.commands.getAllSlash(), framework.i18n, locales);
ProgressBar NEW
Text-based progress bars and multi-bar displays for Components v2 output.
const { buildProgressBar, buildMultiBar } = require('shiver-framework');
buildProgressBar(750, 1000, { width: 20, showFraction: true });
// ███████████████░░░░░ 75% (750/1000)
buildProgressBar(30, 100, { filled: '▓', empty: '░', prefix: 'XP', showPercent: true });
// XP ▓▓▓▓▓▓░░░░░░░░░░░░░░ 30%
const multibar = buildMultiBar([
{ label: 'CPU', current: 72, total: 100 },
{ label: 'Memory', current: 4200, total: 8192 },
{ label: 'Disk', current: 250, total: 500 }
]);
TableBuilder NEW
Build ASCII tables and export as Discord code blocks.
const { TableBuilder } = require('shiver-framework');
const table = new TableBuilder()
.addColumn('rank', '#', { width: 4, align: 'right' })
.addColumn('name', 'Player', { width: 18 })
.addColumn('score', 'Score', { width: 8, align: 'right' })
.addRow({ rank: '1', name: 'Alice', score: '9800' })
.addRow({ rank: '2', name: 'Bob', score: '7400' })
.addSeparator()
.addRow({ rank: '3', name: 'Charlie', score: '5200' });
await interaction.reply({ content: table.toCodeBlock(), flags: [] });
ListBuilder NEW
Formatted Discord-markdown lists with sections and per-item bold/code styling.
const { ListBuilder } = require('shiver-framework');
const text = new ListBuilder()
.addSection('Server Info')
.add('Name', guild.name, { bold: true })
.add('Members', guild.memberCount)
.addBlank()
.addSection('Settings')
.add('Prefix', framework.options.prefix, { code: true })
.add('Language', 'en')
.build();
TicketSystem NEW
Full ticket channel system: open private channels, assign support roles, close with optional transcript generation.
const { TicketSystem } = require('shiver-framework');
const tickets = new TicketSystem(framework.storage, {
supportRoles: ['supportRoleId'],
categoryId: 'ticketCategoryId'
});
const { channel, ok } = await tickets.open(guild, userId, {
welcome: { content: 'Support will be with you shortly.' }
});
const existingChannel = await tickets.isOpen(guildId, userId);
const { transcript } = await tickets.close(channel, { deleteChannel: true });
console.log(transcript.map(m => `[${m.author}] ${m.content}`).join('\n'));
GiveawaySystem NEW
Full giveaway system with persistent entries, automatic draw timer, and reroll support.
const { GiveawaySystem } = require('shiver-framework');
const giveaways = new GiveawaySystem(framework.storage);
const { id } = await giveaways.start(channel, {
prize: 'Discord Nitro',
winnersCount: 2,
durationMs: 3600000
});
await giveaways.enter(id, userId);
await giveaways.leave(id, userId);
const result = await giveaways.draw(id);
console.log(`Winners: ${result.winners.join(', ')}`);
const rerolled = await giveaways.reroll(id, 1);
console.log(`New winner: ${rerolled.winners[0]}`);
TagSystem NEW
Per-guild custom text tags with variable interpolation. Think YAGPDB-style custom commands backed by your storage.
const { TagSystem } = require('shiver-framework');
const tags = new TagSystem(framework.storage);
await tags.create(guildId, 'rules', 'Welcome {user}! Please read <#channelId>.', userId);
await tags.create(guildId, 'ping', 'Pong! The bot is online.', userId);
const content = await tags.use(guildId, 'rules', { user: interaction.user.username });
await interaction.reply(content);
const allTags = await tags.list(guildId);
const results = await tags.search(guildId, 'rule');
await tags.update(guildId, 'rules', 'New rules content here.');
await tags.delete(guildId, 'rules');
StarboardSystem NEW
Auto-posts popular messages to a starboard channel when they reach a configurable reaction threshold.
const { StarboardSystem } = require('shiver-framework');
const starboard = new StarboardSystem(framework.storage, client);
await starboard.configure(guildId, starboardChannelId, 3, '⭐');
client.on('messageReactionAdd', async (reaction, user) => {
await starboard.handleReaction(reaction, user);
});
const alreadyPosted = await starboard.isPosted(message.id);
Guards NEW
Lightweight, composable guards for quick checks inside command handlers. Each guard has a .check() method and an optional .middleware() adapter.
const { OwnerGuard, GuildGuard, ChannelGuard, RoleGuard, TimeGuard, RateLimitGuard } = require('shiver-framework');
const ownerGuard = new OwnerGuard(['ownerId1', 'ownerId2']);
if (!ownerGuard.check(interaction.user.id)) {
return interaction.reply({ content: 'Owner only.', ephemeral: true });
}
const guildGuard = new GuildGuard(['allowedGuildId']);
const channelGuard = new ChannelGuard(['allowedChannelId']);
const modRole = new RoleGuard(['modRoleId', 'adminRoleId']);
if (!modRole.check(interaction.member)) {
return interaction.reply({ content: 'Mod only.', ephemeral: true });
}
const supportHours = new TimeGuard(9, 17, { timezone: 'Europe/Warsaw' });
if (!supportHours.check()) {
return interaction.reply({ content: 'Support is only available 9-17 Warsaw time.', ephemeral: true });
}
const spamGuard = new RateLimitGuard(3, 10000);
if (!spamGuard.check(interaction.user.id)) {
return interaction.reply({ content: 'Too many requests. Try again soon.', ephemeral: true });
}
Discord text formatting
Complete reference for Discord's markdown syntax. All of these work in regular messages and in Components v2 TextDisplay fields.
| Format | Syntax | Result |
|---|---|---|
| Bold | **text** | text |
| Italic | *text* or _text_ | text |
| Underline | __text__ | text |
| Strikethrough | ~~text~~ | |
| Spoiler | ||text|| | hidden spoiler |
| Inline code | `text` | text |
| Code block | ```js\ncode\n``` | highlighted block |
| Header H1 | # Title | large heading |
| Header H2 | ## Title | medium heading |
| Header H3 | ### Title | small heading |
| Small header | -# text | small muted header |
| Blockquote | > text | single-line quote |
| Multi blockquote | >>> text | multi-line quote |
| Bold + italic | ***text*** | text |
| Masked link | [label](url) | clickable link text |
| List item | - item | bullet list |
| Timestamp | <t:1234567890:R> | dynamic time (R=relative, F=full, D=date, T=time, d=short date) |
{ type: ComponentType.TextDisplay, content: [
'# Server Stats',
'',
'> **Members:** 1,234',
'> **Online:** 456',
'',
'-# Updated <t:' + Math.floor(Date.now()/1000) + ':R>'
].join('\n') }
Emoji & mentions
Format custom emoji and user/role/channel mentions in Discord messages.
| Type | Syntax | Notes |
|---|---|---|
| Custom emoji (static) | <:name:id> | e.g. <:shiver:1234567890> |
| Custom emoji (animated) | <a:name:id> | e.g. <a:loading:987654321> |
| Mention user | <@userId> | Pings the user |
| Mention role | <@&roleId> | Pings the role |
| Mention channel | <#channelId> | Links the channel |
| @everyone | @everyone | Requires permission |
| @here | @here | Online members only |
const status = online ? '<:online:1234567890>' : '<:offline:9876543210>';
const loading = '<a:loading:1122334455>';
await interaction.reply({
content: `${loading} Fetching data for <@${userId}> in <#${channelId}>...`
});
Components v2 guide
Components v2 replaces embeds with a composable component tree. Messages use MessageFlags.IsComponentsV2 and cannot contain embeds, content, stickers, or poll.
Container
const { ComponentType, MessageFlags } = require('discord.js');
const container = {
type: ComponentType.Container,
accentColor: 0x5865f2,
components: [
{ type: ComponentType.TextDisplay, content: '# Hello World\n> Welcome to the server!' },
{ type: ComponentType.Separator, divider: true, spacing: 2 },
{ type: ComponentType.TextDisplay, content: 'This is the main content.' }
]
};
await interaction.reply({ components: [container], flags: MessageFlags.IsComponentsV2 });
Separator
{ type: ComponentType.Separator, divider: true, spacing: 2 }
// divider: true = visible line; false = spacing only
// spacing: 1 = Small, 2 = Large
MediaGallery (images)
{ type: ComponentType.MediaGallery, items: [
{ media: { url: 'attachment://shiver.png' }, description: 'Alt text' }
] }
await interaction.reply({
components: [container],
files: [new AttachmentBuilder(buffer, { name: 'shiver.png' })],
flags: MessageFlags.IsComponentsV2
});
Section (text + thumbnail side-by-side)
{ type: ComponentType.Section,
components: [{ type: ComponentType.TextDisplay, content: '**Alice** - Level 42\n> XP: 8,400' }],
accessory: { type: ComponentType.Thumbnail, media: { url: 'https://cdn.discordapp.com/avatars/...' } }
}
Buttons in ActionRow
const { ButtonStyle } = require('discord.js');
{ type: ComponentType.ActionRow, components: [
{ type: ComponentType.Button, label: 'Confirm', style: ButtonStyle.Success, customId: 'confirm:yes:' + userId },
{ type: ComponentType.Button, label: 'Cancel', style: ButtonStyle.Secondary, customId: 'confirm:no:' + userId },
{ type: ComponentType.Button, label: 'GitHub', style: ButtonStyle.Link, url: 'https://github.com' }
] }
StringSelect dropdown
{ type: ComponentType.StringSelect,
customId: 'settings:theme:' + userId,
placeholder: 'Choose a theme...',
minValues: 1, maxValues: 1,
options: [
{ label: 'Dark', value: 'dark', description: 'Dark mode', emoji: { name: '🌙' }, default: true },
{ label: 'Light', value: 'light', description: 'Light mode', emoji: { name: '☀️' } }
]
}
Rules
- Always set
IsComponentsV2on bothdeferReply({ flags })andeditReply. - Never mix with
embeds,content,stickers, orpollin the same message. - Filename in
AttachmentBuildermust exactly match theattachment://reference. accentColoraccepts a decimal integer, not a hex string.- Max 5 ActionRows per message, max 5 buttons per ActionRow.
Embeds (legacy EmbedBuilder)
Use for legacy compatibility only. All new commands should use Components v2 instead.
const { EmbedBuilder } = require('discord.js');
const embed = new EmbedBuilder()
.setTitle('Server Info')
.setDescription('> **Members:** 1,234\n> **Online:** 456')
.addFields(
{ name: 'Owner', value: '<@ownerId>', inline: true },
{ name: 'Created', value: '<t:1609459200:D>', inline: true }
)
.setColor(0x5865f2)
.setThumbnail(guild.iconURL())
.setImage('https://example.com/banner.png')
.setFooter({ text: 'Shiver Framework', iconURL: client.user.displayAvatarURL() })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
AI rules
Universal guidelines for AI assistants working on Discord bot projects built with Shiver Framework. Copy the block below into your AI system prompt, AGENTS.md, .cursorrules, or .windsurfrules.
# Shiver Framework - AI Rules
## Language & Communication
- Communicate with the user in the same language they use to talk to you.
- Use English for all code: variable names, function names, file names, and comments.
- If the user does not specify a language for communication, default to English.
- Questions are always welcome. Ask clarifying questions immediately when something is ambiguous.
- Be 100% aligned with the user's intent. Confirm understanding before implementing if anything is unclear.
- Never respond with acknowledgment phrases like "Great!", "Sure!", "Understood!" - go directly to work.
- Always finish the task completely without stopping to ask "should I continue?".
## Code Quality
- All functions must be async/await. Never use .then() or callbacks.
- No code comments. Write self-explanatory code through proper naming and structure.
- Write the absolute minimum code needed. No extra features, no unused variables.
- Always finish work fully. Do not stop midway or leave stubs.
- Before marking a task done, verify the project starts cleanly with no errors.
## Discord Bot Standards
- All new command output must use Components v2 (ContainerBuilder / raw API objects with ComponentType).
Never use EmbedBuilder for primary command output in new code.
- Never show raw error messages, stack traces, or technical details to Discord users.
Use generic user-facing messages and log technical details server-side only.
- All sensitive, private, or user-specific replies must be sent with MessageFlags.Ephemeral.
- For slash commands that do I/O or heavy computation, call deferReply at the very start
(before any await) so Discord receives an acknowledgment within 3 seconds.
- Use a consistent customId scheme: command:action:userId (e.g. ticket:close:123456789).
- Collectors must have sensible timeout and idle durations. Always clean up in the "end" handler.
- Respect Discord limits: 25MB per file, 10 files per message, 5 buttons per ActionRow.
## Components v2 Rules
- Never mix components messages with embeds, content, stickers, or poll in the same reply.
- Always set MessageFlags.IsComponentsV2 on both deferReply AND editReply.
- The filename in AttachmentBuilder must exactly match the attachment:// reference.
- accentColor accepts a decimal integer (0x5865f2), not a hex string.
- Use Separator with divider: true for visual lines, divider: false for spacing only.
## Security
- Never expose API keys, tokens, secrets, or stack traces in user-facing messages.
- Log errors server-side (console.error) only. Redact sensitive data in all logs.
- All attachment filenames must use the format: shiver.(ext) or shiver_1.png for multiples.
## Framework First
- When shiver-framework is missing a generic capability, improve the framework first
instead of adding a bot-specific workaround.
- Do not duplicate infrastructure in the bot if the concern belongs in the framework.
- Framework changes must remain broadly useful and reusable - no bot-specific hacks.
## Workspace Facts (customize for your project)
- Commands path: src/commands/
- Entry point: src/index.js
- Framework: shiver-framework (linked locally or installed from npm)
- Bot prefix: , (comma)
- Storage backend: json (or supabase / sqlite / mongo)
Module index
| Area | Path |
|---|---|
| Entry, init | src/index.js |
| Command registry | src/core/CommandRegistry.js |
| Handlers | src/handlers/SlashHandler.js, PrefixHandler, InteractionHandler, AutocompleteHandler, ContextMenuHandler |
| safeRespond | src/handlers/safeRespond.js |
| safeEdit, safeDelete, MessageEditDeleteHelper | src/utils/Helpers.js, src/utils/MessageEditDeleteHelper.js |
| Middleware | src/middleware/*.js |
| Storage, settings | src/storage/StorageAdapter.js, src/settings/SettingsManager.js |
| Health (addRoute) | src/lifecycle/Health.js |
| Ping, HTTP push | src/utils/PingHelper.js, src/utils/httpPush.js |
| Security, redaction | src/security/redact.js |
| CLI | src/cli/index.js |
| AI - FunctionRegistry | src/ai/FunctionRegistry.js |
| AI - AIContext | src/ai/AIContext.js |
| AI - ConversationContext | src/ai/ConversationContext.js |
| AI - PromptBuilder | src/ai/PromptBuilder.js |
| AI - NaturalCommandRouter | src/ai/NaturalCommandRouter.js |
| AI - StructuredOutput | src/ai/StructuredOutput.js |
| Interaction - ComponentRouter | src/routing/ComponentRouter.js |
| Interaction - WizardSession | src/wizard/WizardSession.js |
| Interaction - FormBuilder | src/forms/FormBuilder.js |
| Interaction - VoteManager | src/voting/VoteManager.js |
| Interaction - MessageCollector | src/collectors/MessageCollector.js |
| Interaction - UserSessionStore | src/sessions/UserSessionStore.js |
| Infrastructure - Scheduler | src/scheduler/Scheduler.js |
| Infrastructure - BroadcastManager | src/broadcast/BroadcastManager.js |
| Infrastructure - RequestDeduplicator | src/cache/RequestDeduplicator.js |
| Infrastructure - SafeExecutor | src/utils/SafeExecutor.js |
| Infrastructure - CommandDisabledManager | src/commands/CommandDisabledManager.js |
| Infrastructure - FeatureFlagManager | src/flags/FeatureFlagManager.js |
| DX - HelpGenerator | src/help/HelpGenerator.js |
| DX - CommandSuggester | src/help/CommandSuggester.js |
| DX - DiffTracker | src/utils/DiffTracker.js |
| DX - AlertManager | src/alerts/AlertManager.js |
| DX - LocaleSync | src/locale/LocaleSync.js |
| UI - ProgressBar | src/ui/ProgressBar.js |
| UI - TableBuilder | src/ui/TableBuilder.js |
| UI - ListBuilder | src/ui/ListBuilder.js |
| Systems - TicketSystem | src/systems/TicketSystem.js |
| Systems - GiveawaySystem | src/systems/GiveawaySystem.js |
| Systems - TagSystem | src/systems/TagSystem.js |
| Systems - StarboardSystem | src/systems/StarboardSystem.js |
| Guards | src/guards/Guards.js |
This page is the reference for the framework. For the repository and README, see github.com/yuwxd/shiver-framework.