Skill: jszesze/jszesze-openclaw-docs (version 1.0.0)

--- name: jszesze-openclaw-docs description: OpenClaw documentation helper that caches the live docs index, searches page titles and URLs, fetches raw markdown docs, builds a local docs cache for full-text grep, and tracks index snapshots over time. Use when answering OpenClaw setup, configuration, CLI, channel, automation, architecture, or troubleshooting questions and you want grounded docs links or current page text. --- # OpenClaw docs Use this skill for **OpenClaw product docs**, not for general repo spelunking. ## What this skill is good at - finding the right OpenClaw doc page quickly - fetching raw markdown from `docs.openclaw.ai` - caching the live docs index from `https://docs.openclaw.ai/llms.txt` - downloading docs locally for full-text search - tracking page additions/removals across snapshots of the docs index ## What it does not do - it does **not** inject the whole docs site into context automatically - it does **not** guarantee config snippets are current unless you verify them against fetched docs - it does **not** replace local repo inspection when the answer depends on the user’s actual workspace state ## Workflow 1. Start with the live docs index. 2. Search by keyword to find the most likely page. 3. Fetch the raw markdown page. 4. Quote or summarize the relevant section. 5. Link the exact docs URL in the answer. 6. For broad questions, build a local docs cache and run a full-text search. ## Commands Run scripts with `bash`. ### Index and cache ```bash bash ./scripts/cache.sh status bash ./scripts/cache.sh refresh ``` ### Search and discovery ```bash bash ./scripts/search.sh telegram bash ./scripts/search.sh "group.*policy" bash ./scripts/sitemap.sh bash ./scripts/fetch-doc.sh gateway/configuration bash ./scripts/fetch-doc.sh https://docs.openclaw.ai/channels/telegram ``` ### Local full-text cache ```bash bash ./scripts/build-index.sh fetch bash ./scripts/build-index.sh build bash ./scripts/build-index.sh search "requireMention" # optional smoke-test limit: OPENCLAW_DOCS_FETCH_LIMIT=25 bash ./scripts/build-index.sh fetch ``` ### Change tracking ```bash bash ./scripts/track-changes.sh snapshot bash ./scripts/track-changes.sh list bash ./scripts/track-changes.sh since 2026-05-01 bash ./scripts/recent.sh 7 ``` ## Heuristics - **setup or install**: check `start/`, `install/`, `channels/`, `gateway/` - **config fields or JSON5 shape**: check `gateway/configuration`, `gateway/configuration-reference`, `gateway/configuration-examples` - **CLI questions**: check `cli/` - **channel behavior**: check `channels/<provider>` and `channels/troubleshooting` - **architecture or concepts**: check `concepts/` - **automation**: check `automation/` - **control UI, dashboard, web surfaces**: check `web/` - **node/device questions**: check `nodes/` ## Good defaults - Prefer fetched raw markdown over guessing from memory. - Prefer the smallest relevant page instead of dumping a broad overview page. - If a snippet matters operationally, verify it against the fetched page before giving it to the user. - For ambiguous terms, search first, then fetch. ## Verified snippet notes See `./snippets/common-configs.md` for a few small, verified patterns copied from current docs pages. Treat them as helpers, not as a schema reference.

path: scripts/build-index.sh
content: #!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"

case "${1:-}" in
  fetch)
    count=$(fetch_all_docs)
    echo "Fetched $count docs into $CACHE_DIR/docs"
    ;;
  build)
    ensure_cache_dirs
    manifest="$CACHE_DIR/local-index.tsv"
    : > "$manifest"
    while IFS=$'\t' read -r title url; do
      [[ -n "$url" ]] || continue
      path=$(download_doc "$url")
      printf '%s\t%s\t%s\n' "$title" "$url" "$path" >> "$manifest"
    done < <(extract_doc_tsv)
    echo "Built local manifest: $manifest"
    echo "Docs indexed: $(wc -l < "$manifest" | tr -d ' ')"
    ;;
  search)
    shift
    if [[ $# -lt 1 ]]; then
      echo "Usage: bash ./scripts/build-index.sh search <query>"
      exit 1
    fi
    search_cached_docs "$*"
    ;;
  *)
    echo "Usage: bash ./scripts/build-index.sh {fetch|build|search <query>}"
    exit 1
    ;;
esac


path: scripts/cache.sh
content: #!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"

case "${1:-}" in
  status)
    ensure_cache_dirs
    idx=$(index_file)
    if [[ -s "$idx" ]]; then
      age=$(file_age_seconds "$idx")
      docs_count=$(extract_doc_tsv | wc -l | tr -d ' ')
      echo "Index cache: $idx"
      echo "Age seconds: $age"
      echo "Docs listed: $docs_count"
      echo "Docs cache dir: $CACHE_DIR/docs"
      echo "Snapshots dir: $CACHE_DIR/snapshots"
    else
      echo "Index cache missing. Run: bash ./scripts/cache.sh refresh"
    fi
    ;;
  refresh)
    dest=$(refresh_index)
    count=$(extract_doc_tsv | wc -l | tr -d ' ')
    echo "Refreshed docs index: $dest"
    echo "Docs listed: $count"
    ;;
  *)
    echo "Usage: bash ./scripts/cache.sh {status|refresh}"
    exit 1
    ;;
