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

spearkit

v0.4.0

Published

discord.js++ — a developer-experience-first Discord library. Drop-in compatible with discord.js, with ergonomic events, slash commands, and interactive components.

Downloads

317

Readme

spearkit

discord.js++ — a developer-experience-first layer over discord.js.

spearkit re-exports the entire discord.js surface (so it's a drop-in replacement) and adds an ergonomic, fully type-safe API for the things that are tedious in raw discord.js: setting up events, defining slash commands, and wiring interactive components. No any, no unknown leaking into your handlers — option values, custom-id params and modal fields are all inferred.

npm install spearkit discord.js

Batteries included

  • Type-safe slash commands, options, subcommands, autocomplete, buttons, selects and modals — no interactionCreate switch.
  • Cooldowns — per user/guild/channel/global, with per-role/per-user exemptions and overrides (guide).
  • Scheduled tasks — cron and interval jobs, started on ready (guide).
  • Prefix commands — classic !text commands that share cooldowns (guide).
  • Structured logging — leveled, scoped, pluggable; every error flows through it (guide).
  • Usage tracking — record who used what to a database and/or a Discord channel (guide).
  • dotenv built in — auto-load .env and read typed env vars (guide).
  • Plugins & file-based loading for organising larger bots.
  • Guards — declarative requireAnyRole/requireUserPermissions/guildOnly/requireOwner preconditions on commands, components and prefix commands (guide).
  • Context-menu commandsuserCommand / messageCommand with typed targetUser / targetMessage (guide).
  • Preset embedsctx.success/info/warn/error and client.embeds factory with configurable colors/icons (API ref).
  • Pagination & confirmationpaginate(...) and confirm(...) button flows with user-only filter and timeout.
  • Typed prefix argsprefixCommand({ args: a => a.snowflake("target").duration("d").rest("reason"), run: ctx => ctx.options }).
  • PrimitivesKeyedLock, safeFetch.{member,channel,...}, formatDuration/parseDuration/discordTimestamp, MemoryCache (TTL + counters + rate limit), loadConfig (JSON/JSON5/YAML).
  • Logger transports — multi-sink (consoleSink, jsonlSink, webhookSink); per-level routing.
  • Scheduler extrasscheduler.delay/followUp/reconcile for one-shot jobs and on-ready recovery.
  • Deploy strategydeployAllCommands({ dryRun, strategy: "diff" }) for safe CI deploys.
  • Auto-defercommand({ autoDefer: true }) / new SpearClient({ autoDefer: true }) to dodge Unknown interaction (10062) on slow handlers (API ref).
  • Graceful shutdownclient.enableGracefulShutdown({ onShutdown }) for clean SIGINT/SIGTERM teardown (API ref).
  • Permissions & moderationmoderationCheck, missingPermissions, canActOn, ctx.botMissing(...) role-hierarchy/permission preflights (API ref).
  • Persistent storageMemoryStore/JsonStore key-value stores + typed per-guild createSettings(...) (API ref).
  • Collectorsctx.awaitMessageFrom(...), ctx.awaitModal(...), awaitComponent(...) without hand-rolled collectors (API ref).
  • Discord error helpersisDiscordError(err, DiscordErrorCode.UnknownMessage), explainDiscordError(...) (API ref).
  • Dynamic prefixes — per-guild prefix resolution via prefix: { dynamic } (guide).

Documentation

  • Docs site (website/) — a Fumadocs site themed like the discord.js docs. Run it with cd website && pnpm install && pnpm dev.
  • Guides & API reference (docs/) — the Markdown the site is built from.
  • Examples (examples/) — one folder per topic (commands, options, components, events, loading, …).

For AI agents

spearkit ships machine-readable guidance so coding agents write correct code with it:

AGENTS.md, llms.txt, llms-full.txt, docs/ and the agent skill ship in the npm package as plain files (no install hook), so an installed copy lives under node_modules/spearkit/ — e.g. node_modules/spearkit/.claude/skills/spearkit/SKILL.md. The llms files are generated from docs/; run npm run docs:llms after editing docs.

Quick start

import { SpearClient, Intents, command, option, event } from "spearkit";

const client = new SpearClient({ intents: Intents.default });

const ping = command({
  name: "ping",
  description: "Check latency",
  run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
});

const ready = event("clientReady", (c) => console.log(`Online as ${c.user.tag}`));

client.register(ping, ready);
await client.start(process.env.DISCORD_TOKEN);
await client.deployCommands({ guildId: "YOUR_GUILD_ID" }); // instant in one guild

Slash commands with inferred options

Option values are typed from your declaration. Required options are non-nullable; optional ones are T | undefined; choices narrow to a literal union.

import { command, option } from "spearkit";

export default command({
  name: "echo",
  description: "Repeat a message",
  options: {
    text: option.string({ description: "What to say", required: true }),
    times: option.integer({ description: "Repeat count", minValue: 1, maxValue: 5 }),
    visibility: option.string({
      description: "Who sees it",
      choices: [
        { name: "Everyone", value: "public" },
        { name: "Just me", value: "private" },
      ],
    }),
  },
  run: (ctx) => {
    ctx.options.text;       // string
    ctx.options.times;      // number | undefined
    ctx.options.visibility; // "public" | "private" | undefined
    return ctx.reply({
      content: ctx.options.text.repeat(ctx.options.times ?? 1),
      ephemeral: ctx.options.visibility === "private",
    });
  },
});

Builders: string, integer, number, boolean, user, channel, role, mentionable, attachment.

Autocomplete

Co-locate the suggestion provider with the option:

option.string({
  description: "Fruit",
  required: true,
  autocomplete: (ctx) =>
    fruits.filter((f) => f.startsWith(ctx.value)).map((f) => ({ name: f, value: f })),
});

Subcommands

import { commandGroup, subcommand, subcommandGroup, option } from "spearkit";

commandGroup({
  name: "admin",
  description: "Admin tools",
  guildOnly: true,
  subcommands: {
    say: subcommand({
      description: "Make the bot speak",
      options: { message: option.string({ description: "Message", required: true }) },
      run: (ctx) => ctx.reply(ctx.options.message),
    }),
  },
  groups: {
    users: subcommandGroup({
      description: "Manage users",
      subcommands: {
        ban: subcommand({
          description: "Ban a user",
          options: { target: option.user({ description: "Who", required: true }) },
          run: (ctx) => ctx.reply(`Banned ${ctx.options.target.tag}`),
        }),
      },
    }),
  },
});

Interactive components

Define the component, its custom-id pattern and its handler in one place. Params in the custom-id pattern ({name}) are typed everywhere — both in the handler's ctx.params and in the build() call.

import { button, stringSelect, modal, textInput, row } from "spearkit";

const vote = button({
  id: "vote:{choice}",            // {choice} becomes a typed param
  label: "Yes",
  style: "Success",
  run: (ctx) => ctx.update(`You chose ${ctx.params.choice}`), // ctx.params.choice: string
});

const colour = stringSelect({
  id: "colour",
  placeholder: "Pick a colour",
  options: [
    { label: "Red", value: "red" },
    { label: "Blue", value: "blue" },
  ],
  run: (ctx) => ctx.reply({ content: ctx.values.join(", "), ephemeral: true }),
});

const feedback = modal({
  id: "feedback:{ticket}",
  title: "Feedback",
  fields: {
    summary: textInput({ label: "Summary", required: true }),
    detail: textInput({ label: "Details", style: "Paragraph" }),
  },
  run: (ctx) =>
    // ctx.params.ticket: string, ctx.fields.summary / ctx.fields.detail: string
    ctx.reply({ content: `#${ctx.params.ticket}: ${ctx.fields.summary}`, ephemeral: true }),
});

client.register(vote, colour, feedback);

// Use them in a message — build() requires exactly the params the pattern declares:
await channel.send({
  content: "Choose:",
  components: [row(vote.build({ choice: "yes" })), row(colour.build())],
});

Component builders: button, linkButton, stringSelect, userSelect, roleSelect, channelSelect, mentionableSelect, modal (+ textInput), row.

spearkit routes interactions automatically by the custom-id namespace and decodes the params for you — no interactionCreate switch statements.

File-based loading

Drop commands, events and components in a folder (default or named exports) and load them all:

await client.load(new URL("./commands", import.meta.url).pathname);

Plugins

import { definePlugin } from "spearkit";

const moderation = definePlugin({
  name: "moderation",
  setup(client) {
    client.register(/* commands, events, components */);
  },
});

await client.use(moderation);

Drop-in replacement

Everything discord.js exports is available from spearkit, so you can migrate incrementally:

import { Client, EmbedBuilder, GatewayIntentBits } from "spearkit"; // all from discord.js

Error handling

Handler errors never crash the process. Customise the response:

client.commands.onError((error, interaction) => {
  console.error(error);
  return interaction.reply({ content: "Oops.", flags: 64 });
});
client.components.onError((error) => console.error(error));

License

PolyForm Noncommercial License 1.0.0.

spearkit is free for noncommercial use — personal projects, learning, research, and use by nonprofit/educational/government organizations. You may not use it in software or projects you sell or that are built for commercial advantage without a separate commercial license. See LICENSE for the full terms, or open an issue to discuss a commercial license.