Skill: xieshaocong33ethan/exa-full (version 1.2.1)

--- name: exa-full version: 1.2.1 description: Exa AI search + Research API. Supports web/code search, content extraction, and async multi-step research tasks with outputSchema. homepage: https://exa.ai metadata: {"openclaw":{"emoji":"🔍","requires":{"bins":["curl","jq"],"env":["EXA_API_KEY"]}}} --- # Exa - Search + Research Use this skill for web search, code-context search, URL content extraction, and async research workflows. ## What This Skill Does - Run Exa web search with optional category and domain filters. - Retrieve full page content (and optional subpage crawling). - Find code and docs context for programming queries. - Run async research tasks (one-shot or create/poll workflows). - Support optional structured outputs via `outputSchema`. ## Setup Set `EXA_API_KEY` using one of these methods. ```bash export EXA_API_KEY="your-exa-api-key" ``` ```bash # .env next to SKILL.md EXA_API_KEY=your-exa-api-key ``` Behavior: - If `EXA_API_KEY` is missing in the environment, scripts load only `EXA_API_KEY` from `.env`. - Other `.env` variables are ignored by the loader. ## Safety and Data Handling - `SCHEMA_FILE` content is sent to `https://api.exa.ai/research/v1` as `outputSchema`. - Never use sensitive local files for `SCHEMA_FILE` (for example: `.env`, key/cert files, secrets, internal confidential docs). - `research_create.sh` blocks obvious sensitive paths/suffixes (for example: `.env`, `.pem`, `.key`, `.p12`, `.pfx`, `id_rsa`). ## Command Quick Reference ### Search ```bash bash scripts/search.sh "query" ``` Main env vars: - `NUM=10` (max 100) - `TYPE=auto` (`auto`, `neural`, `fast`, `deep`, `instant`) - `CATEGORY=` (`company`, `research paper`, `news`, `tweet`, `personal site`, `financial report`, `people`) - `DOMAINS=domain1.com,domain2.com` - `EXCLUDE=domain1.com,domain2.com` - `SINCE=YYYY-MM-DD` - `UNTIL=YYYY-MM-DD` - `LOCATION=NL` Constraints: - `EXCLUDE` is not supported when `CATEGORY=company` or `CATEGORY=people`. - `SINCE` and `UNTIL` are not supported when `CATEGORY=company` or `CATEGORY=people`. - When `CATEGORY=people`, `DOMAINS` accepts LinkedIn domains only (`linkedin.com`, `www.linkedin.com`, `*.linkedin.com`). ### Content Extraction ```bash bash scripts/content.sh "url1" "url2" ``` Main env vars: - `MAX_CHARACTERS=2000` - `HIGHLIGHT_SENTENCES=3` - `HIGHLIGHTS_PER_URL=2` - `SUBPAGES=10` - `SUBPAGE_TARGET="docs,reference,api"` - `LIVECRAWL=preferred` (`preferred`, `always`, `fallback`) - `LIVECRAWL_TIMEOUT=12000` ### Code Context Search ```bash bash scripts/code.sh "query" [num_results] ``` ### Research (One-shot) ```bash bash scripts/research.sh "instructions" ``` Main env vars: - `MODEL=exa-research` or `MODEL=exa-research-pro` - `SCHEMA_FILE=path/to/schema.json` - `POLL_INTERVAL=2` - `MAX_WAIT_SECONDS=240` - `EVENTS=true` ### Research (Create/Poll) ```bash bash scripts/research_create.sh "instructions" | jq bash scripts/research_poll.sh "researchId" | jq ``` ## Agent Decision Rules ### Choose `TYPE` for Search Use this decision order: 1. User explicitly asks for realtime or autocomplete -> `TYPE=instant`. 2. Task needs broad coverage or deeper synthesis -> `TYPE=deep`. 3. User asks for speed/quality balance -> `TYPE=fast`. 4. Otherwise -> `TYPE=auto` (default). Fallback/escalation: - If too slow or time-sensitive: `deep -> auto -> fast -> instant`. - If too shallow: `instant -> fast -> auto -> deep`. - Explicit user requirement always wins. Recommended pattern: ```bash TYPE=auto bash scripts/search.sh "query" ``` ## Common Pitfalls - Do not pass sensitive files to `SCHEMA_FILE`. - Do not combine `CATEGORY=people|company` with `EXCLUDE`, `SINCE`, or `UNTIL`. - Prefer `https://docs.exa.ai/` for subpage crawling seeds (more reliable than `https://exa.ai/docs/reference/`). ## More Examples See [EXAMPLES.md](EXAMPLES.md) for grouped command examples and edge-case workflows.

