Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pkg/channels/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,3 @@ func TestBaseChannelIsAllowed(t *testing.T) {
})
}
}

8 changes: 4 additions & 4 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,11 @@ func TestDefaultConfig_WebTools(t *testing.T) {
cfg := DefaultConfig()

// Verify web tools defaults
if cfg.Tools.Web.Search.MaxResults != 5 {
t.Error("Expected MaxResults 5, got ", cfg.Tools.Web.Search.MaxResults)
if cfg.Tools.Web.Brave.MaxResults != 5 {
t.Error("Expected Brave MaxResults 5, got ", cfg.Tools.Web.Brave.MaxResults)
}
if cfg.Tools.Web.Search.APIKey != "" {
t.Error("Search API key should be empty by default")
if cfg.Tools.Web.Brave.APIKey != "" {
t.Error("Brave API key should be empty by default")
}
}

Expand Down
30 changes: 29 additions & 1 deletion pkg/providers/claude_cli_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
)
Expand Down Expand Up @@ -41,13 +42,40 @@ func (p *ClaudeCliProvider) Chat(ctx context.Context, messages []Message, tools
if p.workspace != "" {
cmd.Dir = p.workspace
}

// Remove CLAUDECODE env var to allow nesting (Claude Code blocks
// subprocess launches when this variable is set).
env := os.Environ()
filtered := env[:0]
for _, e := range env {
if !strings.HasPrefix(e, "CLAUDECODE=") {
filtered = append(filtered, e)
}
}
cmd.Env = filtered

cmd.Stdin = bytes.NewReader([]byte(prompt))

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
err := cmd.Run()

// Parse JSON from stdout even if exit code is non-zero,
// because claude writes diagnostic noise to stderr but still
// produces valid JSON output.
if stdoutStr := stdout.String(); stdoutStr != "" {
resp, parseErr := p.parseClaudeCliResponse(stdoutStr)
if parseErr == nil && resp != nil && resp.Content != "" {
return resp, nil
}
}

if err != nil {
if ctx.Err() == context.Canceled {
return nil, ctx.Err()
}
if stderrStr := stderr.String(); stderrStr != "" {
return nil, fmt.Errorf("claude cli error: %s", stderrStr)
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/tools/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (p *DuckDuckGoSearchProvider) Search(ctx context.Context, query string, cou
func (p *DuckDuckGoSearchProvider) extractResults(html string, count int, query string) (string, error) {
// Simple regex based extraction for DDG HTML
// Strategy: Find all result containers or key anchors directly

// Try finding the result links directly first, as they are the most critical
// Pattern: <a class="result__a" href="...">Title</a>
// The previous regex was a bit strict. Let's make it more flexible for attributes order/content
Expand All @@ -133,14 +133,14 @@ func (p *DuckDuckGoSearchProvider) extractResults(html string, count int, query
// But simple global search for snippets might mismatch order.
// Since we only have the raw HTML string, let's just extract snippets globally and assume order matches (risky but simple for regex)
// Or better: Let's assume the snippet follows the link in the HTML

// A better regex approach: iterate through text and find matches in order
// But for now, let's grab all snippets too
reSnippet := regexp.MustCompile(`<a class="result__snippet[^"]*".*?>([\s\S]*?)</a>`)
snippetMatches := reSnippet.FindAllStringSubmatch(html, count+5)

maxItems := min(len(matches), count)

for i := 0; i < maxItems; i++ {
urlStr := matches[i][1]
title := stripTags(matches[i][2])
Expand All @@ -157,7 +157,7 @@ func (p *DuckDuckGoSearchProvider) extractResults(html string, count int, query
}

lines = append(lines, fmt.Sprintf("%d. %s\n %s", i+1, title, urlStr))

// Attempt to attach snippet if available and index aligns
if i < len(snippetMatches) {
snippet := stripTags(snippetMatches[i][1])
Expand Down
25 changes: 9 additions & 16 deletions pkg/tools/web_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,30 +173,23 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
}
}

// TestWebTool_WebSearch_NoApiKey verifies error handling when API key is missing
// TestWebTool_WebSearch_NoApiKey verifies that no tool is created when API key is missing
func TestWebTool_WebSearch_NoApiKey(t *testing.T) {
tool := NewWebSearchTool("", 5)
ctx := context.Background()
args := map[string]interface{}{
"query": "test",
}

result := tool.Execute(ctx, args)

// Should return error result
if !result.IsError {
t.Errorf("Expected error when API key is missing")
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: ""})
if tool != nil {
t.Errorf("Expected nil tool when Brave API key is empty")
}

// Should mention missing API key
if !strings.Contains(result.ForLLM, "BRAVE_API_KEY") && !strings.Contains(result.ForUser, "BRAVE_API_KEY") {
t.Errorf("Expected API key error message, got ForLLM: %s", result.ForLLM)
// Also nil when nothing is enabled
tool = NewWebSearchTool(WebSearchToolOptions{})
if tool != nil {
t.Errorf("Expected nil tool when no provider is enabled")
}
}

// TestWebTool_WebSearch_MissingQuery verifies error handling for missing query
func TestWebTool_WebSearch_MissingQuery(t *testing.T) {
tool := NewWebSearchTool("test-key", 5)
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: "test-key", BraveMaxResults: 5})
ctx := context.Background()
args := map[string]interface{}{}

Expand Down