mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
feat(ipuaro): add edit tools (v0.6.0)
Add file editing capabilities: - EditLinesTool: replace lines with hash conflict detection - CreateFileTool: create files with directory auto-creation - DeleteFileTool: delete files from filesystem and storage Total: 664 tests, 97.77% coverage
This commit is contained in:
140
packages/ipuaro/src/infrastructure/tools/edit/CreateFileTool.ts
Normal file
140
packages/ipuaro/src/infrastructure/tools/edit/CreateFileTool.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { promises as fs } from "node:fs"
|
||||
import * as path from "node:path"
|
||||
import type { ITool, ToolContext, ToolParameterSchema } from "../../../domain/services/ITool.js"
|
||||
import { createFileData } from "../../../domain/value-objects/FileData.js"
|
||||
import {
|
||||
createErrorResult,
|
||||
createSuccessResult,
|
||||
type ToolResult,
|
||||
} from "../../../domain/value-objects/ToolResult.js"
|
||||
import { hashLines } from "../../../shared/utils/hash.js"
|
||||
|
||||
/**
|
||||
* Result data from create_file tool.
|
||||
*/
|
||||
export interface CreateFileResult {
|
||||
path: string
|
||||
lines: number
|
||||
size: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool for creating new files.
|
||||
* Creates a new file with the specified content.
|
||||
* Requires user confirmation before creating.
|
||||
*/
|
||||
export class CreateFileTool implements ITool {
|
||||
readonly name = "create_file"
|
||||
readonly description =
|
||||
"Create a new file with the specified content. " +
|
||||
"The file path must be within the project root. " +
|
||||
"Requires confirmation before creating."
|
||||
readonly parameters: ToolParameterSchema[] = [
|
||||
{
|
||||
name: "path",
|
||||
type: "string",
|
||||
description: "File path relative to project root",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
type: "string",
|
||||
description: "File content",
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
readonly requiresConfirmation = true
|
||||
readonly category = "edit" as const
|
||||
|
||||
validateParams(params: Record<string, unknown>): string | null {
|
||||
if (typeof params.path !== "string" || params.path.trim() === "") {
|
||||
return "Parameter 'path' is required and must be a non-empty string"
|
||||
}
|
||||
|
||||
if (typeof params.content !== "string") {
|
||||
return "Parameter 'content' is required and must be a string"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async execute(params: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
|
||||
const startTime = Date.now()
|
||||
const callId = `${this.name}-${String(startTime)}`
|
||||
|
||||
const relativePath = params.path as string
|
||||
const content = params.content as string
|
||||
|
||||
const absolutePath = path.resolve(ctx.projectRoot, relativePath)
|
||||
|
||||
if (!absolutePath.startsWith(ctx.projectRoot)) {
|
||||
return createErrorResult(
|
||||
callId,
|
||||
"Path must be within project root",
|
||||
Date.now() - startTime,
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const exists = await this.fileExists(absolutePath)
|
||||
if (exists) {
|
||||
return createErrorResult(
|
||||
callId,
|
||||
`File already exists: ${relativePath}`,
|
||||
Date.now() - startTime,
|
||||
)
|
||||
}
|
||||
|
||||
const lines = content.split("\n")
|
||||
|
||||
const confirmed = await ctx.requestConfirmation(
|
||||
`Create new file: ${relativePath} (${String(lines.length)} lines)`,
|
||||
{
|
||||
filePath: relativePath,
|
||||
oldLines: [],
|
||||
newLines: lines,
|
||||
startLine: 1,
|
||||
},
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return createErrorResult(
|
||||
callId,
|
||||
"File creation cancelled by user",
|
||||
Date.now() - startTime,
|
||||
)
|
||||
}
|
||||
|
||||
const dirPath = path.dirname(absolutePath)
|
||||
await fs.mkdir(dirPath, { recursive: true })
|
||||
await fs.writeFile(absolutePath, content, "utf-8")
|
||||
|
||||
const stats = await fs.stat(absolutePath)
|
||||
const fileData = createFileData(lines, hashLines(lines), stats.size, stats.mtimeMs)
|
||||
await ctx.storage.setFile(relativePath, fileData)
|
||||
|
||||
const result: CreateFileResult = {
|
||||
path: relativePath,
|
||||
lines: lines.length,
|
||||
size: stats.size,
|
||||
}
|
||||
|
||||
return createSuccessResult(callId, result, Date.now() - startTime)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return createErrorResult(callId, message, Date.now() - startTime)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file exists.
|
||||
*/
|
||||
private async fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user