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

@treecordit/discord

v0.1.2

Published

Treecord's Discord bot core for Bun: TreeClient with slash-command routing, TreeCommand/SubCommand/SubCommandGroup builders, dev/admin guards, a folder command loader with hash-diffed pushes, a pluggable config store (Redis default), a generic per-interac

Readme

@treecordit/discord

Treecord's Discord bot core for Bun — the shared engine extracted from the Treecord commission and internal bots, packaged for reuse. It runs a whole bot from the file system: commands, events, and component (button/select/modal) handlers each load from their folder, so index.ts is just env + directories. On top of that you get hash-diffed command pushes (so you don't spam the Discord API), dev/admin/beta guards, reusable checks for gating, an opt-in plugin system (premium, linked roles, social pollers), a pluggable config store (Redis by default), a generic per-interaction context hook, branded length-safe embeds, pagination, and a colored logger.

Proprietary. The published package contains an obfuscated build + type declarations only — never the source. Client repos can depend on it (and even be handed over) without exposing the implementation.

Dual distribution (private full types, public any)

A typed library can't hide its types and stay usable — the .d.ts is the contract consumers compile against. So this package ships two builds with the same name, version, and (obfuscated) runtime — only dist/index.d.ts differs:

| build | registry | types | who | | ----------- | --------------------------------- | ------------------------------------------------- | ----------------------- | | private | GitHub Packages (needs GHCR token) | full — real generics, autocomplete, TCtx flow | you / your CI | | public | npm registry.npmjs.org (tokenless) | every export is any — runs identically | client hand-off repos |

Both are published from one source on each release (release:private + release:public). The runtime is obfuscated, and the only types that reach the public are useless.

Install (in a consuming project)

