diff options
| author | Taylor Mullen <[email protected]> | 2025-04-15 21:41:08 -0700 |
|---|---|---|
| committer | Taylor Mullen <[email protected]> | 2025-04-17 13:19:55 -0400 |
| commit | add233c5043264d47ecc6d3339a383f41a241ae8 (patch) | |
| tree | 3d80d412ed805007132cf44257bbd7667005dcd8 /packages/cli/src/tools/write-file.tool.ts | |
Initial commit of Gemini Code CLI
This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks.
The code was migrated from a previous git repository as a single squashed commit.
Core Features & Components:
* **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools).
* **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements.
* **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for:
* File system listing (`ls`)
* File reading (`read-file`)
* Content searching (`grep`)
* File globbing (`glob`)
* File editing (`edit`)
* File writing (`write-file`)
* Executing bash commands (`terminal`)
* **State Management:** Handles the streaming state of Gemini responses and manages the conversation history.
* **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup.
* **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts.
This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment.
---
Created by yours truly: __Gemini Code__
Diffstat (limited to 'packages/cli/src/tools/write-file.tool.ts')
| -rw-r--r-- | packages/cli/src/tools/write-file.tool.ts | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/packages/cli/src/tools/write-file.tool.ts b/packages/cli/src/tools/write-file.tool.ts new file mode 100644 index 00000000..e832357b --- /dev/null +++ b/packages/cli/src/tools/write-file.tool.ts @@ -0,0 +1,201 @@ +import fs from 'fs'; +import path from 'path'; +import { ToolResult } from './ToolResult.js'; +import { BaseTool } from './BaseTool.js'; +import { SchemaValidator } from '../utils/schemaValidator.js'; +import { makeRelative, shortenPath } from '../utils/paths.js'; +import { ToolCallConfirmationDetails, ToolConfirmationOutcome, ToolEditConfirmationDetails } from '../ui/types.js'; +import * as Diff from 'diff'; + +/** + * Parameters for the WriteFile tool + */ +export interface WriteFileToolParams { + /** + * The absolute path to the file to write to + */ + file_path: string; + + /** + * The content to write to the file + */ + content: string; +} + +/** + * Standardized result from the WriteFile tool + */ +export interface WriteFileToolResult extends ToolResult { +} + +/** + * Implementation of the WriteFile tool that writes files to the filesystem + */ +export class WriteFileTool extends BaseTool<WriteFileToolParams, WriteFileToolResult> { + public static readonly Name: string = 'write_file'; + private shouldAlwaysWrite = false; + + /** + * The root directory that this tool is grounded in. + * All file operations will be restricted to this directory. + */ + private rootDirectory: string; + + /** + * Creates a new instance of the WriteFileTool + * @param rootDirectory Root directory to ground this tool in. All operations will be restricted to this directory. + */ + constructor(rootDirectory: string) { + super( + WriteFileTool.Name, + 'WriteFile', + 'Writes content to a specified file in the local filesystem.', + { + 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.', + type: 'string' + }, + content: { + description: 'The content to write to the file.', + type: 'string' + } + }, + required: ['file_path', 'content'], + type: 'object' + } + ); + + // Set the root directory + this.rootDirectory = path.resolve(rootDirectory); + } + + /** + * Checks if a path is within the root directory + * @param pathToCheck The path to check + * @returns True if the path is within the root directory, false otherwise + */ + private isWithinRoot(pathToCheck: string): boolean { + const normalizedPath = path.normalize(pathToCheck); + const normalizedRoot = path.normalize(this.rootDirectory); + + // Ensure the normalizedRoot ends with a path separator for proper path comparison + const rootWithSep = normalizedRoot.endsWith(path.sep) + ? normalizedRoot + : normalizedRoot + path.sep; + + return normalizedPath === normalizedRoot || normalizedPath.startsWith(rootWithSep); + } + + /** + * Validates the parameters for the WriteFile tool + * @param params Parameters to validate + * @returns True if parameters are valid, false otherwise + */ + invalidParams(params: WriteFileToolParams): string | null { + if (this.schema.parameters && !SchemaValidator.validate(this.schema.parameters as Record<string, unknown>, params)) { + return 'Parameters failed schema validation.'; + } + + // Ensure path is absolute + if (!path.isAbsolute(params.file_path)) { + return `File path must be absolute: ${params.file_path}`; + } + + // Ensure path is within the root directory + if (!this.isWithinRoot(params.file_path)) { + return `File path must be within the root directory (${this.rootDirectory}): ${params.file_path}`; + } + + return null; + } + + /** + * Determines if the tool should prompt for confirmation before execution + * @param params Parameters for the tool execution + * @returns Whether or not execute should be confirmed by the user. + */ + async shouldConfirmExecute(params: WriteFileToolParams): Promise<ToolCallConfirmationDetails | false> { + if (this.shouldAlwaysWrite) { + return false; + } + + const relativePath = makeRelative(params.file_path, this.rootDirectory); + const fileName = path.basename(params.file_path); + + let currentContent = ''; + try { + currentContent = fs.readFileSync(params.file_path, 'utf8'); + } catch (error) { + // File may not exist, which is fine + } + + const fileDiff = Diff.createPatch( + fileName, + currentContent, + params.content, + 'Current', + 'Proposed', + { context: 3, ignoreWhitespace: true} + ); + + const confirmationDetails: ToolEditConfirmationDetails = { + title: `Confirm Write: ${shortenPath(relativePath)}`, + fileName, + fileDiff, + onConfirm: async (outcome: ToolConfirmationOutcome) => { + if (outcome === ToolConfirmationOutcome.ProceedAlways) { + this.shouldAlwaysWrite = true; + } + }, + }; + return confirmationDetails; + } + + /** + * Gets a description of the file writing operation + * @param params Parameters for the file writing + * @returns A string describing the file being written to + */ + getDescription(params: WriteFileToolParams): string { + const relativePath = makeRelative(params.file_path, this.rootDirectory); + return `Writing to ${shortenPath(relativePath)}`; + } + + /** + * Executes the file writing operation + * @param params Parameters for the file writing + * @returns Result of the file writing operation + */ + async execute(params: WriteFileToolParams): Promise<WriteFileToolResult> { + const validationError = this.invalidParams(params); + if (validationError) { + return { + llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`, + returnDisplay: '**Error:** Failed to execute tool.' + }; + } + + try { + // Ensure parent directories exist + const dirName = path.dirname(params.file_path); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + + // Write the file + fs.writeFileSync(params.file_path, params.content, 'utf8'); + + return { + llmContent: `Successfully wrote to file: ${params.file_path}`, + returnDisplay: `Wrote to ${shortenPath(makeRelative(params.file_path, this.rootDirectory))}` + }; + } catch (error) { + const errorMsg = `Error writing to file: ${error instanceof Error ? error.message : String(error)}`; + return { + llmContent: `Error writing to file ${params.file_path}: ${errorMsg}`, + returnDisplay: `Failed to write to file: ${errorMsg}` + }; + } + } +} |