path: EXAMPLES.md
content: # Exa Skill Examples

Use this file for concrete command patterns. Keep `SKILL.md` for execution rules and safety constraints.

## Search Examples

When to use: web lookup, entity discovery, domain-filtered search, people/company/news/paper workflows.

```bash
# Basic search
bash scripts/search.sh "AI agents 2024"

# Realtime UX / autocomplete
TYPE=instant bash scripts/search.sh "best ramen near me open now"

# High-quality general lookup
TYPE=auto bash scripts/search.sh "latest AI model release notes"

# Speed/quality balance
TYPE=fast bash scripts/search.sh "top battery recycling companies europe"

# Comprehensive search
TYPE=deep bash scripts/search.sh "long-term impact of solid-state batteries on EV supply chain"

# LinkedIn people search
CATEGORY=people bash scripts/search.sh "software engineer Amsterdam"

# Company search
CATEGORY=company bash scripts/search.sh "fintech startup Netherlands"

# Research paper search
CATEGORY="research paper" bash scripts/search.sh "transformer architecture"

# News from specific domains
CATEGORY=news DOMAINS="reuters.com,bbc.com" bash scripts/search.sh "Netherlands"

# Date-filtered news
CATEGORY=news SINCE="2026-01-01" UNTIL="2026-02-01" bash scripts/search.sh "tech layoffs"
```

Notes:
- With `CATEGORY=people`, `DOMAINS` only supports LinkedIn domains.
- With `CATEGORY=people` or `CATEGORY=company`, avoid `EXCLUDE`, `SINCE`, and `UNTIL`.

## Content Extraction and Crawling Examples

When to use: pull full text from known URLs, summarize pages, crawl related docs pages.

```bash
# Extract one or more URLs
bash scripts/content.sh "https://exa.ai/docs" "https://docs.exa.ai/"

# Increase text budget for richer downstream reasoning
MAX_CHARACTERS=10000 bash scripts/content.sh "https://exa.ai/docs/llms.txt" | jq

# Crawl docs subpages from a seed URL
SUBPAGES=10 SUBPAGE_TARGET="docs,reference,api" LIVECRAWL=preferred LIVECRAWL_TIMEOUT=12000 \
  bash scripts/content.sh "https://docs.exa.ai/" | jq
```

Notes:
- Prefer `https://docs.exa.ai/` as a crawl seed for docs subpages.
- `LIVECRAWL` accepts `preferred`, `always`, or `fallback`.

## Code Context Search Examples

When to use: find API usage examples, implementation patterns, and doc/code snippets.

```bash
# Default result count (10)
bash scripts/code.sh "Next.js partial prerendering config"

# Explicit result count
bash scripts/code.sh "Rust axum middleware auth example" 20
```

## Research Examples (Async)

When to use: multi-source synthesis, citation-heavy research, or structured output requirements.

### One-shot flow (create + poll)

```bash
bash scripts/research.sh "Compare the current flagship GPUs from NVIDIA, AMD, and Intel."
```

### One-shot with structured output

```bash
MODEL=exa-research-pro \
SCHEMA_FILE="./schema.json" \
POLL_INTERVAL=2 \
MAX_WAIT_SECONDS=300 \
EVENTS=true \
bash scripts/research.sh "Estimate the global battery recycling market size in 2030 by region."
```

### Two-step flow (manual control)

