gadget/docs/archive/tools/file/write.ts

203 lines
5.2 KiB
TypeScript

// 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<string> {
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();