create a bot

create-yaebal scaffolds a ready-to-run bot — a beautiful terminal ui when your terminal supports it, plain prompts when it doesn't, and a one-shot flags mode for ci.

scaffold

run it with your package manager's create shortcut. it works the same under node, bun and deno — the runtime that launches it is just the default, you can pick any target.

pnpm
pnpm create yaebal

the interactive ui

in a real terminal you get a centred, keyboard-driven wizard rendered with pure ansi — no react, no solid, no native dependencies, just node:readline and escape codes. it walks seven steps: name → template → runtime → package manager → plugins → deploy → review, with a text input, single-selects and a scrollable multi-select; everything is one key away. picking the plugin template skips runtime, plugins and deploy entirely — none of them apply to a plugin package.

keydoes
↑ / ↓move within a list
enterconfirm the step / create the project
spacetoggle a plugin (or a git/install/ci switch)
a / nselect all / none on the plugins step
go back a step
escback, or cancel on the first step

if the terminal can't host a ui — piped output, CI, a dumb terminal, or --no-tui — it transparently falls back to plain one-line prompts (and, failing that, to defaults). nothing about the output changes; only how you answer does.

because it's pure ansi, the ui runs everywhere out of the box — node 20+, bun and deno, no flags, no prebuilt binaries. the only requirement is an interactive terminal. it also repaints cleanly on a terminal resize, and always restores your cursor/raw-mode/screen — even if the process is killed outright rather than exited through a keypress.

flags mode

pass any answer as a flag and the wizard skips that question; pass --yes and it skips all of them. this is the path to use in scripts and ci. a flag given a missing or invalid value (--runtime with no value, --runtime rust) is reported as a warning instead of silently swallowing whatever comes after it on the command line.

terminal
# non-interactive — pass everything up front
pnpm create yaebal my-bot --runtime bun --template commands --plugins session,again,fmt

# webhook bot, deployed to cloudflare workers, with a ci workflow
pnpm create yaebal my-bot -t webhook -d cloudflare --ci --yes

# take every default, no questions (great for ci / scripts)
pnpm create yaebal my-bot --yes

# pull in the entire plugin catalog
pnpm create yaebal my-bot --plugins all --no-install
options
name                                  the project (folder) name
-r, --runtime <node|bun|deno>         target runtime          (default: detected)
-m, --pm <npm|pnpm|yarn|bun|deno>     package manager         (default: detected)
-t, --template <name>                 minimal · echo · commands · buttons ·
                                      conversation · i18n · session-counter ·
                                      webhook · runner · rich-message · broadcast ·
                                      toml · plugin
-p, --plugins <a,b | all | none>      comma list of @yaebal plugins
-d, --deploy <target>                 none · docker · compose · fly · railway ·
                                      cloudflare · vercel
    --ci / --no-ci                     add a github actions ci workflow
    --git / --no-git                   initialise a git repo (+ first commit)
    --install / --no-install           install dependencies after scaffolding
    --tui / --no-tui                   force the interactive ui on/off
-c, --config <path>                   read defaults from a config file
    --no-config                        skip config-file autodetect
-y, --yes                             accept defaults, no prompts
    --json                             print the result as json (with --yes)
-h, --help · -v, --version

config file

drop a create-yaebal.json next to where you run the command — or add a "create-yaebal" key to a local package.json — to pre-fill any answer. cli flags always win; the file only fills in what a flag left unset.

create-yaebal.json
{
  "runtime": "bun",
  "template": "commands",
  "plugins": ["session", "again", "fmt"],
  "deploy": "cloudflare",
  "ci": true
}

templates

