feat: Add intelligent auto-router and enhanced integrations

- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
admin
2026-01-28 00:27:56 +04:00
Unverified
parent 3b128ba3bd
commit b52318eeae
1724 changed files with 351216 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
# Discord Bot Token
# Get this from Discord Developer Portal: https://discord.com/developers/applications
DISCORD_BOT_TOKEN=your_discord_bot_token_here
# LLM API Key
# Required: Set this to your OpenAI API key
# Get one at: https://platform.openai.com/account/api-keys
OPENAI_API_KEY=your_openai_api_key_here
# Alternative LLM providers (uncomment one and use it in agent-config.yml)
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
# GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
# Rate limiting settings (optional)
# Enable/disable rate limiting per user (default: true)
DISCORD_RATE_LIMIT_ENABLED=true
# Cooldown in seconds between messages from the same user (default: 5)
DISCORD_RATE_LIMIT_SECONDS=5

View File

@@ -0,0 +1,249 @@
# Discord Bot Example
This is a **reference implementation** showing how to integrate DextoAgent with Discord using discord.js. It demonstrates:
- Connecting to Discord's WebSocket API
- Processing messages and commands
- Handling image attachments
- Managing per-user conversation sessions
- Integrating tool calls with Discord messages
## ⚠️ Important: This is a Reference Implementation
This example is provided to show how to build Discord integrations with Dexto. While it works, it's not a production-ready bot and may lack:
- Advanced error recovery and retry logic
- Comprehensive logging and monitoring
- Scalability features for large deployments
- Advanced permission management
Use this as a foundation to build your own customized Discord bot!
## Quick Start
### 1. Get Your Discord Bot Token
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
2. Click "New Application" and give it a name
3. In the sidebar, navigate to **Bot** → Click **Add Bot**
4. Under the TOKEN section, click **Copy** (or **Reset Token** if you need a new one)
5. Save this token - you'll need it in the next step
### 2. Set Up Your Environment Variables
Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
Edit `.env` and add:
1. **Your Discord Bot Token** (required):
```
DISCORD_BOT_TOKEN=your_token_here
```
2. **Your LLM API Key** (required):
For OpenAI (default):
```
OPENAI_API_KEY=your_openai_api_key_here
```
Or use a different provider and update `agent-config.yml`:
```
# ANTHROPIC_API_KEY=your_key_here
# GOOGLE_GENERATIVE_AI_API_KEY=your_key_here
```
**Get API Keys:**
- **OpenAI**: https://platform.openai.com/account/api-keys
- **Anthropic**: https://console.anthropic.com/account/keys
- **Google**: https://ai.google.dev
### 3. Invite Bot to Your Server
1. In Developer Portal, go to **OAuth2** → **URL Generator**
2. Select scopes: `bot`
3. Select permissions: `Send Messages`, `Read Messages`, `Read Message History`, `Attach Files`
4. Copy the generated URL and visit it to invite your bot to your server
### 4. Install Dependencies
Install the required dependencies:
```bash
pnpm install
```
### 5. Run the Bot
Start the bot:
```bash
pnpm start
```
You should see:
```
🚀 Initializing Discord bot...
Discord bot logged in as YourBotName#1234
✅ Discord bot is running!
```
## Usage
### In DMs
Simply send a message to the bot - it will respond using the configured LLM.
### In Server Channels
Use the `!ask` prefix:
```
!ask What is the capital of France?
```
The bot will respond with the agent's response, splitting long messages to respect Discord's 2000-character limit.
### Image Support
Send an image attachment with or without text, and the bot will process it using the agent's vision capabilities.
### Audio Support
Send audio files (MP3, WAV, OGG, etc.), and the bot will:
- Transcribe the audio (if model supports speech recognition)
- Analyze the audio content
- Use audio as context for responses
Simply attach an audio file to your message and the bot will process it using the agent's multimodal capabilities.
### Reset Conversation
To start a fresh conversation session, DM the bot with:
```
/reset
```
## Configuration
### Switching LLM Providers
The bot comes configured with OpenAI by default. To use a different provider:
1. **Update `agent-config.yml`** - Change the `llm` section:
```yaml
# For Anthropic Claude:
llm:
provider: anthropic
model: claude-sonnet-4-5-20250929
apiKey: $ANTHROPIC_API_KEY
# For Google Gemini:
llm:
provider: google
model: gemini-2.0-flash
apiKey: $GOOGLE_GENERATIVE_AI_API_KEY
```
2. **Set the API key in `.env`**:
```
ANTHROPIC_API_KEY=your_key_here
# or
GOOGLE_GENERATIVE_AI_API_KEY=your_key_here
```
### Environment Variables
Create a `.env` file with:
- **`DISCORD_BOT_TOKEN`** (Required): Your bot's authentication token
- **`OPENAI_API_KEY`** (Required for OpenAI): Your OpenAI API key
- **`ANTHROPIC_API_KEY`** (Optional): For using Claude models
- **`GOOGLE_GENERATIVE_AI_API_KEY`** (Optional): For using Gemini models
- **`DISCORD_RATE_LIMIT_ENABLED`** (Optional): Enable/disable rate limiting (default: true)
- **`DISCORD_RATE_LIMIT_SECONDS`** (Optional): Cooldown between messages per user (default: 5)
## Features
### Rate Limiting
By default, the bot enforces a 5-second cooldown per user to prevent spam. Adjust or disable via environment variables.
### Tool Notifications
When the LLM calls a tool (e.g., making an API call), the bot sends a notification message so users can see what's happening:
```
🔧 Calling tool get_weather with args: {...}
```
### Session Management
Each Discord user gets their own persistent conversation session during the bot's lifetime. Messages from different users don't interfere with each other.
### Large Responses
Responses longer than Discord's 2000-character limit are automatically split into multiple messages.
## Limitations
- **No persistence across restarts**: Sessions are lost when the bot restarts. For persistent sessions, implement a database layer.
- **Simple message handling**: Only responds to text and images. Doesn't support all Discord features like reactions, threads, etc.
- **Per-deployment limits**: The bot runs as a single instance. For horizontal scaling, implement clustering.
## Architecture
```
Discord Message
startDiscordBot() wires up event handlers
agent.generate() processes the message
Response sent back to Discord
agentEventBus emits events (tool calls, etc.)
Tool notifications sent to channel
```
## Troubleshooting
### Bot doesn't respond to messages
- Check that the bot has permission to send messages in the channel
- Ensure `DISCORD_BOT_TOKEN` is correct in `.env`
- Verify bot has `Message Content Intent` enabled in Developer Portal
### "DISCORD_BOT_TOKEN is not set"
- Check that `.env` file exists in the example directory
- Verify the token is correctly copied from Developer Portal
### Rate limiting errors
- Check `DISCORD_RATE_LIMIT_SECONDS` setting
- Set `DISCORD_RATE_LIMIT_ENABLED=false` to disable rate limiting
### Image processing fails
- Ensure attachments are under 5MB
- Check network connectivity for downloading attachments
## Next Steps
To customize this bot:
1. **Modify `agent-config.yml`**:
- Change the LLM provider/model
- Add MCP servers for additional capabilities
- Customize the system prompt
2. **Extend `bot.ts`**:
- Add new command handlers
- Implement additional Discord features
- Add logging/monitoring
3. **Deploy**:
- Run on a server/VPS that stays online 24/7
- Use process managers like PM2 to auto-restart on crashes
- Consider hosting on platforms like Railway, Heroku, or AWS
## Documentation
- [Discord.js Documentation](https://discord.js.org/)
- [Discord Developer Portal](https://discord.com/developers/applications)
- [Dexto Documentation](https://dexto.dev)
- [Dexto Agent API](https://docs.dexto.dev)
## License
MIT

View File

@@ -0,0 +1,78 @@
# Discord Bot Agent Configuration
# This agent is optimized for Discord bot interactions.
# For more configuration options, see: https://docs.dexto.dev/guides/configuring-dexto
# LLM Configuration
# The bot uses this LLM provider and model for all interactions
llm:
provider: openai
model: gpt-5-mini
apiKey: $OPENAI_API_KEY
# System Prompt
# Defines the bot's personality, capabilities, and behavior
# This prompt is customized for Discord-specific constraints (2000 char message limit)
systemPrompt: |
You are a helpful and friendly Discord bot powered by Dexto. You assist users with a wide range of tasks
including answering questions, providing information, and helping with coding, writing, and analysis.
## Your Capabilities
- File System Access: Read and explore files in your working directory
- Web Browsing: Visit websites and extract information (when configured)
- Code Analysis: Help with programming, debugging, and code review
- Information Retrieval: Answer questions and provide explanations
- Creative Tasks: Writing, brainstorming, and content generation
## Response Guidelines
- Keep responses concise and Discord-friendly (under 2000 characters where possible)
- Use plain text formatting (avoid markdown syntax like ###, ---, etc.)
- For code, use simple indentation without backtick formatting
- For emphasis, use CAPS or simple punctuation like asterisks *like this*
- Break long responses into multiple messages if needed
- Be helpful, respectful, and inclusive
- If you can't do something, explain why clearly
## Usage in Discord
- Direct Messages: Respond to all messages sent to you
- Server Channels: Respond to messages using the !ask prefix (e.g., !ask what is TypeScript?)
- Attachments: You can process images attached to messages
Always aim to be helpful, accurate, and concise in your responses using plain text format.
# MCP Servers Configuration
# These provide the tools and capabilities your bot can use
mcpServers:
# Add exa server for web search capabilities
exa:
type: http
url: https://mcp.exa.ai/mcp
# Tool Confirmation Configuration
# Discord bots auto-approve tool calls, so we disable confirmation
toolConfirmation:
mode: auto-approve
allowedToolsStorage: memory
# Storage Configuration
# Data storage for sessions, memories, and caching
storage:
cache:
type: in-memory
database:
type: sqlite
# Database file will be created in ~/.dexto/agents/discord-bot-agent/
blob:
type: local
maxBlobSize: 52428800 # 50MB per attachment
maxTotalSize: 1073741824 # 1GB total
cleanupAfterDays: 30
# Memory Configuration - enables the bot to remember context
memories:
enabled: true
priority: 40
limit: 10
includeTags: true
# Optional: Greeting message shown when bot starts
greeting: "Hi! I'm a Discord bot powered by Dexto. Type `!ask your question` in channels or just message me directly!"

View File

@@ -0,0 +1,319 @@
import dotenv from 'dotenv';
import { Client, GatewayIntentBits, Partials } from 'discord.js';
import https from 'https';
import http from 'http'; // ADDED for http support
import { DextoAgent, logger } from '@dexto/core';
// Load environment variables
dotenv.config();
const token = process.env.DISCORD_BOT_TOKEN;
// User-based cooldown system for Discord interactions
const userCooldowns = new Map<string, number>();
const RATE_LIMIT_ENABLED = process.env.DISCORD_RATE_LIMIT_ENABLED?.toLowerCase() !== 'false'; // default-on
let COOLDOWN_SECONDS = Number(process.env.DISCORD_RATE_LIMIT_SECONDS ?? 5);
if (Number.isNaN(COOLDOWN_SECONDS) || COOLDOWN_SECONDS < 0) {
console.error(
'DISCORD_RATE_LIMIT_SECONDS must be a non-negative number. Defaulting to 5 seconds.'
);
COOLDOWN_SECONDS = 5; // Default to a safe value
}
// Helper to detect MIME type from file extension
function getMimeTypeFromPath(filePath: string): string {
const ext = filePath.split('.').pop()?.toLowerCase() || '';
const mimeTypes: Record<string, string> = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
ogg: 'audio/ogg',
mp3: 'audio/mpeg',
wav: 'audio/wav',
m4a: 'audio/mp4',
};
return mimeTypes[ext] || 'application/octet-stream';
}
// Helper to download a file URL and convert it to base64
async function downloadFileAsBase64(
fileUrl: string,
fileName?: string
): Promise<{ base64: string; mimeType: string }> {
return new Promise((resolve, reject) => {
const protocol = fileUrl.startsWith('https:') ? https : http; // Determine protocol
const MAX_BYTES = 5 * 1024 * 1024; // 5 MB hard cap
let downloadedBytes = 0;
const req = protocol.get(fileUrl, (res) => {
// Store the request object
if (res.statusCode && res.statusCode >= 400) {
// Clean up response stream
res.resume();
return reject(
new Error(`Failed to download file: ${res.statusCode} ${res.statusMessage}`)
);
}
const chunks: Buffer[] = [];
res.on('data', (chunk) => {
downloadedBytes += chunk.length;
if (downloadedBytes > MAX_BYTES) {
// Clean up response stream before destroying request
res.resume();
req.destroy(new Error('Attachment exceeds 5 MB limit')); // Destroy the request
// No explicit reject here, as 'error' on req should handle it or timeout will occur
return;
}
chunks.push(chunk);
});
res.on('end', () => {
if (req.destroyed) return; // If request was destroyed due to size limit, do nothing
const buffer = Buffer.concat(chunks);
let contentType =
(res.headers['content-type'] as string) || 'application/octet-stream';
// If server returns generic octet-stream, try to detect from file name
if (contentType === 'application/octet-stream' && fileName) {
contentType = getMimeTypeFromPath(fileName);
}
resolve({ base64: buffer.toString('base64'), mimeType: contentType });
});
// Handle errors on the response stream itself (e.g., premature close)
res.on('error', (err) => {
if (!req.destroyed) {
// Avoid double-rejection if req.destroy() already called this
reject(err);
}
});
});
// Handle errors on the request object (e.g., socket hang up, DNS resolution error, or from req.destroy())
req.on('error', (err) => {
reject(err);
});
// Optional: Add a timeout for the request
req.setTimeout(30000, () => {
// 30 seconds timeout
if (!req.destroyed) {
req.destroy(new Error('File download timed out'));
}
});
});
}
// Insert initDiscordBot to wire up a Discord client given pre-initialized services
export function startDiscordBot(agent: DextoAgent) {
if (!token) {
throw new Error('DISCORD_BOT_TOKEN is not set');
}
const agentEventBus = agent.agentEventBus;
// Helper to get or create session for a Discord user
// Each Discord user gets their own persistent session
function getDiscordSessionId(userId: string): string {
return `discord-${userId}`;
}
// Create Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages,
],
partials: [Partials.Channel],
});
client.once('ready', () => {
console.log(`Discord bot logged in as ${client.user?.tag || 'Unknown'}`);
});
client.on('messageCreate', async (message) => {
// Ignore bots
if (message.author.bot) return;
if (RATE_LIMIT_ENABLED && COOLDOWN_SECONDS > 0) {
// Only apply cooldown if enabled and seconds > 0
const now = Date.now();
const cooldownEnd = userCooldowns.get(message.author.id) || 0;
if (now < cooldownEnd) {
const timeLeft = (cooldownEnd - now) / 1000;
try {
await message.reply(
`Please wait ${timeLeft.toFixed(1)} more seconds before using this command again.`
);
} catch (replyError) {
console.error('Error sending cooldown message:', replyError);
}
return;
}
}
let userText: string | undefined = message.content;
let imageDataInput: { image: string; mimeType: string } | undefined;
let fileDataInput: { data: string; mimeType: string; filename?: string } | undefined;
// Helper to determine if mime type is audio
const isAudioMimeType = (mimeType: string): boolean => {
return mimeType.startsWith('audio/');
};
// Handle attachments (images and audio)
if (message.attachments.size > 0) {
const attachment = message.attachments.first();
if (attachment && attachment.url) {
try {
const { base64, mimeType } = await downloadFileAsBase64(
attachment.url,
attachment.name || 'file'
);
if (isAudioMimeType(mimeType)) {
// Handle audio files
fileDataInput = {
data: base64,
mimeType,
filename: attachment.name || 'audio.wav',
};
// Add context if only audio (no text in message)
if (!userText) {
userText =
'(User sent an audio message for transcription and analysis)';
}
} else if (mimeType.startsWith('image/')) {
// Handle image files
imageDataInput = { image: base64, mimeType };
userText = message.content || '';
}
} catch (downloadError) {
console.error('Failed to download attachment:', downloadError);
try {
await message.reply(
`⚠️ Failed to download attachment: ${downloadError instanceof Error ? downloadError.message : 'Unknown error'}. Please try again or send the message without the attachment.`
);
} catch (replyError) {
console.error('Error sending attachment failure message:', replyError);
}
// Continue without the attachment - if there's text content, process that
if (!userText) {
return; // If there's no text and attachment failed, nothing to process
}
}
}
}
// Only respond to !ask prefix or DMs
if (!message.guild || (userText && userText.startsWith('!ask '))) {
if (userText && userText.startsWith('!ask ')) {
userText = userText.substring(5).trim();
}
if (!userText) return;
// Subscribe for toolCall events
const toolCallHandler = (payload: {
toolName: string;
args: unknown;
callId?: string;
sessionId: string;
}) => {
message.channel.send(`🔧 Calling tool **${payload.toolName}**`).catch((error) => {
console.error(
`Failed to send tool call notification for ${payload.toolName} to channel ${message.channel.id}:`,
error
);
});
};
agentEventBus.on('llm:tool-call', toolCallHandler);
try {
const sessionId = getDiscordSessionId(message.author.id);
await message.channel.sendTyping();
// Build content array from message and attachments
const content: import('@dexto/core').ContentPart[] = [];
if (userText) {
content.push({ type: 'text', text: userText });
}
if (imageDataInput) {
content.push({
type: 'image',
image: imageDataInput.image,
mimeType: imageDataInput.mimeType,
});
}
if (fileDataInput) {
content.push({
type: 'file',
data: fileDataInput.data,
mimeType: fileDataInput.mimeType,
filename: fileDataInput.filename,
});
}
const response = await agent.generate(content, sessionId);
const responseText = response.content;
// Handle Discord's 2000 character limit
const MAX_LENGTH = 1900; // Leave some buffer
if (responseText && responseText.length <= MAX_LENGTH) {
await message.reply(responseText);
} else if (responseText) {
// Split into chunks and send multiple messages
let remaining = responseText;
let isFirst = true;
while (remaining && remaining.length > 0) {
const chunk = remaining.substring(0, MAX_LENGTH);
remaining = remaining.substring(MAX_LENGTH);
if (isFirst) {
await message.reply(chunk);
isFirst = false;
} else {
// For subsequent chunks, use message.channel.send to avoid a chain of replies
// Adding a small delay helps with ordering and rate limits
await new Promise((resolve) => setTimeout(resolve, 250)); // 250ms delay
await message.channel.send(chunk);
}
}
} else {
await message.reply(
'🤖 I received your message but could not generate a response.'
);
}
// Log token usage if available (optional analytics)
if (response.usage) {
logger.debug(
`Session ${sessionId} - Tokens: input=${response.usage.inputTokens}, output=${response.usage.outputTokens}`
);
}
} catch (error) {
console.error('Error handling Discord message', error);
try {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
await message.reply(`❌ Error: ${errorMessage}`);
} catch (replyError) {
console.error('Error sending error reply:', replyError);
}
} finally {
agentEventBus.off('llm:tool-call', toolCallHandler);
// Set cooldown for the user after processing
if (RATE_LIMIT_ENABLED && COOLDOWN_SECONDS > 0) {
userCooldowns.set(message.author.id, Date.now() + COOLDOWN_SECONDS * 1000);
}
}
}
});
client.login(token);
return client;
}

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env node
import 'dotenv/config';
import { DextoAgent } from '@dexto/core';
import { loadAgentConfig, enrichAgentConfig } from '@dexto/agent-management';
import { startDiscordBot } from './bot.js';
async function main() {
try {
// Load agent configuration from local agent-config.yml
console.log('🚀 Initializing Discord bot...');
const configPath = './agent-config.yml';
const config = await loadAgentConfig(configPath);
const enrichedConfig = enrichAgentConfig(config, configPath);
// Create and start the Dexto agent
const agent = new DextoAgent(enrichedConfig, configPath);
await agent.start();
// Start the Discord bot
console.log('📡 Starting Discord bot connection...');
startDiscordBot(agent);
console.log('✅ Discord bot is running! Send messages or use !ask <question> prefix.');
console.log(' In DMs, just send your message without the !ask prefix.');
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\n🛑 Shutting down...');
await agent.stop();
process.exit(0);
});
} catch (error) {
console.error('❌ Failed to start Discord bot:', error);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,30 @@
{
"name": "dexto-discord-bot-example",
"version": "1.0.0",
"description": "Discord bot integration example using Dexto",
"type": "module",
"main": "dist/main.js",
"scripts": {
"start": "tsx main.ts",
"build": "tsc",
"dev": "tsx watch main.ts"
},
"keywords": [
"dexto",
"discord",
"bot",
"ai",
"agent"
],
"license": "MIT",
"dependencies": {
"@dexto/core": "^1.1.3",
"@dexto/agent-management": "^1.1.3",
"discord.js": "^14.19.3",
"dotenv": "^16.3.1"
},
"devDependencies": {
"tsx": "^4.7.1",
"typescript": "^5.5.4"
}
}

2019
dexto/examples/discord-bot/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff