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

novaapp-sdk

v1.4.2

Published

Official SDK for building bots on the Nova platform

Readme

novaapp-sdk

Official SDK for building bots on the Nova platform.

Current version: 1.3.0

Table of contents


Installation

npm install novaapp-sdk

Quick start

import { NovaClient, EmbedBuilder } from 'novaapp-sdk'

const client = new NovaClient({ token: process.env.NOVA_BOT_TOKEN! })

client.on('ready', async (bot) => {
  console.log(`Logged in as ${bot.botUser.username}`)

  await client.commands.setSlash([
    { name: 'ping', description: 'Check if the bot is alive' },
  ])
})

// Slash command routing
client.command('ping', async (interaction) => {
  await interaction.reply('Pong! 🏓')
})

// Button routing
client.button('confirm', async (interaction) => {
  await interaction.replyEphemeral('Confirmed!')
})

// Modal submission routing
client.modal('report_modal', async (interaction) => {
  const reason = interaction.modalData['reason']
  await interaction.replyEphemeral(`Report received: ${reason}`)
})

await client.connect()

Configuration

| Option | Type | Default | Description | |---|---|---|---| | token | string | required | Your bot token (nova_bot_...) | | baseUrl | string | https://novachatapp.com | Override for self-hosted Nova servers |


Routing handlers

Instead of handling everything inside interactionCreate, use the built-in router:

// /slash and !prefix commands
client.command('ban', async (interaction) => {
  const userId = interaction.options.getUser('user', true)
  const reason = interaction.options.getString('reason') ?? 'No reason given'
  await client.members.ban(interaction.serverId!, userId, reason)
  await interaction.reply(`✅ Banned <@${userId}>`)
})

// Button clicks
client.button('delete_confirm', async (interaction) => {
  await interaction.replyEphemeral('Deleted.')
})

// Select menus
client.selectMenu('colour_pick', async (interaction) => {
  const chosen = interaction.values[0]
  await interaction.reply(`You picked: ${chosen}`)
})

// Modal submissions
client.modal('feedback_modal', async (interaction) => {
  const text = interaction.modalData['feedback']
  await interaction.replyEphemeral(`Thanks: ${text}`)
})

Rich wrappers

NovaInteraction

Returned by every interaction event and routing handler.

client.on('interactionCreate', async (interaction) => {
  interaction.id           // string
  interaction.type         // 'SLASH_COMMAND' | 'BUTTON_CLICK' | 'SELECT_MENU' | 'MODAL_SUBMIT' | …
  interaction.commandName  // string | null
  interaction.customId     // string | null  (buttons / selects / modals)
  interaction.userId       // string
  interaction.channelId    // string
  interaction.serverId     // string | null
  interaction.values       // string[]  (select menu choices)
  interaction.modalData    // Record<string, string>  (modal submitted values)

  // Type guards
  interaction.isSlashCommand()
  interaction.isPrefixCommand()
  interaction.isCommand()       // slash OR prefix
  interaction.isButton()
  interaction.isSelectMenu()
  interaction.isModalSubmit()
  interaction.isAutocomplete()
  interaction.isContextMenu()

  // Typed option accessor
  const userId = interaction.options.getUser('user', true)
  const reason = interaction.options.getString('reason') ?? 'No reason'
  const count  = interaction.options.getInteger('count')  // number | null

  // Actions
  await interaction.reply('Hello!')
  await interaction.reply({ content: 'Hi', ephemeral: true, embed: { title: 'Stats' } })
  await interaction.replyEphemeral('Only you can see this.')
  await interaction.defer()            // acknowledge — shows loading state
  await interaction.editReply('Done!') // edit after defer()

  // Open a modal and await its submission
  const submitted = await interaction.openModal(
    new ModalBuilder()
      .setTitle('Report')
      .setCustomId('report')
      .addField(new TextInputBuilder().setCustomId('reason').setLabel('Reason').setRequired(true))
  )
  if (submitted) {
    await submitted.replyEphemeral(`Received: ${submitted.modalData['reason']}`)
  }
})

NovaMessage

Returned by messageCreate / messageUpdate events and message fetch calls.

