UNPKG

sourcewizard

Version:

SourceWizard - AI-powered setup wizard for dev tools and libraries with MCP integration

577 lines (576 loc) 23.9 kB
import { getBulkTargetData, detectRepo, executeAddCommand } from "./repository-detector.js"; import { toolDefinitions } from "./tools-schema.js"; import * as path from "path"; import { promises as fsp } from "fs"; import { spawn } from "child_process"; export class NewAgent { projectContext; cwd; serverUrl; apiKey; jwt; onStepFinish; constructor(options) { this.cwd = options.cwd; this.projectContext = options.projectContext; this.onStepFinish = options.onStepFinish; this.serverUrl = options.serverUrl; this.apiKey = options.apiKey; this.jwt = options.jwt; if (!this.apiKey && !this.jwt) { throw new Error("API key or JWT token is required"); } } getAuthHeaders() { const headers = { 'Content-Type': 'application/json' }; if (this.apiKey) { headers["x-api-key"] = this.apiKey; } if (this.jwt) { headers["Authorization"] = `Bearer ${this.jwt}`; } return headers; } async searchPackages(query) { // Add bulk target data to project context const bulkTargetData = await getBulkTargetData(this.projectContext.targets, this.cwd); this.projectContext.target_dependencies = bulkTargetData; // Step 1: Create search agent run const searchResponse = await fetch(`${this.serverUrl}/api/agent/search`, { method: 'POST', headers: this.getAuthHeaders(), body: JSON.stringify({ query, project_context: this.projectContext }) }); if (!searchResponse.ok) { throw new Error(`Failed to create search agent run: ${searchResponse.statusText}`); } const { agent_id } = await searchResponse.json(); // Step 2: Start the conversation return this.runConversation(agent_id); } async installPackage(packageName) { // Add bulk target data to project context const bulkTargetData = await getBulkTargetData(this.projectContext.targets, this.cwd); this.projectContext.target_dependencies = bulkTargetData; // Step 1: Create install agent run const installResponse = await fetch(`${this.serverUrl}/api/agent/install`, { method: 'POST', headers: this.getAuthHeaders(), body: JSON.stringify({ package: packageName, project_context: this.projectContext }) }); if (!installResponse.ok) { const errorText = await installResponse.text(); throw new Error(`Failed to create install agent run: ${installResponse.statusText} - ${errorText}`); } const { agent_id } = await installResponse.json(); // Step 2: Start the conversation return this.runConversation(agent_id); } async runConversation(agent_id, payload) { let currentPayload = payload; const maxIterations = 50; // Prevent infinite loops let iterationCount = 0; while (iterationCount < maxIterations) { iterationCount++; const eventsResponse = await fetch(`${this.serverUrl}/api/agent/events`, { method: 'POST', headers: this.getAuthHeaders(), body: JSON.stringify({ agent_id, payload: currentPayload }) }); if (!eventsResponse.ok) { const errorText = await eventsResponse.text(); throw new Error(`Failed to process agent events: ${eventsResponse.statusText} - ${errorText}`); } const result = await eventsResponse.json(); // Notify about the step with structured data if (this.onStepFinish) { let stepData = { text: result.message || '', toolCalls: result.action === 'tool_call' ? [{ toolName: result.tool_name, args: result.data }] : [], toolResults: [], finishReason: result.action === 'response' ? 'stop' : undefined, usage: {}, structuredData: result.data // Pass through the structured data }; // For install operations, extract stage information if (result.data?.stage) { stepData.stage = result.data.stage; stepData.description = result.data.description; } // For search operations, extract package data if (result.data?.packages) { stepData.packages = result.data.packages; stepData.query = result.data.query; stepData.totalAvailable = result.data.total_available; } this.onStepFinish(stepData); } // Handle different response types switch (result.action) { case 'response': // Final response from LLM with structured data return { text: result.message, toolCalls: [], toolResults: [], finishReason: result.data?.finishReason || 'stop', usage: {}, structuredData: result.data, // Include specific fields for backward compatibility and easy access packages: result.data?.packages, query: result.data?.query, totalAvailable: result.data?.total_available, stage: result.data?.stage, description: result.data?.description }; case 'tool_call': // LLM wants to call a tool - execute it and continue const toolResult = await this.executeTool(result.tool_name, result.data); // Prepare tool result payload for next iteration currentPayload = JSON.stringify({ tool_call_id: result.data.tool_call_id, result: toolResult }); // Continue to next iteration break; case 'error': // Log the error but check if it's a recoverable error vs system error console.warn('Agent received error response:', { message: result.message, timestamp: result.timestamp }); // If this is a system-level error (not a tool failure), notify and throw if (result.message && (result.message.includes('Internal server error') || result.message.includes('Authentication required') || result.message.includes('Insufficient credits'))) { if (this.onStepFinish) { this.onStepFinish({ text: result.message || '', toolCalls: [], toolResults: [], finishReason: 'stop', usage: {}, structuredData: result.data, error: result.message || 'System error occurred', isError: true }); } throw new Error(`System error: ${result.message}`); } // For other errors (likely tool failures), return a response but don't throw // This allows the conversation to continue naturally return { text: result.message || 'An error occurred, but continuing...', toolCalls: [], toolResults: [], finishReason: undefined, // Don't mark as finished usage: {}, structuredData: result.data, error: result.message, stage: 'error', description: result.message || 'Error occurred' }; default: throw new Error(`Unknown agent response action: ${result.action}`); } } // If we've reached the maximum number of iterations, throw an error throw new Error(`Conversation exceeded maximum iterations (${maxIterations}).`); } async executeTool(toolName, args) { console.log(`Executing tool: ${toolName}`, args); // Remove tool_call_id from args as it's not part of the tool parameters const { tool_call_id, ...toolArgs } = args; // Validate arguments against tool schema const toolDefinition = toolDefinitions[toolName]; if (!toolDefinition) { throw new Error(`Unknown tool: ${toolName}`); } try { toolDefinition.parameters.parse(toolArgs); } catch (error) { if (error instanceof Error && error.name === 'ZodError') { throw new Error(`Invalid arguments for tool ${toolName}: ${error.message}`); } throw new Error(`Failed to validate arguments for tool ${toolName}: ${error}`); } switch (toolName) { case 'read_file': return this.readFile(toolArgs.path); case 'write_file': return this.writeFile(toolArgs.path, toolArgs.content); case 'create_file': return this.createFile(toolArgs.path, toolArgs.content); case 'list_directory': return this.listDirectory(toolArgs.path, toolArgs.include_hidden); case 'append_to_file': return this.appendToFile(toolArgs.path, toolArgs.content); case 'delete_file': return this.deleteFile(toolArgs.path, toolArgs.recursive); case 'edit_file': return this.editFile(toolArgs.path, toolArgs.old_string, toolArgs.new_string); case 'get_bulk_target_data': return this.getBulkTargetData(toolArgs.targetNames, toolArgs.repoPath); case 'typecheck': return this.typecheck(toolArgs.target, toolArgs.repoPath); case 'add_package': return this.addPackage(toolArgs.packageName, toolArgs.target, toolArgs.isDev, toolArgs.useWorkspace, toolArgs.additionalFlags, toolArgs.repoPath); default: // This should never be reached due to validation above throw new Error(`Unhandled tool: ${toolName}`); } } // Tool implementations async readFile(filePath) { try { const absolutePath = path.resolve(this.cwd, filePath); const content = await fsp.readFile(absolutePath, 'utf-8'); return { path: filePath, content, success: true }; } catch (error) { return { path: filePath, error: error instanceof Error ? error.message : String(error), success: false }; } } async writeFile(filePath, content) { try { const absolutePath = path.resolve(this.cwd, filePath); await fsp.writeFile(absolutePath, content, 'utf-8'); return { path: filePath, success: true, message: 'File written successfully' }; } catch (error) { return { path: filePath, error: error instanceof Error ? error.message : String(error), success: false }; } } async createFile(filePath, content) { try { const absolutePath = path.resolve(this.cwd, filePath); const dir = path.dirname(absolutePath); await fsp.mkdir(dir, { recursive: true }); // Check if file already exists try { await fsp.access(absolutePath); return { path: filePath, error: 'File already exists', success: false }; } catch { // File doesn't exist, proceed with creation await fsp.writeFile(absolutePath, content, 'utf-8'); return { path: filePath, success: true, message: 'File created successfully' }; } } catch (error) { return { path: filePath, error: error instanceof Error ? error.message : String(error), success: false }; } } async listDirectory(dirPath, includeHidden = false) { try { const absolutePath = path.resolve(this.cwd, dirPath); const entries = await fsp.readdir(absolutePath, { withFileTypes: true }); const items = entries .filter((entry) => includeHidden || !entry.name.startsWith('.')) .map((entry) => ({ name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', path: path.join(dirPath, entry.name) })); return { path: dirPath, items, success: true }; } catch (error) { return { path: dirPath, error: error instanceof Error ? error.message : String(error), success: false }; } } async appendToFile(filePath, content) { try { const absolutePath = path.resolve(this.cwd, filePath); await fsp.appendFile(absolutePath, content, 'utf-8'); return { path: filePath, success: true, message: 'Content appended successfully' }; } catch (error) { return { path: filePath, error: error instanceof Error ? error.message : String(error), success: false }; } } async deleteFile(filePath, recursive = false) { try { const absolutePath = path.resolve(this.cwd, filePath); const stats = await fsp.lstat(absolutePath); if (stats.isDirectory()) { await fsp.rmdir(absolutePath, { recursive }); } else { await fsp.unlink(absolutePath); } return { path: filePath, success: true, message: `${stats.isDirectory() ? 'Directory' : 'File'} deleted successfully` }; } catch (error) { return { path: filePath, error: error instanceof Error ? error.message : String(error), success: false }; } } async editFile(filePath, oldString, newString) { try { const absolutePath = path.resolve(this.cwd, filePath); const currentContent = await fsp.readFile(absolutePath, 'utf-8'); if (!currentContent.includes(oldString)) { return { path: filePath, error: 'Old string not found in file', success: false }; } const newContent = currentContent.replace(oldString, newString); await fsp.writeFile(absolutePath, newContent, 'utf-8'); return { path: filePath, success: true, message: 'File edited successfully' }; } catch (error) { return { path: filePath, error: error instanceof Error ? error.message : String(error), success: false }; } } async getBulkTargetData(targetNames, repoPath) { try { const processedTargetNames = Array.isArray(targetNames) ? targetNames : [targetNames]; const resolvedRepoPath = repoPath ? path.resolve(this.cwd, repoPath) : this.cwd; const requestedTargets = {}; const availableTargetNames = Object.keys(this.projectContext.targets || {}); const invalidTargets = []; for (const targetName of processedTargetNames) { if (this.projectContext.targets && this.projectContext.targets[targetName]) { requestedTargets[targetName] = this.projectContext.targets[targetName]; } else { invalidTargets.push(targetName); } } if (invalidTargets.length > 0) { return { success: false, error: `Invalid target names: ${invalidTargets.join(', ')}. Available targets: ${availableTargetNames.join(', ')}` }; } if (Object.keys(requestedTargets).length === 0) { return { success: false, error: 'No valid targets specified' }; } const result = await getBulkTargetData(requestedTargets, resolvedRepoPath); return { success: true, data: result, requestedTargets: Object.keys(requestedTargets), availableTargets: availableTargetNames }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } async typecheck(target, repoPath) { try { const resolvedRepoPath = repoPath ? path.resolve(this.cwd, repoPath) : this.cwd; let output = ""; let hasError = false; // Get repository info const repo = await detectRepo(resolvedRepoPath); if (!repo.targets || Object.keys(repo.targets).length === 0) { return { success: false, error: "No targets found in repository", target: target || "default", repoPath: resolvedRepoPath, }; } // Find the appropriate target const targetKey = target ? Object.keys(repo.targets).find(key => key.includes(target)) : Object.keys(repo.targets)[0]; if (!targetKey) { return { success: false, error: `Target "${target}" not found`, target: target || "default", repoPath: resolvedRepoPath, }; } const targetInfo = repo.targets[targetKey]; const checkActions = targetInfo.actions?.check || []; if (checkActions.length === 0) { return { success: false, error: "No check actions defined for target", target: target || "default", repoPath: resolvedRepoPath, }; } // Execute check commands silently (no output to console) for (const action of checkActions) { const workingDir = targetInfo.path === "//" ? resolvedRepoPath : path.resolve(resolvedRepoPath, targetInfo.path); output += `Running: ${action.command} in ${workingDir}\n`; await new Promise((resolve, reject) => { const [cmd, ...args] = action.command.split(" "); const child = spawn(cmd, args, { cwd: workingDir, stdio: 'pipe', // Capture all output, don't inherit to console shell: true }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on("close", (code) => { // Capture both stdout and stderr for complete output if (stdout.trim()) { output += `${stdout.trim()}\n`; } if (stderr.trim()) { output += `${stderr.trim()}\n`; } if (code !== 0) { hasError = true; } resolve(); }); child.on("error", (error) => { hasError = true; output += `Failed to execute: ${error.message}\n`; resolve(); }); }); } return { success: !hasError, output: output.trim() || "No output captured", target: targetKey, repoPath: resolvedRepoPath, message: hasError ? "Type checking completed with errors" : "Type checking completed successfully" }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), target: target || "default", repoPath: repoPath || this.cwd, }; } } async addPackage(packageName, target, isDev, useWorkspace, additionalFlags, repoPath) { const resolvedRepoPath = repoPath ? path.resolve(this.cwd, repoPath) : this.cwd; let output = ""; let hasError = false; try { await executeAddCommand(target, resolvedRepoPath, { packageName, isDev, useWorkspace, additionalFlags, onOutput: (message, type) => { output += `[${type}] ${message}\n`; if (type === 'error') { hasError = true; } } }); return { success: !hasError, output: output.trim(), target: target || "default", packageName, isDev, useWorkspace, additionalFlags, repoPath: resolvedRepoPath, message: hasError ? `Failed to add package ${packageName}` : `Package ${packageName} added successfully` }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), output: output.trim(), // Include the command output for debugging target: target || "default", packageName, isDev, useWorkspace, additionalFlags, repoPath: repoPath || this.cwd, }; } } }