2026-03-07 13:05:58 -07:00
import { resolve } from "path"
2026-02-08 13:57:26 +09:00
import { tool , type ToolDefinition } from "@opencode-ai/plugin/tool"
import { DEFAULT_MAX_DIAGNOSTICS } from "./constants"
2026-03-07 13:05:58 -07:00
import { aggregateDiagnosticsForDirectory } from "./directory-diagnostics"
2026-02-08 13:57:26 +09:00
import { filterDiagnosticsBySeverity , formatDiagnostic } from "./lsp-formatters"
2026-03-07 13:05:58 -07:00
import { isDirectoryPath , withLspClient } from "./lsp-client-wrapper"
2026-02-08 13:57:26 +09:00
import type { Diagnostic } from "./types"
export const lsp_diagnostics : ToolDefinition = tool ( {
2026-03-07 13:05:58 -07:00
description :
2026-03-27 12:54:10 +09:00
'Get errors, warnings, hints from language server BEFORE running build. Use filePath for a single file, or filePath with extension for a directory. Do NOT pass both filePath and directory — use filePath for everything.' ,
2026-02-08 13:57:26 +09:00
args : {
2026-03-27 12:54:10 +09:00
filePath : tool.schema
. string ( )
. optional ( )
. describe ( "File or directory path to check diagnostics for" ) ,
directory : tool.schema
. string ( )
. optional ( )
. describe ( "Alias for filePath when checking a directory. Do NOT provide both filePath and directory." ) ,
2026-02-08 13:57:26 +09:00
severity : tool.schema
. enum ( [ "error" , "warning" , "information" , "hint" , "all" ] )
. optional ( )
. describe ( "Filter by severity level" ) ,
2026-03-07 13:05:58 -07:00
extension : tool.schema
. string ( )
. optional ( )
2026-03-27 12:54:10 +09:00
. describe ( "Required if target is a directory. E.g., '.ts', '.py', '.go', '.java'" ) ,
2026-02-08 13:57:26 +09:00
} ,
2026-02-16 22:12:21 +09:00
execute : async ( args , _context ) = > {
2026-02-08 13:57:26 +09:00
try {
2026-03-27 12:54:10 +09:00
// Accept either filePath or directory (treat directory as alias for filePath)
const targetPath = args . filePath || args . directory
if ( ! targetPath ) {
throw new Error ( "Provide either 'filePath' or 'directory' parameter." )
}
if ( args . filePath && args . directory ) {
// Instead of erroring, just use filePath and ignore directory
// This prevents model confusion from causing hard failures
}
const absPath = resolve ( targetPath )
2026-03-07 13:05:58 -07:00
if ( isDirectoryPath ( absPath ) ) {
if ( ! args . extension ) {
throw new Error (
` Directory path requires 'extension' parameter. \ n \ n ` +
` Example: lsp_diagnostics(filePath="src", extension=".ts") \ n \ n ` +
` Supported extensions: .ts, .tsx, .js, .py, .go, etc. `
)
}
return await aggregateDiagnosticsForDirectory ( absPath , args . extension , args . severity )
}
2026-03-27 12:54:10 +09:00
const result = await withLspClient ( targetPath , async ( client ) = > {
return ( await client . diagnostics ( targetPath ) ) as { items? : Diagnostic [ ] } | Diagnostic [ ] | null
2026-02-08 13:57:26 +09:00
} )
let diagnostics : Diagnostic [ ] = [ ]
if ( result ) {
if ( Array . isArray ( result ) ) {
diagnostics = result
} else if ( result . items ) {
diagnostics = result . items
}
}
diagnostics = filterDiagnosticsBySeverity ( diagnostics , args . severity )
if ( diagnostics . length === 0 ) {
const output = "No diagnostics found"
return output
}
const total = diagnostics . length
const truncated = total > DEFAULT_MAX_DIAGNOSTICS
const limited = truncated ? diagnostics . slice ( 0 , DEFAULT_MAX_DIAGNOSTICS ) : diagnostics
const lines = limited . map ( formatDiagnostic )
if ( truncated ) {
lines . unshift ( ` Found ${ total } diagnostics (showing first ${ DEFAULT_MAX_DIAGNOSTICS } ): ` )
}
const output = lines . join ( "\n" )
return output
} catch ( e ) {
const output = ` Error: ${ e instanceof Error ? e.message : String ( e ) } `
throw new Error ( output )
}
} ,
} )