client.on('messageCreate', async (msg) => {
  msg.id          // string
  msg.content     // string
  msg.channelId   // string
  msg.author      // { id, username, displayName, avatar, isBot }
  msg.embed       // Embed | null
  msg.components  // MessageComponent[]
  msg.attachments // Attachment[]
  msg.reactions   // Reaction[]
  msg.replyToId   // string | null
  msg.createdAt   // Date
  msg.editedAt    // Date | null

  msg.isFromBot()
  msg.hasEmbed()
  msg.hasComponents()
  msg.isEdited()

  await msg.reply('Got it!')
  await msg.edit('Updated')
  await msg.delete()
  await msg.pin()
  await msg.unpin()
  await msg.react('👍')
  await msg.removeReaction('👍')
  const fresh = await msg.fetch()

  console.log(msg.url)  // '/channels/<id>/messages/<id>'
})

NovaChannel

Returned by client.fetchChannel() and client.fetchChannels().

const channel = await client.fetchChannel('channel-id')

channel.id        // string
channel.name      // string
channel.type      // 'TEXT' | 'VOICE' | 'ANNOUNCEMENT' | 'FORUM' | 'STAGE'
channel.serverId  // string | null
channel.topic     // string | null
channel.position  // number
channel.slowMode  // number (seconds; 0 = disabled)
channel.createdAt // Date

channel.isText()
channel.isVoice()
channel.isAnnouncement()
channel.isForum()
channel.isStage()
channel.hasSlowMode()

await channel.send('Hello!')
await channel.send({ content: 'Hi', embed: { title: 'News' } })
const messages = await channel.fetchMessages({ limit: 50 })
const pins     = await channel.fetchPins()
await channel.startTyping()
const updated  = await channel.edit({ topic: 'New topic', slowMode: 5 })
await channel.delete()

NovaMember

Returned by client.fetchMember() and client.fetchMembers().

const member = await client.fetchMember('server-id', 'user-id')

member.userId       // string
member.serverId     // string
member.username     // string
member.displayName  // string
member.avatar       // string | null
member.role         // 'OWNER' | 'ADMIN' | 'MEMBER'
member.status       // 'ONLINE' | 'IDLE' | 'DND' | 'OFFLINE'
member.isBot        // boolean
member.joinedAt     // Date

member.isOwner()
member.isAdmin()          // ADMIN *or* OWNER
member.isRegularMember()
member.isOnline()
member.isIdle()
member.isDND()
member.isOffline()

await member.kick()
await member.ban('Spamming')
await member.dm('Welcome!')
await member.addRole('role-id')
await member.removeRole('role-id')

API reference

client.messages

await client.messages.send(channelId, { content: 'Hi!' })
await client.messages.send(channelId, { embed: { title: 'Report', color: '#5865F2' } })
await client.messages.send(channelId, {
  content: 'Pick one:',
  components: [
    { type: 'button', customId: 'yes', label: 'Yes', style: 'success' },
    { type: 'button', customId: 'no',  label: 'No',  style: 'danger'  },
  ],
})

await client.messages.edit(messageId, { content: 'Updated' })
await client.messages.delete(messageId)

const msg      = await client.messages.fetchOne(messageId)
const messages = await client.messages.fetch(channelId, { limit: 50, before: cursorId })
const pins     = await client.messages.fetchPinned(channelId)

await client.messages.pin(messageId)
await client.messages.unpin(messageId)
await client.messages.typing(channelId)

await client.messages.addReaction(messageId, '👍')
await client.messages.removeReaction(messageId, '👍')
const reactions = await client.messages.fetchReactions(messageId)

client.channels

const channels = await client.channels.list(serverId)
const channel  = await client.channels.fetch(channelId)

const newChan = await client.channels.create(serverId, {
  name: 'announcements', type: 'ANNOUNCEMENT', topic: 'Official news',
})

await client.channels.edit(channelId, { topic: 'New topic', slowMode: 10 })
await client.channels.delete(channelId)

const messages = await client.channels.fetchMessages(channelId, { limit: 20 })
const pins     = await client.channels.fetchPins(channelId)
await client.channels.startTyping(channelId)

Rich wrapper shortcuts (return NovaChannel):

const channel  = await client.fetchChannel(channelId)
const channels = await client.fetchChannels(serverId)
const textOnly = channels.filter(c => c.isText())

client.reactions

await client.reactions.add(messageId, '🎉')
await client.reactions.remove(messageId, '🎉')
await client.reactions.removeAll(messageId)          // requires messages.manage
await client.reactions.removeEmoji(messageId, '🎉')  // all of one emoji

const all    = await client.reactions.fetch(messageId)
// [{ emoji: '🎉', count: 3, users: [{ id, username, displayName, avatar }] }, …]

const detail = await client.reactions.fetchEmoji(messageId, '🎉')
// { emoji: '🎉', count: 3, users: […] }