```bash
# Create
bash scripts/research_create.sh "Create a timeline of major OpenAI product releases from 2015 to 2023." | jq

# Capture researchId
RID="$(bash scripts/research_create.sh "Create a timeline of major OpenAI product releases from 2015 to 2023." | jq -r '.researchId')"

# Poll
bash scripts/research_poll.sh "$RID" | jq
```

Safety reminder:
- `SCHEMA_FILE` contents are uploaded as `outputSchema`.
- Never pass sensitive local files (for example: `.env`, private keys, certificate bundles, credential files).


path: scripts/content.sh
content: #!/bin/bash
# Exa contents retrieval (URLs/IDs) + optional subpage crawling
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
source "$SCRIPT_DIR/env.sh"

if [ $# -eq 0 ]; then
    echo "Usage: bash content.sh \"url1\" \"url2\" ..." >&2
    exit 1
fi

if [ -z "${EXA_API_KEY:-}" ]; then
    echo "Error: EXA_API_KEY is not set." >&2
    echo "Set EXA_API_KEY (env var or .env file)." >&2
    exit 1
fi

# Defaults (optional env vars)
MAX_CHARACTERS="${MAX_CHARACTERS:-2000}"
HIGHLIGHT_SENTENCES="${HIGHLIGHT_SENTENCES:-3}"
HIGHLIGHTS_PER_URL="${HIGHLIGHTS_PER_URL:-2}"

# Optional crawling (see docs: https://docs.exa.ai/reference/crawling-subpages)
# SUBPAGES=10
# SUBPAGE_TARGET="docs,tutorial"
# LIVECRAWL="preferred"|"always"|"fallback"
# LIVECRAWL_TIMEOUT=12000

# Build ids array (Exa accepts URLs as ids for /contents)
IDS=$(printf '%s\n' "$@" | jq -R . | jq -s .)

# Build payload
PAYLOAD=$(jq -n \
    --argjson ids "$IDS" \
    --argjson maxChars "$MAX_CHARACTERS" \
    --argjson numSentences "$HIGHLIGHT_SENTENCES" \
    --argjson highlightsPerUrl "$HIGHLIGHTS_PER_URL" \
    '{
        ids: $ids,
        text: { maxCharacters: $maxChars },
        highlights: { numSentences: $numSentences, highlightsPerUrl: $highlightsPerUrl },
        summary: {}
    }')

# Add subpage crawling if set
if [ -n "${SUBPAGES:-}" ]; then
    PAYLOAD=$(echo "$PAYLOAD" | jq --argjson subpages "$SUBPAGES" '. + {subpages: $subpages}')
fi

if [ -n "${SUBPAGE_TARGET:-}" ]; then
    PAYLOAD=$(echo "$PAYLOAD" | jq --arg t "$SUBPAGE_TARGET" '. + {subpageTarget: ($t | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0)))}')
fi

# Add livecrawl settings if set
if [ -n "${LIVECRAWL:-}" ]; then
    PAYLOAD=$(echo "$PAYLOAD" | jq --arg mode "$LIVECRAWL" '. + {livecrawl: $mode}')
fi

if [ -n "${LIVECRAWL_TIMEOUT:-}" ]; then
    PAYLOAD=$(echo "$PAYLOAD" | jq --argjson ms "$LIVECRAWL_TIMEOUT" '. + {livecrawlTimeout: $ms}')
fi

# Call API
exa_post_json "https://api.exa.ai/contents" "$PAYLOAD"


path: scripts/search.sh
content: #!/bin/bash
# Exa neural web search with full options
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
source "$SCRIPT_DIR/env.sh"

QUERY="$1"

if [ -z "$QUERY" ]; then
    echo "Usage: bash search.sh \"query\"" >&2
    echo "" >&2
    echo "Options (env vars):" >&2
    echo "  NUM=10          Number of results" >&2
    echo "  TYPE=auto       Search type: auto, neural, fast, deep, instant" >&2
    echo "  CATEGORY=       Category: company, research paper, news, tweet, personal site, financial report, people" >&2
    echo "  DOMAINS=        Include domains (comma-separated)" >&2
    echo "  EXCLUDE=        Exclude domains (comma-separated; not supported with CATEGORY=company|people)" >&2
    echo "  SINCE=          Published after (YYYY-MM-DD; not supported with CATEGORY=company|people)" >&2
    echo "  UNTIL=          Published before (YYYY-MM-DD; not supported with CATEGORY=company|people)" >&2
    echo "  LOCATION=       User location (country code)" >&2
    exit 1
fi

if [ -z "${EXA_API_KEY:-}" ]; then
    echo "Error: EXA_API_KEY is not set." >&2
    echo "Set EXA_API_KEY (env var or .env file)." >&2
    exit 1
fi

# Defaults
NUM="${NUM:-10}"
TYPE="${TYPE:-auto}"
LOCATION="${LOCATION:-NL}"

# Build base payload
PAYLOAD=$(jq -n \
    --arg query "$QUERY" \
    --arg type "$TYPE" \
    --argjson numResults "$NUM" \
    --arg location "$LOCATION" \
    '{
        query: $query,
        type: $type,
        numResults: $numResults,
        userLocation: $location,
        contents: {
            text: { maxCharacters: 500 },
            highlights: { numSentences: 2, highlightsPerUrl: 1 },
            summary: {}
        }
    }')

# Add category if set
if [ -n "${CATEGORY:-}" ]; then
    PAYLOAD=$(echo "$PAYLOAD" | jq --arg cat "$CATEGORY" '. + {category: $cat}')
fi

# Add includeDomains if set
if [ -n "${DOMAINS:-}" ]; then
    # people category only accepts LinkedIn domains
    if [ "${CATEGORY:-}" = "people" ]; then
        IFS=',' read -r -a DOMAIN_ARRAY <<< "$DOMAINS"
        FILTERED_DOMAINS=()
        for d in "${DOMAIN_ARRAY[@]}"; do
            # Trim whitespace
            domain="$(echo "$d" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
            case "$domain" in
                linkedin.com|www.linkedin.com|*.linkedin.com)
                    FILTERED_DOMAINS+=("$domain")
                    ;;
                *)
                    ;;
            esac
        done
        if [ "${#FILTERED_DOMAINS[@]}" -eq 0 ]; then
            echo "Warning: CATEGORY=people only accepts LinkedIn domains in DOMAINS; skipping includeDomains" >&2
        else
            DOMAINS_CSV="$(IFS=,; echo "${FILTERED_DOMAINS[*]}")"
            PAYLOAD=$(echo "$PAYLOAD" | jq --arg domains "$DOMAINS_CSV" '. + {includeDomains: ($domains | split(","))}')
        fi
    else
        PAYLOAD=$(echo "$PAYLOAD" | jq --arg domains "$DOMAINS" '. + {includeDomains: ($domains | split(","))}')
    fi
