npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vimcord

v2.0.7

Published

A powerful and opinionated Discord.js wrapper.

Downloads

638

Readme

██╗   ██╗██╗███╗   ███╗ ██████╗ ██████╗ ██████╗ ██████╗
██║   ██║██║████╗ ████║██╔════╝██╔═══██╗██╔══██╗██╔══██╗
██║   ██║██║██╔████╔██║██║     ██║   ██║██████╔╝██║  ██║
╚██╗ ██╔╝██║██║╚██╔╝██║██║     ██║   ██║██╔══██╗██║  ██║
 ╚████╔╝ ██║██║ ╚═╝ ██║╚██████╗╚██████╔╝██║  ██║██████╔╝
  ╚═══╝  ╚═╝╚═╝     ╚═╝ ╚═════╝ ╚═════╝ ╚═╝  ╚═╝╚═════╝

vhem-cord - a small Discord.js framework for typed modules, command dispatching, and reusable UX tools.

npm version npm downloads TypeScript License Discord.js

Installation · Quick Start · Features · API · Examples


What's Vimcord?

Vimcord wraps Discord.js with a focused module layer, typed command contexts, shared permission checks, global hooks, and UX helpers for embeds, prompts, modals, components, and pagination.

It does not hide Discord.js. You still use Discord.js builders, intents, events, permissions, and interactions directly where that is the right tool.


Installation

pnpm add vimcord discord.js

Optional plugins:

pnpm add @vimcord/plugin-dotenv
pnpm add @vimcord/plugin-mongoose

Quick Start

Minimum Client

import { GatewayIntentBits } from "discord.js";
import { Vimcord } from "vimcord";

const client = new Vimcord({
    client: {
        intents: [GatewayIntentBits.Guilds]
    }
});

await client.login();

login() uses TOKEN, or TOKEN_DEV when client.$devMode is true.

Process CLI

Call setupCLI() once to enable promptless slash commands through the process stdin. This works in a local terminal and in hosting dashboards that provide a console command field.

import { GatewayIntentBits } from "discord.js";
import { setupCLI, Vimcord } from "vimcord";

setupCLI({
    loaders: "auto" // "auto" | "always" | "never"
});

const client = new Vimcord({
    customId: "main",
    client: {
        intents: [GatewayIntentBits.Guilds]
    }
});

Clients participate by default once the process CLI is initialized. Set globals.app.enableCLI to false on clients that should remain unavailable to CLI commands. Enter /help in the process console to view commands.

Modules And Command Dispatch

import { GatewayIntentBits } from "discord.js";
import { Vimcord } from "vimcord";
import { DotEnvPlugin } from "@vimcord/plugin-dotenv";

const client = new Vimcord({
    client: {
        intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent]
    },
    globals: {
        app: {
            name: "My Bot"
        },
        staff: {
            ownerId: "123456789012345678",
            superUsers: ["234567890123456789"]
        }
    },
    connectionRefresh: {
        interval: 60_000,
        maxFailures: 2
    },
    verbose: true
});

client.use(new DotEnvPlugin());

await client.modules.load({
    slashCommands: { dir: "./src/commands/slash", suffix: ".slash" },
    prefixCommands: { dir: "./src/commands/prefix", suffix: ".prefix" },
    events: { dir: "./src/events", suffix: ".event" }
});

client.on("interactionCreate", interaction => {
    void client.modules.commands.dispatchInteraction(interaction);
});

client.on("messageCreate", message => {
    if (message.author.bot) return;
    void client.modules.commands.dispatchMessage(message, ["!"]);
});

await client.login();
await client.modules.commands.push();

Features

Slash Commands

import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
import { SlashCommandModule } from "vimcord";

export default new SlashCommandModule({
    builder: new SlashCommandBuilder()
        .setName("manage")
        .setDescription("Server management")
        .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
        .addSubcommand(subcommand => subcommand.setName("ban").setDescription("Ban a user")),

    routes: [
        {
            path: "ban",
            deferReply: { flags: "Ephemeral" },
            async handler({ interaction }) {
                await interaction.editReply("Ban flow started.");
            }
        }
    ],

    permissions: {
        client: [PermissionFlagsBits.BanMembers]
    }
});

