summaryrefslogtreecommitdiff
path: root/packages/server/src/tools/write-file.ts
diff options
context:
space:
mode:
authorTommaso Sciortino <[email protected]>2025-05-30 18:25:47 -0700
committerGitHub <[email protected]>2025-05-30 18:25:47 -0700
commit21fba832d1b4ea7af43fb887d9b2b38fcf8210d0 (patch)
tree7200d2fac3a55c385e0a2dac34b5282c942364bc /packages/server/src/tools/write-file.ts
parentc81148a0cc8489f657901c2cc7247c0834075e1a (diff)
Rename server->core (#638)
Diffstat (limited to 'packages/server/src/tools/write-file.ts')
-rw-r--r--packages/server/src/tools/write-file.ts336
1 files changed, 0 insertions, 336 deletions
diff --git a/packages/server/src/tools/write-file.ts b/packages/server/src/tools/write-file.ts
deleted file mode 100644
index 2285c819..00000000
--- a/packages/server/src/tools/write-file.ts
+++ /dev/null
@@ -1,336 +0,0 @@
-/**
- * @license
- * Copyright 2025 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import fs from 'fs';
-import path from 'path';
-import * as Diff from 'diff';
-import { Config } from '../config/config.js';
-import {
- BaseTool,
- ToolResult,
- FileDiff,
- ToolEditConfirmationDetails,
- ToolConfirmationOutcome,
- ToolCallConfirmationDetails,
-} from './tools.js';
-import { SchemaValidator } from '../utils/schemaValidator.js';
-import { makeRelative, shortenPath } from '../utils/paths.js';
-import { getErrorMessage, isNodeError } from '../utils/errors.js';
-import {
- ensureCorrectEdit,
- ensureCorrectFileContent,
-} from '../utils/editCorrector.js';
-import { GeminiClient } from '../core/client.js';
-import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js';
-
-/**
- * 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;
-}
-
-interface GetCorrectedFileContentResult {
- originalContent: string;
- correctedContent: string;
- fileExists: boolean;
- error?: { message: string; code?: string };
-}
-
-/**
- * Implementation of the WriteFile tool logic
- */
-export class WriteFileTool extends BaseTool<WriteFileToolParams, ToolResult> {
- static readonly Name: string = 'write_file';
- private readonly client: GeminiClient;
-
- constructor(private readonly config: Config) {
- 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',
- },
- );
-
- this.client = new GeminiClient(this.config);
- }
-
- private isWithinRoot(pathToCheck: string): boolean {
- const normalizedPath = path.normalize(pathToCheck);
- const normalizedRoot = path.normalize(this.config.getTargetDir());
- const rootWithSep = normalizedRoot.endsWith(path.sep)
- ? normalizedRoot
- : normalizedRoot + path.sep;
- return (
- normalizedPath === normalizedRoot ||
- normalizedPath.startsWith(rootWithSep)
- );
- }
-
- validateToolParams(params: WriteFileToolParams): string | null {
- if (
- this.schema.parameters &&
- !SchemaValidator.validate(
- this.schema.parameters as Record<string, unknown>,
- params,
- )
- ) {
- return 'Parameters failed schema validation.';
- }
- const filePath = params.file_path;
- if (!path.isAbsolute(filePath)) {
- return `File path must be absolute: ${filePath}`;
- }
- if (!this.isWithinRoot(filePath)) {
- return `File path must be within the root directory (${this.config.getTargetDir()}): ${filePath}`;
- }
-
- try {
- // This check should be performed only if the path exists.
- // If it doesn't exist, it's a new file, which is valid for writing.
- if (fs.existsSync(filePath)) {
- const stats = fs.lstatSync(filePath);
- if (stats.isDirectory()) {
- return `Path is a directory, not a file: ${filePath}`;
- }
- }
- } catch (statError: unknown) {
- // If fs.existsSync is true but lstatSync fails (e.g., permissions, race condition where file is deleted)
- // this indicates an issue with accessing the path that should be reported.
- return `Error accessing path properties for validation: ${filePath}. Reason: ${statError instanceof Error ? statError.message : String(statError)}`;
- }
-
- return null;
- }
-
- getDescription(params: WriteFileToolParams): string {
- const relativePath = makeRelative(
- params.file_path,
- this.config.getTargetDir(),
- );
- return `Writing to ${shortenPath(relativePath)}`;
- }
-
- /**
- * Handles the confirmation prompt for the WriteFile tool.
- */
- async shouldConfirmExecute(
- params: WriteFileToolParams,
- abortSignal: AbortSignal,
- ): Promise<ToolCallConfirmationDetails | false> {
- if (this.config.getAlwaysSkipModificationConfirmation()) {
- return false;
- }
-
- const validationError = this.validateToolParams(params);
- if (validationError) {
- return false;
- }
-
- const correctedContentResult = await this._getCorrectedFileContent(
- params.file_path,
- params.content,
- abortSignal,
- );
-
- if (correctedContentResult.error) {
- // If file exists but couldn't be read, we can't show a diff for confirmation.
- return false;
- }
-
- const { originalContent, correctedContent } = correctedContentResult;
- const relativePath = makeRelative(
- params.file_path,
- this.config.getTargetDir(),
- );
- const fileName = path.basename(params.file_path);
-
- const fileDiff = Diff.createPatch(
- fileName,
- originalContent, // Original content (empty if new file or unreadable)
- correctedContent, // Content after potential correction
- 'Current',
- 'Proposed',
- DEFAULT_DIFF_OPTIONS,
- );
-
- const confirmationDetails: ToolEditConfirmationDetails = {
- type: 'edit',
- title: `Confirm Write: ${shortenPath(relativePath)}`,
- fileName,
- fileDiff,
- onConfirm: async (outcome: ToolConfirmationOutcome) => {
- if (outcome === ToolConfirmationOutcome.ProceedAlways) {
- this.config.setAlwaysSkipModificationConfirmation(true);
- }
- },
- };
- return confirmationDetails;
- }
-
- async execute(
- params: WriteFileToolParams,
- abortSignal: AbortSignal,
- ): Promise<ToolResult> {
- const validationError = this.validateToolParams(params);
- if (validationError) {
- return {
- llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
- returnDisplay: `Error: ${validationError}`,
- };
- }
-
- const correctedContentResult = await this._getCorrectedFileContent(
- params.file_path,
- params.content,
- abortSignal,
- );
-
- if (correctedContentResult.error) {
- const errDetails = correctedContentResult.error;
- const errorMsg = `Error checking existing file: ${errDetails.message}`;
- return {
- llmContent: `Error checking existing file ${params.file_path}: ${errDetails.message}`,
- returnDisplay: errorMsg,
- };
- }
-
- const {
- originalContent,
- correctedContent: fileContent,
- fileExists,
- } = correctedContentResult;
- // fileExists is true if the file existed (and was readable or unreadable but caught by readError).
- // fileExists is false if the file did not exist (ENOENT).
- const isNewFile =
- !fileExists ||
- (correctedContentResult.error !== undefined &&
- !correctedContentResult.fileExists);
-
- try {
- const dirName = path.dirname(params.file_path);
- if (!fs.existsSync(dirName)) {
- fs.mkdirSync(dirName, { recursive: true });
- }
-
- fs.writeFileSync(params.file_path, fileContent, 'utf8');
-
- // Generate diff for display result
- const fileName = path.basename(params.file_path);
- // 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.
- const currentContentForDiff = correctedContentResult.error
- ? '' // Or some indicator of unreadable content
- : originalContent;
-
- const fileDiff = Diff.createPatch(
- fileName,
- currentContentForDiff,
- fileContent,
- 'Original',
- 'Written',
- DEFAULT_DIFF_OPTIONS,
- );
-
- const llmSuccessMessage = isNewFile
- ? `Successfully created and wrote to new file: ${params.file_path}`
- : `Successfully overwrote file: ${params.file_path}`;
-
- const displayResult: FileDiff = { fileDiff, fileName };
-
- return {
- llmContent: llmSuccessMessage,
- returnDisplay: displayResult,
- };
- } 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: `Error: ${errorMsg}`,
- };
- }
- }
-
- private async _getCorrectedFileContent(
- filePath: string,
- proposedContent: string,
- abortSignal: AbortSignal,
- ): Promise<GetCorrectedFileContentResult> {
- let originalContent = '';
- let fileExists = false;
- let correctedContent = proposedContent;
-
- try {
- originalContent = fs.readFileSync(filePath, 'utf8');
- fileExists = true; // File exists and was read
- } catch (err) {
- if (isNodeError(err) && err.code === 'ENOENT') {
- fileExists = false;
- originalContent = '';
- } else {
- // File exists but could not be read (permissions, etc.)
- fileExists = true; // Mark as existing but problematic
- originalContent = ''; // Can't use its content
- const error = {
- message: getErrorMessage(err),
- code: isNodeError(err) ? err.code : undefined,
- };
- // Return early as we can't proceed with content correction meaningfully
- return { originalContent, correctedContent, fileExists, error };
- }
- }
-
- // If readError is set, we have returned.
- // So, file was either read successfully (fileExists=true, originalContent set)
- // or it was ENOENT (fileExists=false, originalContent='').
-
- if (fileExists) {
- // This implies originalContent is available
- const { params: correctedParams } = await ensureCorrectEdit(
- originalContent,
- {
- old_string: originalContent, // Treat entire current content as old_string
- new_string: proposedContent,
- file_path: filePath,
- },
- this.client,
- abortSignal,
- );
- correctedContent = correctedParams.new_string;
- } else {
- // This implies new file (ENOENT)
- correctedContent = await ensureCorrectFileContent(
- proposedContent,
- this.client,
- abortSignal,
- );
- }
- return { originalContent, correctedContent, fileExists };
- }
-}