package toolbox import ( "fmt" "os" "strings" "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util" toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util" "git.mlow.ca/mlow/lmcli/pkg/api" ) var MODIFY_FILE_DESCRIPTION = []string{ "Modify a file. If the file does not exist, it will be created.", "", "Content can be either inserted, replaced, or removed through a combination of the start, stop, and content parameters.", "Use the start and stop line numbers to limit the range of modification to the file.", "If both `start` and `stop` are left unset (or set to 0), the entire file's contents will be updated.", "If `start` is set to n and `stop` to n+1, content will be inserted at line n (the content that was at line n will be shifted below the newly inserted content).", "If only `start` is set, content from the given line and onwards will be updated.", "If only `stop` is set, content up to but not including the given line will be updated.", "", "Examples:", "1. Append to a file:", " {\"path\": \"example.txt\", \"start\": , \"content\": \"New content to append\"}", "", "2. Insert at a specific line:", " {\"path\": \"example.txt\", \"start\": 5, \"stop\": 5, \"content\": \"New line inserted above the previous line 5\"}", "", "3. Replace a range of lines:", " {\"path\": \"example.txt\", \"start\": 3, \"stop\": 7, \"content\": \"New content replacing lines 3-7\"}", "", "4. Remove a range of lines:", " {\"path\": \"example.txt\", \"start\": 2, \"stop\": 5}", "", "5. Replace entire file contents:", " {\"path\": \"example.txt\", \"content\": \"New file contents\"}", "", "6. Update from a specific line to the end of the file:", " {\"path\": \"example.txt\", \"start\": 10, \"content\": \"New content from line 10 onwards\"}", "", "7. Update from the beginning of the file to a specific line:", " {\"path\": \"example.txt\", \"stop\": 6, \"content\": \"New content for first 5 lines\"}", "", "Note: Always use specific line numbers based on the current file content. Avoid using arbitrarily large numbers for start or stop.", } var ModifyFile = api.ToolSpec{ Name: "modify_file", Description: strings.Join(MODIFY_FILE_DESCRIPTION, "\n"), Parameters: []api.ToolParameter{ { Name: "path", Type: "string", Description: "Path of the file to be modified, relative to the current working directory.", Required: true, }, { Name: "start", Type: "integer", Description: `Start line of the range to modify (inclusive). If omitted, the beginning of the file is implied.`, }, { Name: "stop", Type: "integer", Description: `End line of the range to modify (inclusive). If omitted, the end of the file is implied.`, }, { Name: "content", Type: "string", Description: "Content to insert/replace at the range defined by `start` and `stop`. If omitted, the range is removed.", }, }, Impl: func(tool *api.ToolSpec, args map[string]interface{}) (string, error) { tmp, ok := args["path"] if !ok { return "", fmt.Errorf("path parameter to modify_file was not included.") } path, ok := tmp.(string) if !ok { return "", fmt.Errorf("Invalid path in function arguments: %v", tmp) } var start int tmp, ok = args["start"] if ok { tmp, ok := tmp.(float64) if !ok { return "", fmt.Errorf("Invalid start in function arguments: %v", tmp) } start = int(tmp) } var stop int tmp, ok = args["stop"] if ok { tmp, ok := tmp.(float64) if !ok { return "", fmt.Errorf("Invalid stop in function arguments: %v", tmp) } stop = int(tmp) } var content string tmp, ok = args["content"] if ok { content, ok = tmp.(string) if !ok { return "", fmt.Errorf("Invalid content in function arguments: %v", tmp) } } result := fileModifyContents(path, start, stop, content) ret, err := result.ToJson() if err != nil { return "", fmt.Errorf("Could not serialize result: %v", err) } return ret, nil }, } func fileModifyContents(path string, startLine int, stopLine int, content string) api.CallResult { ok, reason := toolutil.IsPathWithinCWD(path) if !ok { return api.CallResult{Message: reason} } // Read the existing file's content data, err := os.ReadFile(path) if err != nil { if !os.IsNotExist(err) { return api.CallResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())} } _, err = os.Create(path) if err != nil { return api.CallResult{Message: fmt.Sprintf("Could not create new file: %s", err.Error())} } data = []byte{} } lines := strings.Split(string(data), "\n") contentLines := strings.Split(strings.TrimSuffix(content, "\n"), "\n") // If both start and stop are unset, update the entire file if startLine == 0 && stopLine == 0 { lines = contentLines } else { if startLine < 1 { startLine = 1 } if stopLine == 0 || stopLine > len(lines) { stopLine = len(lines) } before := lines[:startLine-1] after := lines[stopLine:] // Handle insertion case if startLine == stopLine { lines = append(before, append(contentLines, lines[startLine-1:]...)...) } else { // If content is omitted, remove the specified range if content == "" { lines = append(before, after...) } else { lines = append(before, append(contentLines, after...)...) } } } newContent := strings.Join(lines, "\n") // Write back to the file err = os.WriteFile(path, []byte(newContent), 0644) if err != nil { return api.CallResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())} } return api.CallResult{Result: util.AddLineNumbers(newContent)} }