package cc.unitmesh.server.cli import cc.unitmesh.agent.AgentTask import cc.unitmesh.agent.CodingAgent import cc.unitmesh.agent.config.McpToolConfigService import cc.unitmesh.agent.config.ToolConfigFile import cc.unitmesh.agent.render.CodingAgentRenderer import cc.unitmesh.agent.tool.filesystem.DefaultToolFileSystem import cc.unitmesh.llm.LLMService import cc.unitmesh.llm.LLMProviderType import cc.unitmesh.llm.ModelConfig import cc.unitmesh.llm.compression.TokenInfo import com.charleskorn.kaml.Yaml import kotlinx.coroutines.runBlocking import java.io.File /** * JVM CLI for testing CodingAgent with autonomous coding tasks * * Usage: * ```bash * ./gradlew :mpp-ui:runCodingCli -PcodingProjectPath=/path/to/project -PcodingTask="Add a hello world function" * ``` */ object CodingCli { @JvmStatic fun main(args: Array) { println("=".repeat(80)) println("AutoDev Coding Agent CLI (JVM)") println("=".repeat(80)) // Parse arguments val projectPath = System.getProperty("projectPath") ?: args.getOrNull(0) ?: run { System.err.println("Usage: -PcodingProjectPath= -PcodingTask= [-PmaxIterations=100]") return } val task = System.getProperty("task") ?: args.getOrNull(1) ?: run { System.err.println("Usage: -PcodingProjectPath= -PcodingTask= [-PmaxIterations=100]") return } val maxIterations = System.getProperty("maxIterations")?.toIntOrNull() ?: 100 println("šŸ“‚ Project Path: $projectPath") println("šŸ“ Task: $task") println("šŸ”„ Max Iterations: $maxIterations") println() runBlocking { try { val projectDir = File(projectPath).absoluteFile if (!projectDir.exists()) { System.err.println("āŒ Project path does not exist: $projectPath") return@runBlocking } val startTime = System.currentTimeMillis() // Load configuration from ~/.autodev/config.yaml val configFile = File(System.getProperty("user.home"), ".autodev/config.yaml") if (!configFile.exists()) { System.err.println("āŒ Configuration file not found: ${configFile.absolutePath}") System.err.println(" Please create ~/.autodev/config.yaml with your LLM configuration") return@runBlocking } val yamlContent = configFile.readText() val yaml = Yaml(configuration = com.charleskorn.kaml.YamlConfiguration(strictMode = false)) val config = yaml.decodeFromString(AutoDevConfig.serializer(), yamlContent) val activeName = config.active val activeConfig = config.configs.find { it.name == activeName } if (activeConfig == null) { System.err.println("āŒ Active configuration '$activeName' not found in config.yaml") System.err.println(" Available configs: ${config.configs.map { it.name }.joinToString(", ")}") return@runBlocking } println("šŸ“ Using config: ${activeConfig.name} (${activeConfig.provider}/${activeConfig.model})") // Convert provider string to LLMProviderType val providerType = when (activeConfig.provider.lowercase()) { "openai" -> LLMProviderType.OPENAI "anthropic" -> LLMProviderType.ANTHROPIC "google" -> LLMProviderType.GOOGLE "deepseek" -> LLMProviderType.DEEPSEEK "ollama" -> LLMProviderType.OLLAMA "openrouter" -> LLMProviderType.OPENROUTER "glm" -> LLMProviderType.GLM "qwen" -> LLMProviderType.QWEN "kimi" -> LLMProviderType.KIMI else -> LLMProviderType.CUSTOM_OPENAI_BASE } val llmService = LLMService( ModelConfig( provider = providerType, modelName = activeConfig.model, apiKey = activeConfig.apiKey, temperature = activeConfig.temperature ?: 0.7, maxTokens = activeConfig.maxTokens ?: 4096, baseUrl = activeConfig.baseUrl ?: "" ) ) val renderer = CodingCliRenderer() val mcpConfigService = McpToolConfigService(ToolConfigFile()) println("🧠 Creating CodingAgent...") val agent = CodingAgent( projectPath = projectDir.absolutePath, llmService = llmService, maxIterations = maxIterations, renderer = renderer, fileSystem = DefaultToolFileSystem(projectDir.absolutePath), mcpToolConfigService = mcpConfigService, enableLLMStreaming = true ) println("āœ… Agent created") println() println("šŸš€ Executing task...") println() val result = agent.execute( AgentTask(requirement = task, projectPath = projectDir.absolutePath), onProgress = { progress -> println(" $progress") } ) val totalTime = System.currentTimeMillis() - startTime println() println("=".repeat(80)) println("šŸ“Š Result:") println("=".repeat(80)) println(result.content) println() if (result.success) { println("āœ… Task completed successfully") } else { println("āŒ Task failed") } println("ā±ļø Total time: ${totalTime}ms") println("šŸ“ˆ Steps: ${result.metadata["steps"] ?: "N/A"}") println("āœļø Edits: ${result.metadata["edits"] ?: "N/A"}") } catch (e: Exception) { System.err.println("āŒ Error: ${e.message}") e.printStackTrace() } } } } /** * Console renderer for CodingCli output */ class CodingCliRenderer : CodingAgentRenderer { // Track active sessions for awaitSessionResult private val activeSessions = mutableMapOf() data class SessionInfo( val sessionId: String, val command: String, val process: Process?, val startTime: Long ) // Thinking block state private var isInThinkBlock = false private val thinkingBuffer = StringBuilder() private val reasoningBuffer = StringBuilder() private var thinkingLineCount = 0 private val maxThinkingLines = 3 private val thinkingLines = mutableListOf() private var currentThinkingLine = StringBuilder() override fun renderIterationHeader(current: Int, max: Int) { println("\n━━━ Iteration $current/$max ━━━") } override fun renderLLMResponseStart() { println("šŸ’­ ") // Reset thinking state isInThinkBlock = false thinkingBuffer.clear() reasoningBuffer.clear() thinkingLines.clear() currentThinkingLine.clear() } override fun renderLLMResponseChunk(chunk: String) { reasoningBuffer.append(chunk) // Extract thinking content val extraction = extractThinkingContent(reasoningBuffer.toString()) // Render thinking content if present if (extraction.hasThinking) { val thinkContent = extraction.thinkingContent.toString() if (thinkContent.isNotEmpty()) { val wasInThinkBlock = isInThinkBlock isInThinkBlock = extraction.hasIncompleteThinkBlock renderThinkingChunk( thinkContent, isStart = !wasInThinkBlock && (extraction.hasCompleteThinkBlock || extraction.hasIncompleteThinkBlock), isEnd = extraction.hasCompleteThinkBlock && !extraction.hasIncompleteThinkBlock ) } } else if (isInThinkBlock && !extraction.hasIncompleteThinkBlock) { isInThinkBlock = false } // Output non-thinking content val cleanContent = extraction.contentWithoutThinking if (cleanContent.isNotEmpty()) { print(cleanContent) System.out.flush() } } override fun renderLLMResponseEnd() { println("\n") } override fun renderThinkingChunk(chunk: String, isStart: Boolean, isEnd: Boolean) { if (isStart) { // Start of thinking block - show header with gray color thinkingLineCount = 0 thinkingLines.clear() currentThinkingLine.clear() print("\u001B[90m🧠 Thinking: ") // Gray color } // Process chunk character by character to handle line breaks for (char in chunk) { if (char == '\n') { // Complete current line thinkingLines.add(currentThinkingLine.toString()) currentThinkingLine.clear() thinkingLineCount++ // Keep only last N lines for scrolling effect if (thinkingLines.size > maxThinkingLines) { thinkingLines.removeAt(0) } // Clear line and reprint last line (scrolling effect) print("\r\u001B[K") // Clear current line print("\u001B[90m🧠 ${thinkingLines.lastOrNull() ?: ""}") } else { currentThinkingLine.append(char) // Print character in gray print("\u001B[90m$char") } } System.out.flush() if (isEnd) { // End of thinking block - reset color and add newline println("\u001B[0m") // Reset color thinkingLines.clear() currentThinkingLine.clear() } } /** * Extract thinking content from the buffer. * Returns a result with content separated from thinking blocks. */ private fun extractThinkingContent(content: String): ThinkingExtractionResult { val result = ThinkingExtractionResult() var remaining = content // Handle complete ... blocks val completeThinkPattern = Regex("([\\s\\S]*?)") val matches = completeThinkPattern.findAll(remaining) for (match in matches) { result.thinkingContent.append(match.groupValues[1]) result.hasCompleteThinkBlock = true } remaining = remaining.replace(completeThinkPattern, "") // Check for incomplete block at the end val openThinkIndex = remaining.lastIndexOf("") if (openThinkIndex != -1) { val closeThinkIndex = remaining.indexOf("", openThinkIndex) if (closeThinkIndex == -1) { // Incomplete think block - extract content after val thinkContent = remaining.substring(openThinkIndex + 7) result.thinkingContent.append(thinkContent) result.hasIncompleteThinkBlock = true remaining = remaining.substring(0, openThinkIndex) } } // Check for partial println(" āŽæ $line") } } } override fun addLiveTerminal( sessionId: String, command: String, workingDirectory: String?, ptyHandle: Any? ) { val process = ptyHandle as? Process activeSessions[sessionId] = SessionInfo( sessionId = sessionId, command = command, process = process, startTime = System.currentTimeMillis() ) println(" ā³ Running: $command") } override fun updateLiveTerminalStatus( sessionId: String, exitCode: Int, executionTimeMs: Long, output: String?, cancelledByUser: Boolean ) { activeSessions.remove(sessionId) val statusSymbol = when { cancelledByUser -> "⚠" exitCode == 0 -> "āœ“" else -> "āœ—" } val statusMessage = if (cancelledByUser) "Cancelled by user" else "Exit code: $exitCode" val preview = (output ?: "").lines().take(3).joinToString(" ").take(100) println(" $statusSymbol $statusMessage (${executionTimeMs}ms)") if (preview.isNotEmpty()) { println(" $preview${if (preview.length < (output ?: "").length) "..." else ""}") } } override suspend fun awaitSessionResult(sessionId: String, timeoutMs: Long): cc.unitmesh.agent.tool.ToolResult { val session = activeSessions[sessionId] if (session == null) { // Session not found - check ShellSessionManager val managedSession = cc.unitmesh.agent.tool.shell.ShellSessionManager.getSession(sessionId) if (managedSession != null) { return awaitManagedSession(managedSession, timeoutMs) } return cc.unitmesh.agent.tool.ToolResult.Error("Session not found: $sessionId") } val process = session.process if (process == null) { return cc.unitmesh.agent.tool.ToolResult.Error("No process handle for session: $sessionId") } return awaitProcess(process, session, timeoutMs) } private suspend fun awaitManagedSession( session: cc.unitmesh.agent.tool.shell.ManagedSession, timeoutMs: Long ): cc.unitmesh.agent.tool.ToolResult { val process = session.processHandle as? Process if (process == null) { return cc.unitmesh.agent.tool.ToolResult.Error("No process handle for session: ${session.sessionId}") } val startWait = System.currentTimeMillis() val checkIntervalMs = 100L while (process.isAlive) { val elapsed = System.currentTimeMillis() - startWait if (elapsed >= timeoutMs) { // Timeout - process still running val output = session.getOutput() return cc.unitmesh.agent.tool.ToolResult.Pending( sessionId = session.sessionId, toolName = "shell", command = session.command, message = "Process still running after ${elapsed}ms", metadata = mapOf( "partial_output" to output.take(1000), "elapsed_ms" to elapsed.toString() ) ) } kotlinx.coroutines.delay(checkIntervalMs) } // Process completed val exitCode = process.exitValue() val output = session.getOutput() val wasCancelledByUser = session.cancelledByUser val executionTimeMs = System.currentTimeMillis() - startWait session.markCompleted(exitCode) return when { wasCancelledByUser -> { // User cancelled the command - return a special result with output cc.unitmesh.agent.tool.ToolResult.Error( message = buildCancelledMessage(session.command, exitCode, output), errorType = "CANCELLED_BY_USER", metadata = mapOf( "exit_code" to exitCode.toString(), "execution_time_ms" to executionTimeMs.toString(), "session_id" to session.sessionId, "cancelled" to "true", "output" to output ) ) } exitCode == 0 -> { cc.unitmesh.agent.tool.ToolResult.Success( content = output.ifEmpty { "(no output)" }, metadata = mapOf( "exit_code" to exitCode.toString(), "execution_time_ms" to executionTimeMs.toString(), "session_id" to session.sessionId ) ) } else -> { cc.unitmesh.agent.tool.ToolResult.Error( message = "Command failed with exit code $exitCode:\n$output", errorType = cc.unitmesh.agent.tool.ToolErrorType.COMMAND_FAILED.code, metadata = mapOf( "exit_code" to exitCode.toString(), "execution_time_ms" to executionTimeMs.toString(), "session_id" to session.sessionId ) ) } } } /** * Build a consistent cancelled message for user-cancelled commands. */ private fun buildCancelledMessage(command: String, exitCode: Int, output: String): String = buildString { appendLine("āš ļø Command cancelled by user") appendLine() appendLine("Command: $command") appendLine("Exit code: $exitCode (SIGKILL)") appendLine() if (output.isNotEmpty()) { appendLine("Output before cancellation:") appendLine(output) } else { appendLine("(no output captured before cancellation)") } } private suspend fun awaitProcess( process: Process, session: SessionInfo, timeoutMs: Long ): cc.unitmesh.agent.tool.ToolResult { val startWait = System.currentTimeMillis() val checkIntervalMs = 100L val outputBuilder = StringBuilder() // Read output in background val stdoutReader = process.inputStream.bufferedReader() val stderrReader = process.errorStream.bufferedReader() while (process.isAlive) { // Read available output while (stdoutReader.ready()) { val line = stdoutReader.readLine() ?: break outputBuilder.appendLine(line) println(" │ $line") } while (stderrReader.ready()) { val line = stderrReader.readLine() ?: break outputBuilder.appendLine("[stderr] $line") println(" │ [stderr] $line") } val elapsed = System.currentTimeMillis() - startWait if (elapsed >= timeoutMs) { // Timeout - process still running return cc.unitmesh.agent.tool.ToolResult.Pending( sessionId = session.sessionId, toolName = "shell", command = session.command, message = "Process still running after ${elapsed}ms", metadata = mapOf( "partial_output" to outputBuilder.toString().take(1000), "elapsed_ms" to elapsed.toString() ) ) } kotlinx.coroutines.delay(checkIntervalMs) } // Read remaining output stdoutReader.forEachLine { line -> outputBuilder.appendLine(line) println(" │ $line") } stderrReader.forEachLine { line -> outputBuilder.appendLine("[stderr] $line") println(" │ [stderr] $line") } // Process completed val exitCode = process.exitValue() val output = outputBuilder.toString() activeSessions.remove(session.sessionId) // Check if cancelled by user from ShellSessionManager val managedSession = cc.unitmesh.agent.tool.shell.ShellSessionManager.getSession(session.sessionId) val wasCancelledByUser = managedSession?.cancelledByUser == true val executionTimeMs = System.currentTimeMillis() - session.startTime val statusSymbol = when { wasCancelledByUser -> "⚠" exitCode == 0 -> "āœ“" else -> "āœ—" } println(" $statusSymbol Exit code: $exitCode (${executionTimeMs}ms)${if (wasCancelledByUser) " [Cancelled by user]" else ""}") return when { wasCancelledByUser -> { cc.unitmesh.agent.tool.ToolResult.Error( message = buildCancelledMessage(session.command, exitCode, output), errorType = "CANCELLED_BY_USER", metadata = mapOf( "exit_code" to exitCode.toString(), "session_id" to session.sessionId, "execution_time_ms" to executionTimeMs.toString(), "cancelled" to "true", "output" to output ) ) } exitCode == 0 -> { cc.unitmesh.agent.tool.ToolResult.Success( content = output.ifEmpty { "(no output)" }, metadata = mapOf( "exit_code" to exitCode.toString(), "session_id" to session.sessionId, "execution_time_ms" to executionTimeMs.toString() ) ) } else -> { cc.unitmesh.agent.tool.ToolResult.Error( message = "Command failed with exit code $exitCode:\n$output", errorType = cc.unitmesh.agent.tool.ToolErrorType.COMMAND_FAILED.code, metadata = mapOf( "exit_code" to exitCode.toString(), "session_id" to session.sessionId, "execution_time_ms" to executionTimeMs.toString() ) ) } } } private fun formatCliParameters(params: String): String { val trimmed = params.trim() // Handle JSON format if (trimmed.startsWith("{") && trimmed.endsWith("}")) { val lines = mutableListOf() val jsonPattern = Regex(""""(\w+)"\s*:\s*("([^"]*)"|(\d+)|true|false|null)""") jsonPattern.findAll(trimmed).forEach { match -> val key = match.groups[1]?.value ?: "" val value = match.groups[3]?.value ?: match.groups[4]?.value ?: match.groups[2]?.value?.removeSurrounding("\"") ?: "" lines.add("$key = $value") } return if (lines.isNotEmpty()) lines.joinToString(", ") else params } return params } override fun renderToolResult( toolName: String, success: Boolean, output: String?, fullOutput: String?, metadata: Map ) { val statusSymbol = if (success) "āœ“" else "āœ—" val preview = (output ?: fullOutput ?: "").lines().take(3).joinToString(" ").take(100) println(" $statusSymbol ${if (preview.length < (output ?: fullOutput ?: "").length) "$preview..." else preview}") } override fun renderTaskComplete(executionTimeMs: Long, toolsUsedCount: Int) { val parts = mutableListOf() if (executionTimeMs > 0) { val seconds = executionTimeMs / 1000.0 val rounded = (seconds * 100).toLong() / 100.0 parts.add("${rounded}s") } if (toolsUsedCount > 0) { parts.add("$toolsUsedCount tools") } val info = if (parts.isNotEmpty()) " (${parts.joinToString(", ")})" else "" println("\nāœ“ Task marked as complete$info") } override fun renderFinalResult(success: Boolean, message: String, iterations: Int) { val symbol = if (success) "āœ…" else "āŒ" println("\n$symbol Final result after $iterations iterations:") println(message) } override fun renderError(message: String) { System.err.println("āŒ Error: $message") } override fun renderRepeatWarning(toolName: String, count: Int) { println("āš ļø Warning: Tool '$toolName' called $count times") } override fun renderRecoveryAdvice(recoveryAdvice: String) { println("šŸ’” Recovery advice: $recoveryAdvice") } override fun updateTokenInfo(tokenInfo: TokenInfo) { // Display token info in CLI println("šŸ“Š Tokens: ${tokenInfo.inputTokens} in / ${tokenInfo.outputTokens} out") } override fun renderUserConfirmationRequest(toolName: String, params: Map) { println("ā“ Confirmation required for: $toolName") println(" Params: $params") } override fun renderAgentSketchBlock( agentName: String, language: String, code: String, metadata: Map ) { println() println("šŸ“Š Agent Sketch Block [$agentName]") println("━".repeat(50)) println("```$language") println(code) println("```") println("━".repeat(50)) } }