client.members

const members = await client.members.list(serverId, { limit: 100 })
await client.members.kick(serverId, userId)
await client.members.ban(serverId, userId, 'Reason')
await client.members.unban(serverId, userId)

const bans = await client.members.listBans(serverId)
// [{ userId, username, displayName, avatar, reason, bannedAt, moderatorId }]

await client.members.dm(userId, 'Hello!')
await client.members.dm(userId, { content: 'Hi', embed: { title: 'Welcome' } })

await client.members.addRole(serverId, userId, roleId)
await client.members.removeRole(serverId, userId, roleId)

Rich wrapper shortcuts (return NovaMember):

const member  = await client.fetchMember(serverId, userId)
const members = await client.fetchMembers(serverId)
const bots    = members.filter(m => m.isBot)

client.servers

const servers = await client.servers.list()
const roles   = await client.servers.listRoles(serverId)
// [{ id, name, color, position, serverId, hoist, createdAt }]

client.commands

await client.commands.setSlash([
  { name: 'ban', description: 'Ban a user', options: [
    { name: 'user',   description: 'User to ban', type: 'USER',   required: true  },
    { name: 'reason', description: 'Reason',      type: 'STRING', required: false },
  ]},
])
const slash = await client.commands.getSlash()
await client.commands.deleteSlash('ban')

await client.commands.setPrefix([{ prefix: '!', name: 'help', description: 'Show help' }])
const prefix = await client.commands.getPrefix()
await client.commands.deletePrefix('!', 'help')

await client.commands.setContext([
  { name: 'Report message', target: 'MESSAGE' },
  { name: 'View profile',   target: 'USER'    },
])

client.interactions

await client.interactions.ack(interaction.id)            // acknowledge (show loading)
await client.interactions.respond(interaction.id, { content: 'Done!', ephemeral: true })

// Open a modal (low-level)
await client.interactions.respond(interaction.id, {
  modal: {
    title: 'Report', customId: 'report_modal',
    fields: [{ customId: 'reason', label: 'Reason', type: 'paragraph', required: true }],
  },
})

const pending = await client.interactions.poll({ limit: 20 })

client.permissions

const result = await client.permissions.get({
  serverId:  'server-id',
  channelId: 'channel-id', // optional
  roleId:    'role-id',    // optional
})
result.permissions // merged Record<string, boolean>
result.records     // raw permission records

client.cron() — recurring tasks

const cancel = client.cron(60_000, async () => {
  const msgs = await client.messages.fetch(channelId, { limit: 1 })
  console.log('Latest:', msgs[0]?.content)
})

cancel() // stop the task

client.setStatus()

client.setStatus('ONLINE')   // default
client.setStatus('IDLE')     // away
client.setStatus('DND')      // Do Not Disturb
client.setStatus('OFFLINE')  // appear offline

client.waitFor() — event fence

// Wait for a message in a specific channel (default 30 s timeout)
const msg = await client.waitFor(
  'messageCreate',
  (m) => m.channelId === channelId,
)

// Wait for a button click, throw after 60 s
const click = await client.waitFor(
  'interactionCreate',
  (i) => i.isButton() && i.customId === 'confirm',
  60_000,
)

Fluent builders

EmbedBuilder

const embed = new EmbedBuilder()
  .setTitle('Server stats')
  .setDescription('All systems operational.')
  .setColor('#5865F2')
  .setThumbnail('https://example.com/logo.png')
  .addField('Members', '1,234', true)
  .addField('Online',  '456',   true)
  .setFooter('Updated just now')
  .setTimestamp()
  .toJSON()

await client.messages.send(channelId, { embed })

ButtonBuilder + ActionRowBuilder

const row = new ActionRowBuilder()
  .addComponent(
    new ButtonBuilder().setCustomId('yes').setLabel('Yes').setStyle('success').toJSON()
  )
  .addComponent(
    new ButtonBuilder().setCustomId('no').setLabel('No').setStyle('danger').toJSON()
  )
  .toJSON()

await client.messages.send(channelId, { content: 'Confirm?', components: [row] })

SelectMenuBuilder

const row = new ActionRowBuilder()
  .addComponent(
    new SelectMenuBuilder()
      .setCustomId('colour_pick')
      .setPlaceholder('Choose a colour')
      .addOption({ label: 'Red',   value: 'red'   })
      .addOption({ label: 'Blue',  value: 'blue'  })
      .addOption({ label: 'Green', value: 'green' })
      .toJSON()
  )
  .toJSON()

