Use consistent param names (#12517)

This commit is contained in:
Tommaso Sciortino
2025-11-06 15:03:52 -08:00
committed by GitHub
parent 5f1208ad81
commit f05d937f39
27 changed files with 553 additions and 525 deletions

View File

@@ -62,8 +62,7 @@ The core comes with a suite of pre-defined tools, typically found in
- **File System Tools:**
- `LSTool` (`ls.ts`): Lists directory contents.
- `ReadFileTool` (`read-file.ts`): Reads the content of a single file. It
takes an `absolute_path` parameter, which must be an absolute path.
- `ReadFileTool` (`read-file.ts`): Reads the content of a single file.
- `WriteFileTool` (`write-file.ts`): Writes content to a file.
- `GrepTool` (`grep.ts`): Searches for patterns in files.
- `GlobTool` (`glob.ts`): Finds files matching glob patterns.

View File

@@ -1,2 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Investigating File Access**\n\nI'm currently focused on the challenge of reading a file. The path provided is `/gemini-cli/.integration-tests/1761766343238/json-output-error/path/to/nonexistent/file.txt`, and I'm anticipating an error. It's safe to assume the file doesn't exist, which I intend to handle by responding with \"File not found\" as instructed.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12303,"totalTokenCount":12418,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":115}},{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Analyzing Error Handling**\n\nI've attempted to read the specified file, expecting an error due to the \"nonexistent\" path. My plan is to catch the error thrown by the `read_file` tool. Upon receiving this error, I'll promptly return \"File not found.\" This is in line with the initial instructions and ensures appropriate error management for the user's intended functionality. I'm now testing the error response.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12303,"totalTokenCount":12467,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":164}},{"candidates":[{"content":{"parts":[{"thoughtSignature":"CiQB0e2Kb0c450IIdZRHl1vvjWDAl9oKa7s5sfFgrTnU0w3qBQwKYgHR7YpvPjZlXSHaJNYgX8IuCvxfyyeACX4NZ8u+u3Z1kqQbgOMpiH6aPYCK9lPyhVftPDBF2m3j7Y2AShwXSpB+9+UB3zphOKCvq6i0ZLvK6QzVynZ1fySQacyjEBD+U6y5CpoBAdHtim9D6oskRu7f3x8rp56h24i6dwb1hzlyqLGl3A5Hsh/fGYjBCxR+Vs+U5Sb7LunmFMKxmO1fktz0x06FUiyaWgAaXl4E4FyLTKs9BbPdgo58uAhaI3vTQybeGZQzkRi6n+ywCniVKaTUd07EHYWdvLiB6x5zFKVOiTmh8PC6I4vNOkJIzFXRfR5aM5QFjEQY8R88HBH+ugraAQHR7YpvuNUG8Ttbzg+kq+kJYMNGDG4zVLPxFBbJZYU5zGLFLrNeEpp9HJ+1N1Pdts17J5pzoefKd2U9H5muPxoeW1CCpFqqKfYZNyoChrQnb4CgWoJ92pcb2UAZbDQ6H4rTyXWdXaAEP1i8rOmdWzI5GFPyxCwBKAbFhfMJOo7P4va5H13XTlLgWAgXlx3n24ookkD/DOL9ro/F6pmV6tzf/j7RVl0V5yozngnrFl4dFsQPp1nDQcFkbRIKHD30byjLGh6Vd6A2lrB477qUaCRfPXtlEgKgsOSYCuABAdHtim+PJPZV3QuQoJ0Qokr+vcdW1W2qzC4Vvt4QRJXyp6+eIS6iNjOisk4jfWLHmLTa11KrUSQsFJnGwsFhe1foOqfO4y46ROBPeSdyuc0gDzSCZPowiovGBwoZd+MbPTTVyExWBo6StVk+xWLj+sUqa8VDof45AOOK9kdzcLQPa2SXIeK6SYLS1zVTrbzGHQDZPv9UC99lmKxPFqAcuwpe9CQre9J3GcZSwd5Mfs/UkHbWui10a8jga4Ck7umjKSDFq/VWiRUp/jh+EN4xeLTG/UQhgH49Gw+Bi1hN1uYKIgHR7YpvS0j1oO2HV/HeK3YxSku9CLQ3OSJGhoAKiu8UGYE=","functionCall":{"name":"read_file","args":{"absolute_path":"/gemini-cli/.integration-tests/1761766343238/json-output-error/path/to/nonexistent/file.txt"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12303,"candidatesTokenCount":58,"totalTokenCount":12525,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":164}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Investigating File Access**\n\nI'm currently focused on the challenge of reading a file. The path provided is `/gemini-cli/.integration-tests/1761766343238/json-output-error/path/to/nonexistent/file.txt`, and I'm anticipating an error. It's safe to assume the file doesn't exist, which I intend to handle by responding with \"File not found\" as instructed.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12303,"totalTokenCount":12418,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":115}},{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Analyzing Error Handling**\n\nI've attempted to read the specified file, expecting an error due to the \"nonexistent\" path. My plan is to catch the error thrown by the `read_file` tool. Upon receiving this error, I'll promptly return \"File not found.\" This is in line with the initial instructions and ensures appropriate error management for the user's intended functionality. I'm now testing the error response.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12303,"totalTokenCount":12467,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":164}},{"candidates":[{"content":{"parts":[{"thoughtSignature":"CiQB0e2Kb0c450IIdZRHl1vvjWDAl9oKa7s5sfFgrTnU0w3qBQwKYgHR7YpvPjZlXSHaJNYgX8IuCvxfyyeACX4NZ8u+u3Z1kqQbgOMpiH6aPYCK9lPyhVftPDBF2m3j7Y2AShwXSpB+9+UB3zphOKCvq6i0ZLvK6QzVynZ1fySQacyjEBD+U6y5CpoBAdHtim9D6oskRu7f3x8rp56h24i6dwb1hzlyqLGl3A5Hsh/fGYjBCxR+Vs+U5Sb7LunmFMKxmO1fktz0x06FUiyaWgAaXl4E4FyLTKs9BbPdgo58uAhaI3vTQybeGZQzkRi6n+ywCniVKaTUd07EHYWdvLiB6x5zFKVOiTmh8PC6I4vNOkJIzFXRfR5aM5QFjEQY8R88HBH+ugraAQHR7YpvuNUG8Ttbzg+kq+kJYMNGDG4zVLPxFBbJZYU5zGLFLrNeEpp9HJ+1N1Pdts17J5pzoefKd2U9H5muPxoeW1CCpFqqKfYZNyoChrQnb4CgWoJ92pcb2UAZbDQ6H4rTyXWdXaAEP1i8rOmdWzI5GFPyxCwBKAbFhfMJOo7P4va5H13XTlLgWAgXlx3n24ookkD/DOL9ro/F6pmV6tzf/j7RVl0V5yozngnrFl4dFsQPp1nDQcFkbRIKHD30byjLGh6Vd6A2lrB477qUaCRfPXtlEgKgsOSYCuABAdHtim+PJPZV3QuQoJ0Qokr+vcdW1W2qzC4Vvt4QRJXyp6+eIS6iNjOisk4jfWLHmLTa11KrUSQsFJnGwsFhe1foOqfO4y46ROBPeSdyuc0gDzSCZPowiovGBwoZd+MbPTTVyExWBo6StVk+xWLj+sUqa8VDof45AOOK9kdzcLQPa2SXIeK6SYLS1zVTrbzGHQDZPv9UC99lmKxPFqAcuwpe9CQre9J3GcZSwd5Mfs/UkHbWui10a8jga4Ck7umjKSDFq/VWiRUp/jh+EN4xeLTG/UQhgH49Gw+Bi1hN1uYKIgHR7YpvS0j1oO2HV/HeK3YxSku9CLQ3OSJGhoAKiu8UGYE=","functionCall":{"name":"read_file","args":{"file_path":"/gemini-cli/.integration-tests/1761766343238/json-output-error/path/to/nonexistent/file.txt"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12303,"candidatesTokenCount":58,"totalTokenCount":12525,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":164}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"File not found"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12588,"candidatesTokenCount":3,"totalTokenCount":12591,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12588}]}}]}

View File

@@ -415,7 +415,7 @@ export async function handleAtCommand({
const processedQueryParts: PartUnion[] = [{ text: initialQueryText }];
const toolArgs = {
paths: pathSpecsToRead,
include: pathSpecsToRead,
file_filtering_options: {
respect_git_ignore: respectFileIgnore.respectGitIgnore,
respect_gemini_ignore: respectFileIgnore.respectGeminiIgnore,

View File

@@ -13,7 +13,6 @@ exports[`Core System Prompt (prompts.ts) > should append userMemory with separat
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read_file or 'write_file'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -76,7 +75,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read_file' or 'write_file'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'run_shell_command' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
@@ -117,7 +115,6 @@ exports[`Core System Prompt (prompts.ts) > should include git instructions when
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read_file or 'write_file'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -180,7 +177,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read_file' or 'write_file'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'run_shell_command' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
@@ -231,7 +227,6 @@ exports[`Core System Prompt (prompts.ts) > should include non-sandbox instructio
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read_file or 'write_file'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -294,7 +289,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read_file' or 'write_file'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'run_shell_command' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
@@ -330,7 +324,6 @@ exports[`Core System Prompt (prompts.ts) > should include sandbox-specific instr
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read_file or 'write_file'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -393,7 +386,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read_file' or 'write_file'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'run_shell_command' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
@@ -429,7 +421,6 @@ exports[`Core System Prompt (prompts.ts) > should include seatbelt-specific inst
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read_file or 'write_file'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -492,7 +483,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read_file' or 'write_file'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'run_shell_command' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
@@ -528,7 +518,6 @@ exports[`Core System Prompt (prompts.ts) > should not include git instructions w
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read_file or 'write_file'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -591,7 +580,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read_file' or 'write_file'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'run_shell_command' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
@@ -627,7 +615,6 @@ exports[`Core System Prompt (prompts.ts) > should return the base prompt when us
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read_file or 'write_file'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -690,7 +677,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read_file' or 'write_file'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'run_shell_command' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
@@ -726,7 +712,6 @@ exports[`Core System Prompt (prompts.ts) > should return the base prompt when us
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read_file or 'write_file'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -789,7 +774,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read_file' or 'write_file'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'run_shell_command' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
@@ -825,7 +809,6 @@ exports[`Core System Prompt (prompts.ts) > should return the interactive avoidan
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read_file or 'write_file'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -888,7 +871,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read_file' or 'write_file'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'run_shell_command' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.

View File

@@ -130,7 +130,6 @@ export function getCoreSystemPrompt(
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., ${READ_FILE_TOOL_NAME} or '${WRITE_FILE_TOOL_NAME}'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.`,
primaryWorkflows_prefix: `
@@ -234,7 +233,6 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like '${READ_FILE_TOOL_NAME}' or '${WRITE_FILE_TOOL_NAME}'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the '${SHELL_TOOL_NAME}' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.

View File

@@ -57,8 +57,8 @@ import os from 'node:os';
import type { Config } from '../config/config.js';
import { ApprovalMode } from '../policy/types.js';
import type { Content, Part, SchemaUnion } from '@google/genai';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { StandardFileSystemService } from '../services/fileSystemService.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
describe('EditTool', () => {
let tool: EditTool;
@@ -88,7 +88,7 @@ describe('EditTool', () => {
getTargetDir: () => rootDir,
getApprovalMode: vi.fn(),
setApprovalMode: vi.fn(),
getWorkspaceContext: () => createMockWorkspaceContext(rootDir),
getWorkspaceContext: () => new WorkspaceContext(rootDir),
getFileSystemService: () => new StandardFileSystemService(),
getIdeMode: () => false,
// getGeminiConfig: () => ({ apiKey: 'test-api-key' }), // This was not a real Config method
@@ -300,17 +300,6 @@ describe('EditTool', () => {
expect(tool.validateToolParams(params)).toBeNull();
});
it('should return error for relative path', () => {
const params: EditToolParams = {
file_path: 'test.txt',
old_string: 'old',
new_string: 'new',
};
expect(tool.validateToolParams(params)).toMatch(
/File path must be absolute/,
);
});
it('should return error for path outside root', () => {
const params: EditToolParams = {
file_path: path.join(tempDir, 'outside-root.txt'),
@@ -332,13 +321,29 @@ describe('EditTool', () => {
filePath = path.join(rootDir, testFile);
});
it('should throw an error if params are invalid', async () => {
it('should resolve relative path and request confirmation', async () => {
fs.writeFileSync(filePath, 'some old content here');
const params: EditToolParams = {
file_path: 'relative.txt',
file_path: testFile, // relative path
old_string: 'old',
new_string: 'new',
};
expect(() => tool.build(params)).toThrow();
// ensureCorrectEdit will be called by shouldConfirmExecute
mockEnsureCorrectEdit.mockResolvedValueOnce({
params: { ...params, file_path: filePath },
occurrences: 1,
});
const invocation = tool.build(params);
const confirmation = await invocation.shouldConfirmExecute(
new AbortController().signal,
);
expect(confirmation).toEqual(
expect.objectContaining({
title: `Confirm Edit: ${testFile}`,
fileName: testFile,
fileDiff: expect.any(String),
}),
);
});
it('should request confirmation for valid edit', async () => {
@@ -531,13 +536,21 @@ describe('EditTool', () => {
});
});
it('should throw error if file path is not absolute', async () => {
it('should resolve relative path and execute successfully', async () => {
const initialContent = 'This is some old text.';
const newContent = 'This is some new text.';
fs.writeFileSync(filePath, initialContent, 'utf8');
const params: EditToolParams = {
file_path: 'relative.txt',
file_path: testFile, // relative path
old_string: 'old',
new_string: 'new',
};
expect(() => tool.build(params)).toThrow(/File path must be absolute/);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toMatch(/Successfully modified file/);
expect(fs.readFileSync(filePath, 'utf8')).toBe(newContent);
});
it('should throw error if file path is empty', async () => {
@@ -927,13 +940,13 @@ describe('EditTool', () => {
expect(result.error?.type).toBe(ToolErrorType.EDIT_NO_CHANGE);
});
it('should throw INVALID_PARAMETERS error for relative path', async () => {
it('should not throw error for relative path', async () => {
const params: EditToolParams = {
file_path: 'relative/path.txt',
old_string: 'a',
new_string: 'b',
};
expect(() => tool.build(params)).toThrow();
expect(() => tool.build(params)).not.toThrow();
});
it('should return FILE_WRITE_FAILURE on write error', async () => {
@@ -1046,9 +1059,7 @@ describe('EditTool', () => {
expect(
(schema.parametersJsonSchema as EditFileParameterSchema).properties
.file_path.description,
).toBe(
"The absolute path to the file to modify (e.g., 'C:\\Users\\project\\file.txt'). Must be an absolute path.",
);
).toBe('The path to the file to modify.');
});
it('should use unix-style path examples on non-windows platforms', () => {
@@ -1059,9 +1070,7 @@ describe('EditTool', () => {
expect(
(schema.parametersJsonSchema as EditFileParameterSchema).properties
.file_path.description,
).toBe(
"The absolute path to the file to modify (e.g., '/home/user/project/file.txt'). Must start with '/'.",
);
).toBe('The path to the file to modify.');
});
});

View File

@@ -7,7 +7,6 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as Diff from 'diff';
import process from 'node:process';
import type {
ToolCallConfirmationDetails,
ToolEditConfirmationDetails,
@@ -71,7 +70,7 @@ export function applyReplacement(
*/
export interface EditToolParams {
/**
* The absolute path to the file to modify
* The path to the file to modify
*/
file_path: string;
@@ -114,6 +113,8 @@ class EditToolInvocation
extends BaseToolInvocation<EditToolParams, ToolResult>
implements ToolInvocation<EditToolParams, ToolResult>
{
private readonly resolvedPath: string;
constructor(
private readonly config: Config,
params: EditToolParams,
@@ -122,10 +123,14 @@ class EditToolInvocation
displayName?: string,
) {
super(params, messageBus, toolName, displayName);
this.resolvedPath = path.resolve(
this.config.getTargetDir(),
this.params.file_path,
);
}
override toolLocations(): ToolLocation[] {
return [{ path: this.params.file_path }];
return [{ path: this.resolvedPath }];
}
/**
@@ -152,7 +157,7 @@ class EditToolInvocation
try {
currentContent = await this.config
.getFileSystemService()
.readTextFile(params.file_path);
.readTextFile(this.resolvedPath);
// Normalize line endings to LF for consistent processing.
currentContent = currentContent.replace(/\r\n/g, '\n');
fileExists = true;
@@ -171,13 +176,13 @@ class EditToolInvocation
// Trying to edit a nonexistent file (and old_string is not empty)
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}`,
raw: `File not found: ${this.resolvedPath}`,
type: ToolErrorType.FILE_NOT_FOUND,
};
} else if (currentContent !== null) {
// Editing an existing file
const correctedEdit = await ensureCorrectEdit(
params.file_path,
this.resolvedPath,
currentContent,
params,
this.config.getGeminiClient(),
@@ -192,13 +197,13 @@ class EditToolInvocation
// 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}`,
raw: `File already exists, cannot create: ${this.resolvedPath}`,
type: ToolErrorType.ATTEMPT_TO_CREATE_EXISTING_FILE,
};
} 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 ${READ_FILE_TOOL_NAME} tool to verify.`,
raw: `Failed to edit, 0 occurrences found for old_string in ${this.resolvedPath}. 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 ${READ_FILE_TOOL_NAME} tool to verify.`,
type: ToolErrorType.EDIT_NO_OCCURRENCE_FOUND,
};
} else if (occurrences !== expectedReplacements) {
@@ -207,13 +212,13 @@ class EditToolInvocation
error = {
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}`,
raw: `Failed to edit, Expected ${expectedReplacements} ${occurrenceTerm} but found ${occurrences} for old_string in file: ${this.resolvedPath}`,
type: ToolErrorType.EDIT_EXPECTED_OCCURRENCE_MISMATCH,
};
} 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}`,
raw: `No changes to apply. The old_string and new_string are identical in file: ${this.resolvedPath}`,
type: ToolErrorType.EDIT_NO_CHANGE,
};
}
@@ -221,7 +226,7 @@ class EditToolInvocation
// 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}`,
raw: `Failed to read content of existing file: ${this.resolvedPath}`,
type: ToolErrorType.READ_CONTENT_FAILURE,
};
}
@@ -239,7 +244,7 @@ class EditToolInvocation
error = {
display:
'No changes to apply. The new content is identical to the current content.',
raw: `No changes to apply. The new content is identical to the current content in file: ${params.file_path}`,
raw: `No changes to apply. The new content is identical to the current content in file: ${this.resolvedPath}`,
type: ToolErrorType.EDIT_NO_CHANGE,
};
}
@@ -281,7 +286,7 @@ class EditToolInvocation
return false;
}
const fileName = path.basename(this.params.file_path);
const fileName = path.basename(this.resolvedPath);
const fileDiff = Diff.createPatch(
fileName,
editData.currentContent ?? '',
@@ -293,14 +298,14 @@ class EditToolInvocation
const ideClient = await IdeClient.getInstance();
const ideConfirmation =
this.config.getIdeMode() && ideClient.isDiffingEnabled()
? ideClient.openDiff(this.params.file_path, editData.newContent)
? ideClient.openDiff(this.resolvedPath, editData.newContent)
: undefined;
const confirmationDetails: ToolEditConfirmationDetails = {
type: 'edit',
title: `Confirm Edit: ${shortenPath(makeRelative(this.params.file_path, this.config.getTargetDir()))}`,
fileName,
filePath: this.params.file_path,
filePath: this.resolvedPath,
fileDiff,
originalContent: editData.currentContent,
newContent: editData.newContent,
@@ -382,12 +387,12 @@ class EditToolInvocation
}
try {
this.ensureParentDirectoriesExist(this.params.file_path);
this.ensureParentDirectoriesExist(this.resolvedPath);
await this.config
.getFileSystemService()
.writeTextFile(this.params.file_path, editData.newContent);
.writeTextFile(this.resolvedPath, editData.newContent);
const fileName = path.basename(this.params.file_path);
const fileName = path.basename(this.resolvedPath);
const originallyProposedContent =
this.params.ai_proposed_content || editData.newContent;
const diffStat = getDiffStat(
@@ -414,11 +419,9 @@ class EditToolInvocation
};
// Log file operation for telemetry (without diff_stat to avoid double-counting)
const mimetype = getSpecificMimeType(this.params.file_path);
const programmingLanguage = getLanguageFromFilePath(
this.params.file_path,
);
const extension = path.extname(this.params.file_path);
const mimetype = getSpecificMimeType(this.resolvedPath);
const programmingLanguage = getLanguageFromFilePath(this.resolvedPath);
const extension = path.extname(this.resolvedPath);
const operation = editData.isNewFile
? FileOperation.CREATE
: FileOperation.UPDATE;
@@ -437,8 +440,8 @@ class EditToolInvocation
const llmSuccessMessageParts = [
editData.isNewFile
? `Created new file: ${this.params.file_path} with provided content.`
: `Successfully modified file: ${this.params.file_path} (${editData.occurrences} replacements).`,
? `Created new file: ${this.resolvedPath} with provided content.`
: `Successfully modified file: ${this.resolvedPath} (${editData.occurrences} replacements).`,
];
if (this.params.modified_by_user) {
llmSuccessMessageParts.push(
@@ -495,7 +498,7 @@ export class EditTool
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.
1. \`file_path\` is the path to the file to modify.
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.
@@ -505,10 +508,7 @@ Expectation for required parameters:
{
properties: {
file_path: {
description:
process.platform === 'win32'
? "The absolute path to the file to modify (e.g., 'C:\\Users\\project\\file.txt'). Must be an absolute path."
: "The absolute path to the file to modify (e.g., '/home/user/project/file.txt'). Must start with '/'.",
description: 'The path to the file to modify.',
type: 'string',
},
old_string: {
@@ -549,12 +549,12 @@ Expectation for required parameters:
return "The 'file_path' parameter must be non-empty.";
}
if (!path.isAbsolute(params.file_path)) {
return `File path must be absolute: ${params.file_path}`;
}
const resolvedPath = path.resolve(
this.config.getTargetDir(),
params.file_path,
);
const workspaceContext = this.config.getWorkspaceContext();
if (!workspaceContext.isPathWithinWorkspace(params.file_path)) {
if (!workspaceContext.isPathWithinWorkspace(resolvedPath)) {
const directories = workspaceContext.getDirectories();
return `File path must be within one of the workspace directories: ${directories.join(', ')}`;
}
@@ -578,13 +578,16 @@ Expectation for required parameters:
}
getModifyContext(_: AbortSignal): ModifyContext<EditToolParams> {
const resolvePath = (filePath: string) =>
path.resolve(this.config.getTargetDir(), filePath);
return {
getFilePath: (params: EditToolParams) => params.file_path,
getCurrentContent: async (params: EditToolParams): Promise<string> => {
try {
return this.config
return await this.config
.getFileSystemService()
.readTextFile(params.file_path);
.readTextFile(resolvePath(params.file_path));
} catch (err) {
if (!isNodeError(err) || err.code !== 'ENOENT') throw err;
return '';
@@ -594,7 +597,7 @@ Expectation for required parameters:
try {
const currentContent = await this.config
.getFileSystemService()
.readTextFile(params.file_path);
.readTextFile(resolvePath(params.file_path));
return applyReplacement(
currentContent,
params.old_string,

View File

@@ -131,7 +131,7 @@ describe('GlobTool', () => {
});
it('should find files in a specified relative path (relative to rootDir)', async () => {
const params: GlobToolParams = { pattern: '*.md', path: 'sub' };
const params: GlobToolParams = { pattern: '*.md', dir_path: 'sub' };
const invocation = globTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Found 2 file(s)');
@@ -220,7 +220,7 @@ describe('GlobTool', () => {
it('should return a PATH_NOT_IN_WORKSPACE error if path is outside workspace', async () => {
// Bypassing validation to test execute method directly
vi.spyOn(globTool, 'validateToolParams').mockReturnValue(null);
const params: GlobToolParams = { pattern: '*.txt', path: '/etc' };
const params: GlobToolParams = { pattern: '*.txt', dir_path: '/etc' };
const invocation = globTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.error?.type).toBe(ToolErrorType.PATH_NOT_IN_WORKSPACE);
@@ -249,18 +249,18 @@ describe('GlobTool', () => {
expected: null,
},
{
name: 'should return null for valid parameters (pattern and path)',
params: { pattern: '*.js', path: 'sub' },
name: 'should return null for valid parameters (pattern and dir_path)',
params: { pattern: '*.js', dir_path: 'sub' },
expected: null,
},
{
name: 'should return null for valid parameters (pattern, path, and case_sensitive)',
params: { pattern: '*.js', path: 'sub', case_sensitive: true },
name: 'should return null for valid parameters (pattern, dir_path, and case_sensitive)',
params: { pattern: '*.js', dir_path: 'sub', case_sensitive: true },
expected: null,
},
{
name: 'should return error if pattern is missing (schema validation)',
params: { path: '.' },
params: { dir_path: '.' },
expected: `params must have required property 'pattern'`,
},
{
@@ -274,9 +274,9 @@ describe('GlobTool', () => {
expected: "The 'pattern' parameter cannot be empty.",
},
{
name: 'should return error if path is not a string (schema validation)',
params: { pattern: '*.ts', path: 123 },
expected: 'params/path must be string',
name: 'should return error if dir_path is not a string (schema validation)',
params: { pattern: '*.ts', dir_path: 123 },
expected: 'params/dir_path must be string',
},
{
name: 'should return error if case_sensitive is not a boolean (schema validation)',
@@ -285,17 +285,20 @@ describe('GlobTool', () => {
},
{
name: "should return error if search path resolves outside the tool's root directory",
params: { pattern: '*.txt', path: '../../../../../../../../../../tmp' },
params: {
pattern: '*.txt',
dir_path: '../../../../../../../../../../tmp',
},
expected: 'resolves outside the allowed workspace directories',
},
{
name: 'should return error if specified search path does not exist',
params: { pattern: '*.txt', path: 'nonexistent_subdir' },
params: { pattern: '*.txt', dir_path: 'nonexistent_subdir' },
expected: 'Search path does not exist',
},
{
name: 'should return error if specified search path is a file, not a directory',
params: { pattern: '*.txt', path: 'fileA.txt' },
params: { pattern: '*.txt', dir_path: 'fileA.txt' },
expected: 'Search path is not a directory',
},
])('$name', ({ params, expected }) => {
@@ -311,8 +314,8 @@ describe('GlobTool', () => {
describe('workspace boundary validation', () => {
it('should validate search paths are within workspace boundaries', () => {
const validPath = { pattern: '*.ts', path: 'sub' };
const invalidPath = { pattern: '*.ts', path: '../..' };
const validPath = { pattern: '*.ts', dir_path: 'sub' };
const invalidPath = { pattern: '*.ts', dir_path: '../..' };
expect(globTool.validateToolParams(validPath)).toBeNull();
expect(globTool.validateToolParams(invalidPath)).toContain(
@@ -321,7 +324,7 @@ describe('GlobTool', () => {
});
it('should provide clear error messages when path is outside workspace', () => {
const invalidPath = { pattern: '*.ts', path: '/etc' };
const invalidPath = { pattern: '*.ts', dir_path: '/etc' };
const error = globTool.validateToolParams(invalidPath);
expect(error).toContain(
@@ -331,7 +334,7 @@ describe('GlobTool', () => {
});
it('should work with paths in workspace subdirectories', async () => {
const params: GlobToolParams = { pattern: '*.md', path: 'sub' };
const params: GlobToolParams = { pattern: '*.md', dir_path: 'sub' };
const invocation = globTool.build(params);
const result = await invocation.execute(abortSignal);

View File

@@ -66,7 +66,7 @@ export interface GlobToolParams {
/**
* The directory to search in (optional, defaults to current directory)
*/
path?: string;
dir_path?: string;
/**
* Whether the search should be case-sensitive (optional, defaults to false)
@@ -100,10 +100,10 @@ class GlobToolInvocation extends BaseToolInvocation<
getDescription(): string {
let description = `'${this.params.pattern}'`;
if (this.params.path) {
if (this.params.dir_path) {
const searchDir = path.resolve(
this.config.getTargetDir(),
this.params.path || '.',
this.params.dir_path || '.',
);
const relativePath = makeRelative(searchDir, this.config.getTargetDir());
description += ` within ${shortenPath(relativePath)}`;
@@ -118,13 +118,13 @@ class GlobToolInvocation extends BaseToolInvocation<
// If a specific path is provided, resolve it and check if it's within workspace
let searchDirectories: readonly string[];
if (this.params.path) {
if (this.params.dir_path) {
const searchDirAbsolute = path.resolve(
this.config.getTargetDir(),
this.params.path,
this.params.dir_path,
);
if (!workspaceContext.isPathWithinWorkspace(searchDirAbsolute)) {
const rawError = `Error: Path "${this.params.path}" is not within any workspace directory`;
const rawError = `Error: Path "${this.params.dir_path}" is not within any workspace directory`;
return {
llmContent: rawError,
returnDisplay: `Path is not within workspace`,
@@ -276,7 +276,7 @@ export class GlobTool extends BaseDeclarativeTool<GlobToolParams, ToolResult> {
"The glob pattern to match against (e.g., '**/*.py', 'docs/*.md').",
type: 'string',
},
path: {
dir_path: {
description:
'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.',
type: 'string',
@@ -314,7 +314,7 @@ export class GlobTool extends BaseDeclarativeTool<GlobToolParams, ToolResult> {
): string | null {
const searchDirAbsolute = path.resolve(
this.config.getTargetDir(),
params.path || '.',
params.dir_path || '.',
);
const workspaceContext = this.config.getWorkspaceContext();

View File

@@ -80,21 +80,21 @@ describe('GrepTool', () => {
});
it('should return null for valid params (pattern and path)', () => {
const params: GrepToolParams = { pattern: 'hello', path: '.' };
const params: GrepToolParams = { pattern: 'hello', dir_path: '.' };
expect(grepTool.validateToolParams(params)).toBeNull();
});
it('should return null for valid params (pattern, path, and include)', () => {
const params: GrepToolParams = {
pattern: 'hello',
path: '.',
dir_path: '.',
include: '*.txt',
};
expect(grepTool.validateToolParams(params)).toBeNull();
});
it('should return error if pattern is missing', () => {
const params = { path: '.' } as unknown as GrepToolParams;
const params = { dir_path: '.' } as unknown as GrepToolParams;
expect(grepTool.validateToolParams(params)).toBe(
`params must have required property 'pattern'`,
);
@@ -108,7 +108,10 @@ describe('GrepTool', () => {
});
it('should return error if path does not exist', () => {
const params: GrepToolParams = { pattern: 'hello', path: 'nonexistent' };
const params: GrepToolParams = {
pattern: 'hello',
dir_path: 'nonexistent',
};
// Check for the core error message, as the full path might vary
expect(grepTool.validateToolParams(params)).toContain(
'Failed to access path stats for',
@@ -118,7 +121,7 @@ describe('GrepTool', () => {
it('should return error if path is a file, not a directory', async () => {
const filePath = path.join(tempRootDir, 'fileA.txt');
const params: GrepToolParams = { pattern: 'hello', path: filePath };
const params: GrepToolParams = { pattern: 'hello', dir_path: filePath };
expect(grepTool.validateToolParams(params)).toContain(
`Path is not a directory: ${filePath}`,
);
@@ -144,7 +147,7 @@ describe('GrepTool', () => {
});
it('should find matches in a specific path', async () => {
const params: GrepToolParams = { pattern: 'world', path: 'sub' };
const params: GrepToolParams = { pattern: 'world', dir_path: 'sub' };
const invocation = grepTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain(
@@ -176,7 +179,7 @@ describe('GrepTool', () => {
);
const params: GrepToolParams = {
pattern: 'hello',
path: 'sub',
dir_path: 'sub',
include: '*.js',
};
const invocation = grepTool.build(params);
@@ -226,7 +229,7 @@ describe('GrepTool', () => {
});
it('should throw an error if params are invalid', async () => {
const params = { path: '.' } as unknown as GrepToolParams; // Invalid: pattern missing
const params = { dir_path: '.' } as unknown as GrepToolParams; // Invalid: pattern missing
expect(() => grepTool.build(params)).toThrow(
/params must have required property 'pattern'/,
);
@@ -323,7 +326,7 @@ describe('GrepTool', () => {
const multiDirGrepTool = new GrepTool(multiDirConfig);
// Search only in the 'sub' directory of the first workspace
const params: GrepToolParams = { pattern: 'world', path: 'sub' };
const params: GrepToolParams = { pattern: 'world', dir_path: 'sub' };
const invocation = multiDirGrepTool.build(params);
const result = await invocation.execute(abortSignal);
@@ -363,7 +366,7 @@ describe('GrepTool', () => {
await fs.mkdir(dirPath, { recursive: true });
const params: GrepToolParams = {
pattern: 'testPattern',
path: path.join('src', 'app'),
dir_path: path.join('src', 'app'),
};
const invocation = grepTool.build(params);
// The path will be relative to the tempRootDir, so we check for containment.
@@ -396,7 +399,7 @@ describe('GrepTool', () => {
const params: GrepToolParams = {
pattern: 'testPattern',
include: '*.ts',
path: path.join('src', 'app'),
dir_path: path.join('src', 'app'),
};
const invocation = grepTool.build(params);
expect(invocation.getDescription()).toContain(
@@ -406,7 +409,7 @@ describe('GrepTool', () => {
});
it('should use ./ for root path in description', () => {
const params: GrepToolParams = { pattern: 'testPattern', path: '.' };
const params: GrepToolParams = { pattern: 'testPattern', dir_path: '.' };
const invocation = grepTool.build(params);
expect(invocation.getDescription()).toBe("'testPattern' within ./");
});

View File

@@ -36,7 +36,7 @@ export interface GrepToolParams {
/**
* The directory to search in (optional, defaults to current directory relative to root)
*/
path?: string;
dir_path?: string;
/**
* File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")
@@ -114,8 +114,8 @@ class GrepToolInvocation extends BaseToolInvocation<
async execute(signal: AbortSignal): Promise<ToolResult> {
try {
const workspaceContext = this.config.getWorkspaceContext();
const searchDirAbs = this.resolveAndValidatePath(this.params.path);
const searchDirDisplay = this.params.path || '.';
const searchDirAbs = this.resolveAndValidatePath(this.params.dir_path);
const searchDirDisplay = this.params.dir_path || '.';
// Determine which directories to search
let searchDirectories: readonly string[];
@@ -299,14 +299,14 @@ class GrepToolInvocation extends BaseToolInvocation<
if (this.params.include) {
description += ` in ${this.params.include}`;
}
if (this.params.path) {
if (this.params.dir_path) {
const resolvedPath = path.resolve(
this.config.getTargetDir(),
this.params.path,
this.params.dir_path,
);
if (
resolvedPath === this.config.getTargetDir() ||
this.params.path === '.'
this.params.dir_path === '.'
) {
description += ` within ./`;
} else {
@@ -585,7 +585,7 @@ export class GrepTool extends BaseDeclarativeTool<GrepToolParams, ToolResult> {
"The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').",
type: 'string',
},
path: {
dir_path: {
description:
'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.',
type: 'string',
@@ -660,10 +660,10 @@ export class GrepTool extends BaseDeclarativeTool<GrepToolParams, ToolResult> {
return `Invalid regular expression pattern provided: ${params.pattern}. Error: ${getErrorMessage(error)}`;
}
// Only validate path if one is provided
if (params.path) {
// Only validate dir_path if one is provided
if (params.dir_path) {
try {
this.resolveAndValidatePath(params.path);
this.resolveAndValidatePath(params.dir_path);
} catch (error) {
return getErrorMessage(error);
}

View File

@@ -12,7 +12,7 @@ import { LSTool } from './ls.js';
import type { Config } from '../config/config.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { ToolErrorType } from './tool-error.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
describe('LSTool', () => {
let lsTool: LSTool;
@@ -22,18 +22,16 @@ describe('LSTool', () => {
const abortSignal = new AbortController().signal;
beforeEach(async () => {
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ls-tool-root-'));
const realTmp = await fs.realpath(os.tmpdir());
tempRootDir = await fs.mkdtemp(path.join(realTmp, 'ls-tool-root-'));
tempSecondaryDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'ls-tool-secondary-'),
path.join(realTmp, 'ls-tool-secondary-'),
);
const mockWorkspaceContext = createMockWorkspaceContext(tempRootDir, [
tempSecondaryDir,
]);
mockConfig = {
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => mockWorkspaceContext,
getWorkspaceContext: () =>
new WorkspaceContext(tempRootDir, [tempSecondaryDir]),
getFileService: () => new FileDiscoveryService(tempRootDir),
getFileFilteringOptions: () => ({
respectGitIgnore: true,
@@ -54,19 +52,23 @@ describe('LSTool', () => {
const testPath = path.join(tempRootDir, 'src');
await fs.mkdir(testPath);
const invocation = lsTool.build({ path: testPath });
const invocation = lsTool.build({ dir_path: testPath });
expect(invocation).toBeDefined();
});
it('should reject relative paths', () => {
expect(() => lsTool.build({ path: './src' })).toThrow(
'Path must be absolute: ./src',
);
it('should accept relative paths', async () => {
const testPath = path.join(tempRootDir, 'src');
await fs.mkdir(testPath);
const relativePath = path.relative(tempRootDir, testPath);
const invocation = lsTool.build({ dir_path: relativePath });
expect(invocation).toBeDefined();
});
it('should reject paths outside workspace with clear error message', () => {
expect(() => lsTool.build({ path: '/etc/passwd' })).toThrow(
expect(() => lsTool.build({ dir_path: '/etc/passwd' })).toThrow(
`Path must be within one of the workspace directories: ${tempRootDir}, ${tempSecondaryDir}`,
);
});
@@ -75,7 +77,7 @@ describe('LSTool', () => {
const testPath = path.join(tempSecondaryDir, 'lib');
await fs.mkdir(testPath);
const invocation = lsTool.build({ path: testPath });
const invocation = lsTool.build({ dir_path: testPath });
expect(invocation).toBeDefined();
});
@@ -90,7 +92,7 @@ describe('LSTool', () => {
'secondary',
);
const invocation = lsTool.build({ path: tempRootDir });
const invocation = lsTool.build({ dir_path: tempRootDir });
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('[DIR] subdir');
@@ -106,7 +108,7 @@ describe('LSTool', () => {
'secondary',
);
const invocation = lsTool.build({ path: tempSecondaryDir });
const invocation = lsTool.build({ dir_path: tempSecondaryDir });
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('secondary-file.txt');
@@ -116,7 +118,7 @@ describe('LSTool', () => {
it('should handle empty directories', async () => {
const emptyDir = path.join(tempRootDir, 'empty');
await fs.mkdir(emptyDir);
const invocation = lsTool.build({ path: emptyDir });
const invocation = lsTool.build({ dir_path: emptyDir });
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toBe(`Directory ${emptyDir} is empty.`);
@@ -128,7 +130,7 @@ describe('LSTool', () => {
await fs.writeFile(path.join(tempRootDir, 'file2.log'), 'content1');
const invocation = lsTool.build({
path: tempRootDir,
dir_path: tempRootDir,
ignore: ['*.log'],
});
const result = await invocation.execute(abortSignal);
@@ -143,7 +145,7 @@ describe('LSTool', () => {
await fs.writeFile(path.join(tempRootDir, 'file2.log'), 'content1');
await fs.writeFile(path.join(tempRootDir, '.git'), '');
await fs.writeFile(path.join(tempRootDir, '.gitignore'), '*.log');
const invocation = lsTool.build({ path: tempRootDir });
const invocation = lsTool.build({ dir_path: tempRootDir });
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('file1.txt');
@@ -156,7 +158,7 @@ describe('LSTool', () => {
await fs.writeFile(path.join(tempRootDir, 'file1.txt'), 'content1');
await fs.writeFile(path.join(tempRootDir, 'file2.log'), 'content1');
await fs.writeFile(path.join(tempRootDir, '.geminiignore'), '*.log');
const invocation = lsTool.build({ path: tempRootDir });
const invocation = lsTool.build({ dir_path: tempRootDir });
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('file1.txt');
@@ -168,7 +170,7 @@ describe('LSTool', () => {
const testPath = path.join(tempRootDir, 'file1.txt');
await fs.writeFile(testPath, 'content1');
const invocation = lsTool.build({ path: testPath });
const invocation = lsTool.build({ dir_path: testPath });
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Path is not a directory');
@@ -178,7 +180,7 @@ describe('LSTool', () => {
it('should handle non-existent paths', async () => {
const testPath = path.join(tempRootDir, 'does-not-exist');
const invocation = lsTool.build({ path: testPath });
const invocation = lsTool.build({ dir_path: testPath });
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Error listing directory');
@@ -192,7 +194,7 @@ describe('LSTool', () => {
await fs.mkdir(path.join(tempRootDir, 'x-dir'));
await fs.mkdir(path.join(tempRootDir, 'y-dir'));
const invocation = lsTool.build({ path: tempRootDir });
const invocation = lsTool.build({ dir_path: tempRootDir });
const result = await invocation.execute(abortSignal);
const lines = (
@@ -217,7 +219,7 @@ describe('LSTool', () => {
const error = new Error('EACCES: permission denied');
vi.spyOn(fs, 'readdir').mockRejectedValueOnce(error);
const invocation = lsTool.build({ path: restrictedDir });
const invocation = lsTool.build({ dir_path: restrictedDir });
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Error listing directory');
@@ -226,12 +228,6 @@ describe('LSTool', () => {
expect(result.error?.type).toBe(ToolErrorType.LS_EXECUTION_ERROR);
});
it('should throw for invalid params at build time', () => {
expect(() => lsTool.build({ path: '../outside' })).toThrow(
'Path must be absolute: ../outside',
);
});
it('should handle errors accessing individual files during listing', async () => {
await fs.writeFile(path.join(tempRootDir, 'file1.txt'), 'content1');
const problematicFile = path.join(tempRootDir, 'problematic.txt');
@@ -248,7 +244,7 @@ describe('LSTool', () => {
return originalStat(p);
});
const invocation = lsTool.build({ path: tempRootDir });
const invocation = lsTool.build({ dir_path: tempRootDir });
const result = await invocation.execute(abortSignal);
// Should still list the other files
@@ -264,7 +260,7 @@ describe('LSTool', () => {
it('should return shortened relative path', () => {
const deeplyNestedDir = path.join(tempRootDir, 'deeply', 'nested');
const params = {
path: path.join(deeplyNestedDir, 'directory'),
dir_path: path.join(deeplyNestedDir, 'directory'),
};
const invocation = lsTool.build(params);
const description = invocation.getDescription();
@@ -273,11 +269,11 @@ describe('LSTool', () => {
it('should handle paths in secondary workspace', () => {
const params = {
path: path.join(tempSecondaryDir, 'lib'),
dir_path: path.join(tempSecondaryDir, 'lib'),
};
const invocation = lsTool.build(params);
const description = invocation.getDescription();
const expected = path.relative(tempRootDir, params.path);
const expected = path.relative(tempRootDir, params.dir_path);
expect(description).toBe(expected);
});
});
@@ -286,19 +282,19 @@ describe('LSTool', () => {
it('should accept paths in primary workspace directory', async () => {
const testPath = path.join(tempRootDir, 'src');
await fs.mkdir(testPath);
const params = { path: testPath };
const params = { dir_path: testPath };
expect(lsTool.build(params)).toBeDefined();
});
it('should accept paths in secondary workspace directory', async () => {
const testPath = path.join(tempSecondaryDir, 'lib');
await fs.mkdir(testPath);
const params = { path: testPath };
const params = { dir_path: testPath };
expect(lsTool.build(params)).toBeDefined();
});
it('should reject paths outside all workspace directories', () => {
const params = { path: '/etc/passwd' };
const params = { dir_path: '/etc/passwd' };
expect(() => lsTool.build(params)).toThrow(
'Path must be within one of the workspace directories',
);
@@ -310,7 +306,7 @@ describe('LSTool', () => {
'secondary',
);
const invocation = lsTool.build({ path: tempSecondaryDir });
const invocation = lsTool.build({ dir_path: tempSecondaryDir });
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('secondary-file.txt');

View File

@@ -23,7 +23,7 @@ export interface LSToolParams {
/**
* The absolute path to the directory to list
*/
path: string;
dir_path: string;
/**
* Array of glob patterns to ignore (optional)
@@ -110,7 +110,7 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
*/
getDescription(): string {
const relativePath = makeRelative(
this.params.path,
this.params.dir_path,
this.config.getTargetDir(),
);
return shortenPath(relativePath);
@@ -138,30 +138,34 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
* @returns Result of the LS operation
*/
async execute(_signal: AbortSignal): Promise<ToolResult> {
const resolvedDirPath = path.resolve(
this.config.getTargetDir(),
this.params.dir_path,
);
try {
const stats = await fs.stat(this.params.path);
const stats = await fs.stat(resolvedDirPath);
if (!stats) {
// fs.statSync throws on non-existence, so this check might be redundant
// but keeping for clarity. Error message adjusted.
return this.errorResult(
`Error: Directory not found or inaccessible: ${this.params.path}`,
`Error: Directory not found or inaccessible: ${resolvedDirPath}`,
`Directory not found or inaccessible.`,
ToolErrorType.FILE_NOT_FOUND,
);
}
if (!stats.isDirectory()) {
return this.errorResult(
`Error: Path is not a directory: ${this.params.path}`,
`Error: Path is not a directory: ${resolvedDirPath}`,
`Path is not a directory.`,
ToolErrorType.PATH_IS_NOT_A_DIRECTORY,
);
}
const files = await fs.readdir(this.params.path);
const files = await fs.readdir(resolvedDirPath);
if (files.length === 0) {
// Changed error message to be more neutral for LLM
return {
llmContent: `Directory ${this.params.path} is empty.`,
llmContent: `Directory ${resolvedDirPath} is empty.`,
returnDisplay: `Directory is empty.`,
};
}
@@ -169,7 +173,7 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
const relativePaths = files.map((file) =>
path.relative(
this.config.getTargetDir(),
path.join(this.params.path, file),
path.join(resolvedDirPath, file),
),
);
@@ -222,7 +226,7 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
.map((entry) => `${entry.isDirectory ? '[DIR] ' : ''}${entry.name}`)
.join('\n');
let resultMessage = `Directory listing for ${this.params.path}:\n${directoryContent}`;
let resultMessage = `Directory listing for ${resolvedDirPath}:\n${directoryContent}`;
if (ignoredCount > 0) {
resultMessage += `\n\n(${ignoredCount} ignored)`;
}
@@ -264,9 +268,8 @@ export class LSTool extends BaseDeclarativeTool<LSToolParams, ToolResult> {
Kind.Search,
{
properties: {
path: {
description:
'The absolute path to the directory to list (must be absolute, not relative)',
dir_path: {
description: 'The path to the directory to list',
type: 'string',
},
ignore: {
@@ -294,7 +297,7 @@ export class LSTool extends BaseDeclarativeTool<LSToolParams, ToolResult> {
},
},
},
required: ['path'],
required: ['dir_path'],
type: 'object',
},
true,
@@ -311,12 +314,12 @@ export class LSTool extends BaseDeclarativeTool<LSToolParams, ToolResult> {
protected override validateToolParamValues(
params: LSToolParams,
): string | null {
if (!path.isAbsolute(params.path)) {
return `Path must be absolute: ${params.path}`;
}
const resolvedPath = path.resolve(
this.config.getTargetDir(),
params.dir_path,
);
const workspaceContext = this.config.getWorkspaceContext();
if (!workspaceContext.isPathWithinWorkspace(params.path)) {
if (!workspaceContext.isPathWithinWorkspace(resolvedPath)) {
const directories = workspaceContext.getDirectories();
return `Path must be within one of the workspace directories: ${directories.join(
', ',

View File

@@ -17,19 +17,12 @@ import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { StandardFileSystemService } from '../services/fileSystemService.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import type { ToolInvocation, ToolResult } from './tools.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
vi.mock('../telemetry/loggers.js', () => ({
logFileOperation: vi.fn(),
}));
interface ReadFileParameterSchema {
properties: {
absolute_path: {
description: string;
};
};
}
describe('ReadFileTool', () => {
let tempRootDir: string;
let tool: ReadFileTool;
@@ -37,9 +30,8 @@ describe('ReadFileTool', () => {
beforeEach(async () => {
// Create a unique temporary root directory for each test run
tempRootDir = await fsp.mkdtemp(
path.join(os.tmpdir(), 'read-file-tool-root-'),
);
const realTmp = await fsp.realpath(os.tmpdir());
tempRootDir = await fsp.mkdtemp(path.join(realTmp, 'read-file-tool-root-'));
const mockConfigInstance = {
getFileService: () => new FileDiscoveryService(tempRootDir),
@@ -67,24 +59,30 @@ describe('ReadFileTool', () => {
describe('build', () => {
it('should return an invocation for valid params (absolute path within root)', () => {
const params: ReadFileToolParams = {
absolute_path: path.join(tempRootDir, 'test.txt'),
file_path: path.join(tempRootDir, 'test.txt'),
};
const result = tool.build(params);
expect(typeof result).not.toBe('string');
});
it('should throw error if file path is relative', () => {
it('should return an invocation for valid params (relative path within root)', () => {
const params: ReadFileToolParams = {
absolute_path: 'relative/path.txt',
file_path: 'test.txt',
};
expect(() => tool.build(params)).toThrow(
'File path must be absolute, but was relative: relative/path.txt. You must provide an absolute path.',
const result = tool.build(params);
expect(typeof result).not.toBe('string');
const invocation = result as ToolInvocation<
ReadFileToolParams,
ToolResult
>;
expect(invocation.toolLocations()[0].path).toBe(
path.join(tempRootDir, 'test.txt'),
);
});
it('should throw error if path is outside root', () => {
const params: ReadFileToolParams = {
absolute_path: '/outside/root.txt',
file_path: '/outside/root.txt',
};
expect(() => tool.build(params)).toThrow(
/File path must be within one of the workspace directories/,
@@ -94,7 +92,7 @@ describe('ReadFileTool', () => {
it('should allow access to files in project temp directory', () => {
const tempDir = path.join(tempRootDir, '.temp');
const params: ReadFileToolParams = {
absolute_path: path.join(tempDir, 'temp-file.txt'),
file_path: path.join(tempDir, 'temp-file.txt'),
};
const result = tool.build(params);
expect(typeof result).not.toBe('string');
@@ -102,7 +100,7 @@ describe('ReadFileTool', () => {
it('should show temp directory in error message when path is outside workspace and temp dir', () => {
const params: ReadFileToolParams = {
absolute_path: '/completely/outside/path.txt',
file_path: '/completely/outside/path.txt',
};
expect(() => tool.build(params)).toThrow(
/File path must be within one of the workspace directories.*or within the project temp directory/,
@@ -111,16 +109,16 @@ describe('ReadFileTool', () => {
it('should throw error if path is empty', () => {
const params: ReadFileToolParams = {
absolute_path: '',
file_path: '',
};
expect(() => tool.build(params)).toThrow(
/The 'absolute_path' parameter must be non-empty./,
/The 'file_path' parameter must be non-empty./,
);
});
it('should throw error if offset is negative', () => {
const params: ReadFileToolParams = {
absolute_path: path.join(tempRootDir, 'test.txt'),
file_path: path.join(tempRootDir, 'test.txt'),
offset: -1,
};
expect(() => tool.build(params)).toThrow(
@@ -130,7 +128,7 @@ describe('ReadFileTool', () => {
it('should throw error if limit is zero or negative', () => {
const params: ReadFileToolParams = {
absolute_path: path.join(tempRootDir, 'test.txt'),
file_path: path.join(tempRootDir, 'test.txt'),
limit: 0,
};
expect(() => tool.build(params)).toThrow(
@@ -143,7 +141,7 @@ describe('ReadFileTool', () => {
it('should return relative path without limit/offset', () => {
const subDir = path.join(tempRootDir, 'sub', 'dir');
const params: ReadFileToolParams = {
absolute_path: path.join(subDir, 'file.txt'),
file_path: path.join(subDir, 'file.txt'),
};
const invocation = tool.build(params);
expect(typeof invocation).not.toBe('string');
@@ -168,7 +166,7 @@ describe('ReadFileTool', () => {
'limit',
'file.txt',
);
const params: ReadFileToolParams = { absolute_path: deepPath };
const params: ReadFileToolParams = { file_path: deepPath };
const invocation = tool.build(params);
expect(typeof invocation).not.toBe('string');
const desc = (
@@ -181,7 +179,7 @@ describe('ReadFileTool', () => {
it('should handle non-normalized file paths correctly', () => {
const subDir = path.join(tempRootDir, 'sub', 'dir');
const params: ReadFileToolParams = {
absolute_path: path.join(subDir, '..', 'dir', 'file.txt'),
file_path: path.join(subDir, '..', 'dir', 'file.txt'),
};
const invocation = tool.build(params);
expect(typeof invocation).not.toBe('string');
@@ -193,7 +191,7 @@ describe('ReadFileTool', () => {
});
it('should return . if path is the root directory', () => {
const params: ReadFileToolParams = { absolute_path: tempRootDir };
const params: ReadFileToolParams = { file_path: tempRootDir };
const invocation = tool.build(params);
expect(typeof invocation).not.toBe('string');
expect(
@@ -204,42 +202,26 @@ describe('ReadFileTool', () => {
});
});
describe('constructor', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should use windows-style path examples on windows', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
const tool = new ReadFileTool({} as unknown as Config);
const schema = tool.schema;
expect(
(schema.parametersJsonSchema as ReadFileParameterSchema).properties
.absolute_path.description,
).toBe(
"The absolute path to the file to read (e.g., 'C:\\Users\\project\\file.txt'). Relative paths are not supported. You must provide an absolute path.",
);
});
it('should use unix-style path examples on non-windows platforms', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('linux');
const tool = new ReadFileTool({} as unknown as Config);
const schema = tool.schema;
expect(
(schema.parametersJsonSchema as ReadFileParameterSchema).properties
.absolute_path.description,
).toBe(
"The absolute path to the file to read (e.g., '/home/user/project/file.txt'). Relative paths are not supported. You must provide an absolute path.",
);
});
});
describe('execute', () => {
it('should successfully read a file with a relative path', async () => {
const filePath = path.join(tempRootDir, 'textfile.txt');
const fileContent = 'This is a test file.';
await fsp.writeFile(filePath, fileContent, 'utf-8');
const params: ReadFileToolParams = { file_path: 'textfile.txt' };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
>;
expect(await invocation.execute(abortSignal)).toEqual({
llmContent: fileContent,
returnDisplay: '',
});
});
it('should return error if file does not exist', async () => {
const filePath = path.join(tempRootDir, 'nonexistent.txt');
const params: ReadFileToolParams = { absolute_path: filePath };
const params: ReadFileToolParams = { file_path: filePath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -261,7 +243,7 @@ describe('ReadFileTool', () => {
const filePath = path.join(tempRootDir, 'textfile.txt');
const fileContent = 'This is a test file.';
await fsp.writeFile(filePath, fileContent, 'utf-8');
const params: ReadFileToolParams = { absolute_path: filePath };
const params: ReadFileToolParams = { file_path: filePath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -276,7 +258,7 @@ describe('ReadFileTool', () => {
it('should return error if path is a directory', async () => {
const dirPath = path.join(tempRootDir, 'directory');
await fsp.mkdir(dirPath);
const params: ReadFileToolParams = { absolute_path: dirPath };
const params: ReadFileToolParams = { file_path: dirPath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -299,7 +281,7 @@ describe('ReadFileTool', () => {
// 21MB of content exceeds 20MB limit
const largeContent = 'x'.repeat(21 * 1024 * 1024);
await fsp.writeFile(filePath, largeContent, 'utf-8');
const params: ReadFileToolParams = { absolute_path: filePath };
const params: ReadFileToolParams = { file_path: filePath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -318,7 +300,7 @@ describe('ReadFileTool', () => {
const longLine = 'a'.repeat(2500); // Exceeds MAX_LINE_LENGTH_TEXT_FILE (2000)
const fileContent = `Short line\n${longLine}\nAnother short line`;
await fsp.writeFile(filePath, fileContent, 'utf-8');
const params: ReadFileToolParams = { absolute_path: filePath };
const params: ReadFileToolParams = { file_path: filePath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -339,7 +321,7 @@ describe('ReadFileTool', () => {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
]);
await fsp.writeFile(imagePath, pngHeader);
const params: ReadFileToolParams = { absolute_path: imagePath };
const params: ReadFileToolParams = { file_path: imagePath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -360,7 +342,7 @@ describe('ReadFileTool', () => {
// Minimal PDF header
const pdfHeader = Buffer.from('%PDF-1.4');
await fsp.writeFile(pdfPath, pdfHeader);
const params: ReadFileToolParams = { absolute_path: pdfPath };
const params: ReadFileToolParams = { file_path: pdfPath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -381,7 +363,7 @@ describe('ReadFileTool', () => {
// Binary data with null bytes
const binaryData = Buffer.from([0x00, 0xff, 0x00, 0xff]);
await fsp.writeFile(binPath, binaryData);
const params: ReadFileToolParams = { absolute_path: binPath };
const params: ReadFileToolParams = { file_path: binPath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -398,7 +380,7 @@ describe('ReadFileTool', () => {
const svgPath = path.join(tempRootDir, 'image.svg');
const svgContent = '<svg><circle cx="50" cy="50" r="40"/></svg>';
await fsp.writeFile(svgPath, svgContent, 'utf-8');
const params: ReadFileToolParams = { absolute_path: svgPath };
const params: ReadFileToolParams = { file_path: svgPath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -414,7 +396,7 @@ describe('ReadFileTool', () => {
// Create SVG content larger than 1MB
const largeContent = '<svg>' + 'x'.repeat(1024 * 1024 + 1) + '</svg>';
await fsp.writeFile(svgPath, largeContent, 'utf-8');
const params: ReadFileToolParams = { absolute_path: svgPath };
const params: ReadFileToolParams = { file_path: svgPath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -432,7 +414,7 @@ describe('ReadFileTool', () => {
it('should handle empty file', async () => {
const emptyPath = path.join(tempRootDir, 'empty.txt');
await fsp.writeFile(emptyPath, '', 'utf-8');
const params: ReadFileToolParams = { absolute_path: emptyPath };
const params: ReadFileToolParams = { file_path: emptyPath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -450,7 +432,7 @@ describe('ReadFileTool', () => {
await fsp.writeFile(filePath, fileContent, 'utf-8');
const params: ReadFileToolParams = {
absolute_path: filePath,
file_path: filePath,
offset: 5, // Start from line 6
limit: 3,
};
@@ -481,7 +463,7 @@ describe('ReadFileTool', () => {
const tempFileContent = 'This is temporary output content';
await fsp.writeFile(tempFilePath, tempFileContent, 'utf-8');
const params: ReadFileToolParams = { absolute_path: tempFilePath };
const params: ReadFileToolParams = { file_path: tempFilePath };
const invocation = tool.build(params) as ToolInvocation<
ReadFileToolParams,
ToolResult
@@ -498,13 +480,27 @@ describe('ReadFileTool', () => {
path.join(tempRootDir, '.geminiignore'),
['foo.*', 'ignored/'].join('\n'),
);
const mockConfigInstance = {
getFileService: () => new FileDiscoveryService(tempRootDir),
getFileSystemService: () => new StandardFileSystemService(),
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => new WorkspaceContext(tempRootDir),
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
storage: {
getProjectTempDir: () => path.join(tempRootDir, '.temp'),
},
} as unknown as Config;
tool = new ReadFileTool(mockConfigInstance);
});
it('should throw error if path is ignored by a .geminiignore pattern', async () => {
const ignoredFilePath = path.join(tempRootDir, 'foo.bar');
await fsp.writeFile(ignoredFilePath, 'content', 'utf-8');
const params: ReadFileToolParams = {
absolute_path: ignoredFilePath,
file_path: ignoredFilePath,
};
const expectedError = `File path '${ignoredFilePath}' is ignored by configured ignore patterns.`;
expect(() => tool.build(params)).toThrow(expectedError);
@@ -516,7 +512,7 @@ describe('ReadFileTool', () => {
const ignoredFilePath = path.join(ignoredDirPath, 'file.txt');
await fsp.writeFile(ignoredFilePath, 'content', 'utf-8');
const params: ReadFileToolParams = {
absolute_path: ignoredFilePath,
file_path: ignoredFilePath,
};
const expectedError = `File path '${ignoredFilePath}' is ignored by configured ignore patterns.`;
expect(() => tool.build(params)).toThrow(expectedError);
@@ -526,7 +522,7 @@ describe('ReadFileTool', () => {
const allowedFilePath = path.join(tempRootDir, 'allowed.txt');
await fsp.writeFile(allowedFilePath, 'content', 'utf-8');
const params: ReadFileToolParams = {
absolute_path: allowedFilePath,
file_path: allowedFilePath,
};
const invocation = tool.build(params);
expect(typeof invocation).not.toBe('string');

View File

@@ -6,7 +6,6 @@
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import path from 'node:path';
import process from 'node:process';
import { makeRelative, shortenPath } from '../utils/paths.js';
import type { ToolInvocation, ToolLocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
@@ -28,9 +27,9 @@ import { READ_FILE_TOOL_NAME } from './tool-names.js';
*/
export interface ReadFileToolParams {
/**
* The absolute path to the file to read
* The path to the file to read
*/
absolute_path: string;
file_path: string;
/**
* The line number to start reading from (optional)
@@ -47,6 +46,7 @@ class ReadFileToolInvocation extends BaseToolInvocation<
ReadFileToolParams,
ToolResult
> {
private readonly resolvedPath: string;
constructor(
private config: Config,
params: ReadFileToolParams,
@@ -55,23 +55,27 @@ class ReadFileToolInvocation extends BaseToolInvocation<
_toolDisplayName?: string,
) {
super(params, messageBus, _toolName, _toolDisplayName);
this.resolvedPath = path.resolve(
this.config.getTargetDir(),
this.params.file_path,
);
}
getDescription(): string {
const relativePath = makeRelative(
this.params.absolute_path,
this.resolvedPath,
this.config.getTargetDir(),
);
return shortenPath(relativePath);
}
override toolLocations(): ToolLocation[] {
return [{ path: this.params.absolute_path, line: this.params.offset }];
return [{ path: this.resolvedPath, line: this.params.offset }];
}
async execute(): Promise<ToolResult> {
const result = await processSingleFileContent(
this.params.absolute_path,
this.resolvedPath,
this.config.getTargetDir(),
this.config.getFileSystemService(),
this.params.offset,
@@ -111,9 +115,9 @@ ${result.llmContent}`;
typeof result.llmContent === 'string'
? result.llmContent.split('\n').length
: undefined;
const mimetype = getSpecificMimeType(this.params.absolute_path);
const mimetype = getSpecificMimeType(this.resolvedPath);
const programming_language = getProgrammingLanguage({
absolute_path: this.params.absolute_path,
file_path: this.resolvedPath,
});
logFileOperation(
this.config,
@@ -122,7 +126,7 @@ ${result.llmContent}`;
FileOperation.READ,
lines,
mimetype,
path.extname(this.params.absolute_path),
path.extname(this.resolvedPath),
programming_language,
),
);
@@ -154,11 +158,8 @@ export class ReadFileTool extends BaseDeclarativeTool<
Kind.Read,
{
properties: {
absolute_path: {
description:
process.platform === 'win32'
? "The absolute path to the file to read (e.g., 'C:\\Users\\project\\file.txt'). Relative paths are not supported. You must provide an absolute path."
: "The absolute path to the file to read (e.g., '/home/user/project/file.txt'). Relative paths are not supported. You must provide an absolute path.",
file_path: {
description: 'The path to the file to read.',
type: 'string',
},
offset: {
@@ -172,7 +173,7 @@ export class ReadFileTool extends BaseDeclarativeTool<
type: 'number',
},
},
required: ['absolute_path'],
required: ['file_path'],
type: 'object',
},
true,
@@ -184,24 +185,25 @@ export class ReadFileTool extends BaseDeclarativeTool<
protected override validateToolParamValues(
params: ReadFileToolParams,
): string | null {
const filePath = params.absolute_path;
if (params.absolute_path.trim() === '') {
return "The 'absolute_path' parameter must be non-empty.";
}
if (!path.isAbsolute(filePath)) {
return `File path must be absolute, but was relative: ${filePath}. You must provide an absolute path.`;
if (params.file_path.trim() === '') {
return "The 'file_path' parameter must be non-empty.";
}
const workspaceContext = this.config.getWorkspaceContext();
const projectTempDir = this.config.storage.getProjectTempDir();
const resolvedFilePath = path.resolve(filePath);
const resolvedPath = path.resolve(
this.config.getTargetDir(),
params.file_path,
);
const resolvedProjectTempDir = path.resolve(projectTempDir);
const isWithinTempDir =
resolvedFilePath.startsWith(resolvedProjectTempDir + path.sep) ||
resolvedFilePath === resolvedProjectTempDir;
resolvedPath.startsWith(resolvedProjectTempDir + path.sep) ||
resolvedPath === resolvedProjectTempDir;
if (!workspaceContext.isPathWithinWorkspace(filePath) && !isWithinTempDir) {
if (
!workspaceContext.isPathWithinWorkspace(resolvedPath) &&
!isWithinTempDir
) {
const directories = workspaceContext.getDirectories();
return `File path must be within one of the workspace directories: ${directories.join(', ')} or within the project temp directory: ${projectTempDir}`;
}
@@ -214,10 +216,8 @@ export class ReadFileTool extends BaseDeclarativeTool<
const fileService = this.config.getFileService();
const fileFilteringOptions = this.config.getFileFilteringOptions();
if (
fileService.shouldIgnoreFile(params.absolute_path, fileFilteringOptions)
) {
return `File path '${filePath}' is ignored by configured ignore patterns.`;
if (fileService.shouldIgnoreFile(resolvedPath, fileFilteringOptions)) {
return `File path '${resolvedPath}' is ignored by configured ignore patterns.`;
}
return null;

View File

@@ -144,56 +144,56 @@ describe('ReadManyFilesTool', () => {
describe('build', () => {
it('should return an invocation for valid relative paths within root', () => {
const params = { paths: ['file1.txt', 'subdir/file2.txt'] };
const params = { include: ['file1.txt', 'subdir/file2.txt'] };
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should return an invocation for valid glob patterns within root', () => {
const params = { paths: ['*.txt', 'subdir/**/*.js'] };
const params = { include: ['*.txt', 'subdir/**/*.js'] };
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should return an invocation for paths trying to escape the root (e.g., ../) as execute handles this', () => {
const params = { paths: ['../outside.txt'] };
const params = { include: ['../outside.txt'] };
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should return an invocation for absolute paths as execute handles this', () => {
const params = { paths: [path.join(tempDirOutsideRoot, 'absolute.txt')] };
const params = {
include: [path.join(tempDirOutsideRoot, 'absolute.txt')],
};
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should throw error if paths array is empty', () => {
const params = { paths: [] };
const params = { include: [] };
expect(() => tool.build(params)).toThrow(
'params/paths must NOT have fewer than 1 items',
'params/include must NOT have fewer than 1 items',
);
});
it('should return an invocation for valid exclude and include patterns', () => {
const params = {
paths: ['src/**/*.ts'],
exclude: ['**/*.test.ts'],
include: ['src/utils/*.ts'],
include: ['src/**/*.ts', 'src/utils/*.ts'],
};
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should throw error if paths array contains an empty string', () => {
const params = { paths: ['file1.txt', ''] };
const params = { include: ['file1.txt', ''] };
expect(() => tool.build(params)).toThrow(
'params/paths/1 must NOT have fewer than 1 characters',
'params/include/1 must NOT have fewer than 1 characters',
);
});
it('should throw error if include array contains non-string elements', () => {
const params = {
paths: ['file1.txt'],
include: ['*.ts', 123] as string[],
};
expect(() => tool.build(params)).toThrow(
@@ -203,7 +203,7 @@ describe('ReadManyFilesTool', () => {
it('should throw error if exclude array contains non-string elements', () => {
const params = {
paths: ['file1.txt'],
include: ['file1.txt'],
exclude: ['*.log', {}] as string[],
};
expect(() => tool.build(params)).toThrow(
@@ -226,7 +226,7 @@ describe('ReadManyFilesTool', () => {
it('should read a single specified file', async () => {
createFile('file1.txt', 'Content of file1');
const params = { paths: ['file1.txt'] };
const params = { include: ['file1.txt'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const expectedPath = path.join(tempRootDir, 'file1.txt');
@@ -242,7 +242,7 @@ describe('ReadManyFilesTool', () => {
it('should read multiple specified files', async () => {
createFile('file1.txt', 'Content1');
createFile('subdir/file2.js', 'Content2');
const params = { paths: ['file1.txt', 'subdir/file2.js'] };
const params = { include: ['file1.txt', 'subdir/file2.js'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
@@ -267,7 +267,7 @@ describe('ReadManyFilesTool', () => {
createFile('file.txt', 'Text file');
createFile('another.txt', 'Another text');
createFile('sub/data.json', '{}');
const params = { paths: ['*.txt'] };
const params = { include: ['*.txt'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
@@ -292,7 +292,7 @@ describe('ReadManyFilesTool', () => {
it('should respect exclude patterns', async () => {
createFile('src/main.ts', 'Main content');
createFile('src/main.test.ts', 'Test content');
const params = { paths: ['src/**/*.ts'], exclude: ['**/*.test.ts'] };
const params = { include: ['src/**/*.ts'], exclude: ['**/*.test.ts'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
@@ -310,7 +310,7 @@ describe('ReadManyFilesTool', () => {
});
it('should handle nonexistent specific files gracefully', async () => {
const params = { paths: ['nonexistent-file.txt'] };
const params = { include: ['nonexistent-file.txt'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
@@ -324,7 +324,7 @@ describe('ReadManyFilesTool', () => {
it('should use default excludes', async () => {
createFile('node_modules/some-lib/index.js', 'lib code');
createFile('src/app.js', 'app code');
const params = { paths: ['**/*.js'] };
const params = { include: ['**/*.js'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
@@ -344,7 +344,7 @@ describe('ReadManyFilesTool', () => {
it('should NOT use default excludes if useDefaultExcludes is false', async () => {
createFile('node_modules/some-lib/index.js', 'lib code');
createFile('src/app.js', 'app code');
const params = { paths: ['**/*.js'], useDefaultExcludes: false };
const params = { include: ['**/*.js'], useDefaultExcludes: false };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
@@ -373,7 +373,7 @@ describe('ReadManyFilesTool', () => {
'image.png',
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
);
const params = { paths: ['*.png'] }; // Explicitly requesting .png
const params = { include: ['*.png'] }; // Explicitly requesting .png
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
@@ -397,7 +397,7 @@ describe('ReadManyFilesTool', () => {
'myExactImage.png',
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
);
const params = { paths: ['myExactImage.png'] }; // Explicitly requesting by full name
const params = { include: ['myExactImage.png'] }; // Explicitly requesting by full name
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
@@ -416,7 +416,7 @@ describe('ReadManyFilesTool', () => {
it('should skip PDF files if not explicitly requested by extension or name', async () => {
createBinaryFile('document.pdf', Buffer.from('%PDF-1.4...'));
createFile('notes.txt', 'text notes');
const params = { paths: ['*'] }; // Generic glob, not specific to .pdf
const params = { include: ['*'] }; // Generic glob, not specific to .pdf
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
@@ -436,7 +436,7 @@ describe('ReadManyFilesTool', () => {
it('should include PDF files as inlineData parts if explicitly requested by extension', async () => {
createBinaryFile('important.pdf', Buffer.from('%PDF-1.4...'));
const params = { paths: ['*.pdf'] }; // Explicitly requesting .pdf files
const params = { include: ['*.pdf'] }; // Explicitly requesting .pdf files
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
@@ -452,7 +452,7 @@ describe('ReadManyFilesTool', () => {
it('should include PDF files as inlineData parts if explicitly requested by name', async () => {
createBinaryFile('report-final.pdf', Buffer.from('%PDF-1.4...'));
const params = { paths: ['report-final.pdf'] };
const params = { include: ['report-final.pdf'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
@@ -470,7 +470,7 @@ describe('ReadManyFilesTool', () => {
createFile('foo.bar', '');
createFile('bar.ts', '');
createFile('foo.quux', '');
const params = { paths: ['foo.bar', 'bar.ts', 'foo.quux'] };
const params = { include: ['foo.bar', 'bar.ts', 'foo.quux'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.returnDisplay).not.toContain('foo.bar');
@@ -508,7 +508,7 @@ describe('ReadManyFilesTool', () => {
fs.writeFileSync(path.join(tempDir1, 'file1.txt'), 'Content1');
fs.writeFileSync(path.join(tempDir2, 'file2.txt'), 'Content2');
const params = { paths: ['*.txt'] };
const params = { include: ['*.txt'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
@@ -544,7 +544,7 @@ describe('ReadManyFilesTool', () => {
);
createFile('large-file.txt', longContent);
const params = { paths: ['*.txt'] };
const params = { include: ['*.txt'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
@@ -568,7 +568,7 @@ describe('ReadManyFilesTool', () => {
it('should read files with special characters like [] and () in the path', async () => {
const filePath = 'src/app/[test]/(dashboard)/testing/components/code.tsx';
createFile(filePath, 'Content of receive-detail');
const params = { paths: [filePath] };
const params = { include: [filePath] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const expectedPath = path.join(tempRootDir, filePath);
@@ -587,7 +587,7 @@ Content of receive-detail
it('should read files with special characters in the name', async () => {
createFile('file[1].txt', 'Content of file[1]');
const params = { paths: ['file[1].txt'] };
const params = { include: ['file[1].txt'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const expectedPath = path.join(tempRootDir, 'file[1].txt');
@@ -607,15 +607,15 @@ Content of file[1]
describe('Error handling', () => {
it('should return an INVALID_TOOL_PARAMS error if no paths are provided', async () => {
const params = { paths: [], include: [] };
const params = { include: [] };
expect(() => {
tool.build(params);
}).toThrow('params/paths must NOT have fewer than 1 items');
}).toThrow('params/include must NOT have fewer than 1 items');
});
it('should return a READ_MANY_FILES_SEARCH_ERROR on glob failure', async () => {
vi.mocked(glob.glob).mockRejectedValue(new Error('Glob failed'));
const params = { paths: ['*.txt'] };
const params = { include: ['*.txt'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.error?.type).toBe(
@@ -661,7 +661,7 @@ Content of file[1]
return 'text';
});
const params = { paths: files };
const params = { include: files };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
@@ -684,7 +684,7 @@ Content of file[1]
createFile('valid3.txt', 'Valid content 3');
const params = {
paths: [
include: [
'valid1.txt',
'valid2.txt',
'nonexistent-file.txt', // This will fail
@@ -730,7 +730,7 @@ Content of file[1]
return 'text';
});
const invocation = tool.build({ paths: files });
const invocation = tool.build({ include: files });
await invocation.execute(new AbortController().signal);
// Verify concurrent execution pattern

View File

@@ -35,18 +35,10 @@ import { READ_MANY_FILES_TOOL_NAME } from './tool-names.js';
*/
export interface ReadManyFilesParams {
/**
* An array of file paths or directory paths to search within.
* Paths are relative to the tool's configured target directory.
* Glob patterns can be used directly in these paths.
*/
paths: string[];
/**
* Optional. Glob patterns for files to include.
* These are effectively combined with the `paths`.
* Glob patterns for files to include.
* Example: ["*.ts", "src/** /*.md"]
*/
include?: string[];
include: string[];
/**
* Optional. Glob patterns for files/directories to exclude.
@@ -123,9 +115,8 @@ class ReadManyFilesToolInvocation extends BaseToolInvocation<
}
getDescription(): string {
const allPatterns = [...this.params.paths, ...(this.params.include || [])];
const pathDesc = `using patterns:
${allPatterns.join('`, `')}
${this.params.include.join('`, `')}
(within target directory:
${this.config.getTargetDir()}
) `;
@@ -156,12 +147,7 @@ ${finalExclusionPatternsForDescription
}
async execute(signal: AbortSignal): Promise<ToolResult> {
const {
paths: inputPatterns,
include = [],
exclude = [],
useDefaultExcludes = true,
} = this.params;
const { include, exclude = [], useDefaultExcludes = true } = this.params;
const filesToConsider = new Set<string>();
const skippedFiles: Array<{ path: string; reason: string }> = [];
@@ -172,14 +158,13 @@ ${finalExclusionPatternsForDescription
? [...getDefaultExcludes(this.config), ...exclude]
: [...exclude];
const searchPatterns = [...inputPatterns, ...include];
try {
const allEntries = new Set<string>();
const workspaceDirs = this.config.getWorkspaceContext().getDirectories();
for (const dir of workspaceDirs) {
const processedPatterns = [];
for (const p of searchPatterns) {
for (const p of include) {
const normalizedP = p.replace(/\\/g, '/');
const fullPath = path.join(dir, normalizedP);
if (fs.existsSync(fullPath)) {
@@ -272,7 +257,7 @@ ${finalExclusionPatternsForDescription
filePath,
fileExtension,
);
const requestedExplicitly = inputPatterns.some(
const requestedExplicitly = include.some(
(pattern: string) =>
pattern.toLowerCase().includes(fileExtension) ||
pattern.includes(fileNameWithoutExtension),
@@ -367,7 +352,7 @@ ${finalExclusionPatternsForDescription
: undefined;
const mimetype = getSpecificMimeType(filePath);
const programming_language = getProgrammingLanguage({
absolute_path: filePath,
file_path: filePath,
});
logFileOperation(
this.config,
@@ -463,7 +448,7 @@ export class ReadManyFilesTool extends BaseDeclarativeTool<
const parameterSchema = {
type: 'object',
properties: {
paths: {
include: {
type: 'array',
items: {
type: 'string',
@@ -471,17 +456,7 @@ export class ReadManyFilesTool extends BaseDeclarativeTool<
},
minItems: 1,
description:
"Required. An array of glob patterns or paths relative to the tool's target directory. Examples: ['src/**/*.ts'], ['README.md', 'docs/']",
},
include: {
type: 'array',
items: {
type: 'string',
minLength: 1,
},
description:
'Optional. Additional glob patterns to include. These are merged with `paths`. Example: "*.test.ts" to specifically add test files if they were broadly excluded.',
default: [],
'An array of glob patterns or paths. Examples: ["src/**/*.ts"], ["README.md", "docs/"]',
},
exclude: {
type: 'array',
@@ -523,13 +498,13 @@ export class ReadManyFilesTool extends BaseDeclarativeTool<
},
},
},
required: ['paths'],
required: ['include'],
};
super(
ReadManyFilesTool.Name,
'ReadManyFiles',
`Reads content from multiple files specified by paths or glob patterns within a configured target directory. For text files, it concatenates their content into a single string. It is primarily designed for text-based files. However, it can also process image (e.g., .png, .jpg) and PDF (.pdf) files if their file names or extensions are explicitly included in the 'paths' argument. For these explicitly requested non-text files, their data is read and included in a format suitable for model consumption (e.g., base64 encoded).
`Reads content from multiple files specified by glob patterns within a configured target directory. For text files, it concatenates their content into a single string. It is primarily designed for text-based files. However, it can also process image (e.g., .png, .jpg) and PDF (.pdf) files if their file names or extensions are explicitly included in the 'include' argument. For these explicitly requested non-text files, their data is read and included in a format suitable for model consumption (e.g., base64 encoded).
This tool is useful when you need to understand or analyze a collection of files, such as:
- Getting an overview of a codebase or parts of it (e.g., all TypeScript files in the 'src' directory).
@@ -538,7 +513,7 @@ This tool is useful when you need to understand or analyze a collection of files
- Gathering context from multiple configuration files.
- When the user asks to "read all files in X directory" or "show me the content of all Y files".
Use this tool when the user's query implies needing the content of several files simultaneously for context, analysis, or summarization. For text files, it uses default UTF-8 encoding and a '--- {filePath} ---' separator between file contents. The tool inserts a '--- End of content ---' after the last file. Ensure paths are relative to the target directory. Glob patterns like 'src/**/*.js' are supported. Avoid using for single files if a more specific single-file reading tool is available, unless the user specifically requests to process a list containing just one file via this tool. Other binary files (not explicitly requested as image/PDF) are generally skipped. Default excludes apply to common non-text files (except for explicitly requested images/PDFs) and large dependency directories unless 'useDefaultExcludes' is false.`,
Use this tool when the user's query implies needing the content of several files simultaneously for context, analysis, or summarization. For text files, it uses default UTF-8 encoding and a '--- {filePath} ---' separator between file contents. The tool inserts a '--- End of content ---' after the last file. Ensure glob patterns are relative to the target directory. Glob patterns like 'src/**/*.js' are supported. Avoid using for single files if a more specific single-file reading tool is available, unless the user specifically requests to process a list containing just one file via this tool. Other binary files (not explicitly requested as image/PDF) are generally skipped. Default excludes apply to common non-text files (except for explicitly requested images/PDFs) and large dependency directories unless 'useDefaultExcludes' is false.`,
Kind.Read,
parameterSchema,
true, // isOutputMarkdown

View File

@@ -307,21 +307,21 @@ describe('RipGrepTool', () => {
});
it('should return null for valid params (pattern and path)', () => {
const params: RipGrepToolParams = { pattern: 'hello', path: '.' };
const params: RipGrepToolParams = { pattern: 'hello', dir_path: '.' };
expect(grepTool.validateToolParams(params)).toBeNull();
});
it('should return null for valid params (pattern, path, and include)', () => {
const params: RipGrepToolParams = {
pattern: 'hello',
path: '.',
dir_path: '.',
include: '*.txt',
};
expect(grepTool.validateToolParams(params)).toBeNull();
});
it('should return error if pattern is missing', () => {
const params = { path: '.' } as unknown as RipGrepToolParams;
const params = { dir_path: '.' } as unknown as RipGrepToolParams;
expect(grepTool.validateToolParams(params)).toBe(
`params must have required property 'pattern'`,
);
@@ -335,7 +335,7 @@ describe('RipGrepTool', () => {
it('should return error if path does not exist', () => {
const params: RipGrepToolParams = {
pattern: 'hello',
path: 'nonexistent',
dir_path: 'nonexistent',
};
// Check for the core error message, as the full path might vary
expect(grepTool.validateToolParams(params)).toContain(
@@ -346,7 +346,10 @@ describe('RipGrepTool', () => {
it('should return error if path is a file, not a directory', async () => {
const filePath = path.join(tempRootDir, 'fileA.txt');
const params: RipGrepToolParams = { pattern: 'hello', path: filePath };
const params: RipGrepToolParams = {
pattern: 'hello',
dir_path: filePath,
};
expect(grepTool.validateToolParams(params)).toContain(
`Path is not a directory: ${filePath}`,
);
@@ -387,7 +390,7 @@ describe('RipGrepTool', () => {
}),
);
const params: RipGrepToolParams = { pattern: 'world', path: 'sub' };
const params: RipGrepToolParams = { pattern: 'world', dir_path: 'sub' };
const invocation = grepTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain(
@@ -464,7 +467,7 @@ describe('RipGrepTool', () => {
const params: RipGrepToolParams = {
pattern: 'hello',
path: 'sub',
dir_path: 'sub',
include: '*.js',
};
const invocation = grepTool.build(params);
@@ -613,7 +616,7 @@ describe('RipGrepTool', () => {
});
it('should throw an error if params are invalid', async () => {
const params = { path: '.' } as unknown as RipGrepToolParams; // Invalid: pattern missing
const params = { dir_path: '.' } as unknown as RipGrepToolParams; // Invalid: pattern missing
expect(() => grepTool.build(params)).toThrow(
/params must have required property 'pattern'/,
);
@@ -799,7 +802,7 @@ describe('RipGrepTool', () => {
const multiDirGrepTool = new RipGrepTool(multiDirConfig);
// Search only in the 'sub' directory of the first workspace
const params: RipGrepToolParams = { pattern: 'world', path: 'sub' };
const params: RipGrepToolParams = { pattern: 'world', dir_path: 'sub' };
const invocation = multiDirGrepTool.build(params);
const result = await invocation.execute(abortSignal);
@@ -881,7 +884,10 @@ describe('RipGrepTool', () => {
describe('error handling and edge cases', () => {
it('should handle workspace boundary violations', () => {
const params: RipGrepToolParams = { pattern: 'test', path: '../outside' };
const params: RipGrepToolParams = {
pattern: 'test',
dir_path: '../outside',
};
expect(() => grepTool.build(params)).toThrow(/Path validation failed/);
});
@@ -918,7 +924,7 @@ describe('RipGrepTool', () => {
return mockProcess as unknown as ChildProcess;
});
const params: RipGrepToolParams = { pattern: 'test', path: 'empty' };
const params: RipGrepToolParams = { pattern: 'test', dir_path: 'empty' };
const invocation = grepTool.build(params);
const result = await invocation.execute(abortSignal);
@@ -1374,7 +1380,7 @@ describe('RipGrepTool', () => {
await fs.mkdir(dirPath, { recursive: true });
const params: RipGrepToolParams = {
pattern: 'testPattern',
path: path.join('src', 'app'),
dir_path: path.join('src', 'app'),
};
const invocation = grepTool.build(params);
// The path will be relative to the tempRootDir, so we check for containment.
@@ -1405,7 +1411,7 @@ describe('RipGrepTool', () => {
const params: RipGrepToolParams = {
pattern: 'testPattern',
include: '*.ts',
path: path.join('src', 'app'),
dir_path: path.join('src', 'app'),
};
const invocation = grepTool.build(params);
expect(invocation.getDescription()).toContain(
@@ -1415,7 +1421,10 @@ describe('RipGrepTool', () => {
});
it('should use ./ for root path in description', () => {
const params: RipGrepToolParams = { pattern: 'testPattern', path: '.' };
const params: RipGrepToolParams = {
pattern: 'testPattern',
dir_path: '.',
};
const invocation = grepTool.build(params);
expect(invocation.getDescription()).toBe("'testPattern' within ./");
});

View File

@@ -88,7 +88,7 @@ export interface RipGrepToolParams {
/**
* The directory to search in (optional, defaults to current directory relative to root)
*/
path?: string;
dir_path?: string;
/**
* File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")
@@ -163,8 +163,8 @@ class GrepToolInvocation extends BaseToolInvocation<
async execute(signal: AbortSignal): Promise<ToolResult> {
try {
const workspaceContext = this.config.getWorkspaceContext();
const searchDirAbs = this.resolveAndValidatePath(this.params.path);
const searchDirDisplay = this.params.path || '.';
const searchDirAbs = this.resolveAndValidatePath(this.params.dir_path);
const searchDirDisplay = this.params.dir_path || '.';
// Determine which directories to search
let searchDirectories: readonly string[];
@@ -416,14 +416,14 @@ class GrepToolInvocation extends BaseToolInvocation<
if (this.params.include) {
description += ` in ${this.params.include}`;
}
if (this.params.path) {
if (this.params.dir_path) {
const resolvedPath = path.resolve(
this.config.getTargetDir(),
this.params.path,
this.params.dir_path,
);
if (
resolvedPath === this.config.getTargetDir() ||
this.params.path === '.'
this.params.dir_path === '.'
) {
description += ` within ./`;
} else {
@@ -470,7 +470,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
"The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').",
type: 'string',
},
path: {
dir_path: {
description:
'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.',
type: 'string',
@@ -546,9 +546,9 @@ export class RipGrepTool extends BaseDeclarativeTool<
}
// Only validate path if one is provided
if (params.path) {
if (params.dir_path) {
try {
this.resolveAndValidatePath(params.path);
this.resolveAndValidatePath(params.dir_path);
} catch (error) {
return getErrorMessage(error);
}

View File

@@ -15,12 +15,24 @@ import {
type Mock,
} from 'vitest';
const mockPlatform = vi.hoisted(() => vi.fn());
const mockShellExecutionService = vi.hoisted(() => vi.fn());
vi.mock('../services/shellExecutionService.js', () => ({
ShellExecutionService: { execute: mockShellExecutionService },
}));
vi.mock('fs');
vi.mock('os');
vi.mock('node:os', async (importOriginal) => {
const actualOs = await importOriginal<typeof os>();
return {
...actualOs,
default: {
...actualOs,
platform: mockPlatform,
},
platform: mockPlatform,
};
});
vi.mock('crypto');
vi.mock('../utils/summarizer.js');
@@ -43,32 +55,40 @@ import * as summarizer from '../utils/summarizer.js';
import { ToolErrorType } from './tool-error.js';
import { ToolConfirmationOutcome } from './tools.js';
import { OUTPUT_UPDATE_INTERVAL_MS } from './shell.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { SHELL_TOOL_NAME } from './tool-names.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
const originalComSpec = process.env['ComSpec'];
const itWindowsOnly = process.platform === 'win32' ? it : it.skip;
describe('ShellTool', () => {
beforeAll(async () => {
await initializeShellParsers();
});
let shellTool: ShellTool;
let mockConfig: Config;
let mockShellOutputCallback: (event: ShellOutputEvent) => void;
let resolveExecutionPromise: (result: ShellExecutionResult) => void;
let tempRootDir: string;
beforeEach(() => {
vi.clearAllMocks();
tempRootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'shell-test-'));
fs.mkdirSync(path.join(tempRootDir, 'subdir'));
mockConfig = {
getAllowedTools: vi.fn().mockReturnValue([]),
getApprovalMode: vi.fn().mockReturnValue('strict'),
getCoreTools: vi.fn().mockReturnValue([]),
getExcludeTools: vi.fn().mockReturnValue([]),
getDebugMode: vi.fn().mockReturnValue(false),
getTargetDir: vi.fn().mockReturnValue('/test/dir'),
getTargetDir: vi.fn().mockReturnValue(tempRootDir),
getSummarizeToolOutputConfig: vi.fn().mockReturnValue(undefined),
getWorkspaceContext: vi
.fn()
.mockReturnValue(createMockWorkspaceContext('/test/dir')),
.mockReturnValue(new WorkspaceContext(tempRootDir)),
getGeminiClient: vi.fn(),
getEnableInteractiveShell: vi.fn().mockReturnValue(false),
isInteractive: vi.fn().mockReturnValue(true),
@@ -76,8 +96,7 @@ describe('ShellTool', () => {
shellTool = new ShellTool(mockConfig);
vi.mocked(os.platform).mockReturnValue('linux');
vi.mocked(os.tmpdir).mockReturnValue('/tmp');
mockPlatform.mockReturnValue('linux');
(vi.mocked(crypto.randomBytes) as Mock).mockReturnValue(
Buffer.from('abcdef', 'hex'),
);
@@ -97,6 +116,9 @@ describe('ShellTool', () => {
});
afterEach(() => {
if (fs.existsSync(tempRootDir)) {
fs.rmSync(tempRootDir, { recursive: true, force: true });
}
if (originalComSpec === undefined) {
delete process.env['ComSpec'];
} else {
@@ -135,30 +157,27 @@ describe('ShellTool', () => {
);
});
it('should throw an error for a relative directory path', () => {
expect(() =>
shellTool.build({ command: 'ls', directory: 'rel/path' }),
).toThrow('Directory must be an absolute path.');
it('should return an invocation for a valid relative directory path', () => {
const invocation = shellTool.build({
command: 'ls',
dir_path: 'subdir',
});
expect(invocation).toBeDefined();
});
it('should throw an error for a directory outside the workspace', () => {
(mockConfig.getWorkspaceContext as Mock).mockReturnValue(
createMockWorkspaceContext('/test/dir', ['/another/workspace']),
);
const outsidePath = path.resolve(tempRootDir, '../outside');
expect(() =>
shellTool.build({ command: 'ls', directory: '/not/in/workspace' }),
shellTool.build({ command: 'ls', dir_path: outsidePath }),
).toThrow(
"Directory '/not/in/workspace' is not within any of the registered workspace directories.",
`Directory '${outsidePath}' is not within any of the registered workspace directories.`,
);
});
it('should return an invocation for a valid absolute directory path', () => {
(mockConfig.getWorkspaceContext as Mock).mockReturnValue(
createMockWorkspaceContext('/test/dir', ['/another/workspace']),
);
const invocation = shellTool.build({
command: 'ls',
directory: '/test/dir/subdir',
dir_path: path.join(tempRootDir, 'subdir'),
});
expect(invocation).toBeDefined();
});
@@ -189,32 +208,31 @@ describe('ShellTool', () => {
const promise = invocation.execute(mockAbortSignal);
resolveShellExecution({ pid: 54321 });
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(`54321${EOL}54322${EOL}`); // Service PID and background PID
// Simulate pgrep output file creation by the shell command
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
fs.writeFileSync(tmpFile, `54321${EOL}54322${EOL}`);
const result = await promise;
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
const wrappedCommand = `{ my-command & }; __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
expect(mockShellExecutionService).toHaveBeenCalledWith(
wrappedCommand,
'/test/dir',
tempRootDir,
expect.any(Function),
mockAbortSignal,
false,
{},
);
expect(result.llmContent).toContain('Background PIDs: 54322');
expect(vi.mocked(fs.unlinkSync)).toHaveBeenCalledWith(tmpFile);
// The file should be deleted by the tool
expect(fs.existsSync(tmpFile)).toBe(false);
});
it('should use the provided directory as cwd', async () => {
(mockConfig.getWorkspaceContext as Mock).mockReturnValue(
createMockWorkspaceContext('/test/dir'),
);
it('should use the provided absolute directory as cwd', async () => {
const subdir = path.join(tempRootDir, 'subdir');
const invocation = shellTool.build({
command: 'ls',
directory: '/test/dir/subdir',
dir_path: subdir,
});
const promise = invocation.execute(mockAbortSignal);
resolveShellExecution();
@@ -224,7 +242,28 @@ describe('ShellTool', () => {
const wrappedCommand = `{ ls; }; __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
expect(mockShellExecutionService).toHaveBeenCalledWith(
wrappedCommand,
'/test/dir/subdir',
subdir,
expect.any(Function),
mockAbortSignal,
false,
{},
);
});
it('should use the provided relative directory as cwd', async () => {
const invocation = shellTool.build({
command: 'ls',
dir_path: 'subdir',
});
const promise = invocation.execute(mockAbortSignal);
resolveShellExecution();
await promise;
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
const wrappedCommand = `{ ls; }; __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
expect(mockShellExecutionService).toHaveBeenCalledWith(
wrappedCommand,
path.join(tempRootDir, 'subdir'),
expect.any(Function),
mockAbortSignal,
false,
@@ -235,7 +274,7 @@ describe('ShellTool', () => {
itWindowsOnly(
'should not wrap command on windows',
async () => {
vi.mocked(os.platform).mockReturnValue('win32');
mockPlatform.mockReturnValue('win32');
const invocation = shellTool.build({ command: 'dir' });
const promise = invocation.execute(mockAbortSignal);
resolveShellExecution({
@@ -251,7 +290,7 @@ describe('ShellTool', () => {
await promise;
expect(mockShellExecutionService).toHaveBeenCalledWith(
'dir',
'/test/dir',
tempRootDir,
expect.any(Function),
mockAbortSignal,
false,
@@ -303,12 +342,6 @@ describe('ShellTool', () => {
);
});
it('should throw an error for invalid directory', () => {
expect(() =>
shellTool.build({ command: 'ls', directory: 'nonexistent' }),
).toThrow('Directory must be an absolute path.');
});
it('should summarize output when configured', async () => {
(mockConfig.getSummarizeToolOutputConfig as Mock).mockReturnValue({
[SHELL_TOOL_NAME]: { tokenBudget: 1000 },
@@ -345,15 +378,17 @@ describe('ShellTool', () => {
it('should clean up the temp file on synchronous execution error', async () => {
const error = new Error('sync spawn error');
mockShellExecutionService.mockImplementation(() => {
// Create the temp file before throwing to simulate it being left behind
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
fs.writeFileSync(tmpFile, '');
throw error;
});
vi.mocked(fs.existsSync).mockReturnValue(true); // Pretend the file exists
const invocation = shellTool.build({ command: 'a-command' });
await expect(invocation.execute(mockAbortSignal)).rejects.toThrow(error);
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
expect(vi.mocked(fs.unlinkSync)).toHaveBeenCalledWith(tmpFile);
expect(fs.existsSync(tmpFile)).toBe(false);
});
describe('Streaming to `updateOutput`', () => {
@@ -501,18 +536,15 @@ describe('ShellTool', () => {
describe('getDescription', () => {
it('should return the windows description when on windows', () => {
vi.mocked(os.platform).mockReturnValue('win32');
mockPlatform.mockReturnValue('win32');
const shellTool = new ShellTool(mockConfig);
expect(shellTool.description).toMatchSnapshot();
});
it('should return the non-windows description when not on windows', () => {
vi.mocked(os.platform).mockReturnValue('linux');
mockPlatform.mockReturnValue('linux');
const shellTool = new ShellTool(mockConfig);
expect(shellTool.description).toMatchSnapshot();
});
});
});
beforeAll(async () => {
await initializeShellParsers();
});

View File

@@ -49,7 +49,7 @@ export const OUTPUT_UPDATE_INTERVAL_MS = 1000;
export interface ShellToolParams {
command: string;
description?: string;
directory?: string;
dir_path?: string;
}
export class ShellToolInvocation extends BaseToolInvocation<
@@ -71,8 +71,8 @@ export class ShellToolInvocation extends BaseToolInvocation<
let description = `${this.params.command}`;
// append optional [in directory]
// note description is needed even if validation fails due to absolute path
if (this.params.directory) {
description += ` [in ${this.params.directory}]`;
if (this.params.dir_path) {
description += ` [in ${this.params.dir_path}]`;
} else {
description += ` [current working directory ${process.cwd()}]`;
}
@@ -161,7 +161,9 @@ export class ShellToolInvocation extends BaseToolInvocation<
return `{ ${command} }; __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`;
})();
const cwd = this.params.directory || this.config.getTargetDir();
const cwd = this.params.dir_path
? path.resolve(this.config.getTargetDir(), this.params.dir_path)
: this.config.getTargetDir();
let cumulativeOutput: string | AnsiOutput = '';
let lastUpdateTime = Date.now();
@@ -260,7 +262,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
llmContent = [
`Command: ${this.params.command}`,
`Directory: ${this.params.directory || '(root)'}`,
`Directory: ${this.params.dir_path || '(root)'}`,
`Output: ${result.output || '(empty)'}`,
`Error: ${finalError}`, // Use the cleaned error string.
`Exit Code: ${result.exitCode ?? '(none)'}`,
@@ -403,10 +405,10 @@ export class ShellTool extends BaseDeclarativeTool<
description:
'Brief description of the command for the user. Be specific and concise. Ideally a single sentence. Can be up to 3 sentences for clarity. No line breaks.',
},
directory: {
dir_path: {
type: 'string',
description:
'(OPTIONAL) The absolute path of the directory to run the command in. If not provided, the project root directory is used. Must be a directory within the workspace and must already exist.',
'(OPTIONAL) The path of the directory to run the command in. If not provided, the project root directory is used. Must be a directory within the workspace and must already exist.',
},
},
required: ['command'],
@@ -437,17 +439,14 @@ export class ShellTool extends BaseDeclarativeTool<
if (getCommandRoots(params.command).length === 0) {
return 'Could not identify command root to obtain permission from user.';
}
if (params.directory) {
if (!path.isAbsolute(params.directory)) {
return 'Directory must be an absolute path.';
}
const workspaceDirs = this.config.getWorkspaceContext().getDirectories();
const isWithinWorkspace = workspaceDirs.some((wsDir) =>
params.directory!.startsWith(wsDir),
if (params.dir_path) {
const resolvedPath = path.resolve(
this.config.getTargetDir(),
params.dir_path,
);
if (!isWithinWorkspace) {
return `Directory '${params.directory}' is not within any of the registered workspace directories.`;
const workspaceContext = this.config.getWorkspaceContext();
if (!workspaceContext.isPathWithinWorkspace(resolvedPath)) {
return `Directory '${resolvedPath}' is not within any of the registered workspace directories.`;
}
}
return null;

View File

@@ -334,7 +334,7 @@ export function getErrorReplaceResult(
*/
export interface EditToolParams {
/**
* The absolute path to the file to modify
* The path to the file to modify
*/
file_path: string;
@@ -853,20 +853,18 @@ export class SmartEditTool
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 and that \`old_string\` and \`new_string\` are different.
4. \`instruction\` is the detailed instruction of what needs to be changed. It is important to Make it specific and detailed so developers or large language models can understand what needs to be changed and perform the changes on their own if necessary.
5. NEVER escape \`old_string\` or \`new_string\`, that would break the exact literal text requirement.
1. \`old_string\` MUST be the exact literal text to replace (including all whitespace, indentation, newlines, and surrounding code etc.).
2. \`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 and that \`old_string\` and \`new_string\` are different.
3. \`instruction\` is the detailed instruction of what needs to be changed. It is important to Make it specific and detailed so developers or large language models can understand what needs to be changed and perform the changes on their own if necessary.
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.
6. Prefer to break down complex and long changes into multiple smaller atomic calls to this tool. Always check the content of the file after changes or not finding a string to match.
5. Prefer to break down complex and long changes into multiple smaller atomic calls to this tool. Always check the content of the file after changes or not finding a string to match.
**Multiple replacements:** If there are multiple and ambiguous occurences of the \`old_string\` in the file, the tool will also fail.`,
Kind.Edit,
{
properties: {
file_path: {
description:
"The absolute path to the file to modify. Must start with '/'.",
description: 'The path to the file to modify.',
type: 'string',
},
instruction: {
@@ -923,11 +921,10 @@ A good instruction should concisely answer:
if (!path.isAbsolute(filePath)) {
// Attempt to auto-correct to an absolute path
const result = correctPath(filePath, this.config);
if (result.success) {
filePath = result.correctedPath;
} else {
if (!result.success) {
return result.error;
}
filePath = result.correctedPath;
}
params.file_path = filePath;

View File

@@ -32,10 +32,10 @@ import {
ensureCorrectEdit,
ensureCorrectFileContent,
} from '../utils/editCorrector.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { StandardFileSystemService } from '../services/fileSystemService.js';
import type { DiffUpdateResult } from '../ide/ide-client.js';
import { IdeClient } from '../ide/ide-client.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
const rootDir = path.resolve(os.tmpdir(), 'gemini-cli-test-root');
@@ -75,7 +75,7 @@ const mockConfigInternal = {
getBaseLlmClient: vi.fn(), // Initialize as a plain mock function
getFileSystemService: () => fsService,
getIdeMode: vi.fn(() => false),
getWorkspaceContext: () => createMockWorkspaceContext(rootDir),
getWorkspaceContext: () => new WorkspaceContext(rootDir),
getApiKey: () => 'test-key',
getModel: () => 'test-model',
getSandbox: () => false,
@@ -207,9 +207,14 @@ describe('WriteFileTool', () => {
expect(invocation.params).toEqual(params);
});
it('should throw an error for a relative path', () => {
const params = { file_path: 'test.txt', content: 'hello' };
expect(() => tool.build(params)).toThrow(/File path must be absolute/);
it('should return an invocation for a valid relative path within root', () => {
const params = {
file_path: 'test.txt',
content: 'hello',
};
const invocation = tool.build(params);
expect(invocation).toBeDefined();
expect(invocation.params).toEqual(params);
});
it('should throw an error for a path outside root', () => {
@@ -565,6 +570,25 @@ describe('WriteFileTool', () => {
describe('execute', () => {
const abortSignal = new AbortController().signal;
it('should write a new file with a relative path', async () => {
const relativePath = 'execute_relative_new_file.txt';
const filePath = path.join(rootDir, relativePath);
const content = 'Content for relative path file.';
mockEnsureCorrectFileContent.mockResolvedValue(content);
const params = { file_path: relativePath, content };
const invocation = tool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toMatch(
/Successfully created and wrote to new file/,
);
expect(fs.existsSync(filePath)).toBe(true);
const writtenContent = await fsService.readTextFile(filePath);
expect(writtenContent).toBe(content);
});
it('should return error if _getCorrectedFileContent returns an error during execute', async () => {
const filePath = path.join(rootDir, 'execute_error_file.txt');
const params = { file_path: filePath, content: 'test content' };

View File

@@ -143,6 +143,8 @@ class WriteFileToolInvocation extends BaseToolInvocation<
WriteFileToolParams,
ToolResult
> {
private readonly resolvedPath: string;
constructor(
private readonly config: Config,
params: WriteFileToolParams,
@@ -151,15 +153,19 @@ class WriteFileToolInvocation extends BaseToolInvocation<
displayName?: string,
) {
super(params, messageBus, toolName, displayName);
this.resolvedPath = path.resolve(
this.config.getTargetDir(),
this.params.file_path,
);
}
override toolLocations(): ToolLocation[] {
return [{ path: this.params.file_path }];
return [{ path: this.resolvedPath }];
}
override getDescription(): string {
const relativePath = makeRelative(
this.params.file_path,
this.resolvedPath,
this.config.getTargetDir(),
);
return `Writing to ${shortenPath(relativePath)}`;
@@ -174,7 +180,7 @@ class WriteFileToolInvocation extends BaseToolInvocation<
const correctedContentResult = await getCorrectedFileContent(
this.config,
this.params.file_path,
this.resolvedPath,
this.params.content,
abortSignal,
);
@@ -186,10 +192,10 @@ class WriteFileToolInvocation extends BaseToolInvocation<
const { originalContent, correctedContent } = correctedContentResult;
const relativePath = makeRelative(
this.params.file_path,
this.resolvedPath,
this.config.getTargetDir(),
);
const fileName = path.basename(this.params.file_path);
const fileName = path.basename(this.resolvedPath);
const fileDiff = Diff.createPatch(
fileName,
@@ -203,14 +209,14 @@ class WriteFileToolInvocation extends BaseToolInvocation<
const ideClient = await IdeClient.getInstance();
const ideConfirmation =
this.config.getIdeMode() && ideClient.isDiffingEnabled()
? ideClient.openDiff(this.params.file_path, correctedContent)
? ideClient.openDiff(this.resolvedPath, correctedContent)
: undefined;
const confirmationDetails: ToolEditConfirmationDetails = {
type: 'edit',
title: `Confirm Write: ${shortenPath(relativePath)}`,
fileName,
filePath: this.params.file_path,
filePath: this.resolvedPath,
fileDiff,
originalContent,
newContent: correctedContent,
@@ -232,11 +238,10 @@ class WriteFileToolInvocation extends BaseToolInvocation<
}
async execute(abortSignal: AbortSignal): Promise<ToolResult> {
const { file_path, content, ai_proposed_content, modified_by_user } =
this.params;
const { content, ai_proposed_content, modified_by_user } = this.params;
const correctedContentResult = await getCorrectedFileContent(
this.config,
file_path,
this.resolvedPath,
content,
abortSignal,
);
@@ -244,7 +249,7 @@ class WriteFileToolInvocation extends BaseToolInvocation<
if (correctedContentResult.error) {
const errDetails = correctedContentResult.error;
const errorMsg = errDetails.code
? `Error checking existing file '${file_path}': ${errDetails.message} (${errDetails.code})`
? `Error checking existing file '${this.resolvedPath}': ${errDetails.message} (${errDetails.code})`
: `Error checking existing file: ${errDetails.message}`;
return {
llmContent: errorMsg,
@@ -269,17 +274,17 @@ class WriteFileToolInvocation extends BaseToolInvocation<
!correctedContentResult.fileExists);
try {
const dirName = path.dirname(file_path);
const dirName = path.dirname(this.resolvedPath);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName, { recursive: true });
}
await this.config
.getFileSystemService()
.writeTextFile(file_path, fileContent);
.writeTextFile(this.resolvedPath, fileContent);
// Generate diff for display result
const fileName = path.basename(file_path);
const fileName = path.basename(this.resolvedPath);
// If there was a readError, originalContent in correctedContentResult is '',
// but for the diff, we want to show the original content as it was before the write if possible.
// However, if it was unreadable, currentContentForDiff will be empty.
@@ -306,8 +311,8 @@ class WriteFileToolInvocation extends BaseToolInvocation<
const llmSuccessMessageParts = [
isNewFile
? `Successfully created and wrote to new file: ${file_path}.`
: `Successfully overwrote file: ${file_path}.`,
? `Successfully created and wrote to new file: ${this.resolvedPath}.`
: `Successfully overwrote file: ${this.resolvedPath}.`,
];
if (modified_by_user) {
llmSuccessMessageParts.push(
@@ -316,9 +321,9 @@ class WriteFileToolInvocation extends BaseToolInvocation<
}
// Log file operation for telemetry (without diff_stat to avoid double-counting)
const mimetype = getSpecificMimeType(file_path);
const programmingLanguage = getLanguageFromFilePath(file_path);
const extension = path.extname(file_path);
const mimetype = getSpecificMimeType(this.resolvedPath);
const programmingLanguage = getLanguageFromFilePath(this.resolvedPath);
const extension = path.extname(this.resolvedPath);
const operation = isNewFile ? FileOperation.CREATE : FileOperation.UPDATE;
logFileOperation(
@@ -352,17 +357,17 @@ class WriteFileToolInvocation extends BaseToolInvocation<
if (isNodeError(error)) {
// Handle specific Node.js errors with their error codes
errorMsg = `Error writing to file '${file_path}': ${error.message} (${error.code})`;
errorMsg = `Error writing to file '${this.resolvedPath}': ${error.message} (${error.code})`;
// Log specific error types for better debugging
if (error.code === 'EACCES') {
errorMsg = `Permission denied writing to file: ${file_path} (${error.code})`;
errorMsg = `Permission denied writing to file: ${this.resolvedPath} (${error.code})`;
errorType = ToolErrorType.PERMISSION_DENIED;
} else if (error.code === 'ENOSPC') {
errorMsg = `No space left on device: ${file_path} (${error.code})`;
errorMsg = `No space left on device: ${this.resolvedPath} (${error.code})`;
errorType = ToolErrorType.NO_SPACE_LEFT;
} else if (error.code === 'EISDIR') {
errorMsg = `Target is a directory, not a file: ${file_path} (${error.code})`;
errorMsg = `Target is a directory, not a file: ${this.resolvedPath} (${error.code})`;
errorType = ToolErrorType.TARGET_IS_DIRECTORY;
}
@@ -411,8 +416,7 @@ export class WriteFileTool
{
properties: {
file_path: {
description:
"The absolute path to the file to write to (e.g., '/home/user/project/file.txt'). Relative paths are not supported.",
description: 'The path to the file to write to.',
type: 'string',
},
content: {
@@ -438,12 +442,10 @@ export class WriteFileTool
return `Missing or empty "file_path"`;
}
if (!path.isAbsolute(filePath)) {
return `File path must be absolute: ${filePath}`;
}
const resolvedPath = path.resolve(this.config.getTargetDir(), filePath);
const workspaceContext = this.config.getWorkspaceContext();
if (!workspaceContext.isPathWithinWorkspace(filePath)) {
if (!workspaceContext.isPathWithinWorkspace(resolvedPath)) {
const directories = workspaceContext.getDirectories();
return `File path must be within one of the workspace directories: ${directories.join(
', ',
@@ -451,14 +453,14 @@ export class WriteFileTool
}
try {
if (fs.existsSync(filePath)) {
const stats = fs.lstatSync(filePath);
if (fs.existsSync(resolvedPath)) {
const stats = fs.lstatSync(resolvedPath);
if (stats.isDirectory()) {
return `Path is a directory, not a file: ${filePath}`;
return `Path is a directory, not a file: ${resolvedPath}`;
}
}
} catch (statError: unknown) {
return `Error accessing path properties for validation: ${filePath}. Reason: ${
return `Error accessing path properties for validation: ${resolvedPath}. Reason: ${
statError instanceof Error ? statError.message : String(statError)
}`;
}

View File

@@ -233,7 +233,7 @@ export function shortenPath(filePath: string, maxLen: number = 35): string {
/**
* Calculates the relative path from a root directory to a target path.
* Ensures both paths are resolved before calculating.
* If targetPath is relative, it is returned as-is.
* Returns '.' if the target path is the same as the root directory.
*
* @param targetPath The absolute or relative path to make relative.
@@ -244,10 +244,11 @@ export function makeRelative(
targetPath: string,
rootDirectory: string,
): string {
const resolvedTargetPath = path.resolve(targetPath);
if (!path.isAbsolute(targetPath)) {
return targetPath;
}
const resolvedRootDirectory = path.resolve(rootDirectory);
const relativePath = path.relative(resolvedRootDirectory, resolvedTargetPath);
const relativePath = path.relative(resolvedRootDirectory, targetPath);
// If the paths are the same, path.relative returns '', return '.' instead
return relativePath || '.';

View File

@@ -68,7 +68,7 @@ describe('WorkspaceContext with real filesystem', () => {
it('should resolve relative paths to absolute', () => {
const workspaceContext = new WorkspaceContext(cwd);
const relativePath = path.relative(cwd, otherDir);
workspaceContext.addDirectory(relativePath, cwd);
workspaceContext.addDirectory(relativePath);
const directories = workspaceContext.getDirectories();
expect(directories).toEqual([cwd, otherDir]);

View File

@@ -7,7 +7,6 @@
import { isNodeError } from '../utils/errors.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as process from 'node:process';
import { debugLogger } from './debugLogger.js';
export type Unsubscribe = () => void;
@@ -24,11 +23,14 @@ export class WorkspaceContext {
/**
* Creates a new WorkspaceContext with the given initial directory and optional additional directories.
* @param directory The initial working directory (usually cwd)
* @param targetDir The initial working directory (usually cwd)
* @param additionalDirectories Optional array of additional directories to include
*/
constructor(directory: string, additionalDirectories: string[] = []) {
this.addDirectory(directory);
constructor(
readonly targetDir: string,
additionalDirectories: string[] = [],
) {
this.addDirectory(targetDir);
for (const additionalDirectory of additionalDirectories) {
this.addDirectory(additionalDirectory);
}
@@ -66,9 +68,9 @@ export class WorkspaceContext {
* @param directory The directory path to add (can be relative or absolute)
* @param basePath Optional base path for resolving relative paths (defaults to cwd)
*/
addDirectory(directory: string, basePath: string = process.cwd()): void {
addDirectory(directory: string): void {
try {
const resolved = this.resolveAndValidateDir(directory, basePath);
const resolved = this.resolveAndValidateDir(directory);
if (this.directories.has(resolved)) {
return;
}
@@ -81,13 +83,8 @@ export class WorkspaceContext {
}
}
private resolveAndValidateDir(
directory: string,
basePath: string = process.cwd(),
): string {
const absolutePath = path.isAbsolute(directory)
? directory
: path.resolve(basePath, directory);
private resolveAndValidateDir(directory: string): string {
const absolutePath = path.resolve(this.targetDir, directory);
if (!fs.existsSync(absolutePath)) {
throw new Error(`Directory does not exist: ${absolutePath}`);
@@ -154,7 +151,7 @@ export class WorkspaceContext {
*/
private fullyResolvedPath(pathToCheck: string): string {
try {
return fs.realpathSync(pathToCheck);
return fs.realpathSync(path.resolve(this.targetDir, pathToCheck));
} catch (e: unknown) {
if (
isNodeError(e) &&