Initial Commit

This commit is contained in:
Regis David Souza Mesquita 2024-12-07 11:08:15 +00:00
parent 4c0797a45d
commit dce138339d
No known key found for this signature in database
17 changed files with 577 additions and 374 deletions

23
.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# IDE
.vscode/
.idea/
# Build
bin/
search
# Test coverage
coverage/
# Environment variables
.env
# Go specific
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
go.work

127
Makefile Normal file
View file

@ -0,0 +1,127 @@
.PHONY: build clean test test-coverage test-verbose install lint run setup help local-install
# Build variables
BINARY_NAME=search
BUILD_DIR=bin
CMD_DIR=cmd/search
COVERAGE_DIR=coverage
GO_BIN=$(HOME)/go/bin
# Go commands
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOLINT=golangci-lint
# Build the project
build:
@echo "Building..."
@mkdir -p $(BUILD_DIR)
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(CMD_DIR)
# Clean build files
clean:
@echo "Cleaning..."
@rm -rf $(BUILD_DIR) $(COVERAGE_DIR)
$(GOCLEAN)
# Run all tests
test:
@echo "Running tests..."
$(GOTEST) ./...
# Run tests with coverage
test-coverage:
@echo "Running tests with coverage..."
@mkdir -p $(COVERAGE_DIR)
$(GOTEST) -coverprofile=$(COVERAGE_DIR)/coverage.out ./...
$(GOCMD) tool cover -html=$(COVERAGE_DIR)/coverage.out -o $(COVERAGE_DIR)/coverage.html
@echo "Coverage report generated in $(COVERAGE_DIR)/coverage.html"
# Run tests with verbose output
test-verbose:
@echo "Running tests (verbose)..."
$(GOTEST) -v ./...
# Install dependencies
deps:
@echo "Downloading dependencies..."
$(GOMOD) download
$(GOMOD) tidy
# Run linter
lint:
@echo "Running linter..."
$(GOLINT) run
# Install the binary
install:
@echo "Installing to $(GO_BIN)..."
@mkdir -p $(GO_BIN)
$(GOBUILD) -o $(GO_BIN)/$(BINARY_NAME) ./$(CMD_DIR)
@if ! echo "$$PATH" | grep -q "$(GO_BIN)"; then \
echo "Warning: $(GO_BIN) is not in your PATH"; \
echo "Add this line to your ~/.bashrc or ~/.zshrc:"; \
echo " export PATH=\$$PATH:$(GO_BIN)"; \
fi
# Install to /usr/local/bin (requires sudo)
local-install: build
@echo "Installing to /usr/local/bin..."
@sudo install -m 755 $(BUILD_DIR)/$(BINARY_NAME) /usr/local/bin/$(BINARY_NAME)
# Run the application (requires query argument)
run:
@if [ -z "$(query)" ]; then \
echo "Usage: make run query='your search query'"; \
exit 1; \
fi
@$(BUILD_DIR)/$(BINARY_NAME) $(query)
# Setup project structure
setup:
@echo "Setting up project structure..."
@mkdir -p cmd/search internal/adapters internal/formatter
@if [ -f main.go ]; then \
echo "Moving main.go to cmd/search/..."; \
rm cmd/search/main.go || true; \
mv main.go cmd/search/; \
fi
@if [ -d adapters ]; then \
echo "Moving adapter files to internal/adapters/..."; \
mv adapters/* internal/adapters/ 2>/dev/null || true; \
rmdir adapters; \
fi
@if [ -f main_test.go ]; then \
echo "Moving main_test.go to internal/formatter/formatter_test.go..."; \
mv main_test.go internal/formatter/formatter_test.go; \
fi
@if [ -f search.py ]; then \
echo "Removing Python version..."; \
rm search.py; \
fi
@echo "Updating go.mod..."
@$(GOMOD) init github.com/regismesquita/search-cli || true
@$(GOMOD) tidy
@echo "Project structure setup complete"
# Help
help:
@echo "Available commands:"
@echo " make build - Build the project"
@echo " make clean - Clean build files"
@echo " make test - Run tests"
@echo " make test-coverage - Run tests with coverage report"
@echo " make test-verbose - Run tests with verbose output"
@echo " make deps - Download dependencies"
@echo " make lint - Run linter"
@echo " make install - Install binary to ~/go/bin"
@echo " make local-install - Install binary to /usr/local/bin"
@echo " make run - Run the application (requires query='your search query')"
@echo " make setup - Setup project structure"
# Default target
.DEFAULT_GOAL := help

131
README.md Normal file
View file

@ -0,0 +1,131 @@
# Search CLI
A command-line interface for searching using Serper and Tavily APIs.
## Features
- Search using Google (via Serper API)
- Search and extract content using Tavily API
- JSON output support
- Configurable search depth (for Tavily)
## Installation
### Using Go Install
```bash
go install github.com/regismesquita/search-cli/cmd/search@latest
```
### Using mise (formerly rtx)
```bash
mise use -g go@latest
mise install github.com/regismesquita/search-cli/cmd/search@latest
```
### From Source
```bash
# Clone the repository
git clone https://github.com/regismesquita/search-cli
cd search-cli
# Build and install
make build
make install # Installs to ~/go/bin
# or
make local-install # Installs to /usr/local/bin (requires sudo)
```
## Configuration
Set your API keys as environment variables:
```bash
# For Serper
export SERPER_API_KEY=your_key_here
# For Tavily
export TAVILY_API_KEY=your_key_here
```
## Usage
### Basic Search (using Serper)
```bash
# Default search using Serper
search "your query"
# or explicitly
search -s "your query"
```
### Using Tavily
```bash
# Basic search
search -t "your query"
# Advanced search
search -t -depth advanced "your query"
# Extract content from URLs
search -t -e "https://example.com"
```
### JSON Output
```bash
search -json "your query"
search -t -json "your query"
search -t -e -json "https://example.com"
```
## Options
- `-s`: Use Serper (default)
- `-t`: Use Tavily
- `-e`: Extract content (Tavily only)
- `-json`: Output in JSON format
- `-depth`: Search depth for Tavily (basic or advanced)
## Development
```bash
# Run tests
make test
# Run tests with coverage
make test-coverage
# Run linter
make lint
# Clean build artifacts
make clean
```
## Project Structure
```
.
├── cmd/
│ └── search/ # Main application
│ └── main.go
├── internal/
│ ├── adapters/ # API providers
│ │ ├── serper.go
│ │ ├── tavily.go
│ │ └── types.go
│ └── formatter/ # Output formatting
│ └── formatter.go
├── go.mod
└── Makefile
```
## License
MIT
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -am 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

View file

@ -1,67 +0,0 @@
package adapters
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func TestSerperProvider_Search(t *testing.T) {
// Mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "POST" {
t.Errorf("Expected POST request, got %s", r.Method)
}
if r.Header.Get("X-API-KEY") != "test-key" {
t.Errorf("Expected API key header, got %s", r.Header.Get("X-API-KEY"))
}
// Return mock response
response := map[string]interface{}{
"organic": []map[string]interface{}{
{
"link": "https://example.com",
"snippet": "Example content",
},
},
}
json.NewEncoder(w).Encode(response)
}))
defer server.Close()
// Set test environment
os.Setenv("SERPER_API_KEY", "test-key")
defer os.Unsetenv("SERPER_API_KEY")
provider, err := NewSerperProvider()
if err != nil {
t.Fatalf("Failed to create provider: %v", err)
}
// Override API URL for testing
originalURL := "https://google.serper.dev/search"
http.DefaultClient = server.Client()
results, err := provider.Search("test query", nil)
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if len(results.Results) != 1 {
t.Errorf("Expected 1 result, got %d", len(results.Results))
}
if results.Results[0].URL != "https://example.com" {
t.Errorf("Expected URL https://example.com, got %s", results.Results[0].URL)
}
if results.Results[0].Content != "Example content" {
t.Errorf("Expected content 'Example content', got %s", results.Results[0].Content)
}
// Restore original URL
_ = originalURL
}

View file

@ -1,116 +0,0 @@
package adapters
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func TestTavilyProvider_Search(t *testing.T) {
// Mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "POST" {
t.Errorf("Expected POST request, got %s", r.Method)
}
// Verify request body contains API key
var reqBody map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
t.Fatalf("Failed to decode request body: %v", err)
}
if reqBody["api_key"] != "test-key" {
t.Errorf("Expected API key in body, got %v", reqBody["api_key"])
}
// Return mock response
response := map[string]interface{}{
"results": []map[string]interface{}{
{
"url": "https://example.com",
"content": "Example content",
},
},
}
json.NewEncoder(w).Encode(response)
}))
defer server.Close()
// Set test environment
os.Setenv("TAVILY_API_KEY", "test-key")
defer os.Unsetenv("TAVILY_API_KEY")
provider, err := NewTavilyProvider()
if err != nil {
t.Fatalf("Failed to create provider: %v", err)
}
// Override API URL for testing
http.DefaultClient = server.Client()
results, err := provider.Search("test query", map[string]string{"depth": "advanced"})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if len(results.Results) != 1 {
t.Errorf("Expected 1 result, got %d", len(results.Results))
}
if results.Results[0].URL != "https://example.com" {
t.Errorf("Expected URL https://example.com, got %s", results.Results[0].URL)
}
if results.Results[0].Content != "Example content" {
t.Errorf("Expected content 'Example content', got %s", results.Results[0].Content)
}
}
func TestTavilyProvider_Extract(t *testing.T) {
// Mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "POST" {
t.Errorf("Expected POST request, got %s", r.Method)
}
// Return mock response
response := ExtractResponse{
Results: []ExtractResult{
{
URL: "https://example.com",
RawContent: "Example content",
},
},
}
json.NewEncoder(w).Encode(response)
}))
defer server.Close()
// Set test environment
os.Setenv("TAVILY_API_KEY", "test-key")
defer os.Unsetenv("TAVILY_API_KEY")
provider, err := NewTavilyProvider()
if err != nil {
t.Fatalf("Failed to create provider: %v", err)
}
// Override API URL for testing
http.DefaultClient = server.Client()
results, err := provider.Extract([]string{"https://example.com"})
if err != nil {
t.Fatalf("Extract failed: %v", err)
}
if len(results.Results) != 1 {
t.Errorf("Expected 1 result, got %d", len(results.Results))
}
if results.Results[0].URL != "https://example.com" {
t.Errorf("Expected URL https://example.com, got %s", results.Results[0].URL)
}
}

View file

@ -7,15 +7,15 @@ import (
"os" "os"
"strings" "strings"
"search-cli/internal/adapters" "github.com/regismesquita/search-cli/internal/adapters"
"search-cli/internal/formatter" "github.com/regismesquita/search-cli/internal/formatter"
) )
func main() { func main() {
jsonOutput := flag.Bool("json", false, "Output in JSON format") jsonOutput := flag.Bool("json", false, "Output in JSON format")
tavilyMode := flag.Bool("tavily", false, "Use Tavily as provider") tavilyMode := flag.Bool("t", false, "Use Tavily as provider")
serperMode := flag.Bool("serper", false, "Use Serper as provider (default)") serperMode := flag.Bool("s", false, "Use Serper as provider (default)")
extractMode := flag.Bool("extract", false, "Extract content from URLs (Tavily only)") extractMode := flag.Bool("e", false, "Extract content from URLs (Tavily only)")
depth := flag.String("depth", "basic", "Search depth (basic or advanced) - only for Tavily") depth := flag.String("depth", "basic", "Search depth (basic or advanced) - only for Tavily")
flag.Parse() flag.Parse()
@ -26,13 +26,13 @@ func main() {
// Validate provider flags // Validate provider flags
if *tavilyMode && *serperMode { if *tavilyMode && *serperMode {
fmt.Fprintln(os.Stderr, "Error: Cannot use both -tavily and -serper") fmt.Fprintln(os.Stderr, "Error: Cannot use both -t and -s")
os.Exit(1) os.Exit(1)
} }
// Validate extract mode // Validate extract mode
if *extractMode && *serperMode { if *extractMode && *serperMode {
fmt.Fprintln(os.Stderr, "Error: Extract mode is only available with Tavily") fmt.Fprintln(os.Stderr, "Error: Extract mode (-e) is only available with Tavily (-t)")
os.Exit(1) os.Exit(1)
} }

2
go.mod
View file

@ -1,3 +1,3 @@
module search-cli module github.com/regismesquita/search-cli
go 1.21 go 1.21

View file

@ -11,6 +11,7 @@ import (
type SerperProvider struct { type SerperProvider struct {
apiKey string apiKey string
baseURL string
} }
type serperRequest struct { type serperRequest struct {
@ -29,7 +30,10 @@ func NewSerperProvider() (*SerperProvider, error) {
if apiKey == "" { if apiKey == "" {
return nil, fmt.Errorf("SERPER_API_KEY environment variable is not set") return nil, fmt.Errorf("SERPER_API_KEY environment variable is not set")
} }
return &SerperProvider{apiKey: apiKey}, nil return &SerperProvider{
apiKey: apiKey,
baseURL: "https://google.serper.dev/search",
}, nil
} }
func (s *SerperProvider) Search(query string, options map[string]string) (*SearchResponse, error) { func (s *SerperProvider) Search(query string, options map[string]string) (*SearchResponse, error) {
@ -39,7 +43,7 @@ func (s *SerperProvider) Search(query string, options map[string]string) (*Searc
return nil, err return nil, err
} }
req, err := http.NewRequest("POST", "https://google.serper.dev/search", bytes.NewBuffer(jsonData)) req, err := http.NewRequest("POST", s.baseURL, bytes.NewBuffer(jsonData))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -0,0 +1,89 @@
package adapters
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func setupMockServer(t *testing.T, expectedResponse interface{}) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "POST" {
t.Errorf("Expected POST request, got %s", r.Method)
}
if r.Header.Get("X-API-KEY") != "test-key" {
t.Errorf("Expected API key header, got %s", r.Header.Get("X-API-KEY"))
}
json.NewEncoder(w).Encode(expectedResponse)
}))
}
func TestSerperProvider_Search(t *testing.T) {
mockServer := NewTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "POST" {
t.Errorf("Expected POST request, got %s", r.Method)
}
if r.Header.Get("X-API-KEY") != "test-key" {
t.Errorf("Expected API key header, got %s", r.Header.Get("X-API-KEY"))
}
// Return mock response
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"organic": [
{
"link": "https://example.com",
"snippet": "Example content"
}
]
}`))
}))
defer mockServer.Close()
// Set test environment
oldKey := os.Getenv("SERPER_API_KEY")
os.Setenv("SERPER_API_KEY", "test-key")
defer os.Setenv("SERPER_API_KEY", oldKey)
provider, err := NewSerperProvider()
if err != nil {
t.Fatalf("Failed to create provider: %v", err)
}
provider.baseURL = mockServer.URL
results, err := provider.Search("test query", nil)
if err != nil {
t.Fatalf("Search failed: %v", err)
}
// Verify results
if len(results.Results) != 1 {
t.Errorf("Expected 1 result, got %d", len(results.Results))
}
expected := SearchResult{
URL: "https://example.com",
Content: "Example content",
}
if results.Results[0] != expected {
t.Errorf("Expected %+v, got %+v", expected, results.Results[0])
}
}
func TestSerperProvider_NoAPIKey(t *testing.T) {
// Clear API key
oldKey := os.Getenv("SERPER_API_KEY")
os.Unsetenv("SERPER_API_KEY")
defer os.Setenv("SERPER_API_KEY", oldKey)
_, err := NewSerperProvider()
if err == nil {
t.Error("Expected error when no API key is set")
}
}

View file

@ -11,6 +11,7 @@ import (
type TavilyProvider struct { type TavilyProvider struct {
apiKey string apiKey string
baseURL string
} }
func NewTavilyProvider() (*TavilyProvider, error) { func NewTavilyProvider() (*TavilyProvider, error) {
@ -18,7 +19,10 @@ func NewTavilyProvider() (*TavilyProvider, error) {
if apiKey == "" { if apiKey == "" {
return nil, fmt.Errorf("TAVILY_API_KEY environment variable is not set") return nil, fmt.Errorf("TAVILY_API_KEY environment variable is not set")
} }
return &TavilyProvider{apiKey: apiKey}, nil return &TavilyProvider{
apiKey: apiKey,
baseURL: "https://api.tavily.com", // default URL
}, nil
} }
func (t *TavilyProvider) Extract(urls []string) (*ExtractResponse, error) { func (t *TavilyProvider) Extract(urls []string) (*ExtractResponse, error) {
@ -35,7 +39,7 @@ func (t *TavilyProvider) Extract(urls []string) (*ExtractResponse, error) {
return nil, err return nil, err
} }
req, err := http.NewRequest("POST", "https://api.tavily.com/extract", bytes.NewBuffer(jsonData)) req, err := http.NewRequest("POST", t.baseURL+"/extract", bytes.NewBuffer(jsonData))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -87,7 +91,7 @@ func (t *TavilyProvider) Search(query string, options map[string]string) (*Searc
return nil, err return nil, err
} }
req, err := http.NewRequest("POST", "https://api.tavily.com/search", bytes.NewBuffer(jsonData)) req, err := http.NewRequest("POST", t.baseURL+"/search", bytes.NewBuffer(jsonData))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -0,0 +1,143 @@
package adapters
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func setupTavilyMockServer(t *testing.T, expectedResponse interface{}) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "POST" {
t.Errorf("Expected POST request, got %s", r.Method)
}
// Verify request body contains API key
var reqBody map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
t.Fatalf("Failed to decode request body: %v", err)
}
if reqBody["api_key"] != "test-key" {
t.Errorf("Expected API key in body, got %v", reqBody["api_key"])
}
json.NewEncoder(w).Encode(expectedResponse)
}))
}
func TestTavilyProvider_Search(t *testing.T) {
mockServer := NewTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "POST" {
t.Errorf("Expected POST request, got %s", r.Method)
}
// Return mock response
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"results": [
{
"url": "https://example.com",
"content": "Example content"
}
]
}`))
}))
defer mockServer.Close()
// Set test environment
oldKey := os.Getenv("TAVILY_API_KEY")
os.Setenv("TAVILY_API_KEY", "test-key")
defer os.Setenv("TAVILY_API_KEY", oldKey)
provider, err := NewTavilyProvider()
if err != nil {
t.Fatalf("Failed to create provider: %v", err)
}
provider.baseURL = mockServer.URL // Set mock server URL
results, err := provider.Search("test query", map[string]string{"depth": "advanced"})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
// Verify results
if len(results.Results) != 1 {
t.Errorf("Expected 1 result, got %d", len(results.Results))
}
expected := SearchResult{
URL: "https://example.com",
Content: "Example content",
}
if results.Results[0] != expected {
t.Errorf("Expected %+v, got %+v", expected, results.Results[0])
}
}
func TestTavilyProvider_Extract(t *testing.T) {
mockServer := NewTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "POST" {
t.Errorf("Expected POST request, got %s", r.Method)
}
// Return mock response
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"results": [
{
"url": "https://example.com",
"raw_content": "Example content"
}
]
}`))
}))
defer mockServer.Close()
// Set test environment
oldKey := os.Getenv("TAVILY_API_KEY")
os.Setenv("TAVILY_API_KEY", "test-key")
defer os.Setenv("TAVILY_API_KEY", oldKey)
provider, err := NewTavilyProvider()
if err != nil {
t.Fatalf("Failed to create provider: %v", err)
}
provider.baseURL = mockServer.URL // Set mock server URL
results, err := provider.Extract([]string{"https://example.com"})
if err != nil {
t.Fatalf("Extract failed: %v", err)
}
// Verify results
if len(results.Results) != 1 {
t.Errorf("Expected 1 result, got %d", len(results.Results))
}
expected := ExtractResult{
URL: "https://example.com",
RawContent: "Example content",
}
if results.Results[0] != expected {
t.Errorf("Expected %+v, got %+v", expected, results.Results[0])
}
}
func TestTavilyProvider_NoAPIKey(t *testing.T) {
// Clear API key
oldKey := os.Getenv("TAVILY_API_KEY")
os.Unsetenv("TAVILY_API_KEY")
defer os.Setenv("TAVILY_API_KEY", oldKey)
_, err := NewTavilyProvider()
if err == nil {
t.Error("Expected error when no API key is set")
}
}

View file

@ -0,0 +1,30 @@
package adapters
import (
"net/http"
"net/http/httptest"
)
// TestServer represents a mock server for testing
type TestServer struct {
*httptest.Server
OriginalClient *http.Client
}
// NewTestServer creates a new test server and patches the default HTTP client
func NewTestServer(handler http.HandlerFunc) *TestServer {
server := httptest.NewServer(handler)
originalClient := http.DefaultClient
http.DefaultClient = server.Client()
return &TestServer{
Server: server,
OriginalClient: originalClient,
}
}
// Close closes the server and restores the original HTTP client
func (s *TestServer) Close() {
http.DefaultClient = s.OriginalClient
s.Server.Close()
}

View file

@ -34,3 +34,9 @@ type ExtractResponse struct {
FailedResults []FailedResult `json:"failed_results"` FailedResults []FailedResult `json:"failed_results"`
ResponseTime float64 `json:"response_time"` ResponseTime float64 `json:"response_time"`
} }
// Add base URL configuration
type Config struct {
APIKey string
BaseURL string
}

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"search-cli/internal/adapters" "github.com/regismesquita/search-cli/internal/adapters"
) )
func FormatResults(results *adapters.SearchResponse) { func FormatResults(results *adapters.SearchResponse) {

View file

@ -1,4 +1,4 @@
package main package formatter
import ( import (
"bytes" "bytes"
@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"search-cli/adapters" "github.com/regismesquita/search-cli/internal/adapters"
) )
func TestFormatResults(t *testing.T) { func TestFormatResults(t *testing.T) {
@ -24,7 +24,7 @@ func TestFormatResults(t *testing.T) {
r, w, _ := os.Pipe() r, w, _ := os.Pipe()
os.Stdout = w os.Stdout = w
formatResults(results) FormatResults(results)
w.Close() w.Close()
os.Stdout = old os.Stdout = old
@ -62,7 +62,7 @@ func TestFormatExtractResults(t *testing.T) {
r, w, _ := os.Pipe() r, w, _ := os.Pipe()
os.Stdout = w os.Stdout = w
formatExtractResults(results) FormatExtractResults(results)
w.Close() w.Close()
os.Stdout = old os.Stdout = old

115
main.go
View file

@ -1,115 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"search-cli/adapters"
)
func formatResults(results *adapters.SearchResponse) {
for _, result := range results.Results {
fmt.Printf("\n🔗 %s\n", result.URL)
fmt.Printf("📝 %s\n", result.Content)
fmt.Println(strings.Repeat("-", 80))
}
}
func formatExtractResults(results *adapters.ExtractResponse) {
for _, result := range results.Results {
fmt.Printf("\n🔗 %s\n", result.URL)
fmt.Printf("📄 %s\n", result.RawContent)
fmt.Println(strings.Repeat("-", 80))
}
if len(results.FailedResults) > 0 {
fmt.Println("\n❌ Failed URLs:")
for _, failed := range results.FailedResults {
fmt.Printf("URL: %s\nError: %s\n", failed.URL, failed.Error)
fmt.Println(strings.Repeat("-", 40))
}
}
}
func main() {
jsonOutput := flag.Bool("json", false, "Output in JSON format")
tavilyMode := flag.Bool("tavily", false, "Use Tavily as provider")
serperMode := flag.Bool("serper", false, "Use Serper as provider (default)")
extractMode := flag.Bool("extract", false, "Extract content from URLs (Tavily only)")
depth := flag.String("depth", "basic", "Search depth (basic or advanced) - only for Tavily")
flag.Parse()
if flag.NArg() < 1 {
fmt.Fprintln(os.Stderr, "Error: Query or URLs required")
os.Exit(1)
}
// Validate provider flags
if *tavilyMode && *serperMode {
fmt.Fprintln(os.Stderr, "Error: Cannot use both -tavily and -serper")
os.Exit(1)
}
// Validate extract mode
if *extractMode && *serperMode {
fmt.Fprintln(os.Stderr, "Error: Extract mode is only available with Tavily")
os.Exit(1)
}
if *extractMode {
provider, err := adapters.NewTavilyProvider()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
urls := strings.Fields(strings.Join(flag.Args(), " "))
results, err := provider.Extract(urls)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if *jsonOutput {
json.NewEncoder(os.Stdout).Encode(results)
} else {
formatExtractResults(results)
}
} else {
var searchProvider adapters.SearchProvider
var err error
// Default to Serper unless Tavily is explicitly requested
if *tavilyMode {
searchProvider, err = adapters.NewTavilyProvider()
} else {
searchProvider, err = adapters.NewSerperProvider()
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
options := map[string]string{}
if *tavilyMode {
options["depth"] = *depth
}
query := flag.Arg(0)
results, err := searchProvider.Search(query, options)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if *jsonOutput {
json.NewEncoder(os.Stdout).Encode(results)
} else {
formatResults(results)
}
}
}

View file

@ -1,56 +0,0 @@
#!/usr/bin/env python3
import argparse
import requests
import json
import sys
import os
from typing import Dict, Any
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
if not TAVILY_API_KEY:
print("Error: TAVILY_API_KEY environment variable is not set", file=sys.stderr)
sys.exit(1)
API_URL = "https://api.tavily.com/search"
def search(query: str, search_depth: str = "basic") -> Dict[Any, Any]:
headers = {"api-key": TAVILY_API_KEY}
params = {
"query": query,
"search_depth": search_depth,
"include_domains": [],
"exclude_domains": []
}
try:
response = requests.post(API_URL, json=params, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def format_results(data: Dict[Any, Any]) -> None:
for result in data['results']:
print(f"\n🔗 {result['url']}")
print(f"📝 {result['content']}\n")
print("-" * 80)
def main():
parser = argparse.ArgumentParser(description="Tavily Search CLI")
parser.add_argument("query", help="Search query")
parser.add_argument("--json", action="store_true", help="Output in JSON format")
parser.add_argument("--depth", choices=["basic", "advanced"], default="basic",
help="Search depth (basic or advanced)")
args = parser.parse_args()
results = search(args.query, args.depth)
if args.json:
print(json.dumps(results, indent=2))
else:
format_results(results)
if __name__ == "__main__":
main()