package tools import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" ) // Tool represents a function-calling tool exposed to an agent workflow. type Tool struct { Name string `json:"name"` Description string `json:"description"` Parameters Schema `json:"parameters"` } // Schema defines the JSON schema for tool parameters. type Schema struct { Type string `json:"type"` Description string `json:"description,omitempty"` Properties map[string]Schema `json:"properties,omitempty"` Required []string `json:"required,omitempty"` Items *Schema `json:"items,omitempty"` } // Result is the output from executing a tool. type Result struct { Content string `json:"content"` Success bool `json:"success"` Error string `json:"error,omitempty"` TotalTokens int `json:"total_tokens,omitempty"` } // WorkspaceValidator ensures operations stay within the allowed workspace. type WorkspaceValidator struct { workspacePath string } func cancelledResult() (*Result, error) { return &Result{Success: false, Error: "operation cancelled"}, nil } func preferredShellPath() string { shell := strings.TrimSpace(os.Getenv("SHELL")) if shell != "" { if strings.Contains(shell, string(os.PathSeparator)) { return shell } if resolved, err := exec.LookPath(shell); err == nil { return resolved } } if runtime.GOOS == "darwin" { return "/bin/zsh" } return "/bin/sh" } func shellQuote(arg string) string { if arg == "" { return "''" } return "'" + strings.ReplaceAll(arg, "'", `'"'"'`) + "'" } func buildShellCommand(command string, args []string) string { parts := make([]string, 0, 1+len(args)) if trimmed := strings.TrimSpace(command); trimmed != "" { parts = append(parts, trimmed) } for _, arg := range args { parts = append(parts, shellQuote(arg)) } return strings.TrimSpace(strings.Join(parts, " ")) } // NewWorkspaceValidator creates a validator for the given workspace. func NewWorkspaceValidator(workspacePath string) *WorkspaceValidator { absPath, err := filepath.Abs(workspacePath) if err != nil { absPath = workspacePath } return &WorkspaceValidator{workspacePath: absPath} } func (v *WorkspaceValidator) resolvePath(path string) (string, error) { trimmed := strings.TrimSpace(path) if trimmed == "" || trimmed == "." { return v.workspacePath, nil } candidate := trimmed if !filepath.IsAbs(candidate) { candidate = filepath.Join(v.workspacePath, candidate) } absPath, err := filepath.Abs(candidate) if err != nil { return "", fmt.Errorf("invalid path: %w", err) } cleaned := filepath.Clean(absPath) if !strings.HasPrefix(cleaned, v.workspacePath) && cleaned != v.workspacePath { return "", fmt.Errorf("access denied: path '%s' is outside workspace '%s'", path, v.workspacePath) } return cleaned, nil } // Validate checks if a path is within the workspace and returns the resolved absolute path. func (v *WorkspaceValidator) Validate(path string) (string, error) { return v.resolvePath(path) } // IsWithinWorkspace checks if a path is within the workspace. func (v *WorkspaceValidator) IsWithinWorkspace(path string) bool { _, err := v.resolvePath(path) return err == nil } func normalizeLineEndings(text string) string { return strings.ReplaceAll(text, "\r\n", "\n") } func splitLines(text string) []string { text = normalizeLineEndings(text) if text == "" { return nil } lines := strings.Split(text, "\n") if len(lines) > 0 && lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] } return lines } func formatNumberedLines(lines []string, startLine int) string { if len(lines) == 0 { return "" } var builder strings.Builder for i, line := range lines { if i > 0 { builder.WriteByte('\n') } fmt.Fprintf(&builder, "%d: %s", startLine+i, line) } return builder.String() } func spliceLineRange(existing []string, startLine, endLine int, replacement []string) ([]string, error) { if startLine < 1 { return nil, fmt.Errorf("start_line must be >= 1") } if endLine < -1 { return nil, fmt.Errorf("end_line must be -1 or >= 0") } totalLines := len(existing) if endLine == -1 { endLine = totalLines } if endLine < startLine-1 { return nil, fmt.Errorf("end_line must be -1 or at least start_line-1") } if startLine > totalLines+1 { return nil, fmt.Errorf("start_line %d is beyond EOF; file has %d lines", startLine, totalLines) } if endLine > totalLines { return nil, fmt.Errorf("end_line %d is beyond EOF; file has %d lines", endLine, totalLines) } prefix := append([]string(nil), existing[:startLine-1]...) suffix := append([]string(nil), existing[endLine:]...) nextLines := make([]string, 0, len(prefix)+len(replacement)+len(suffix)) nextLines = append(nextLines, prefix...) nextLines = append(nextLines, replacement...) nextLines = append(nextLines, suffix...) return nextLines, nil } func lineRangeLines(lines []string, startLine, endLine int) ([]string, error) { if startLine < 1 { return nil, fmt.Errorf("start_line must be >= 1") } if endLine < -1 { return nil, fmt.Errorf("end_line must be -1 or >= 0") } totalLines := len(lines) if endLine == -1 { endLine = totalLines } if endLine < startLine-1 { return nil, fmt.Errorf("end_line must be -1 or at least start_line-1") } if startLine > totalLines+1 { return nil, fmt.Errorf("start_line %d is beyond EOF; file has %d lines", startLine, totalLines) } if endLine > totalLines { return nil, fmt.Errorf("end_line %d is beyond EOF; file has %d lines", endLine, totalLines) } if endLine < startLine-1 { return []string{}, nil } if totalLines == 0 || endLine < startLine { return []string{}, nil } return append([]string(nil), lines[startLine-1:endLine]...), nil } func findUniqueLineSequence(haystack, needle []string) (int, error) { if len(needle) == 0 { return 0, fmt.Errorf("expected_text cannot be empty") } if len(needle) > len(haystack) { return 0, fmt.Errorf("expected text has %d lines, but the requested range only has %d lines", len(needle), len(haystack)) } matchIndex := -1 matchCount := 0 for i := 0; i <= len(haystack)-len(needle); i++ { matched := true for j := range needle { if haystack[i+j] != needle[j] { matched = false break } } if matched { matchCount++ if matchIndex == -1 { matchIndex = i } } } switch { case matchCount == 0: return 0, fmt.Errorf("expected text was not found within the requested range") case matchCount > 1: return 0, fmt.Errorf("expected text matched %d locations within the requested range", matchCount) default: return matchIndex, nil } } func stripReadFileLinePrefix(line string) (string, bool) { if line == "" { return "", false } i := 0 for i < len(line) && line[i] >= '0' && line[i] <= '9' { i++ } if i == 0 || i >= len(line) || line[i] != ':' { return "", false } rest := line[i+1:] if strings.HasPrefix(rest, " ") { rest = rest[1:] } return rest, true } func normalizePatchExpectedLines(expectedText string) []string { lines := splitLines(expectedText) if len(lines) == 0 { return nil } if len(lines) >= 2 && strings.HasPrefix(lines[0], "File: ") && strings.HasPrefix(lines[1], "Lines ") && strings.Contains(lines[1], " of ") { lines = lines[2:] normalized := make([]string, len(lines)) for i, line := range lines { if stripped, ok := stripReadFileLinePrefix(line); ok { normalized[i] = stripped } else { normalized[i] = line } } return normalized } normalized := make([]string, len(lines)) allNumbered := true for i, line := range lines { stripped, ok := stripReadFileLinePrefix(line) if !ok { allNumbered = false break } normalized[i] = stripped } if allNumbered { return normalized } return lines } // --- File Tools --- // ReadFile reads a line range from a file within the workspace. func (v *WorkspaceValidator) ReadFile(ctx context.Context, path string, startLine, endLine int) (*Result, error) { if ctx != nil && ctx.Err() != nil { return cancelledResult() } absPath, err := v.Validate(path) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } data, err := os.ReadFile(absPath) if err != nil { return &Result{Success: false, Error: fmt.Sprintf("failed to read file: %v", err)}, nil } if startLine < 1 { return &Result{Success: false, Error: "start_line must be >= 1"}, nil } if endLine != -1 && endLine < 1 { return &Result{Success: false, Error: "end_line must be -1 or >= 1"}, nil } if endLine != -1 && endLine < startLine { return &Result{Success: false, Error: "end_line must be -1 or greater than or equal to start_line"}, nil } lines := splitLines(string(data)) totalLines := len(lines) if totalLines == 0 { return &Result{ Content: fmt.Sprintf("File: %s\nFile is empty.", absPath), Success: true, }, nil } actualEnd := totalLines if endLine != -1 { if endLine < totalLines { actualEnd = endLine } } if startLine > totalLines { displayEnd := "EOF" if endLine != -1 { displayEnd = fmt.Sprintf("%d", endLine) } return &Result{ Content: fmt.Sprintf( "File: %s\nRequested lines %d-%s are beyond EOF. File has %d lines.", absPath, startLine, displayEnd, totalLines, ), Success: true, }, nil } if actualEnd < startLine { return &Result{ Content: fmt.Sprintf( "File: %s\nRequested lines %d-%d are empty. File has %d lines.", absPath, startLine, actualEnd, totalLines, ), Success: true, }, nil } selected := lines[startLine-1 : actualEnd] displayEnd := fmt.Sprintf("%d", actualEnd) if endLine == -1 && actualEnd == totalLines { displayEnd = "EOF" } return &Result{ Content: fmt.Sprintf( "File: %s\nLines %d-%s of %d:\n%s", absPath, startLine, displayEnd, totalLines, formatNumberedLines(selected, startLine), ), Success: true, }, nil } // WriteFile replaces a line range in a file within the workspace, creating parent directories if needed. func (v *WorkspaceValidator) WriteFile(ctx context.Context, path string, startLine, endLine int, content string) (*Result, error) { if ctx != nil && ctx.Err() != nil { return cancelledResult() } absPath, err := v.Validate(path) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } dir := filepath.Dir(absPath) if err := os.MkdirAll(dir, 0755); err != nil { return &Result{Success: false, Error: fmt.Sprintf("failed to create directories: %v", err)}, nil } existingData, readErr := os.ReadFile(absPath) if readErr != nil && !os.IsNotExist(readErr) { return &Result{Success: false, Error: fmt.Sprintf("failed to read existing file: %v", readErr)}, nil } existingLines := splitLines(string(existingData)) replacementLines := splitLines(content) originalEndLine := endLine nextLines, err := spliceLineRange(existingLines, startLine, endLine, replacementLines) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } nextContent := strings.Join(nextLines, "\n") if err := os.WriteFile(absPath, []byte(nextContent), 0644); err != nil { return &Result{Success: false, Error: fmt.Sprintf("failed to write file: %v", err)}, nil } action := "replaced" if os.IsNotExist(readErr) || (len(existingLines) == 0 && startLine == 1 && originalEndLine <= 0) { action = "created" } else if originalEndLine < startLine { action = "inserted" } replacedCount := 0 if endLine == -1 { replacedCount = len(existingLines) - startLine + 1 if replacedCount < 0 { replacedCount = 0 } } else if endLine >= startLine { replacedCount = endLine - startLine + 1 } message := fmt.Sprintf("Successfully %s %s", action, absPath) if action == "inserted" { message = fmt.Sprintf("Successfully inserted %d lines before line %d in %s", len(replacementLines), startLine, absPath) } else if action == "created" { message = fmt.Sprintf("Successfully created %s with %d lines", absPath, len(nextLines)) } else { message = fmt.Sprintf("Successfully replaced %d lines starting at line %d in %s", replacedCount, startLine, absPath) } return &Result{Content: message, Success: true}, nil } // PatchFile replaces the unique snippet matching the expected text within a requested line window. func (v *WorkspaceValidator) PatchFile(ctx context.Context, path string, startLine, endLine int, expectedText, content string) (*Result, error) { if ctx != nil && ctx.Err() != nil { return cancelledResult() } absPath, err := v.Validate(path) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } dir := filepath.Dir(absPath) if err := os.MkdirAll(dir, 0755); err != nil { return &Result{Success: false, Error: fmt.Sprintf("failed to create directories: %v", err)}, nil } existingData, readErr := os.ReadFile(absPath) if readErr != nil && !os.IsNotExist(readErr) { return &Result{Success: false, Error: fmt.Sprintf("failed to read existing file: %v", readErr)}, nil } existingLines := splitLines(string(existingData)) windowLines, err := lineRangeLines(existingLines, startLine, endLine) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } expectedLines := normalizePatchExpectedLines(expectedText) matchOffset, err := findUniqueLineSequence(windowLines, expectedLines) if err != nil { displayEnd := "EOF" if endLine != -1 { displayEnd = fmt.Sprintf("%d", endLine) } return &Result{ Success: false, Error: fmt.Sprintf( "patch guard failed for requested range %d-%s: %v. Re-read the exact snippet and include a little more context if needed.", startLine, displayEnd, err, ), }, nil } actualStartLine := startLine + matchOffset actualEndLine := actualStartLine + len(expectedLines) - 1 replacementLines := splitLines(content) nextLines, err := spliceLineRange(existingLines, actualStartLine, actualEndLine, replacementLines) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } nextContent := strings.Join(nextLines, "\n") if err := os.WriteFile(absPath, []byte(nextContent), 0644); err != nil { return &Result{Success: false, Error: fmt.Sprintf("failed to write file: %v", err)}, nil } return &Result{ Content: fmt.Sprintf("Successfully patched lines %d-%d in %s", actualStartLine, actualEndLine, absPath), Success: true, }, nil } // CreateDirectory creates a directory within the workspace. func (v *WorkspaceValidator) CreateDirectory(ctx context.Context, path string) (*Result, error) { if ctx != nil && ctx.Err() != nil { return cancelledResult() } absPath, err := v.Validate(path) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } if err := os.MkdirAll(absPath, 0755); err != nil { return &Result{Success: false, Error: fmt.Sprintf("failed to create directory: %v", err)}, nil } return &Result{Content: fmt.Sprintf("Successfully created directory: %s", absPath), Success: true}, nil } // DeleteFile removes a file within the workspace. func (v *WorkspaceValidator) DeleteFile(ctx context.Context, path string) (*Result, error) { if ctx != nil && ctx.Err() != nil { return cancelledResult() } absPath, err := v.Validate(path) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } if err := os.Remove(absPath); err != nil { if os.IsNotExist(err) { return &Result{Success: false, Error: fmt.Sprintf("file not found: %s", path)}, nil } return &Result{Success: false, Error: fmt.Sprintf("failed to delete file: %v", err)}, nil } return &Result{Content: fmt.Sprintf("Successfully deleted: %s", absPath), Success: true}, nil } // ListDirectory lists files and directories within the workspace. func (v *WorkspaceValidator) ListDirectory(ctx context.Context, path string) (*Result, error) { if ctx != nil && ctx.Err() != nil { return cancelledResult() } absPath, err := v.Validate(path) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } entries, err := os.ReadDir(absPath) if err != nil { return &Result{Success: false, Error: fmt.Sprintf("failed to read directory: %v", err)}, nil } var lines []string lines = append(lines, fmt.Sprintf("Contents of %s:", absPath)) for _, entry := range entries { if ctx != nil && ctx.Err() != nil { return cancelledResult() } mode := entry.Type() var prefix string if mode.IsDir() { prefix = "D " } else { prefix = "F " } info, err := entry.Info() size := "0" if err == nil && info != nil { size = fmt.Sprintf("%d", info.Size()) } name := entry.Name() if mode.IsDir() { name += "/" } lines = append(lines, fmt.Sprintf("%s %8s %s", prefix, size, name)) } return &Result{Content: strings.Join(lines, "\n")}, nil } // --- Search Tools --- // SearchFiles searches for files matching a pattern within the workspace. func (v *WorkspaceValidator) SearchFiles(ctx context.Context, pathPattern string) (*Result, error) { if ctx != nil && ctx.Err() != nil { return cancelledResult() } // Validate the base path baseDir := filepath.Dir(pathPattern) if baseDir != "." && baseDir != "/" { if _, err := v.Validate(baseDir); err != nil { return &Result{Success: false, Error: err.Error()}, nil } } var matches []string pattern := pathPattern err := filepath.Walk(v.workspacePath, func(currentPath string, info os.FileInfo, err error) error { if ctx != nil && ctx.Err() != nil { return ctx.Err() } if err != nil { return nil } if info.IsDir() { return nil } relPath, _ := filepath.Rel(v.workspacePath, currentPath) if matched, _ := filepath.Match(pattern, relPath); matched { matches = append(matches, relPath) return nil } if matched, _ := filepath.Match(pattern, filepath.Base(relPath)); matched { matches = append(matches, relPath) } return nil }) if ctx != nil && ctx.Err() != nil { return cancelledResult() } if err != nil { if errors.Is(err, context.Canceled) { return cancelledResult() } return &Result{Success: false, Error: fmt.Sprintf("search failed: %v", err)}, nil } if len(matches) == 0 { return &Result{Content: "No files found matching pattern.", Success: true}, nil } var output []string output = append(output, fmt.Sprintf("Found %d files matching '%s':", len(matches), pattern)) for _, m := range matches { output = append(output, " "+m) } return &Result{Content: strings.Join(output, "\n")}, nil } // Grep searches for text content within files in the workspace. func (v *WorkspaceValidator) Grep(ctx context.Context, searchTerm string, path string) (*Result, error) { if ctx != nil && ctx.Err() != nil { return cancelledResult() } absPath, err := v.Validate(path) if err != nil { return &Result{Success: false, Error: err.Error()}, nil } var results []string count := 0 err = filepath.Walk(absPath, func(currentPath string, info os.FileInfo, err error) error { if ctx != nil && ctx.Err() != nil { return ctx.Err() } if err != nil { return nil } if info.IsDir() { return nil } if count >= 100 { return fmt.Errorf("max results reached") } data, err := os.ReadFile(currentPath) if err != nil { return nil } relPath, _ := filepath.Rel(v.workspacePath, currentPath) lines := strings.Split(string(data), "\n") for i, line := range lines { if strings.Contains(line, searchTerm) { results = append(results, fmt.Sprintf("%s:%d: %s", relPath, i+1, strings.TrimSpace(line))) count++ } } return nil }) if ctx != nil && ctx.Err() != nil { return cancelledResult() } if count >= 100 { results = append(results, "... (truncated at 100 results)") } if len(results) == 0 { return &Result{Content: fmt.Sprintf("No matches for '%s' in %s", searchTerm, path)}, nil } return &Result{Content: strings.Join(results, "\n")}, nil } // --- Shell Tools --- // RunCommand executes a shell command within the workspace. func (v *WorkspaceValidator) RunCommand(ctx context.Context, command string, args []string) (*Result, error) { if ctx != nil && ctx.Err() != nil { return cancelledResult() } shellCommand := buildShellCommand(command, args) if shellCommand == "" { return &Result{Success: false, Error: "command is required"}, nil } // Check for dangerous commands dangerousCmds := []string{"rm -rf /", "rm -rf *", "dd if=", "mkfs", "fdisk", "> /dev/"} for _, d := range dangerousCmds { if strings.Contains(strings.ToLower(shellCommand), d) { return &Result{Success: false, Error: fmt.Sprintf("dangerous command blocked: %s", shellCommand)}, nil } } if ctx == nil { ctx = context.Background() } shellPath := preferredShellPath() shellArgs := []string{"-lc", shellCommand} switch filepath.Base(shellPath) { case "zsh", "bash": shellArgs[0] = "-lic" } cmd := exec.CommandContext(ctx, shellPath, shellArgs...) cmd.Dir = v.workspacePath output, err := cmd.CombinedOutput() outputStr := string(output) if err != nil { if ctx.Err() != nil { return cancelledResult() } exitCode := -1 var exitErr *exec.ExitError if errors.As(err, &exitErr) { exitCode = exitErr.ExitCode() } return &Result{ Content: outputStr, Success: false, Error: fmt.Sprintf("command failed (exit %d): %v", exitCode, err), }, nil } // Truncate output maxLen := 50000 if len(outputStr) > maxLen { outputStr = outputStr[:maxLen] + "\n... [output truncated]" } return &Result{Content: outputStr, Success: true}, nil } // GetWorkspacePath returns the workspace path. func (v *WorkspaceValidator) GetWorkspacePath() string { return v.workspacePath } // --- Tool Definitions --- // AvailableTools returns the list of workspace tool definitions for function calling. func AvailableTools(workspacePath string) []Tool { tools := []Tool{ { Name: "read_file", Description: "Read a line range from a file in the workspace. Use start_line=1 and end_line=-1 to read the full file. The output includes line numbers and a header, and it can be pasted directly into patch_file as expected_text for the same snippet or range.", Parameters: Schema{ Type: "object", Properties: map[string]Schema{ "path": { Type: "string", Description: "The path of the file to read (relative to workspace)", }, "start_line": { Type: "integer", Description: "The first line to read, starting at 1. Defaults to 1.", }, "end_line": { Type: "integer", Description: "The last line to read, inclusive. Use -1 to read through EOF. Defaults to -1.", }, }, Required: []string{"path"}, }, }, { Name: "patch_file", Description: "Guarded snippet replacement within a line window. Like write_file, but only applies if expected_text matches one unique contiguous snippet inside the requested range. expected_text can be raw file text or the line-numbered output from read_file. Use this for surgical edits to avoid clobbering concurrent changes.", Parameters: Schema{ Type: "object", Properties: map[string]Schema{ "path": { Type: "string", Description: "The path of the file to patch (relative to workspace)", }, "start_line": { Type: "integer", Description: "The first line of the search window, starting at 1. Defaults to 1.", }, "end_line": { Type: "integer", Description: "The last line of the search window, inclusive. Use -1 to search through EOF.", }, "expected_text": { Type: "string", Description: "The current text to match. This can be raw file text or the output from read_file for a matching snippet within the requested window.", }, "content": { Type: "string", Description: "The replacement text for the requested line range", }, }, Required: []string{"path", "expected_text", "content"}, }, }, { Name: "write_file", Description: "Replace a line range in a file. Use start_line=1 and end_line=-1 to replace the full file or create a new file. Use end_line=start_line-1 to insert content before a line, such as start_line=1 and end_line=0 to prepend to an existing file. Prefer patch_file when you want to guard against stale content.", Parameters: Schema{ Type: "object", Properties: map[string]Schema{ "path": { Type: "string", Description: "The path of the file to write (relative to workspace)", }, "start_line": { Type: "integer", Description: "The first line to replace, starting at 1. Defaults to 1.", }, "end_line": { Type: "integer", Description: "The last line to replace, inclusive. Use -1 to replace through EOF. Use start_line-1 to insert before start_line.", }, "content": { Type: "string", Description: "The replacement text for the requested line range", }, }, Required: []string{"path", "content"}, }, }, { Name: "create_directory", Description: "Create a new directory or get an existing one within the workspace.", Parameters: Schema{ Type: "object", Properties: map[string]Schema{ "path": { Type: "string", Description: "The path of the directory to create (relative to workspace)", }, }, Required: []string{"path"}, }, }, { Name: "delete_file", Description: "Delete a file from the workspace.", Parameters: Schema{ Type: "object", Properties: map[string]Schema{ "path": { Type: "string", Description: "The path of the file to delete (relative to workspace)", }, }, Required: []string{"path"}, }, }, { Name: "list_directory", Description: "List the contents of a directory within the workspace. Shows file sizes and types.", Parameters: Schema{ Type: "object", Properties: map[string]Schema{ "path": { Type: "string", Description: "The path of the directory to list (relative to workspace, defaults to workspace root if '.' or empty)", }, }, Required: []string{"path"}, }, }, { Name: "search_files", Description: "Search for files by name pattern within the workspace directory tree.", Parameters: Schema{ Type: "object", Properties: map[string]Schema{ "path_pattern": { Type: "string", Description: "The glob pattern to match (e.g., '*.go', 'src/**/*.ts')", }, }, Required: []string{"path_pattern"}, }, }, { Name: "grep", Description: "Search for text content within files in the workspace. Limited to 100 results.", Parameters: Schema{ Type: "object", Properties: map[string]Schema{ "search_term": { Type: "string", Description: "The text pattern to search for", }, "path": { Type: "string", Description: "The directory to search in (relative to workspace)", }, }, Required: []string{"search_term", "path"}, }, }, { Name: "run_command", Description: "Execute a shell command through the user's login shell within the workspace directory. Shell syntax, pipes, redirects, and installed tools available on PATH are supported.", Parameters: Schema{ Type: "object", Properties: map[string]Schema{ "command": { Type: "string", Description: "The command to execute (e.g., 'ls', 'cat', 'go build', 'npm test')", }, "args": { Type: "array", Items: &Schema{ Type: "string", }, Description: "Arguments for the command", }, }, Required: []string{"command"}, }, }, } _ = workspacePath return tools }