2023-11-25 17:55:18 -07:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-11-25 23:35:22 -07:00
|
|
|
"strings"
|
2023-11-25 17:55:18 -07:00
|
|
|
|
|
|
|
openai "github.com/sashabaranov/go-openai"
|
|
|
|
)
|
|
|
|
|
|
|
|
type FunctionResult struct {
|
|
|
|
Message string `json:"message"`
|
|
|
|
Result any `json:"result,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type FunctionParameter struct {
|
|
|
|
Type string `json:"type"` // "string", "integer", "boolean"
|
|
|
|
Description string `json:"description"`
|
|
|
|
Enum []string `json:"enum,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type FunctionParameters struct {
|
|
|
|
Type string `json:"type"` // "object"
|
|
|
|
Properties map[string]FunctionParameter `json:"properties"`
|
|
|
|
Required []string `json:"required,omitempty"` // required function parameter names
|
|
|
|
}
|
|
|
|
|
|
|
|
type AvailableTool struct {
|
|
|
|
openai.Tool
|
|
|
|
// The tool's implementation. Returns a string, as tool call results
|
|
|
|
// are treated as normal messages with string contents.
|
|
|
|
Impl func(arguments map[string]interface{}) (string, error)
|
|
|
|
}
|
|
|
|
|
2023-11-26 08:52:00 -07:00
|
|
|
const (
|
|
|
|
READ_DIR_DESCRIPTION = `Return the contents of the CWD (current working directory).
|
2023-11-25 17:55:18 -07:00
|
|
|
|
|
|
|
Results are returned as JSON in the following format:
|
|
|
|
{
|
2023-11-25 23:37:54 -07:00
|
|
|
"message": "success", // if successful, or a different message indicating failure
|
2023-11-26 00:32:28 -07:00
|
|
|
// result may be an empty array if there are no files in the directory
|
2023-11-25 17:55:18 -07:00
|
|
|
"result": [
|
2023-11-25 23:37:54 -07:00
|
|
|
{"name": "a_file", "type": "file", "size": 123},
|
|
|
|
{"name": "a_directory/", "type": "dir", "size": 11},
|
2023-11-25 17:55:18 -07:00
|
|
|
... // more files or directories
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2023-11-25 23:37:54 -07:00
|
|
|
For files, size represents the size (in bytes) of the file.
|
2023-11-26 08:52:00 -07:00
|
|
|
For directories, size represents the number of entries in that directory.`
|
|
|
|
|
|
|
|
READ_FILE_DESCRIPTION = `Read the contents of a text file relative to the current working directory.
|
|
|
|
|
|
|
|
Each line of the file is prefixed with its line number and a tabs (\t) to make
|
|
|
|
it make it easier to see which lines to change for other modifications.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
{
|
|
|
|
"message": "success", // if successful, or a different message indicating failure
|
|
|
|
"result": "1\tthe contents\n2\tof the file\n"
|
|
|
|
}`
|
|
|
|
|
|
|
|
WRITE_FILE_DESCRIPTION = `Write the provided contents to a file relative to the current working directory.
|
|
|
|
|
|
|
|
Result is returned as JSON in the following format:
|
|
|
|
{
|
|
|
|
"message": "success", // if successful, or a different message indicating failure
|
|
|
|
}`
|
|
|
|
|
2023-11-26 10:33:33 -07:00
|
|
|
FILE_INSERT_LINES = `Insert lines into a file.`
|
2023-11-26 08:52:00 -07:00
|
|
|
|
2023-11-26 13:39:37 -07:00
|
|
|
FILE_REPLACE_LINES = `Replace or remove a range of lines within a file.`
|
2023-11-26 08:52:00 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
var AvailableTools = map[string]AvailableTool{
|
|
|
|
"read_dir": {
|
|
|
|
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
|
|
|
|
Name: "read_dir",
|
|
|
|
Description: READ_DIR_DESCRIPTION,
|
2023-11-25 17:55:18 -07:00
|
|
|
Parameters: FunctionParameters{
|
|
|
|
Type: "object",
|
|
|
|
Properties: map[string]FunctionParameter{
|
|
|
|
"relative_dir": {
|
|
|
|
Type: "string",
|
|
|
|
Description: "If set, read the contents of a directory relative to the current one.",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}},
|
|
|
|
Impl: func(args map[string]interface{}) (string, error) {
|
|
|
|
var relativeDir string
|
|
|
|
tmp, ok := args["relative_dir"]
|
|
|
|
if ok {
|
|
|
|
relativeDir, ok = tmp.(string)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Invalid relative_dir in function arguments: %v", tmp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ReadDir(relativeDir), nil
|
|
|
|
},
|
|
|
|
},
|
2023-11-26 00:32:28 -07:00
|
|
|
"read_file": {
|
|
|
|
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
|
2023-11-26 08:52:00 -07:00
|
|
|
Name: "read_file",
|
|
|
|
Description: READ_FILE_DESCRIPTION,
|
2023-11-26 00:32:28 -07:00
|
|
|
Parameters: FunctionParameters{
|
|
|
|
Type: "object",
|
|
|
|
Properties: map[string]FunctionParameter{
|
|
|
|
"path": {
|
|
|
|
Type: "string",
|
|
|
|
Description: "Path to a file within the current working directory to read.",
|
|
|
|
},
|
|
|
|
},
|
2023-11-26 03:43:25 -07:00
|
|
|
Required: []string{"path"},
|
2023-11-26 00:32:28 -07:00
|
|
|
},
|
|
|
|
}},
|
|
|
|
Impl: func(args map[string]interface{}) (string, error) {
|
|
|
|
tmp, ok := args["path"]
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Path parameter to read_file was not included.")
|
|
|
|
}
|
|
|
|
path, ok := tmp.(string)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
|
|
|
}
|
|
|
|
return ReadFile(path), nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"write_file": {
|
|
|
|
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
|
2023-11-26 08:52:00 -07:00
|
|
|
Name: "write_file",
|
|
|
|
Description: WRITE_FILE_DESCRIPTION,
|
2023-11-26 00:32:28 -07:00
|
|
|
Parameters: FunctionParameters{
|
|
|
|
Type: "object",
|
|
|
|
Properties: map[string]FunctionParameter{
|
|
|
|
"path": {
|
|
|
|
Type: "string",
|
|
|
|
Description: "Path to a file within the current working directory to write to.",
|
|
|
|
},
|
|
|
|
"content": {
|
|
|
|
Type: "string",
|
|
|
|
Description: "The content to write to the file. Overwrites any existing content!",
|
|
|
|
},
|
|
|
|
},
|
2023-11-26 03:43:25 -07:00
|
|
|
Required: []string{"path", "content"},
|
2023-11-26 00:32:28 -07:00
|
|
|
},
|
|
|
|
}},
|
|
|
|
Impl: func(args map[string]interface{}) (string, error) {
|
|
|
|
tmp, ok := args["path"]
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Path parameter to write_file was not included.")
|
|
|
|
}
|
|
|
|
path, ok := tmp.(string)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
|
|
|
}
|
|
|
|
tmp, ok = args["content"]
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Content parameter to write_file was not included.")
|
|
|
|
}
|
|
|
|
content, ok := tmp.(string)
|
|
|
|
if !ok {
|
2023-11-26 03:43:25 -07:00
|
|
|
return "", fmt.Errorf("Invalid content in function arguments: %v", tmp)
|
2023-11-26 00:32:28 -07:00
|
|
|
}
|
|
|
|
return WriteFile(path, content), nil
|
|
|
|
},
|
|
|
|
},
|
2023-11-26 10:33:33 -07:00
|
|
|
"file_insert_lines": {
|
2023-11-26 03:51:06 -07:00
|
|
|
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
|
2023-11-26 10:33:33 -07:00
|
|
|
Name: "file_insert_lines",
|
|
|
|
Description: FILE_INSERT_LINES,
|
2023-11-26 03:51:06 -07:00
|
|
|
Parameters: FunctionParameters{
|
|
|
|
Type: "object",
|
|
|
|
Properties: map[string]FunctionParameter{
|
|
|
|
"path": {
|
|
|
|
Type: "string",
|
|
|
|
Description: "Path of the file to be modified, relative to the current working directory.",
|
|
|
|
},
|
2023-11-26 13:42:31 -07:00
|
|
|
"position": {
|
2023-11-26 10:33:33 -07:00
|
|
|
Type: "integer",
|
2023-11-26 13:42:31 -07:00
|
|
|
Description: `Which line to insert content *before*.`,
|
2023-11-26 10:33:33 -07:00
|
|
|
},
|
|
|
|
"content": {
|
2023-11-26 03:51:06 -07:00
|
|
|
Type: "string",
|
2023-11-26 10:33:33 -07:00
|
|
|
Description: `The content to insert.`,
|
|
|
|
},
|
|
|
|
},
|
2023-11-26 13:42:31 -07:00
|
|
|
Required: []string{"path", "position", "content"},
|
2023-11-26 10:33:33 -07:00
|
|
|
},
|
|
|
|
}},
|
|
|
|
Impl: func(args map[string]interface{}) (string, error) {
|
|
|
|
tmp, ok := args["path"]
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("path parameter to write_file was not included.")
|
|
|
|
}
|
|
|
|
path, ok := tmp.(string)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
|
|
|
}
|
2023-11-26 13:42:31 -07:00
|
|
|
var position int
|
|
|
|
tmp, ok = args["position"]
|
2023-11-26 10:33:33 -07:00
|
|
|
if ok {
|
|
|
|
tmp, ok := tmp.(float64)
|
|
|
|
if !ok {
|
2023-11-26 13:42:31 -07:00
|
|
|
return "", fmt.Errorf("Invalid position in function arguments: %v", tmp)
|
2023-11-26 10:33:33 -07:00
|
|
|
}
|
2023-11-26 13:42:31 -07:00
|
|
|
position = int(tmp)
|
2023-11-26 10:33:33 -07:00
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2023-11-26 13:42:31 -07:00
|
|
|
return FileInsertLines(path, position, content), nil
|
2023-11-26 10:33:33 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
"file_replace_lines": {
|
|
|
|
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
|
|
|
|
Name: "file_replace_lines",
|
|
|
|
Description: FILE_REPLACE_LINES,
|
|
|
|
Parameters: FunctionParameters{
|
|
|
|
Type: "object",
|
|
|
|
Properties: map[string]FunctionParameter{
|
|
|
|
"path": {
|
|
|
|
Type: "string",
|
|
|
|
Description: "Path of the file to be modified, relative to the current working directory.",
|
2023-11-26 03:51:06 -07:00
|
|
|
},
|
|
|
|
"start_line": {
|
|
|
|
Type: "integer",
|
2023-11-26 13:39:37 -07:00
|
|
|
Description: `Line number which specifies the start of the replacement range (inclusive).`,
|
2023-11-26 03:51:06 -07:00
|
|
|
},
|
|
|
|
"end_line": {
|
|
|
|
Type: "integer",
|
2023-11-26 13:39:37 -07:00
|
|
|
Description: `Line number which specifies the end of the replacement range (inclusive). If unset, range extends to end of file.`,
|
2023-11-26 03:51:06 -07:00
|
|
|
},
|
|
|
|
"content": {
|
|
|
|
Type: "string",
|
2023-11-26 13:39:37 -07:00
|
|
|
Description: `Content to replace specified range. Omit to remove the specified range.`,
|
2023-11-26 03:51:06 -07:00
|
|
|
},
|
|
|
|
},
|
2023-11-26 13:39:37 -07:00
|
|
|
Required: []string{"path", "start_line"},
|
2023-11-26 03:51:06 -07:00
|
|
|
},
|
|
|
|
}},
|
|
|
|
Impl: func(args map[string]interface{}) (string, error) {
|
|
|
|
tmp, ok := args["path"]
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("path parameter to write_file was not included.")
|
|
|
|
}
|
|
|
|
path, ok := tmp.(string)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
|
|
|
}
|
|
|
|
var start_line int
|
|
|
|
tmp, ok = args["start_line"]
|
|
|
|
if ok {
|
|
|
|
tmp, ok := tmp.(float64)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Invalid start_line in function arguments: %v", tmp)
|
|
|
|
}
|
|
|
|
start_line = int(tmp)
|
|
|
|
}
|
|
|
|
var end_line int
|
|
|
|
tmp, ok = args["end_line"]
|
|
|
|
if ok {
|
|
|
|
tmp, ok := tmp.(float64)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Invalid end_line in function arguments: %v", tmp)
|
|
|
|
}
|
|
|
|
end_line = 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-26 10:33:33 -07:00
|
|
|
return FileReplaceLines(path, start_line, end_line, content), nil
|
|
|
|
},
|
|
|
|
},
|
2023-11-25 17:55:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
func resultToJson(result FunctionResult) string {
|
|
|
|
if result.Message == "" {
|
|
|
|
// When message not supplied, assume success
|
|
|
|
result.Message = "success"
|
|
|
|
}
|
|
|
|
|
|
|
|
jsonBytes, err := json.Marshal(result)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("Could not marshal FunctionResult to JSON: %v\n", err)
|
|
|
|
}
|
|
|
|
return string(jsonBytes)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ExecuteToolCalls handles the execution of all tool_calls provided, and
|
|
|
|
// returns their results formatted as []Message(s) with role: 'tool' and.
|
|
|
|
func ExecuteToolCalls(toolCalls []openai.ToolCall) ([]Message, error) {
|
|
|
|
var toolResults []Message
|
|
|
|
for _, toolCall := range toolCalls {
|
|
|
|
if toolCall.Type != "function" {
|
|
|
|
// unsupported tool type
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
tool, ok := AvailableTools[toolCall.Function.Name]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("Requested tool '%s' does not exist. Hallucination?", toolCall.Function.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
var functionArgs map[string]interface{}
|
|
|
|
err := json.Unmarshal([]byte(toolCall.Function.Arguments), &functionArgs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Could not unmarshal tool arguments. Malformed JSON? Error: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: ability to silence this
|
|
|
|
fmt.Fprintf(os.Stderr, "INFO: Executing tool '%s' with args %s\n", toolCall.Function.Name, toolCall.Function.Arguments)
|
|
|
|
|
|
|
|
// Execute the tool
|
|
|
|
toolResult, err := tool.Impl(functionArgs)
|
|
|
|
if err != nil {
|
|
|
|
// This can happen if the model missed or supplied invalid tool args
|
|
|
|
return nil, fmt.Errorf("Tool '%s' error: %v\n", toolCall.Function.Name, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
toolResults = append(toolResults, Message{
|
|
|
|
Role: "tool",
|
|
|
|
OriginalContent: toolResult,
|
|
|
|
ToolCallID: sql.NullString{String: toolCall.ID, Valid: true},
|
|
|
|
// name is not required since the introduction of ToolCallID
|
|
|
|
// hypothesis: by setting it, we inform the model of what a
|
|
|
|
// function's purpose was if future requests omit the function
|
|
|
|
// definition
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return toolResults, nil
|
|
|
|
}
|
|
|
|
|
2023-11-25 23:35:22 -07:00
|
|
|
// isPathContained attempts to verify whether `path` is the same as or
|
|
|
|
// contained within `directory`. It is overly cautious, returning false even if
|
|
|
|
// `path` IS contained within `directory`, but the two paths use different
|
|
|
|
// casing, and we happen to be on a case-insensitive filesystem.
|
|
|
|
// This is ultimately to attempt to stop an LLM from going outside of where I
|
|
|
|
// tell it to. Additional layers of security should be considered.. run in a
|
|
|
|
// VM/container.
|
|
|
|
func isPathContained(directory string, path string) (bool, error) {
|
|
|
|
// Clean and resolve symlinks for both paths
|
2023-11-26 00:32:28 -07:00
|
|
|
path, err := filepath.Abs(path)
|
2023-11-25 23:35:22 -07:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
2023-11-26 00:32:28 -07:00
|
|
|
|
|
|
|
// check if path exists
|
|
|
|
_, err = os.Stat(path)
|
2023-11-25 23:35:22 -07:00
|
|
|
if err != nil {
|
2023-11-26 00:32:28 -07:00
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return false, fmt.Errorf("Could not stat path: %v", err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
path, err = filepath.EvalSymlinks(path)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
2023-11-25 23:35:22 -07:00
|
|
|
}
|
|
|
|
|
2023-11-26 00:32:28 -07:00
|
|
|
directory, err = filepath.Abs(directory)
|
2023-11-25 23:35:22 -07:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
2023-11-26 00:32:28 -07:00
|
|
|
directory, err = filepath.EvalSymlinks(directory)
|
2023-11-25 23:35:22 -07:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Case insensitive checks
|
2023-11-26 00:32:28 -07:00
|
|
|
if !strings.EqualFold(path, directory) &&
|
|
|
|
!strings.HasPrefix(strings.ToLower(path), strings.ToLower(directory)+string(os.PathSeparator)) {
|
2023-11-25 23:35:22 -07:00
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func isPathWithinCWD(path string) (bool, *FunctionResult) {
|
|
|
|
cwd, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
return false, &FunctionResult{Message: "Failed to determine current working directory"}
|
|
|
|
}
|
|
|
|
if ok, err := isPathContained(cwd, path); !ok {
|
|
|
|
if err != nil {
|
|
|
|
return false, &FunctionResult{Message: fmt.Sprintf("Could not determine whether path '%s' is within the current working directory: %s", path, err.Error())}
|
|
|
|
}
|
|
|
|
return false, &FunctionResult{Message: fmt.Sprintf("Path '%s' is not within the current working directory", path)}
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2023-11-25 17:55:18 -07:00
|
|
|
func ReadDir(path string) string {
|
2023-11-25 23:35:22 -07:00
|
|
|
// TODO(?): implement whitelist - list of directories which model is allowed to work in
|
|
|
|
if path == "" {
|
|
|
|
path = "."
|
|
|
|
}
|
|
|
|
ok, res := isPathWithinCWD(path)
|
|
|
|
if !ok {
|
|
|
|
return resultToJson(*res)
|
|
|
|
}
|
|
|
|
|
|
|
|
files, err := os.ReadDir(path)
|
2023-11-25 17:55:18 -07:00
|
|
|
if err != nil {
|
|
|
|
return resultToJson(FunctionResult{
|
|
|
|
Message: err.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
var dirContents []map[string]interface{}
|
|
|
|
for _, f := range files {
|
|
|
|
info, _ := f.Info()
|
|
|
|
|
2023-11-25 23:37:54 -07:00
|
|
|
name := f.Name()
|
2023-11-26 00:32:28 -07:00
|
|
|
if strings.HasPrefix(name, ".") {
|
|
|
|
// skip hidden files
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-11-25 23:37:54 -07:00
|
|
|
entryType := "file"
|
|
|
|
size := info.Size()
|
2023-11-25 17:55:18 -07:00
|
|
|
|
|
|
|
if info.IsDir() {
|
2023-11-25 23:37:54 -07:00
|
|
|
name += "/"
|
|
|
|
entryType = "dir"
|
2023-11-25 17:55:18 -07:00
|
|
|
subdirfiles, _ := os.ReadDir(filepath.Join(".", path, info.Name()))
|
2023-11-25 23:37:54 -07:00
|
|
|
size = int64(len(subdirfiles))
|
2023-11-25 17:55:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
dirContents = append(dirContents, map[string]interface{}{
|
2023-11-25 23:37:54 -07:00
|
|
|
"name": name,
|
|
|
|
"type": entryType,
|
|
|
|
"size": size,
|
2023-11-25 17:55:18 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return resultToJson(FunctionResult{Result: dirContents})
|
|
|
|
}
|
2023-11-26 00:32:28 -07:00
|
|
|
|
|
|
|
func ReadFile(path string) string {
|
|
|
|
ok, res := isPathWithinCWD(path)
|
|
|
|
if !ok {
|
|
|
|
return resultToJson(*res)
|
|
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())})
|
|
|
|
}
|
2023-11-26 03:43:47 -07:00
|
|
|
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
|
|
content := strings.Builder{}
|
|
|
|
for i, line := range lines {
|
|
|
|
content.WriteString(fmt.Sprintf("%d\t%s\n", i+1, line))
|
|
|
|
}
|
|
|
|
|
2023-11-26 00:32:28 -07:00
|
|
|
return resultToJson(FunctionResult{
|
2023-11-26 03:43:47 -07:00
|
|
|
Result: content.String(),
|
2023-11-26 00:32:28 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func WriteFile(path string, content string) string {
|
|
|
|
ok, res := isPathWithinCWD(path)
|
|
|
|
if !ok {
|
|
|
|
return resultToJson(*res)
|
|
|
|
}
|
|
|
|
err := os.WriteFile(path, []byte(content), 0644)
|
|
|
|
if err != nil {
|
|
|
|
return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())})
|
|
|
|
}
|
|
|
|
return resultToJson(FunctionResult{})
|
|
|
|
}
|
2023-11-26 03:51:06 -07:00
|
|
|
|
2023-11-26 13:42:31 -07:00
|
|
|
func FileInsertLines(path string, position int, content string) string {
|
2023-11-26 03:51:06 -07:00
|
|
|
ok, res := isPathWithinCWD(path)
|
|
|
|
if !ok {
|
|
|
|
return resultToJson(*res)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read the existing file's content
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())})
|
|
|
|
}
|
|
|
|
_, err = os.Create(path)
|
|
|
|
if err != nil {
|
|
|
|
return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not create new file: %s", err.Error())})
|
|
|
|
}
|
|
|
|
data = []byte{}
|
|
|
|
}
|
|
|
|
|
2023-11-26 13:42:31 -07:00
|
|
|
if position < 1 {
|
2023-11-26 10:33:33 -07:00
|
|
|
return resultToJson(FunctionResult{Message: "start_line cannot be less than 1"})
|
2023-11-26 03:51:06 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
|
|
contentLines := strings.Split(strings.Trim(content, "\n"), "\n")
|
|
|
|
|
2023-11-26 13:42:31 -07:00
|
|
|
before := lines[:position-1]
|
|
|
|
after := lines[position-1:]
|
2023-11-26 10:33:33 -07:00
|
|
|
lines = append(before, append(contentLines, after...)...)
|
|
|
|
|
|
|
|
newContent := strings.Join(lines, "\n")
|
|
|
|
|
|
|
|
// Join the lines and write back to the file
|
|
|
|
err = os.WriteFile(path, []byte(newContent), 0644)
|
|
|
|
if err != nil {
|
|
|
|
return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())})
|
|
|
|
}
|
|
|
|
|
|
|
|
return resultToJson(FunctionResult{Result: newContent})
|
|
|
|
}
|
|
|
|
|
|
|
|
func FileReplaceLines(path string, startLine int, endLine int, content string) string {
|
|
|
|
ok, res := isPathWithinCWD(path)
|
|
|
|
if !ok {
|
|
|
|
return resultToJson(*res)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read the existing file's content
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())})
|
2023-11-26 03:51:06 -07:00
|
|
|
}
|
2023-11-26 10:33:33 -07:00
|
|
|
_, err = os.Create(path)
|
|
|
|
if err != nil {
|
|
|
|
return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not create new file: %s", err.Error())})
|
|
|
|
}
|
|
|
|
data = []byte{}
|
|
|
|
}
|
|
|
|
|
|
|
|
if startLine < 1 {
|
|
|
|
return resultToJson(FunctionResult{Message: "start_line cannot be less than 1"})
|
|
|
|
}
|
|
|
|
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
|
|
contentLines := strings.Split(strings.Trim(content, "\n"), "\n")
|
|
|
|
|
|
|
|
if endLine == 0 || endLine > len(lines) {
|
|
|
|
endLine = len(lines)
|
|
|
|
}
|
|
|
|
|
|
|
|
before := lines[:startLine-1]
|
|
|
|
after := lines[endLine:]
|
|
|
|
|
|
|
|
lines = append(before, append(contentLines, after...)...)
|
|
|
|
newContent := strings.Join(lines, "\n")
|
|
|
|
|
|
|
|
// Join the lines and write back to the file
|
|
|
|
err = os.WriteFile(path, []byte(newContent), 0644)
|
|
|
|
if err != nil {
|
|
|
|
return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())})
|
|
|
|
}
|
|
|
|
|
|
|
|
return resultToJson(FunctionResult{Result: newContent})
|
|
|
|
|
|
|
|
}
|