sourcegraph.go
1 package tools 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "strings" 11 "time" 12 ) 13 14 // SourcegraphTool searches code across public repositories 15 type SourcegraphTool struct{} 16 17 func (s SourcegraphTool) Name() string { 18 return "sourcegraph" 19 } 20 21 func (s SourcegraphTool) Description() string { 22 return "Search code across public repositories using Sourcegraph. Input: {\"query\":\"search term\",\"count\":10,\"timeout\":30}" 23 } 24 25 type SourcegraphParams struct { 26 Query string `json:"query"` 27 Count int `json:"count"` 28 ContextWindow int `json:"context_window"` 29 Timeout int `json:"timeout"` 30 } 31 32 func (s SourcegraphTool) Call(ctx context.Context, input string) (string, error) { 33 var params SourcegraphParams 34 35 input = strings.TrimSpace(input) 36 37 // Try JSON parsing 38 if strings.HasPrefix(input, "{") { 39 if err := json.Unmarshal([]byte(input), ¶ms); err != nil { 40 params.Query = input 41 } 42 } else { 43 params.Query = input 44 } 45 46 if params.Query == "" { 47 return "", fmt.Errorf("query is required") 48 } 49 50 if params.Count <= 0 { 51 params.Count = 10 52 } else if params.Count > 20 { 53 params.Count = 20 54 } 55 56 if params.ContextWindow <= 0 { 57 params.ContextWindow = 10 58 } 59 60 // Create HTTP client 61 client := &http.Client{ 62 Timeout: 30 * time.Second, 63 } 64 65 // Handle custom timeout 66 if params.Timeout > 0 { 67 if params.Timeout > 120 { 68 params.Timeout = 120 69 } 70 var cancel context.CancelFunc 71 ctx, cancel = context.WithTimeout(ctx, time.Duration(params.Timeout)*time.Second) 72 defer cancel() 73 } 74 75 // Build GraphQL request 76 type graphqlRequest struct { 77 Query string `json:"query"` 78 Variables struct { 79 Query string `json:"query"` 80 } `json:"variables"` 81 } 82 83 request := graphqlRequest{ 84 Query: "query Search($query: String!) { search(query: $query, version: V2, patternType: keyword ) { results { matchCount, limitHit, resultCount, results { __typename, ... on FileMatch { repository { name }, file { path, url }, lineMatches { preview, lineNumber, offsetAndLengths } } } } } }", 85 } 86 request.Variables.Query = params.Query 87 88 requestBody, err := json.Marshal(request) 89 if err != nil { 90 return "", fmt.Errorf("failed to marshal request: %w", err) 91 } 92 93 // Make request 94 req, err := http.NewRequestWithContext(ctx, "POST", "https://sourcegraph.com/.api/graphql", bytes.NewBuffer(requestBody)) 95 if err != nil { 96 return "", fmt.Errorf("failed to create request: %w", err) 97 } 98 99 req.Header.Set("Content-Type", "application/json") 100 req.Header.Set("User-Agent", "kamaji/1.0") 101 102 resp, err := client.Do(req) 103 if err != nil { 104 return "", fmt.Errorf("request failed: %w", err) 105 } 106 defer resp.Body.Close() 107 108 if resp.StatusCode != http.StatusOK { 109 return "", fmt.Errorf("request failed with status: %d", resp.StatusCode) 110 } 111 112 // Read response 113 body, err := io.ReadAll(resp.Body) 114 if err != nil { 115 return "", fmt.Errorf("failed to read response: %w", err) 116 } 117 118 // Parse response 119 var result struct { 120 Data struct { 121 Search struct { 122 Results struct { 123 MatchCount int `json:"matchCount"` 124 Results []struct { 125 TypeName string `json:"__typename"` 126 Repository struct { 127 Name string `json:"name"` 128 } `json:"repository"` 129 File struct { 130 Path string `json:"path"` 131 URL string `json:"url"` 132 } `json:"file"` 133 LineMatches []struct { 134 Preview string `json:"preview"` 135 LineNumber int `json:"lineNumber"` 136 } `json:"lineMatches"` 137 } `json:"results"` 138 } `json:"results"` 139 } `json:"search"` 140 } `json:"data"` 141 Errors []struct { 142 Message string `json:"message"` 143 } `json:"errors"` 144 } 145 146 if err := json.Unmarshal(body, &result); err != nil { 147 return "", fmt.Errorf("failed to parse response: %w", err) 148 } 149 150 if len(result.Errors) > 0 { 151 return "", fmt.Errorf("sourcegraph error: %s", result.Errors[0].Message) 152 } 153 154 // Format output 155 var output strings.Builder 156 output.WriteString(fmt.Sprintf("Found %d matches\n\n", result.Data.Search.Results.MatchCount)) 157 158 count := 0 159 for _, match := range result.Data.Search.Results.Results { 160 if count >= params.Count { 161 break 162 } 163 164 if match.TypeName == "FileMatch" { 165 output.WriteString(fmt.Sprintf("📦 %s\n", match.Repository.Name)) 166 output.WriteString(fmt.Sprintf("📄 %s\n", match.File.Path)) 167 output.WriteString(fmt.Sprintf("🔗 %s\n", match.File.URL)) 168 169 for _, lineMatch := range match.LineMatches { 170 preview := strings.TrimSpace(lineMatch.Preview) 171 if len(preview) > 100 { 172 preview = preview[:100] + "..." 173 } 174 output.WriteString(fmt.Sprintf(" Line %d: %s\n", lineMatch.LineNumber, preview)) 175 } 176 output.WriteString("\n") 177 count++ 178 } 179 } 180 181 if count == 0 { 182 return "No matches found", nil 183 } 184 185 return output.String(), nil 186 }