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

kore.bun

v2.0.5

Published

A complete, ultra-configurable, strictly-typed Discord bot framework for Bun. Built on top of discord.js and Prisma.

Readme

kore.bun

A complete, ultra-configurable, strictly-typed Discord bot framework for Bun. Bring your own Prisma client (or none at all) and get full type inference everywhere -- kore.bun never imports @prisma/client itself.

This version breaks the previous kore.bun API. If you are upgrading from v1.x, please simply redo your project. Thank you for your understanding

Table of contents

Features

  • Zero-magic Prisma integration -- pass your generated PrismaClient type once via createKore<PrismaClient>() and every defineCommand/defineEvent/... helper is typed against it automatically.
  • File-based loaders for commands, events, buttons, select menus and modals, fully path-configurable.
  • Built-in interaction router with access control (owner-only, allow-lists, role/permission checks, invoker-restriction), cooldowns (in-memory or Prisma-backed), auto-defer safety, and graceful error panels.
  • Discord Components V2 UI kit -- panels, buttons, selects, modals, pagination, all typed.
  • Command registration with diffing -- hashes your command tree and skips registration when nothing changed.
  • Health server (/health, /ready) via Bun.serve, with pluggable health checks.
  • Migration guard -- refuse to boot with pending migrations, pluggable check function.
  • Sharding helpers -- createShardingManager, cross-shard IPC, shard stat aggregation.
  • A real CLI (kore.bun) -- init, dev, build, doctor, commands ... -- fully colored, zero external dependencies (raw ANSI).
  • Strict TypeScript throughout.

Requirements

  • Bun >= 1.1.0
  • discord.js ^14.27.0 and zod ^4.x (installed as dependencies of kore.bun)
  • Optional: @prisma/client >= 7.10.0 plus a matching driver adapter (e.g. @prisma/adapter-pg + pg for Postgres) if you want persistence -- kore.bun treats Prisma as a peer dependency and never bundles it

Install

bun add kore.bun discord.js zod

# optional, only if you want Prisma-backed persistence:
bun add @prisma/client @prisma/adapter-pg pg
bun add -d prisma @types/pg

Quick start

bunx kore init my-bot
cd my-bot
bun install
cp .env.example .env          # fill in DISCORD_TOKEN, DISCORD_ID, DATABASE_URL
bunx prisma generate && bunx prisma migrate dev
bun run dev

Project structure

kore init scaffolds a project shaped like this:

my-bot/
  src/
    index.ts            # entrypoint -- builds the KoreClient and starts it
    config.ts           # defineConfig(...)
    kore.ts             # createKore<PrismaClient>() -- your typed define* helpers
    prisma.ts           # PrismaClient + driver adapter singleton
    commands/           # slash commands (file-based, auto-loaded)
    events/             # discord.js events (file-based, auto-loaded)
    buttons/            # button handlers (file-based, auto-loaded)
    selectMenus/        # select menu handlers (file-based, auto-loaded)
    modals/             # modal handlers (file-based, auto-loaded)
  prisma/
    schema.prisma
  .env.example

Every directory under paths in your config is discovered recursively and hot-loaded on boot -- no manual registration arrays to maintain.

The typing pattern

Create src/kore.ts once, binding every helper to your own generated Prisma type:

// src/kore.ts
import { createKore } from 'kore.bun'
import type { PrismaClient } from '@prisma/client'

export const { defineCommand, defineEvent, defineButton, defineSelectMenu, defineModal } = createKore<PrismaClient>()

From then on, client.prisma is fully typed everywhere without a single manual annotation:

// src/commands/ping.ts
import { SlashCommandBuilder } from 'discord.js'
import { defineCommand } from '../kore'
import { infoPanel, respond } from 'kore.bun'

export default defineCommand({
  data: new SlashCommandBuilder().setName('ping').setDescription('Replies with pong.'),
  async execute(client, interaction) {
    await respond(interaction, { components: [infoPanel('Pong!', `Latency: ${Math.round(client.ws.ping)}ms`)] })
  },
})

