Initial Commit
This commit is contained in:
parent
4c0797a45d
commit
dce138339d
17 changed files with 577 additions and 374 deletions
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal 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
127
Makefile
Normal 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
131
README.md
Normal 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
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
2
go.mod
|
|
@ -1,3 +1,3 @@
|
||||||
module search-cli
|
module github.com/regismesquita/search-cli
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ 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
|
||||||
}
|
}
|
||||||
89
internal/adapters/serper_test.go
Normal file
89
internal/adapters/serper_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,8 @@ 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
|
||||||
}
|
}
|
||||||
143
internal/adapters/tavily_test.go
Normal file
143
internal/adapters/tavily_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
30
internal/adapters/testing_helpers.go
Normal file
30
internal/adapters/testing_helpers.go
Normal 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()
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
115
main.go
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
56
search.py
56
search.py
|
|
@ -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()
|
|
||||||
Loading…
Add table
Reference in a new issue