fi

# Add excludeDomains if set (not supported with category=company or people; API returns INVALID_REQUEST_BODY)
if [ -n "${EXCLUDE:-}" ]; then
    case "${CATEGORY:-}" in
        company|people)
            echo "Warning: EXCLUDE is not supported with CATEGORY=$CATEGORY; skipping excludeDomains" >&2
            ;;
        *)
            PAYLOAD=$(echo "$PAYLOAD" | jq --arg domains "$EXCLUDE" '. + {excludeDomains: ($domains | split(","))}')
            ;;
    esac
fi

# Add date filters if set
if [ -n "${SINCE:-}" ]; then
    case "${CATEGORY:-}" in
        company|people)
            echo "Warning: SINCE is not supported with CATEGORY=$CATEGORY; skipping startPublishedDate" >&2
            ;;
        *)
            PAYLOAD=$(echo "$PAYLOAD" | jq --arg date "${SINCE}T00:00:00.000Z" '. + {startPublishedDate: $date}')
            ;;
    esac
fi

if [ -n "${UNTIL:-}" ]; then
    case "${CATEGORY:-}" in
        company|people)
            echo "Warning: UNTIL is not supported with CATEGORY=$CATEGORY; skipping endPublishedDate" >&2
            ;;
        *)
            PAYLOAD=$(echo "$PAYLOAD" | jq --arg date "${UNTIL}T23:59:59.999Z" '. + {endPublishedDate: $date}')
            ;;
    esac