Prefix Commands

import { PrefixCommandModule } from "vimcord";

export default new PrefixCommandModule({
    name: "ping",
    aliases: ["p"],
    async execute({ message, messageContent, splitContent }) {
        const args = splitContent({ lowercase: true });
        await message.reply(`Pong. You sent ${args.length} arg${args.length === 1 ? "" : "s"}.`);
    }
});

Event Modules

import { Events } from "discord.js";
import { EventModule } from "vimcord";

export default new EventModule({
    name: "logMessages",
    event: Events.MessageCreate,
    async execute({ args: [message] }) {
        if (message.author.bot) return;
        console.log(`${message.author.tag}: ${message.content}`);
    }
});

Global Command Hooks

import { defineGlobalCommandHooks } from "vimcord";

defineGlobalCommandHooks({
    slash: {
        async onError({ interaction, error }) {
            const content = `Something went wrong: ${error?.message ?? "Unknown error"}`;

            if (interaction.replied || interaction.deferred) {
                await interaction.followUp({ content, flags: "Ephemeral" });
                return;
            }

            await interaction.reply({ content, flags: "Ephemeral" });
        }
    }
});

Command-local hooks override client.globals.hooks, and client.globals.hooks override defineGlobalCommandHooks().

BetterEmbed

import { BetterEmbed, defineGlobalUxConfig } from "vimcord";

defineGlobalUxConfig({
    embedColor: "#5865F2",
    embedColorDev: "#FF9D00"
});

const embed = new BetterEmbed({
    context: { interaction },
    title: "Welcome, $USER_NAME",
    description: [showAvatar && "Your avatar: $USER_AVATAR", "", "Today is $MONTH/$DAY/$YEAR"],
    timestamp: true
});

await embed.send(interaction);

BetterContainer And Components V2

import { ButtonStyle } from "discord.js";
import { BetterContainer } from "vimcord";

const container = new BetterContainer({ color: "#5865F2" })
    .addText("## Account linked")
    .addText(["Your Discord account is connected.", "Use the dashboard button to manage settings."])
    .addSection({
        text: "Open your dashboard.",
        button: {
            customId: "dashboard:open",
            label: "Dashboard",
            style: ButtonStyle.Primary
        }
    });

await container.send(interaction);

Paginator

import { ButtonBuilder, ButtonStyle } from "discord.js";
import { BetterContainer, Paginator, PaginationTimeout, PaginationType } from "vimcord";

const intro = new BetterContainer().addText("## Help").addText("Choose a page below.");
const moderation = new BetterContainer().addText("## Moderation").addText("Ban, kick, and timeout commands.");

const paginator = new Paginator({
    type: PaginationType.LongSkip,
    skipSize: 5,
    idle: 60_000,
    onTimeout: PaginationTimeout.DisableComponents
});

paginator
    .addChapter([intro, "Use `/help command` for command-specific help."], { label: "General", emoji: "📖" })
    .addChapter([moderation], { label: "Moderation", emoji: "🛡️" });

paginator.insertButtonAt(
    0,
    new ButtonBuilder().setCustomId("help:feedback").setLabel("Feedback").setStyle(ButtonStyle.Secondary)
);

paginator.on("pageChange", (_page, index) => {
    console.log(`Viewing chapter ${index.chapter}, page ${index.nested}`);
});

// Custom component handlers receive the original interaction without an automatic acknowledgement.
paginator.on("help:feedback", async buttonInteraction => {
    await buttonInteraction.reply({ content: "Thanks for your feedback!", flags: "Ephemeral" });
});

await paginator.send(interaction);

Custom paginator handlers must call showModal, update, deferUpdate, reply, or otherwise acknowledge the interaction within Discord's response window. To acknowledge a custom interaction before its handler runs, pass { deferUpdate: true } as the third argument to paginator.on().

Prompt

import { BetterEmbed, promptMessage, ResolveAction } from "vimcord";