Bootstrapping

// src/index.ts
import { KoreClient, createEnv } from 'kore.bun'
import { z } from 'zod'
import config from './config'
import { prisma } from './prisma'

const env = createEnv({ DATABASE_URL: z.url() })

const client = new KoreClient({
  config,
  token: env.DISCORD_TOKEN,
  applicationId: env.DISCORD_ID,
  prisma,
})

await client.bootstrap()
await client.start()

bootstrap() loads every command/event/button/select-menu/modal, hydrates cooldowns, and registers slash commands (diffed against a local cache so unchanged trees are skipped). start() logs the client in.

Configuration

defineConfig validates and normalizes everything with zod. Every field is optional with sane defaults:

// src/config.ts
import { defineConfig } from 'kore.bun'

export default defineConfig({
  devGuildId: '123456789012345678',
  ownerIds: ['123456789012345678'],
  intents: ['Guilds', 'GuildMembers'],

  paths: {
    root: 'src',
    commandsDir: 'commands',
    eventsDir: 'events',
    buttonsDir: 'buttons',
    selectMenusDir: 'selectMenus',
    modalsDir: 'modals',
  },

  logging: { level: 'info', fileEnabled: true, directory: 'logs' },
  health: { enabled: true, port: 3000 },
  migrations: { allowPending: false },
  cooldown: { defaultSeconds: 0 },
  interactions: { ackSafetyMarginMs: 2000, shutdownDrainMs: 10_000 },
  autocomplete: { debounceMs: 250, cacheTtlMs: 15_000 },
  commands: { cachePath: '.cache/commands.json' },
})

Prisma integration

kore.bun never depends on @prisma/client -- you own the client, the schema, and the migrations. All Prisma helpers live under the kore.bun/prisma subpath and infer their types straight from whatever delegate/client you pass in:

// src/prisma.ts
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! })

export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter })

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

new PrismaClient() with no options will throw. Swap PrismaPg for the adapter matching your database (@prisma/adapter-libsql, @prisma/adapter-planetscale, etc.).

// src/index.ts
import { createPrismaCooldownStore, createPrismaHealthCheck, createPrismaMigrateStatusCheck } from 'kore.bun'
import { prisma } from './prisma'

const cooldownStore = createPrismaCooldownStore(prisma.cooldown)
const healthCheck = createPrismaHealthCheck(prisma)
const migrationCheck = createPrismaMigrateStatusCheck(process.env.DATABASE_URL!)

UI kit & builders

kore.bun ships a full Discord Components V2 kit. Everything is a plain function that returns a discord.js builder instance, so you can keep chaining .add*Components(...) on top of what kore.bun gives you. Every export below is available directly from kore.bun (re-exported from kore.bun/ui), fully typed, with zero extra imports needed beyond discord.js itself if you want to go lower-level.

Low-level builders (src/components/builders.ts)

These wrap the raw Components V2 builders with sane defaults:

import { COLORS, container, textDisplay, separator, section, thumbnail, mediaGallery, fileComponent } from 'kore.bun'

| Export | Signature | Description | | --- | --- | --- | | COLORS | { primary, success, danger, warning, neutral } | Preset accent colors (hex numbers) used by every panel helper. | | textDisplay(content) | (content: string) => TextDisplayBuilder | Markdown-capable text block. | | separator(spacing?, divider?) | (spacing?: SeparatorSpacingSize, divider?: boolean) => SeparatorBuilder | Visual divider; defaults to Small spacing with a divider line. | | container(accentColor?) | (accentColor?: number \| RGBTuple) => ContainerBuilder | Root Components V2 container, optionally accent-colored. | | section() | () => SectionBuilder | Empty section builder for text + accessory layouts. | | thumbnail(url, description?) | (url: string, description?: string) => ThumbnailBuilder | Small image accessory. | | mediaGallery(urls) | (urls: readonly string[]) => MediaGalleryBuilder | Gallery built from a list of image URLs. | | fileComponent(attachmentUrl) | (attachmentUrl: string) => FileBuilder | Attaches a file component by URL/attachment reference. |