fi

# Call API
exa_post_json "https://api.exa.ai/search" "$PAYLOAD"


path: scripts/research_poll.sh
content: #!/bin/bash
# Exa Research: poll a research task until completion
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
source "$SCRIPT_DIR/env.sh"

RESEARCH_ID="$1"

if [ -z "$RESEARCH_ID" ]; then
  echo "Usage: bash research_poll.sh \"researchId\"" >&2
  echo "" >&2
  echo "Options (env vars):" >&2
  echo "  POLL_INTERVAL=2       Seconds between polls" >&2
  echo "  MAX_WAIT_SECONDS=240  Max wait time before timing out" >&2
  echo "  EVENTS=true           Include detailed event log in each poll response" >&2
  exit 1
fi

if [ -z "${EXA_API_KEY:-}" ]; then
  echo "Error: EXA_API_KEY is not set." >&2
  echo "Set EXA_API_KEY (env var or .env file)." >&2
  exit 1
fi

POLL_INTERVAL="${POLL_INTERVAL:-2}"
MAX_WAIT_SECONDS="${MAX_WAIT_SECONDS:-240}"

start_ts="$(date +%s)"

while true; do
  now_ts="$(date +%s)"
  elapsed="$(( now_ts - start_ts ))"
  if [ "$elapsed" -ge "$MAX_WAIT_SECONDS" ]; then
    echo "Error: timed out after ${MAX_WAIT_SECONDS}s waiting for ${RESEARCH_ID}" >&2
    exit 2
  fi

  resp="$(EVENTS="${EVENTS:-false}" bash "$(dirname "$0")/research_get.sh" "$RESEARCH_ID")"
  status="$(echo "$resp" | jq -r '.status // empty')"
  status_lc="$(echo "$status" | tr '[:upper:]' '[:lower:]')"

  if [ -z "$status_lc" ]; then
    echo "$resp"
    echo "Error: could not parse status for ${RESEARCH_ID}" >&2
    exit 2
  fi

  if [ "$status_lc" = "completed" ]; then
    echo "$resp"
    exit 0
  fi

  if [ "$status_lc" = "failed" ] || [ "$status_lc" = "canceled" ] || [ "$status_lc" = "cancelled" ]; then
    echo "$resp"
    exit 2
  fi

  sleep "$POLL_INTERVAL"
done



path: scripts/research_get.sh
content: #!/bin/bash
# Exa Research: get a research task by researchId
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
source "$SCRIPT_DIR/env.sh"

RESEARCH_ID="$1"

if [ -z "$RESEARCH_ID" ]; then
  echo "Usage: bash research_get.sh \"researchId\"" >&2
  echo "" >&2
  echo "Options (env vars):" >&2
  echo "  EVENTS=true     Include detailed event log" >&2
  exit 1
fi

if [ -z "${EXA_API_KEY:-}" ]; then
  echo "Error: EXA_API_KEY is not set." >&2
  echo "Set EXA_API_KEY (env var or .env file)." >&2
  exit 1
fi

URL="https://api.exa.ai/research/v1/${RESEARCH_ID}"

if [ "${EVENTS:-false}" = "true" ]; then
  URL="${URL}?events=true"
fi

curl -s -X GET "$URL" \
  -H "x-[REDACTED_SECRET]" \
  -H 'Content-Type: application/json'



path: scripts/research_create.sh
content: #!/bin/bash
# Exa Research: create an async research task (returns researchId)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
source "$SCRIPT_DIR/env.sh"

INSTRUCTIONS="$1"

