refactored the dir structure
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
package sidekick
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultInternalTools returns a map of standard file I/O tools
|
||||
func DefaultInternalTools() map[string]ToolDefinition {
|
||||
return map[string]ToolDefinition{
|
||||
"read_file": {
|
||||
Name: "read_file",
|
||||
Description: "Read a file, optionally with line windowing. " +
|
||||
"Every line is prefixed with 'line:hash:' (e.g. '1:abcd:content').",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{"type": "string"},
|
||||
"offset": map[string]interface{}{"type": "integer",
|
||||
"description": "Starting line number (0-indexed)"},
|
||||
"limit": map[string]interface{}{"type": "integer", "description": "Number of lines to read"},
|
||||
},
|
||||
"required": []string{"path"},
|
||||
},
|
||||
Internal: true,
|
||||
Handler: readFileHandler,
|
||||
},
|
||||
"write_file": {
|
||||
Name: "write_file",
|
||||
Description: "Overwrite a file with new content. Automatically strips 'line:hash:' prefixes if present.",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{"type": "string"},
|
||||
"content": map[string]interface{}{"type": "string"},
|
||||
},
|
||||
"required": []string{"path", "content"},
|
||||
},
|
||||
Internal: true,
|
||||
Handler: writeFileHandler,
|
||||
},
|
||||
"grep_file": {
|
||||
Name: "grep_file",
|
||||
Description: "Regex search with surrounding context. " +
|
||||
"Every line is prefixed with 'line:hash:' (e.g. '1:abcd:content').",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{"type": "string"},
|
||||
"pattern": map[string]interface{}{"type": "string"},
|
||||
"context_lines": map[string]interface{}{"type": "integer"},
|
||||
},
|
||||
"required": []string{"path", "pattern"},
|
||||
},
|
||||
Internal: true,
|
||||
Handler: grepFileHandler,
|
||||
},
|
||||
"edit_file": {
|
||||
Name: "edit_file",
|
||||
Description: "Replace a string or a block of lines in a file. " +
|
||||
"If 'old_string' contains 'line:hash:' prefixes for every line, it performs a precise replacement " +
|
||||
"at the specified line numbers. Otherwise, it performs a standard first-occurrence replacement. " +
|
||||
"Both multiline strings and 'line:hash:' stripping are supported.",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{"type": "string"},
|
||||
"old_string": map[string]interface{}{"type": "string"},
|
||||
"new_string": map[string]interface{}{"type": "string"},
|
||||
},
|
||||
"required": []string{"path", "old_string", "new_string"},
|
||||
},
|
||||
Internal: true,
|
||||
Handler: editFileHandler,
|
||||
},
|
||||
"shell_exec": {
|
||||
Name: "shell_exec",
|
||||
Description: "Run a POSIX shell command and return its combined stdout and stderr.",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"command": map[string]interface{}{"type": "string"},
|
||||
},
|
||||
"required": []string{"command"},
|
||||
},
|
||||
Internal: true,
|
||||
Handler: shellExecHandler,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func shellExecHandler(args map[string]interface{}) (string, error) {
|
||||
cmdStr, ok := args["command"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("command is required")
|
||||
}
|
||||
|
||||
cmd := exec.Command("sh", "-c", cmdStr)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
result := string(output)
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("%s\nError: %v", result, err)
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
return "(empty output)", nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func readFileHandler(args map[string]interface{}) (string, error) {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("path is required")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
|
||||
offset := 0
|
||||
if off, ok := args["offset"].(float64); ok {
|
||||
offset = int(off)
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
limit := len(lines)
|
||||
if lim, ok := args["limit"].(float64); ok {
|
||||
limit = int(lim)
|
||||
}
|
||||
|
||||
end := offset + limit
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
if offset >= len(lines) {
|
||||
return "", nil // Empty reading window
|
||||
}
|
||||
|
||||
var outputLines []string
|
||||
for i, line := range lines[offset:end] {
|
||||
lineNum := offset + i + 1
|
||||
outputLines = append(outputLines, fmt.Sprintf("%d:%s:%s", lineNum, hashLine(line), line))
|
||||
}
|
||||
|
||||
return strings.Join(outputLines, "\n"), nil
|
||||
}
|
||||
|
||||
func writeFileHandler(args map[string]interface{}) (string, error) {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("path is required")
|
||||
}
|
||||
content, ok := args["content"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("content is required")
|
||||
}
|
||||
|
||||
content = stripHashlines(content)
|
||||
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Successfully wrote to %s", path), nil
|
||||
}
|
||||
|
||||
func grepFileHandler(args map[string]interface{}) (string, error) {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("path is required")
|
||||
}
|
||||
pattern, ok := args["pattern"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("pattern is required")
|
||||
}
|
||||
|
||||
contextLines := 0
|
||||
if cl, ok := args["context_lines"].(float64); ok {
|
||||
contextLines = int(cl)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid regex: %w", err)
|
||||
}
|
||||
|
||||
var results []string
|
||||
for i, line := range lines {
|
||||
if re.MatchString(line) {
|
||||
start := i - contextLines
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := i + contextLines + 1
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
|
||||
results = append(results, fmt.Sprintf("--- Match around line %d ---", i+1))
|
||||
for j := start; j < end; j++ {
|
||||
results = append(results, fmt.Sprintf("%d:%s:%s", j+1, hashLine(lines[j]), lines[j]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return "No matches found.", nil
|
||||
}
|
||||
return strings.Join(results, "\n"), nil
|
||||
}
|
||||
func editFileHandler(args map[string]interface{}) (string, error) {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("path is required")
|
||||
}
|
||||
oldStr, ok := args["old_string"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("old_string is required")
|
||||
}
|
||||
newStr, ok := args["new_string"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("new_string is required")
|
||||
}
|
||||
|
||||
newStr = stripHashlines(newStr)
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
strContent := string(content)
|
||||
|
||||
hasHashlines, startLine, strippedOldStr := parseHashlines(oldStr)
|
||||
|
||||
if hasHashlines && startLine > 0 {
|
||||
fileLines := strings.Split(strContent, "\n")
|
||||
oldLines := strings.Split(strippedOldStr, "\n")
|
||||
|
||||
startIdx := startLine - 1
|
||||
endIdx := startIdx + len(oldLines)
|
||||
|
||||
if startIdx >= 0 && endIdx <= len(fileLines) {
|
||||
match := true
|
||||
for i := 0; i < len(oldLines); i++ {
|
||||
if fileLines[startIdx+i] != oldLines[i] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
var finalLines []string
|
||||
finalLines = append(finalLines, fileLines[:startIdx]...)
|
||||
if newStr != "" {
|
||||
finalLines = append(finalLines, strings.Split(newStr, "\n")...)
|
||||
}
|
||||
finalLines = append(finalLines, fileLines[endIdx:]...)
|
||||
|
||||
newContent := strings.Join(finalLines, "\n")
|
||||
err = os.WriteFile(path, []byte(newContent), 0644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Successfully edited %s", path), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to simple replacement
|
||||
strippedOldStr = stripHashlines(oldStr)
|
||||
if !strings.Contains(strContent, strippedOldStr) {
|
||||
return "", fmt.Errorf("old_string not found in file")
|
||||
}
|
||||
|
||||
// Replace first occurrence only
|
||||
newContent := strings.Replace(strContent, strippedOldStr, newStr, 1)
|
||||
|
||||
err = os.WriteFile(path, []byte(newContent), 0644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Successfully edited %s", path), nil
|
||||
}
|
||||
|
||||
func parseHashlines(s string) (bool, int, string) {
|
||||
if s == "" {
|
||||
return false, 0, s
|
||||
}
|
||||
var result []string
|
||||
lines := strings.Split(s, "\n")
|
||||
startLine := -1
|
||||
for i, line := range lines {
|
||||
parts := strings.SplitN(line, ":", 3)
|
||||
if len(parts) == 3 {
|
||||
isDigit := len(parts[0]) > 0
|
||||
for _, c := range parts[0] {
|
||||
if c < '0' || c > '9' {
|
||||
isDigit = false
|
||||
break
|
||||
}
|
||||
}
|
||||
isHex := len(parts[1]) == 4
|
||||
for _, c := range parts[1] {
|
||||
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
|
||||
isHex = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isDigit && isHex {
|
||||
if i == 0 {
|
||||
fmt.Sscanf(parts[0], "%d", &startLine)
|
||||
}
|
||||
result = append(result, parts[2])
|
||||
continue
|
||||
}
|
||||
}
|
||||
return false, 0, s
|
||||
}
|
||||
return true, startLine, strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func stripHashlines(s string) string {
|
||||
var result []string
|
||||
lines := strings.Split(s, "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.SplitN(line, ":", 3)
|
||||
if len(parts) == 3 {
|
||||
isDigit := len(parts[0]) > 0
|
||||
for _, c := range parts[0] {
|
||||
if c < '0' || c > '9' {
|
||||
isDigit = false
|
||||
break
|
||||
}
|
||||
}
|
||||
isHex := len(parts[1]) == 4
|
||||
for _, c := range parts[1] {
|
||||
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
|
||||
isHex = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isDigit && isHex {
|
||||
result = append(result, parts[2])
|
||||
continue
|
||||
}
|
||||
}
|
||||
result = append(result, line)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func hashLine(line string) string {
|
||||
var h uint32 = 2166136261
|
||||
for i := 0; i < len(line); i++ {
|
||||
h ^= uint32(line[i])
|
||||
h *= 16777619
|
||||
}
|
||||
return fmt.Sprintf("%04x", h&0xffff)
|
||||
}
|
||||
Reference in New Issue
Block a user