UNPKG

abyss-ai

Version:

Autonomous AI coding agent - enhanced OpenCode with autonomous capabilities

1,508 lines (1,265 loc) • 51.6 kB
import { cmd } from "./cmd" import * as prompts from "@clack/prompts" import { UI } from "../ui" import { AgentCoordinator } from "../../agent/coordinator/agent-coordinator" import { LargeFileProcessor } from "../../agent/file-processing/large-file-processor" import { QuestionGenerator } from "../../agent/question-generation/question-generator" import { Agent } from "../../agent/agent" import { Session } from "../../session" import { Identifier } from "../../id/id" import { AgentTask, ReasoningMode, type ProcessingContext } from "../../agent/types/agent" import { App } from "../../app/app" import { BatchProcessor, BatchResult } from "./batch-processor" import { AgentMemory } from "../../agent/memory/agent-memory" import path from "path" // Multi-Agent Analysis Command const MultiAgentAnalyzeCommand = cmd({ command: "analyze [file]", describe: "analyze code using Abyss multi-agent reasoning system", builder: (yargs) => yargs .positional("file", { type: "string", describe: "file to analyze", }) .option("reasoning-modes", { type: "array", describe: "reasoning modes to use", choices: ["ultrathinking", "ultrareasoning", "hybrid-reasoning", "hybrid-thinking"], default: ["ultrathinking", "ultrareasoning", "hybrid-reasoning", "hybrid-thinking"], }) .option("output", { type: "string", describe: "output format", choices: ["json", "text", "summary", "questions"], default: "text", }) .option("analysis-mode", { type: "string", describe: "analysis approach to use", choices: ["question-driven", "legacy", "auto"], default: "question-driven", }) .option("show-questions", { type: "boolean", describe: "show generated questions in output", default: false, }), async handler(args) { await App.provide({ cwd: process.cwd() }, async (_app) => { UI.empty() prompts.intro("Multi-Agent Code Analysis") let filePath: string if (args.file) { filePath = path.resolve(args.file) } else { const fileInput = await prompts.text({ message: "File to analyze", placeholder: "Enter file path", validate: (x) => (x.length > 0 ? undefined : "File path required"), }) if (prompts.isCancel(fileInput)) throw new UI.CancelledError() filePath = path.resolve(fileInput) } // Check if file exists const file = Bun.file(filePath) if (!(await file.exists())) { prompts.log.error(`File not found: ${filePath}`) return } const spinner = prompts.spinner() spinner.start("Initializing multi-agent system...") try { // Initialize agent coordinator const coordinator = new AgentCoordinator() await coordinator.initializeAgents() // Map string reasoning modes to enum values (for legacy compatibility) const _reasoningModes = (args.reasoningModes as string[]).map(mode => { switch (mode) { case "ultrathinking": return ReasoningMode.ULTRATHINKING case "ultrareasoning": return ReasoningMode.ULTRAREASONING case "hybrid-reasoning": return ReasoningMode.HYBRID_REASONING case "hybrid-thinking": return ReasoningMode.HYBRID_THINKING default: return ReasoningMode.ULTRATHINKING } }) spinner.message("Reading and analyzing file...") // Read file content const content = await file.text() const _fileStats = await file.stat() // Determine if we need large file processing const isLargeFile = content.length > 50000 // 50KB threshold let results if (isLargeFile) { spinner.message("Processing large file with chunking...") const processor = new LargeFileProcessor() const context: ProcessingContext = { filePath, language: detectLanguage(filePath), complexity: assessBasicComplexity(content) } const processingResult = await processor.processLargeFile(filePath, context) results = [processingResult] } else { spinner.message("Analyzing with question-driven multi-agent system...") // Use question-driven approach with OpenCode's agent system results = await processWithQuestionDrivenAgents(content, filePath, spinner) } spinner.stop("Analysis complete") // Display results based on output format await displayResults(results, args.output as string, filePath, { showQuestions: args.showQuestions as boolean, analysisMode: args.analysisMode as string }) // Cleanup await coordinator.dispose() } catch (error) { spinner.stop("Analysis failed") prompts.log.error(`Analysis failed: ${error instanceof Error ? error.message : String(error)}`) throw error } prompts.outro("Multi-agent analysis complete") }) }, }) // YOLO Analysis Command (Autonomous Mode) - Now the main command export const YoloCommand = cmd({ command: "yolo [file]", describe: "autonomous mode - analyze and fix code without prompts", builder: (yargs) => yargs .positional("file", { type: "string", describe: "file or directory to analyze and fix autonomously", }) .option("directory", { type: "boolean", describe: "process entire directory recursively", default: false, }) .option("file-patterns", { type: "array", describe: "file patterns to include (for directory mode)", default: ["**/*.{js,ts,jsx,tsx,py,java,cpp,c,go,rs}"], }) .option("exclude-patterns", { type: "array", describe: "file patterns to exclude", default: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**"], }) .option("max-files", { type: "number", describe: "maximum number of files to process", default: 50, }) .option("concurrent", { type: "number", describe: "number of files to process concurrently", default: 3, }) .option("dry-run", { type: "boolean", describe: "preview changes without modifying files", default: false, }) .option("backup", { type: "boolean", describe: "create backup before making changes", default: true, }) .option("rollback", { type: "string", describe: "rollback from specific backup directory", }) .option("list-backups", { type: "boolean", describe: "list available backups", default: false, }) .option("safety-checks", { type: "boolean", describe: "enable additional safety checks", default: true, }), async handler(args) { await App.provide({ cwd: process.cwd() }, async (_app) => { UI.empty() // Handle special actions first if (args.listBackups) { await listAvailableBackups(process.cwd()) return } if (args.rollback) { await rollbackFromBackup(args.rollback) return } prompts.intro("šŸš€ YOLO Mode - Autonomous Analysis & Fixes") let targetPath: string if (args.file) { targetPath = path.resolve(args.file) } else { const pathInput = await prompts.text({ message: args.directory ? "Directory to analyze and fix" : "File to analyze and fix", placeholder: args.directory ? "Enter directory path" : "Enter file path", validate: (x) => (x.length > 0 ? undefined : "Path required"), }) if (prompts.isCancel(pathInput)) throw new UI.CancelledError() targetPath = path.resolve(pathInput) } // Check if path exists const stat = await Bun.file(targetPath).exists() || await Bun.file(targetPath + '/').exists() if (!stat) { prompts.log.error(`Path not found: ${targetPath}`) return } // Determine if processing directory or single file const isDirectory = args.directory || (await Bun.file(targetPath).stat()).isDirectory if (isDirectory) { await processDirectoryWithYolo(targetPath, args) } else { await processSingleFileWithYolo(targetPath, args) } }) }, }) // Directory processing with YOLO async function processDirectoryWithYolo(directoryPath: string, args: any) { const spinner = prompts.spinner() spinner.start("šŸ—‚ļø Initializing batch processing...") try { // Check if YOLO agent exists const availableAgents = await Agent.list() const yoloAgent = availableAgents.find(agent => agent.name === 'yolo') if (!yoloAgent) { spinner.stop("YOLO agent not found") prompts.log.error("YOLO agent not configured. Please create a 'yolo' agent first.") prompts.log.info("You can create one using: abyss agent create") return } const agent = await Agent.get('yolo') if (!agent) { spinner.stop("YOLO agent configuration error") prompts.log.error("YOLO agent configuration is invalid.") return } // Setup batch processor and memory system const batchProcessor = new BatchProcessor({ directory: directoryPath, filePatterns: args.filePatterns as string[], excludePatterns: args.excludePatterns as string[], maxFiles: args.maxFiles as number, concurrent: args.concurrent as number, dryRun: args.dryRun as boolean, backupEnabled: args.backup as boolean }) const agentMemory = new AgentMemory(directoryPath) spinner.message("šŸ” Finding files to process...") const files = await batchProcessor.findFiles() if (files.length === 0) { spinner.stop("No files found") prompts.log.warn("No files found matching the specified patterns") return } spinner.message(`šŸ“ Found ${files.length} files`) if (args.dryRun) { prompts.log.info("šŸ” DRY RUN MODE - Previewing files that would be processed:") files.forEach((file, index) => { console.log(` ${index + 1}. ${path.relative(directoryPath, file)}`) }) prompts.outro("Dry run complete - no files were modified") return } // Confirm before processing const shouldProceed = await prompts.confirm({ message: `Process ${files.length} files with YOLO autonomous mode?`, initialValue: true, }) if (prompts.isCancel(shouldProceed) || !shouldProceed) { prompts.log.info("Operation cancelled") return } spinner.start("šŸš€ Processing files with YOLO...") // Process files in batch const results = await batchProcessor.processBatch(async (filePath: string) => { const fileStartTime = Date.now() try { spinner.message(`Processing: ${path.relative(directoryPath, filePath)}`) // Read file content const content = await Bun.file(filePath).text() const fileType = detectLanguage(filePath) // Get contextual advice from memory const contextualAdvice = agentMemory.generateContextualAdvice(filePath, fileType) // Create a session for YOLO analysis const sessionId = Identifier.ascending("session") const session = await Session.create(sessionId) const messageID = Identifier.ascending("message") const yoloPrompt = `AUTONOMOUS MODE: Analyze and fix this file without asking for permission. File: ${filePath} Content: \`\`\` ${content} \`\`\` ${contextualAdvice} Instructions: 1. Analyze the code thoroughly for issues, bugs, and improvements 2. Fix them directly using write/edit tools 3. Focus on: syntax errors, type issues, best practices, security issues 4. Be concise - this is part of a batch operation 5. Only make changes if there are clear improvements to be made 6. Learn from the contextual advice above from previous similar analyses Work autonomously - no permission requests needed.` const result = await Session.chat({ messageID, sessionID: session.id, modelID: agent.model?.modelID || "anthropic/claude-3-5-sonnet-20241022", providerID: agent.model?.providerID || "anthropic", mode: "build", system: agent.prompt, tools: agent.tools, parts: [ { id: Identifier.ascending("part"), type: "text", text: yoloPrompt, }, ], }) // Analyze result const toolParts = result.parts.filter((x) => x.type === "tool") const changes = toolParts.filter(t => (t as any).tool_name === 'write' || (t as any).tool_name === 'edit' ).length const responseText = result.parts.findLast((x) => x.type === "text")?.text || "" const issues = extractIssueCount(responseText) // Record memory for learning const changesApplied = extractChangesApplied(responseText, toolParts) const issuesFound = extractIssuesFound(responseText) agentMemory.addMemory({ filePath, fileType, issuesFound, changesApplied, successMetrics: { syntaxValid: changes > 0, // Assume valid if changes were made testsPass: responseText.toLowerCase().includes('test') && responseText.toLowerCase().includes('pass'), lintClean: !responseText.toLowerCase().includes('lint error') }, patterns: { commonIssues: issuesFound, effectiveFixes: changesApplied, riskySections: [] }, context: { language: fileType, complexity: assessBasicComplexity(content) } }) return { file: path.relative(directoryPath, filePath), success: true, changes, issues, processingTime: Date.now() - fileStartTime } } catch (error) { return { file: path.relative(directoryPath, filePath), success: false, changes: 0, issues: 0, processingTime: Date.now() - fileStartTime, error: error instanceof Error ? error.message : String(error) } } }) spinner.stop("āœ… Batch processing complete") // Display summary const summary = batchProcessor.getSummary() console.log(`\nšŸš€ YOLO Batch Processing Results\n`) console.log(`Directory: ${path.basename(directoryPath)}`) console.log(`Files Processed: ${summary.successful}/${summary.total}`) console.log(`Success Rate: ${Math.round(summary.successRate * 100)}%`) console.log(`Total Changes: ${summary.totalChanges}`) console.log(`Total Issues Found: ${summary.totalIssues}`) console.log(`Processing Time: ${Math.round(summary.elapsedTime / 1000)}s`) console.log(`Average Time/File: ${Math.round(summary.averageTime)}ms`) // Show failed files if any const failedResults = results.filter(r => !r.success) if (failedResults.length > 0) { console.log(`\nāŒ Failed Files:`) failedResults.forEach(result => { console.log(` • ${result.file}: ${result.error}`) }) } // Show most impacted files const changedFiles = results.filter(r => r.changes > 0).sort((a, b) => b.changes - a.changes) if (changedFiles.length > 0) { console.log(`\nšŸ”§ Files with Changes:`) changedFiles.slice(0, 10).forEach(result => { console.log(` • ${result.file}: ${result.changes} changes, ${result.issues} issues`) }) if (changedFiles.length > 10) { console.log(` ... and ${changedFiles.length - 10} more files`) } } } catch (error) { spinner.stop("Batch processing failed") prompts.log.error(`Batch processing failed: ${error instanceof Error ? error.message : String(error)}`) throw error } prompts.outro("YOLO batch processing complete - check your files for autonomous improvements!") } // Single file processing with YOLO async function processSingleFileWithYolo(filePath: string, args: any) { const spinner = prompts.spinner() spinner.start("Launching YOLO agent...") try { // Check if YOLO agent exists const availableAgents = await Agent.list() const yoloAgent = availableAgents.find(agent => agent.name === 'yolo') if (!yoloAgent) { spinner.stop("YOLO agent not found") prompts.log.error("YOLO agent not configured. Please create a 'yolo' agent first.") prompts.log.info("You can create one using: opencode agent create") return } spinner.message("Reading file and preparing autonomous analysis...") // Read file content const content = await Bun.file(filePath).text() const fileType = detectLanguage(filePath) // Setup memory system const agentMemory = new AgentMemory(path.dirname(filePath)) const contextualAdvice = agentMemory.generateContextualAdvice(filePath, fileType) // Create backup if enabled if (args.backup) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-') const backupPath = `${filePath}.backup-${timestamp}` await Bun.write(backupPath, content) prompts.log.info(`Backup created: ${backupPath}`) } // Create a session for YOLO analysis const sessionId = Identifier.ascending("session") const session = await Session.create(sessionId) const messageID = Identifier.ascending("message") const agent = await Agent.get('yolo') if (!agent) { spinner.stop("YOLO agent configuration error") prompts.log.error("YOLO agent configuration is invalid.") return } spinner.message("šŸ¤– YOLO agent is working autonomously...") const yoloPrompt = `AUTONOMOUS MODE: Analyze and fix this file without asking for permission. File: ${filePath} Content: \`\`\` ${content} \`\`\` ${contextualAdvice} Instructions: 1. Analyze the code thoroughly 2. Identify any issues, bugs, or improvements 3. Fix them directly (use write/edit tools) 4. Run tests if available (use bash tool) 5. Provide a summary of what was done 6. Learn from the contextual advice above from previous similar analyses Work autonomously - no permission requests needed.` const result = await Session.chat({ messageID, sessionID: session.id, modelID: agent.model?.modelID || "anthropic/claude-3-5-sonnet-20241022", providerID: agent.model?.providerID || "anthropic", mode: "build", system: agent.prompt, tools: agent.tools, parts: [ { id: Identifier.ascending("part"), type: "text", text: yoloPrompt, }, ], }) spinner.stop("šŸŽ‰ YOLO analysis complete") // Display results const textPart = result.parts.findLast((x) => x.type === "text") const toolParts = result.parts.filter((x) => x.type === "tool") const responseText = textPart?.text || "" // Record memory for learning const changesApplied = extractChangesApplied(responseText, toolParts) const issuesFound = extractIssuesFound(responseText) agentMemory.addMemory({ filePath, fileType, issuesFound, changesApplied, successMetrics: { syntaxValid: toolParts.length > 0, testsPass: responseText.toLowerCase().includes('test') && responseText.toLowerCase().includes('pass'), lintClean: !responseText.toLowerCase().includes('lint error') }, patterns: { commonIssues: issuesFound, effectiveFixes: changesApplied, riskySections: [] }, context: { language: fileType, complexity: assessBasicComplexity(content) } }) console.log(`\nšŸš€ YOLO Autonomous Analysis Results\n`) console.log(`File: ${path.basename(filePath)}`) console.log(`Tools Used: ${toolParts.length} tool calls`) console.log(`\nšŸ“‹ Summary:`) if (responseText) { console.log(responseText) } else { console.log("YOLO agent completed the analysis. Check the file for changes.") } if (toolParts.length > 0) { console.log(`\nšŸ”§ Actions Taken:`) toolParts.forEach((tool, index) => { console.log(`${index + 1}. ${(tool as any).tool_name || 'Tool'}: ${(tool as any).description || 'Action performed'}`) }) } } catch (error) { spinner.stop("YOLO analysis failed") prompts.log.error(`YOLO analysis failed: ${error instanceof Error ? error.message : String(error)}`) throw error } prompts.outro("YOLO mode complete - check your files for autonomous improvements!") } function extractIssueCount(text: string): number { const lowerText = text.toLowerCase() const issueKeywords = ['error', 'bug', 'issue', 'problem', 'warning', 'fix'] let count = 0 for (const keyword of issueKeywords) { const matches = lowerText.match(new RegExp(keyword, 'g')) if (matches) { count += matches.length } } return Math.min(count, 10) // Cap at 10 to avoid noise } function extractChangesApplied(responseText: string, toolParts: any[]): string[] { const changes: string[] = [] // Extract changes mentioned in response text const changeKeywords = ['fixed', 'updated', 'added', 'removed', 'refactored', 'improved'] const lines = responseText.split('\n') for (const line of lines) { for (const keyword of changeKeywords) { if (line.toLowerCase().includes(keyword)) { const cleanLine = line.trim().replace(/^[-•*]\s*/, '') if (cleanLine.length > 0 && cleanLine.length < 100) { changes.push(cleanLine) } } } } // Extract changes from tool calls toolParts.forEach(tool => { const toolName = (tool as any).tool_name || 'Unknown' const description = (tool as any).description || `Used ${toolName} tool` changes.push(description) }) return changes.slice(0, 10) // Limit to prevent noise } function extractIssuesFound(responseText: string): string[] { const issues: string[] = [] const lines = responseText.split('\n') let inIssueSection = false for (const line of lines) { const lowerLine = line.toLowerCase() // Check if we're entering an issue section if (lowerLine.includes('issue') || lowerLine.includes('problem') || lowerLine.includes('error') || lowerLine.includes('bug')) { inIssueSection = true } // Extract issue if we're in a section and line looks like an issue if (inIssueSection && (line.trim().startsWith('•') || line.trim().startsWith('-') || line.trim().startsWith('*'))) { const cleanLine = line.trim().replace(/^[-•*]\s*/, '') if (cleanLine.length > 0 && cleanLine.length < 150) { issues.push(cleanLine) } } // Stop if we hit a different section if (line.trim() === '' || (lowerLine.includes('fix') && !lowerLine.includes('issue')) || lowerLine.includes('recommendation') || lowerLine.includes('summary')) { inIssueSection = false } } return issues.slice(0, 10) // Limit to prevent noise } // Safety and backup management functions async function listAvailableBackups(projectPath: string): Promise<void> { const spinner = prompts.spinner() spinner.start("Scanning for backups...") try { const backupDirs: string[] = [] // Find .abyss-backup-* directories const entries = await Bun.file(projectPath).text().then(() => null, () => null) // Use a simple file listing approach const files = await Bun.file(path.join(projectPath, '.')).text().catch(() => '') // This is a simplified version - in a real implementation you'd use proper directory traversal const possibleBackups = [ '.abyss-backup-2024-01-01T00-00-00-000Z', '.abyss-backup-2024-01-02T00-00-00-000Z' ] for (const backupName of possibleBackups) { const backupPath = path.join(projectPath, backupName) const backupInfoPath = path.join(backupPath, '.backup-info.json') if (await Bun.file(backupInfoPath).exists()) { backupDirs.push(backupName) } } spinner.stop("Backup scan complete") if (backupDirs.length === 0) { prompts.log.info("No backups found in this directory") return } console.log(`\nšŸ“¦ Available Backups (${backupDirs.length} found):\n`) for (const backupDir of backupDirs) { try { const backupInfoPath = path.join(projectPath, backupDir, '.backup-info.json') const backupInfo = JSON.parse(await Bun.file(backupInfoPath).text()) const timestamp = new Date(backupInfo.timestamp).toLocaleString() const fileCount = backupInfo.files?.length || 0 console.log(`šŸ“ ${backupDir}`) console.log(` Created: ${timestamp}`) console.log(` Files: ${fileCount}`) console.log(` Rollback: abyss multiagent yolo --rollback "${backupDir}"`) console.log() } catch (error) { console.log(`šŸ“ ${backupDir} (corrupted backup info)`) console.log() } } } catch (error) { spinner.stop("Backup scan failed") prompts.log.error(`Failed to list backups: ${error}`) } } async function rollbackFromBackup(backupDir: string): Promise<void> { const spinner = prompts.spinner() spinner.start("Preparing rollback...") try { const projectPath = process.cwd() const fullBackupPath = path.resolve(backupDir.startsWith('/') ? backupDir : path.join(projectPath, backupDir)) const backupInfoPath = path.join(fullBackupPath, '.backup-info.json') if (!(await Bun.file(backupInfoPath).exists())) { spinner.stop("Backup not found") prompts.log.error(`Backup not found: ${fullBackupPath}`) prompts.log.info("Use --list-backups to see available backups") return } const backupInfo = JSON.parse(await Bun.file(backupInfoPath).text()) spinner.message(`Found backup from ${new Date(backupInfo.timestamp).toLocaleString()}`) console.log(`\nšŸ”„ Rollback Preview:`) console.log(` Backup: ${path.basename(fullBackupPath)}`) console.log(` Created: ${new Date(backupInfo.timestamp).toLocaleString()}`) console.log(` Files to restore: ${backupInfo.files?.length || 0}`) console.log() const shouldProceed = await prompts.confirm({ message: `Proceed with rollback? This will overwrite current files.`, initialValue: false, }) if (prompts.isCancel(shouldProceed) || !shouldProceed) { prompts.log.info("Rollback cancelled") return } spinner.start("Rolling back files...") let restoredCount = 0 let failedCount = 0 for (const relativePath of backupInfo.files || []) { try { const backupFilePath = path.join(fullBackupPath, relativePath) const originalFilePath = path.join(backupInfo.originalDirectory || projectPath, relativePath) if (await Bun.file(backupFilePath).exists()) { const content = await Bun.file(backupFilePath).text() await Bun.write(originalFilePath, content) restoredCount++ } else { failedCount++ } spinner.message(`Restored ${restoredCount} files...`) } catch (error) { failedCount++ prompts.log.warn(`Failed to restore ${relativePath}: ${error}`) } } spinner.stop("Rollback complete") console.log(`\nāœ… Rollback Results:`) console.log(` Files restored: ${restoredCount}`) if (failedCount > 0) { console.log(` Files failed: ${failedCount}`) } prompts.outro("Rollback completed successfully") } catch (error) { spinner.stop("Rollback failed") prompts.log.error(`Rollback failed: ${error instanceof Error ? error.message : String(error)}`) } } // Memory Command - simplified export const MemoryCommand = cmd({ command: "memory", describe: "show learning and memory statistics", builder: (yargs) => yargs .option("clear-old", { type: "number", describe: "clear memories older than N days", default: 0, }) .option("project", { type: "string", describe: "project path for memory analysis", default: process.cwd(), }), async handler(args) { UI.empty() prompts.intro("🧠 YOLO Agent Memory & Learning") const spinner = prompts.spinner() spinner.start("Loading agent memory...") try { const agentMemory = new AgentMemory(args.project) if (args.clearOld > 0) { const removedCount = agentMemory.clearOldMemories(args.clearOld) prompts.log.info(`Cleared ${removedCount} memories older than ${args.clearOld} days`) } const stats = agentMemory.getMemoryStats() spinner.stop("Memory loaded") console.log(`\nšŸ“Š Memory Statistics:\n`) console.log(`Total Memories: ${stats.totalMemories}`) console.log(`Success Rate: ${Math.round(stats.successRate * 100)}%`) console.log(`Average Complexity: ${stats.averageComplexity.toFixed(2)}`) console.log(`\nšŸ”¤ Language Breakdown:`) Object.entries(stats.languageBreakdown) .sort((a, b) => b[1] - a[1]) .forEach(([language, count]) => { console.log(` ${language}: ${count} files`) }) // Show learning patterns for top languages const topLanguages = Object.entries(stats.languageBreakdown) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([lang]) => lang) for (const language of topLanguages) { const patterns = agentMemory.getLearningPatterns(language) if (patterns.length > 0) { console.log(`\nšŸŽÆ ${language.toUpperCase()} Learning Patterns:`) patterns.slice(0, 5).forEach(pattern => { console.log(` • ${pattern.pattern}`) console.log(` Frequency: ${pattern.frequency}, Success: ${Math.round(pattern.successRate * 100)}%`) console.log(` ${pattern.recommendation}`) }) } } } catch (error) { spinner.stop("Memory analysis failed") prompts.log.error(`Memory analysis failed: ${error instanceof Error ? error.message : String(error)}`) } prompts.outro("Memory analysis complete") }, }) // Multi-Agent Status Command const MultiAgentStatusCommand = cmd({ command: "status", describe: "show multi-agent system status", async handler() { UI.empty() prompts.intro("Multi-Agent System Status") const spinner = prompts.spinner() spinner.start("Checking system status...") try { const coordinator = new AgentCoordinator() await coordinator.initializeAgents() const status = coordinator.getStatus() const healthCheck = await coordinator.healthCheck() spinner.stop("Status retrieved") // Display status prompts.log.info(`Registered Agents: ${status.registeredAgents}`) prompts.log.info(`Active Tasks: ${status.activeTasks}`) prompts.log.info(`Queued Tasks: ${status.queuedTasks}`) prompts.log.info(`Total Processed: ${status.totalTasksProcessed}`) prompts.log.info(`Average Processing Time: ${Math.round(status.averageProcessingTime)}ms`) prompts.log.info(`Success Rate: ${Math.round(status.successRate * 100)}%`) prompts.log.info(`System Health: ${healthCheck ? "āœ… Healthy" : "āŒ Issues detected"}`) if (status.agentUtilization && Object.keys(status.agentUtilization).length > 0) { prompts.log.info("Agent Utilization:") Object.entries(status.agentUtilization).forEach(([agentId, utilization]) => { prompts.log.info(` ${agentId}: ${Math.round(Number(utilization) * 100)}%`) }) } await coordinator.dispose() } catch (error) { spinner.stop("Status check failed") prompts.log.error(`Status check failed: ${error instanceof Error ? error.message : String(error)}`) } prompts.outro("Status check complete") }, }) // Main Multi-Agent Command export const MultiAgentCommand = cmd({ command: "multiagent", describe: "Abyss advanced multi-agent analysis system", builder: (yargs) => yargs .command(MultiAgentAnalyzeCommand) .command(MultiAgentAnalyzeCommand) .command(MemoryCommand) .command(MultiAgentStatusCommand) .demandCommand() .help(), async handler() { // This will show help when no subcommand is provided }, }) // Question-driven analysis using OpenCode's agent system async function processWithQuestionDrivenAgents(content: string, filePath: string, spinner: any) { const results = [] try { // Generate questions based on content and context const questionGenerator = new QuestionGenerator() const task: AgentTask = { id: `analysis-${Date.now()}`, type: 'code-analysis', data: content, context: { sessionId: `session-${Date.now()}`, filePath, language: detectLanguage(filePath), fileSize: content.length, lineCount: content.split('\n').length }, reasoningModes: [ReasoningMode.ULTRATHINKING, ReasoningMode.HYBRID_REASONING], priority: 1, timeout: 30000 } const questionResult = await questionGenerator.generateQuestions(task) spinner.message(`Generated ${questionResult.questions.length} specialized analysis questions...`) // Get available agents const availableAgents = await Agent.list() const agentMap = new Map(availableAgents.map(agent => [agent.name, agent])) // Process each question with the appropriate agent for (const question of questionResult.questions) { const agentName = question.targetAgentName if (!agentMap.has(agentName)) { prompts.log.warn(`Agent '${agentName}' not found, skipping question: ${question.perspective}`) continue } spinner.message(`${question.perspective}: Analyzing with ${agentName}...`) try { const agent = await Agent.get(agentName) if (!agent) { prompts.log.warn(`Agent '${agentName}' configuration is invalid`) continue } // Create a session for this analysis const sessionId = Identifier.ascending("session") const session = await Session.create(sessionId) const messageID = Identifier.ascending("message") // Run the agent with the specific question const analysisPrompt = `${question.question} File: ${filePath} Content: \`\`\` ${content} \`\`\` Please provide a detailed analysis focusing on: ${question.perspective}` const result = await Session.chat({ messageID, sessionID: session.id, modelID: agent.model?.modelID || "anthropic/claude-3-5-sonnet-20241022", providerID: agent.model?.providerID || "anthropic", mode: "build", system: agent.prompt, tools: agent.tools, parts: [ { id: Identifier.ascending("part"), type: "text", text: analysisPrompt, }, ], }) // Extract the response const textPart = result.parts.findLast((x) => x.type === "text") const analysisResult = { agentId: agentName, agentType: agentName, taskId: question.id, reasoningMode: question.reasoningMode, result: { perspective: question.perspective, question: question.question, analysis: textPart?.text || "No analysis provided", insights: extractInsights(textPart?.text || ""), issues: extractIssues(textPart?.text || ""), recommendations: extractRecommendations(textPart?.text || "") }, confidence: calculateConfidence(textPart?.text || ""), processingTime: 0, // TODO: Add timing metadata: { questionId: question.id, questionPerspective: question.perspective, originalQuestion: question.question, agentUsed: agentName } } results.push(analysisResult) } catch (error) { prompts.log.warn(`Failed to run ${agentName}: ${error instanceof Error ? error.message : String(error)}`) } } return results } catch (error) { prompts.log.error(`Question-driven analysis failed: ${error instanceof Error ? error.message : String(error)}`) return [] } } // Helper functions for result extraction function extractInsights(text: string): string[] { const insights = [] const lines = text.split('\n') let inInsightSection = false for (const line of lines) { if (line.toLowerCase().includes('insight') && line.includes(':')) { inInsightSection = true continue } if (inInsightSection && line.trim().startsWith('•') || line.trim().startsWith('-')) { insights.push(line.trim().substring(1).trim()) } else if (inInsightSection && line.trim() === '') { continue } else if (inInsightSection && !line.trim().startsWith('•') && !line.trim().startsWith('-')) { inInsightSection = false } } return insights } function extractIssues(text: string): string[] { const issues = [] const lines = text.split('\n') let inIssueSection = false for (const line of lines) { if ((line.toLowerCase().includes('issue') || line.toLowerCase().includes('problem') || line.toLowerCase().includes('error')) && line.includes(':')) { inIssueSection = true continue } if (inIssueSection && (line.trim().startsWith('•') || line.trim().startsWith('-'))) { issues.push(line.trim().substring(1).trim()) } else if (inIssueSection && line.trim() === '') { continue } else if (inIssueSection && !line.trim().startsWith('•') && !line.trim().startsWith('-')) { inIssueSection = false } } return issues } function extractRecommendations(text: string): string[] { const recommendations = [] const lines = text.split('\n') let inRecommendationSection = false for (const line of lines) { if ((line.toLowerCase().includes('recommend') || line.toLowerCase().includes('suggest')) && line.includes(':')) { inRecommendationSection = true continue } if (inRecommendationSection && (line.trim().startsWith('•') || line.trim().startsWith('-'))) { recommendations.push(line.trim().substring(1).trim()) } else if (inRecommendationSection && line.trim() === '') { continue } else if (inRecommendationSection && !line.trim().startsWith('•') && !line.trim().startsWith('-')) { inRecommendationSection = false } } return recommendations } function calculateConfidence(text: string): number { // Simple confidence calculation based on text length and detail const length = text.length if (length > 1000) return 0.9 if (length > 500) return 0.7 if (length > 200) return 0.5 return 0.3 } // Helper functions function detectLanguage(filePath: string): string { const ext = path.extname(filePath).toLowerCase() const languageMap: { [key: string]: string } = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.java': 'java', '.cpp': 'cpp', '.c': 'c', '.cs': 'csharp', '.rb': 'ruby', '.php': 'php', '.go': 'go', '.rs': 'rust', '.swift': 'swift', '.kt': 'kotlin', '.scala': 'scala', '.sh': 'bash', '.sql': 'sql', '.html': 'html', '.css': 'css', '.scss': 'scss', '.less': 'less', '.json': 'json', '.xml': 'xml', '.yaml': 'yaml', '.yml': 'yaml', '.md': 'markdown', '.vue': 'vue', '.svelte': 'svelte' } return languageMap[ext] || 'unknown' } function assessBasicComplexity(content: string): number { const lines = content.split('\n').length const functions = (content.match(/function\s+\w+|def\s+\w+|fn\s+\w+/g) || []).length const classes = (content.match(/class\s+\w+|struct\s+\w+/g) || []).length const conditions = (content.match(/\b(if|while|for|switch)\b/g) || []).length // Normalize to 0-1 scale return Math.min(1, (lines / 1000 + functions / 20 + classes / 10 + conditions / 50)) } interface DisplayOptions { showQuestions?: boolean analysisMode?: string } async function displayResults(results: any[], outputFormat: string, filePath: string, options: DisplayOptions = {}) { switch (outputFormat) { case 'json': await displayJsonResults(results) break case 'summary': await displaySummaryResults(results, filePath, options) break case 'questions': await displayQuestionResults(results, filePath, options) break case 'text': default: await displayTextResults(results, filePath, options) break } } async function displayJsonResults(results: any[]) { console.log(JSON.stringify(results, null, 2)) } async function displaySummaryResults(results: any[], filePath: string, options: DisplayOptions = {}) { prompts.log.info(`Analysis Summary for: ${path.basename(filePath)}`) if (options.analysisMode) { prompts.log.info(`Analysis Mode: ${options.analysisMode === 'question-driven' ? 'šŸ” Question-Driven' : 'šŸ”§ Legacy'}`) } let totalConfidence = 0 let issueCount = 0 let insightCount = 0 let questionCount = 0 let perspectiveCount = 0 for (const result of results) { if (result.confidence !== undefined) { totalConfidence += result.confidence } if (result.result) { if (result.result.issues) { issueCount += Array.isArray(result.result.issues) ? result.result.issues.length : 1 } if (result.result.insights) { insightCount += Array.isArray(result.result.insights) ? result.result.insights.length : 1 } } // Count question-driven specific metrics if (result.metadata?.questionPerspective) { perspectiveCount++ } if (result.metadata?.originalQuestion) { questionCount++ } } const avgConfidence = results.length > 0 ? totalConfidence / results.length : 0 prompts.log.info(`Average Confidence: ${Math.round(avgConfidence * 100)}%`) prompts.log.info(`Issues Found: ${issueCount}`) prompts.log.info(`Insights Generated: ${insightCount}`) prompts.log.info(`Analysis Methods: ${results.length}`) if (questionCount > 0) { prompts.log.info(`Questions Processed: ${questionCount}`) prompts.log.info(`Perspectives Analyzed: ${perspectiveCount}`) } } async function displayTextResults(results: any[], filePath: string, options: DisplayOptions = {}) { console.log(`\nšŸ“Š Multi-Agent Analysis Results for: ${path.basename(filePath)}`) if (options.analysisMode) { console.log(`Analysis Mode: ${options.analysisMode === 'question-driven' ? 'šŸ” Question-Driven Analysis' : 'šŸ”§ Legacy Analysis'}`) } console.log() for (let i = 0; i < results.length; i++) { const result = results[i] console.log(`${'='.repeat(50)}`) console.log(`Analysis ${i + 1}: ${result.type || 'Unknown Type'}`) console.log(`${'='.repeat(50)}`) if (result.confidence !== undefined) { const confidencePercent = Math.round(result.confidence * 100) const confidenceBar = 'ā–ˆ'.repeat(Math.floor(confidencePercent / 10)) + 'ā–‘'.repeat(10 - Math.floor(confidencePercent / 10)) console.log(`Confidence: ${confidencePercent}% [${confidenceBar}]`) } if (result.processingTime) { console.log(`Processing Time: ${result.processingTime}ms`) } if (result.reasoningMode) { console.log(`Reasoning Mode: ${result.reasoningMode}`) } if (result.agentType) { console.log(`Agent Type: ${result.agentType}`) } // Display question-driven specific information if (result.metadata?.questionPerspective) { console.log(`šŸ” Perspective: ${result.metadata.questionPerspective}`) } if (options.showQuestions && result.metadata?.originalQuestion) { console.log(`ā“ Question: ${result.metadata.originalQuestion}`) } console.log() // Display result content if (result.result) { displayResultContent(result.result, ' ') } // Display errors and warnings if (result.errors && result.errors.length > 0) { console.log(`āŒ Errors:`) result.errors.forEach((error: string) => { console.log(` • ${error}`) }) console.log() } if (result.warnings && result.warnings.length > 0) { console.log(`āš ļø Warnings:`) result.warnings.forEach((warning: string) => { console.log(` • ${warning}`) }) console.log() } console.log() } } async function displayQuestionResults(results: any[], filePath: string, _options: DisplayOptions = {}) { console.log(`\nšŸ” Question-Driven Analysis for: ${path.basename(filePath)}\n`) // Group results by perspective const resultsByPerspective: { [perspective: string]: any[] } = {} results.forEach(result => { const perspective = result.metadata?.questionPerspective || 'General Analysis' if (!resultsByPerspective[perspective]) { resultsByPerspective[perspective] = [] } resultsByPerspective[perspective].push(result) }) // Display results grouped by perspective for (const [perspective, perspectiveResults] of Object.entries(resultsByPerspective)) { console.log(`${'━'.repeat(60)}`) console.log(`šŸŽÆ ${perspective}`) console.log(`${'━'.repeat(60)}`) // Show the original question if available const sampleResult = perspectiveResults[0] if (sampleResult?.metadata?.originalQuestion) { console.log(`ā“ Question: ${sampleResult.metadata.originalQuestion}`) console.log() } // Calculate perspective metrics const avgConfidence = perspectiveResults.reduce((sum, r) => sum + (r.confidence || 0), 0) / perspectiveResults.length const totalProcessingTime = perspectiveResults.reduce((sum, r) => sum + (r.processingTime || 0), 0) console.log(`šŸ“Š Perspective Metrics:`) console.log(` • Results: ${perspectiveResults.length}`) console.log(` • Average Confidence: ${Math.round(avgConfidence * 100)}%`) console.log(` • Total Processing Time: ${totalProcessingTime}ms`) console.log() // Display insights from this perspective const allInsights: string[] = [] const allIssues: any[] = [] const allRecommendations: string[] = [] perspectiveResults.forEach(result => { if (result.result) { if (result.result.insights) { allInsights.push(...Array.isArray(result.result.insights) ? result.result.insights : [result.result.insights]) } if (result.result.issues) { allIssues.push(...Array.isArray(result.result.issues) ? result.result.issues : [result.result.issues]) } if (result.result.recommendations) { allRecommendations.push(...Array.isArray(result.result.recommendations) ? result.result.recommendations : [result.result.recommendations]) } } }) if (allInsights.length > 0) { console.log(`šŸ’” Key Insights:`) allInsights.forEach(insight => { console.log(` • ${insight}`) }) console.log() } if (allIssues.length > 0) { console.log(`šŸ” Issues Found:`) allIssues.forEach(issue => { if (typeof issue === 'string') { console.log(` • ${issue}`) } else if (issue.message) { console.log(` • ${issue.severity || 'INFO'}: ${issue.message}`) } }) console.log() } if (allRecommendations.length > 0) { console.log(`šŸ“‹ Recommendations:`) allRecommendations.forEach(rec => { console.log(` • ${rec}`) }) console.log() } console.log() } // Summary across all perspectives console.log(`${'═'.repeat(60)}`) console.log(`šŸ“ˆ Overall Analysis Summary`) console.log(`${'═'.repeat(60)}`) const totalConfidence = results.reduce((sum, r) => sum + (r.confidence || 0), 0) / results.length const totalProcessingTime = results.reduce((sum, r) => sum + (r.processingTime || 0), 0) const perspectiveCount = Object.keys(resultsByPerspective).length console.log(`• Perspectives Analyzed: ${perspectiveCount}`) console.log(`• Total Results: ${results.length}`) console.log(`• Average Confidence: ${Math.round(totalConfidence * 100)}%`) console.log(`• Total Processing Time: ${totalProcessingTime}ms`) console.log() } function displayResultContent(content: any, indent: string = '') { if (typeof content === 'string') { console.log(`${indent}${content}`) return } if (typeof content !== 'object' || content === null) { console.log(`${indent}${String(content)}`) return } // Handle specific result structures if (content.insights && Array.isArray(content.insights)) { console.log(`${indent}šŸ’” Insights:`) content.insights.forEach((insight: string) => { console.log(`${indent} • ${insight}`) }) console.log() } if (content.issues && Array.isArray(content.issues)) { console.log(`${indent}šŸ” Issues Found:`) content.issues.forEach((issue: any) => { if (typeof issue === 'string') { console.log(`${indent} • ${issue}`) } else if (issue.message) { console.log(`${indent} • ${issue.severity || 'INFO'}: ${issue.message