Skill: jayegt002/tavily-search-skill (version 1.0.7)

--- name: tavily-search-skill description: Web search via Tavily API (alternative to Brave). Use when the user asks to search the web / look up sources / find links and Brave web_search is unavailable or undesired. metadata: openclaw: homepage: https://github.com/JayeGT002/Tavily-Search-Skill --- # Tavily Search Skill OpenClaw Skill for performing high quality web searches using the Tavily API. ## Directory Structure ``` tavily-search-skill/ ├── SKILL.md ← must stay at root ├── search.sh ← must stay at root (entry script) ├── apikey ← API Key file (user creates this) ├── _meta.json └── blocklist/ ├── blocklist.json ← blacklist config └── filter_blocklist.py ← filter script ``` ## Agent Workflow When instructed to install this skill, follow these steps in order: ### Step 1: Install the skill ```bash git clone https://github.com/JayeGT002/Tavily-Search-Skill.git tavily-search-skill cd tavily-search-skill ``` ### Step 2: Request API Key from user Ask the user: "请提供你的 Tavily API Key，用于配置搜索功能。获取地址：https://app.tavily.com/api-keys" **Wait for the user's API Key reply before proceeding.** ### Step 3: Create apikey file ```bash echo "USER_PROVIDED_API_KEY" > apikey chmod 600 apikey ``` ### Step 4: Verify installation ```bash ./search.sh "test" ``` A successful response contains search results and quota info. --- ## Blacklist Management The blacklist filters out low quality or untrusted sources from search results. ### View current blacklist ```bash cat blocklist/blocklist.json ``` ### Add a domain to blacklist When the user says "block [domain]", update `blocklist/blocklist.json`: Root domains automatically match all subdomains. Example: adding `csdn.net` also blocks `blog.csdn.net`, `download.csdn.net`, etc. ### Filter feedback When results are filtered, a message is written to stderr (not visible in normal output). Check stderr if you suspect filtering is silently removing results. --- ## Usage ### Basic Search ```bash ./search.sh "search query" ``` ### Specify Result Count ```bash ./search.sh "query" 10 ``` ### Include Images ```bash ./search.sh "query" 5 true ``` ## Dependencies - `curl` - `jq` Install if missing: - Ubuntu/Debian: `sudo apt-get install curl jq` - macOS: `brew install curl jq` - Alpine: `apk add curl jq`

path: _meta.json
content: {"ownerId": [REDACTED_SECRET], "slug": "tavily-search-skill", "version": "1.0.7", "publishedAt": 1773128020068}

path: blocklist/blocklist.json
content: {
  "blocked": [
    {"domain": "baidu.com", "reason": "百度搜索（机器生成/过时内容）"},
    {"domain": "csdn.net", "reason": "CSDN（爬虫聚合/SEO作弊）"},
    {"domain": "jianshu.com", "reason": "简书（洗稿/标题党/同质化）"},
    {"domain": "toutiao.com", "reason": "今日头条（AI垃圾内容重灾区）"},
    {"domain": "mp.sohu.com", "reason": "搜狐自媒体（营销号聚集地）"}
  ]
}


path: blocklist/filter_blocklist.py
content: import sys, json
from urllib.parse import urlparse

def get_domain(url):
    try:
        host = urlparse(url).netloc.lower()
        if ':' in host:
            host = host.split(':')[0]
        return host
    except:
        return ''

if len(sys.argv) < 3:
    print("Usage: filter_blocklist.py <blocklist.json> <input.json>", file=sys.stderr)
    sys.exit(1)

blocklist_file = sys.argv[1]
input_file = sys.argv[2]

try:
    with open(blocklist_file, 'r') as f:
        blocklist = json.load(f)
    with open(input_file, 'r') as f:
        data = json.load(f)
    
    # Build blocked set from root domains only
    blocked = set(item['domain'] for item in blocklist.get('blocked', []))
    
    orig = len(data.get('results', []))
    kept = []
    for r in data.get('results', []):
        d = get_domain(r.get('url', ''))
        if not any(d == b or d.endswith('.' + b) for b in blocked):
            kept.append(r)
    data['results'] = kept
    
    dropped = orig - len(data['results'])
    if dropped:
        print(f'[BLOCKLIST] Filtered {dropped} results', file=sys.stderr)
    print(json.dumps(data, ensure_ascii=False))

