package tools import ( "context" "encoding/json" "errors" "fmt" "os" "os/exec" "strings" "agentu/internal/llm" ) type ShellRunTool struct { workingDir string } func NewShellRunTool(workingDir string) *ShellRunTool { return &ShellRunTool{workingDir: workingDir} } func (t *ShellRunTool) Definition() llm.Tool { return llm.Tool{ Type: "function", Function: llm.ToolFunction{ Name: "shell_run", Description: "Run a non-interactive shell command on the local machine. Use for builds, tests, local status inspection, and local automation. Avoid commands that require an interactive TTY, credentials, or open long-running sessions.", Parameters: JSONSchema(`{ "type": "object", "properties": { "command": {"type": "string", "description": "Command to execute."}, "workdir": {"type": "string", "description": "Optional working directory. Relative paths are resolved from the configured agent working directory."} }, "required": ["command"], "additionalProperties": false }`), }, } } type shellRunArgs struct { Command string `json:"command"` Workdir string `json:"workdir"` } func (t *ShellRunTool) Execute(ctx context.Context, raw json.RawMessage) (string, error) { var args shellRunArgs if err := json.Unmarshal(raw, &args); err != nil { return "", fmt.Errorf("parse arguments: %w", err) } if strings.TrimSpace(args.Command) == "" { return "", errors.New("command is required") } workdir, err := resolvePath(t.workingDir, args.Workdir) if err != nil { return "", err } shell := os.Getenv("SHELL") if shell == "" { shell = "/bin/sh" } cmd := exec.CommandContext(ctx, shell, "-lc", args.Command) cmd.Dir = workdir output, err := cmd.CombinedOutput() text := FormatResult(string(output)) if errors.Is(ctx.Err(), context.DeadlineExceeded) { return text, fmt.Errorf("command timed out: %w", ctx.Err()) } if err != nil { if text != "" { return text, fmt.Errorf("command failed: %w", err) } return "", fmt.Errorf("command failed: %w", err) } return text, nil }