Skill: michaelasper/kagi (version 0.1.0)

--- name: kagi description: Use the Kagi API (Search API + FastGPT) for web research when you want higher-quality results than Brave/Google, or when Brave Search is rate-limited. Triggers: “search with Kagi”, “Kagi search”, “use FastGPT”, “Kagi FastGPT”, “Kagi summarize”, or when you want programmatic web search via Kagi’s API token. --- # Kagi (API) Use the bundled Python scripts to call Kagi’s API from the OpenClaw host. ## Quick start 1) Create a token in https://kagi.com/settings/api 2) Export it for your shell/session: ```bash export KAGI_API_TOKEN='…' ``` 3) Run a search: ```bash python3 scripts/kagi_search.py "haaps glass" --limit 10 --json ``` 4) Or ask FastGPT (LLM + web search): ```bash python3 scripts/kagi_fastgpt.py "Summarize the latest Haaps glass mentions" --json ``` ## Tasks ### 1) Web search (Kagi Search API) Use when you need a normal ranked list of results (URLs/titles/snippets). Command: ```bash python3 scripts/kagi_search.py "<query>" [--limit N] [--json] ``` Notes: - Defaults to printing a readable digest; use `--json` for raw API output. - The script automatically sets `Authorization: Bot <token>`. ### 2) Answer/summarize with citations (FastGPT) Use when you want a short answer grounded in web results, including reference URLs. Command: ```bash python3 scripts/kagi_fastgpt.py "<question>" [--cache true|false] [--json] ``` ### 3) Using Kagi as a drop-in for web_search If Brave Search is rate-limited (429) or you want better results: - Use `scripts/kagi_search.py` to fetch results - Then use the main agent model to synthesize / summarize based on the returned URLs/snippets ## Files - API reference snippets: `references/kagi-api.md` - Python client + CLIs: `scripts/kagi_client.py`, `scripts/kagi_search.py`, `scripts/kagi_fastgpt.py`

path: README.md
content: # kagi-skill

OpenClaw skill: Kagi API (Search + FastGPT)

## What’s here

- `SKILL.md` — skill metadata + usage notes
- `scripts/` — tiny Python (no deps) wrappers for Kagi API
- `references/` — quick API/auth notes

## Setup

Create a Kagi API [REDACTED_SECRET]

Make it available to the scripts:

```bash
export KAGI_API_TOKEN='...'
```

## Usage

```bash
python3 scripts/kagi_search.py "steve jobs" --limit 5
python3 scripts/kagi_fastgpt.py "Summarize Python 3.11" 
```


path: references/kagi-api.md
content: # Kagi API quick reference

Source: https://help.kagi.com/kagi/api/

## Auth

Kagi uses an HTTP Authorization header:

```
Authorization: Bot <KAGI_API_TOKEN>
```

Tokens are created at: https://kagi.com/settings/api

## Base URL

- `https://kagi.com/api/v0`

## Endpoints used by this skill

### Search API

- `GET /search?q=<query>`
- Returns `meta` + `data[]` results (url/title/snippet/etc.)

Example:

```bash
curl -H "Authorization: Bot $KAGI_API_TOKEN" \
  "https://kagi.com/api/v0/search?q=steve+jobs"
```

### FastGPT

- `POST /fastgpt`
- JSON body: `{ "query": "…", "cache": true }`

Example:

```bash
curl -H "Authorization: Bot $KAGI_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"query":"Python 3.11"}' \
  https://kagi.com/api/v0/fastgpt
```


path: scripts/kagi_client.py
content: #!/usr/bin/env python3
"""Minimal Kagi API client (no external deps).

Env:
  - KAGI_API_TOKEN: required

Docs:
  - https://help.kagi.com/kagi/api/
"""

from __future__ import annotations

import json
import os
import urllib.parse
import urllib.request
from typing import Any, Dict, Optional


class KagiError(RuntimeError):
    pass


BASE_URL = "https://kagi.com/api/v0"


def _token() -> str:
    tok = os.environ.get("KAGI_API_TOKEN") or os.environ.get("KAGI_API_KEY")
    if not tok:
        raise KagiError(
            "Missing KAGI_API_TOKEN (or KAGI_API_KEY). Create one at https://kagi.com/settings/api"
        )
    return tok.strip()


