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

@mundobobba/mbjs

v1.0.1

Published

Official SDK for building bots on the Mundo Bobba platform

Readme

@mundobobba/bot-sdk

Official SDK for building bots on the Mundo Bobba platform.

npm version License: MIT

Installation

npm install @mundobobba/bot-sdk

System requirements (for music bots)

  • yt-dlp installed and available in PATH
  • FFmpeg installed and available in PATH
  • A cookies.txt file from YouTube in your bot's working directory

Quick start

import { MundobobbaBot } from '@mundobobba/bot-sdk';

const bot = new MundobobbaBot({
  token: process.env.BOT_TOKEN!,
  prefix: '!',
});

bot.on('ready', () => {
  console.log(`Bot online as ${bot.botName}`);
});

bot.textCommand('ping', (ctx) => {
  ctx.reply('Pong!');
});

Getting your token

  1. Go to mundobobba.com and sign in
  2. Navigate to Messages > Bots
  3. Create a new bot and copy the generated token
  4. Invite the bot to a conversation using the invite link

Text commands

Register commands that respond to messages starting with your prefix.

const bot = new MundobobbaBot({ token: '...', prefix: '!' });

// Simple reply
bot.textCommand('hello', (ctx) => {
  ctx.reply(`Hello, ${ctx.invokedBy.name}!`);
});

// Access command arguments
bot.textCommand('echo', (ctx) => {
  ctx.reply(ctx.args || 'Nothing to echo.');
});

// Built-in commands: !ping, !uptime, !help
bot.useBuiltins();

Command context

Every command handler receives a CommandContext with:

| Property | Type | Description | |---|---|---| | command | string | Command name | | args | string | Arguments after the command | | convId | number | Conversation ID | | invokedBy | { id, name } | User who invoked | | reply(message) | function | Public reply (everyone sees) | | replyEphemeral(message) | function | Reply only the invoker sees | | deferReply() | function | Show "thinking..." indicator | | followUp(message) | function | Additional message in same block |


Embeds

Send rich cards with title, description, color, fields, images, and more.

bot.textCommand('stats', async (ctx) => {
  const user = await bot.getUser(ctx.invokedBy.id);

  ctx.reply({
    embed: {
      title: user.name,
      description: `Level ${user.level}`,
      color: '#5865F2',
      thumbnail: `https://habbo.com/avatar/${user.name}`,
      fields: [
        { name: 'Coins', value: user.coin.toLocaleString(), inline: true },
        { name: 'XP', value: user.xp.toLocaleString(), inline: true },
      ],
      footer: { text: 'Mundo Bobba' },
      timestamp: new Date().toISOString(),
    },
  });
});

Buttons

Add interactive buttons to your messages.

bot.textCommand('confirm', (ctx) => {
  ctx.reply({
    content: 'Are you sure?',
    components: [{
      type: 'action_row',
      components: [
        { type: 'button', style: 'success', label: 'Yes', customId: 'yes' },
        { type: 'button', style: 'danger', label: 'No', customId: 'no' },
        { type: 'button', style: 'link', label: 'Docs', url: 'https://mundobobba.com' },
      ],
    }],
  });
});

bot.on('interaction', (ctx) => {
  if (ctx.customId === 'yes') ctx.reply('Confirmed!');
  if (ctx.customId === 'no') ctx.replyEphemeral('Cancelled.');
});

Button styles

| Style | Appearance | |---|---| | 'primary' | Blue (default action) | | 'secondary' | Gray (secondary action) | | 'success' | Green (confirm) | | 'danger' | Red (destructive) | | 'link' | Opens a URL (no interaction event) |


Select menus

Add dropdown menus to your messages.

bot.textCommand('class', (ctx) => {
  ctx.reply({
    embed: { title: 'Choose your class', color: '#FF9900' },
    components: [{
      type: 'action_row',
      components: [{
        type: 'select',
        customId: 'class_select',
        placeholder: 'Select a class...',
        options: [
          { label: 'Warrior', value: 'warrior', emoji: '⚔️' },
          { label: 'Mage', value: 'mage', emoji: '🧙' },
          { label: 'Archer', value: 'archer', emoji: '🏹' },
        ],
      }],
    }],
  });
});

bot.on('interaction', (ctx) => {
  if (ctx.customId === 'class_select') {
    ctx.reply(`You chose: ${ctx.values[0]}`);
  }
});

Deferred replies

Show a "thinking..." indicator while processing.

bot.textCommand('weather', async (ctx) => {
  const deferred = ctx.deferReply();

  const data = await fetchWeatherAPI(ctx.args);

  deferred.editReply({
    embed: {
      title: `Weather in ${data.city}`,
      description: `${data.temp} - ${data.condition}`,
      color: '#87CEEB',
    },
  });
});

Ephemeral messages

Send messages only the invoker can see.

