2026-03-21 16:20:16 +02:00
package sidekick
import (
"fmt"
"os"
2026-03-21 17:11:17 +02:00
"os/exec"
2026-03-21 16:20:16 +02:00
"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 ,
},
2026-03-21 17:11:17 +02:00
"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 ,
},
2026-03-21 16:20:16 +02:00
}
}
2026-03-21 17:11:17 +02:00
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
}
2026-03-21 16:20:16 +02:00
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 )
}