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

@clustersoft/mgr-bot-sdk

v1.14.1

Published

Official bot framework for ClusterSoft Messenger (MGR) — a typed, zero-dependency SDK with long-polling and webhook transports, middleware, and inline keyboards.

Readme

@clustersoft/mgr-bot-sdk

The official framework for building bots on ClusterSoft Messenger (MGR) — typed, batteries-included, and dependency-free.

npm version types included node license

Build a bot in a handful of lines — commands, buttons, media, sessions — and run it with long-polling or a webhook. The API follows a familiar, conventional bot model, so if you have written a bot before, you already know this one.

  • 🟢 Zero runtime dependencies — native fetch and http, nothing else
  • 🧩 TypeScript-first — full types shipped; works from plain JavaScript too
  • 💬 Rich messaging — text, media, reactions, typing, edit/delete, inline mode
  • 📎 Media, end to end — upload from disk and download received files (CDN-backed)
  • 🧠 Stateful — Koa-style middleware, per-update state, persistent session (in-memory or Redis)
  • 🔌 Two transports — long-polling out of the box, webhook for production
  • 🛟 Resilient — auto-retry with backoff and graceful shutdown

Features at a glance

| Area | What you get | |------|--------------| | Messaging | send · reply · edit · delete · copy · reactions · typing indicator | | Media | photos, documents, video, audio, voice — upload bytes or reuse a file_id; download via getFile | | Location & contacts | send and receive map points and phone contacts | | Keyboards | inline keyboards + callback queries · inline mode (@bot query) | | Routing | command · hears (text/regex) · action (buttons) · on (update types) | | State | middleware (use) · error handler (catch) · ctx.state (per update) · ctx.session (persistent) | | Sessions | in-memory by default · plug in Redis with your own ioredis client | | Transports | long-polling · webhook (standalone or mounted) · graceful SIGINT/SIGTERM | | Bot profile | name, description, short description, command menu, webhook management |


Table of contents


Installation

npm install @clustersoft/mgr-bot-sdk

Requires Node.js ≥ 18 (for the built-in fetch).

