chore: snapshot main sync

This commit is contained in:
2026-06-17 20:08:31 +00:00
parent b18de2a331
commit 8bd32537cf
354 changed files with 10208 additions and 9230 deletions

View File

@@ -0,0 +1,469 @@
package main
import (
"bufio"
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/exec"
"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, CodeSearchDefinition}
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 CodeSearchDefinition = ToolDefinition{
Name: "code_search",
Description: `Search for code patterns using ripgrep (rg).
Use this to find code patterns, function definitions, variable usage, or any text in the codebase.
You can search by pattern, file type, or directory.`,
InputSchema: CodeSearchInputSchema,
Function: CodeSearch,
}
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 CodeSearchInput struct {
Pattern string `json:"pattern" jsonschema_description:"The search pattern or regex to look for"`
Path string `json:"path,omitempty" jsonschema_description:"Optional path to search in (file or directory)"`
FileType string `json:"file_type,omitempty" jsonschema_description:"Optional file extension to limit search to (e.g., 'go', 'js', 'py')"`
CaseSensitive bool `json:"case_sensitive,omitempty" jsonschema_description:"Whether the search should be case sensitive (default: false)"`
}
var CodeSearchInputSchema = GenerateSchema[CodeSearchInput]()
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)
cmd := exec.Command("find", dir, "-type", "f", "-not", "-path", "*/.devenv/*", "-not", "-path", "*/.git/*")
output, err := cmd.Output()
if err != nil {
log.Printf("Failed to list files in %s: %v", dir, err)
return "", err
}
files := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(files) == 1 && files[0] == "" {
files = []string{}
}
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 CodeSearch(input json.RawMessage) (string, error) {
codeSearchInput := CodeSearchInput{}
err := json.Unmarshal(input, &codeSearchInput)
if err != nil {
return "", err
}
if codeSearchInput.Pattern == "" {
log.Printf("CodeSearch failed: pattern is required")
return "", fmt.Errorf("pattern is required")
}
log.Printf("Searching for pattern: %s", codeSearchInput.Pattern)
// Build ripgrep command
args := []string{"rg", "--line-number", "--with-filename", "--color=never"}
// Add case sensitivity flag
if !codeSearchInput.CaseSensitive {
args = append(args, "--ignore-case")
}
// Add file type filter if specified
if codeSearchInput.FileType != "" {
args = append(args, "--type", codeSearchInput.FileType)
}
// Add pattern
args = append(args, codeSearchInput.Pattern)
// Add path if specified
if codeSearchInput.Path != "" {
args = append(args, codeSearchInput.Path)
} else {
args = append(args, ".")
}
if a := false; a { // This is a hack to access verbose mode
log.Printf("Executing ripgrep with args: %v", args)
}
cmd := exec.Command(args[0], args[1:]...)
output, err := cmd.Output()
// ripgrep returns exit code 1 when no matches are found, which is not an error
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 {
log.Printf("No matches found for pattern: %s", codeSearchInput.Pattern)
return "No matches found", nil
}
log.Printf("Ripgrep command failed: %v", err)
return "", fmt.Errorf("search failed: %w", err)
}
result := strings.TrimSpace(string(output))
lines := strings.Split(result, "\n")
log.Printf("Found %d matches for pattern: %s", len(lines), codeSearchInput.Pattern)
// Limit output to prevent overwhelming responses
if len(lines) > 50 {
result = strings.Join(lines[:50], "\n") + fmt.Sprintf("\n... (showing first 50 of %d matches)", len(lines))
}
return 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,
}
}