except Exception as e:
    print(f'[BLOCKLIST] Error: {e}', file=sys.stderr)
    try:
        with open(input_file, 'r') as f:
            print(f.read(), end='')
    except:
        pass


path: search.sh
content: #!/bin/bash
# Tavily Search Script with blocklist filtering
# Usage: ./search.sh "query" [max_results] [include_images]

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# ===== Key loading =====
if [ -z "$TAVILY_API_KEY" ]; then
    if [ -f "${SCRIPT_DIR}/apikey" ]; then
        TAVILY_API_KEY="$(cat "${SCRIPT_DIR}/apikey" 2>/dev/null)"
    fi
fi

if [ -z "$TAVILY_API_KEY" ]; then
    echo "ERROR: TAVILY_API_KEY is not configured" >&2
    echo "Please either:" >&2
    echo "  1. Set env var: export TAVILY_API_KEY='your-api-key'" >&2
    echo "  2. Create apikey file in the same directory as this script" >&2
    exit 1
fi

TAVILY_ENDPOINT="https://api.tavily.com/search"
TAVILY_USAGE_ENDPOINT="https://api.tavily.com/usage"

# ===== Parse args =====
query="${1:-}"
max_results="${2:-5}"
include_images="${3:-false}"

if [ -z "$query" ]; then
    echo "Usage: ./search.sh \"query\" [max_results] [include_images]" >&2
    exit 1
fi

# ===== Make API call =====
images_flag="false"
if [ "$include_images" = "true" ] || [ "$include_images" = "1" ]; then
    images_flag="true"
fi

# Build JSON payload via jq to avoid shell injection
# query is passed as --arg value (no shell expansion), max_results as tonumber
json_payload=$(jq -n \
    --arg q "$query" \
    --arg mr "$max_results" \
    --arg ii "$images_flag" \
    '{
        query: $q,
        search_depth: "basic",
        max_results: ($mr | tonumber),
        include_images: ($ii == "true")
    }')

response=$(curl -s -w "\n%{http_code}" -X POST "$TAVILY_ENDPOINT" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $TAVILY_API_KEY" \
    -d "$json_payload")

http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')

if [ "$http_code" != "200" ]; then
    echo "{\"error\": \"HTTP $http_code\", \"body\": $body}" >&2
    exit 1
fi

# ===== Get quota =====
quota_response=$(curl -s -X GET "$TAVILY_USAGE_ENDPOINT" \
    -H "Authorization: Bearer $TAVILY_API_KEY")

plan=$(echo "$quota_response" | jq -r '.account.current_plan // "unknown"' 2>/dev/null)
used=$(echo "$quota_response" | jq -r '.account.plan_usage // 0' 2>/dev/null)
total=$(echo "$quota_response" | jq -r '.account.plan_limit // 1000' 2>/dev/null)
remaining=$(echo "$quota_response" | jq -r '(.account.plan_limit // 1000) - (.account.plan_usage // 0)' 2>/dev/null)

quota_info="{\"plan\": \"$plan\", \"total\": $total, \"used\": $used, \"remaining\": $remaining}"

# ===== Build output with quota =====
output=$(echo "$body" | jq --argjson quota "$quota_info" '{query: .query, results: .results, quota_info: $quota, response_time: .response_time}')

# ===== Apply blocklist filter =====
blocklist_file="${SCRIPT_DIR}/blocklist/blocklist.json"
filter_script="${SCRIPT_DIR}/blocklist/filter_blocklist.py"

if [ -f "$blocklist_file" ] && [ -f "$filter_script" ]; then
    # Use random temp file to avoid conflicts
    tmpfile=$(mktemp /tmp/tavily_XXXXXX.json)
    echo "$output" > "$tmpfile"
    filtered=$(python3 "$filter_script" "$blocklist_file" "$tmpfile" 2>&1)
    rm -f "$tmpfile"
    echo "$filtered"
else
    echo "$output"
fi


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)