a template is more than a bot body — it pulls in the plugins it needs, adds the real imports and wiring, and (for webhook / runner) even swaps the bootstrap. everything a template generates type-checks against the real @yaebal/* apis — enforced by a compile smoke test that renders every template, every deploy target and the full plugin catalog against the workspace packages.

templatewhat you get
minimaljust /start + a text echo
echoecho text, photos and stickers back
commands/start /help /ping via a typed registry + synced / menu (commands)
buttonsinline keyboard + typed callback_data (keyboard, callback-data)
conversationawait-style multi-step dialog (conversation)
i18nmulti-language bot with a /lang toggle (i18n)
session-counterper-chat counter on ctx.session (session)
webhookedge/serverless deploy via serve() (web)
runnerconcurrent long-polling via run() (runner)
rich-messagesendRichMessage block builder + a streaming draft demo (rich)
broadcastsubscriber list + typed broadcast jobs (broadcast)
tomldeclarative routes in bot.toml + a handler registry (toml)
pluginreusable plugin package with src, tests and examples

plugins listed in brackets are added and wired automatically — on top of anything you pick yourself.

deploy targets

orthogonal to the template — pick one with -d/--deploy or on the wizard's deploy step (not offered for --template plugin, which has nowhere to deploy). docker/compose/fly/railway all run the same long-polling bot you'd run locally, just containerized — a single-stage Dockerfile matching your runtime, since these templates run straight from .ts with no build step. cloudflare and vercel are serverless: they take over the bootstrap the same way the webhook/runner templates already replace bot.start(), adding @yaebal/web and a SECRET_TOKEN to .env.example.

targetadds
nonenothing — deploy however you like
dockera Dockerfile + .dockerignore
composedocker + compose.yaml for a local/vps run
flydocker + fly.toml
railwaydocker + railway.json
cloudflarewrangler.jsoncexport default { fetch: cloudflareAdapter(bot) }, no server to run
vercelvercel.json + api/bot.ts — an edge function via webhook(), alongside the normal polling src/index.ts

--ci is independent of deploy: it adds a github actions workflow that installs and typechecks on every push (plugin packages get test/build too).

plugins

every published, bot-installable @yaebal/* plugin is offered — enforced by a test that diffs the catalog against the packages in the workspace. the ones with a clean default wiring (session, again, throttle, ratelimiter, i18n, files, prompt, split, …) are dropped straight into the bot chain; the rest are added to package.json with a commented import so the starter always type-checks — you wire them in when you need them. analytics and feature-flags also leave a commented-out showcase of their other real adapters (posthog/plausible/sqlite/clickhouse, launchdarkly/growthbook) right next to the wired default.

what you get

structure
my-bot/
  package.json        # scripts for your runtime, @yaebal deps
  tsconfig.json       # strict, esm, nodenext, noUncheckedIndexedAccess
  src/index.ts        # a working bot, wired with the plugins you picked
  .env.example        # BOT_TOKEN= (+ SECRET_TOKEN= for a serverless deploy)
  .gitignore
  README.md
  # + whatever --deploy / --ci add: Dockerfile, wrangler.jsonc, api/bot.ts, .github/workflows/ci.yml…

a minimal run, generated from --template minimal with session + again:

src/index.ts
import { Bot } from "@yaebal/core";
import { session } from "@yaebal/session";
import { autoRetry } from "@yaebal/again";

const token = process.env.BOT_TOKEN;
if (!token) {
  console.error("✗ set BOT_TOKEN in your environment (copy .env.example → .env)");
  process.exit(1);
}

const bot = new Bot(token)
  .install(session({ initial: () => ({ count: 0 }) }));

bot.command("start", (ctx) => ctx.reply("hello! i'm a yaebal bot 🤖"));
bot.on("message:text", (ctx) => ctx.reply(ctx.text));

// transformers applied to the outgoing api
autoRetry(bot.api);

await bot.start();

then

terminal
cd my-bot
pnpm install
# add your BOT_TOKEN to .env
pnpm dev

swap pnpm for whatever you chose — the printed next-steps already use your package manager. drop your BOT_TOKEN into .env and you're live. package names can be scoped (@your-org/my-plugin) — the folder created is the unscoped part.