await client.messages.send(channelId, { content: 'Pick:', components: [row] })

ModalBuilder + TextInputBuilder

const modal = new ModalBuilder()
  .setTitle('Submit a report')
  .setCustomId('report_modal')
  .addField(
    new TextInputBuilder()
      .setCustomId('subject')
      .setLabel('Subject')
      .setStyle('short')
      .setRequired(true)
      .setMaxLength(100)
  )
  .addField(
    new TextInputBuilder()
      .setCustomId('description')
      .setLabel('Description')
      .setStyle('paragraph')
      .setRequired(true)
  )

const submitted = await interaction.openModal(modal)
if (submitted) {
  const subject = submitted.modalData['subject']
  const desc    = submitted.modalData['description']
  await submitted.replyEphemeral(`Filed: ${subject}`)
}

SlashCommandBuilder

const cmd = new SlashCommandBuilder()
  .setName('kick')
  .setDescription('Kick a member')
  .addOption(
    new SlashCommandOptionBuilder()
      .setName('user').setDescription('Member to kick').setType('USER').setRequired(true)
  )
  .toJSON()

await client.commands.setSlash([cmd])

PollBuilder

const poll = new PollBuilder()
  .setQuestion('Best programming language?')
  .addOption({ id: 'ts', label: 'TypeScript', emoji: '💙' })
  .addOption({ id: 'rs', label: 'Rust',       emoji: '🦀' })
  .addOption({ id: 'py', label: 'Python',     emoji: '🐍' })
  .setMultipleChoice(false)
  .toJSON()

await client.messages.send(channelId, { content: 'Vote below!', ...poll })

MessageBuilder

Compose content, embeds, and components fluently:

const msg = new MessageBuilder()
  .setContent('New announcement!')
  .setEmbed(
    new EmbedBuilder()
      .setTitle('v2.0 Released')
      .setDescription('Check the changelog for details.')
      .setColor('#57F287')
      .toJSON()
  )
  .addRow(
    new ActionRowBuilder()
      .addComponent(
        new ButtonBuilder().setCustomId('changelog').setLabel('View changelog').setStyle('primary').toJSON()
      )
      .toJSON()
  )
  .build()

await client.messages.send(channelId, msg)

Utilities

Collection<K, V>

A supercharged Map with array-style helpers:

const col = new Collection<string, User>()
col.set('1', { id: '1', name: 'Alice' })
col.set('2', { id: '2', name: 'Bob'   })

col.first()                        // { id: '1', name: 'Alice' }
col.last()                         // { id: '2', name: 'Bob' }
col.random()                       // random value
col.find(v => v.name === 'Bob')    // { id: '2', … }
col.filter(v => v.name.length > 3) // Collection with Alice
col.map(v => v.name)               // ['Alice', 'Bob']
col.some(v => v.name === 'Alice')  // true
col.every(v => v.id.length > 0)    // true
col.toArray()                      // [{ … }, { … }]

Paginator<T>

Cursor-based async paginator for any list API:

const paginator = new Paginator(async (cursor) => {
  const messages = await client.messages.fetch(channelId, {
    limit: 50, before: cursor ?? undefined,
  })
  return { items: messages, cursor: messages.at(-1)?.id ?? null }
})

// Async iteration
for await (const msg of paginator) {
  console.log(msg.content)
}

// Collect everything at once
const all = await paginator.fetchAll()

// Collect first N
const first200 = await paginator.fetchN(200)

// Reset and iterate again from the start
paginator.reset()

PermissionsBitfield

Work with Nova bot permissions as a typed bitfield:

import { PermissionsBitfield, Permissions } from 'novaapp-sdk'

const perms = new PermissionsBitfield({
  [Permissions.MESSAGES_READ]:  true,
  [Permissions.MESSAGES_WRITE]: true,
})

perms.has(Permissions.MESSAGES_READ)    // true
perms.has(Permissions.CHANNELS_MANAGE)  // false
perms.hasAll(Permissions.MESSAGES_READ, Permissions.MESSAGES_WRITE) // true
perms.hasAny(Permissions.CHANNELS_MANAGE, Permissions.SERVERS_MANAGE) // false
perms.missing(Permissions.MESSAGES_MANAGE, Permissions.MEMBERS_BAN)
// ['messages.manage', 'members.ban']

const extended   = perms.grant(Permissions.CHANNELS_MANAGE)
const restricted = perms.deny(Permissions.MESSAGES_WRITE)
const merged     = serverPerms.merge(channelOverrides)

