nub/internal/tool/builtin/read.go
Tom a97013d876 initial commit
- v 0.1.0 siehe CHANGELOG.md
2026-07-25 11:08:01 +02:00

98 lines
2.3 KiB
Go

package builtin
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"nub/internal/tool"
)
const defaultReadLimit = 2000
type ReadTool struct{}
func (ReadTool) Name() string { return "read" }
func (ReadTool) Description() string {
return "Liest eine Datei mit Zeilennummern, optional ab einem Offset mit Limit."
}
func (ReadTool) Schema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"path": {"type": "string"},
"offset": {"type": "integer", "description": "1-basierte Startzeile, Default 1"},
"limit": {"type": "integer", "description": "maximale Zeilenzahl, Default 2000"}
},
"required": ["path"]
}`)
}
type readInput struct {
Path string `json:"path"`
Offset int `json:"offset"`
Limit int `json:"limit"`
}
func (ReadTool) Run(ctx context.Context, input json.RawMessage, env tool.Env) (tool.Result, error) {
var in readInput
if err := json.Unmarshal(input, &in); err != nil {
return tool.Result{ForModel: "invalid input: " + err.Error(), IsError: true}, nil
}
path, err := resolvePath(env.RepoRoot, env.Cwd, in.Path)
if err != nil {
return tool.Result{ForModel: err.Error(), IsError: true}, nil
}
data, err := os.ReadFile(path)
if err != nil {
return tool.Result{ForModel: fmt.Sprintf("read %s: %v", in.Path, err), IsError: true}, nil
}
if bytes.IndexByte(data, 0) != -1 {
return tool.Result{ForModel: fmt.Sprintf("%s: binary file, not readable as text", in.Path), IsError: true}, nil
}
offset := in.Offset
if offset < 1 {
offset = 1
}
limit := in.Limit
if limit <= 0 {
limit = defaultReadLimit
}
lines := strings.Split(string(data), "\n")
if offset > len(lines) {
return tool.Result{ForModel: fmt.Sprintf("%s: offset %d beyond file end (%d lines)", in.Path, offset, len(lines)), IsError: true}, nil
}
end := offset - 1 + limit
if end > len(lines) {
end = len(lines)
}
var b strings.Builder
for i := offset - 1; i < end; i++ {
fmt.Fprintf(&b, "%6d\t%s\n", i+1, lines[i])
}
truncated := end < len(lines)
return tool.Result{
ForModel: b.String(),
ForUI: FileResult{
Path: in.Path,
Lines: end - (offset - 1),
Truncated: truncated,
},
}, nil
}
// FileResult ist die strukturierte UI-Repräsentation für read/write.
type FileResult struct {
Path string
Lines int
Truncated bool
}