const result = await promptMessage(interaction, {
    embed: new BetterEmbed({
        context: { interaction },
        title: "Delete this message?",
        description: "This action cannot be undone."
    }),
    participants: [interaction.user],
    timeout: 30_000,
    onConfirm: ResolveAction.DisableComponents,
    onReject: ResolveAction.DisableComponents,
    onTimeout: ResolveAction.DeleteMessage,
    highlightSelectedButton: true
});

if (result.confirmed) {
    await targetMessage.delete();
}

BetterModal

import { TextInputStyle } from "discord.js";
import { BetterModal } from "vimcord";

const modal = new BetterModal({ title: "Create Ticket" })
    .addTextDisplay({
        content: "Please provide the details below so we can help."
    })
    .addTextInput({
        customId: "subject",
        label: "Subject",
        style: TextInputStyle.Short,
        required: true
    })
    .addTextInput({
        customId: "description",
        label: "Description",
        style: TextInputStyle.Paragraph,
        required: false
    })
    .addUserSelect({
        customId: "assignees",
        label: "Assignees",
        maxValues: 3,
        required: false
    });

const result = await modal.showAndAwait(interaction, { timeout: 60_000 });
if (!result) return;

const subject = result.getField<string>("subject", true);
const assignees = result.getField("assignees", "getSelectedUsers");

await result.reply({
    content: `Ticket created: ${subject}`,
    flags: "Ephemeral"
});

DynaSend

import { dynaSend, SendMethod } from "vimcord";

await dynaSend(interaction, {
    sendMethod: interaction.deferred || interaction.replied ? SendMethod.EditReply : SendMethod.Reply,
    content: "Hello from one send helper.",
    embeds: [embed]
});

MongoDB Plugin

import { createMongoPlugin, createMongoSchema, MongoosePlugin } from "@vimcord/plugin-mongoose";

client.use(
    new MongoosePlugin({
        connectionRefresh: {
            interval: 60_000,
            maxFailures: 2
        }
    })
);

const Users = createMongoSchema("Users", {
    userId: { type: String, required: true, unique: true },
    balance: { type: Number, default: 0 },
    createdAt: { type: Date, default: Date.now }
});

const user = await Users.fetch({ userId: "123" }, undefined, { required: true });
await Users.upsert({ userId: "123" }, { $inc: { balance: 100 } });

const SoftDeletePlugin = createMongoPlugin(builder => {
    builder.schema.add({ deletedAt: { type: Date, default: null } });
    builder.extend({
        async softDelete(filter) {
            return this.update(filter, { deletedAt: new Date() });
        }
    });
});

Users.use(SoftDeletePlugin);

Client Logger

client.logger.success("Startup complete");
client.logger.debug("Only shown when verbose mode is enabled");

const loader = client.logger.loader("Syncing commands...");
await client.modules.commands.push();
loader.succeed("Commands synced");

API Reference

Core Exports

| Export | Description | | --- | --- | | Vimcord | Discord.js client subclass with modules, plugins, globals, status, and logging | | SlashCommandModule | Slash command module with optional subcommand routes | | PrefixCommandModule | Prefix command module with aliases and parsed message content | | MessageContextCommandModule | Message context menu command module | | UserContextCommandModule | User context menu command module | | EventModule | Typed Discord event module | | BetterEmbed | Embed helper with context formatting | | BetterContainer | Components V2 container helper | | Paginator | Component-based paginator | | BetterModal | Modal helper with V2 component support | | promptMessage, promptModal | Confirmation prompt helpers | | dynaSend | Send helper for interactions, channels, messages, members, and users | | defineGlobalUxConfig | Global UX defaults for embeds, collectors, paginator, and prompts | | defineGlobalCommandHooks | Global command hooks used when modules do not define local hooks | | setupCLI | Initializes the process-wide stdin CLI and its client targets | | CLILogger | CLI-specific logger with target headers, groups, tables, styles, and loaders |

Client Options