bot.textCommand('secret', (ctx) => {
  ctx.replyEphemeral(`Only you can see this, ${ctx.invokedBy.name}!`);
});

Modals / Forms

Open a form dialog and handle submissions.

bot.textCommand('feedback', (ctx) => {
  bot.showModal(ctx.convId, ctx.invokedBy.id, {
    customId: 'feedback_form',
    title: 'Send Feedback',
    fields: [
      { type: 'text', customId: 'subject', label: 'Subject', required: true },
      { type: 'textarea', customId: 'body', label: 'Message', maxLength: 500 },
    ],
  });
});

bot.on('modalSubmit', (ctx) => {
  if (ctx.customId === 'feedback_form') {
    ctx.reply(`Thanks for your feedback, ${ctx.invokedBy.name}!`);
    console.log(ctx.values); // { subject: '...', body: '...' }
  }
});

Collectors

Wait for messages or interactions with a timeout.

// Wait for a message
bot.textCommand('ask', async (ctx) => {
  ctx.reply('What is your favorite color? (15s)');

  const messages = await bot.awaitMessages(ctx.convId, {
    filter: (m) => m.senderId === ctx.invokedBy.id,
    timeout: 15_000,
    max: 1,
  });

  if (messages.length > 0) {
    ctx.followUp(`You said: ${messages[0].content}`);
  } else {
    ctx.followUp('Time is up!');
  }
});

// Wait for a button click
const clicks = await bot.awaitInteraction(ctx.convId, {
  filter: (i) => i.customId === 'confirm',
  timeout: 30_000,
  max: 1,
});

Paginated embeds

Navigate between pages using buttons.

import { PaginatedEmbed } from '@mundobobba/bot-sdk';

bot.textCommand('rules', (ctx) => {
  const pages = [
    { title: 'Rules — General', description: '1. Be respectful\n2. No spam', color: '#5865F2' },
    { title: 'Rules — Chat', description: '3. Use correct channels\n4. No flood', color: '#5865F2' },
    { title: 'Rules — Trade', description: '5. No scam\n6. Fair prices', color: '#5865F2' },
  ];

  const paginator = new PaginatedEmbed(pages);
  paginator.send(ctx);

  // Handle page navigation
  bot.on('interaction', (i) => paginator.handleInteraction(i));
});

Confirm dialogs

Ask yes/no questions with a Promise.

import { ConfirmDialog } from '@mundobobba/bot-sdk';

bot.textCommand('reset', async (ctx) => {
  const confirmed = await ConfirmDialog.ask(ctx, {
    title: 'Confirm Reset',
    description: 'Are you sure you want to reset your data?',
    color: '#FF0000',
  });

  if (confirmed) ctx.followUp('Data has been reset.');
  else ctx.followUp('Cancelled.');
});

// Required: handle confirm dialog interactions
bot.on('interaction', (ctx) => ConfirmDialog.handleInteraction(ctx.customId));

Presence

Set the bot's status and activity.

bot.setPresence({
  status: 'online',
  activity: { type: 'listening', name: 'Lo-fi Hip Hop' },
});

| Status | Description | |---|---| | 'online' | Green indicator | | 'idle' | Yellow indicator | | 'dnd' | Red indicator |

| Activity type | Display | |---|---| | 'playing' | Playing [name] | | 'listening' | Listening to [name] | | 'watching' | Watching [name] | | 'streaming' | Streaming [name] |


Event listeners

// Any chat message in conversations with the bot
bot.on('message', (msg) => {
  console.log(`${msg.senderName}: ${msg.content}`);
});

// User reacted to a message
bot.on('reaction', (data) => {
  console.log(`${data.userName} reacted with ${data.emoji}`);
});

// User joined a conversation
bot.on('memberJoin', (data) => {
  bot.sendMessage(data.convId, `Welcome, ${data.userName}!`);
});

// User left a conversation
bot.on('memberLeave', (data) => {
  bot.sendMessage(data.convId, `${data.userName} left.`);
});

// Someone mentioned @BotName
bot.on('mention', (data) => {
  bot.sendMessage(data.convId, `You called, ${data.userName}?`);
});

// A message was edited
bot.on('messageEdit', (data) => {
  console.log(`Message ${data.messageId} was edited.`);
});

// A message was deleted
bot.on('messageDelete', (data) => {
  console.log(`Message ${data.messageId} was deleted.`);
});

Platform API

Query platform data directly from your bot.

// Get user info
const user = await bot.getUser(userId);
console.log(user.name, user.level, user.coin);

// Get user permissions
const perms = await bot.getPermissions(userId);
if (!perms.isAdmin) ctx.replyEphemeral('No permission.');

// Get conversation info
const conv = await bot.getConversation(convId);
console.log(conv.name, conv.memberCount, conv.members);

// Get user profile (full)
const profile = await bot.api('getUserProfile', { name: 'PlayerOne' });