def _request(method: str, path: str, *, params: Optional[Dict[str, str]] = None, json_body: Any = None, timeout: int = 60) -> Dict[str, Any]:
    url = BASE_URL + path
    if params:
        url += "?" + urllib.parse.urlencode(params)

    headers = {
        "Authorization": f"Bot {_token()}",
        "Accept": "application/json",
    }

    data = None
    if json_body is not None:
        data = json.dumps(json_body).encode("utf-8")
        headers["Content-Type"] = "application/json"

    req = urllib.request.Request(url, method=method.upper(), headers=headers, data=data)

    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8")
            try:
                return json.loads(raw)
            except json.JSONDecodeError as e:
                raise KagiError(f"Non-JSON response from Kagi: {e}\n---\n{raw[:500]}")
    except urllib.error.HTTPError as e:
        body = ""
        try:
            body = e.read().decode("utf-8")
        except Exception:
            pass
        raise KagiError(f"Kagi HTTP {e.code}: {body or e.reason}")
    except urllib.error.URLError as e:
        raise KagiError(f"Kagi request failed: {e}")


def search(query: str) -> Dict[str, Any]:
    return _request("GET", "/search", params={"q": query})


def fastgpt(query: str, *, cache: bool = True, web_search: bool = True) -> Dict[str, Any]:
    # NOTE: docs say web_search is currently out of service (must be true), but we keep param for forward-compat.
    body = {"query": query, "cache": bool(cache), "web_search": bool(web_search)}
    return _request("POST", "/fastgpt", json_body=body)


path: scripts/kagi_fastgpt.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import sys

from kagi_client import KagiError, fastgpt


def main() -> int:
    ap = argparse.ArgumentParser(description="Kagi FastGPT API wrapper")
    ap.add_argument("query", help="Question/prompt")
    ap.add_argument("--cache", default="true", choices=["true", "false"], help="Allow cached responses (default: true)")
    ap.add_argument("--json", action="store_true", help="Print raw JSON")
    args = ap.parse_args()

    try:
        out = fastgpt(args.query, cache=(args.cache == "true"))
    except KagiError as e:
        print(str(e), file=sys.stderr)
        return 2

    if args.json:
        print(json.dumps(out, indent=2, ensure_ascii=False))
        return 0

    data = (out.get("data") or {})
    output = (data.get("output") or "").strip()
    refs = data.get("references") or []

    print(output)
    if refs:
        print("\nReferences:")
        for r in refs:
            title = r.get("title") or "(no title)"
            url = r.get("url") or ""
            print(f"- {title} — {url}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/kagi_search.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import sys

from kagi_client import KagiError, search


def main() -> int:
    ap = argparse.ArgumentParser(description="Kagi Search API wrapper")
    ap.add_argument("query", help="Search query")
    ap.add_argument("--limit", type=int, default=10, help="Max results to print (default: 10)")
    ap.add_argument("--json", action="store_true", help="Print raw JSON")
    args = ap.parse_args()

    try:
        out = search(args.query)
    except KagiError as e:
        print(str(e), file=sys.stderr)
        return 2

    if args.json:
        print(json.dumps(out, indent=2, ensure_ascii=False))
        return 0

    meta = out.get("meta", {})
    data = out.get("data", []) or []

    print(f"Kagi Search: {args.query}")
    if meta:
        ms = meta.get("ms")
        bal = meta.get("api_balance")
        node = meta.get("node")
        extras = []
        if ms is not None:
            extras.append(f"{ms}ms")
        if node:
            extras.append(str(node))
        if bal is not None:
            extras.append(f"balance={bal}")
        if extras:
            print("(" + ", ".join(extras) + ")")
    print()

    for i, r in enumerate(data[: args.limit], start=1):
        title = r.get("title") or "(no title)"
        url = r.get("url") or ""
        snippet = (r.get("snippet") or "").strip().replace("\n", " ")
        print(f"{i}. {title}")
        if url:
            print(f"   {url}")
        if snippet:
            print(f"   {snippet}")
        print()

    return 0


if __name__ == "__main__":
    raise SystemExit(main())


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