if [ -z "$INSTRUCTIONS" ]; then
  echo "Usage: bash research_create.sh \"instructions\"" >&2
  echo "" >&2
  echo "Options (env vars):" >&2
  echo "  MODEL=exa-research        Research model: exa-research, exa-research-pro" >&2
  echo "  SCHEMA_FILE=path.json     JSON Schema file for outputSchema (optional)" >&2
  echo "                            Warning: SCHEMA_FILE content is sent to api.exa.ai; do not use sensitive files" >&2
  exit 1
fi

if [ -z "${EXA_API_KEY:-}" ]; then
  echo "Error: EXA_API_KEY is not set." >&2
  echo "Set EXA_API_KEY (env var or .env file)." >&2
  exit 1
fi

MODEL="${MODEL:-exa-research}"

if [ -n "${SCHEMA_FILE:-}" ]; then
  # Security guard: refuse obviously sensitive local files from being uploaded as outputSchema.
  _schema_lc="$(printf '%s' "$SCHEMA_FILE" | tr '[:upper:]' '[:lower:]')"
  case "$_schema_lc" in
    .env|.env.*|*.env|*.env.*|*.key|*.pem|*.p12|*.pfx|*.jks|*.keystore|*.der|*.crt|*.cer|*id_rsa*|*id_ecdsa*|*id_ed25519*)
      echo "Error: Refusing SCHEMA_FILE path that looks sensitive: $SCHEMA_FILE" >&2
      echo "Use a dedicated JSON schema file (for example: schema.json)." >&2
      exit 1
      ;;
  esac

  if [ ! -f "$SCHEMA_FILE" ]; then
    echo "Error: SCHEMA_FILE does not exist: $SCHEMA_FILE" >&2
    exit 1
  fi

  # Guard: reject files larger than 50MB
  _size="$(wc -c < "$SCHEMA_FILE")"
  if [ "$_size" -gt 52428800 ]; then
    echo "Error: SCHEMA_FILE exceeds 50MB limit: $SCHEMA_FILE" >&2
    exit 1
  fi

  OUTPUT_SCHEMA_JSON="$(jq -c '.' "$SCHEMA_FILE")"

  PAYLOAD="$(jq -n \
    --arg instructions "$INSTRUCTIONS" \
    --arg model "$MODEL" \
    --argjson outputSchema "$OUTPUT_SCHEMA_JSON" \
    '{ instructions: $instructions, model: $model, outputSchema: $outputSchema }')"
else
  PAYLOAD="$(jq -n \
    --arg instructions "$INSTRUCTIONS" \
    --arg model "$MODEL" \
    '{ instructions: $instructions, model: $model }')"
fi

curl -s -X POST 'https://api.exa.ai/research/v1' \
  -H "x-[REDACTED_SECRET]" \
  -H 'Content-Type: application/json' \
  -d "$PAYLOAD"



path: scripts/research.sh
content: #!/bin/bash
# Exa Research: one-shot (create + poll until finished)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
source "$SCRIPT_DIR/env.sh"

INSTRUCTIONS="$1"

if [ -z "$INSTRUCTIONS" ]; then
  echo "Usage: bash research.sh \"instructions\"" >&2
  echo "" >&2
  echo "Options (env vars):" >&2
  echo "  MODEL=exa-research        Research model: exa-research, exa-research-pro" >&2
  echo "  SCHEMA_FILE=path.json     JSON Schema file for outputSchema (optional)" >&2
  echo "  POLL_INTERVAL=2           Seconds between polls" >&2
  echo "  MAX_WAIT_SECONDS=240      Max wait time before timing out" >&2
  echo "  EVENTS=true               Include detailed event log in final result" >&2
  exit 1
fi

create_resp="$(MODEL="${MODEL:-exa-research}" SCHEMA_FILE="${SCHEMA_FILE:-}" bash "$(dirname "$0")/research_create.sh" "$INSTRUCTIONS")"
research_id="$(echo "$create_resp" | jq -r '.researchId // empty')"

if [ -z "$research_id" ]; then
  echo "$create_resp"
  echo "Error: create did not return researchId" >&2
  exit 2