Scheduled messages

Send messages at a specific time.

await bot.schedule(convId, 'Good morning!', new Date('2026-04-06T08:00:00Z'));

await bot.schedule(convId, {
  embed: { title: 'Reminder', description: 'Meeting starts now!', color: '#FF0000' },
}, new Date(Date.now() + 60_000)); // 1 minute from now

Music bot

Build a music bot with audio streaming.

import { MundobobbaBot, AudioStreamer, YouTubeResolver, QueueService } from '@mundobobba/bot-sdk';

const TOKEN = process.env.BOT_TOKEN!;
const bot = new MundobobbaBot({ token: TOKEN, prefix: '!' });
const streamer = new AudioStreamer({ token: TOKEN });
const youtube = new YouTubeResolver();
const queue = new QueueService();

bot.textCommand('play', async (ctx) => {
  const result = await youtube.search(ctx.args);
  if (!result) { ctx.reply('Not found.'); return; }

  if (streamer.isStreaming(ctx.convId)) {
    queue.add(ctx.convId, { ...result, addedBy: ctx.invokedBy.name });
    ctx.reply(`Added to queue: ${result.title}`);
    return;
  }

  ctx.reply(`Now playing: ${result.title}`);
  bot.broadcastMusicPlaying(ctx.convId, result);

  await streamer.start(ctx.convId, result.url, (convId) => {
    const next = queue.shift(convId);
    if (!next) { bot.broadcastMusicStopped(convId); return; }
    bot.broadcastMusicPlaying(convId, next);
    streamer.start(convId, next.url, () => {});
  });
});

bot.textCommand('stop', (ctx) => {
  streamer.stop(ctx.convId);
  queue.clear(ctx.convId);
  bot.broadcastMusicStopped(ctx.convId);
  ctx.reply('Stopped.');
});

Rate limiter

Prevent command spam.

import { RateLimiter } from '@mundobobba/bot-sdk';

// Per user (default)
const limiter = new RateLimiter();

// Per conversation
const limiter = new RateLimiter({ mode: 'conversation' });

// Custom cooldowns
const limiter = new RateLimiter({
  cooldowns: { play: 5000, skip: 2000 },
});

bot.textCommand('play', (ctx) => {
  const result = limiter.check(ctx.invokedBy.id, 'play');
  if (!result.allowed) {
    ctx.replyEphemeral(`Wait ${result.remainingSec}s.`);
    return;
  }
  // ... handle command
});

Edit and delete messages

// Edit a bot message
bot.editMessage('bot_123', convId, 'Updated content');

// Delete a bot message
bot.deleteMessage('bot_123', convId);

Slash commands

Register commands that appear in the platform's command picker.

bot.on('ready', () => {
  bot.registerCommands([
    {
      name: 'play',
      description: 'Play a song',
      options: [{ name: 'query', description: 'Song name or URL', required: true }],
    },
    { name: 'stop', description: 'Stop playback' },
  ]);
});

bot.on('command', (ctx) => {
  if (ctx.command === 'play') {
    // Handle slash command
  }
});

API Reference

MundobobbaBot

| Method | Description | |---|---| | textCommand(name, handler) | Register a text command | | useBuiltins() | Register ping, uptime, help | | registerCommands(commands) | Register slash commands | | sendMessage(convId, message) | Send public message | | sendEphemeral(convId, message, userId) | Send ephemeral message | | editMessage(msgId, convId, message) | Edit bot message | | deleteMessage(msgId, convId) | Delete bot message | | showModal(convId, userId, modal) | Open modal dialog | | setPresence(presence) | Set bot status | | broadcastMusicPlaying(convId, info) | Broadcast now playing | | broadcastMusicStopped(convId) | Broadcast music stopped | | broadcastQueueUpdated(convId, queue) | Update queue UI | | api(method, params) | Call platform API | | getUser(userId) | Get user info | | getConversation(convId) | Get conversation info | | getPermissions(userId) | Get user permissions | | schedule(convId, message, date) | Schedule a message | | awaitMessages(convId, options) | Wait for messages | | awaitInteraction(convId, options) | Wait for interactions |

Events

| Event | Payload | Description | |---|---|---| | ready | — | Bot connected | | command | CommandContext | Slash command invoked | | message | ChatMessage | Any chat message | | interaction | ComponentInteraction | Button/select interaction | | reaction | ReactionEvent | Message reaction | | memberJoin | MemberEvent | User joined | | memberLeave | MemberEvent | User left | | mention | { userId, userName, convId, content } | Bot was mentioned | | modalSubmit | ModalSubmitEvent | Modal form submitted | | messageEdit | { messageId, content, convId } | Message edited | | messageDelete | { messageId, convId } | Message deleted | | disconnect | — | WebSocket disconnected | | error | Error | WebSocket error |


License

MIT