perms.toArray() // ['messages.read', 'messages.write']

Available constants in Permissions:

| Constant | Value | |---|---| | MESSAGES_READ | 'messages.read' | | MESSAGES_WRITE | 'messages.write' | | MESSAGES_MANAGE | 'messages.manage' | | CHANNELS_MANAGE | 'channels.manage' | | MEMBERS_KICK | 'members.kick' | | MEMBERS_BAN | 'members.ban' | | MEMBERS_ROLES | 'members.roles' | | SERVERS_MANAGE | 'servers.manage' |

CooldownManager

Per-user / per-command cooldown tracking:

const cooldowns = new CooldownManager()

client.command('daily', async (interaction) => {
  if (cooldowns.isOnCooldown(interaction.userId, 'daily', 86_400_000)) {
    const remaining = cooldowns.getRemaining(interaction.userId, 'daily', 86_400_000)
    await interaction.replyEphemeral(`Try again in ${formatDuration(remaining)}.`)
    return
  }
  cooldowns.set(interaction.userId, 'daily')
  await interaction.reply('Here is your daily reward!')
})

Logger

Coloured, namespaced console logger:

const log = new Logger('ModBot')

log.info('Bot started')
log.success('Command registered: ban')
log.warn('Rate limit approaching')
log.error('Failed to ban user', err)
log.debug('Payload', interaction.toJSON())

Time utilities

import {
  sleep, withTimeout,
  formatDuration, formatRelative,
  parseTimestamp, countdown,
} from 'novaapp-sdk'

await sleep(2_000)                          // wait 2 s
const data = await withTimeout(fetch(), 5_000) // throw if > 5 s

formatDuration(90_000)          // '1m 30s'
formatDuration(3_661_000)       // '1h 1m 1s'

formatRelative(Date.now() - 4_000)       // 'just now'
formatRelative(Date.now() - 90_000)      // '1 minute ago'
formatRelative(Date.now() + 60_000)      // 'in 1 minute'
formatRelative(Date.now() - 86_400_000)  // 'yesterday'

parseTimestamp('2026-01-01T00:00:00Z')   // Date
parseTimestamp(null)                     // null

const { days, hours, minutes, seconds } = countdown(new Date('2027-01-01'))

Events

// Connection
client.on('ready',      (bot)    => console.log('Ready!', bot.botUser.username))
client.on('disconnect', (reason) => console.log('Disconnected:', reason))
client.on('error',      (err)    => console.error(err))

// Interactions
client.on('interactionCreate', (interaction) => { /* NovaInteraction */ })

// Messages
client.on('messageCreate', (msg)  => { /* NovaMessage */ })
client.on('messageUpdate', (msg)  => { /* NovaMessage */ })
client.on('messageDelete', (data) => { /* { id, channelId } */ })
client.on('messagePinned', (data) => { /* { messageId, channelId, pinnedBy } */ })

// Reactions
client.on('reactionAdd',    (data) => { /* { messageId, channelId, userId, emoji } */ })
client.on('reactionRemove', (data) => { /* { messageId, channelId, userId, emoji } */ })

// Members
client.on('memberAdd',      (data) => { /* { serverId, userId, username } */ })
client.on('memberRemove',   (data) => { /* { serverId, userId } */ })
client.on('memberUpdate',   (data) => { /* { serverId, userId } */ })
client.on('memberBanned',   (data) => { /* { userId, serverId, moderatorId, reason } */ })
client.on('memberUnbanned', (data) => { /* { userId, serverId } */ })
client.on('typingStart',    (data) => { /* { channelId, userId } */ })

// Channels
client.on('channelCreate', (channel) => { /* Channel */ })
client.on('channelUpdate', (channel) => { /* Channel */ })
client.on('channelDelete', (data)    => { /* { id, serverId } */ })

// Roles
client.on('roleCreate', (data) => { /* { id, name, color, serverId, position, hoist, createdAt } */ })
client.on('roleDelete', (data) => { /* { id, serverId } */ })

// Voice
client.on('voiceJoin',  (data) => { /* { userId, channelId, serverId } */ })
client.on('voiceLeave', (data) => { /* { userId, channelId, serverId } */ })

// Raw event stream
client.on('event', (event) => {
  console.log(event.type, event.data, event.timestamp)
})

Raw event types

