Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/publish-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -80,6 +81,7 @@ jobs:
"@commandkit/devtools"
"@commandkit/cache"
"@commandkit/analytics"
"@commandkit/ai"
)

for pkg in "${PACKAGES[@]}"; do
Expand Down
2 changes: 2 additions & 0 deletions apps/test-bot/commandkit.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ 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: [
i18n(),
legacy({ skipBuiltInValidations: true }),
devtools(),
cache(),
ai(),
],
});
7 changes: 5 additions & 2 deletions apps/test-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
19 changes: 19 additions & 0 deletions apps/test-bot/src/ai.ts
Original file line number Diff line number Diff line change
@@ -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)
);
},
});
1 change: 1 addition & 0 deletions apps/test-bot/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Client } from 'discord.js';
import { Logger, commandkit } from 'commandkit';
import './ai';

const client = new Client({
intents: [
Expand Down
35 changes: 34 additions & 1 deletion apps/test-bot/src/app/commands/(leveling)/xp.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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,
};
}
10 changes: 10 additions & 0 deletions apps/website/docs/guide/06-plugins/official-plugins/06-ai.mdx
Original file line number Diff line number Diff line change
@@ -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.
135 changes: 135 additions & 0 deletions apps/website/docs/guide/13-ai-powered-commands/01-introduction.mdx
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions packages/ai/README.md
Original file line number Diff line number Diff line change
@@ -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()],
})
```
38 changes: 38 additions & 0 deletions packages/ai/package.json
Original file line number Diff line number Diff line change
@@ -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 <hello@twlite.dev>",
"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"
}
}
25 changes: 25 additions & 0 deletions packages/ai/src/ai-context-worker.ts
Original file line number Diff line number Diff line change
@@ -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, F extends (...args: any[]) => R>(
ctx: AiContext,
message: Message,
callback: F,
): R {
return worker.run({ message, ctx }, callback);
}
30 changes: 30 additions & 0 deletions packages/ai/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { CommandKit } from 'commandkit';
import { Client, Message } from 'discord.js';

export interface AiContextOptions<
T extends Record<string, unknown> = Record<string, unknown>,
> {
message: Message;
params: T;
commandkit: CommandKit;
}

export class AiContext<
T extends Record<string, unknown> = Record<string, unknown>,
> {
public params!: T;
public message!: Message;
public client!: Client;
public commandkit!: CommandKit;

public constructor(options: AiContextOptions<T>) {
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;
}
}
Loading
Loading