Custom IDs (src/components/customId.ts)

Buttons, selects and modals all encode extra state in their customId using a :-separated scheme:

import { buildCustomId, parseCustomId } from 'kore.bun'

buildCustomId('confirm', 'user123', 2) // -> "confirm:user123:2"
parseCustomId('confirm:user123:2')     // -> { id: 'confirm', args: ['user123', '2'] }

The router calls parseCustomId for you before invoking your button/select/modal handler, so execute(client, interaction, args) already receives args as a string[].

Panels (src/ui/panels.ts)

Panels are the standard way to reply to interactions -- every one of them returns a ContainerBuilder ready to hand to respond():

import { infoPanel, successPanel, errorPanel, warningPanel, listPanel, keyValuePanel, sectionedPanel, titledPanel } from 'kore.bun'

| Function | Description | | --- | --- | | titledPanel(title, message, color?) | Base building block -- a ## title heading followed by a message, in any color. | | infoPanel(title, message) | Primary-colored informational panel. | | successPanel(title, message) | Green panel, prefixes the title with ✅. | | errorPanel(message, title?) | Red panel, prefixes the title with ⚠️ (title defaults to "Error"). | | warningPanel(title, message) | Yellow panel, prefixes the title with ⚠️. | | listPanel(title, items, color?) | Renders items as a bulleted list under a heading; shows _Nothing to show._ when empty. | | keyValuePanel(title, entries, color?) | Renders a Record<string, string \| number> as **key:** value lines. | | sectionedPanel(title, sections, color?) | Heading followed by multiple text blocks, each separated by a divider. |

await respond(interaction, {
  components: [successPanel('Ban applied', `${target.tag} has been banned.`)],
  ephemeral: true,
})

Buttons (src/ui/buttons.ts)

import { buttonRow, confirmCancelRow, paginationRow, linkButton, type ButtonDefinition } from 'kore.bun'

| Function | Description | | --- | --- | | buttonRow(definitions: ButtonDefinition[]) | Builds an ActionRowBuilder<ButtonBuilder> from { id, label, style?, args?, disabled?, emoji? } definitions; id/args are packed into the custom ID via buildCustomId. | | confirmCancelRow(token, confirmId?, cancelId?) | Ready-made Confirm (success) / Cancel (danger) row, both carrying token as an arg. | | paginationRow(baseId, page, totalPages) | ◀ / page / totalPages / ▶ row, auto-disabling the arrows at the bounds. | | linkButton(url, label) | A ButtonStyle.Link button (not part of a row -- add it yourself). |

Select menus (src/ui/selects.ts)

import { stringSelectRow, userSelectRow, roleSelectRow, channelSelectRow, type SelectOption, type StringSelectDefinition } from 'kore.bun'

| Function | Description | | --- | --- | | stringSelectRow(definition: StringSelectDefinition) | { id, placeholder, options, minValues?, maxValues?, args? } -> ActionRowBuilder<StringSelectMenuBuilder>. | | userSelectRow(id, placeholder, minValues?, maxValues?) | Ready-made user select row. | | roleSelectRow(id, placeholder, minValues?, maxValues?) | Ready-made role select row. | | channelSelectRow(id, placeholder, minValues?, maxValues?) | Ready-made channel select row. |

Modals (src/ui/modals.ts)

buildModal turns a declarative field list into a fully-built ModalBuilder, using the new LabelBuilder-wrapped input components. Supported field types: input, string, user, role, channel, file, mentionable, checkbox, checkboxgroup, radiogroup.

import { buildModal, type ModalSpec, type ModalFieldDefinition } from 'kore.bun'

const modal = buildModal({
  customId: 'feedback',
  title: 'Send feedback',
  fields: [
    { type: 'input', customId: 'message', label: 'Your message', required: true, maxLength: 500 },
    {
      type: 'radiogroup',
      customId: 'severity',
      label: 'Severity',
      options: [
        { label: 'Minor', value: 'minor', default: true },
        { label: 'Major', value: 'major' },
      ],
    },
  ],
})

