From b37ffa415a22b0e7f8e50dbe1b4b388ba00c02a3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 21:44:57 -0500 Subject: [PATCH] feat: add change directory tool to CLI (#12) Added a new tool 'change_directory' that allows changing the current working directory. The tool: - Takes a single path parameter - Returns the full path on success - Returns an error if directory doesn't exist Co-authored-by: github-actions[bot] --- internal/agent/anthropic.go | 21 ++++++++++++++++ internal/agent/deepseek.go | 17 +++++++++++++ internal/agent/gemini.go | 27 ++++++++++++++++++++ internal/agent/openai.go | 17 +++++++++++++ internal/agent/tools.go | 50 +++++++++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+) diff --git a/internal/agent/anthropic.go b/internal/agent/anthropic.go index bb2b5da..12a2934 100644 --- a/internal/agent/anthropic.go +++ b/internal/agent/anthropic.go @@ -96,6 +96,14 @@ func NewAnthropicExecutor(baseUrl string, apiKey string, logger Logger, ignorer Properties: a.F[any](getRelatedFilesTool.InputSchema["properties"]), }), }, + &a.BetaToolParam{ + Name: a.String(changeDirectoryTool.Name), + Description: a.String(changeDirectoryTool.Description), + InputSchema: a.F(a.BetaToolInputSchemaParam{ + Type: a.F(a.BetaToolInputSchemaTypeObject), + Properties: a.F[any](changeDirectoryTool.InputSchema["properties"]), + }), + }, }), } @@ -304,6 +312,19 @@ func (s *anthropicExecutor) Execute(input string) error { } s.logger.Printf("getting related files: %s", strings.Join(relatedFilesToolInput.InputFiles, ", ")) result, err = executeGetRelatedFilesTool(relatedFilesToolInput.InputFiles, s.ignorer) + case changeDirectoryTool.Name: + changeDirToolInput := struct { + Path string `json:"path"` + }{} + jsonInput, marshalErr := json.Marshal(block.Input) + if marshalErr != nil { + return fmt.Errorf("failed to marshal change directory tool input: %w", marshalErr) + } + if err := json.Unmarshal(jsonInput, &changeDirToolInput); err != nil { + return fmt.Errorf("failed to unmarshal change directory tool arguments: %w", err) + } + s.logger.Printf("changing directory to: %s", changeDirToolInput.Path) + result, err = executeChangeDirectoryTool(changeDirToolInput.Path) default: return fmt.Errorf("unexpected tool use block type: %s", block.Name) } diff --git a/internal/agent/deepseek.go b/internal/agent/deepseek.go index 7cd12b2..d09907d 100644 --- a/internal/agent/deepseek.go +++ b/internal/agent/deepseek.go @@ -76,6 +76,14 @@ func NewDeepSeekExecutor(baseUrl string, apiKey string, logger Logger, ignorer * Parameters: oai.F(oai.FunctionParameters(getRelatedFilesTool.InputSchema)), }), }, + { + Type: oai.F(oai.ChatCompletionToolTypeFunction), + Function: oai.F(oai.FunctionDefinitionParam{ + Name: oai.F(changeDirectoryTool.Name), + Description: oai.F(changeDirectoryTool.Description), + Parameters: oai.F(oai.FunctionParameters(changeDirectoryTool.InputSchema)), + }), + }, }), } @@ -178,6 +186,15 @@ func (o *deepseekExecutor) Execute(input string) error { } o.logger.Printf("getting related files: %s", strings.Join(relatedFilesToolInput.InputFiles, ", ")) result, err = executeGetRelatedFilesTool(relatedFilesToolInput.InputFiles, o.ignorer) + case changeDirectoryTool.Name: + var changeDirToolInput struct { + Path string `json:"path"` + } + if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &changeDirToolInput); err != nil { + return fmt.Errorf("failed to unmarshal change directory tool arguments: %w", err) + } + o.logger.Printf("changing directory to: %s", changeDirToolInput.Path) + result, err = executeChangeDirectoryTool(changeDirToolInput.Path) default: return fmt.Errorf("unexpected tool name: %s", toolCall.Function.Name) } diff --git a/internal/agent/gemini.go b/internal/agent/gemini.go index d28711a..9bae8b0 100644 --- a/internal/agent/gemini.go +++ b/internal/agent/gemini.go @@ -130,6 +130,20 @@ func NewGeminiExecutor(baseUrl string, apiKey string, logger Logger, ignorer *gi Required: []string{"input_files"}, }, }, + { + Name: changeDirectoryTool.Name, + Description: changeDirectoryTool.Description, + Parameters: &genai.Schema{ + Type: genai.TypeObject, + Properties: map[string]*genai.Schema{ + "path": { + Type: genai.TypeString, + Description: "The path to change to, can be relative or absolute", + }, + }, + Required: []string{"path"}, + }, + }, }, }, } @@ -256,6 +270,19 @@ func (g *geminiExecutor) Execute(input string) error { } g.logger.Printf("getting related files: %s", strings.Join(relatedFilesToolInput.InputFiles, ", ")) result, err = executeGetRelatedFilesTool(relatedFilesToolInput.InputFiles, g.ignorer) + case changeDirectoryTool.Name: + var changeDirToolInput struct { + Path string `json:"path"` + } + jsonInput, marshalErr := json.Marshal(v.Args) + if marshalErr != nil { + return fmt.Errorf("failed to marshal change directory tool input: %w", marshalErr) + } + if err := json.Unmarshal(jsonInput, &changeDirToolInput); err != nil { + return fmt.Errorf("failed to unmarshal change directory tool arguments: %w", err) + } + g.logger.Printf("changing directory to: %s", changeDirToolInput.Path) + result, err = executeChangeDirectoryTool(changeDirToolInput.Path) default: return fmt.Errorf("unexpected tool name: %s", v.Name) } diff --git a/internal/agent/openai.go b/internal/agent/openai.go index cd8af5c..6c06faf 100644 --- a/internal/agent/openai.go +++ b/internal/agent/openai.go @@ -95,6 +95,14 @@ func NewOpenAIExecutor(baseUrl string, apiKey string, logger Logger, ignorer *gi Parameters: oai.F(oai.FunctionParameters(getRelatedFilesTool.InputSchema)), }), }, + { + Type: oai.F(oai.ChatCompletionToolTypeFunction), + Function: oai.F(oai.FunctionDefinitionParam{ + Name: oai.F(changeDirectoryTool.Name), + Description: oai.F(changeDirectoryTool.Description), + Parameters: oai.F(oai.FunctionParameters(changeDirectoryTool.InputSchema)), + }), + }, }), } @@ -191,6 +199,15 @@ func (o *openaiExecutor) Execute(input string) error { } o.logger.Printf("getting related files: %s", strings.Join(relatedFilesToolInput.InputFiles, ", ")) result, err = executeGetRelatedFilesTool(relatedFilesToolInput.InputFiles, o.ignorer) + case changeDirectoryTool.Name: + var changeDirToolInput struct { + Path string `json:"path"` + } + if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &changeDirToolInput); err != nil { + return fmt.Errorf("failed to unmarshal change directory tool arguments: %w", err) + } + o.logger.Printf("changing directory to: %s", changeDirToolInput.Path) + result, err = executeChangeDirectoryTool(changeDirToolInput.Path) default: return fmt.Errorf("unexpected tool name: %s", toolCall.Function.Name) } diff --git a/internal/agent/tools.go b/internal/agent/tools.go index cb12fe7..5aafc06 100644 --- a/internal/agent/tools.go +++ b/internal/agent/tools.go @@ -167,6 +167,24 @@ var getRelatedFilesTool = Tool{ }, } +var changeDirectoryTool = Tool{ + Name: "change_directory", + Description: `A tool to change the current working directory +* The tool accepts a single parameter "path" specifying the target directory +* Returns the full path of the new directory if successful +* Returns an error message if the directory doesn't exist`, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "The path to change to, can be relative or absolute", + }, + }, + "required": []string{"path"}, + }, +} + type ToolResult struct { ToolUseID string Content any @@ -319,4 +337,36 @@ func executeGetRelatedFilesTool(inputFiles []string, ignorer *ignore.GitIgnore) return &ToolResult{ Content: sb.String(), }, nil +} + +// executeChangeDirectoryTool validates and executes the change directory tool +func executeChangeDirectoryTool(path string) (*ToolResult, error) { + // Get absolute path + absPath, err := filepath.Abs(path) + if err != nil { + return &ToolResult{ + Content: fmt.Sprintf("Error resolving absolute path: %s", err), + IsError: true, + }, nil + } + + // Check if directory exists + if info, err := os.Stat(absPath); err != nil || !info.IsDir() { + return &ToolResult{ + Content: fmt.Sprintf("Directory does not exist: %s", absPath), + IsError: true, + }, nil + } + + // Change directory + if err := os.Chdir(absPath); err != nil { + return &ToolResult{ + Content: fmt.Sprintf("Error changing directory: %s", err), + IsError: true, + }, nil + } + + return &ToolResult{ + Content: absPath, + }, nil } \ No newline at end of file