@@ -29,7 +29,8 @@ import {
2929 everyLineHasLineNumbers ,
3030} from "../integrations/misc/extract-text"
3131import { countFileLines } from "../integrations/misc/line-counter"
32- import { fetchInstructions } from "./prompts/instructions/instructions"
32+ import { fetchInstructionsTool } from "./tools/fetchInstructionsTool"
33+ import { readFileTool } from "./tools/readFileTool"
3334import { ExitCodeDetails } from "../integrations/terminal/TerminalProcess"
3435import { Terminal } from "../integrations/terminal/Terminal"
3536import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
@@ -82,11 +83,9 @@ import { insertGroups } from "./diff/insert-groups"
8283import { telemetryService } from "../services/telemetry/TelemetryService"
8384import { validateToolUse , isToolAllowedForMode , ToolName } from "./mode-validator"
8485import { parseXml } from "../utils/xml"
85- import { readLines } from "../integrations/misc/read-lines"
8686import { getWorkspacePath } from "../utils/path"
87- import { isBinaryFile } from "isbinaryfile"
8887
89- type ToolResponse = string | Array < Anthropic . TextBlockParam | Anthropic . ImageBlockParam >
88+ export type ToolResponse = string | Array < Anthropic . TextBlockParam | Anthropic . ImageBlockParam >
9089type UserContent = Array < Anthropic . Messages . ContentBlockParam >
9190
9291export type ClineEvents = {
@@ -148,9 +147,11 @@ export class Cline extends EventEmitter<ClineEvents> {
148147 private askResponseText ?: string
149148 private askResponseImages ?: string [ ]
150149 private lastMessageTs ?: number
151- private consecutiveMistakeCount : number = 0
150+ // Not private since it needs to be accessible by tools
151+ consecutiveMistakeCount : number = 0
152152 private consecutiveMistakeCountForApplyDiff : Map < string , number > = new Map ( )
153- private providerRef : WeakRef < ClineProvider >
153+ // Not private since it needs to be accessible by tools
154+ providerRef : WeakRef < ClineProvider >
154155 private abort : boolean = false
155156 didFinishAbortingStream = false
156157 abandoned = false
@@ -2254,207 +2255,13 @@ export class Cline extends EventEmitter<ClineEvents> {
22542255 }
22552256
22562257 case "read_file" : {
2257- const relPath : string | undefined = block . params . path
2258- const startLineStr : string | undefined = block . params . start_line
2259- const endLineStr : string | undefined = block . params . end_line
2260-
2261- // Get the full path and determine if it's outside the workspace
2262- const fullPath = relPath ? path . resolve ( this . cwd , removeClosingTag ( "path" , relPath ) ) : ""
2263- const isOutsideWorkspace = isPathOutsideWorkspace ( fullPath )
2264-
2265- const sharedMessageProps : ClineSayTool = {
2266- tool : "readFile" ,
2267- path : getReadablePath ( this . cwd , removeClosingTag ( "path" , relPath ) ) ,
2268- isOutsideWorkspace,
2269- }
2270- try {
2271- if ( block . partial ) {
2272- const partialMessage = JSON . stringify ( {
2273- ...sharedMessageProps ,
2274- content : undefined ,
2275- } satisfies ClineSayTool )
2276- await this . ask ( "tool" , partialMessage , block . partial ) . catch ( ( ) => { } )
2277- break
2278- } else {
2279- if ( ! relPath ) {
2280- this . consecutiveMistakeCount ++
2281- pushToolResult ( await this . sayAndCreateMissingParamError ( "read_file" , "path" ) )
2282- break
2283- }
2284-
2285- // Check if we're doing a line range read
2286- let isRangeRead = false
2287- let startLine : number | undefined = undefined
2288- let endLine : number | undefined = undefined
2289-
2290- // Check if we have either range parameter
2291- if ( startLineStr || endLineStr ) {
2292- isRangeRead = true
2293- }
2294-
2295- // Parse start_line if provided
2296- if ( startLineStr ) {
2297- startLine = parseInt ( startLineStr )
2298- if ( isNaN ( startLine ) ) {
2299- // Invalid start_line
2300- this . consecutiveMistakeCount ++
2301- await this . say ( "error" , `Failed to parse start_line: ${ startLineStr } ` )
2302- pushToolResult ( formatResponse . toolError ( "Invalid start_line value" ) )
2303- break
2304- }
2305- startLine -= 1 // Convert to 0-based index
2306- }
2307-
2308- // Parse end_line if provided
2309- if ( endLineStr ) {
2310- endLine = parseInt ( endLineStr )
2311-
2312- if ( isNaN ( endLine ) ) {
2313- // Invalid end_line
2314- this . consecutiveMistakeCount ++
2315- await this . say ( "error" , `Failed to parse end_line: ${ endLineStr } ` )
2316- pushToolResult ( formatResponse . toolError ( "Invalid end_line value" ) )
2317- break
2318- }
2319-
2320- // Convert to 0-based index
2321- endLine -= 1
2322- }
2323-
2324- const accessAllowed = this . rooIgnoreController ?. validateAccess ( relPath )
2325- if ( ! accessAllowed ) {
2326- await this . say ( "rooignore_error" , relPath )
2327- pushToolResult ( formatResponse . toolError ( formatResponse . rooIgnoreError ( relPath ) ) )
2328-
2329- break
2330- }
2331-
2332- this . consecutiveMistakeCount = 0
2333- const absolutePath = path . resolve ( this . cwd , relPath )
2334- const completeMessage = JSON . stringify ( {
2335- ...sharedMessageProps ,
2336- content : absolutePath ,
2337- } satisfies ClineSayTool )
2338-
2339- const didApprove = await askApproval ( "tool" , completeMessage )
2340- if ( ! didApprove ) {
2341- break
2342- }
2343-
2344- // Get the maxReadFileLine setting
2345- const { maxReadFileLine = 500 } = ( await this . providerRef . deref ( ) ?. getState ( ) ) ?? { }
2346-
2347- // Count total lines in the file
2348- let totalLines = 0
2349- try {
2350- totalLines = await countFileLines ( absolutePath )
2351- } catch ( error ) {
2352- console . error ( `Error counting lines in file ${ absolutePath } :` , error )
2353- }
2354-
2355- // now execute the tool like normal
2356- let content : string
2357- let isFileTruncated = false
2358- let sourceCodeDef = ""
2359-
2360- const isBinary = await isBinaryFile ( absolutePath ) . catch ( ( ) => false )
2361-
2362- if ( isRangeRead ) {
2363- if ( startLine === undefined ) {
2364- content = addLineNumbers ( await readLines ( absolutePath , endLine , startLine ) )
2365- } else {
2366- content = addLineNumbers (
2367- await readLines ( absolutePath , endLine , startLine ) ,
2368- startLine + 1 ,
2369- )
2370- }
2371- } else if ( ! isBinary && maxReadFileLine >= 0 && totalLines > maxReadFileLine ) {
2372- // If file is too large, only read the first maxReadFileLine lines
2373- isFileTruncated = true
2374-
2375- const res = await Promise . all ( [
2376- maxReadFileLine > 0 ? readLines ( absolutePath , maxReadFileLine - 1 , 0 ) : "" ,
2377- parseSourceCodeDefinitionsForFile ( absolutePath , this . rooIgnoreController ) ,
2378- ] )
2379-
2380- content = res [ 0 ] . length > 0 ? addLineNumbers ( res [ 0 ] ) : ""
2381- const result = res [ 1 ]
2382- if ( result ) {
2383- sourceCodeDef = `\n\n${ result } `
2384- }
2385- } else {
2386- // Read entire file
2387- content = await extractTextFromFile ( absolutePath )
2388- }
2389-
2390- // Add truncation notice if applicable
2391- if ( isFileTruncated ) {
2392- content += `\n\n[Showing only ${ maxReadFileLine } of ${ totalLines } total lines. Use start_line and end_line if you need to read more]${ sourceCodeDef } `
2393- }
2394-
2395- pushToolResult ( content )
2396- break
2397- }
2398- } catch ( error ) {
2399- await handleError ( "reading file" , error )
2400- break
2401- }
2258+ readFileTool ( this , block , askApproval , handleError , pushToolResult , removeClosingTag )
2259+ break
24022260 }
24032261
24042262 case "fetch_instructions" : {
2405- const task : string | undefined = block . params . task
2406- const sharedMessageProps : ClineSayTool = {
2407- tool : "fetchInstructions" ,
2408- content : task ,
2409- }
2410- try {
2411- if ( block . partial ) {
2412- const partialMessage = JSON . stringify ( {
2413- ...sharedMessageProps ,
2414- content : undefined ,
2415- } satisfies ClineSayTool )
2416- await this . ask ( "tool" , partialMessage , block . partial ) . catch ( ( ) => { } )
2417- break
2418- } else {
2419- if ( ! task ) {
2420- this . consecutiveMistakeCount ++
2421- pushToolResult (
2422- await this . sayAndCreateMissingParamError ( "fetch_instructions" , "task" ) ,
2423- )
2424- break
2425- }
2426-
2427- this . consecutiveMistakeCount = 0
2428- const completeMessage = JSON . stringify ( {
2429- ...sharedMessageProps ,
2430- content : task ,
2431- } satisfies ClineSayTool )
2432-
2433- const didApprove = await askApproval ( "tool" , completeMessage )
2434- if ( ! didApprove ) {
2435- break
2436- }
2437-
2438- // now fetch the content and provide it to the agent.
2439- const provider = this . providerRef . deref ( )
2440- const mcpHub = provider ?. getMcpHub ( )
2441- if ( ! mcpHub ) {
2442- throw new Error ( "MCP hub not available" )
2443- }
2444- const diffStrategy = this . diffStrategy
2445- const context = provider ?. context
2446- const content = await fetchInstructions ( task , { mcpHub, diffStrategy, context } )
2447- if ( ! content ) {
2448- pushToolResult ( formatResponse . toolError ( `Invalid instructions request: ${ task } ` ) )
2449- break
2450- }
2451- pushToolResult ( content )
2452- break
2453- }
2454- } catch ( error ) {
2455- await handleError ( "fetch instructions" , error )
2456- break
2457- }
2263+ fetchInstructionsTool ( this , block , askApproval , handleError , pushToolResult )
2264+ break
24582265 }
24592266
24602267 case "list_files" : {
@@ -2508,10 +2315,10 @@ export class Cline extends EventEmitter<ClineEvents> {
25082315 }
25092316 }
25102317 case "list_code_definition_names" : {
2511- const relDirPath : string | undefined = block . params . path
2318+ const relPath : string | undefined = block . params . path
25122319 const sharedMessageProps : ClineSayTool = {
25132320 tool : "listCodeDefinitionNames" ,
2514- path : getReadablePath ( this . cwd , removeClosingTag ( "path" , relDirPath ) ) ,
2321+ path : getReadablePath ( this . cwd , removeClosingTag ( "path" , relPath ) ) ,
25152322 }
25162323 try {
25172324 if ( block . partial ) {
@@ -2522,19 +2329,35 @@ export class Cline extends EventEmitter<ClineEvents> {
25222329 await this . ask ( "tool" , partialMessage , block . partial ) . catch ( ( ) => { } )
25232330 break
25242331 } else {
2525- if ( ! relDirPath ) {
2332+ if ( ! relPath ) {
25262333 this . consecutiveMistakeCount ++
25272334 pushToolResult (
25282335 await this . sayAndCreateMissingParamError ( "list_code_definition_names" , "path" ) ,
25292336 )
25302337 break
25312338 }
25322339 this . consecutiveMistakeCount = 0
2533- const absolutePath = path . resolve ( this . cwd , relDirPath )
2534- const result = await parseSourceCodeForDefinitionsTopLevel (
2535- absolutePath ,
2536- this . rooIgnoreController ,
2537- )
2340+ const absolutePath = path . resolve ( this . cwd , relPath )
2341+ let result : string
2342+ try {
2343+ const stats = await fs . stat ( absolutePath )
2344+ if ( stats . isFile ( ) ) {
2345+ const fileResult = await parseSourceCodeDefinitionsForFile (
2346+ absolutePath ,
2347+ this . rooIgnoreController ,
2348+ )
2349+ result = fileResult ?? "No source code definitions found in this file."
2350+ } else if ( stats . isDirectory ( ) ) {
2351+ result = await parseSourceCodeForDefinitionsTopLevel (
2352+ absolutePath ,
2353+ this . rooIgnoreController ,
2354+ )
2355+ } else {
2356+ result = "The specified path is neither a file nor a directory."
2357+ }
2358+ } catch {
2359+ result = `${ absolutePath } : does not exist or cannot be accessed.`
2360+ }
25382361 const completeMessage = JSON . stringify ( {
25392362 ...sharedMessageProps ,
25402363 content : result ,
0 commit comments