discord.js and zod are peer dependencies; Bun ≥ 1.1 is required (the client uses Bun.nanoseconds, Bun.hash, and — for the default config store — Bun's RedisClient).

Which build you get is decided by the .npmrc — npm/bun can't try the private registry and fall back to public on a 401, so the choice is explicit. Copy consumer/select-registry.mjs (+ the two .npmrc templates) from this repo into your project and run:

node select-registry.mjs      # GHCR_TOKEN set → private full types; unset → public `any`
bun add @treecordit/discord discord.js zod

Quickstart

A whole bot in one file (plus a folder of commands):

// src/index.ts
import { GatewayIntentBits } from 'discord.js'
import { TreeClient } from '@treecordit/discord'
import { join } from 'node:path'

const client = new TreeClient({
  token: process.env.DISCORD_TOKEN!,
  guildId: process.env.DISCORD_GUILD_ID, // omit to register commands globally
  developerIds: ['1234567890'],
  brand: 'Treecord', // TreeEmbed footer prefix
  handlersDir: import.meta.dir, // auto-loads ./commands, ./events, ./components
  intents: [GatewayIntentBits.Guilds],
})

await client.start() // logs in, loads everything, pushes commands, routes interactions

handlersDir is the whole configuration surface: point it at a folder with commands/, events/, and components/ subfolders (any subset). Need a folder elsewhere? Override any one with commandsDir / eventsDir / componentsDir.

// src/commands/ping.ts
import { TreeCommand, TreeEmbed } from '@treecordit/discord'

export default new TreeCommand('ping', 'Check the bot is alive', async ({ interaction }) => {
  await interaction.editReply({
    embeds: [new TreeEmbed().setNamedColor('success').setTitle('Pong!')],
  })
})

That's it — start() deferred the reply for you, enforced any guards, and only pushed to Discord if ping's definition changed since last boot. See docs/CONVENTIONS.md for the full command-file layout and copy-paste templates, and example/ for a runnable starter.

Guards

Every command and subcommand can be flagged. Guards propagate from a parent command down to its subcommands, and denied invocations get a stock ephemeral embed (logged):

new TreeCommand('config', 'Server configuration', handler).setAdminOnly(true)
new TreeCommand('eval', 'Run code', handler).setDeveloperOnly(true) // devs = developerIds
subcommand.setBeta(true) // prefixes the description with [BETA]

developerIds come from the client options (or configureBot). Admins are resolved from the member's Administrator permission (developers are always admins). A command may also declare a public boolean option; passing public: true opts that one invocation out of an ephemeral reply.

Per-interaction context (ctx)

Attach anything to each invocation — a loaded user record, a db handle, a request logger — with a context hook. Its return type flows into every handler as ctx:

interface Ctx { user: DataUser }

const client = new TreeClient<Ctx>({
  // ...
  resolveContext: async (interaction) => ({ user: await DataUser.get(interaction.user.id) }),
})

// in a command:
export default new TreeCommand<Ctx>('me', 'Your profile', async ({ interaction, ctx }) => {
  await interaction.editReply(`You are ${ctx.user.username}`)
})

Without a resolveContext, ctx is undefined (and TreeClient's default TCtx is undefined). The same ctx is resolved for component interactions too.

Events

Each file in events/ default-exports a TreeEvent. The event name is explicit, so the handler args are fully typed from discord.js — and several files can handle the same event:

// events/guildMemberAdd.ts
import { TreeEvent } from '@treecordit/discord'

export default new TreeEvent('guildMemberAdd', async (client, member) => {
  await member.send(`Welcome to ${member.guild.name}!`)
})

Pass { once: true } as the third argument for one-shot events. Your own client.on(...) listeners still work alongside the loaded ones.

Components (buttons, selects, modals)

Each file in components/ default-exports a TreeButton, TreeSelectMenu, or TreeModal. Handlers are matched by customId prefix (longest match wins); parse any trailing data yourself from rest:

// components/repost.ts  — matches customId "repost~<videoId>"
import { TreeButton } from '@treecordit/discord'

export default new TreeButton(
  'repost~',
  async ({ interaction, rest }) => {
    const videoId = rest // everything after the prefix
    await interaction.reply({ content: `Reposting ${videoId}…`, ephemeral: true })
  },
  { defer: 'update', developerOnly: true }, // optional: auto-ack + guards
)

Components get the same dev/admin guards, checks, and ctx as commands. They aren't auto-deferred by default (buttons often update rather than reply); set defer: 'reply' | 'update' to opt in.

Checks and opt-in modules (premium, linked roles, social)

A check is a reusable gate (context) => pass() | deny({...}) you attach to any command or component. It's the extension point for premium/entitlement gating, linked-role requirements, cooldowns — anything that decides "may this proceed?" without baking policy into the core.

import { premiumCheck } from '@treecordit/discord'

// Premium: requires an active Discord entitlement (monetization). No API call —
// interactions carry their entitlements. Pass { skuId } to require a specific product.
new TreeCommand('vip', 'Members-only', handler).addCheck(premiumCheck({ skuId: env.SKU_ID }))

Plugins are opt-in feature bundles you pass to plugins: [...] (leave them out and none of it runs). They can register handlers/checks during setup and start background work during onReady:

import { linkedRoles, pollingPlugin } from '@treecordit/discord'
import { ApplicationRoleConnectionMetadataType as MetaType } from 'discord.js'

const client = new TreeClient({
  // ...
  plugins: [
    // Linked roles: registers your role-connection metadata with Discord on ready.
    linkedRoles({
      metadata: [
        { key: 'level', name: 'Level', description: 'Server level', type: MetaType.IntegerGreaterThanOrEqual },
      ],
    }),
    // "Social" / background pattern: run something on an interval.
    pollingPlugin({
      name: 'youtube-alerts',
      intervalMs: 60_000,
      onTick: async (bot) => { /* fetch new uploads, post them */ },
    }),
  ],
})

Write your own plugin by returning a { name, setup?, onReady? } object — that's the whole interface. (The linked-role user-facing OAuth step is a small web endpoint you host; see docs/CONVENTIONS.md.)

Config store (per-bot variables)

Commands can declare variableKeys; those keys are read from the active ConfigStore before each run and passed in as configs:

new TreeCommand('welcome', 'Post the welcome message', async ({ interaction, configs }) => {
  await interaction.editReply(configs.welcomeMessage ?? 'Welcome!')
}, ['welcomeMessage']) // ← variableKeys

The default store is Bun's Redis client (lazily connected from REDIS_URL; a bot that never reads config never connects). Swap it for anything — Postgres, an HTTP API, an in-memory map:

import { setConfigStore, type ConfigStore } from '@treecordit/discord'

const memory = new Map<string, string>()
setConfigStore({
  async get(k) { return memory.get(k) ?? null },
  async set(k, v) { memory.set(k, v) },
  async getMany(keys) { return Object.fromEntries(keys.map((k) => [k, memory.get(k) ?? null])) },
} satisfies ConfigStore)

Environment helpers

baseBotEnv / redisBotEnv are zod shapes for the variables every bot needs. Spread them into your own @t3-oss/env-core schema (or a plain z.object) and add your own:

import { createEnv } from '@t3-oss/env-core'
import { baseBotEnv, redisBotEnv } from '@treecordit/discord'
import { z } from 'zod'

export const env = createEnv({
  server: { ...baseBotEnv, ...redisBotEnv, YOUTUBE_API_KEY: z.string() },
  runtimeEnv: process.env,
  emptyStringAsUndefined: true,
})

Publishing (maintainers)

One tag publishes both builds (the release workflow runs both scripts):

git tag v0.x.y && git push --tags
# or locally:
bun run release:private   # full types  → GitHub Packages
bun run release:public    # all-`any`    → public npm (needs the @treecordit npm org)
  • The tarball includes only dist/index.js + dist/index.d.ts (files in package.json): obfuscated runtime + a single declaration file. No sources, no per-file declarations, no sourcemaps.
  • release:public swaps in the loose dist/index.d.ts (via scripts/gen-loose-dts.mjs, derived from the full one so it can't drift), publishes, then rebuilds the full types locally.
  • The .d.ts keeps discord.js and zod as imports (peers the consumer already has) rather than inlining them.
  • Honest caveat: obfuscation raises the effort to read the code dramatically, but no JS obfuscation is irreversible — treat the license (UNLICENSED) and package access (restricted) as the real protection, with obfuscation as the deterrent layer.

What this package is not

Persistence (users, stats, leaderboards), external integrations (YouTube, social platforms), and domain commands stay per-project — they're coupled to each bot's schema. The primitives here are what they're built from.