fi

EVENTS="${EVENTS:-false}" POLL_INTERVAL="${POLL_INTERVAL:-2}" MAX_WAIT_SECONDS="${MAX_WAIT_SECONDS:-240}" \
  bash "$(dirname "$0")/research_poll.sh" "$research_id"



path: scripts/env.sh
content: #!/bin/bash
# Load EXA_API_KEY from .env if not already set
if [ -z "${EXA_API_KEY:-}" ]; then
  _env_file="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/../.env"
  if [ -f "$_env_file" ]; then
    # Safe parse: only extract EXA_API_KEY lines, never execute .env content
    _val="$(grep -E '^(export[[:space:]]+)?EXA_API_KEY=' "$_env_file" | tail -n1 | sed 's/^export[[:space:]]*//' | cut -d'=' -f2-)"
    # Strip optional surrounding quotes
    _val="${_val#\"}" ; _val="${_val%\"}"
    _val="${_val#\'}" ; _val="${_val%\'}"
    if [ -n "$_val" ]; then
      export EXA_API_KEY="$_val"
    fi
  fi
  unset _env_file _val
fi

# Post JSON payload to Exa API with resilient response handling.
# - Separates HTTP status from response body.
# - Validates JSON before returning.
# - Retries parsing after removing non-printable control characters.
exa_post_json() {
  local endpoint="$1"
  local payload="$2"
  local raw http body clean
  local retry_count="${EXA_RETRY_COUNT:-2}"
  local retry_delay="${EXA_RETRY_DELAY:-1}"

  if [ -z "${EXA_API_KEY:-}" ]; then
    echo "Error: EXA_API_KEY is not set." >&2
    return 1
  fi

  raw="$(
    LC_ALL=C curl -sS --fail-with-body \
      --retry "$retry_count" \
      --retry-delay "$retry_delay" \
      -X POST "$endpoint" \
      -H "x-[REDACTED_SECRET]" \
      -H "Content-Type: application/json" \
      -d "$payload" \
      -w $'\n%{http_code}'
  )"

  http="${raw##*$'\n'}"
  body="${raw%$'\n'*}"

  if ! [[ "$http" =~ ^[0-9]{3}$ ]]; then
    echo "Error: Unable to parse HTTP status from Exa response." >&2
    printf "%s\n" "$raw" >&2
    return 1
  fi

  if [[ "$http" != 2* ]]; then
    echo "Error: Exa API returned HTTP $http." >&2
    printf "%s\n" "$body" >&2
    return 1
  fi

  if printf "%s" "$body" | jq -e . >/dev/null 2>&1; then
    printf "%s\n" "$body"
    return 0
  fi

  clean="$(printf "%s" "$body" | tr -d '\000-\010\013\014\016-\037')"
  if printf "%s" "$clean" | jq -e . >/dev/null 2>&1; then
    printf "%s\n" "$clean"
    return 0
  fi

  echo "Error: Exa API response is not valid JSON (even after sanitize)." >&2
  return 1
}


path: scripts/code.sh
content: #!/bin/bash
# Exa code context search
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
source "$SCRIPT_DIR/env.sh"

QUERY="$1"
NUM_RESULTS="${2:-10}"

if [ -z "$QUERY" ]; then
    echo "Usage: $0 \"query\" [num_results]" >&2
    exit 1
fi

if [ -z "${EXA_API_KEY:-}" ]; then
    echo "Error: EXA_API_KEY is not set." >&2
    echo "Set EXA_API_KEY (env var or .env file)." >&2
    exit 1
fi

# Optimized settings for code/docs
# No domain restrictions - let Exa's neural search find the best docs/repos
PAYLOAD=$(jq -n \
    --arg query "$QUERY" \
    --argjson numResults "$NUM_RESULTS" \
    '{
        query: $query,
        type: "auto",
        numResults: $numResults,
        contents: {
            text: true,
            highlights: true
        }
    }')

exa_post_json "https://api.exa.ai/search" "$PAYLOAD"


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