package webfetch import ( "context" "fmt" "io" "net/http" "strings" "time" "git.yeij.top/AskaEth/Cyrene-Plugins/sdk" ) type WebFetchPlugin struct { sdk.BasePlugin client *http.Client } func NewWebFetchPlugin() *WebFetchPlugin { return &WebFetchPlugin{client: &http.Client{Timeout: 15 * time.Second}} } func (p *WebFetchPlugin) Metadata() sdk.PluginMetadata { return sdk.PluginMetadata{ Name: "web_fetch", DisplayName: "Web Fetch", Version: "1.0.0", Description: "Fetch and extract text content from URLs", Category: "network", Author: sdk.PluginAuthor{Name: "Cyrene Team"}, } } func (p *WebFetchPlugin) Tools() []sdk.Tool { return []sdk.Tool{&WebFetchTool{client: p.client}} } type WebFetchTool struct { sdk.BaseTool client *http.Client } func (t *WebFetchTool) Definition() sdk.ToolDefinition { return sdk.ToolDefinition{ ID: "web_fetch", Name: "web_fetch", DisplayName: "Web Fetch", Description: "Fetch content of a specified URL. Returns plain text summary (first 2000 characters). HTTP/HTTPS only.", Category: "network", Complexity: sdk.ComplexitySimple, Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{"url": map[string]interface{}{"type": "string"}}, "required": []string{"url"}, }, } } func (t *WebFetchTool) Validate(args map[string]interface{}) error { if _, ok := args["url"]; !ok { return fmt.Errorf("missing required parameter: url") } return nil } func (t *WebFetchTool) Execute(_ context.Context, args map[string]interface{}) (*sdk.ToolResult, error) { urlStr, _ := args["url"].(string) if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") { return &sdk.ToolResult{ToolName: "web_fetch", Success: false, Error: "only http/https URLs allowed"}, nil } req, _ := http.NewRequest("GET", urlStr, nil) req.Header.Set("User-Agent", "CyreneBot/1.0") resp, err := t.client.Do(req) if err != nil { return &sdk.ToolResult{ToolName: "web_fetch", Success: false, Error: err.Error()}, nil } defer resp.Body.Close() bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 100*1024)) text := stripHTMLFull(string(bodyBytes)) text = removeBlankLines(text) runes := []rune(text) if len(runes) > 2000 { text = string(runes[:2000]) + "..." } return &sdk.ToolResult{ToolName: "web_fetch", Success: true, Output: fmt.Sprintf( "URL: %s\nStatus: %d\nContent-Type: %s\n\n%s", urlStr, resp.StatusCode, resp.Header.Get("Content-Type"), text)}, nil } func stripHTMLFull(s string) string { result := make([]rune, 0, len([]rune(s))) inTag := false for _, r := range s { if r == '<' { inTag = true continue } if r == '>' { inTag = false continue } if !inTag { result = append(result, r) } } return string(result) } func removeBlankLines(s string) string { lines := strings.Split(s, "\n") var result []string for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" { result = append(result, trimmed) } } return strings.Join(result, "\n") }