| event.type | Fires when | |---|---| | message.created | A message is sent | | message.edited | A message is edited | | message.deleted | A message is deleted | | message.reaction_added | A reaction is added | | message.reaction_removed | A reaction is removed | | message.pinned | A message is pinned | | user.joined_server | A user joins a server | | user.left_server | A user leaves a server | | user.updated_profile | A user updates their profile | | user.banned | A user is banned | | user.unbanned | A user is unbanned | | user.role_added | A user receives a role | | user.role_removed | A role is removed from a user | | user.started_typing | A user starts typing | | user.voice_joined | A user joins a voice channel | | user.voice_left | A user leaves a voice channel | | interaction.slash_command | A slash command is used | | interaction.button_click | A button is clicked | | interaction.select_menu | A select menu is used | | interaction.modal_submit | A modal is submitted | | interaction.autocomplete | An autocomplete request fires | | server.updated | Server settings change | | channel.created | A channel is created | | channel.deleted | A channel is deleted | | channel.updated | A channel is updated | | role.created | A role is created | | role.deleted | A role is deleted |


WebSocket helpers

// Send a message via WebSocket (lower latency than HTTP)
client.wsSend(channelId, 'Hello!')

// Typing indicators
client.wsTypingStart(channelId)
client.wsTypingStop(channelId)

Examples

Moderation bot

import { NovaClient, EmbedBuilder, CooldownManager, Logger } from 'novaapp-sdk'

const client    = new NovaClient({ token: process.env.NOVA_BOT_TOKEN! })
const log       = new Logger('ModBot')
const cooldowns = new CooldownManager()

client.on('ready', async (bot) => {
  log.success(`Logged in as ${bot.botUser.username}`)

  await client.commands.setSlash([
    {
      name: 'ban',
      description: 'Ban a user from the server',
      options: [
        { name: 'user',   type: 'USER',   description: 'User to ban',    required: true  },
        { name: 'reason', type: 'STRING', description: 'Reason for ban', required: false },
      ],
    },
  ])
})

client.command('ban', async (interaction) => {
  if (cooldowns.isOnCooldown(interaction.userId, 'ban', 3_000)) {
    await interaction.replyEphemeral('Slow down!')
    return
  }
  cooldowns.set(interaction.userId, 'ban')

  const userId = interaction.options.getUser('user', true)
  const reason = interaction.options.getString('reason') ?? 'No reason provided'

  try {
    await client.members.ban(interaction.serverId!, userId, reason)
    await interaction.reply({
      embed: new EmbedBuilder()
        .setTitle('🔨 User banned')
        .setDescription(`<@${userId}> was banned.\n**Reason:** ${reason}`)
        .setColor('#FF4444')
        .setTimestamp()
        .toJSON(),
    })
  } catch (err) {
    log.error('Ban failed', err)
    await interaction.replyEphemeral(`❌ Failed: ${(err as Error).message}`)
  }
})

client.on('memberBanned', ({ userId, serverId, reason }) => {
  log.warn(`${userId} banned from ${serverId} — ${reason ?? 'no reason'}`)
})

client.on('error', (err) => log.error('Gateway error', err))
await client.connect()

Multi-step modal form

client.command('report', async (interaction) => {
  const submitted = await interaction.openModal(
    new ModalBuilder()
      .setTitle('Submit a report')
      .setCustomId('report_modal')
      .addField(
        new TextInputBuilder().setCustomId('subject').setLabel('Subject').setStyle('short').setRequired(true)
      )
      .addField(
        new TextInputBuilder().setCustomId('details').setLabel('Details').setStyle('paragraph').setRequired(true)
      )
  )

  if (!submitted) { await interaction.replyEphemeral('Cancelled.'); return }

  await client.messages.send(logChannelId, {
    embed: new EmbedBuilder()
      .setTitle(`📋 ${submitted.modalData['subject']}`)
      .setDescription(submitted.modalData['details'])
      .addField('From', `<@${interaction.userId}>`, true)
      .setTimestamp()
      .toJSON(),
  })

  await submitted.replyEphemeral('Your report has been submitted.')
})

Paginate all messages in a channel

import { Paginator, formatRelative } from 'novaapp-sdk'

const paginator = new Paginator(async (cursor) => {
  const messages = await client.messages.fetch(channelId, {
    limit: 100, before: cursor ?? undefined,
  })
  return { items: messages, cursor: messages.at(-1)?.id ?? null }
})

let total = 0
for await (const msg of paginator) {
  console.log(`[${formatRelative(msg.createdAt)}] ${msg.author.username}: ${msg.content}`)
  total++
}
console.log(`Scanned ${total} messages`)

