/ go / internal / tools / sourcegraph.go
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), &params); 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  }