package main import ( "bufio" "context" "encoding/json" "flag" "fmt" "log" "os" "os/exec" "path" "path/filepath" "strings" "github.com/anthropics/anthropic-sdk-go" "github.com/invopop/jsonschema" ) func main() { verbose := flag.Bool("verbose", false, "enable verbose logging") flag.Parse() if *verbose { log.SetOutput(os.Stderr) log.SetFlags(log.LstdFlags | log.Lshortfile) log.Println("Verbose logging enabled") } else { log.SetOutput(os.Stdout) log.SetFlags(0) log.SetPrefix("") } client := anthropic.NewClient() if *verbose { log.Println("Anthropic client initialized") } scanner := bufio.NewScanner(os.Stdin) getUserMessage := func() (string, bool) { if !scanner.Scan() { return "", false } return scanner.Text(), true } tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition, BashDefinition, EditFileDefinition} if *verbose { log.Printf("Initialized %d tools", len(tools)) } agent := NewAgent(&client, getUserMessage, tools, *verbose) err := agent.Run(context.TODO()) if err != nil { fmt.Printf("Error: %s\n", err.Error()) } } func NewAgent( client *anthropic.Client, getUserMessage func() (string, bool), tools []ToolDefinition, verbose bool, ) *Agent { return &Agent{ client: client, getUserMessage: getUserMessage, tools: tools, verbose: verbose, } } type Agent struct { client *anthropic.Client getUserMessage func() (string, bool) tools []ToolDefinition verbose bool } func (a *Agent) Run(ctx context.Context) error { conversation := []anthropic.MessageParam{} if a.verbose { log.Println("Starting chat session with tools enabled") } fmt.Println("Chat with Claude (use 'ctrl-c' to quit)") for { fmt.Print("\u001b[94mYou\u001b[0m: ") userInput, ok := a.getUserMessage() if !ok { if a.verbose { log.Println("User input ended, breaking from chat loop") } break } // Skip empty messages if userInput == "" { if a.verbose { log.Println("Skipping empty message") } continue } if a.verbose { log.Printf("User input received: %q", userInput) } userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)) conversation = append(conversation, userMessage) if a.verbose { log.Printf("Sending message to Claude, conversation length: %d", len(conversation)) } message, err := a.runInference(ctx, conversation) if err != nil { if a.verbose { log.Printf("Error during inference: %v", err) } return err } conversation = append(conversation, message.ToParam()) // Keep processing until Claude stops using tools for { // Collect all tool uses and their results var toolResults []anthropic.ContentBlockParamUnion var hasToolUse bool if a.verbose { log.Printf("Processing %d content blocks from Claude", len(message.Content)) } for _, content := range message.Content { switch content.Type { case "text": fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text) case "tool_use": hasToolUse = true toolUse := content.AsToolUse() if a.verbose { log.Printf("Tool use detected: %s with input: %s", toolUse.Name, string(toolUse.Input)) } fmt.Printf("\u001b[96mtool\u001b[0m: %s(%s)\n", toolUse.Name, string(toolUse.Input)) // Find and execute the tool var toolResult string var toolError error var toolFound bool for _, tool := range a.tools { if tool.Name == toolUse.Name { if a.verbose { log.Printf("Executing tool: %s", tool.Name) } toolResult, toolError = tool.Function(toolUse.Input) fmt.Printf("\u001b[92mresult\u001b[0m: %s\n", toolResult) if toolError != nil { fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) } if a.verbose { if toolError != nil { log.Printf("Tool execution failed: %v", toolError) } else { log.Printf("Tool execution successful, result length: %d chars", len(toolResult)) } } toolFound = true break } } if !toolFound { toolError = fmt.Errorf("tool '%s' not found", toolUse.Name) fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) } // Add tool result to collection if toolError != nil { toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, toolError.Error(), true)) } else { toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, toolResult, false)) } } } // If there were no tool uses, we're done if !hasToolUse { break } // Send all tool results back and get Claude's response if a.verbose { log.Printf("Sending %d tool results back to Claude", len(toolResults)) } toolResultMessage := anthropic.NewUserMessage(toolResults...) conversation = append(conversation, toolResultMessage) // Get Claude's response after tool execution message, err = a.runInference(ctx, conversation) if err != nil { if a.verbose { log.Printf("Error during followup inference: %v", err) } return err } conversation = append(conversation, message.ToParam()) if a.verbose { log.Printf("Received followup response with %d content blocks", len(message.Content)) } // Continue loop to process the new message } } if a.verbose { log.Println("Chat session ended") } return nil } func (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) { anthropicTools := []anthropic.ToolUnionParam{} for _, tool := range a.tools { anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{ OfTool: &anthropic.ToolParam{ Name: tool.Name, Description: anthropic.String(tool.Description), InputSchema: tool.InputSchema, }, }) } if a.verbose { log.Printf("Making API call to Claude with model: %s and %d tools", anthropic.ModelClaude3_7SonnetLatest, len(anthropicTools)) } message, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaude3_7SonnetLatest, MaxTokens: int64(1024), Messages: conversation, Tools: anthropicTools, }) if a.verbose { if err != nil { log.Printf("API call failed: %v", err) } else { log.Printf("API call successful, response received") } } return message, err } type ToolDefinition struct { Name string `json:"name"` Description string `json:"description"` InputSchema anthropic.ToolInputSchemaParam `json:"input_schema"` Function func(input json.RawMessage) (string, error) } var ReadFileDefinition = ToolDefinition{ Name: "read_file", Description: "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.", InputSchema: ReadFileInputSchema, Function: ReadFile, } var ListFilesDefinition = ToolDefinition{ Name: "list_files", Description: "List files and directories at a given path. If no path is provided, lists files in the current directory.", InputSchema: ListFilesInputSchema, Function: ListFiles, } var BashDefinition = ToolDefinition{ Name: "bash", Description: "Execute a bash command and return its output. Use this to run shell commands.", InputSchema: BashInputSchema, Function: Bash, } var EditFileDefinition = ToolDefinition{ Name: "edit_file", Description: `Make edits to a text file. Replaces 'old_str' with 'new_str' in the given file. 'old_str' and 'new_str' MUST be different from each other. If the file specified with path doesn't exist, it will be created. `, InputSchema: EditFileInputSchema, Function: EditFile, } type ReadFileInput struct { Path string `json:"path" jsonschema_description:"The relative path of a file in the working directory."` } var ReadFileInputSchema = GenerateSchema[ReadFileInput]() type ListFilesInput struct { Path string `json:"path,omitempty" jsonschema_description:"Optional relative path to list files from. Defaults to current directory if not provided."` } var ListFilesInputSchema = GenerateSchema[ListFilesInput]() type BashInput struct { Command string `json:"command" jsonschema_description:"The bash command to execute."` } var BashInputSchema = GenerateSchema[BashInput]() type EditFileInput struct { Path string `json:"path" jsonschema_description:"The path to the file"` OldStr string `json:"old_str" jsonschema_description:"Text to search for - must match exactly and must only have one match exactly"` NewStr string `json:"new_str" jsonschema_description:"Text to replace old_str with"` } var EditFileInputSchema = GenerateSchema[EditFileInput]() func ReadFile(input json.RawMessage) (string, error) { readFileInput := ReadFileInput{} err := json.Unmarshal(input, &readFileInput) if err != nil { panic(err) } log.Printf("Reading file: %s", readFileInput.Path) content, err := os.ReadFile(readFileInput.Path) if err != nil { log.Printf("Failed to read file %s: %v", readFileInput.Path, err) return "", err } log.Printf("Successfully read file %s (%d bytes)", readFileInput.Path, len(content)) return string(content), nil } func ListFiles(input json.RawMessage) (string, error) { listFilesInput := ListFilesInput{} err := json.Unmarshal(input, &listFilesInput) if err != nil { panic(err) } dir := "." if listFilesInput.Path != "" { dir = listFilesInput.Path } log.Printf("Listing files in directory: %s", dir) var files []string err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } relPath, err := filepath.Rel(dir, path) if err != nil { return err } // Skip .devenv directory and its contents if info.IsDir() && (relPath == ".devenv" || strings.HasPrefix(relPath, ".devenv/")) { return filepath.SkipDir } if relPath != "." { if info.IsDir() { files = append(files, relPath+"/") } else { files = append(files, relPath) } } return nil }) if err != nil { log.Printf("Failed to list files in %s: %v", dir, err) return "", err } result, err := json.Marshal(files) if err != nil { return "", err } log.Printf("Successfully listed %d files in %s", len(files), dir) return string(result), nil } func Bash(input json.RawMessage) (string, error) { bashInput := BashInput{} err := json.Unmarshal(input, &bashInput) if err != nil { return "", err } log.Printf("Executing bash command: %s", bashInput.Command) cmd := exec.Command("bash", "-c", bashInput.Command) output, err := cmd.CombinedOutput() if err != nil { log.Printf("Bash command failed: %v", err) return fmt.Sprintf("Command failed with error: %s\nOutput: %s", err.Error(), string(output)), nil } log.Printf("Bash command executed successfully, output length: %d chars", len(output)) return strings.TrimSpace(string(output)), nil } func EditFile(input json.RawMessage) (string, error) { editFileInput := EditFileInput{} err := json.Unmarshal(input, &editFileInput) if err != nil { return "", err } if editFileInput.Path == "" || editFileInput.OldStr == editFileInput.NewStr { log.Printf("EditFile failed: invalid input parameters") return "", fmt.Errorf("invalid input parameters") } log.Printf("Editing file: %s (replacing %d chars with %d chars)", editFileInput.Path, len(editFileInput.OldStr), len(editFileInput.NewStr)) content, err := os.ReadFile(editFileInput.Path) if err != nil { if os.IsNotExist(err) && editFileInput.OldStr == "" { log.Printf("File does not exist, creating new file: %s", editFileInput.Path) return createNewFile(editFileInput.Path, editFileInput.NewStr) } log.Printf("Failed to read file %s: %v", editFileInput.Path, err) return "", err } oldContent := string(content) // Special case: if old_str is empty, we're appending to the file var newContent string if editFileInput.OldStr == "" { newContent = oldContent + editFileInput.NewStr } else { // Count occurrences first to ensure we have exactly one match count := strings.Count(oldContent, editFileInput.OldStr) if count == 0 { log.Printf("EditFile failed: old_str not found in file %s", editFileInput.Path) return "", fmt.Errorf("old_str not found in file") } if count > 1 { log.Printf("EditFile failed: old_str found %d times in file %s, must be unique", count, editFileInput.Path) return "", fmt.Errorf("old_str found %d times in file, must be unique", count) } newContent = strings.Replace(oldContent, editFileInput.OldStr, editFileInput.NewStr, 1) } err = os.WriteFile(editFileInput.Path, []byte(newContent), 0644) if err != nil { log.Printf("Failed to write file %s: %v", editFileInput.Path, err) return "", err } log.Printf("Successfully edited file %s", editFileInput.Path) return "OK", nil } func createNewFile(filePath, content string) (string, error) { log.Printf("Creating new file: %s (%d bytes)", filePath, len(content)) dir := path.Dir(filePath) if dir != "." { log.Printf("Creating directory: %s", dir) err := os.MkdirAll(dir, 0755) if err != nil { log.Printf("Failed to create directory %s: %v", dir, err) return "", fmt.Errorf("failed to create directory: %w", err) } } err := os.WriteFile(filePath, []byte(content), 0644) if err != nil { log.Printf("Failed to create file %s: %v", filePath, err) return "", fmt.Errorf("failed to create file: %w", err) } log.Printf("Successfully created file %s", filePath) return fmt.Sprintf("Successfully created file %s", filePath), nil } func GenerateSchema[T any]() anthropic.ToolInputSchemaParam { reflector := jsonschema.Reflector{ AllowAdditionalProperties: false, DoNotReference: true, } var v T schema := reflector.Reflect(v) return anthropic.ToolInputSchemaParam{ Properties: schema.Properties, } }