2025-04-19 19:45:42 +01:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-06-08 18:56:58 +01:00
import * as fs from 'fs' ;
import * as path from 'path' ;
2025-04-19 19:45:42 +01:00
import * as Diff from 'diff' ;
2025-04-21 10:53:11 -04:00
import {
2025-08-07 10:05:37 -07:00
BaseDeclarativeTool ,
2025-08-13 12:58:26 -03:00
Kind ,
2025-04-21 10:53:11 -04:00
ToolCallConfirmationDetails ,
ToolConfirmationOutcome ,
ToolEditConfirmationDetails ,
2025-08-07 10:05:37 -07:00
ToolInvocation ,
2025-07-17 16:25:23 -06:00
ToolLocation ,
2025-04-21 10:53:11 -04:00
ToolResult ,
ToolResultDisplay ,
} from './tools.js' ;
2025-08-01 11:20:08 -04:00
import { ToolErrorType } from './tool-error.js' ;
2025-04-19 19:45:42 +01:00
import { makeRelative , shortenPath } from '../utils/paths.js' ;
import { isNodeError } from '../utils/errors.js' ;
2025-06-02 22:05:45 +02:00
import { Config , ApprovalMode } from '../config/config.js' ;
2025-05-25 14:16:08 -07:00
import { ensureCorrectEdit } from '../utils/editCorrector.js' ;
2025-08-08 10:08:07 +05:30
import { DEFAULT_DIFF_OPTIONS , getDiffStat } from './diffOptions.js' ;
2025-06-06 22:54:37 -07:00
import { ReadFileTool } from './read-file.js' ;
2025-08-06 10:50:02 -07:00
import { ModifiableDeclarativeTool , ModifyContext } from './modifiable-tool.js' ;
2025-08-06 20:55:29 +00:00
import { IDEConnectionStatus } from '../ide/ide-client.js' ;
2025-08-22 17:47:32 +05:30
import { FileOperation } from '../telemetry/metrics.js' ;
import { logFileOperation } from '../telemetry/loggers.js' ;
import { FileOperationEvent } from '../telemetry/types.js' ;
import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js' ;
import { getSpecificMimeType } from '../utils/fileUtils.js' ;
2025-04-19 19:45:42 +01:00
2025-08-07 10:05:37 -07:00
export function applyReplacement (
currentContent : string | null ,
oldString : string ,
newString : string ,
isNewFile : boolean ,
) : string {
if ( isNewFile ) {
return newString ;
}
if ( currentContent === null ) {
// Should not happen if not a new file, but defensively return empty or newString if oldString is also empty
return oldString === '' ? newString : '' ;
}
// If oldString is empty and it's not a new file, do not modify the content.
if ( oldString === '' && ! isNewFile ) {
return currentContent ;
}
return currentContent . replaceAll ( oldString , newString ) ;
}
2025-04-19 19:45:42 +01:00
/ * *
* Parameters for the Edit tool
* /
export interface EditToolParams {
/ * *
* The absolute path to the file to modify
* /
file_path : string ;
/ * *
2025-06-08 16:20:43 -07:00
* The text to replace
2025-04-19 19:45:42 +01:00
* /
2025-06-08 16:20:43 -07:00
old_string : string ;
/ * *
* The text to replace it with
* /
new_string : string ;
2025-06-01 17:49:48 -04:00
/ * *
* Number of replacements expected . Defaults to 1 if not specified .
* Use when you want to replace multiple occurrences .
* /
expected_replacements? : number ;
2025-06-28 19:02:44 +01:00
/ * *
* Whether the edit was modified manually by the user .
* /
modified_by_user? : boolean ;
2025-08-08 10:08:07 +05:30
/ * *
* Initially proposed string .
* /
ai_proposed_string? : string ;
2025-04-19 19:45:42 +01:00
}
2025-06-08 16:20:43 -07:00
interface CalculatedEdit {
currentContent : string | null ;
newContent : string ;
occurrences : number ;
2025-08-01 11:20:08 -04:00
error ? : { display : string ; raw : string ; type : ToolErrorType } ;
2025-06-08 16:20:43 -07:00
isNewFile : boolean ;
2025-04-19 19:45:42 +01:00
}
2025-08-07 10:05:37 -07:00
class EditToolInvocation implements ToolInvocation < EditToolParams , ToolResult > {
constructor (
private readonly config : Config ,
public params : EditToolParams ,
) { }
2025-04-19 19:45:42 +01:00
2025-08-07 10:05:37 -07:00
toolLocations ( ) : ToolLocation [ ] {
return [ { path : this.params.file_path } ] ;
2025-05-25 14:16:08 -07:00
}
2025-04-19 19:45:42 +01:00
/ * *
2025-06-08 16:20:43 -07:00
* Calculates the potential outcome of an edit operation .
* @param params Parameters for the edit operation
* @returns An object describing the potential edit outcome
* @throws File system errors if reading the file fails unexpectedly ( e . g . , permissions )
2025-04-19 19:45:42 +01:00
* /
2025-06-08 16:20:43 -07:00
private async calculateEdit (
2025-05-27 23:40:25 -07:00
params : EditToolParams ,
abortSignal : AbortSignal ,
2025-06-08 16:20:43 -07:00
) : Promise < CalculatedEdit > {
const expectedReplacements = params . expected_replacements ? ? 1 ;
2025-04-19 19:45:42 +01:00
let currentContent : string | null = null ;
let fileExists = false ;
let isNewFile = false ;
2025-06-08 16:20:43 -07:00
let finalNewString = params . new_string ;
let finalOldString = params . old_string ;
let occurrences = 0 ;
2025-08-01 11:20:08 -04:00
let error :
| { display : string ; raw : string ; type : ToolErrorType }
| undefined = undefined ;
2025-04-19 19:45:42 +01:00
try {
2025-08-18 16:29:45 -06:00
currentContent = await this . config
. getFileSystemService ( )
. readTextFile ( params . file_path ) ;
2025-06-09 12:19:42 -07:00
// Normalize line endings to LF for consistent processing.
currentContent = currentContent . replace ( /\r\n/g , '\n' ) ;
2025-04-19 19:45:42 +01:00
fileExists = true ;
} catch ( err : unknown ) {
if ( ! isNodeError ( err ) || err . code !== 'ENOENT' ) {
2025-06-08 16:20:43 -07:00
// Rethrow unexpected FS errors (permissions, etc.)
2025-04-19 19:45:42 +01:00
throw err ;
}
2025-06-08 16:20:43 -07:00
fileExists = false ;
2025-04-19 19:45:42 +01:00
}
2025-06-08 16:20:43 -07:00
if ( params . old_string === '' && ! fileExists ) {
// Creating a new file
2025-04-19 19:45:42 +01:00
isNewFile = true ;
} else if ( ! fileExists ) {
2025-07-21 17:54:44 -04:00
// Trying to edit a nonexistent file (and old_string is not empty)
2025-06-08 16:20:43 -07:00
error = {
display : ` File not found. Cannot apply edit. Use an empty old_string to create a new file. ` ,
raw : ` File not found: ${ params . file_path } ` ,
2025-08-01 11:20:08 -04:00
type : ToolErrorType . FILE_NOT_FOUND ,
2025-06-08 16:20:43 -07:00
} ;
} else if ( currentContent !== null ) {
// Editing an existing file
const correctedEdit = await ensureCorrectEdit (
2025-07-07 10:28:56 -07:00
params . file_path ,
2025-06-08 16:20:43 -07:00
currentContent ,
params ,
2025-07-07 15:01:59 -07:00
this . config . getGeminiClient ( ) ,
2025-06-08 16:20:43 -07:00
abortSignal ,
) ;
finalOldString = correctedEdit . params . old_string ;
finalNewString = correctedEdit . params . new_string ;
occurrences = correctedEdit . occurrences ;
if ( params . old_string === '' ) {
// Error: Trying to create a file that already exists
error = {
display : ` Failed to edit. Attempted to create a file that already exists. ` ,
raw : ` File already exists, cannot create: ${ params . file_path } ` ,
2025-08-01 11:20:08 -04:00
type : ToolErrorType . ATTEMPT_TO_CREATE_EXISTING_FILE ,
2025-06-08 16:20:43 -07:00
} ;
} else if ( occurrences === 0 ) {
error = {
display : ` Failed to edit, could not find the string to replace. ` ,
raw : ` Failed to edit, 0 occurrences found for old_string in ${ params . file_path } . No edits made. The exact text in old_string was not found. Ensure you're not escaping content incorrectly and check whitespace, indentation, and context. Use ${ ReadFileTool . Name } tool to verify. ` ,
2025-08-01 11:20:08 -04:00
type : ToolErrorType . EDIT_NO_OCCURRENCE_FOUND ,
2025-06-08 16:20:43 -07:00
} ;
} else if ( occurrences !== expectedReplacements ) {
2025-07-21 17:54:44 -04:00
const occurrenceTerm =
2025-06-29 20:53:59 +01:00
expectedReplacements === 1 ? 'occurrence' : 'occurrences' ;
2025-06-08 16:20:43 -07:00
error = {
2025-07-21 17:54:44 -04:00
display : ` Failed to edit, expected ${ expectedReplacements } ${ occurrenceTerm } but found ${ occurrences } . ` ,
raw : ` Failed to edit, Expected ${ expectedReplacements } ${ occurrenceTerm } but found ${ occurrences } for old_string in file: ${ params . file_path } ` ,
2025-08-01 11:20:08 -04:00
type : ToolErrorType . EDIT_EXPECTED_OCCURRENCE_MISMATCH ,
2025-06-08 16:20:43 -07:00
} ;
2025-07-22 06:24:02 +09:00
} else if ( finalOldString === finalNewString ) {
error = {
display : ` No changes to apply. The old_string and new_string are identical. ` ,
raw : ` No changes to apply. The old_string and new_string are identical in file: ${ params . file_path } ` ,
2025-08-01 11:20:08 -04:00
type : ToolErrorType . EDIT_NO_CHANGE ,
2025-07-22 06:24:02 +09:00
} ;
2025-06-08 16:20:43 -07:00
}
} else {
// Should not happen if fileExists and no exception was thrown, but defensively:
error = {
display : ` Failed to read content of file. ` ,
raw : ` Failed to read content of existing file: ${ params . file_path } ` ,
2025-08-01 11:20:08 -04:00
type : ToolErrorType . READ_CONTENT_FAILURE ,
2025-06-08 16:20:43 -07:00
} ;
2025-04-19 19:45:42 +01:00
}
2025-08-07 10:05:37 -07:00
const newContent = applyReplacement (
2025-06-08 16:20:43 -07:00
currentContent ,
finalOldString ,
finalNewString ,
isNewFile ,
) ;
2025-05-25 14:16:08 -07:00
2025-06-08 16:20:43 -07:00
return {
currentContent ,
newContent ,
occurrences ,
error ,
2025-04-19 19:45:42 +01:00
isNewFile ,
} ;
}
2025-04-21 10:53:11 -04:00
/ * *
* Handles the confirmation prompt for the Edit tool in the CLI .
* It needs to calculate the diff to show the user .
* /
async shouldConfirmExecute (
2025-05-27 23:40:25 -07:00
abortSignal : AbortSignal ,
2025-04-21 10:53:11 -04:00
) : Promise < ToolCallConfirmationDetails | false > {
2025-06-02 22:05:45 +02:00
if ( this . config . getApprovalMode ( ) === ApprovalMode . AUTO_EDIT ) {
2025-04-21 10:53:11 -04:00
return false ;
}
2025-05-25 14:16:08 -07:00
2025-06-10 18:05:11 +00:00
let editData : CalculatedEdit ;
2025-04-21 10:53:11 -04:00
try {
2025-08-07 10:05:37 -07:00
editData = await this . calculateEdit ( this . params , abortSignal ) ;
2025-06-10 18:05:11 +00:00
} catch ( error ) {
const errorMsg = error instanceof Error ? error.message : String ( error ) ;
console . log ( ` Error preparing edit: ${ errorMsg } ` ) ;
return false ;
2025-06-08 16:20:43 -07:00
}
2025-05-12 23:23:24 -07:00
2025-06-10 18:05:11 +00:00
if ( editData . error ) {
console . log ( ` Error: ${ editData . error . display } ` ) ;
return false ;
2025-06-06 22:54:37 -07:00
}
2025-06-08 16:20:43 -07:00
2025-08-07 10:05:37 -07:00
const fileName = path . basename ( this . params . file_path ) ;
2025-06-08 16:20:43 -07:00
const fileDiff = Diff . createPatch (
fileName ,
2025-06-10 18:05:11 +00:00
editData . currentContent ? ? '' ,
editData . newContent ,
2025-06-08 16:20:43 -07:00
'Current' ,
'Proposed' ,
DEFAULT_DIFF_OPTIONS ,
) ;
2025-08-06 20:55:29 +00:00
const ideClient = this . config . getIdeClient ( ) ;
const ideConfirmation =
this . config . getIdeMode ( ) &&
ideClient ? . getConnectionStatus ( ) . status === IDEConnectionStatus . Connected
2025-08-07 10:05:37 -07:00
? ideClient . openDiff ( this . params . file_path , editData . newContent )
2025-08-06 20:55:29 +00:00
: undefined ;
2025-06-08 16:20:43 -07:00
const confirmationDetails : ToolEditConfirmationDetails = {
type : 'edit' ,
2025-08-07 10:05:37 -07:00
title : ` Confirm Edit: ${ shortenPath ( makeRelative ( this . params . file_path , this . config . getTargetDir ( ) ) ) } ` ,
2025-06-08 16:20:43 -07:00
fileName ,
2025-08-07 10:05:37 -07:00
filePath : this.params.file_path ,
2025-06-08 16:20:43 -07:00
fileDiff ,
2025-07-17 16:25:23 -06:00
originalContent : editData.currentContent ,
newContent : editData.newContent ,
2025-06-08 16:20:43 -07:00
onConfirm : async ( outcome : ToolConfirmationOutcome ) = > {
if ( outcome === ToolConfirmationOutcome . ProceedAlways ) {
this . config . setApprovalMode ( ApprovalMode . AUTO_EDIT ) ;
}
2025-08-06 20:55:29 +00:00
if ( ideConfirmation ) {
const result = await ideConfirmation ;
if ( result . status === 'accepted' && result . content ) {
// TODO(chrstn): See https://github.com/google-gemini/gemini-cli/pull/5618#discussion_r2255413084
// for info on a possible race condition where the file is modified on disk while being edited.
2025-08-07 10:05:37 -07:00
this . params . old_string = editData . currentContent ? ? '' ;
this . params . new_string = result . content ;
2025-08-06 20:55:29 +00:00
}
}
2025-06-08 16:20:43 -07:00
} ,
2025-08-06 20:55:29 +00:00
ideConfirmation ,
2025-06-08 16:20:43 -07:00
} ;
return confirmationDetails ;
2025-04-21 10:53:11 -04:00
}
2025-04-19 19:45:42 +01:00
2025-08-07 10:05:37 -07:00
getDescription ( ) : string {
2025-07-14 22:55:49 -07:00
const relativePath = makeRelative (
2025-08-07 10:05:37 -07:00
this . params . file_path ,
2025-07-14 22:55:49 -07:00
this . config . getTargetDir ( ) ,
) ;
2025-08-07 10:05:37 -07:00
if ( this . params . old_string === '' ) {
2025-06-08 16:20:43 -07:00
return ` Create ${ shortenPath ( relativePath ) } ` ;
2025-06-06 22:54:37 -07:00
}
2025-06-03 08:59:17 -07:00
2025-06-08 16:20:43 -07:00
const oldStringSnippet =
2025-08-07 10:05:37 -07:00
this . params . old_string . split ( '\n' ) [ 0 ] . substring ( 0 , 30 ) +
( this . params . old_string . length > 30 ? '...' : '' ) ;
2025-06-08 16:20:43 -07:00
const newStringSnippet =
2025-08-07 10:05:37 -07:00
this . params . new_string . split ( '\n' ) [ 0 ] . substring ( 0 , 30 ) +
( this . params . new_string . length > 30 ? '...' : '' ) ;
2025-06-08 16:20:43 -07:00
2025-08-07 10:05:37 -07:00
if ( this . params . old_string === this . params . new_string ) {
2025-06-08 16:20:43 -07:00
return ` No file changes to ${ shortenPath ( relativePath ) } ` ;
2025-06-05 06:48:03 -07:00
}
2025-06-08 16:20:43 -07:00
return ` ${ shortenPath ( relativePath ) } : ${ oldStringSnippet } => ${ newStringSnippet } ` ;
2025-04-19 19:45:42 +01:00
}
/ * *
* Executes the edit operation with the given parameters .
* @param params Parameters for the edit operation
* @returns Result of the edit operation
* /
2025-08-07 10:05:37 -07:00
async execute ( signal : AbortSignal ) : Promise < ToolResult > {
2025-06-08 16:20:43 -07:00
let editData : CalculatedEdit ;
2025-04-19 19:45:42 +01:00
try {
2025-08-07 10:05:37 -07:00
editData = await this . calculateEdit ( this . params , signal ) ;
2025-06-08 16:20:43 -07:00
} catch ( error ) {
const errorMsg = error instanceof Error ? error.message : String ( error ) ;
return {
llmContent : ` Error preparing edit: ${ errorMsg } ` ,
returnDisplay : ` Error preparing edit: ${ errorMsg } ` ,
2025-08-01 11:20:08 -04:00
error : {
message : errorMsg ,
type : ToolErrorType . EDIT_PREPARATION_FAILURE ,
} ,
2025-06-08 16:20:43 -07:00
} ;
}
2025-04-19 19:45:42 +01:00
2025-06-08 16:20:43 -07:00
if ( editData . error ) {
return {
llmContent : editData.error.raw ,
returnDisplay : ` Error: ${ editData . error . display } ` ,
2025-08-01 11:20:08 -04:00
error : {
message : editData.error.raw ,
type : editData . error . type ,
} ,
2025-06-08 16:20:43 -07:00
} ;
}
try {
2025-08-07 10:05:37 -07:00
this . ensureParentDirectoriesExist ( this . params . file_path ) ;
2025-08-18 16:29:45 -06:00
await this . config
. getFileSystemService ( )
. writeTextFile ( this . params . file_path , editData . newContent ) ;
2025-04-19 19:45:42 +01:00
let displayResult : ToolResultDisplay ;
2025-08-22 17:47:32 +05:30
const fileName = path . basename ( this . params . file_path ) ;
const originallyProposedContent =
this . params . ai_proposed_string || this . params . new_string ;
const diffStat = getDiffStat (
fileName ,
editData . currentContent ? ? '' ,
originallyProposedContent ,
this . params . new_string ,
) ;
2025-06-08 16:20:43 -07:00
if ( editData . isNewFile ) {
2025-08-07 10:05:37 -07:00
displayResult = ` Created ${ shortenPath ( makeRelative ( this . params . file_path , this . config . getTargetDir ( ) ) ) } ` ;
2025-06-08 16:20:43 -07:00
} else {
// Generate diff for display, even though core logic doesn't technically need it
// The CLI wrapper will use this part of the ToolResult
2025-04-19 19:45:42 +01:00
const fileDiff = Diff . createPatch (
fileName ,
2025-06-08 16:20:43 -07:00
editData . currentContent ? ? '' , // Should not be null here if not isNewFile
editData . newContent ,
2025-04-19 19:45:42 +01:00
'Current' ,
'Proposed' ,
2025-05-25 22:38:44 -07:00
DEFAULT_DIFF_OPTIONS ,
2025-04-19 19:45:42 +01:00
) ;
2025-07-17 16:25:23 -06:00
displayResult = {
fileDiff ,
fileName ,
originalContent : editData.currentContent ,
newContent : editData.newContent ,
2025-08-08 10:08:07 +05:30
diffStat ,
2025-07-17 16:25:23 -06:00
} ;
2025-04-19 19:45:42 +01:00
}
2025-06-28 19:02:44 +01:00
const llmSuccessMessageParts = [
editData . isNewFile
2025-08-07 10:05:37 -07:00
? ` Created new file: ${ this . params . file_path } with provided content. `
: ` Successfully modified file: ${ this . params . file_path } ( ${ editData . occurrences } replacements). ` ,
2025-06-28 19:02:44 +01:00
] ;
2025-08-07 10:05:37 -07:00
if ( this . params . modified_by_user ) {
2025-06-28 19:02:44 +01:00
llmSuccessMessageParts . push (
2025-08-07 10:05:37 -07:00
` User modified the \` new_string \` content to be: ${ this . params . new_string } . ` ,
2025-06-28 19:02:44 +01:00
) ;
}
2025-04-19 19:45:42 +01:00
2025-08-22 17:47:32 +05:30
const lines = editData . newContent . split ( '\n' ) . length ;
const mimetype = getSpecificMimeType ( this . params . file_path ) ;
const extension = path . extname ( this . params . file_path ) ;
const programming_language = getProgrammingLanguage ( {
file_path : this.params.file_path ,
} ) ;
logFileOperation (
this . config ,
new FileOperationEvent (
EditTool . Name ,
editData . isNewFile ? FileOperation.CREATE : FileOperation.UPDATE ,
lines ,
mimetype ,
extension ,
diffStat ,
programming_language ,
) ,
) ;
2025-04-19 19:45:42 +01:00
return {
2025-06-28 19:02:44 +01:00
llmContent : llmSuccessMessageParts.join ( ' ' ) ,
2025-04-19 19:45:42 +01:00
returnDisplay : displayResult ,
} ;
} catch ( error ) {
const errorMsg = error instanceof Error ? error.message : String ( error ) ;
return {
2025-06-08 16:20:43 -07:00
llmContent : ` Error executing edit: ${ errorMsg } ` ,
returnDisplay : ` Error writing file: ${ errorMsg } ` ,
2025-08-01 11:20:08 -04:00
error : {
message : errorMsg ,
type : ToolErrorType . FILE_WRITE_FAILURE ,
} ,
2025-04-19 19:45:42 +01:00
} ;
}
}
/ * *
* Creates parent directories if they don ' t exist
* /
private ensureParentDirectoriesExist ( filePath : string ) : void {
const dirName = path . dirname ( filePath ) ;
if ( ! fs . existsSync ( dirName ) ) {
fs . mkdirSync ( dirName , { recursive : true } ) ;
}
}
2025-08-07 10:05:37 -07:00
}
/ * *
* Implementation of the Edit tool logic
* /
export class EditTool
extends BaseDeclarativeTool < EditToolParams , ToolResult >
implements ModifiableDeclarativeTool < EditToolParams >
{
static readonly Name = 'replace' ;
constructor ( private readonly config : Config ) {
super (
EditTool . Name ,
'Edit' ,
` Replaces text within a file. By default, replaces a single occurrence, but can replace multiple occurrences when \` expected_replacements \` is specified. This tool requires providing significant context around the change to ensure precise targeting. Always use the ${ ReadFileTool . Name } tool to examine the file's current content before attempting a text replacement.
The user has the ability to modify the \ ` new_string \` content. If modified, this will be stated in the response.
Expectation for required parameters :
1 . \ ` file_path \` MUST be an absolute path; otherwise an error will be thrown.
2 . \ ` old_string \` MUST be the exact literal text to replace (including all whitespace, indentation, newlines, and surrounding code etc.).
3 . \ ` new_string \` MUST be the exact literal text to replace \` old_string \` with (also including all whitespace, indentation, newlines, and surrounding code etc.). Ensure the resulting code is correct and idiomatic.
4 . NEVER escape \ ` old_string \` or \` new_string \` , that would break the exact literal text requirement.
* * Important : * * If ANY of the above are not satisfied , the tool will fail . CRITICAL for \ ` old_string \` : Must uniquely identify the single instance to change. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string matches multiple locations, or does not match exactly, the tool will fail.
* * Multiple replacements : * * Set \ ` expected_replacements \` to the number of occurrences you want to replace. The tool will replace ALL occurrences that match \` old_string \` exactly. Ensure the number of replacements matches your expectation. ` ,
2025-08-13 12:58:26 -03:00
Kind . Edit ,
2025-08-07 10:05:37 -07:00
{
properties : {
file_path : {
description :
"The absolute path to the file to modify. Must start with '/'." ,
2025-08-11 16:12:41 -07:00
type : 'string' ,
2025-08-07 10:05:37 -07:00
} ,
old_string : {
description :
'The exact literal text to replace, preferably unescaped. For single replacements (default), include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. For multiple replacements, specify expected_replacements parameter. If this string is not the exact literal text (i.e. you escaped it) or does not match exactly, the tool will fail.' ,
2025-08-11 16:12:41 -07:00
type : 'string' ,
2025-08-07 10:05:37 -07:00
} ,
new_string : {
description :
'The exact literal text to replace `old_string` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic.' ,
2025-08-11 16:12:41 -07:00
type : 'string' ,
2025-08-07 10:05:37 -07:00
} ,
expected_replacements : {
2025-08-11 16:12:41 -07:00
type : 'number' ,
2025-08-07 10:05:37 -07:00
description :
'Number of replacements expected. Defaults to 1 if not specified. Use when you want to replace multiple occurrences.' ,
minimum : 1 ,
} ,
} ,
required : [ 'file_path' , 'old_string' , 'new_string' ] ,
2025-08-11 16:12:41 -07:00
type : 'object' ,
2025-08-07 10:05:37 -07:00
} ,
) ;
}
/ * *
* Validates the parameters for the Edit tool
* @param params Parameters to validate
* @returns Error message string or null if valid
* /
2025-08-19 13:55:06 -07:00
protected override validateToolParamValues (
params : EditToolParams ,
) : string | null {
if ( ! params . file_path ) {
return "The 'file_path' parameter must be non-empty." ;
2025-08-07 10:05:37 -07:00
}
if ( ! path . isAbsolute ( params . file_path ) ) {
return ` File path must be absolute: ${ params . file_path } ` ;
}
const workspaceContext = this . config . getWorkspaceContext ( ) ;
if ( ! workspaceContext . isPathWithinWorkspace ( params . file_path ) ) {
const directories = workspaceContext . getDirectories ( ) ;
return ` File path must be within one of the workspace directories: ${ directories . join ( ', ' ) } ` ;
}
return null ;
}
protected createInvocation (
params : EditToolParams ,
) : ToolInvocation < EditToolParams , ToolResult > {
return new EditToolInvocation ( this . config , params ) ;
}
2025-06-14 19:20:04 +01:00
getModifyContext ( _ : AbortSignal ) : ModifyContext < EditToolParams > {
return {
getFilePath : ( params : EditToolParams ) = > params . file_path ,
getCurrentContent : async ( params : EditToolParams ) : Promise < string > = > {
try {
2025-08-18 16:29:45 -06:00
return this . config
. getFileSystemService ( )
. readTextFile ( params . file_path ) ;
2025-06-14 19:20:04 +01:00
} catch ( err ) {
if ( ! isNodeError ( err ) || err . code !== 'ENOENT' ) throw err ;
return '' ;
}
} ,
getProposedContent : async ( params : EditToolParams ) : Promise < string > = > {
try {
2025-08-18 16:29:45 -06:00
const currentContent = await this . config
. getFileSystemService ( )
. readTextFile ( params . file_path ) ;
2025-08-07 10:05:37 -07:00
return applyReplacement (
2025-06-14 19:20:04 +01:00
currentContent ,
params . old_string ,
params . new_string ,
params . old_string === '' && currentContent === '' ,
) ;
} catch ( err ) {
if ( ! isNodeError ( err ) || err . code !== 'ENOENT' ) throw err ;
return '' ;
}
} ,
createUpdatedParams : (
2025-06-16 02:00:41 +01:00
oldContent : string ,
2025-06-14 19:20:04 +01:00
modifiedProposedContent : string ,
originalParams : EditToolParams ,
2025-08-08 10:08:07 +05:30
) : EditToolParams = > {
const content = originalParams . new_string ;
return {
. . . originalParams ,
ai_proposed_string : content ,
old_string : oldContent ,
new_string : modifiedProposedContent ,
modified_by_user : true ,
} ;
} ,
2025-06-14 19:20:04 +01:00
} ;
}
2025-04-19 19:45:42 +01:00
}