@yaebal/toml

declarative toml routes for yaebal bots. describe simple commands, text routes, message filters, callback queries and replies in toml, then keep real logic in typescript handlers.

install

terminal
pnpm add @yaebal/toml

bot.toml

all route arrays are optional. an empty config is valid and registers nothing. every route must define at least reply or handler. hears takes exactly one of text (exact match) or regex; callbacks takes exactly one of data or regex. a regex value is compiled with new RegExp() at install time — an invalid pattern is a startup error, not a dead route.

bot.toml
[bot]
name = "demo"

[[commands]]
name = "start"
description = "say hello"
reply = "привет! я бот из toml."

[[commands]]
name = "ping"
description = "check the bot is alive"
handler = "ping"

[[hears]]
regex = "^p[io]ng$"
reply = "pong"

[[messages]]
on = "message:text"
contains = "yaebal"
reply = "yaebal мощь"

[[callbacks]]
data = "profile"
handler = "profileCallback"

[[callbacks]]
regex = "^item:\\d+$"
reply = "открываю товар…"

usage

call installToml once before bot.start(). it accepts a file path, a raw toml string, or an already parsed object, and returns the same bot or composer instance. the whole config is validated up front, so a bad route can never leave the bot half-wired.

index.ts
import { Bot } from "@yaebal/core";
import { installToml } from "@yaebal/toml";

const bot = new Bot(process.env.BOT_TOKEN!);

installToml(bot, "./bot.toml", {
  syncCommands: true, // push described commands to the / menu on start
  handlers: {
    ping: async (ctx) => {
      await ctx.reply("pong from typescript");
    },
    profileCallback: async (ctx) => {
      await ctx.answerCallbackQuery();
      await ctx.reply("profile");
    },
  },
});

await bot.start();

command menu

with syncCommands: true, commands that have a description are pushed to the telegram command menu (setMyCommands) once the bot starts. commands without a description are still routed — they just stay out of the menu. syncCommands needs a Bot target (it hooks onStart); installing on a plain Composer fails with a readable error.

handler registry

handler = "name" looks up options.handlers.name. if the handler is missing, installation fails with a readable startup error instead of silently skipping the route.

tomlregistered as
[[commands]] name = "start" reply = "hi"bot.command("start", ctx => ctx.reply("hi"))
[[hears]] text = "ping" reply = "pong"bot.hears("ping", ...) — exact match
[[hears]] regex = "^p[io]ng$"bot.hears(/^p[io]ng$/, ...)
[[messages]] on = "message:text" contains = "x"bot.on("message:text", ...) plus a text filter
[[callbacks]] data = "profile" handler = "profile"bot.callbackQuery("profile", handlers.profile)
[[callbacks]] regex = "^item:\d+$" reply = "…"bot.callbackQuery(/^item:\d+$/, ...)
if both handler and reply are present, the handler wins. the reply is only used when no handler is configured. callback routes that use reply answer the callback query first (answerCallbackQuery), so the button never hangs on a spinner — handler-based callback routes answer it themselves.

example errors: Missing handler "ping" referenced in commands[1], commands[0] must define either reply or handler, hears[0] must define either text or regex, messages[0].on unknown update type "mesage".

validation

the config is validated with zod before anything touches the bot: every route needs a response, regex patterns must compile, and the update-type prefix of messages[].on is checked against the real bot api update names (generated from the telegram schema), so a typo like "mesage:text" fails at install instead of registering a route that never matches.

plugin usage

createTomlPlugin returns a normal yaebal plugin, so it can be installed through bot.install() or composed with other feature modules.

plugin.ts
import { createTomlPlugin } from "@yaebal/toml";

bot.install(createTomlPlugin("./bot.toml", { handlers }));

api

exportsignaturedescription
installToml(bot, configPathOrObject, options?) => botparse, validate and register routes on an existing bot or composer
createTomlPlugin(configPathOrObject, options?) => Plugincreate a yaebal-compatible plugin that installs the toml routes
parseTomlConfig(input) => TomlBotConfigparse from file path, raw toml string or parsed object, then validate
validateTomlConfig(input) => TomlBotConfigruntime validation through zod with human-readable error paths

options.handlers is the named handler registry; options.syncCommands enables the command-menu sync described above.

raw strings and objects

file paths are the common case, but tests and generated configs can pass raw toml or an object. a path that does not exist fails with a clear read error; a path-looking string that cannot be parsed as toml gets a "is this a missing file?" hint.

config.ts
import { parseTomlConfig, validateTomlConfig } from "@yaebal/toml";

const config = parseTomlConfig(`[[commands]]
name = "start"
reply = "hi"
`);

validateTomlConfig(config);

limits

toml is not a replacement for typescript. use it for routes and simple replies; use the handler registry for database calls, external services, branching flows, permissions and anything that needs compile-time types.

status: experimental. the format is intentionally small: commands, hears, message filters and callback queries. no hidden dependency graph, no dynamic import magic, no second routing engine.