UNPKG

hataraku

Version:

An autonomous coding agent for building AI-powered development tools. The name "Hataraku" (åƒć) means "to work" in Japanese.

568 lines • 30 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.processStreams = processStreams; exports.executeWithConfig = executeWithConfig; exports.main = main; exports.runCLI = runCLI; const client_bedrock_agent_runtime_1 = require("@aws-sdk/client-bedrock-agent-runtime"); const credential_providers_1 = require("@aws-sdk/credential-providers"); const chalk_1 = __importDefault(require("chalk")); const colors_1 = require("../utils/colors"); const node_stream_1 = require("node:stream"); const prompts_1 = require("@inquirer/prompts"); const agents_1 = require("../core/agents"); const bedrock_1 = require("../core/providers/bedrock"); const knowledge_base_1 = require("../core/providers/knowledge-base"); const play_audio_1 = require("../core/tools/play-audio"); const config_loader_1 = require("../config/config-loader"); const ProfileManager_1 = require("../config/ProfileManager"); const ai_sdk_provider_1 = require("@openrouter/ai-sdk-provider"); const thread_1 = require("../core/thread/thread"); /** * Convert a model name to an AWS Bedrock model ARN * @param modelName The model name to convert * @param region The AWS region * @returns The model ARN */ function convertToModelArn(modelName, region = 'us-east-1') { // If already an ARN, return as is if (modelName.startsWith('arn:')) { return modelName; } // Handle different model formats if (modelName.includes('/')) { // Format like "anthropic/claude-3-5-sonnet" const [provider, model] = modelName.split('/'); if (provider.toLowerCase() === 'anthropic') { return `arn:aws:bedrock:${region}::foundation-model/anthropic.${model}:0`; } else if (provider.toLowerCase() === 'amazon') { return `arn:aws:bedrock:${region}::foundation-model/amazon.${model}:0`; } else if (provider.toLowerCase() === 'meta') { return `arn:aws:bedrock:${region}::foundation-model/meta.${model}:0`; } else if (provider.toLowerCase() === 'cohere') { return `arn:aws:bedrock:${region}::foundation-model/cohere.${model}:0`; } else if (provider.toLowerCase() === 'ai21') { return `arn:aws:bedrock:${region}::foundation-model/ai21.${model}:0`; } } // Default to Anthropic if just a model name is provided return `arn:aws:bedrock:${region}::foundation-model/anthropic.${modelName}:0`; } /** * Process streams from agent responses */ async function processStreams(textStream, options) { const consoleStream = new node_stream_1.PassThrough(); const sourceStream = new node_stream_1.PassThrough(); let fullText = ''; // Collect full text for TTS // Convert AsyncIterable to stream const streamPromise = (async () => { try { for await (const chunk of textStream) { sourceStream.write(chunk); } sourceStream.end(); } catch (err) { sourceStream.destroy(err); throw err; // Re-throw to be caught by the outer try-catch } })(); // Pipe source to console sourceStream.pipe(consoleStream); // Set up console output with color consoleStream.on('data', (chunk) => { // Color the agent's output in green process.stdout.write(colors_1.colors.success(chunk.toString())); }); // Wait for both the stream processing and console output to finish await Promise.all([ streamPromise, new Promise((resolve, reject) => { consoleStream.on('end', resolve); consoleStream.on('error', reject); }) ]); // Add a newline at the end for better formatting process.stdout.write('\n'); } /** * Execute a task with the given configuration */ async function executeWithConfig(task, cliOptions, interactive) { try { const configLoader = new config_loader_1.ConfigLoader(); const { profile, agent } = await configLoader.getEffectiveConfig(cliOptions); // Determine configuration options const shouldUseInteractive = interactive !== undefined ? interactive : false; const shouldPlaySound = cliOptions.sound !== undefined ? cliOptions.sound : profile.options?.sound; let shouldStream = cliOptions.stream !== undefined ? cliOptions.stream : profile.options?.stream; const shouldShowVerbose = cliOptions.verbose === true; // Determine which model to use let model; const provider = cliOptions.provider || profile.provider; const modelName = cliOptions.model || profile.model; if (!provider) { colors_1.log.error('Error: No provider specified in profile or command line.'); return 1; } if (!modelName) { colors_1.log.error('Error: No model specified in profile or command line.'); return 1; } // Set up the model based on provider switch (provider.toLowerCase()) { case 'knowledge-base': const client = new client_bedrock_agent_runtime_1.BedrockAgentRuntimeClient({ region: cliOptions.region || process.env.AWS_REGION || 'us-east-1', credentials: await (0, credential_providers_1.fromIni)({ profile: cliOptions.profile })() }); // Configure knowledge base provider with options from CLI, env vars, profile, or defaults const kbConfig = { region: cliOptions.region || process.env.AWS_REGION, knowledgeBaseId: cliOptions.kbId || process.env.KB_ID, profile: cliOptions.profile }; // If a model is specified, convert it to a model ARN if (cliOptions.model) { kbConfig.modelArn = convertToModelArn(cliOptions.model, kbConfig.region || 'us-east-1'); } else if (process.env.KB_MODEL_ARN) { // Use environment variable if available kbConfig.modelArn = process.env.KB_MODEL_ARN; } // Check if there are knowledge base settings in the profile const profileConfig = await configLoader.getEffectiveConfig(cliOptions); if (profileConfig.profile.knowledgeBase) { // Use profile settings as fallbacks if (!kbConfig.knowledgeBaseId) { kbConfig.knowledgeBaseId = profileConfig.profile.knowledgeBase.knowledgeBaseId; } if (!kbConfig.modelArn) { kbConfig.modelArn = profileConfig.profile.knowledgeBase.modelArn; } if (!kbConfig.region) { kbConfig.region = profileConfig.profile.knowledgeBase.region; } } // Verify that knowledge base ID is provided if (!kbConfig.knowledgeBaseId) { console.error(chalk_1.default.red('Error: Knowledge Base ID is required when using the knowledge-base provider.')); console.error(chalk_1.default.yellow('You can provide it using one of the following methods:')); console.error(chalk_1.default.yellow(' 1. Command line option: --kb-id <your-kb-id>')); console.error(chalk_1.default.yellow(' 2. Environment variable: KB_ID=<your-kb-id>')); console.error(chalk_1.default.yellow(' 3. Profile configuration: Run "hataraku profile set-kb <profile-name>"')); process.exit(1); } if (shouldShowVerbose) { console.log('Knowledge Base Configuration:', kbConfig); } const kbProvider = await (0, knowledge_base_1.createKnowledgeBaseProvider)(client, kbConfig); // For knowledge base provider, disable streaming shouldStream = false; // Create a model wrapper that implements the LanguageModelV1 interface model = { specificationVersion: 'v1', provider: 'knowledge-base', modelId: 'knowledge-base', defaultObjectGenerationMode: 'json', async chat(messages) { if (shouldShowVerbose) { console.log('chat called with messages:', JSON.stringify(messages, null, 2)); } const lastMessage = messages[messages.length - 1]; if (!lastMessage || lastMessage.role !== 'user') { if (shouldShowVerbose) { console.log('Message validation failed:', lastMessage); } throw new Error('Knowledge base provider only supports user messages'); } // Show loading indicator console.log(chalk_1.default.blue('šŸ” Processing knowledge base query...')); const startTime = Date.now(); const response = await kbProvider(lastMessage.content); // Calculate processing time const endTime = Date.now(); const processingTime = ((endTime - startTime) / 1000).toFixed(2); // Only output the clean response to the console if (provider.toLowerCase() === 'knowledge-base' && !shouldShowVerbose) { // Clear previous output console.clear(); // Output only the clean response with improved formatting console.log(chalk_1.default.bold.cyan('šŸ“ Response:')); console.log(chalk_1.default.gray('─'.repeat(50))); console.log(chalk_1.default.white(response.content)); console.log(chalk_1.default.gray(`\nProcessing time: ${processingTime} seconds`)); // Display sources if available if (response.sources && response.sources.length > 0) { console.log(chalk_1.default.bold('\n\nšŸ“š Sources:')); console.log(chalk_1.default.gray('─'.repeat(50))); response.sources.forEach((source, index) => { console.log(`\n${chalk_1.default.blue(`[${index + 1}]`)} ${chalk_1.default.bold(source.title || `${source.sourceType} Document`)}`); if (source.url) { console.log(` ${chalk_1.default.cyan('šŸ”— URL:')} ${chalk_1.default.underline(source.url)}`); } // Show author if available if (source.metadata && source.metadata['x-amz-bedrock-kb-author']) { console.log(` ${chalk_1.default.yellow('šŸ‘¤ Author:')} ${source.metadata['x-amz-bedrock-kb-author']}`); } // Show creation date if available if (source.metadata && source.metadata['x-amz-bedrock-kb-createdAt']) { console.log(` ${chalk_1.default.green('šŸ“… Created:')} ${source.metadata['x-amz-bedrock-kb-createdAt']}`); } console.log(chalk_1.default.gray('─'.repeat(30))); }); } } return { choices: [{ message: { role: 'assistant', content: response.content } }] }; }, async doGenerate(messages) { if (shouldShowVerbose) { console.log('doGenerate called with messages:', JSON.stringify(messages, null, 2)); } // Extract the user's query from the complex message format let userQuery = ''; // Handle different message formats try { // Check if messages is an object with a prompt property if (messages && typeof messages === 'object' && !Array.isArray(messages) && 'prompt' in messages) { const messagesObj = messages; if (Array.isArray(messagesObj.prompt)) { // Look for user messages in the prompt array for (let i = 0; i < messagesObj.prompt.length; i++) { const msg = messagesObj.prompt[i]; if (msg && msg.role === 'user' && msg.content) { if (typeof msg.content === 'string') { userQuery = msg.content; break; } else if (Array.isArray(msg.content)) { // Look for text content in the content array for (let j = 0; j < msg.content.length; j++) { const item = msg.content[j]; if (item && item.type === 'text' && item.text) { userQuery = item.text; break; } } if (userQuery) break; } } } } } else if (Array.isArray(messages)) { // Standard array of messages format // Find the last user message for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; if (msg.role === 'user') { if (typeof msg.content === 'string') { userQuery = msg.content; break; } else if (Array.isArray(msg.content)) { // Look for text content in the content array for (let j = 0; j < msg.content.length; j++) { const item = msg.content[j]; if (item && item.type === 'text' && item.text) { userQuery = item.text; break; } } if (userQuery) break; } } } } } catch (error) { console.error('Error parsing messages:', error); } if (!userQuery) { console.log('Could not extract user query from messages'); throw new Error('Could not extract user query from messages'); } if (shouldShowVerbose) { console.log('Extracted user query:', userQuery); } try { console.log(chalk_1.default.blue('šŸ” Processing knowledge base query...')); const startTime = Date.now(); const response = await kbProvider(userQuery); const endTime = Date.now(); const processingTime = ((endTime - startTime) / 1000).toFixed(2); // Clear previous output console.clear(); // Output only the clean response with improved formatting console.log(chalk_1.default.bold.cyan('šŸ“ Response:')); console.log(chalk_1.default.gray('─'.repeat(50))); console.log(chalk_1.default.white(response.content)); console.log(chalk_1.default.gray(`\nProcessing time: ${processingTime} seconds`)); // Display sources if available if (response.sources && response.sources.length > 0) { console.log(chalk_1.default.bold('\n\nšŸ“š Sources:')); console.log(chalk_1.default.gray('─'.repeat(50))); response.sources.forEach((source, index) => { console.log(`\n${chalk_1.default.blue(`[${index + 1}]`)} ${chalk_1.default.bold(source.title || `${source.sourceType} Document`)}`); if (source.url) { console.log(` ${chalk_1.default.cyan('šŸ”— URL:')} ${chalk_1.default.underline(source.url)}`); } // Show author if available if (source.metadata && source.metadata['x-amz-bedrock-kb-author']) { console.log(` ${chalk_1.default.yellow('šŸ‘¤ Author:')} ${source.metadata['x-amz-bedrock-kb-author']}`); } // Show creation date if available if (source.metadata && source.metadata['x-amz-bedrock-kb-createdAt']) { console.log(` ${chalk_1.default.green('šŸ“… Created:')} ${source.metadata['x-amz-bedrock-kb-createdAt']}`); } console.log(chalk_1.default.gray('─'.repeat(30))); }); } return { choices: [{ message: { role: 'assistant', content: response.content } }], usage: { promptTokens: 0, // Knowledge base doesn't provide token counts completionTokens: 0, totalTokens: 0 } }; } catch (error) { console.error('Error from knowledge base provider:', error); // Format a user-friendly error message let errorMessage = 'An error occurred while querying the knowledge base.'; if (error.name === 'ExpiredTokenException') { errorMessage = 'Your AWS credentials have expired. Please refresh your AWS credentials and try again.'; } else if (error.name === 'AccessDeniedException') { errorMessage = 'Access denied to the knowledge base. Please check your AWS permissions.'; } else if (error.name === 'ResourceNotFoundException') { errorMessage = 'Knowledge base not found. Please check your knowledge base ID.'; } else if (error.name === 'ValidationException') { errorMessage = 'Invalid request to the knowledge base. Please check your configuration.'; } else if (error.message) { errorMessage = `Error: ${error.message}`; } return { choices: [{ message: { role: 'assistant', content: errorMessage } }], usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 } }; } } }; break; case 'bedrock': model = await (0, bedrock_1.createBedrockModel)(cliOptions.profile, cliOptions.model); break; case 'openrouter': default: if (!cliOptions.apiKey) { throw new Error('API key is required for OpenRouter'); } const openRouter = (0, ai_sdk_provider_1.createOpenRouter)({ apiKey: cliOptions.apiKey }); model = openRouter; break; } // Initialize agent using our factory function and include MCP tools const cliAgent = await (0, agents_1.createCLIAgent)(model, { verbose: shouldShowVerbose }); // Output verbose information if enabled if (shouldShowVerbose) { colors_1.log.system('\nVerbose mode enabled. Showing intermediate task information.'); colors_1.log.system(`Using provider: ${provider}, model: ${modelName}`); colors_1.log.system(`Stream: ${shouldStream ? 'enabled' : 'disabled'}, Sound: ${shouldPlaySound ? 'enabled' : 'disabled'}`); } colors_1.log.system('\nšŸ” Processing task...'); // Options object for functions const options = { sound: shouldPlaySound, stream: shouldStream, verbose: shouldShowVerbose }; if (shouldUseInteractive) { // Interactive mode const thread = new thread_1.Thread(); async function runInteractiveTask(currentTask) { let taskToRun = currentTask; if (!taskToRun) { taskToRun = await (0, prompts_1.input)({ message: 'Enter your task, or type "/exit" to exit\n\n> ', default: task // Use provided task argument as default if available }); if (!taskToRun || taskToRun === '/exit' || taskToRun === '/quit') { colors_1.log.warning('Exiting interactive mode.'); return 0; } } // Handle slash commands if (taskToRun.startsWith('/')) { const command = taskToRun.toLowerCase(); switch (command) { case '/cost': const messages = thread.getMessages(); const lastMessage = messages[messages.length - 1]; const usage = lastMessage.providerOptions; colors_1.log.system(`\nUsage stats: ${JSON.stringify(usage, null, 2)}`); return runInteractiveTask(); default: colors_1.log.warning(`Unknown command: ${command}`); colors_1.log.system('Available commands:'); colors_1.log.system(' /cost - Show usage stats for this session'); colors_1.log.system(' /exit - Exit interactive mode (same as "/quit")'); return runInteractiveTask(); } } colors_1.log.system(`\nExecuting task: ${taskToRun}`); try { if (shouldStream) { const result = await cliAgent.task(taskToRun, { stream: true, verbose: shouldShowVerbose, thread }); await processStreams(result, options); } else { const result = await cliAgent.task(taskToRun, { verbose: shouldShowVerbose, thread }); colors_1.log.success(result); } // Add a small delay to ensure task history is saved await new Promise(resolve => setTimeout(resolve, 100)); // Finally play the celebratory audio if (shouldPlaySound) { await play_audio_1.playAudioTool.execute({ path: 'audio/celebration.wav' }, { toolCallId: 'celebration', messages: [] }); } // Prompt for next task return runInteractiveTask(); } catch (error) { colors_1.log.error(`Error executing task: ${error}`); return runInteractiveTask(); } } return runInteractiveTask(task); } else { // Normal mode if (!task) { colors_1.log.error('Error: Please provide a task or question'); return 1; } colors_1.log.system(`\nExecuting task: ${task}`); try { if (shouldStream) { const result = await cliAgent.task(task, { stream: true, verbose: shouldShowVerbose }); await processStreams(result, options); } else { const result = await cliAgent.task(task, { verbose: shouldShowVerbose }); colors_1.log.success(result); } // Add a small delay to ensure task history is saved await new Promise(resolve => setTimeout(resolve, 100)); if (shouldPlaySound) { await play_audio_1.playAudioTool.execute({ path: 'audio/celebration.wav' }, { toolCallId: 'celebration', messages: [] }); } return 0; } catch (error) { colors_1.log.error(`Error executing task: ${error}`); return 1; } } } catch (error) { colors_1.log.error(`Error: ${error}`); colors_1.log.warning('\nDebug Information:'); colors_1.log.warning(`Provider: ${cliOptions.provider}`); colors_1.log.warning(`Model: ${cliOptions.model}`); return 1; } } /** * Main function to handle command line invocation */ async function main(task, program) { const options = program?.opts() || {}; const cliOptions = { profile: options.profile, provider: options.provider, model: options.model, apiKey: options.apiKey, stream: options.stream, sound: options.sound, verbose: options.verbose, agent: options.agent, region: options.region }; // Handle interactive mode separately const isInteractive = options.interactive; return executeWithConfig(task, cliOptions, isInteractive); } /** * Function for programmatic CLI usage */ async function runCLI(input) { try { const configLoader = new config_loader_1.ConfigLoader(); const profile = await (new ProfileManager_1.ProfileManager()).getActiveProfile(); const cliOptions = { provider: profile.provider, model: profile.model, stream: false, sound: false, verbose: false }; await executeWithConfig(input, cliOptions, false); } catch (error) { colors_1.log.error(`Error: ${error}`); throw error; } } //# sourceMappingURL=execution.js.map