this file maps out the design of kitten my goal with kitten is to write a light abstraction atop discord.js to provide a simple but powerful sort of interaction routing system. and it's fully type-safe, too. ## commands we define a command with `kitten.command`, options are typed dynamically so you get awesome typesafety in the `run` callback. ```ts const whois = kitten.command("whois", { description: "get information about a user", options: { user: option.user("the user to get information about", { required: true }), ephemeral: option.boolean("whether to hide the response"), }, async run(interaction, { user, ephemeral }) { // user: GuildMember/User // ephemeral: boolean | undefined }, }); ``` subcommands are similar. ```ts const command = kitten.command("command", { description: "a command with subcommands", }); command.subcommand("sub1", { description: "the first subcommand", options: { user: option.user("a user option", { required: true }), }, async run(interaction, { user }) { // user: GuildMember/User }, }); ``` kitten will handle registering commands and running commands when they're invoked. ## component routing buttons, modals, and select menus work similarly. ```ts const banButton = kitten.button("close-ticket", { options: { ticketId: option.string(), }, async run(interaction, { ticketId }) { // ticketId: string | undefined }, }); const button = new ButtonBuilder().setCustomId( banButton.id({ ticketId: "1234" }), ); ``` this would serialise to `close-ticket:1234` ## autocomplete you can deifne autocomplete handlers inside of optino definitions. ```ts const repoSearch = kitten.command("repo", { description: "search a github repository", options: { name: option.string("the repository name", { required: true, async autocomplete(interaction, value) { const matches = await searchRepos(value); return matches.map((r) => ({ name: r.name, value: r.id })); }, }), }, async run(interaction, { name }) {}, }); ``` ## middleware ```ts const base = kitten.builder(); const authed = base .use(async (interaction) => { const dbUser = await db.getUser(interaction.user.id); if (!dbUser) { await interaction.reply({ content: "no account found", ephemeral: true }); throw new HaltExecution(); } return { dbUser }; }) .use(async (interaction, ctx) => { // ctx.dbUser: DbUser const isPremium = ctx.dbUser.subscription === "premium"; return { isPremium }; }); const premiumStats = authed.command("meow", { description: "meow", async run(interaction, args, ctx) { // ctx.dbUser is typed // ctx.isPremium is typed }, }); ``` ## usage ```ts import { Client, GatewayIntentBits } from "discord.js"; import { Kitten } from "kitten"; const client = new Client({ intents: [GatewayIntentBits.Guilds] }); const kitten = new Kitten(client); import * as commands from "./commands"; import * as buttons from "./components"; kitten.register({ commands, components }); client.once("ready", async () => { const isDev = process.env.NODE_ENV === "development"; try { await kitten.sync({ guildId: isDev ? process.env.GUILD_ID : undefined, }); } catch (error) { console.error("failed to sync commands:", error); } }); await client.login(process.env.BOT_TOKEN); ```