UNPKG

@bestdefense/bd-agent

Version:

An AI-powered coding assistant CLI that connects to AWS Bedrock

498 lines 29.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startChat = startChat; const chalk_1 = __importDefault(require("chalk")); const bedrock_client_1 = require("../services/bedrock-client"); const conversation_manager_1 = require("../services/conversation-manager"); const tool_executor_1 = require("../services/tool-executor"); const memory_manager_1 = require("../services/memory-manager"); const slash_commands_1 = require("../services/slash-commands"); const history_manager_1 = require("../services/history-manager"); const readline_with_history_1 = require("../utils/readline-with-history"); const prompts_1 = require("../utils/prompts"); const markdown_renderer_1 = require("../utils/markdown-renderer"); const error_handler_1 = require("../utils/error-handler"); async function startChat(options) { // Initialize CLAUDE.md support (0, prompts_1.initializeClaudeMd)(process.cwd()); const client = new bedrock_client_1.BedrockClient(); const conversation = new conversation_manager_1.ConversationManager(); const toolExecutor = new tool_executor_1.ToolExecutor(); const memoryManager = new memory_manager_1.MemoryManager(client); const slashCommands = new slash_commands_1.SlashCommandManager(); const historyManager = new history_manager_1.HistoryManager(); // Connect memory manager to conversation conversation.setMemoryManager(memoryManager); console.log(chalk_1.default.blue('BD Agent - AI Coding Assistant')); console.log(chalk_1.default.gray('Type "exit" or "quit" to end the session')); console.log(chalk_1.default.gray('Type "/help" for available commands')); console.log(chalk_1.default.gray('Use ↑/↓ arrow keys to navigate command history\n')); // Create readline interface with history support const readlineWithHistory = new readline_with_history_1.ReadlineWithHistory(historyManager); // Handle stdin close event (happens when piping input) process.stdin.on('end', () => { console.log(chalk_1.default.gray('\nInput stream ended.')); readlineWithHistory.close(); }); // Keep the process alive process.stdin.resume(); while (true) { try { // Get input with history support let message; if (process.stdin.isTTY) { // Use custom readline with history for TTY message = await readlineWithHistory.question(chalk_1.default.green('You: ')); } else { // Fallback for non-TTY environments message = await readlineWithHistory.questionSimple(chalk_1.default.green('You: ')); } if (['exit', 'quit'].includes(message.toLowerCase().trim())) { console.log(chalk_1.default.yellow('\nGoodbye!')); readlineWithHistory.close(); break; } // Memory management commands if (message.trim() === '/memory') { const stats = conversation.getMemoryStats(); if (stats) { console.log(chalk_1.default.cyan('\nMemory Statistics:')); console.log(chalk_1.default.gray(`Total messages: ${stats.totalMessages}`)); console.log(chalk_1.default.gray(`Summaries created: ${stats.summaryCount}`)); console.log(chalk_1.default.gray(`Max messages: ${stats.config.maxMessages}`)); console.log(chalk_1.default.gray(`Summary threshold: ${stats.config.summaryThreshold}`)); } else { console.log(chalk_1.default.yellow('Memory management not available')); } console.log(''); continue; } if (message.trim() === '/clear') { conversation.clear(); memoryManager.clear(); console.log(chalk_1.default.green('Conversation cleared!\n')); continue; } if (message.trim() === '/help') { console.log(chalk_1.default.cyan('\nAvailable commands:')); console.log(chalk_1.default.gray(' /memory - Show memory statistics')); console.log(chalk_1.default.gray(' /history - Show command history')); console.log(chalk_1.default.gray(' /clear - Clear conversation history')); console.log(chalk_1.default.gray(' /commands - List available slash commands')); console.log(chalk_1.default.gray(' /help - Show this help message')); console.log(chalk_1.default.gray(' exit - Exit the chat session')); console.log(chalk_1.default.gray('\nNavigation:')); console.log(chalk_1.default.gray(' ↑/↓ - Navigate through command history')); console.log(chalk_1.default.gray(' Ctrl+A - Move to beginning of line')); console.log(chalk_1.default.gray(' Ctrl+E - Move to end of line')); console.log(chalk_1.default.gray('\nSlash commands:')); const commands = slashCommands.getAllCommands(); for (const cmd of commands.slice(0, 5)) { console.log(chalk_1.default.gray(` /${cmd.name} - ${cmd.description}`)); } if (commands.length > 5) { console.log(chalk_1.default.gray(` ... and ${commands.length - 5} more (use /commands to see all)`)); } console.log(''); continue; } if (message.trim() === '/history') { const history = historyManager.getHistory(20); // Show last 20 commands if (history.length === 0) { console.log(chalk_1.default.yellow('No command history available yet.')); } else { console.log(chalk_1.default.cyan('\nCommand History (most recent first):')); history.reverse().forEach((cmd, index) => { console.log(chalk_1.default.gray(` ${index + 1}. ${cmd}`)); }); } console.log(''); continue; } if (message.trim() === '/commands') { console.log(chalk_1.default.cyan('\nAvailable slash commands:')); const commands = slashCommands.getAllCommands(); for (const cmd of commands) { console.log(chalk_1.default.yellow(` /${cmd.name}`) + chalk_1.default.gray(` - ${cmd.description}`)); if (cmd.arguments && cmd.arguments.length > 0) { console.log(chalk_1.default.gray(` Arguments: ${cmd.arguments.join(', ')}`)); } } console.log(chalk_1.default.gray(`\nCustom commands directory: ${slashCommands.getCommandsDirectory()}`)); console.log(''); continue; } if (!message.trim()) { continue; } // Check if it's a slash command let processedMessage = message; if (message.trim().startsWith('/')) { const parts = message.trim().split(' '); const commandName = parts[0].substring(1); // Remove the '/' const args = parts.slice(1); const expandedPrompt = slashCommands.executeCommand(commandName, args); if (expandedPrompt) { processedMessage = expandedPrompt; console.log(chalk_1.default.gray(`Executing /${commandName}...\n`)); } else { console.log(chalk_1.default.red(`Unknown command: /${commandName}`)); console.log(chalk_1.default.gray('Use /help to see available commands\n')); continue; } } conversation.addMessage('user', processedMessage); console.log(chalk_1.default.gray('Analyzing your request...')); let timeoutCleared = false; let isResponseReceived = false; // Add timeout for initial response const initialTimeout = setTimeout(() => { if (!timeoutCleared && !isResponseReceived) { console.log(chalk_1.default.red('\n✗ Response timeout - the AI is taking longer than expected')); console.log(chalk_1.default.yellow('Please try again or check your AWS Bedrock connection.')); } }, 30000); // 30 second timeout for initial response try { const messages = await conversation.getOptimizedMessages(); const tools = toolExecutor.getAvailableTools(); const systemPrompt = (0, prompts_1.getSystemPrompt)(); let assistantMessage = ''; let pendingToolCalls = []; let isFirstChunk = true; const markdownRenderer = new markdown_renderer_1.StreamingMarkdownRenderer(); const stream = client.streamMessage(messages, tools, systemPrompt); for await (const chunk of stream) { if (typeof chunk === 'string') { if (isFirstChunk) { timeoutCleared = true; isResponseReceived = true; clearTimeout(initialTimeout); // Clear the timeout when we get first response console.log(chalk_1.default.blue('\nAssistant:')); isFirstChunk = false; } // Process chunk through streaming markdown renderer const formatted = markdownRenderer.addChunk(chunk); process.stdout.write(formatted); assistantMessage += chunk; } else if (chunk && 'toolUseId' in chunk) { if (!isFirstChunk) { // Flush any remaining markdown content const remaining = markdownRenderer.flush(); if (remaining) process.stdout.write(remaining); console.log(); // New line after text output } else { timeoutCleared = true; isResponseReceived = true; clearTimeout(initialTimeout); // Clear the timeout when we get first response } pendingToolCalls.push(chunk); } } if (assistantMessage || pendingToolCalls.length > 0) { // Flush any remaining content const remaining = markdownRenderer.flush(); if (remaining) process.stdout.write(remaining); if (assistantMessage) console.log(); // Ensure we end with a newline // Add assistant message with both text and tool use blocks const contentBlocks = []; if (assistantMessage) { contentBlocks.push({ text: assistantMessage }); } for (const toolCall of pendingToolCalls) { contentBlocks.push({ toolUse: toolCall }); } if (contentBlocks.length > 0) { conversation.addMessage('assistant', contentBlocks); } } // Tool execution loop - keep executing tools until no more are requested let toolRounds = 0; const MAX_TOOL_ROUNDS = 100; // Allow extended tool usage for mission completion let consecutiveToolOnlyRounds = 0; while (pendingToolCalls.length > 0 && toolRounds < MAX_TOOL_ROUNDS) { toolRounds++; const currentToolCalls = [...pendingToolCalls]; pendingToolCalls = []; // Clear for next round for (const toolCall of currentToolCalls) { // Create more descriptive spinner text based on tool let spinnerText = `Executing ${toolCall.name}...`; switch (toolCall.name) { case 'read_file': spinnerText = `Reading file: ${toolCall.input?.path || toolCall.input?.file_path || 'unknown'}`; break; case 'read': spinnerText = `Reading file: ${toolCall.input?.file_path || toolCall.input?.path || 'unknown'}`; break; case 'write_file': case 'write': spinnerText = `Writing to file: ${toolCall.input.file_path || 'unknown'}`; break; case 'edit_file': case 'edit': case 'multi_edit': spinnerText = `Editing file: ${toolCall.input.file_path || 'unknown'}`; break; case 'list_directory': spinnerText = `Listing directory: ${toolCall.input.path || '.'}`; break; case 'run_command': case 'bash': spinnerText = `Running command: ${toolCall.input.command?.substring(0, 50) || 'unknown'}${toolCall.input.command?.length > 50 ? '...' : ''}`; break; case 'grep': spinnerText = `Searching for pattern: "${toolCall.input.pattern?.substring(0, 30) || 'unknown'}"${toolCall.input.pattern?.length > 30 ? '...' : ''}`; break; case 'glob': spinnerText = `Finding files matching: ${toolCall.input.pattern || 'unknown'}`; break; case 'git_status': spinnerText = 'Checking git status...'; break; case 'git_diff': spinnerText = 'Getting git diff...'; break; case 'git_add': spinnerText = `Staging files: ${toolCall.input.files?.join(', ') || 'unknown'}`; break; case 'git_commit': spinnerText = 'Creating git commit...'; break; } console.log(chalk_1.default.yellow(`⚡ ${spinnerText}`)); try { // Check if tool exists if (!toolExecutor.hasTool(toolCall.name)) { throw new Error(`Unknown tool: ${toolCall.name}. Did you mean 'read_file'?`); } const result = await toolExecutor.executeTool(toolCall.name, toolCall.input); // Create success message based on tool and result let successMessage = `${toolCall.name} completed`; switch (toolCall.name) { case 'read_file': if (result.success && result.content) { const lines = result.content.split('\n').length; successMessage = `Read ${lines} lines from ${toolCall.input?.path || 'file'}`; } break; case 'read': if (result.success && result.content) { const lines = result.content.split('\n').length; successMessage = `Read ${lines} lines from ${toolCall.input?.file_path || 'file'}`; } break; case 'write_file': case 'write': if (result.success) { successMessage = `Successfully wrote to ${toolCall.input.file_path}`; } break; case 'edit_file': case 'edit': if (result.success) { successMessage = `Successfully edited ${toolCall.input.file_path}`; } break; case 'multi_edit': if (result.success && toolCall.input.edits) { successMessage = `Applied ${toolCall.input.edits.length} edits to ${toolCall.input.file_path}`; } break; case 'grep': if (result.success && result.matches) { successMessage = `Found ${result.matches.length} matches`; } else if (result.success && result.count !== undefined) { successMessage = `Found ${result.count} total matches`; } break; case 'glob': if (result.success && result.files) { successMessage = `Found ${result.files.length} files matching pattern`; } break; case 'run_command': case 'bash': if (result.success) { successMessage = `Command executed successfully`; } break; } console.log(chalk_1.default.green(`✓ ${successMessage}`)); conversation.addToolResult(toolCall.toolUseId, result); if (result) { console.log(chalk_1.default.gray(`\nTool Result (${toolCall.name}):`)); // Special handling for edit tools to show diffs if ((toolCall.name === 'edit_file' || toolCall.name === 'edit' || toolCall.name === 'multi_edit') && result.success && result.diff) { console.log((0, markdown_renderer_1.renderDiff)(result.diff.old, result.diff.new)); if (result.preview) { console.log(chalk_1.default.gray('\nPreview:')); console.log(result.preview); } } else if (result.error) { // Show errors in a more readable format console.log(chalk_1.default.red(result.error)); } else if ((toolCall.name === 'grep' || toolCall.name === 'glob') && result.success) { // Special formatting for search results if (result.matches) { console.log(chalk_1.default.cyan(`Found ${result.matches.length} matches:`)); result.matches.forEach((match) => { console.log(` ${chalk_1.default.yellow(match.file)}:${chalk_1.default.green(match.line)} ${match.match}`); }); } else if (result.files) { console.log(chalk_1.default.cyan(`Found ${result.files.length} files:`)); result.files.forEach((file) => { console.log(` ${chalk_1.default.yellow(file)}`); }); } else if (result.count !== undefined) { console.log(chalk_1.default.cyan(`Total matches: ${result.count}`)); } } else { // Regular JSON output for other tools console.log(chalk_1.default.gray(JSON.stringify(result, null, 2))); } } } catch (error) { console.log(chalk_1.default.red(`✗ ${toolCall.name} failed`)); const toolError = new error_handler_1.ToolExecutionError(error.message || String(error), toolCall.name, error); console.error((0, error_handler_1.formatError)(toolError)); conversation.addToolResult(toolCall.toolUseId, `Error: ${error.message || error}`); } } // Only show spinner if we're expecting a follow-up console.log(''); // Add spacing console.log(chalk_1.default.cyan('⏳ Waiting for AI response...')); let streamTimeout; try { const followUpMessages = await conversation.getOptimizedMessages(); // Add timeout for streaming - give more time for follow-up responses streamTimeout = setTimeout(() => { if (!followUpMessage && pendingToolCalls.length === 0) { console.log(chalk_1.default.gray('(AI completed tool execution without additional commentary)')); } }, 10000); // 10 second timeout for follow-up const followUpStream = client.streamMessage(followUpMessages, tools, systemPrompt); let followUpMessage = ''; let isFirstFollowUpChunk = true; const followUpRenderer = new markdown_renderer_1.StreamingMarkdownRenderer(); for await (const chunk of followUpStream) { if (typeof chunk === 'string') { if (isFirstFollowUpChunk) { console.log(chalk_1.default.blue('\nAssistant:')); isFirstFollowUpChunk = false; } const formatted = followUpRenderer.addChunk(chunk); process.stdout.write(formatted); followUpMessage += chunk; } else if (chunk && 'toolUseId' in chunk) { // Handle additional tool calls in follow-up if (isFirstFollowUpChunk) { isFirstFollowUpChunk = false; } // Collect the new tool call for the next iteration console.log(chalk_1.default.yellow(`\n[AI requests tool: ${chunk.name}]`)); pendingToolCalls.push(chunk); } } // Clear timeout since we completed successfully clearTimeout(streamTimeout); // Ensure spinner is stopped even if no chunks received if (isFirstFollowUpChunk) { } if (followUpMessage || pendingToolCalls.length > 0) { const remaining = followUpRenderer.flush(); if (remaining) process.stdout.write(remaining); if (followUpMessage) console.log(); // Ensure we end with a newline // Track if this was a tool-only response if (!followUpMessage && pendingToolCalls.length > 0) { consecutiveToolOnlyRounds++; } else { consecutiveToolOnlyRounds = 0; } // Add assistant message with both text and any new tool calls const followUpContentBlocks = []; if (followUpMessage) { followUpContentBlocks.push({ text: followUpMessage }); } for (const toolCall of pendingToolCalls) { followUpContentBlocks.push({ toolUse: toolCall }); } if (followUpContentBlocks.length > 0) { conversation.addMessage('assistant', followUpContentBlocks); } } } catch (followUpError) { clearTimeout(streamTimeout); console.log(chalk_1.default.red('✗ Failed to process tool results')); console.error(chalk_1.default.red(`Error: ${followUpError.message}`)); // If it's a specific error about the model not responding, handle gracefully if (followUpError.message?.includes('timeout') || followUpError.message?.includes('stream')) { console.log(chalk_1.default.yellow('\nThe AI completed the tool execution but didn\'t provide additional commentary.')); } } // Check if we should continue the loop if (pendingToolCalls.length === 0) { // No more tools to execute, we're done break; } // Stop if too many consecutive tool-only responses if (consecutiveToolOnlyRounds >= 100) { console.log(chalk_1.default.yellow('\nThe AI is requesting many tools without providing explanations. Stopping here.')); console.log(chalk_1.default.gray('You can ask a follow-up question to get more details.')); break; } if (toolRounds >= MAX_TOOL_ROUNDS) { console.log(chalk_1.default.yellow('\nReached maximum tool execution rounds.')); break; } // Otherwise, continue with the next round of tool execution } // End of while loop for tool execution } catch (error) { timeoutCleared = true; clearTimeout(initialTimeout); // Clear timeout on error console.error((0, error_handler_1.formatError)(error)); // If it's a retryable error, suggest retrying if (error.retryable) { console.log(chalk_1.default.yellow('\nWould you like to try again? The error might be temporary.')); } // Don't break the main loop - continue to accept new prompts // This matches Claude's behavior of continuing after errors } console.log(''); } catch (outerError) { console.error(chalk_1.default.red(`\nFatal error in chat loop: ${outerError.message}`)); console.error(chalk_1.default.gray(outerError.stack)); readlineWithHistory.close(); break; } } // This should never be reached unless exit/quit is typed console.log(chalk_1.default.gray('Chat session ended.')); process.stdin.pause(); } //# sourceMappingURL=chat.js.map