The package ships compiled CommonJS (dist/*.js) plus type definitions (dist/*.d.ts), so it works identically from TypeScript and plain JavaScript:

// TypeScript / ES modules
import { Bot, Markup } from '@clustersoft/mgr-bot-sdk'
// Plain JavaScript (CommonJS)
const { Bot, Markup } = require('@clustersoft/mgr-bot-sdk')

Quick start

import { Bot, Markup } from '@clustersoft/mgr-bot-sdk'

const bot = new Bot(process.env.BOT_TOKEN!, {
  apiRoot: 'https://api.clustersoft.uz/api/v1/botapi',
})

bot.start((ctx) => ctx.reply('Hello! 👋'))
bot.help((ctx) => ctx.reply('Send me any text and I will echo it.'))
bot.command('ping', (ctx) => ctx.reply('pong'))
bot.hears(/hello/i, (ctx) => ctx.reply('Hi there!'))

bot.on('message', (ctx) => {
  if (ctx.text) ctx.reply(`Echo: ${ctx.text}`)
})

bot.hears(/thanks/i, (ctx) => ctx.react('❤️')) // react to a message

bot.command('report', async (ctx) => {
  await ctx.sendTyping()             // show "typing…" during slow work
  const data = await buildReport()   // takes a few seconds
  await ctx.reply(data)
})

bot.launch() // start long-polling

bot.launch() resolves once polling is running and then stays pending until the process receives SIGINT/SIGTERM (handled automatically) or you call bot.stop().


Getting a token

Open a chat with BotKing (@botking_bot) inside MGR and send /newbot. Follow the prompts to pick a name and username; BotKing returns a token shaped like abc123:XXXXXXXX. Pass it as the first argument to new Bot().

Keep your token secret. Prefer reading it from an environment variable (process.env.BOT_TOKEN) rather than hard-coding it.


Core concepts

  • Update — a single incoming event. Exactly one of message, callback_query, or inline_query is present.
  • Context (ctx) — a per-update wrapper that exposes convenient accessors (ctx.text, ctx.from, ctx.chat, …) and actions (ctx.reply, …).
  • Handler — a function bound to a match rule (a command, a regex, a button). An update is delivered to the first matching handler only, so register more specific handlers before broader ones.
  • Middleware — runs for every update before handlers, in registration order, and can short-circuit or enrich ctx.state.

Handlers

| Method | Fires when | |--------|------------| | bot.start(fn) | the user sends /start | | bot.help(fn) | the user sends /help | | bot.command(name \| names, fn) | the message is /name (accepts a single command or an array) | | bot.hears(substring \| regex, fn) | message text matches | | bot.action(data \| regex, fn) | an inline button with matching callback_data is pressed | | bot.on(type \| types, fn) | the update is of the given kind(s): 'message', 'callback_query', 'inline_query' | | bot.inlineQuery(fn) | shorthand for on('inline_query', fn) |

bot.command(['buy', 'purchase'], (ctx) => ctx.reply('Opening the shop…'))
bot.command('echo', (ctx) => ctx.reply(ctx.args || '(nothing to echo)'))

ctx.command gives the parsed command name and ctx.args gives the text after it — for /echo hello world, they are "echo" and "hello world".


Middleware

Middleware follows the Koa signature (ctx, next). Call next() to continue; skip it to stop the update from reaching handlers.

// Logging
bot.use(async (ctx, next) => {
  const t = Date.now()
  await next()
  console.log(`update ${ctx.update.update_id} took ${Date.now() - t}ms`)
})

// Simple auth gate — populate ctx.state for downstream handlers
const ADMINS = new Set(['1001', '1002'])
bot.use(async (ctx, next) => {
  ctx.state.isAdmin = !!ctx.from && ADMINS.has(ctx.from.id)
  await next()
})

bot.command('secret', (ctx) => {
  if (!ctx.state.isAdmin) return ctx.reply('Forbidden')
  return ctx.reply('🔑 Admin area')
})

Error handling

Any error thrown by a handler or middleware is routed to the handler you set with bot.catch(). The default handler logs to console.error.

bot.catch((err, ctx) => {
  console.error(`Error while handling update ${ctx.update.update_id}:`, err)
})

The polling loop itself is resilient: transient getUpdates failures are retried with exponential backoff (1s → 30s, reset on success), so a brief network blip will not crash the bot.


Sessions

Call bot.session() to get a persistent, per-user ctx.session — ideal for multi-step forms and menus. It is loaded before your handlers and saved after, so state carries across messages from the same user.

bot.session()

// Remembers state per user, across messages
bot.command('count', (ctx) => {
  ctx.session.n = (ctx.session.n ?? 0) + 1
  return ctx.reply(`Count: ${ctx.session.n}`)
})

Sessions shine for multi-step conversations — for example, a small form:

bot.command('setname', (ctx) => {
  ctx.session.step = 'name'
  return ctx.reply('What is your name?')
})

bot.on('message', (ctx) => {
  if (ctx.session.step !== 'name' || !ctx.text) return
  ctx.session.name = ctx.text
  ctx.session.step = undefined
  return ctx.reply(`Nice to meet you, ${ctx.session.name}!`)
})

By default the key is "<chatId>:<fromId>" (per user, per chat) and storage is in-memory. For persistent or multi-instance bots, pass a store and/or a custom getSessionKey:

bot.session({
  store: myStore,                         // implements SessionStore
  getSessionKey: (ctx) => ctx.from?.id,   // one session per user, any chat
  defaultSession: () => ({ step: 0 }),
})

A SessionStore is any { get, set, delete } (sync or async). Set ctx.session = null to clear the stored session.

Redis-backed sessions

The SDK ships a RedisSessionStore and stays dependency-free — you bring your own ioredis (or compatible) client. Install it, connect, and plug it in; without it, sessions simply stay in-memory.

import Redis from 'ioredis'
import { Bot, RedisSessionStore } from '@clustersoft/mgr-bot-sdk'

const redis = new Redis(process.env.REDIS_URL) // your own client

bot.session({
  store: new RedisSessionStore(redis, {
    prefix: 'mybot:sess:',  // key prefix (default "session:")
    ttlSeconds: 86400,      // optional expiry, refreshed on write
  }),
})

Any client exposing get/set/del works (the RedisLike interface). Values are JSON-serialised; keys are "<prefix><sessionKey>".


Inline keyboards & callbacks

import { Markup } from '@clustersoft/mgr-bot-sdk'

bot.command('menu', (ctx) =>
  ctx.reply('Choose one:', Markup.inlineKeyboard([
    [Markup.button.callback('Yes', 'yes'), Markup.button.callback('No', 'no')],
    [Markup.button.url('Website', 'https://clustersoft.uz')],
  ])),
)

bot.action('yes', (ctx) => ctx.answerCbQuery('Thanks!', true)) // blocking alert
bot.action('no',  (ctx) => ctx.answerCbQuery('No worries'))    // small toast

You can also match callback data with a regular expression:

bot.action(/^item:(\d+)$/, (ctx) => {
  const id = ctx.callbackData!.split(':')[1]
  return ctx.answerCbQuery(`Selected item ${id}`)
})

Editing messages in place

After a button press, update the message the button was attached to instead of sending a new one — the classic counter / pagination pattern. A bot may edit only its own messages.

const kb = (n: number) => Markup.inlineKeyboard([
  [Markup.button.callback('➖', `set:${n - 1}`), Markup.button.callback('➕', `set:${n + 1}`)],
])

bot.command('counter', (ctx) => ctx.reply('Count: 0', kb(0)))

bot.action(/^set:(-?\d+)$/, async (ctx) => {
  const n = Number(ctx.callbackData!.split(':')[1])
  await ctx.editMessageText(`Count: ${n}`, kb(n))
  await ctx.answerCbQuery()
})
  • ctx.editMessageText(text, extra?) — replace text (and optionally the keyboard).
  • ctx.editMessageReplyMarkup(markup?) — replace only the keyboard; omit it to remove the keyboard. Keeps the existing text.
  • ctx.deleteMessage() — delete the callback message entirely.

These act on the message tied to the current callback. To target an arbitrary message, use the client directly: bot.api.editMessageText(chatId, messageId, text) or bot.api.deleteMessage(chatId, messageId).

// A self-destructing confirmation
bot.action('dismiss', (ctx) => ctx.deleteMessage())

A bot may edit or delete only its own messages — attempting to modify a user's message fails with a 403.


Reply keyboards

A reply keyboard replaces the user's on-screen keyboard with your own custom buttons, shown below the input box. Tapping a button sends its label as an ordinary message, so you handle it like any other text.

import { Markup } from '@clustersoft/mgr-bot-sdk'

bot.command('menu', (ctx) =>
  ctx.reply('Main menu:', Markup.keyboard([
    ['Balance', 'History'],
    ['Settings'],
  ], { resize: true })),
)

bot.hears('Balance', (ctx) => ctx.reply('Your balance is 0'))
  • Markup.keyboard(rows, opts?) — bare strings become buttons; pass { resize: true } to shrink the keyboard and { oneTime: true } to hide it after a single tap.

A button can instead ask the user to share their phone number or location. The reply arrives as a normal message carrying ctx.contact / ctx.location.

bot.command('start', (ctx) =>
  ctx.reply('Please share your phone:', Markup.keyboard([
    [Markup.button.contactRequest('📱 Share phone')],
    [Markup.button.locationRequest('📍 Share location')],
  ], { resize: true, oneTime: true })),
)

bot.on('message', (ctx) => {
  if (ctx.contact) return ctx.reply(`Thanks, ${ctx.contact.phone_number}`)
})

When you're done, remove the keyboard to restore the user's normal one:

bot.command('done', (ctx) => ctx.reply('All set!', Markup.removeKeyboard()))

Internationalisation (i18n)

An MGR update identifies who sent it, but carries no language field — so a bot cannot detect the user's language automatically. The rule that follows is simple: ask once, then remember. The companion package @clustersoft/i18n provides the translation engine plus a /bot layer that plugs straight into this SDK: per-update locale resolution, ctx.t, and a ready-made language picker.

npm install @clustersoft/i18n

Quick start

Register bot.session() first — the chosen locale rides along in the session — then the i18n middleware. From that point every handler has ctx.t.

import { Bot } from '@clustersoft/mgr-bot-sdk'
import { I18n } from '@clustersoft/i18n'
import { BotI18n } from '@clustersoft/i18n/bot'
import '@clustersoft/i18n/bot/augment' // types for ctx.t / ctx.i18n

const i18n = new I18n({ defaultLocale: 'uz', directory: './locales' })
const botI18n = new BotI18n(i18n)

const bot = new Bot(process.env.BOT_TOKEN!, {
  apiRoot: 'https://api.clustersoft.uz/api/v1/botapi',
})

bot.session()                  // locale is stored in the session
bot.use(botI18n.middleware())  // attaches ctx.t and ctx.i18n

bot.start((ctx) => ctx.reply(ctx.t('welcome', { name: 'Eldor' })))
bot.command('balance', (ctx) => ctx.reply(ctx.t('balance', { amount: 45000 })))

bot.launch()
const { Bot } = require('@clustersoft/mgr-bot-sdk')
const { I18n } = require('@clustersoft/i18n')
const { BotI18n } = require('@clustersoft/i18n/bot')

const i18n = new I18n({ defaultLocale: 'uz', directory: './locales' })
const botI18n = new BotI18n(i18n)
const bot = new Bot(process.env.BOT_TOKEN, { apiRoot: '…/api/v1/botapi' })

bot.session()
bot.use(botI18n.middleware())
bot.start((ctx) => ctx.reply(ctx.t('welcome', { name: 'Eldor' })))
bot.launch()

Asking for a language

ctx.i18n.languageKeyboard() builds an inline keyboard with one button per loaded locale — each label written in its own language, the active one marked — emitting locale:<code> when pressed. handleLanguageCallback(bot) registers the matching action() handler: it persists the choice, acknowledges the press, and calls your onChanged.

bot.start(async (ctx) => {
  if (ctx.session.locale) return ctx.reply(ctx.t('welcome', { name: 'Eldor' }))
  await ctx.reply(ctx.t('choose_language'), ctx.i18n.languageKeyboard({ columns: 2 }))
})

bot.command('lang', (ctx) =>
  ctx.reply(ctx.t('choose_language'), ctx.i18n.languageKeyboard()),
)

botI18n.handleLanguageCallback(bot, {
  // ctx.t already speaks the newly chosen language here
  onChanged: (ctx) => ctx.editMessageText(ctx.t('welcome', { name: 'Eldor' })),
})

Register handleLanguageCallback before any broad action(/…/) handler — an update reaches the first matching handler only. Inside a handler you can also switch the locale yourself with await ctx.i18n.setLocale('ru'), which applies it to the rest of the update and persists it.

Where the locale is stored

By default the locale lives on ctx.session (SessionLocaleStore), so it is persisted by whatever session backend the bot already uses — including RedisSessionStore. Nothing else to wire up:

bot.session({ store: new RedisSessionStore(redis) })
bot.use(botI18n.middleware())

To keep the locale outside the session — or to read it from your own user table — pass a store or a resolver:

import Redis from 'ioredis'
import { RedisLocaleStore } from '@clustersoft/i18n'

const botI18n = new BotI18n(i18n, {
  store: new RedisLocaleStore(new Redis(process.env.REDIS_URL!)),
  resolveLocale: (ctx) => db.getUserLang(ctx.from?.id), // highest priority
  getLocaleKey: (ctx) => `${ctx.from?.id}`,             // default "<chatId>:<fromId>"
})

Message files, plural forms, {name} interpolation, namespaces, Intl number/date/currency formatting and the uz (Latin) vs oz (Cyrillic) convention are documented in the @clustersoft/i18n README.


Inline mode

When a user types @your_bot <query> in any composer, your bot receives an inline query and answers with a list of results.

import { articleResult } from '@clustersoft/mgr-bot-sdk'

bot.inlineQuery((ctx) => {
  const q = ctx.inlineQuery!.query || 'hello'
  return ctx.answerInlineQuery([
    articleResult('1', `Send: ${q}`, q, { description: 'Send this text' }),
    articleResult('2', `UPPER: ${q.toUpperCase()}`, q.toUpperCase()),
  ])
})

Webhook mode

For production, let the platform push updates to your public HTTPS endpoint instead of polling.

await bot.launchWebhook({
  domain: 'https://bot.example.com', // public origin the platform POSTs to
  path: '/hook',
  port: 8443,
  secretToken: process.env.WEBHOOK_SECRET, // echoed as X-ClusterSoft-Bot-Secret
})

Or mount the handler on an HTTP server / framework you already run:

import http from 'http'

http.createServer(bot.webhookCallback('/hook')).listen(8443)
await bot.api.setWebhook('https://bot.example.com/hook', {
  secret_token: process.env.WEBHOOK_SECRET,
})

Validate the X-ClusterSoft-Bot-Secret request header against your secretToken at your edge/proxy to reject forged calls.


The Context object

| Member | Type | Description | |--------|------|-------------| | ctx.update | Update | the raw incoming update | | ctx.message | Message? | present for message updates | | ctx.callbackQuery | CallbackQuery? | present for button presses | | ctx.inlineQuery | InlineQuery? | present for inline queries | | ctx.text | string? | text of the incoming message | | ctx.command | string? | parsed command name, e.g. echo for /echo hi | | ctx.args | string | text after the command, trimmed | | ctx.from | User? | who triggered the update | | ctx.chat | Chat? | the conversation (for messages / callbacks) | | ctx.callbackData | string? | callback_data of the pressed button | | ctx.state | Record<string, unknown> | per-update scratch space for middleware | | ctx.session | any | persistent per-user session (with bot.session()) | | ctx.me | User | the bot's own account | | ctx.api | ApiClient | the full Bot API client | | ctx.reply(text, extra?) | Promise<Message> | reply in the current chat | | ctx.replyWithPhoto(file, extra?) / ctx.replyWithDocument(file, extra?) | Promise<Message> | reply with media (upload or file_id) | | ctx.getFileUrl(fileId) | Promise<{ file_url }> | download URL for received media | | ctx.fileId | string? | file_id of the message's media | | ctx.replyWithLocation(lat, lng) / ctx.replyWithContact(phone, name, extra?) | Promise<Message> | reply with a location / contact | | ctx.location / ctx.contact | object? | shared location / contact on the message | | ctx.getChat() | Promise<ChatFullInfo> | info about the current chat | | ctx.sendChatAction(action?) / ctx.sendTyping() | Promise<boolean> | show a transient status | | ctx.editMessageText(text, extra?) | Promise<Message> | edit the callback message's text/keyboard | | ctx.editMessageReplyMarkup(markup?) | Promise<Message> | edit only the callback message's keyboard | | ctx.deleteMessage() | Promise<boolean> | delete the callback message | | ctx.react(emoji?) | Promise<boolean> | react to the current message (omit to clear) | | ctx.answerCbQuery(text?, alert?) | Promise<boolean> | acknowledge a callback | | ctx.answerInlineQuery(results) | Promise<boolean> | answer an inline query |


Bot API client

bot.api (also ctx.api) is a typed HTTP client for the Bot API. Use it for anything the ctx shortcuts don't cover:

await bot.api.setMyCommands([
  { command: 'start', description: 'Start the bot' },
  { command: 'help',  description: 'Show help' },
])

await bot.api.sendMessage(chatId, 'Direct message')

// Call a method that isn't wrapped yet:
await bot.api.raw('someNewMethod', { foo: 1 })

Failed calls throw an ApiError with .code and .description:

import { ApiError } from '@clustersoft/mgr-bot-sdk'

try {
  await bot.api.sendMessage(chatId, '')
} catch (err) {
  if (err instanceof ApiError) console.error(err.code, err.description)
}

Media

A bot can upload a fresh file, re-send media it received by file_id, and download received media.

import { readFileSync } from 'fs'

// Upload from disk (Buffer) — or { source, filename, contentType }
await bot.api.sendPhoto(chatId, readFileSync('cat.jpg'), { caption: 'A cat' })
await bot.api.sendDocument(chatId, {
  source: readFileSync('report.pdf'),
  filename: 'report.pdf',
  contentType: 'application/pdf',
})

// Re-send received media by file_id (a string)
bot.on('message', async (ctx) => {
  if (ctx.fileId) await ctx.replyWithPhoto(ctx.fileId, { caption: 'Got it!' })
})

// Download a received file → short-lived URL
bot.on('message', async (ctx) => {
  if (ctx.fileId) {
    const { file_url } = await ctx.getFileUrl(ctx.fileId)
    console.log('download from', file_url)
  }
})
  • ctx.fileIdfile_id of whatever media the message carries (photo, video, audio, document, sticker, animation, voice).
  • Send methods sendPhoto/sendDocument/sendVideo/sendAudio/sendVoice accept either a file_id string or file bytes (Buffer/Uint8Array or { source, filename?, contentType? }).
  • getFile(fileId, chatId){ file_id, file_url } (URL valid for a few minutes).

Location & contacts

// Send a point on the map, or share a phone contact
await bot.api.sendLocation(chatId, 41.311081, 69.240562)
await bot.api.sendContact(chatId, '+998901234567', 'Aziz', { last_name: 'Karimov' })

// Receive them
bot.on('message', (ctx) => {
  if (ctx.location) return ctx.reply(`📍 ${ctx.location.latitude}, ${ctx.location.longitude}`)
  if (ctx.contact)  return ctx.reply(`📞 ${ctx.contact.first_name}: ${ctx.contact.phone_number}`)
})

Chat info & bot profile

// Info about a chat the bot belongs to
const chat = await bot.api.getChat(chatId)   // { id, type, title?, member_count? }
const here = await ctx.getChat()             // the current chat

// Copy a message (no "forwarded from" header)
await bot.api.copyMessage(targetChatId, fromChatId, messageId)

// Present the bot like BotFather does
await bot.api.setMyDescription('I turn your notes into reminders.')      // empty-chat screen
await bot.api.setMyShortDescription('Notes → reminders, instantly.')     // profile page
await bot.api.deleteMyCommands()                                         // clear the menu

Available methods

| Method | Purpose | |--------|---------| | getMe() | bot account info | | getUpdates(params) | fetch updates (used internally by polling) | | sendMessage(chatId, text, extra?) | send a text message | | sendPhoto / sendDocument / sendVideo / sendAudio / sendVoice (chatId, file, extra?) | send media (upload bytes or file_id) | | sendLocation(chatId, latitude, longitude) | send a point on the map | | sendContact(chatId, phoneNumber, firstName, extra?) | send a phone contact | | getFile(fileId, chatId) | resolve a received file_id to a download URL | | sendChatAction(chatId, action?) | show a transient status ("typing…") | | copyMessage(chatId, fromChatId, messageId, extra?) | send a copy of a message | | editMessageText(chatId, messageId, text, extra?) | edit a message's text/keyboard | | editMessageReplyMarkup(chatId, messageId, markup?) | edit only a message's keyboard | | deleteMessage(chatId, messageId) | delete a message | | setMessageReaction(chatId, messageId, emoji?) | react to a message (omit emoji to clear) | | getChat(chatId) | get info about a chat | | answerCallbackQuery(id, extra?) | acknowledge a button press | | answerInlineQuery(id, results) | answer an inline query | | setMyCommands(commands) / getMyCommands() / deleteMyCommands() | manage the command menu | | getMyName() | the bot's display name | | setMyDescription(text) / getMyDescription() | empty-chat description (≤512) | | setMyShortDescription(text) / getMyShortDescription() | profile short description (≤120) | | setWebhook(url, extra?) / deleteWebhook(dropPending?) / getWebhookInfo() | manage webhooks | | raw(method, params?) | call any method by name |


API reference

new Bot(token, options)

| Option | Type | Default | Description | |--------|------|---------|-------------| | apiRoot | string | — | Platform Bot API root, e.g. https://…/api/v1/botapi |

bot.launch(options?)

| Option | Type | Default | Description | |--------|------|---------|-------------| | timeout | number | 30 | long-poll timeout, seconds | | dropPendingUpdates | boolean | false | skip updates queued while offline | | handleSignals | boolean | true | auto-stop on SIGINT/SIGTERM |

bot.launchWebhook(options)

| Option | Type | Default | Description | |--------|------|---------|-------------| | domain | string | — | public HTTPS origin | | path | string | '/' | update endpoint path | | port | number | — | local port to bind | | secretToken | string | — | shared secret header value | | dropPendingUpdates | boolean | false | skip queued updates on register |

Other lifecycle methods: bot.webhookCallback(path?), bot.stop().


Examples

Runnable examples live in examples/:

| File | Shows | |------|-------| | echo.ts | polling, middleware, catch, command/args (TypeScript) | | echo.js | the same idea in plain CommonJS JavaScript | | menu.ts | inline keyboard + inline mode | | counter.ts | editing a message in place after a button press | | webhook.ts | webhook transport |

BOT_TOKEN=<token> API_ROOT=https://…/api/v1/botapi npx ts-node examples/echo.ts

License

MIT © ClusterSoft