package cc.unitmesh.agent import cc.unitmesh.agent.config.JsToolConfigFile import cc.unitmesh.agent.language.LanguageDetector import cc.unitmesh.agent.linter.LintFileResult import cc.unitmesh.agent.linter.LintIssue import cc.unitmesh.agent.linter.LintSeverity import cc.unitmesh.agent.render.DefaultCodingAgentRenderer import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.promise import kotlin.js.Promise /** * JS-friendly version of ReviewTask */ @JsExport data class JsReviewTask( val filePaths: Array, val reviewType: String = "COMPREHENSIVE", val projectPath: String, val additionalContext: String = "" ) { fun toCommon(): ReviewTask { val type = try { ReviewType.valueOf(reviewType.uppercase()) } catch (e: Exception) { ReviewType.COMPREHENSIVE } return ReviewTask( filePaths = filePaths.toList(), reviewType = type, projectPath = projectPath, additionalContext = additionalContext ) } } /** * JS-friendly version of ReviewFinding */ @JsExport data class JsReviewFinding( val severity: String, val category: String, val description: String, val filePath: String? = null, val lineNumber: Int? = null, val suggestion: String? = null ) { companion object { fun fromCommon(finding: ReviewFinding): JsReviewFinding { return JsReviewFinding( severity = finding.severity.name, category = finding.category, description = finding.description, filePath = finding.filePath, lineNumber = finding.lineNumber, suggestion = finding.suggestion ) } } } /** * JS-friendly version of CodeReviewResult */ @JsExport data class JsCodeReviewResult( val success: Boolean, val message: String, val findings: Array ) { companion object { fun fromCommon(result: CodeReviewResult): JsCodeReviewResult { return JsCodeReviewResult( success = result.success, message = result.message, findings = result.findings.map { JsReviewFinding.fromCommon(it) }.toTypedArray() ) } } } /** * JS-friendly version of LintResult */ @JsExport data class JsLintResult( val filePath: String, val issues: Array, val success: Boolean, val errorMessage: String? = null ) /** * JS-friendly version of LintIssue */ @JsExport data class JsLintIssue( val line: Int, val column: Int, val severity: String, val message: String, val rule: String? = null, val suggestion: String? = null ) /** * JS-friendly version of LintFileResult */ @JsExport data class JsLintFileResult( val filePath: String, val linterName: String, val errorCount: Int, val warningCount: Int, val infoCount: Int, val issues: Array ) /** * JS-friendly Code Review Agent for CLI usage */ @JsExport class JsCodeReviewAgent( private val projectPath: String, private val llmService: cc.unitmesh.llm.JsKoogLLMService, private val maxIterations: Int = 50, private val renderer: JsCodingAgentRenderer? = null, private val mcpServers: dynamic = null, private val toolConfig: JsToolConfigFile? = null ) { // Internal Kotlin CodeReviewAgent private val agent: CodeReviewAgent = CodeReviewAgent( projectPath = projectPath, llmService = llmService.service, maxIterations = maxIterations, renderer = if (renderer != null) JsRendererAdapter(renderer) else DefaultCodingAgentRenderer(), mcpToolConfigService = createToolConfigService(toolConfig) ) /** * Create tool config service from JS tool config */ private fun createToolConfigService(jsToolConfig: JsToolConfigFile?): cc.unitmesh.agent.config.McpToolConfigService { return if (jsToolConfig != null) { cc.unitmesh.agent.config.McpToolConfigService(jsToolConfig.toCommon()) } else { cc.unitmesh.agent.config.McpToolConfigService(cc.unitmesh.agent.config.ToolConfigFile()) } } /** * Execute code review task (main analysis method) * This is the unified method that replaces both analyzeIntent and analyzeWithDataDriven * * @param reviewType Type of review (e.g., "COMPREHENSIVE", "SECURITY") * @param filePaths Array of file paths to review * @param codeContent Object mapping file paths to their content * @param lintResults Object mapping file paths to their lint results (formatted strings) * @param diffContext Optional diff context string * @param commitMessage Optional commit message for intent analysis * @param commitId Optional commit ID * @param repoUrl Optional repository URL for issue tracking * @param issueToken Optional issue tracker token * @param useTools Whether to use tools for analysis (default: false for data-driven mode) * @param analyzeIntent Whether to perform intent analysis (default: false) * @param language Language for the prompt ("EN" or "ZH") * @param onChunk Optional callback for streaming response chunks * @return Promise resolving to JsAnalysisResult */ @JsName("executeTask") fun executeTask( reviewType: String, filePaths: Array = emptyArray(), codeContent: dynamic = null, lintResults: dynamic = null, diffContext: String = "", commitMessage: String = "", commitId: String = "", repoUrl: String = "", issueToken: String = "", useTools: Boolean = false, analyzeIntent: Boolean = false, language: String = "EN", onChunk: ((String) -> Unit)? = null ): Promise { return GlobalScope.promise { // Convert JS dynamic objects to Kotlin maps val codeContentMap = if (codeContent != null) { convertDynamicToMap(codeContent) } else { emptyMap() } val lintResultsMap = if (lintResults != null) { convertDynamicToMap(lintResults) } else { emptyMap() } // Create ReviewTask val reviewTypeEnum = try { ReviewType.valueOf(reviewType.uppercase()) } catch (e: Exception) { ReviewType.COMPREHENSIVE } val task = ReviewTask( filePaths = filePaths.toList(), reviewType = reviewTypeEnum, projectPath = projectPath, additionalContext = diffContext ) // Execute the task val result = agent.execute(task, onProgress = onChunk ?: {}) // Return unified result JsAnalysisResult( success = result.success, content = result.content, mermaidDiagram = null, // Not supported in basic execution issuesAnalyzed = emptyArray(), usedTools = false ) } } /** * Generate fixes for identified issues * Uses git patch, lint results, and analysis output to provide actionable fixes * * @param patch Git diff/patch content showing the changes * @param lintResults Array of JS lint results * @param analysisOutput The AI analysis output from previous analysis step * @param language Language for the prompt ("EN" or "ZH") * @param onChunk Optional callback for streaming response chunks * @return Promise resolving to the fix generation result as markdown string */ @JsName("generateFixes") fun generateFixes( patch: String, lintResults: Array, analysisOutput: String, language: String = "EN", onChunk: ((String) -> Unit)? = null ): Promise { return GlobalScope.promise { // Convert JS lint results to Kotlin LintFileResult val kotlinLintResults = lintResults.map { jsResult -> LintFileResult( filePath = jsResult.filePath, linterName = jsResult.linterName, errorCount = jsResult.errorCount, warningCount = jsResult.warningCount, infoCount = jsResult.infoCount, issues = jsResult.issues.map { jsIssue -> LintIssue( line = jsIssue.line, column = jsIssue.column, severity = when (jsIssue.severity.uppercase()) { "ERROR" -> LintSeverity.ERROR "WARNING" -> LintSeverity.WARNING else -> LintSeverity.INFO }, message = jsIssue.message, rule = jsIssue.rule, suggestion = jsIssue.suggestion ) } ) } val result = agent.generateFixes( patch = patch, lintResults = kotlinLintResults, analysisOutput = analysisOutput, language = language, onProgress = onChunk ?: {} ) result.content } } /** * Helper to convert JS dynamic object to Kotlin Map */ private fun convertDynamicToMap(obj: dynamic): Map { val map = mutableMapOf() if (obj != null && obj != undefined) { val keys = js("Object.keys(obj)") as Array for (key in keys) { val value = obj[key] if (value != null && value != undefined) { map[key] = value.toString() } } } return map } } /** * Export-friendly analysis result */ @JsExport data class JsAnalysisResult( val success: Boolean, val content: String, val mermaidDiagram: String?, val issuesAnalyzed: Array, val usedTools: Boolean ) /** * Diff-related exports for analyzing changes */ @JsExport data class JsDiffFile( val path: String, val oldPath: String?, val changeType: String, val addedLines: Int, val deletedLines: Int, val hunks: Array ) @JsExport data class JsDiffHunk( val oldStartLine: Int, val oldLineCount: Int, val newStartLine: Int, val newLineCount: Int, val header: String, val addedLines: Array, val deletedLines: Array, val contextLines: Array ) /** * Parse git diff and extract changed files */ @JsExport object JsDiffUtils { /** * Parse git diff output and return list of changed files * Simplified version that doesn't require mpp-ui dependency */ @JsName("parseDiff") fun parseDiff(diffContent: String): Array { val files = mutableListOf() val lines = diffContent.lines() var i = 0 while (i < lines.size) { val line = lines[i] if (line.startsWith("diff --git ")) { val regex = Regex("""diff --git a/(.*?) b/(.*?)$""") val match = regex.find(line) if (match != null) { val oldPath = match.groupValues[1] val newPath = match.groupValues[2] var changeType = "EDIT" // Look ahead for file status var j = i + 1 while (j < lines.size && j < i + 10) { when { lines[j].startsWith("new file mode") -> changeType = "CREATE" lines[j].startsWith("deleted file mode") -> changeType = "DELETE" lines[j].startsWith("@@") -> break } j++ } if (newPath != "/dev/null") { files.add(JsDiffFile( path = newPath, oldPath = if (oldPath != "/dev/null") oldPath else null, changeType = changeType, addedLines = 0, deletedLines = 0, hunks = emptyArray() )) } } } i++ } return files.toTypedArray() } /** * Extract file paths from diff */ @JsName("extractFilePaths") fun extractFilePaths(diffContent: String): Array { val paths = mutableSetOf() val lines = diffContent.lines() for (line in lines) { when { // Git diff format: diff --git a/path b/path line.startsWith("diff --git ") -> { val regex = Regex("""diff --git a/(.*?) b/(.*?)$""") val match = regex.find(line) if (match != null) { val path = match.groupValues[2] if (path != "/dev/null") { paths.add(path) } } } // Standard diff format: +++ b/path line.startsWith("+++ ") -> { var path = line.substring(4).trim() // Remove b/ prefix if present if (path.startsWith("b/")) { path = path.substring(2) } if (path != "/dev/null" && path.isNotEmpty()) { paths.add(path) } } } } return paths.toTypedArray() } } /** * Linter exports for JS */ @JsExport class JsLinterRegistry { private val registry = cc.unitmesh.agent.linter.LinterRegistry.getInstance() /** * Find suitable linters for files */ @JsName("findLintersForFiles") fun findLintersForFiles(filePaths: Array): Array { val linters = registry.findLintersForFiles(filePaths.toList()) return linters.map { it.name }.toTypedArray() } /** * Detect language from file path */ @JsName("detectLanguage") fun detectLanguage(filePath: String): String? { return LanguageDetector.detectLanguage(filePath) } /** * Get recommended linters for a language */ @JsName("getLintersForLanguage") fun getLintersForLanguage(language: String): Array { return LanguageDetector.getLinterNamesForLanguage(language).toTypedArray() } }