// src/tools/file/write.ts // Copyright (C) 2025 DTP Technologies, LLC // All Rights Reserved import fs from "node:fs/promises"; import path from "node:path"; import type { ToolDefinition } from "../../lib/ai-client.js"; import { ChatSessionMode } from "@/models/chat-session.js"; import type { IChatFileOperation } from "../../models/chat-history.js"; import { DtpTool, ToolMetadata, type ToolArguments, type ToolContext, } from "../../lib/tool.js"; import { validateProjectPath } from "../../lib/path-security.js"; import { PROJECT_ROOT } from "../../config/env.js"; import HostMonitorService from "../../services/host-monitor.js"; const BINARY_EXTENSIONS = new Set([ ".o", ".obj", ".a", ".lib", ".so", ".dll", ".exe", ".bin", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp", ".pdf", ".zip", ".tar", ".gz", ".bz2", ".7z", ".wasm", ".pyc", ".class", ]); export class FileWriteTool extends DtpTool { get name(): string { return "FileWriteTool"; } get slug(): string { return "file-write"; } get metadata(): ToolMetadata { return { name: this.definition.function.name || "file_write", category: "file", tags: ["file", "write", "create", "save"], modes: [ ChatSessionMode.Plan, ChatSessionMode.Build, ChatSessionMode.Test, ChatSessionMode.Ship, ChatSessionMode.Develop, ], }; } public definition: ToolDefinition = { type: "function", function: { name: "file_write", description: "Create a new file or overwrite an existing file with the given content. Parent directories are created automatically.", parameters: { type: "object", properties: { path: { type: "string", description: "Path to the file to create or overwrite (relative or absolute).", }, content: { type: "string", description: "The content to write to the file.", }, }, required: ["path", "content"], }, }, }; public async execute( _context: ToolContext, args: ToolArguments, ): Promise { const filePath = args.path as string | undefined; const content = args.content as string | undefined; if (!filePath || filePath.trim().length === 0) { return this.error("MISSING_PARAMETER", "File path must not be empty.", { parameter: "path", recoveryHint: "Provide a valid file path.", }); } if (content === undefined) { return this.error("MISSING_PARAMETER", "Content must not be undefined.", { parameter: "content", recoveryHint: "Provide the content to write to the file.", }); } // Validate path security - prevent path traversal attacks const pathValidation = validateProjectPath(filePath, PROJECT_ROOT); if (!pathValidation.valid || !pathValidation.resolvedPath) { return this.error( "SECURITY_VIOLATION", pathValidation.error || "Invalid path", { parameter: "path", recoveryHint: "Provide a valid relative path within the project directory.", }, ); } const resolvedPath = pathValidation.resolvedPath; try { const dir = path.dirname(resolvedPath); await fs.mkdir(dir, { recursive: true }); // Capture existing content for diff (if file exists) let oldContent: string | undefined; let isExisting = false; try { oldContent = await fs.readFile(resolvedPath, "utf-8"); isExisting = true; } catch { // new file - no previous content } const contentStr = String(content); await fs.writeFile(resolvedPath, contentStr, "utf-8"); const byteCount = Buffer.byteLength(contentStr, "utf-8"); const created = !isExisting; const binary = BINARY_EXTENSIONS.has( path.extname(filePath).toLowerCase(), ); const fileOperation: IChatFileOperation = { type: "write", path: filePath, isBinary: binary, }; if (!binary) { const newLines = contentStr.split("\n").length; const oldLines = oldContent ? oldContent.split("\n").length : 0; fileOperation.linesAdded = isExisting ? Math.max(0, newLines - oldLines) : newLines; fileOperation.linesRemoved = isExisting ? Math.max(0, oldLines - newLines) : 0; } HostMonitorService.fileOperation(byteCount); HostMonitorService.toolCall(byteCount); // Build plain text response with metadata header const outputLines = [ `PATH: ${filePath}`, `FILE OPERATION: write`, `CREATED: ${created ? "true" : "false"}`, `BYTES WRITTEN: ${byteCount}`, "---", `File written: ${filePath} (${byteCount} bytes)`, ]; return outputLines.join("\n"); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.log.error("Failed to write file", { path: filePath, error: errorMessage, }); return this.error( "OPERATION_FAILED", `Failed to write file: ${errorMessage}`, ); } } } export default new FileWriteTool();