Self-hosted deployments

const client = new NovaClient({
  token:   'nova_bot_...',
  baseUrl: 'https://my-nova-server.example.com',
})

Rate limits

The Nova API enforces a limit of 50 requests/second per bot. The SDK surfaces a 429 error if exceeded. Use sleep() and exponential back-off for bulk operations.


License

MIT

import { NovaClient } from 'nova-bot-sdk'

const client = new NovaClient({ token: 'nova_bot_your_token_here' })

client.on('ready', (bot) => {
  console.log(`Logged in as ${bot.botUser.username}`)
})

client.on('interactionCreate', async (interaction) => {
  if (interaction.commandName === 'ping') {
    await client.interactions.respond(interaction.id, { content: 'Pong! 🏓' })
  }
})

// Register commands once at startup
await client.connect()

await client.commands.setSlash([
  { name: 'ping', description: 'Check if the bot is alive' },
])

Configuration

| Option | Type | Default | Description | |---|---|---|---| | token | string | required | Your bot token (nova_bot_...) | | baseUrl | string | https://novachatapp.com | Override for self-hosted deployments |

API Reference

client.messages

// Send a message
await client.messages.send(channelId, { content: 'Hello!' })

// Send with embed
await client.messages.send(channelId, {
  embed: {
    title: 'Report',
    description: 'All systems operational.',
    color: 0x5865f2,
  },
})

// Send with action buttons
await client.messages.send(channelId, {
  content: 'Confirm action?',
  components: [
    { type: 'button', customId: 'confirm', label: 'Confirm', style: 'success' },
    { type: 'button', customId: 'cancel',  label: 'Cancel',  style: 'danger'  },
  ],
})

// Edit a message
await client.messages.edit(messageId, { content: 'Updated!' })

// Delete a message
await client.messages.delete(messageId)

// Fetch recent messages
const messages = await client.messages.fetch(channelId, { limit: 50 })

// Show typing indicator
await client.messages.typing(channelId)

client.commands

// Slash commands
await client.commands.setSlash([
  { name: 'ban', description: 'Ban a user', options: [
    { name: 'user', description: 'User to ban', type: 'USER', required: true },
    { name: 'reason', description: 'Reason', type: 'STRING' },
  ]},
])
const slash = await client.commands.getSlash()
await client.commands.deleteSlash('ban')

// Prefix commands  
await client.commands.setPrefix([
  { prefix: '!', name: 'help',  description: 'Show help' },
  { prefix: '!', name: 'stats', description: 'Show bot stats' },
])
const prefix = await client.commands.getPrefix()
await client.commands.deletePrefix('!', 'help')

// Context menu commands (right-click)
await client.commands.setContext([
  { name: 'Report message', target: 'MESSAGE' },
  { name: 'View profile',   target: 'USER'    },
])

client.interactions

client.on('interactionCreate', async (interaction) => {
  // Acknowledge immediately (shows loading state)
  await client.interactions.ack(interaction.id)

  // Do your work...
  const result = await doExpensiveWork()

  // Respond with result
  await client.interactions.respond(interaction.id, {
    content: result,
    ephemeral: true, // only visible to the user who triggered it
  })
})

// Poll for pending interactions (HTTP polling mode, no WebSocket)
const pending = await client.interactions.poll({ limit: 20 })

client.members

// List server members
const members = await client.members.list(serverId, { limit: 100 })

// Kick a member (cannot kick OWNER or ADMIN)
await client.members.kick(serverId, userId)

// Ban a member
await client.members.ban(serverId, userId, 'Spamming')

client.servers

// List all servers the bot is in
const servers = await client.servers.list()
console.log(`Active in ${servers.length} servers`)

Events

// Connection
client.on('ready',      (bot)          => console.log('Ready!', bot.botUser.username))
client.on('disconnect', (reason)       => console.log('Disconnected:', reason))
client.on('error',      (err)          => console.error('Error:', err.message))

// Interactions
client.on('interactionCreate', (interaction) => { /* ... */ })

// Messages (convenience events)
client.on('messageCreate', (data) => { /* ... */ })
client.on('messageUpdate', (data) => { /* ... */ })
client.on('messageDelete', (data) => { /* ... */ })
client.on('reactionAdd',   (data) => { /* ... */ })
client.on('reactionRemove',(data) => { /* ... */ })

// Members
client.on('memberAdd',    (data) => { /* ... */ })
client.on('memberRemove', (data) => { /* ... */ })
client.on('typingStart',  (data) => { /* ... */ })

