Skill: ericsantos/jina-reader (version 0.0.2)

--- name: jina-reader description: "Web content extraction via Jina AI Reader API. Three modes: read (URL to markdown), search (web search + full content), ground (fact-checking). Extracts clean content without exposing server IP." homepage: https://jina.ai/reader metadata: {"clawdbot":{"emoji":"📖","requires":{"bins":["curl","jq"]},"primaryEnv":"JINA_API_KEY"}} --- # Jina Reader Extract clean web content via Jina AI — without exposing your server IP. ## Read a URL ```bash {baseDir}/scripts/reader.sh "https://example.com/article" ``` ## Search the web (top 5 results with full content) ```bash {baseDir}/scripts/reader.sh --mode search "latest AI news 2025" ``` ## Fact-check a statement ```bash {baseDir}/scripts/reader.sh --mode ground "OpenAI was founded in 2015" ``` ## Options | Flag | Description | Default | |------|-------------|---------| | `--mode` | `read`, `search`, `ground` | `read` | | `--selector` | CSS selector to extract specific region | — | | `--wait` | CSS selector to wait for before extraction | — | | `--remove` | CSS selectors to remove (comma-separated) | — | | `--proxy` | Country code for geo-proxy (`br`, `us`, etc.) | — | | `--nocache` | Force fresh content (skip cache) | off | | `--format` | `markdown`, `html`, `text`, `screenshot` | `markdown` | | `--json` | Raw JSON output | off | ## Examples ```bash # Extract article content {baseDir}/scripts/reader.sh "https://blog.example.com/post" # Extract specific section via CSS selector {baseDir}/scripts/reader.sh --selector "article.main" "https://example.com" # Remove nav and ads before extraction {baseDir}/scripts/reader.sh --remove "nav,footer,.ads" "https://example.com" # Search with JSON output {baseDir}/scripts/reader.sh --mode search --json "AI enterprise trends" # Read via Brazil proxy {baseDir}/scripts/reader.sh --proxy br "https://example.com.br" # Fact-check a claim {baseDir}/scripts/reader.sh --mode ground "Tesla is the most valuable car company" ``` ## API Key Resolution order: 1. `$JINA_API_KEY` env var 2. `~/.config/jina/api_key` file (canonical local path) Either is fine. Pick one: ```bash # Option A: env var export JINA_API_KEY="jina_..." # Option B: config file (preferred for local installs) mkdir -p ~/.config/jina && echo "jina_..." > ~/.config/jina/api_key chmod 600 ~/.config/jina/api_key ``` Free tier: 10M tokens (no signup needed). Get key at https://jina.ai/reader/ ## Pricing - **Read:** ~$0.005/page (standard) | 3x for ReaderLM-v2 - **Search:** 10K tokens fixed + variable per result - **Ground:** ~300K tokens/request (~30s latency) ## Why Jina Reader? - **IP protection** — requests route through Jina's infra, not your server - **Clean markdown** — readability extraction + optional ReaderLM-v2 - **Dynamic content** — headless Chrome renders JavaScript - **Structured extraction** — JSON schema support for data extraction

path: scripts/reader.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Jina AI Reader - Web content extraction
# Usage: reader.sh [options] "URL or query"

MODE="read"
SELECTOR=""
WAIT_SELECTOR=""
REMOVE_SELECTOR=""
PROXY_COUNTRY=""
NO_CACHE=false
FORMAT="markdown"
JSON_OUTPUT=false
INPUT=""

usage() {
  echo "Usage: $(basename "$0") [options] \"URL or query\""
  echo ""
  echo "Modes:"
  echo "  --mode read     Convert URL to markdown (default)"
  echo "  --mode search   Web search with full content extraction"
  echo "  --mode ground   Fact-check a statement"
  echo ""
  echo "Options:"
  echo "  --selector CSS    Extract specific region"
  echo "  --wait CSS        Wait for element before extraction"
  echo "  --remove CSS      Remove elements (comma-separated)"
  echo "  --proxy CC        Country code for geo-proxy"
  echo "  --nocache         Force fresh content"
  echo "  --format FMT      markdown|html|text|screenshot"
  echo "  --json            Raw JSON output"
  exit 1
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --mode) MODE="$2"; shift 2 ;;
    --selector) SELECTOR="$2"; shift 2 ;;
    --wait) WAIT_SELECTOR="$2"; shift 2 ;;
    --remove) REMOVE_SELECTOR="$2"; shift 2 ;;
    --proxy) PROXY_COUNTRY="$2"; shift 2 ;;
    --nocache) NO_CACHE=true; shift ;;
    --format) FORMAT="$2"; shift 2 ;;
    --json) JSON_OUTPUT=true; shift ;;
    --help|-h) usage ;;
    -*) echo "Unknown option: $1" >&2; usage ;;
    *) INPUT="$1"; shift ;;
  esac
done

if [[ -z "$INPUT" ]]; then
  echo "Error: No URL or query provided" >&2
  usage
fi