esac


path: scripts/fetch-doc.sh
content: #!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"

if [[ $# -lt 1 ]]; then
  echo "Usage: bash ./scripts/fetch-doc.sh <path-or-url>"
  exit 1
fi

url=$(normalize_doc_url "$1")
cache_path=$(download_doc "$url")
echo "# Source: $url"
echo "# Cached: $cache_path"
echo
cat "$cache_path"


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

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CACHE_DIR="${OPENCLAW_DOCS_CACHE_DIR:-$SKILL_DIR/.cache}"
INDEX_URL="${OPENCLAW_DOCS_INDEX_URL:-https://docs.openclaw.ai/llms.txt}"
BASE_URL="${OPENCLAW_DOCS_BASE_URL:-https://docs.openclaw.ai}"
CACHE_TTL_SECONDS="${OPENCLAW_DOCS_CACHE_TTL_SECONDS:-3600}"

ensure_cache_dirs() {
  mkdir -p "$CACHE_DIR" "$CACHE_DIR/docs" "$CACHE_DIR/snapshots"
}

index_file() {
  printf '%s\n' "$CACHE_DIR/llms.txt"
}

file_age_seconds() {
  local path="$1"
  if [[ ! -e "$path" ]]; then
    printf '999999999\n'
    return
  fi
  local now mtime
  now=$(date +%s)
  mtime=$(stat -c %Y "$path")
  printf '%s\n' "$((now - mtime))"
}

refresh_index() {
  ensure_cache_dirs
  local dest tmp
  dest=$(index_file)
  tmp="${dest}.tmp"
  curl -fsSL "$INDEX_URL" -o "$tmp"
  mv "$tmp" "$dest"
  printf '%s\n' "$dest"
}

ensure_index() {
  ensure_cache_dirs
  local dest age
  dest=$(index_file)
  age=$(file_age_seconds "$dest")
  if [[ ! -s "$dest" || "$age" -gt "$CACHE_TTL_SECONDS" ]]; then
    refresh_index >/dev/null
  fi
  printf '%s\n' "$dest"
}

extract_doc_tsv() {
  local idx
  idx=$(ensure_index)
  perl -ne 'print "$1\t$2\n" if /^- \[(.*)\]\((https:\/\/docs\.openclaw\.ai\/[^)]+\.md)\)$/' "$idx"
}

extract_doc_urls() {
  extract_doc_tsv | cut -f2
}

normalize_doc_url() {
  local input="${1:-}"
  if [[ -z "$input" ]]; then
    echo "Usage: provide a docs path like gateway/configuration or a full docs.openclaw.ai URL" >&2
    return 1
  fi

  if [[ "$input" =~ ^https?:// ]]; then
    local base_no_query query
    base_no_query="${input%%\?*}"
    query=""
    if [[ "$input" == *\?* ]]; then
      query="?${input#*\?}"
    fi
    if [[ "$base_no_query" != *.md ]]; then
      base_no_query="${base_no_query%/}.md"
    fi
    printf '%s%s\n' "$base_no_query" "$query"
    return
  fi

  local path="$input"
  path="${path#/}"
  path="${path%.html}"
  if [[ "$path" != *.md ]]; then
    path="${path}.md"
  fi
  printf '%s/%s\n' "$BASE_URL" "$path"
}

doc_cache_path() {
  local url="$1"
  local without_query rel
  without_query="${url%%\?*}"
  rel="${without_query#${BASE_URL}/}"
  printf '%s\n' "$CACHE_DIR/docs/$rel"
}

download_doc() {
  local url="$1"
  local dest tmp force_fetch
  ensure_cache_dirs
  dest=$(doc_cache_path "$url")
  mkdir -p "$(dirname "$dest")"
  force_fetch="${OPENCLAW_DOCS_FORCE_FETCH:-0}"
  if [[ -s "$dest" && "$force_fetch" != "1" ]]; then
    printf '%s\n' "$dest"
    return
  fi
  tmp="${dest}.tmp"
  curl -fsSL "$url" -o "$tmp"
  mv "$tmp" "$dest"
  printf '%s\n' "$dest"
}

snapshot_path() {
  local stamp="${1:-}"
  printf '%s\n' "$CACHE_DIR/snapshots/${stamp}.tsv"
}

snapshot_now() {
  local stamp dest
  stamp=$(date -u +%Y%m%dT%H%M%SZ)
  dest=$(snapshot_path "$stamp")
  extract_doc_tsv | sort > "$dest"
  printf '%s\n' "$dest"
}

list_snapshots() {
  ensure_cache_dirs
  find "$CACHE_DIR/snapshots" -maxdepth 1 -type f -name '*.tsv' -printf '%f\n' | sort
}

resolve_snapshot() {
  local ref="${1:-}"
  ensure_cache_dirs
  if [[ -z "$ref" ]]; then
    return 1
  fi
  if [[ -f "$ref" ]]; then
    printf '%s\n' "$ref"
    return
  fi
  if [[ -f "$(snapshot_path "$ref")" ]]; then
    snapshot_path "$ref"
    return
  fi
  local match
  match=$(find "$CACHE_DIR/snapshots" -maxdepth 1 -type f -name "${ref}*.tsv" | sort | head -n1 || true)
  if [[ -n "$match" ]]; then
    printf '%s\n' "$match"
    return
  fi
  return 1
}

compare_snapshots() {
  local older="$1"
  local newer="$2"
  local a b added removed
  a=$(mktemp)
  b=$(mktemp)
  trap 'rm -f "$a" "$b" "$added" "$removed"' RETURN
  sort "$older" > "$a"
  sort "$newer" > "$b"
  added=$(mktemp)
  removed=$(mktemp)
  comm -13 "$a" "$b" > "$added"
  comm -23 "$a" "$b" > "$removed"

  echo "Comparing $(basename "$older") -> $(basename "$newer")"
  echo
  echo "Added pages: $(wc -l < "$added" | tr -d ' ')"
  if [[ -s "$added" ]]; then
    cut -f1,2 "$added"
  else
    echo "(none)"
  fi
  echo
  echo "Removed pages: $(wc -l < "$removed" | tr -d ' ')"
  if [[ -s "$removed" ]]; then
    cut -f1,2 "$removed"
  else
    echo "(none)"
  fi
}

fetch_all_docs() {
  ensure_cache_dirs
  local count=0 limit
  limit="${OPENCLAW_DOCS_FETCH_LIMIT:-0}"
  while IFS=$'\t' read -r _title url; do
    [[ -n "$url" ]] || continue
    if [[ "$limit" -gt 0 && "$count" -ge "$limit" ]]; then
      break
    fi
    download_doc "$url" >/dev/null
    count=$((count + 1))
  done < <(extract_doc_tsv)
  printf '%s\n' "$count"
}

search_cached_docs() {
  local query="$1"
  local docs_dir="$CACHE_DIR/docs"
  if [[ ! -d "$docs_dir" ]] || ! { find "$docs_dir" -type f -print -quit | grep -q .; }; then
    echo "No cached docs yet. Run: bash ./scripts/build-index.sh fetch" >&2
    return 1
  fi
  if command -v rg >/dev/null 2>&1; then
    rg -n -i --glob '*.md' "$query" "$docs_dir"
  else
    grep -Rni --include='*.md' "$query" "$docs_dir"
  fi
}


path: scripts/recent.sh
content: #!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"

DAYS="${1:-7}"
ensure_cache_dirs
latest=$(find "$CACHE_DIR/snapshots" -maxdepth 1 -type f -name '*.tsv' -printf '%T@ %p\n' | sort -n | tail -n1 | cut -d' ' -f2- || true)
if [[ -z "$latest" ]]; then
  echo "No snapshots yet. Run: bash ./scripts/track-changes.sh snapshot"
  exit 1
fi

threshold=$(date -u -d "$DAYS days ago" +%s)
base=$(find "$CACHE_DIR/snapshots" -maxdepth 1 -type f -name '*.tsv' -printf '%T@ %p\n' | sort -n | awk -v t="$threshold" '$1 >= t {print substr($0, index($0,$2)); exit}')
if [[ -z "$base" || "$base" == "$latest" ]]; then
  echo "Not enough snapshots within the last $DAYS days to compare."
  echo "Run another snapshot later: bash ./scripts/track-changes.sh snapshot"
  exit 1
fi

compare_snapshots "$base" "$latest"


path: scripts/search.sh
content: #!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"

if [[ $# -lt 1 ]]; then
  echo "Usage: bash ./scripts/search.sh <keyword or regex>"
  exit 1
fi

query="$*"
extract_doc_tsv | awk -F '\t' -v q="$query" 'BEGIN{IGNORECASE=1} $1 ~ q || $2 ~ q {printf "- %s\n  %s\n", $1, $2; found=1} END{if (!found) exit 2}'


path: scripts/sitemap.sh
content: #!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"

extract_doc_tsv | awk -F '\t' '
function section(url, rest, parts, n) {
  sub(/^https:\/\/docs.openclaw.ai\//, "", url)
  rest = url
  sub(/\.md$/, "", rest)
  n = split(rest, parts, "/")
  if (n == 1) return "root"
  return parts[1]
}
{
  sec = section($2)
  docs[sec] = docs[sec] sprintf("- %s\n  %s\n", $1, $2)
  counts[sec]++
}
END {
  for (sec in counts) keys[++n] = sec
  asort(keys)
  for (i = 1; i <= n; i++) {
    sec = keys[i]
    printf "## %s (%d)\n%s\n", sec, counts[sec], docs[sec]
  }
}'


path: scripts/track-changes.sh
content: #!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"

case "${1:-}" in
  snapshot)
    path=$(snapshot_now)
    echo "Saved snapshot: $path"
    ;;
  list)
    snapshots=$(list_snapshots || true)
    if [[ -z "$snapshots" ]]; then
      echo "No snapshots yet. Run: bash ./scripts/track-changes.sh snapshot"
      exit 1
    fi
    while IFS= read -r file; do
      full="$CACHE_DIR/snapshots/$file"
      echo "$file  $(wc -l < "$full" | tr -d ' ') pages"
    done <<< "$snapshots"
    ;;
  since)
    if [[ -z "${2:-}" ]]; then
      echo "Usage: bash ./scripts/track-changes.sh since <snapshot-prefix|YYYY-MM-DD>"
      exit 1
    fi
    latest=$(find "$CACHE_DIR/snapshots" -maxdepth 1 -type f -name '*.tsv' | sort | tail -n1 || true)
    if [[ -z "$latest" ]]; then
      echo "No snapshots yet. Run: bash ./scripts/track-changes.sh snapshot"
      exit 1
    fi
    ref="${2:-}"
    older=""
    if older=$(resolve_snapshot "$ref" 2>/dev/null); then
      :
    else
      cutoff=$(date -u -d "$ref" +%s 2>/dev/null || true)
      if [[ -n "$cutoff" ]]; then
        older=$(find "$CACHE_DIR/snapshots" -maxdepth 1 -type f -name '*.tsv' -printf '%T@ %p\n' | sort -n | awk -v t="$cutoff" '$1 >= t {print substr($0, index($0,$2)); exit}')
      fi
    fi
    if [[ -z "$older" ]]; then
      echo "Could not resolve snapshot or date: $ref"
      exit 1
    fi
    if [[ "$older" == "$latest" ]]; then
      echo "Only one matching snapshot is available, nothing to compare yet."
      echo "Run another snapshot later: bash ./scripts/track-changes.sh snapshot"
      exit 1
    fi
    compare_snapshots "$older" "$latest"
    ;;
  *)
    echo "Usage: bash ./scripts/track-changes.sh {snapshot|list|since <snapshot-prefix|YYYY-MM-DD>}"
    exit 1
    ;;
esac


path: snippets/common-configs.md
content: # Verified OpenClaw config snippets

These are small snippets copied from current docs pages at cleanup time. Re-verify against fetched docs before editing a live config.

## Absolute minimum

Source: `gateway/configuration-examples`

```json5
{
  agent: { workspace: "~/.openclaw/workspace" },
  channels: { whatsapp: { allowFrom: ["+15555550123"] } },
}
```

## Telegram quick setup

Source: `channels/telegram`

```json5
{
  channels: {
    telegram: {
      enabled: true,
      botToken: "123:abc",
      dmPolicy: "pairing",
      groups: { "*": { requireMention: true } },
    },
  },
}
```

## Telegram pairing commands

Source: `channels/telegram`

```bash
openclaw gateway
openclaw pairing list telegram
openclaw pairing approve telegram <CODE>
```

## Configuration pages to verify before edits

- `gateway/configuration`
- `gateway/configuration-reference`
- `gateway/configuration-examples`


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