// All raw events
client.on('event', (event) => {
  console.log(event.type, event.data, event.timestamp)
})

All event types

| Event type | Fired when | |---|---| | message.created | A message is sent in a server the bot is in | | message.edited | A message is edited | | message.deleted | A message is deleted | | message.reaction_added | A reaction is added | | message.reaction_removed | A reaction is removed | | message.pinned | A message is pinned | | user.joined_server | A user joins a server | | user.left_server | A user leaves a server | | user.updated_profile | A user updates their profile | | user.banned | A user is banned | | user.unbanned | A user is unbanned | | user.role_added | A user receives a role | | user.role_removed | A role is removed from a user | | user.started_typing | A user starts typing | | user.voice_joined | A user joins a voice channel | | user.voice_left | A user leaves a voice channel | | interaction.slash_command | A slash command is used | | interaction.button_click | A button is clicked | | interaction.select_menu | A select menu item is chosen | | interaction.modal_submit | A modal is submitted | | interaction.autocomplete | An autocomplete request fires | | server.updated | Server settings change | | channel.created | A channel is created | | channel.deleted | A channel is deleted | | channel.updated | A channel is updated | | role.created | A role is created | | role.deleted | A role is deleted |

WebSocket helpers

For ultra-low-latency message sending without an HTTP round-trip:

// Send via WebSocket (fire and forget)
client.wsSend(channelId, 'Hello!')

// Typing indicators via WebSocket
client.wsTypingStart(channelId)
client.wsTypingStop(channelId)

Full example — Moderation bot

import { NovaClient } from 'nova-bot-sdk'

const client = new NovaClient({ token: process.env.NOVA_BOT_TOKEN! })

client.on('ready', async (bot) => {
  console.log(`[${bot.botUser.username}] Ready!`)

  await client.commands.setSlash([
    {
      name: 'ban',
      description: 'Ban a user from the server',
      options: [
        { name: 'user',   description: 'User to ban',    type: 'USER',   required: true  },
        { name: 'reason', description: 'Reason for ban', type: 'STRING', required: false },
      ],
    },
    {
      name: 'warn',
      description: 'Warn a user',
      options: [
        { name: 'user',    description: 'User to warn',  type: 'USER',   required: true },
        { name: 'message', description: 'Warning message', type: 'STRING', required: true },
      ],
    },
  ])
})

client.on('interactionCreate', async (interaction) => {
  if (interaction.commandName === 'ban') {
    await client.interactions.ack(interaction.id)

    const data = interaction.data as { options?: Array<{ name: string; value: string }> }
    const userId = data.options?.find((o) => o.name === 'user')?.value
    const reason = data.options?.find((o) => o.name === 'reason')?.value ?? 'No reason provided'

    if (!userId || !interaction.serverId) {
      await client.interactions.respond(interaction.id, {
        content: '❌ Missing user or server context.',
        ephemeral: true,
      })
      return
    }

    try {
      await client.members.ban(interaction.serverId, userId, reason)
      await client.interactions.respond(interaction.id, {
        embed: {
          title: '🔨 User banned',
          description: `<@${userId}> was banned. Reason: ${reason}`,
          color: 0xff4444,
        },
      })
    } catch (err: unknown) {
      const message = err instanceof Error ? err.message : 'Unknown error'
      await client.interactions.respond(interaction.id, {
        content: `❌ Failed to ban: ${message}`,
        ephemeral: true,
      })
    }
  }

  if (interaction.commandName === 'warn') {
    const data = interaction.data as { options?: Array<{ name: string; value: string }> }
    const userId = data.options?.find((o) => o.name === 'user')?.value
    const message = data.options?.find((o) => o.name === 'message')?.value

    await client.interactions.respond(interaction.id, {
      embed: {
        title: '⚠️ Warning issued',
        description: `<@${userId}> — ${message}`,
        color: 0xffa500,
      },
    })
  }
})

client.on('error', (err) => {
  console.error('[Bot]', err.message)
})

client.connect().then(() => console.log('Connected to Nova gateway'))

Self-hosted deployments

If you're running your own Nova server, pass the base URL:

const client = new NovaClient({
  token: 'nova_bot_...',
  baseUrl: 'https://my-nova-server.example.com',
})

Rate limits

The Nova API enforces a limit of 50 requests/second per bot. The SDK will surface a 429 error if you exceed this. Build in exponential back-off for batch operations.

License

MIT