# API key resolution order:
#   1. $JINA_API_KEY (env)
#   2. ~/.config/jina/api_key (canonical local path)
[REDACTED_SECRET]"
if [[ -z "$API_KEY" && -r "${HOME}/.config/jina/api_key" ]]; then
  API_KEY="$(tr -d '[:space:]' < "${HOME}/.config/jina/api_key")"
fi

# Build headers array
HEADERS=()

if [[ -n "$API_KEY" ]]; then
  HEADERS+=(-H "Authorization: Bearer $API_KEY")
fi

HEADERS+=(-H "Accept: application/json")

if [[ -n "$SELECTOR" ]]; then
  HEADERS+=(-H "x-target-selector: $SELECTOR")
fi

if [[ -n "$WAIT_SELECTOR" ]]; then
  HEADERS+=(-H "x-wait-for-selector: $WAIT_SELECTOR")
fi

if [[ -n "$REMOVE_SELECTOR" ]]; then
  HEADERS+=(-H "x-remove-selector: $REMOVE_SELECTOR")
fi

if [[ -n "$PROXY_COUNTRY" ]]; then
  HEADERS+=(-H "x-country-proxy: $PROXY_COUNTRY")
fi

if [[ "$NO_CACHE" == true ]]; then
  HEADERS+=(-H "x-no-cache: true")
fi

if [[ "$FORMAT" != "markdown" ]]; then
  HEADERS+=(-H "x-respond-with: $FORMAT")
fi

# Execute based on mode
case "$MODE" in
  read)
    RESPONSE=$(curl -sS "${HEADERS[@]}" "https://r.jina.ai/${INPUT}" 2>&1)
    ;;
  search)
    ENCODED_QUERY=$(printf '%s' "$INPUT" | jq -sRr @uri)
    RESPONSE=$(curl -sS "${HEADERS[@]}" "https://s.jina.ai/${ENCODED_QUERY}" 2>&1)
    ;;
  ground)
    RESPONSE=$(curl -sS "${HEADERS[@]}" \
      -H "Content-Type: application/json" \
      -X POST "https://g.jina.ai/" \
      -d "$(jq -n --arg s "$INPUT" '{statement: $s}')" 2>&1)
    ;;
  *)
    echo "Error: Invalid mode '$MODE'. Use: read, search, ground" >&2
    exit 1
    ;;
esac

# Check for errors (Jina returns code 200 on success, 4xx/5xx on error)
RESP_CODE=$(echo "$RESPONSE" | jq -r '.code // empty' 2>/dev/null)
if [[ -n "$RESP_CODE" && "$RESP_CODE" != "200" ]]; then
  ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message // .readableMessage // "Unknown error"')
  echo "Error ($RESP_CODE): $ERROR_MSG" >&2
  exit 1
fi

# Output
if [[ "$JSON_OUTPUT" == true ]]; then
  echo "$RESPONSE" | jq .
  exit 0
fi

# Format output based on mode
case "$MODE" in
  read)
    TITLE=$(echo "$RESPONSE" | jq -r '.data.title // empty')
    URL=$(echo "$RESPONSE" | jq -r '.data.url // empty')
    CONTENT=$(echo "$RESPONSE" | jq -r '.data.content // .data // empty')

    if [[ -n "$TITLE" ]]; then
      echo "# $TITLE"
      echo ""
    fi
    if [[ -n "$URL" ]]; then
      echo "Source: $URL"
      echo ""
    fi
    if [[ -n "$CONTENT" ]]; then
      echo "$CONTENT"
    else
      echo "Error: No content extracted" >&2
      echo "$RESPONSE" | jq . >&2
      exit 1
    fi
    ;;

  search)
    RESULTS=$(echo "$RESPONSE" | jq -r '.data // empty')
    if [[ -z "$RESULTS" || "$RESULTS" == "null" ]]; then
      echo "Error: No search results" >&2
      echo "$RESPONSE" | jq . >&2
      exit 1
    fi

    echo "$RESPONSE" | jq -r '.data[] | "## \(.title // "Untitled")\nURL: \(.url // "N/A")\n\n\(.content // .description // "No content")\n\n---\n"'
    ;;

  ground)
    FACTUALITY=$(echo "$RESPONSE" | jq -r '.data.factuality // empty')
    RESULT=$(echo "$RESPONSE" | jq -r '.data.result // empty')
    REASON=$(echo "$RESPONSE" | jq -r '.data.reason // empty')

    echo "Factuality: $FACTUALITY"
    echo "Result: $RESULT"
    echo ""
    if [[ -n "$REASON" ]]; then
      echo "$REASON"
    fi

    REFERENCES=$(echo "$RESPONSE" | jq -r '.data.references[]?.url // empty' 2>/dev/null)
    if [[ -n "$REFERENCES" ]]; then
      echo ""
      echo "---"
      echo "Sources:"
      echo "$REFERENCES" | while IFS= read -r url; do
        echo "  - $url"
      done
    fi
    ;;
esac


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