const client = new Vimcord({
    customId: "main",
    client: {
        intents: [GatewayIntentBits.Guilds]
    },
    globals: {
        app: {
            name: "My Bot",
            devMode: false,
            enableCLI: true,
            disableBanner: false
        },
        staff: {
            ownerId: "123456789012345678",
            superUsers: [],
            superUserRoles: []
        }
    },
    connectionRefresh: {
        interval: 60_000,
        requestTimeout: 10_000,
        maxFailures: 2,
        maxRefreshAttempts: 3
    },
    verbose: false
});

Module Loading

await client.modules.load({
    slashCommands: { dir: "./src/commands/slash", suffix: ".slash" },
    prefixCommands: { dir: "./src/commands/prefix", suffix: ".prefix" },
    messageContextCommands: { dir: "./src/commands/message-context", suffix: ".mctx" },
    userContextCommands: { dir: "./src/commands/user-context", suffix: ".uctx" },
    events: { dir: "./src/events", suffix: ".event" }
});

Runtime Configuration

client.configure({
    app: {
        name: "My Bot",
        devMode: true
    },
    staff: {
        ownerId: "123456789012345678",
        superUserRoles: ["987654321098765432"]
    },
    hooks: {
        prefix: {
            async onError({ message, error }) {
                await message.reply(`Command failed: ${error?.message ?? "Unknown error"}`);
            }
        }
    }
});

Plugin CLI And Health Contributions

Plugin install hooks receive a typed context. Contributions remain scoped to that client and are removed automatically when the plugin unloads.

import type { VimcordPluginContext } from "vimcord";
import { VimcordPlugin } from "vimcord";

class ServicePlugin extends VimcordPlugin {
    override name = "service";
    override description = "Example service integration";
    override version = "1.0.0";

    constructor(private readonly service: { ping(): Promise<void> }) {
        super();
    }

    override install({ cli, health }: VimcordPluginContext) {
        cli.registerCommand({
            name: "service-info",
            description: "Displays service information.",
            execute: ({ logger, client }) => logger.header("Service Info", client)
        });

        health.register({
            id: "service",
            label: "Service API",
            check: async () => {
                const startedAt = performance.now();
                await this.service.ping();
                return { status: "healthy", latencyMs: performance.now() - startedAt };
            }
        });
    }

    override uninstall() {}
}

Examples

Status Rotation

Status config is user-owned. Vimcord does not provide a default status rotation, and a single profile applies in both development and production.

import { ActivityType } from "discord.js";

await client.status.set({
    interval: 30_000,
    randomize: true,
    activity: [
        { name: "$GUILD_COUNT servers", type: ActivityType.Watching, status: "online" },
        { name: "Need help? Use /help", type: ActivityType.Custom, status: "online" }
    ]
});

Pass production and/or development profiles when their statuses differ. An omitted profile leaves that environment's status blank; profiles do not fall back across environments.

Command Dispatch Events

import { Events } from "discord.js";
import { EventModule } from "vimcord";

export default new EventModule({
    name: "dispatchInteractions",
    event: Events.InteractionCreate,
    async execute({ client, args: [interaction] }) {
        await client.modules.commands.dispatchInteraction(interaction);
    }
});
import { Events } from "discord.js";
import { EventModule } from "vimcord";

export default new EventModule({
    name: "dispatchPrefixCommands",
    event: Events.MessageCreate,
    async execute({ client, args: [message] }) {
        if (message.author.bot) return;
        await client.modules.commands.dispatchMessage(message, ["!", "?"]);
    }
});

Bot Staff Checks

const isStaff = await client.isBotStaff(interaction.user.id);
if (!isStaff) {
    await interaction.reply({ content: "This is staff-only.", flags: "Ephemeral" });
}

Environment Variables

TOKEN=your_production_bot_token
TOKEN_DEV=your_development_bot_token

MONGO_URI=mongodb://localhost:27017/discord-bot
MONGO_URI_DEV=mongodb://localhost:27017/discord-bot-dev

About

Created by xsqu1znt with a simple goal: make Discord bot development enjoyable again.

License: MIT


Built with love for the Discord.js community

Found a bug? Open an issue