@bestdefense/bd-agent
Version:
An AI-powered coding assistant CLI that connects to AWS Bedrock
498 lines • 29.6 kB
JavaScript
;
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