package main import ( "bufio" "context" "encoding/json" "flag" "fmt" "log" "os" "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} 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, } 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]() func ReadFile(input json.RawMessage) (string, error) { readFileInput := ReadFileInput{} err := json.Unmarshal(input, &readFileInput) if err != nil { panic(err) } content, err := os.ReadFile(readFileInput.Path) if err != nil { return "", err } 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 } log.Printf("Successfully listed %d items in %s", len(files), dir) result, err := json.Marshal(files) if err != nil { return "", err } return string(result), 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, } }