2025-03-25 20:04:36 +08:00
package tools
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
2025-03-28 05:35:48 +08:00
"github.com/kujtimiihoxha/termai/internal/config"
2025-04-03 21:20:15 +08:00
"github.com/kujtimiihoxha/termai/internal/lsp"
2025-03-25 20:04:36 +08:00
"github.com/kujtimiihoxha/termai/internal/permission"
)
2025-04-03 21:20:15 +08:00
type writeTool struct {
lspClients map [ string ] * lsp . Client
}
2025-03-25 20:04:36 +08:00
const (
WriteToolName = "write"
)
type WriteParams struct {
FilePath string ` json:"file_path" `
Content string ` json:"content" `
}
2025-03-28 05:35:48 +08:00
type WritePermissionsParams struct {
FilePath string ` json:"file_path" `
Content string ` json:"content" `
}
func ( w * writeTool ) Info ( ) ToolInfo {
return ToolInfo {
Name : WriteToolName ,
Description : writeDescription ( ) ,
Parameters : map [ string ] any {
"file_path" : map [ string ] any {
"type" : "string" ,
"description" : "The path to the file to write" ,
2025-03-25 20:04:36 +08:00
} ,
2025-03-28 05:35:48 +08:00
"content" : map [ string ] any {
"type" : "string" ,
"description" : "The content to write to the file" ,
2025-03-25 20:04:36 +08:00
} ,
2025-03-28 05:35:48 +08:00
} ,
Required : [ ] string { "file_path" , "content" } ,
}
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
// Run implements Tool.
func ( w * writeTool ) Run ( ctx context . Context , call ToolCall ) ( ToolResponse , error ) {
2025-03-25 20:04:36 +08:00
var params WriteParams
2025-03-28 05:35:48 +08:00
if err := json . Unmarshal ( [ ] byte ( call . Input ) , & params ) ; err != nil {
return NewTextErrorResponse ( fmt . Sprintf ( "error parsing parameters: %s" , err ) ) , nil
2025-03-25 20:04:36 +08:00
}
if params . FilePath == "" {
2025-03-28 05:35:48 +08:00
return NewTextErrorResponse ( "file_path is required" ) , nil
}
if params . Content == "" {
return NewTextErrorResponse ( "content is required" ) , nil
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
// Handle relative paths
filePath := params . FilePath
if ! filepath . IsAbs ( filePath ) {
filePath = filepath . Join ( config . WorkingDirectory ( ) , filePath )
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
// Check if file exists and is a directory
fileInfo , err := os . Stat ( filePath )
2025-03-25 20:04:36 +08:00
if err == nil {
if fileInfo . IsDir ( ) {
2025-03-28 05:35:48 +08:00
return NewTextErrorResponse ( fmt . Sprintf ( "Path is a directory, not a file: %s" , filePath ) ) , nil
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
// Check if file was modified since last read
2025-03-25 20:04:36 +08:00
modTime := fileInfo . ModTime ( )
2025-03-28 05:35:48 +08:00
lastRead := getLastReadTime ( filePath )
2025-03-25 20:04:36 +08:00
if modTime . After ( lastRead ) {
2025-03-28 05:35:48 +08:00
return NewTextErrorResponse ( fmt . Sprintf ( "File %s has been modified since it was last read.\nLast modification: %s\nLast read: %s\n\nPlease read the file again before modifying it." ,
filePath , modTime . Format ( time . RFC3339 ) , lastRead . Format ( time . RFC3339 ) ) ) , nil
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
// Optional: Get old content for diff
oldContent , readErr := os . ReadFile ( filePath )
if readErr == nil && string ( oldContent ) == params . Content {
return NewTextErrorResponse ( fmt . Sprintf ( "File %s already contains the exact content. No changes made." , filePath ) ) , nil
}
2025-03-25 20:04:36 +08:00
} else if ! os . IsNotExist ( err ) {
2025-03-28 05:35:48 +08:00
return NewTextErrorResponse ( fmt . Sprintf ( "Failed to access file: %s" , err ) ) , nil
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
// Create parent directories if needed
dir := filepath . Dir ( filePath )
if err = os . MkdirAll ( dir , 0 o755 ) ; err != nil {
return NewTextErrorResponse ( fmt . Sprintf ( "Failed to create parent directories: %s" , err ) ) , nil
}
2025-04-03 21:20:15 +08:00
notifyLspOpenFile ( ctx , filePath , w . lspClients )
2025-04-04 20:36:57 +08:00
// Get old content for diff if file exists
oldContent := ""
if fileInfo != nil && ! fileInfo . IsDir ( ) {
oldBytes , readErr := os . ReadFile ( filePath )
if readErr == nil {
oldContent = string ( oldBytes )
}
}
2025-03-25 20:04:36 +08:00
p := permission . Default . Request (
permission . CreatePermissionRequest {
2025-03-28 05:35:48 +08:00
Path : filePath ,
2025-03-25 20:04:36 +08:00
ToolName : WriteToolName ,
2025-03-28 05:35:48 +08:00
Action : "create" ,
Description : fmt . Sprintf ( "Create file %s" , filePath ) ,
Params : WritePermissionsParams {
FilePath : filePath ,
2025-04-04 20:36:57 +08:00
Content : GenerateDiff ( oldContent , params . Content ) ,
2025-03-25 20:04:36 +08:00
} ,
} ,
)
if ! p {
2025-03-28 05:35:48 +08:00
return NewTextErrorResponse ( fmt . Sprintf ( "Permission denied to create file: %s" , filePath ) ) , nil
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
// Write the file
err = os . WriteFile ( filePath , [ ] byte ( params . Content ) , 0 o644 )
2025-03-25 20:04:36 +08:00
if err != nil {
2025-03-28 05:35:48 +08:00
return NewTextErrorResponse ( fmt . Sprintf ( "Failed to write file: %s" , err ) ) , nil
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
// Record the file write
recordFileWrite ( filePath )
recordFileRead ( filePath )
2025-03-25 20:04:36 +08:00
2025-04-03 21:20:15 +08:00
result := fmt . Sprintf ( "File successfully written: %s" , filePath )
result = fmt . Sprintf ( "<result>\n%s\n</result>" , result )
result += appendDiagnostics ( filePath , w . lspClients )
return NewTextResponse ( result ) , nil
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
func writeDescription ( ) string {
return ` File writing tool that creates or updates files in the filesystem , allowing you to save or modify text content .
2025-03-25 20:04:36 +08:00
2025-03-28 05:35:48 +08:00
WHEN TO USE THIS TOOL :
- Use when you need to create a new file
- Helpful for updating existing files with modified content
- Perfect for saving generated code , configurations , or text data
2025-03-25 20:04:36 +08:00
2025-03-28 05:35:48 +08:00
HOW TO USE :
- Provide the path to the file you want to write
- Include the content to be written to the file
- The tool will create any necessary parent directories
2025-03-25 20:04:36 +08:00
2025-03-28 05:35:48 +08:00
FEATURES :
- Can create new files or overwrite existing ones
- Creates parent directories automatically if they don ' t exist
- Checks if the file has been modified since last read for safety
- Avoids unnecessary writes when content hasn ' t changed
2025-03-25 20:04:36 +08:00
2025-03-28 05:35:48 +08:00
LIMITATIONS :
- You should read a file before writing to it to avoid conflicts
- Cannot append to files ( rewrites the entire file )
2025-03-25 20:04:36 +08:00
2025-03-28 05:35:48 +08:00
TIPS :
- Use the View tool first to examine existing files before modifying them
- Use the LS tool to verify the correct location when creating new files
- Combine with Glob and Grep tools to find and modify multiple files
- Always include descriptive comments when making changes to existing code `
2025-03-25 20:04:36 +08:00
}
2025-04-03 21:20:15 +08:00
func NewWriteTool ( lspClients map [ string ] * lsp . Client ) BaseTool {
return & writeTool {
lspClients ,
}
2025-03-25 20:04:36 +08:00
}