diff --git a/.github/workflows/publish-dev.yaml b/.github/workflows/publish-dev.yaml index 47d33e42..6c84ed1f 100644 --- a/.github/workflows/publish-dev.yaml +++ b/.github/workflows/publish-dev.yaml @@ -59,6 +59,7 @@ jobs: "@commandkit/devtools:packages/devtools" "@commandkit/cache:packages/cache" "@commandkit/analytics:packages/analytics" + "@commandkit/ai:packages/ai" ) for entry in "${PACKAGES[@]}"; do @@ -80,6 +81,7 @@ jobs: "@commandkit/devtools" "@commandkit/cache" "@commandkit/analytics" + "@commandkit/ai" ) for pkg in "${PACKAGES[@]}"; do diff --git a/apps/test-bot/commandkit.config.ts b/apps/test-bot/commandkit.config.ts index 59aaac0b..86dea81c 100644 --- a/apps/test-bot/commandkit.config.ts +++ b/apps/test-bot/commandkit.config.ts @@ -3,6 +3,7 @@ import { legacy } from '@commandkit/legacy'; import { i18n } from '@commandkit/i18n'; import { devtools } from '@commandkit/devtools'; import { cache } from '@commandkit/cache'; +import { ai } from '@commandkit/ai'; export default defineConfig({ plugins: [ @@ -10,5 +11,6 @@ export default defineConfig({ legacy({ skipBuiltInValidations: true }), devtools(), cache(), + ai(), ], }); diff --git a/apps/test-bot/package.json b/apps/test-bot/package.json index bfe54b11..95a80080 100644 --- a/apps/test-bot/package.json +++ b/apps/test-bot/package.json @@ -10,15 +10,18 @@ "node": "node" }, "dependencies": { + "@ai-sdk/google": "^1.2.19", + "@commandkit/ai": "workspace:*", "@commandkit/cache": "workspace:*", "@commandkit/devtools": "workspace:*", "@commandkit/i18n": "workspace:*", "@commandkit/legacy": "workspace:*", "commandkit": "workspace:*", "discord.js": "^14.19.1", - "dotenv": "^16.4.7" + "dotenv": "^16.4.7", + "zod": "^3.25.56" }, "devDependencies": { "tsx": "^4.7.0" } -} +} \ No newline at end of file diff --git a/apps/test-bot/src/ai.ts b/apps/test-bot/src/ai.ts new file mode 100644 index 00000000..3e6b8f1b --- /dev/null +++ b/apps/test-bot/src/ai.ts @@ -0,0 +1,19 @@ +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { configureAI } from '@commandkit/ai'; + +const google = createGoogleGenerativeAI({ + apiKey: process.env.GOOGLE_API_KEY, +}); + +const model = google.languageModel('gemini-2.0-flash'); + +configureAI({ + selectAiModel: async () => { + return { model }; + }, + messageFilter: async (message) => { + return ( + message.inGuild() && message.mentions.users.has(message.client.user.id) + ); + }, +}); diff --git a/apps/test-bot/src/app.ts b/apps/test-bot/src/app.ts index 4a30385c..8a8eb46e 100644 --- a/apps/test-bot/src/app.ts +++ b/apps/test-bot/src/app.ts @@ -1,5 +1,6 @@ import { Client } from 'discord.js'; import { Logger, commandkit } from 'commandkit'; +import './ai'; const client = new Client({ intents: [ diff --git a/apps/test-bot/src/app/commands/(leveling)/xp.ts b/apps/test-bot/src/app/commands/(leveling)/xp.ts index 75b456e4..dd50c24c 100644 --- a/apps/test-bot/src/app/commands/(leveling)/xp.ts +++ b/apps/test-bot/src/app/commands/(leveling)/xp.ts @@ -1,12 +1,22 @@ import { ChatInputCommandContext, CommandData } from 'commandkit'; -import { database } from '../../../database/store.ts'; +import { database } from '@/database/store.ts'; import { cacheTag } from '@commandkit/cache'; +import { AiConfig, AiContext } from '@commandkit/ai'; +import { z } from 'zod'; export const command: CommandData = { name: 'xp', description: 'This is an xp command.', }; +export const aiConfig: AiConfig = { + description: 'Get the XP of a user in a guild.', + parameters: z.object({ + guildId: z.string().describe('The ID of the guild.'), + userId: z.string().describe('The ID of the user.'), + }), +}; + async function getUserXP(guildId: string, userId: string) { 'use cache'; @@ -39,3 +49,26 @@ export async function chatInput({ interaction }: ChatInputCommandContext) { ], }); } + +export async function ai(ctx: AiContext) { + const message = ctx.message; + + if (!message.inGuild()) { + return { + error: 'This tool can only be used in a guild.', + }; + } + + const { guildId, userId } = ctx.params as { + guildId: string; + userId: string; + }; + + const xp = await getUserXP(guildId, userId); + + return { + userId, + guildId, + xp, + }; +} diff --git a/apps/website/docs/guide/06-plugins/official-plugins/06-ai.mdx b/apps/website/docs/guide/06-plugins/official-plugins/06-ai.mdx new file mode 100644 index 00000000..be579bb5 --- /dev/null +++ b/apps/website/docs/guide/06-plugins/official-plugins/06-ai.mdx @@ -0,0 +1,10 @@ +--- +title: AI Plugin +description: Learn how to use the AI plugin to power your bot's commands and events. +--- + +## AI Plugin + +The AI plugin allows you to execute your bot commands using large language models. This enables you to use your bot's features entirely through natural language. + +Please refer to the [AI Powered Commands](../../13-ai-powered-commands/01-introduction.mdx) guide for more details. diff --git a/apps/website/docs/guide/13-ai-powered-commands/01-introduction.mdx b/apps/website/docs/guide/13-ai-powered-commands/01-introduction.mdx new file mode 100644 index 00000000..a4e6e595 --- /dev/null +++ b/apps/website/docs/guide/13-ai-powered-commands/01-introduction.mdx @@ -0,0 +1,135 @@ +--- +title: AI Powered Commands +description: Learn how to use a large language model to power your commands. +--- + +## Introduction + +CommandKit's `@commandkit/ai` plugin allows you to execute your bot commands using large language models. This enables you to use your bot's features entirely through natural language. + +:::warning +This is an experimental feature and is subject to change. +::: + +## Installation + +```bash +npm install @commandkit/ai +``` + +## Usage + +```typescript title="commandkit.config.ts" +import { defineConfig } from 'commandkit'; +import { ai } from '@commandkit/ai'; + +export default defineConfig({ + plugins: [ai()], +}); +``` + +## Setting up the AI model + +CommandKit allows you to dynamically specify the AI model to use for your bot. + +```typescript title="src/ai.ts" +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { configureAI } from '@commandkit/ai'; + +const google = createGoogleGenerativeAI({ + apiKey: process.env.GOOGLE_API_KEY, +}); + +const model = google.languageModel('gemini-2.0-flash'); + +configureAI({ + // commandkit will call this function + // to determine which AI model to use + selectAiModel: async () => { + return { model }; + }, + messageFilter: async (message) => { + // only respond to messages in guilds that mention the bot + return ( + message.inGuild() && message.mentions.users.has(message.client.user.id) + ); + }, +}); +``` + +Now you can simply import this file in your `app.ts`, + +```ts title="app.ts" +import { Client } from 'discord.js'; +// simply import the ai file +import './ai'; + +const client = new Client({...}); + +export default client; +``` + +## Creating AI commands + +AI commands can be created by exporting a function called `ai` from your command file. You can also export `aiConfig` object along with the `ai` function to specify the parameters for the command. + +```typescript title="src/commands/balance.ts" +import { ApplicationCommandOptionType } from 'discord.js'; +import { CommandData, ChatInputCommand } from 'commandkit'; +import { AiCommand } from '@commandkit/ai'; +import { z } from 'zod'; + +export const command: CommandData = { + name: 'balance', + description: 'Get the current balance of the user.', + options: [ + { + name: 'user', + description: 'The user to get the balance for.', + type: ApplicationCommandOptionType.User, + }, + ], +}; + +export const aiConfig: AiConfig = { + parameters: z.object({ + userId: z.string().describe('The ID of the user to get the balance of'), + }), +}; + +export const chatInput: ChatInputCommand = async (ctx) => { + const { interaction } = ctx; + const user = interaction.options.getUser('user'); + const balance = await db.getBalance(user.id); + + await interaction.reply({ + content: `The balance of ${user.username} is ${balance}`, + }); +}; + +// AI will call this function to get the balance of the user +export const ai: AiCommand = async (ctx) => { + const { userId } = ctx.params; + const balance = await db.getBalance(userId); + + // return object with the balance + return { + userId, + balance, + }; +}; +``` + +Now, you can simply mention the bot in a message to get the balance of the user. Eg: + +```text +@bot what is the balance of @user? +``` + +AI can also call multiple commands in a single message. Eg: + +```text +@bot show me the basic details of the user @user and also include the balance of that user. +``` + +The above prompt will call the built-in `getUserInfo` tool and the `balance` command. diff --git a/packages/ai/README.md b/packages/ai/README.md new file mode 100644 index 00000000..53410749 --- /dev/null +++ b/packages/ai/README.md @@ -0,0 +1,19 @@ +# `@commandkit/ai` + +Supercharge your CommandKit project with AI capabilities. + +## Installation + +```bash +npm install @commandkit/ai +``` + +## Usage + +```ts +import { ai } from '@commandkit/ai'; + +export default defineConfig({ + plugins: [ai()], +}) +``` \ No newline at end of file diff --git a/packages/ai/package.json b/packages/ai/package.json new file mode 100644 index 00000000..8f522206 --- /dev/null +++ b/packages/ai/package.json @@ -0,0 +1,38 @@ +{ + "name": "@commandkit/ai", + "version": "0.1.0", + "description": "Supercharge your CommandKit bot with AI capabilities", + "files": [ + "dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "lint": "tsc --noEmit", + "build": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/underctrl-io/commandkit.git" + }, + "keywords": [ + "commandkit", + "ai" + ], + "author": "twilight ", + "license": "MIT", + "bugs": { + "url": "https://github.com/underctrl-io/commandkit/issues" + }, + "homepage": "https://github.com/underctrl-io/commandkit#readme", + "devDependencies": { + "commandkit": "workspace:*", + "discord.js": "^14.19.3", + "tsconfig": "workspace:*", + "typescript": "^5.7.3" + }, + "dependencies": { + "ai": "^4.3.16", + "zod": "^3.25.48" + } +} \ No newline at end of file diff --git a/packages/ai/src/ai-context-worker.ts b/packages/ai/src/ai-context-worker.ts new file mode 100644 index 00000000..f3b81b30 --- /dev/null +++ b/packages/ai/src/ai-context-worker.ts @@ -0,0 +1,25 @@ +import { Message } from 'discord.js'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { AiContext } from './context'; + +const worker = new AsyncLocalStorage<{ message: Message; ctx: AiContext }>(); + +export function getAiWorkerContext(): { message: Message; ctx: AiContext } { + const ctx = worker.getStore(); + + if (!ctx) { + throw new Error( + 'AI context is not available. Ensure you are using AI in a CommandKit environment.', + ); + } + + return ctx; +} + +export function runInAiWorkerContext R>( + ctx: AiContext, + message: Message, + callback: F, +): R { + return worker.run({ message, ctx }, callback); +} diff --git a/packages/ai/src/context.ts b/packages/ai/src/context.ts new file mode 100644 index 00000000..2009a1ab --- /dev/null +++ b/packages/ai/src/context.ts @@ -0,0 +1,30 @@ +import type { CommandKit } from 'commandkit'; +import { Client, Message } from 'discord.js'; + +export interface AiContextOptions< + T extends Record = Record, +> { + message: Message; + params: T; + commandkit: CommandKit; +} + +export class AiContext< + T extends Record = Record, +> { + public params!: T; + public message!: Message; + public client!: Client; + public commandkit!: CommandKit; + + public constructor(options: AiContextOptions) { + this.params = options.params; + this.message = options.message; + this.commandkit = options.commandkit; + this.client = options.commandkit.client; + } + + public setParams(params: T): void { + this.params = params; + } +} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts new file mode 100644 index 00000000..1b6b3479 --- /dev/null +++ b/packages/ai/src/index.ts @@ -0,0 +1,10 @@ +import { AiPlugin } from './plugin'; +import { AiPluginOptions } from './types'; + +export function ai(options?: AiPluginOptions) { + return new AiPlugin(options ?? {}); +} + +export * from './types'; +export * from './plugin'; +export * from './context'; diff --git a/packages/ai/src/plugin.ts b/packages/ai/src/plugin.ts new file mode 100644 index 00000000..92c53d85 --- /dev/null +++ b/packages/ai/src/plugin.ts @@ -0,0 +1,333 @@ +import { CommandKitPluginRuntime, RuntimePlugin } from 'commandkit/plugin'; +import { AiPluginOptions, MessageFilter, SelectAiModel } from './types'; +import { LoadedCommand, Logger } from 'commandkit'; +import { AiContext } from './context'; +import { Collection, Events, Message, TextChannel } from 'discord.js'; +import { tool, Tool, generateText } from 'ai'; +import { z } from 'zod'; +import { getAiWorkerContext, runInAiWorkerContext } from './ai-context-worker'; + +type WithAI = T & { + data: { + ai: (ctx: AiContext) => Promise | unknown; + aiConfig: AiConfig; + } & T['data']; + tool: Tool; +}; + +export interface AiConfig { + description?: string; + parameters: any; +} + +let messageFilter: MessageFilter | null = null; +let selectAiModel: SelectAiModel | null = null; +let generateSystemPrompt: ((message: Message) => Promise) | undefined; + +export interface ConfigureAI { + messageFilter?: MessageFilter; + selectAiModel?: SelectAiModel; + systemPrompt?: (message: Message) => Promise; +} + +export function configureAI(config: ConfigureAI): void { + if (config.messageFilter) { + messageFilter = config.messageFilter; + } + + if (config.selectAiModel) { + selectAiModel = config.selectAiModel; + } + + if (config.systemPrompt) { + generateSystemPrompt = config.systemPrompt; + } +} + +export class AiPlugin extends RuntimePlugin { + public readonly name = 'AiPlugin'; + private toolsRecord: Record = {}; + private defaultTools: Record = {}; + private onMessageFunc: ((message: Message) => Promise) | null = null; + + public constructor(options: AiPluginOptions) { + super(options); + } + + public async activate(ctx: CommandKitPluginRuntime): Promise { + this.onMessageFunc = (message) => this.handleMessage(ctx, message); + ctx.commandkit.client.on(Events.MessageCreate, this.onMessageFunc); + + this.createDefaultTools(ctx); + + Logger.info(`Plugin ${this.name} activated`); + } + + private createDefaultTools(ctx: CommandKitPluginRuntime): void { + const { commandkit } = ctx; + const client = commandkit.client; + + this.defaultTools.getUserById = tool({ + description: 'Get user information by ID', + parameters: z.object({ + userId: z + .string() + .describe( + 'The ID of the user to retrieve. This is a Discord snowflake string.', + ), + }), + execute: async (params) => { + const user = await client.users.fetch(params.userId, { + force: false, + cache: true, + }); + + return user.toJSON(); + }, + }); + + this.defaultTools.getChannelById = tool({ + description: 'Get channel information by ID', + parameters: z.object({ + channelId: z + .string() + .describe( + 'The ID of the channel to retrieve. This is a Discord snowflake string.', + ), + }), + execute: async (params) => { + const channel = await client.channels.fetch(params.channelId, { + force: false, + cache: true, + }); + + if (!channel) { + throw new Error(`Channel with ID ${params.channelId} not found.`); + } + + return channel.toJSON(); + }, + }); + + this.defaultTools.getGuildById = tool({ + description: 'Get guild information by ID', + parameters: z.object({ + guildId: z + .string() + .describe( + 'The ID of the guild to retrieve. This is a Discord snowflake string.', + ), + }), + execute: async (params) => { + const guild = await client.guilds.fetch({ + guild: params.guildId, + force: false, + cache: true, + }); + + if (!guild) { + throw new Error(`Guild with ID ${params.guildId} not found.`); + } + + return { + id: guild.id, + name: guild.name, + icon: guild.iconURL(), + memberCount: guild.memberCount, + }; + }, + }); + + this.defaultTools.getCurrentUser = tool({ + description: 'Get information about the current discord bot user', + parameters: z.object({}), + execute: async () => { + const user = client.user; + + if (!user) { + throw new Error('Bot user is not available.'); + } + + return user.toJSON(); + }, + }); + + this.defaultTools.getAvailableCommands = tool({ + description: 'Get all available commands', + parameters: z.object({}), + execute: async () => { + return ctx.commandkit.commandHandler.getCommandsArray().map((cmd) => ({ + name: cmd.data.command.name, + description: cmd.data.command.description, + category: cmd.command.category, + })); + }, + }); + } + + public async deactivate(ctx: CommandKitPluginRuntime): Promise { + this.toolsRecord = {}; + if (this.onMessageFunc) { + ctx.commandkit.client.off(Events.MessageCreate, this.onMessageFunc); + this.onMessageFunc = null; + } + Logger.info(`Plugin ${this.name} deactivated`); + } + + private async handleMessage( + pluginContext: CommandKitPluginRuntime, + message: Message, + ): Promise { + if (message.author.bot || !Object.keys(this.toolsRecord).length) return; + + const aiModelSelector = selectAiModel; + if (!message.content?.length || !aiModelSelector) return; + + if (!message.channel.isTextBased() || !message.channel.isSendable()) return; + + const shouldContinue = messageFilter ? await messageFilter(message) : true; + if (!shouldContinue) return; + + const ctx = new AiContext({ + message, + params: {}, + commandkit: pluginContext.commandkit, + }); + + const systemPrompt = + (await generateSystemPrompt?.(message)) || + `You are a helpful AI discord bot. Your name is ${message.client.user.username} and your id is ${message.client.user.id}. + You are designed to assist users with their questions and tasks. You also have access to various tools that can help you perform tasks. + Tools are basically like commands that you can execute to perform specific actions based on user input. + Keep the response short and concise, and only use tools when necessary. Keep the response length under 2000 characters. + Do not include your own text in the response unless necessary. For text formatting, you can use discord's markdown syntax. + ${message.inGuild() ? `\nYou are currently in a guild named ${message.guild.name} whose id is ${message.guildId}. While in guild, you can fetch member information if needed.` : '\nYou are currently in a direct message with the user.'}`; + + const userInfo = ` + ${message.author.id} + ${message.author.username} + ${message.author.displayName} + ${message.author.avatarURL()} + `; + + await runInAiWorkerContext(ctx, message, async () => { + const channel = message.channel as TextChannel; + const stopTyping = await this.startTyping(channel); + + try { + const { model, options } = await aiModelSelector(message); + const result = await generateText({ + abortSignal: AbortSignal.timeout(60_000), + model, + tools: { ...this.toolsRecord, ...this.defaultTools }, + prompt: `${userInfo}\nUser: ${message.content}\nAI:`, + system: systemPrompt, + maxSteps: 5, + providerOptions: options, + }); + + stopTyping(); + + if (!!result.text) { + await message.reply({ + content: result.text, + allowedMentions: { parse: [] }, + }); + } + } catch (e) { + Logger.error(`Error processing AI message: ${e}`); + const channel = message.channel as TextChannel; + + if (channel.isSendable()) { + await message + .reply({ + content: 'An error occurred while processing your request.', + allowedMentions: { parse: [] }, + }) + .catch((e) => Logger.error(`Failed to send error message: ${e}`)); + } + } finally { + stopTyping(); + } + }); + } + + private async startTyping(channel: TextChannel): Promise<() => void> { + let stopped = false; + + const runner = async () => { + if (stopped) return clearInterval(typingInterval); + + if (channel.isSendable()) { + await channel.sendTyping().catch(Object); + } + }; + + const typingInterval = setInterval(runner, 3000).unref(); + + await runner(); + + return () => { + stopped = true; + clearInterval(typingInterval); + }; + } + + public async onBeforeCommandsLoad( + ctx: CommandKitPluginRuntime, + ): Promise { + this.toolsRecord = {}; + } + + async onAfterCommandsLoad(ctx: CommandKitPluginRuntime): Promise { + const commands = ctx.commandkit.commandHandler + .getCommandsArray() + .filter( + (command) => + 'ai' in command.data && + typeof command.data.ai === 'function' && + 'aiConfig' in command.data, + ); + + if (!commands.length) { + Logger.warn( + 'No commands with AI functionality found. Ensure commands are properly configured.', + ); + return; + } + + const tools = new Collection>(); + + for (const command of commands) { + const cmd = command as WithAI; + if (!cmd.data.ai || !cmd.data.aiConfig) { + continue; + } + + const description = + cmd.data.aiConfig.description || cmd.data.command.description; + + const cmdTool = tool({ + description, + type: 'function', + parameters: cmd.data.aiConfig.parameters, + async execute(params) { + const ctx = getAiWorkerContext(); + ctx.ctx.setParams(params); + + return cmd.data.ai(ctx.ctx); + }, + }); + + cmd.tool = cmdTool; + + tools.set(cmd.command.name, cmd); + } + + this.toolsRecord = Object.fromEntries( + tools.map((toolCmd) => { + return [toolCmd.command.name, toolCmd.tool]; + }), + ); + } +} diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts new file mode 100644 index 00000000..be09631b --- /dev/null +++ b/packages/ai/src/types.ts @@ -0,0 +1,18 @@ +import { LanguageModelV1, ProviderMetadata } from 'ai'; +import { Message } from 'discord.js'; +import { AiContext } from './context'; + +export type CommandFilterFunction = (commandName: string) => boolean; + +export type MessageFilter = (message: Message) => Promise; + +export type SelectAiModel = (message: Message) => Promise<{ + model: LanguageModelV1; + options?: ProviderMetadata; +}>; + +export interface AiPluginOptions {} + +export type AiCommand> = ( + ctx: AiContext, +) => Promise | unknown; diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json new file mode 100644 index 00000000..95b7159f --- /dev/null +++ b/packages/ai/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "declaration": true, + "inlineSourceMap": true, + "target": "ES2020", + "module": "CommonJS", + "noEmit": false + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/commandkit/src/app/handlers/AppCommandHandler.ts b/packages/commandkit/src/app/handlers/AppCommandHandler.ts index e7b5c0cf..42a3932f 100644 --- a/packages/commandkit/src/app/handlers/AppCommandHandler.ts +++ b/packages/commandkit/src/app/handlers/AppCommandHandler.ts @@ -25,7 +25,7 @@ import colors from '../../utils/colors'; export type RunCommand = (fn: T) => T; -interface AppCommand { +interface AppCommandNative { command: SlashCommandBuilder | Record; chatInput?: (ctx: Context) => Awaitable; autocomplete?: (ctx: Context) => Awaitable; @@ -34,6 +34,8 @@ interface AppCommand { userContextMenu?: (ctx: Context) => Awaitable; } +type AppCommand = AppCommandNative & Record; + interface AppCommandMiddleware { beforeExecute: (ctx: Context) => Awaitable; afterExecute: (ctx: Context) => Awaitable; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6249c38b..c7f2243e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,12 @@ importers: apps/test-bot: dependencies: + '@ai-sdk/google': + specifier: ^1.2.19 + version: 1.2.19(zod@3.25.56) + '@commandkit/ai': + specifier: workspace:* + version: link:../../packages/ai '@commandkit/cache': specifier: workspace:* version: link:../../packages/cache @@ -68,6 +74,9 @@ importers: dotenv: specifier: ^16.4.7 version: 16.5.0 + zod: + specifier: ^3.25.56 + version: 3.25.56 devDependencies: tsx: specifier: ^4.7.0 @@ -134,6 +143,28 @@ importers: specifier: ~5.8.0 version: 5.8.3 + packages/ai: + dependencies: + ai: + specifier: ^4.3.16 + version: 4.3.16(react@19.1.0)(zod@3.25.48) + zod: + specifier: ^3.25.48 + version: 3.25.48 + devDependencies: + commandkit: + specifier: workspace:* + version: link:../commandkit + discord.js: + specifier: ^14.19.3 + version: 14.19.3 + tsconfig: + specifier: workspace:* + version: link:../tsconfig + typescript: + specifier: ^5.7.3 + version: 5.8.3 + packages/analytics: devDependencies: '@umami/node': @@ -556,6 +587,38 @@ importers: packages: + '@ai-sdk/google@1.2.19': + resolution: {integrity: sha512-Xgl6eftIRQ4srUdCzxM112JuewVMij5q4JLcNmHcB68Bxn9dpr3MVUSPlJwmameuiQuISIA8lMB+iRiRbFsaqA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/provider-utils@2.2.8': + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider@1.1.3': + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + + '@ai-sdk/react@1.2.12': + resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/ui-utils@1.2.11': + resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + '@algolia/autocomplete-core@1.17.9': resolution: {integrity: sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==} @@ -2354,6 +2417,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@oxc-project/runtime@0.72.2': resolution: {integrity: sha512-J2lsPDen2mFs3cOA1gIBd0wsHEhum2vTnuKIRwmj3HJJcIz/XgeNdzvgSOioIXOJgURIpcDaK05jwaDG1rhDwg==} engines: {node: '>=6.9.0'} @@ -3613,6 +3680,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/diff-match-patch@1.0.36': + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -3953,6 +4023,16 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ai@4.3.16: + resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + react: + optional: true + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -4818,6 +4898,9 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + diff@7.0.0: resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} engines: {node: '>=0.3.1'} @@ -5883,6 +5966,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -5891,6 +5977,11 @@ packages: engines: {node: '>=6'} hasBin: true + jsondiffpatch@0.6.0: + resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -7685,6 +7776,9 @@ packages: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + select-hose@2.0.0: resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} @@ -7974,6 +8068,11 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + swr@2.3.3: + resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + tailwind-merge@3.3.0: resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} @@ -8027,6 +8126,10 @@ packages: peerDependencies: tslib: ^2 + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} @@ -8677,9 +8780,17 @@ packages: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + peerDependencies: + zod: ^3.24.1 + zod@3.25.48: resolution: {integrity: sha512-0X1mz8FtgEIvaxGjdIImYpZEaZMrund9pGXm3M6vM7Reba0e2eI71KPjSCGXBfwKDPwPoywf6waUKc3/tFvX2Q==} + zod@3.25.56: + resolution: {integrity: sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==} + zustand@5.0.5: resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==} engines: {node: '>=12.20.0'} @@ -8703,6 +8814,47 @@ packages: snapshots: + '@ai-sdk/google@1.2.19(zod@3.25.56)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.56) + zod: 3.25.56 + + '@ai-sdk/provider-utils@2.2.8(zod@3.25.48)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 3.25.48 + + '@ai-sdk/provider-utils@2.2.8(zod@3.25.56)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 3.25.56 + + '@ai-sdk/provider@1.1.3': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@1.2.12(react@19.1.0)(zod@3.25.48)': + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.48) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.48) + react: 19.1.0 + swr: 2.3.3(react@19.1.0) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.25.48 + + '@ai-sdk/ui-utils@1.2.11(zod@3.25.48)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.48) + zod: 3.25.48 + zod-to-json-schema: 3.24.5(zod@3.25.48) + '@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.19.0)(algoliasearch@5.19.0)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-plugin-algolia-insights': 1.17.9(@algolia/client-search@5.19.0)(algoliasearch@5.19.0)(search-insights@2.17.3) @@ -11807,6 +11959,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.18.0 + '@opentelemetry/api@1.9.0': {} + '@oxc-project/runtime@0.72.2': {} '@oxc-project/types@0.72.2': {} @@ -12955,6 +13109,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/diff-match-patch@1.0.36': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -13398,6 +13554,18 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ai@4.3.16(react@19.1.0)(zod@3.25.48): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.48) + '@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.25.48) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.48) + '@opentelemetry/api': 1.9.0 + jsondiffpatch: 0.6.0 + zod: 3.25.48 + optionalDependencies: + react: 19.1.0 + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -14279,6 +14447,8 @@ snapshots: didyoumean@1.2.2: {} + diff-match-patch@1.0.5: {} + diff@7.0.0: {} diff@8.0.2: {} @@ -15516,10 +15686,18 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} + jsondiffpatch@0.6.0: + dependencies: + '@types/diff-match-patch': 1.0.36 + chalk: 5.4.1 + diff-match-patch: 1.0.5 + jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -17791,6 +17969,8 @@ snapshots: extend-shallow: 2.0.1 kind-of: 6.0.3 + secure-json-parse@2.7.0: {} + select-hose@2.0.0: {} selfsigned@2.4.1: @@ -18139,6 +18319,12 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 + swr@2.3.3(react@19.1.0): + dependencies: + dequal: 2.0.3 + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) + tailwind-merge@3.3.0: {} tailwindcss@3.4.17: @@ -18209,6 +18395,8 @@ snapshots: dependencies: tslib: 2.8.1 + throttleit@2.1.0: {} + thunky@1.1.0: {} tiny-invariant@1.3.3: {} @@ -18942,8 +19130,14 @@ snapshots: yocto-queue@1.1.1: {} + zod-to-json-schema@3.24.5(zod@3.25.48): + dependencies: + zod: 3.25.48 + zod@3.25.48: {} + zod@3.25.56: {} + zustand@5.0.5(@types/react@19.1.6)(immer@9.0.21)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): optionalDependencies: '@types/react': 19.1.6