Every field shares customId, label, description?, required?; select-style fields (string, user, role, channel, mentionable) additionally accept minValues?, maxValues?, placeholder?, disabled? and a defaults/options payload matching their Discord counterpart.

Pagination (src/ui/pagination.ts)

import { paginate, type PaginateOptions, type PaginatedResult } from 'kore.bun'

const { container: view, page, totalPages } = paginate({
  title: 'Leaderboard',
  items: rows,
  page: currentPage,
  pageSize: 10,
  render: (row, index) => `**${index + 1}.** ${row.name} -- ${row.score}`,
  buttonBaseId: 'leaderboard',
})

await respond(interaction, { components: [view] })

paginate() slices items for the given page/pageSize, renders each visible item with render, and appends a paginationRow wired to buttonBaseId -- your button handler just needs to read args (['prev' | 'next' | 'noop', currentPage]) and call paginate() again with the new page.

Sending it all: respond()

import { respond, type RespondOptions } from 'kore.bun'

await respond(interaction, { components: [infoPanel('Hello', 'World')], ephemeral: true })

respond() is Components-V2-aware -- it auto-picks reply/editReply/followUp depending on the interaction's state and always sets MessageFlags.IsComponentsV2 (plus Ephemeral when requested), so you never juggle flags manually.

Access control & cooldowns

Commands and components accept an optional access object, enforced automatically by the built-in router:

export default defineCommand({
  data: new SlashCommandBuilder().setName('ban').setDescription('Bans a member.'),
  access: {
    requiredPermissions: ['BanMembers'],
    cooldownSeconds: 10,
  },
  async execute(client, interaction) {
    // ...
  },
})

Supported checks: ownerOnly, allowedUserIds, allowedRoleIds, requiredPermissions, restrictToInvoker (components only), and cooldownSeconds. Cooldowns default to an in-memory store; swap in createPrismaCooldownStore(prisma.cooldown) for persistence across restarts.

Health checks & migration guard

import { startHealthServer } from 'kore.bun'
import { createPrismaHealthCheck, createPrismaMigrateStatusCheck } from 'kore.bun/prisma'
import { assertMigrationsApplied } from 'kore.bun'

assertMigrationsApplied(config.migrations.allowPending, createPrismaMigrateStatusCheck(env.DATABASE_URL))
startHealthServer({ client, checks: [createPrismaHealthCheck(prisma)] })

startHealthServer exposes /health and /ready via Bun.serve, aggregating your custom checks alongside gateway readiness. assertMigrationsApplied refuses to boot when pending migrations are detected, unless migrations.allowPending is true.

The CLI

The kore.bun binary ships with the package (installed automatically as a dependency, or run directly via bunx kore.bun):

kore.bun init [name]                # Scaffold a new project in the current directory
kore.bun dev                        # bun run --watch src/index.ts
kore.bun build                      # Type-check and bundle for production
kore.bun doctor                     # Diagnose your local environment and project setup
kore.bun commands clear-global      # Clear all globally registered slash commands
kore.bun commands clear-guild <id>  # Clear all slash commands for a specific guild
kore.bun commands list-global       # List all globally registered slash commands
kore.bun commands list-guild <id>   # List all slash commands for a specific guild
kore.bun commands reset-cache       # Delete the local command registration cache
kore.bun version / kore.bun help    # Print version / usage

Every command accepts --token and --app-id overrides (defaulting to DISCORD_TOKEN/DISCORD_ID). Output is colored with hand-rolled ANSI escapes -- no chalk, no commander, no ora -- and respects NO_COLOR / FORCE_COLOR.

Sharding

// src/shard.ts
import { createShardingManager } from 'kore.bun/shard'

createShardingManager({ token: process.env.DISCORD_TOKEN!, entry: './src/index.ts' })

createShardingManager wires up shard lifecycle logging and relays IPC messages broadcast via broadcastToShards/onShardMessage between shards automatically.

License

WTFPL -- do whatever the fuck you want with this code.