yaebal meta

the batteries-included entry point — the core engine, the auto-generated per-update contexts, and the most-used plugins behind a single import. media.path() just works on node, bun and deno, and the same bot runs behind long polling or a webhook on the edge. for a minimal build, use @yaebal/core directly.

install

terminal
pnpm add yaebal

quick start

bot.ts
import { Bot, html } from "yaebal";

const bot = new Bot(process.env.BOT_TOKEN!)
  .command("start", (ctx) => ctx.send("hi 🐴"))
  .on("message:text", (ctx) => ctx.reply(html`you said: <b>${ctx.text}</b>`));

await bot.start(); // long polling — or hand updates to a webhook

.on("message:text") narrows the context — inside, ctx.text is a string, not string | undefined. every chain method (command / on / derive / install / …) flows the context type forward, so plugin-added properties are typed with no casting.

rich, typed contexts

createBot() grafts the auto-generated shortcut methods onto every update — ctx.react, ctx.editText, ctx.pin, … — typed to the matching update, not just present at runtime.

rich.ts
import { createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.on("message:text", (ctx) => ctx.react("🔥"));          // MessageContext
bot.on("callback_query:data", (ctx) => ctx.answer("ok")); // CallbackQueryContext

what's in the box

one import { … } from "yaebal" gives you, ready to use:

fromexportswhat
coreBot, Composer, Context, mediathe engine, filter queries, transports
contextscreateBot, per-update context classestyped ctx.react / ctx.editText / …
keyboardInlineKeyboard, Keyboardfluent keyboard builders
callback-datacallbackDatatyped callback_data pack / unpack
fmthtml, mdtagged templates with auto-escaping
filtersfilters, and, or, notcomposable, type-narrowing filters
sessionsessionper-chat state, pluggable storage
i18ni18nper-chat locale, ctx.t
webserve, webhook, setWebhookwebhooks on edge/web runtimes

a quick tour

keyboards + typed callback data:

keyboards.ts
import { InlineKeyboard, callbackData } from "yaebal";

const vote = callbackData("vote", { id: Number });

bot.command("poll", (ctx) =>
  ctx.send("pick one", {
    reply_markup: new InlineKeyboard()
      .text("👍", vote.pack({ id: 1 }))
      .text("👎", vote.pack({ id: 2 }))
      .build(),
  }),
);

bot.on("callback_query:data", (ctx) => {
  const data = vote.unpack(ctx.callbackQuery.data);
  if (data) ctx.answer(`voted ${data.id}`);
});

per-chat sessions and i18n:

stateful.ts
import { session, i18n } from "yaebal";

bot.install(session({ initial: () => ({ count: 0 }) }));
bot.command("count", (ctx) => ctx.reply(`#${++ctx.session.count}`));

bot.install(i18n({
  defaultLocale: "en",
  locales: { en: { hi: "hello" }, ru: { hi: "привет" } },
}));
bot.command("start", (ctx) => ctx.reply(ctx.t("hi")));

media — no platform package to pick:

media.ts
import { media } from "yaebal";

bot.command("pic", (ctx) => ctx.sendPhoto(media.path("./cat.jpg"))); // node/bun/deno
// on edge, send media.url(...) / media.buffer(...) instead

run on the edge over webhooks:

worker.ts
import { Bot, webhook } from "yaebal";

export default {
  fetch(request: Request, env: { BOT_TOKEN: string; SECRET: string }) {
    const bot = new Bot(env.BOT_TOKEN);
    bot.command("start", (ctx) => ctx.reply("running on the edge ⚡"));
    return webhook(bot, { secretToken: env.SECRET })(request);
  },
};
how the rich context works. core's Bot exposes a contextFactory hook; yaebal injects one (richContext) that builds the base Context and grafts the matching generated context's shortcut methods + payload fields onto it. core stays decoupled from @yaebal/contexts — the meta-package does the wiring.
the bundle covers the essentials. everything else is a first-party plugin you add as needed — auto-retry, scenes, conversations, routing, broadcast, the operator panel, and more. see the full plugin catalog.