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..a70b7891 --- /dev/null +++ b/.auto-claude-status @@ -0,0 +1,25 @@ +{ + "active": true, + "spec": "009-enhanced-code-formatting-support", + "state": "building", + "subtasks": { + "completed": 4, + "total": 5, + "in_progress": 1, + "failed": 0 + }, + "phase": { + "current": "Integration and Testing", + "id": null, + "total": 1 + }, + "workers": { + "active": 0, + "max": 1 + }, + "session": { + "number": 6, + "started_at": "2026-02-03T20:34:14.947629" + }, + "last_update": "2026-02-03T20:44:29.372940" +} \ No newline at end of file diff --git a/.claude_settings.json b/.claude_settings.json new file mode 100644 index 00000000..73b6c4aa --- /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/009-enhanced-code-formatting-support/**)", + "Write(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/009-enhanced-code-formatting-support/**)", + "Edit(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/009-enhanced-code-formatting-support/**)", + "Glob(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/009-enhanced-code-formatting-support/**)", + "Grep(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/009-enhanced-code-formatting-support/**)", + "Read(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/009-enhanced-code-formatting-support/.auto-claude/specs/009-enhanced-code-formatting-support/**)", + "Write(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/009-enhanced-code-formatting-support/.auto-claude/specs/009-enhanced-code-formatting-support/**)", + "Edit(/Users/billchirico/Developer/bill-bot/.auto-claude/worktrees/tasks/009-enhanced-code-formatting-support/.auto-claude/specs/009-enhanced-code-formatting-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/config.json b/config.json index 8f7d9c77..126580d2 100644 --- a/config.json +++ b/config.json @@ -3,7 +3,7 @@ "enabled": true, "model": "claude-sonnet-4-20250514", "maxTokens": 1024, - "systemPrompt": "You are Volvox Bot, the friendly AI assistant for the Volvox developer community Discord server.\n\nYou're witty, snarky (but warm), and deeply knowledgeable about programming, software development, and tech.\n\nKey traits:\n- Helpful but not boring\n- Can roast people lightly when appropriate\n- Enthusiastic about cool tech and projects\n- Supportive of beginners learning to code\n- Concise - this is Discord, not an essay\n\n⚠️ CRITICAL RULES:\n- NEVER type @.everyone or @.here (remove the dots) - these ping hundreds of people\n- NEVER use mass mention pings under any circumstances\n- If you need to address the group, say \"everyone\" or \"folks\" without the @ symbol\n\nKeep responses under 2000 chars. Use Discord markdown when helpful.", + "systemPrompt": "You are Volvox Bot, the friendly AI assistant for the Volvox developer community Discord server.\n\nYou're witty, snarky (but warm), and deeply knowledgeable about programming, software development, and tech.\n\nKey traits:\n- Helpful but not boring\n- Can roast people lightly when appropriate\n- Enthusiastic about cool tech and projects\n- Supportive of beginners learning to code\n- Concise - this is Discord, not an essay\n\n📝 CODE FORMATTING:\n- Always use Discord code blocks with language tags: ```js, ```python, ```rust, etc.\n- Use inline code with `backticks` for short snippets, variables, or filenames\n- Keep code examples readable and well-formatted\n- For multi-line code, always include the language tag for syntax highlighting\n\n⚠️ CRITICAL RULES:\n- NEVER type @.everyone or @.here (remove the dots) - these ping hundreds of people\n- NEVER use mass mention pings under any circumstances\n- If you need to address the group, say \"everyone\" or \"folks\" without the @ symbol\n\nKeep responses under 2000 chars. Use Discord markdown when helpful.", "channels": [] }, "welcome": { diff --git a/src/index.js b/src/index.js index 754e73db..93bf36ce 100644 --- a/src/index.js +++ b/src/index.js @@ -70,6 +70,112 @@ function isSpam(content) { return SPAM_PATTERNS.some(pattern => pattern.test(content)); } +/** + * Detect code blocks in message content + * Returns array of code blocks with their language and content + */ +function detectcode(content) { + const blocks = []; + + // Match triple backtick code blocks with optional language + const tripleBacktickRegex = /```(\w+)?\n?([\s\S]*?)```/g; + let match; + + while ((match = tripleBacktickRegex.exec(content)) !== null) { + blocks.push({ + type: 'fenced', + language: match[1] || null, + content: match[2].trim(), + raw: match[0] + }); + } + + // Match inline code (single backticks) only if no fenced blocks found + if (blocks.length === 0) { + const inlineRegex = /`([^`]+)`/g; + while ((match = inlineRegex.exec(content)) !== null) { + blocks.push({ + type: 'inline', + language: null, + content: match[1], + raw: match[0] + }); + } + } + + return blocks; +} + +/** + * Detect programming language from code content + * Returns detected language string or null if unknown + */ +function detectlanguage(code) { + if (!code || typeof code !== 'string') return null; + + const content = code.trim(); + if (content.length === 0) return null; + + // Language patterns with priority order + const patterns = [ + // Markup/Config (check first as they're most distinctive) + { lang: 'json', regex: /^\s*[\[{][\s\S]*[}\]]\s*$/, score: 0 }, + { lang: 'xml', regex: /<\?xml|<\/\w+>|<\w+[^>]*\/?>/, score: 0 }, + { lang: 'html', regex: /||
\s*[{(]|\bimport\s+.*from\s+['"]/, score: 0 }, + + // Java + { lang: 'java', regex: /\bpublic\s+(class|static|void)|System\.out\.print|@Override/, score: 0 }, + + // C/C++ + { lang: 'cpp', regex: /#include\s*<|std::|cout\s*<<|namespace\s+\w+/, score: 0 }, + { lang: 'c', regex: /#include\s*<\w+\.h>|int\s+main\s*\(|\bprintf\s*\(/, score: 0 }, + + // C# + { lang: 'csharp', regex: /\busing\s+System|namespace\s+\w+|Console\.Write/, score: 0 }, + + // Go + { lang: 'go', regex: /^package\s+\w+|func\s+\w+\(.*\)|go\s+func\(/, score: 0 }, + + // Rust + { lang: 'rust', regex: /fn\s+\w+\(|let\s+mut\s+\w+|\bimpl\s+\w+|\buse\s+\w+::/, score: 0 }, + + // Ruby + { lang: 'ruby', regex: /\bdef\s+\w+|\bend\b|\brequire\s+['"]/, score: 0 }, + + // PHP + { lang: 'php', regex: /<\?php|\$\w+\s*=|echo\s+\$/, score: 0 }, + + // SQL + { lang: 'sql', regex: /\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\s+/i, score: 0 }, + ]; + + // Score each pattern + for (const pattern of patterns) { + if (pattern.regex.test(content)) { + pattern.score++; + } + } + + // Return language with highest score + const detected = patterns + .filter(p => p.score > 0) + .sort((a, b) => b.score - a.score)[0]; + + return detected ? detected.lang : null; +} + /** * Get or create conversation history for a channel */ @@ -93,6 +199,74 @@ function addToHistory(channelId, role, content) { } } +/** + * Split message while preserving code blocks + * Returns array of message chunks that respect code block boundaries + */ +function splitMessageSafely(text, maxLength = 2000) { + if (text.length <= maxLength) { + return [text]; + } + + const chunks = []; + let currentChunk = ''; + let inCodeBlock = false; + let codeBlockLanguage = null; + + const lines = text.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lineWithNewline = (i < lines.length - 1) ? line + '\n' : line; + + // Detect code block boundaries + if (line.startsWith('```')) { + if (inCodeBlock) { + // Closing code block + inCodeBlock = false; + codeBlockLanguage = null; + } else { + // Opening code block + inCodeBlock = true; + const match = line.match(/^```(\w+)?/); + codeBlockLanguage = match?.[1] || null; + } + } + + // Check if adding this line would exceed limit + const potentialLength = currentChunk.length + lineWithNewline.length; + + if (potentialLength > maxLength - 10) { // -10 for safety margin and potential code block markers + // If we're in a code block, close it before splitting + if (inCodeBlock) { + currentChunk += '```\n'; + chunks.push(currentChunk); + // Start new chunk with code block + currentChunk = '```' + (codeBlockLanguage || '') + '\n' + lineWithNewline; + } else { + // Not in code block, safe to split + if (currentChunk.trim()) { + chunks.push(currentChunk); + } + currentChunk = lineWithNewline; + } + } else { + currentChunk += lineWithNewline; + } + } + + // Add remaining content + if (currentChunk.trim()) { + // Close any open code block + if (inCodeBlock) { + currentChunk += '```'; + } + chunks.push(currentChunk); + } + + return chunks; +} + /** * Generate AI response using OpenClaw's chat completions endpoint */ @@ -249,11 +423,15 @@ client.on('messageCreate', async (message) => { message.author.username ); - // Split long responses + // Split long responses while preserving code blocks if (response.length > 2000) { - const chunks = response.match(/[\s\S]{1,1990}/g) || []; - for (const chunk of chunks) { - await message.channel.send(chunk); + const chunks = splitMessageSafely(response, 2000); + for (let i = 0; i < chunks.length; i++) { + if (i === 0) { + await message.reply(chunks[i]); + } else { + await message.channel.send(chunks[i]); + } } } else { await message.reply(response);