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

@yaebal/commands

v0.2.0

Published

yaebal commands — one registry for command handlers + the telegram command menu: localized descriptions, scopes, aliases, hidden commands, diff-based sync.

Readme

@yaebal/commands

one registry for command handlers and the telegram / command menu — define each command once (name, menu description, handlers), then wire the handlers with plugin() and push the menu with register() / the diff-aware sync(). supports localized descriptions, menu scopes, aliases and hidden commands, and validates names/descriptions at add() time.

install

pnpm add @yaebal/commands

usage

import { Bot } from "@yaebal/core";
import { commands } from "@yaebal/commands";

const cmd = commands()
  .add("start", "start the bot", async (ctx) => {
    await ctx.reply(`welcome! args: ${ctx.args.join(", ")}`);
  })
  .add("help", "show help", async (ctx) => {
    await ctx.reply("available commands: /start, /help");
  });

const bot = new Bot(process.env.BOT_TOKEN!).install(cmd.plugin()); // wire the handlers

await cmd.sync(bot.api); // push the / menu — only if it changed

bot.start();

typed context

the registry is generic over your bot's accumulated context — handlers see plugin-added properties (and ctx.command / ctx.args) with no casting:

import type { Context } from "@yaebal/core";

type MyContext = Context & { session: { count: number } };

const cmd = commands<MyContext>().add("count", "count up", async (ctx) => {
  await ctx.reply(`count: ${++ctx.session.count}`);
});

// bot must provide MyContext before install — checked by the compiler
bot.install(session({ initial: () => ({ count: 0 }) })).install(cmd.plugin());

localized descriptions

pass per-locale strings with a required default; register() / sync() push the default menu plus one menu per locale (missing locales fall back to default):

const cmd = commands()
  .add("start", { default: "start the bot", ru: "запустить бота" }, handler)
  .add("help", { default: "show help", ru: "показать помощь" }, handler);

await cmd.register(bot.api); // pushes the default menu + the ru menu

scopes

scoped() returns a view whose commands only show in that scope's menu. scoped menus repeat the unscoped commands, because telegram replaces (not merges) the list for users a scope matches:

const cmd = commands().add("start", "start the bot", handler);

cmd.scoped({ type: "all_chat_administrators" })
  .add("ban", "ban a user", banHandler)
  .add("unban", "unban a user", unbanHandler);

await cmd.register(bot.api);
// default menu: /start — admins' menu: /start /ban /unban

note: scope only affects the menu. handlers still run for anyone — guard them yourself (e.g. with @yaebal/filters).

shadowing an unscoped command in one explicit scope

a name may be defined both unscoped and in one explicit scope — the explicit def shadows the unscoped one. that's the escape hatch for targeting the base command's own menu at a scope other than the default (e.g. all_private_chats instead of BotCommandScopeDefault) without losing the auto-repeat into other explicit scopes (like the admin menu above):

const cmd = commands().add("start", "start the bot", handler);
cmd.scoped({ type: "all_private_chats" }).add("start", "start the bot (dm)", dmHandler);

await cmd.register(bot.api);
// default menu: /start (generic text) — private-chats menu: /start (dm text)

since plugin() wires command() by name alone (no scope awareness at runtime), the explicit def's handler wins globally, not just inside its scope — a menu-only shadow (no handlers) falls back to the unscoped handler instead. two explicit scopes can never share a name: nothing at runtime could pick between two differing handlers, so that stays a duplicate command name error.

aliases and hidden commands

const cmd = commands()
  // ["name", ...aliases] — every name is handled, only the first shows in the menu
  .add(["settings", "prefs"], "open settings", handler)
  // handled but never shown in any menu (debug/admin commands)
  .hidden("debug", async (ctx) => ctx.reply(inspect(ctx)));

ephemeral commands

ephemeral() is add() plus is_ephemeral: true on the menu entry (bot api 10.2+): telegram shows the command's invocation only to its sender, and expects an answer within ~15 seconds — pair it with @yaebal/ephemeral's ctx.replyEphemeral() so the answer is private too. sync() diffs the flag, so flipping a command to ephemeral repushes its menu. also available on scoped(...) views.

const cmd = commands().ephemeral("stats", "your personal stats", async (ctx) => {
  await ctx.replyEphemeral(`you: ${await stats(ctx.from.id)}`);
});

menu-only entries

add() without handlers puts a command in the menu without registering middleware — useful when the handler lives elsewhere (a router, a scene):

const cmd = commands().add("report", "file a report"); // handled by a scenes entry point

sync, register, unregister

  • register(api) pushes every (scope, language) menu unconditionally; pass { scope, languageCode } to push a single one.
  • sync(api) reads each menu with getMyCommands first and only pushes the ones that changed — safe to run on every deploy. returns { pushed, skipped }.
  • unregister(api) clears every managed menu via deleteMyCommands.
  • menus() returns everything register() would push; list({ languageCode?, scope? }) returns one menu's { command, description }[] — handy for a /help text:
bot.command("help", (ctx) =>
  ctx.reply(cmd.list().map((c) => `/${c.command} — ${c.description}`).join("\n")),
);

validation

add() / hidden() / ephemeral() throw early (instead of a late bot api 400) on: names not matching [a-z0-9_]{1,32}, duplicate names/aliases, empty or >256-char descriptions, non-ISO-639-1 locale keys, and menus over 100 commands.


part of yaebal — a type-safe, runtime-agnostic Telegram Bot API framework. MIT.