diff --git a/.auto-claude-security.json b/.auto-claude-security.json new file mode 100644 index 00000000..eeb9f8f1 --- /dev/null +++ b/.auto-claude-security.json @@ -0,0 +1,172 @@ +{ + "base_commands": [ + ".", + "[", + "[[", + "ag", + "awk", + "basename", + "bash", + "bc", + "break", + "cat", + "cd", + "chmod", + "clear", + "cmp", + "column", + "comm", + "command", + "continue", + "cp", + "curl", + "cut", + "date", + "df", + "diff", + "dig", + "dirname", + "du", + "echo", + "egrep", + "env", + "eval", + "exec", + "exit", + "expand", + "export", + "expr", + "false", + "fd", + "fgrep", + "file", + "find", + "fmt", + "fold", + "gawk", + "gh", + "git", + "grep", + "gunzip", + "gzip", + "head", + "help", + "host", + "iconv", + "id", + "jobs", + "join", + "jq", + "kill", + "killall", + "less", + "let", + "ln", + "ls", + "lsof", + "man", + "mkdir", + "mktemp", + "more", + "mv", + "nl", + "paste", + "pgrep", + "ping", + "pkill", + "popd", + "printenv", + "printf", + "ps", + "pushd", + "pwd", + "read", + "readlink", + "realpath", + "reset", + "return", + "rev", + "rg", + "rm", + "rmdir", + "sed", + "seq", + "set", + "sh", + "shuf", + "sleep", + "sort", + "source", + "split", + "stat", + "tail", + "tar", + "tee", + "test", + "time", + "timeout", + "touch", + "tr", + "tree", + "true", + "type", + "uname", + "unexpand", + "uniq", + "unset", + "unzip", + "watch", + "wc", + "wget", + "whereis", + "which", + "whoami", + "xargs", + "yes", + "yq", + "zip", + "zsh" + ], + "stack_commands": [ + "node", + "npm", + "npx", + "pnpm", + "pnpx" + ], + "script_commands": [ + "bun", + "npm", + "pnpm", + "yarn" + ], + "custom_commands": [], + "detected_stack": { + "languages": [ + "javascript" + ], + "package_managers": [ + "pnpm" + ], + "frameworks": [], + "databases": [], + "infrastructure": [], + "cloud_providers": [], + "code_quality_tools": [], + "version_managers": [] + }, + "custom_scripts": { + "npm_scripts": [ + "start", + "dev" + ], + "make_targets": [], + "poetry_scripts": [], + "cargo_aliases": [], + "shell_scripts": [] + }, + "project_dir": "/Users/billchirico/Developer/bill-bot", + "created_at": "2026-02-03T19:51:09.135836", + "project_hash": "51a4f617fc8ece9b63e20f8a9950e73b", + "inherited_from": "/Users/billchirico/Developer/bill-bot" +} \ No newline at end of file diff --git a/.auto-claude-status b/.auto-claude-status new file mode 100644 index 00000000..c74f6068 --- /dev/null +++ b/.auto-claude-status @@ -0,0 +1,25 @@ +{ + "active": true, + "spec": "008-slash-commands-support", + "state": "building", + "subtasks": { + "completed": 4, + "total": 7, + "in_progress": 1, + "failed": 0 + }, + "phase": { + "current": "Slash Commands Implementation", + "id": null, + "total": 7 + }, + "workers": { + "active": 0, + "max": 1 + }, + "session": { + "number": 6, + "started_at": "2026-02-03T20:34:15.644896" + }, + "last_update": "2026-02-03T20:43:59.270346" +} \ No newline at end of file diff --git a/.claude_settings.json b/.claude_settings.json new file mode 100644 index 00000000..143a940c --- /dev/null +++ b/.claude_settings.json @@ -0,0 +1,39 @@ +{ + "sandbox": { + "enabled": true, + "autoAllowBashIfSandboxed": true + }, + "permissions": { + "defaultMode": "acceptEdits", + "allow": [ + "Read(./**)", + "Write(./**)", + "Edit(./**)", + "Glob(./**)", + "Grep(./**)", + "Read(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/008-slash-commands-support/**)", + "Write(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/008-slash-commands-support/**)", + "Edit(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/008-slash-commands-support/**)", + "Glob(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/008-slash-commands-support/**)", + "Grep(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/008-slash-commands-support/**)", + "Read(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/008-slash-commands-support/.auto-claude/specs/008-slash-commands-support/**)", + "Write(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/008-slash-commands-support/.auto-claude/specs/008-slash-commands-support/**)", + "Edit(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/008-slash-commands-support/.auto-claude/specs/008-slash-commands-support/**)", + "Read(/Users/billchirico/Developer/bill-bot/.auto-claude/**)", + "Write(/Users/billchirico/Developer/bill-bot/.auto-claude/**)", + "Edit(/Users/billchirico/Developer/bill-bot/.auto-claude/**)", + "Glob(/Users/billchirico/Developer/bill-bot/.auto-claude/**)", + "Grep(/Users/billchirico/Developer/bill-bot/.auto-claude/**)", + "Bash(*)", + "WebFetch(*)", + "WebSearch(*)", + "mcp__context7__resolve-library-id(*)", + "mcp__context7__get-library-docs(*)", + "mcp__graphiti-memory__search_nodes(*)", + "mcp__graphiti-memory__search_facts(*)", + "mcp__graphiti-memory__add_episode(*)", + "mcp__graphiti-memory__get_episodes(*)", + "mcp__graphiti-memory__get_entity_edge(*)" + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2e8157a9..e6c77976 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules/ .env *.log + +# Auto Claude data directory +.auto-claude/ diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 00000000..e8517e4e --- /dev/null +++ b/src/commands.js @@ -0,0 +1,49 @@ +/** + * Bill Bot - Slash Command Definitions + * + * Defines all slash commands for Discord interactions + */ + +import { SlashCommandBuilder } from 'discord.js'; + +/** + * Slash command definitions + * Each command is built using Discord's SlashCommandBuilder + */ +const commands = [ + // /ask - AI chat command + new SlashCommandBuilder() + .setName('ask') + .setDescription('Ask the AI a question') + .addStringOption(option => + option + .setName('question') + .setDescription('Your question for the AI') + .setRequired(true) + ), + + // /help - Show available commands + new SlashCommandBuilder() + .setName('help') + .setDescription('Show available commands and usage instructions'), + + // /clear - Reset conversation history + new SlashCommandBuilder() + .setName('clear') + .setDescription('Clear your conversation history with the bot'), + + // /status - Show bot health and stats + new SlashCommandBuilder() + .setName('status') + .setDescription('Show bot status, uptime, and health information'), +]; + +/** + * Export commands as JSON for registration with Discord API + */ +export const commandData = commands.map(command => command.toJSON()); + +/** + * Export commands array for reference + */ +export default commands; diff --git a/src/index.js b/src/index.js index 754e73db..16c63eb4 100644 --- a/src/index.js +++ b/src/index.js @@ -7,11 +7,12 @@ * - Spam/scam detection and moderation */ -import { Client, GatewayIntentBits, EmbedBuilder, ChannelType } from 'discord.js'; +import { Client, GatewayIntentBits, EmbedBuilder, ChannelType, REST, Routes } from 'discord.js'; import { config as dotenvConfig } from 'dotenv'; import { readFileSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; +import { commandData } from './commands.js'; dotenvConfig(); @@ -50,6 +51,9 @@ const client = new Client({ const conversationHistory = new Map(); const MAX_HISTORY = 20; +// Track bot start time for uptime calculation +const startTime = Date.now(); + // Spam patterns const SPAM_PATTERNS = [ /free\s*(crypto|bitcoin|btc|eth|nft)/i, @@ -148,7 +152,7 @@ You can use Discord markdown formatting.`; */ async function sendSpamAlert(message) { if (!config.moderation?.alertChannelId) return; - + const alertChannel = await client.channels.fetch(config.moderation.alertChannelId).catch(() => null); if (!alertChannel) return; @@ -164,18 +168,53 @@ async function sendSpamAlert(message) { .setTimestamp(); await alertChannel.send({ embeds: [embed] }); - + // Auto-delete if enabled if (config.moderation?.autoDelete) { await message.delete().catch(() => {}); } } +/** + * Register slash commands with Discord + */ +async function deployCommands() { + const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); + + try { + const clientId = process.env.DISCORD_CLIENT_ID; + const guildId = process.env.DISCORD_GUILD_ID; + + if (!clientId) { + throw new Error('DISCORD_CLIENT_ID not set'); + } + + if (guildId) { + // Register commands for a specific guild (faster for development) + await rest.put(Routes.applicationGuildCommands(clientId, guildId), { + body: commandData, + }); + console.log(`โœ… Commands registered for guild ${guildId}`); + } else { + // Register commands globally (takes up to 1 hour to propagate) + await rest.put(Routes.applicationCommands(clientId), { + body: commandData, + }); + console.log('โœ… Commands registered globally'); + } + } catch (err) { + console.error('Failed to register commands:', err.message); + } +} + // Bot ready -client.once('ready', () => { +client.once('ready', async () => { console.log(`โœ… ${client.user.tag} is online!`); console.log(`๐Ÿ“ก Serving ${client.guilds.cache.size} server(s)`); - + + // Register slash commands + await deployCommands(); + if (config.welcome?.enabled) { console.log(`๐Ÿ‘‹ Welcome messages โ†’ #${config.welcome.channelId}`); } @@ -262,6 +301,159 @@ client.on('messageCreate', async (message) => { } }); +// Handle slash command interactions +client.on('interactionCreate', async (interaction) => { + // Only handle slash commands + if (!interaction.isChatInputCommand()) return; + + const { commandName } = interaction; + + try { + if (commandName === 'ask') { + // Get the question from command options + const question = interaction.options.getString('question'); + + // Defer reply since AI generation might take time + await interaction.deferReply(); + + // Generate AI response + const response = await generateResponse( + interaction.channel.id, + question, + interaction.user.username + ); + + // Send response (handle long messages) + if (response.length > 2000) { + const chunks = response.match(/[\s\S]{1,1990}/g) || []; + await interaction.editReply(chunks[0]); + for (let i = 1; i < chunks.length; i++) { + await interaction.followUp(chunks[i]); + } + } else { + await interaction.editReply(response); + } + } else if (commandName === 'help') { + // Create help embed with all available commands + const embed = new EmbedBuilder() + .setColor(0x5865F2) + .setTitle('๐Ÿค– Bill Bot - Available Commands') + .setDescription('Here are all the commands you can use:') + .addFields( + { + name: '/ask', + value: 'Ask the AI a question\n**Usage:** `/ask question: What is JavaScript?`' + }, + { + name: '/help', + value: 'Show this help message\n**Usage:** `/help`' + }, + { + name: '/clear', + value: 'Clear your conversation history with the bot\n**Usage:** `/clear`' + }, + { + name: '/status', + value: 'Show bot status, uptime, and health information\n**Usage:** `/status`' + } + ) + .addFields({ + name: '๐Ÿ’ฌ AI Chat', + value: 'You can also mention me or reply to my messages to chat! I remember the last 20 messages per channel.' + }) + .setFooter({ text: 'Powered by Claude AI via OpenClaw' }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } else if (commandName === 'clear') { + // Clear conversation history for this channel + const channelId = interaction.channel.id; + + if (conversationHistory.has(channelId)) { + conversationHistory.delete(channelId); + await interaction.reply({ + content: '๐Ÿงน Conversation history cleared! Starting fresh.', + ephemeral: true + }); + } else { + await interaction.reply({ + content: 'โœจ No conversation history to clear - already fresh!', + ephemeral: true + }); + } + } else if (commandName === 'status') { + // Calculate uptime + const uptime = Date.now() - startTime; + const seconds = Math.floor(uptime / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + let uptimeStr = ''; + if (days > 0) uptimeStr += `${days}d `; + if (hours % 24 > 0) uptimeStr += `${hours % 24}h `; + if (minutes % 60 > 0) uptimeStr += `${minutes % 60}m `; + uptimeStr += `${seconds % 60}s`; + + // Get memory usage + const memoryUsage = process.memoryUsage(); + const memoryMB = (memoryUsage.heapUsed / 1024 / 1024).toFixed(2); + + // Get server count + const serverCount = client.guilds.cache.size; + + // Check API health + let apiStatus = '๐ŸŸข Operational'; + try { + const healthCheck = await fetch(OPENCLAW_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(OPENCLAW_TOKEN && { 'Authorization': `Bearer ${OPENCLAW_TOKEN}` }) + }, + body: JSON.stringify({ + model: config.ai?.model || 'claude-sonnet-4-20250514', + max_tokens: 1, + messages: [{ role: 'user', content: 'ping' }], + }), + }); + + if (!healthCheck.ok) { + apiStatus = '๐ŸŸก Degraded'; + } + } catch { + apiStatus = '๐Ÿ”ด Unavailable'; + } + + // Create status embed + const embed = new EmbedBuilder() + .setColor(0x43B581) + .setTitle('๐Ÿ“Š Bot Status') + .addFields( + { name: 'โฑ๏ธ Uptime', value: uptimeStr, inline: true }, + { name: '๐Ÿ“ก Servers', value: serverCount.toString(), inline: true }, + { name: '๐Ÿง  Memory', value: `${memoryMB} MB`, inline: true }, + { name: '๐Ÿค– AI Status', value: apiStatus, inline: false }, + { name: '๐Ÿ“ Latency', value: `${client.ws.ping}ms`, inline: true } + ) + .setFooter({ text: `${client.user.tag}` }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + } catch (err) { + console.error(`Error handling /${commandName}:`, err.message); + + const errorMessage = 'Sorry, something went wrong processing your command!'; + + if (interaction.deferred) { + await interaction.editReply(errorMessage).catch(() => {}); + } else { + await interaction.reply({ content: errorMessage, ephemeral: true }).catch(() => {}); + } + } +}); + // Error handling client.on('error', (error) => { console.error('Discord error:', error);