
Openclaw Grok Search
- 133 installs
- 5 repo stars
- Updated February 23, 2026
- stemmaker/openclaw-grok-search
Helps with ai & agent building tasks.
About
openclaw-grok-search is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- openclaw-grok-search
- AI & Agent Building
- AI-coding skill
Openclaw Grok Search by the numbers
- 133 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #3,594 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/stemmaker/openclaw-grok-search --skill openclaw-grok-searchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 133 |
|---|---|
| repo stars | ★ 5 |
| Last updated | February 23, 2026 |
| Repository | stemmaker/openclaw-grok-search ↗ |
What it does
Helps with ai & agent building tasks.
Files
Openclaw Grok Search
Run cross-platform web research and return structured JSON output with content and sources. This skill is project-local and should run directly from the downloaded project directory.
When to Use
Use this skill before answering when any of these apply:
1. The user asks for latest/current/today/recent information. 2. The answer depends on versions, releases, changelogs, or compatibility. 3. The task needs official docs, API references, or source URLs. 4. The user reports an error and root-cause analysis needs web evidence. 5. You are uncertain and need external confirmation before final output.
Quick Start
1. Write config interactively (first run only).
python scripts/configure.py2. Run a query.
python scripts/grok_search.py --query "What changed in Python recently?"Config Priority
1. CLI args such as --base-url and --api-key 2. Environment vars GROK_* 3. Config files
Default config lookup order:
1. config.json 2. config.local.json
Cross-Platform Rules
1. Prefer python ... commands, do not require PowerShell-only syntax. 2. Keep config in the project folder, do not install or copy into ~/.codex. 3. Support GROK_CONFIG_PATH only when you explicitly want a custom path.
Output Shape
Always print JSON with:
1. ok 2. content 3. sources 4. raw
Anti-Patterns
| Prohibited | Correct |
|---|---|
| No source citation | Include Source [<sup>1</sup>](URL) |
| Give up after one failure | Retry at least once |
| Use built-in WebSearch/WebFetch | Use GrokSearch tools/CLI |
{
"author": "橙家厨子",
"email": "tomography2308@163.com",
"base_url": "https://your-grok-endpoint.example",
"api_key": "YOUR_API_KEY",
"model": "grok-4.20-beta",
"timeout_seconds": 60,
"extra_body": {},
"extra_headers": {}
}
openclaw-grok-search
一个基于 grok-skill 改编的跨平台联网搜索 Skill,面向 OpenClawd(龙虾)/Codex 场景,支持 Windows、macOS、Linux 在项目本地目录直接使用。
项目缘起
我最近在玩龙虾(OpenClawd),发现系统自带网络搜索比较鸡肋:
1. Brave API 申请要绑信用卡。 2. 免费版经常触发调用速率限制。 3. 可获取资源不稳定,检索覆盖也受限。
后来逛 L 站时发现了宝藏项目 grok-skill。本项目基于原始项目做了改编,重点增强了可移植性和跨平台兼容性。
原始项目参考:grok-skill(https://github.com/Frankieli123/grok-skill)
核心改造
1. 从 PowerShell 安装/配置改为 Python 脚本,兼容 Win/macOS/Linux。 2. 默认仅在项目下载目录内运行,不再强依赖 ~/.codex 安装路径。 3. 保留 grok_search.py 的结构化 JSON 输出能力(content + sources)。 4. 保留可扩展参数:--extra-body-json、--extra-headers-json。
最近修复(2026-02-23)
为提升第三方 OpenAI 兼容中转站可用性,scripts/grok_search.py 已同步以下兼容增强:
1. SSE 回退解析:当中转站在 stream=false 下仍返回 data: {...} 分片时,自动拼接为标准 chat.completion 结构,避免 json.loads 直接失败。 2. 嵌入 JSON 提取:当模型输出为 <think>...`json {...}`` 或“前置思考 + JSON”时,可提取最终 JSON,恢复 content/sources` 结构化结果。 3. 解析兜底不破坏原行为:若本身就是标准 JSON 响应,仍走原路径,不影响既有使用方式。
快速开始
一、龙虾安装(OpenClaw)
在龙虾里直接让助手安装这个 skill:
- https://github.com/Stemmaker/openclaw-grok-search
可直接使用这句话:
请帮我安装这里的 skill:https://github.com/Stemmaker/openclaw-grok-search二、各大编程 CLI 安装(推荐)
npx skills add Stemmaker/openclaw-grok-search三、手动安装(git clone)
git clone https://github.com/Stemmaker/openclaw-grok-search.git
cd openclaw-grok-search然后在项目目录执行:
python scripts/configure.py
python scripts/grok_search.py --query "What changed in Python recently?"⚙️ 配置说明
方式 A:交互式配置(推荐)
python scripts/configure.py方式 B:手动编辑
编辑项目目录下的配置文件:
openclaw-grok-search/config.json{
"base_url": "https://your-grok-endpoint.example",
"api_key": "YOUR_API_KEY",
"model": "grok-4.20-beta",
"timeout_seconds": 60,
"extra_body": {},
"extra_headers": {}
}| 字段 | 说明 |
|---|---|
base_url | 你的 Grok API 端点地址 |
api_key | 你的 API 密钥(不要提交到 Git) |
model | 模型名称(如 grok-4.20-beta) |
timeout_seconds | 请求超时时间(秒) |
extra_body | 额外的请求体参数 |
extra_headers | 额外的 HTTP 请求头 |
方式 C:环境变量
export GROK_BASE_URL="https://your-grok-endpoint.example"
export GROK_API_KEY="YOUR_API_KEY"
export GROK_MODEL="grok-4.20-beta"公益中转站信息(第三方)
注册链接:
- https://ai.huan666.de/register?aff=eB8Z
站点特性:
1. 注册即送 10 刀。 2. grok-4.20-beta 每次搜索约消耗 0.01刀。 3. 若你有 L 站,可再领站点大佬红包 20 刀。 4. 新上线 Claude Sonnet 4.6(输入 2/M,输出 10/M)。 5. 每天签到可随机领金额。 6. 纯公益站,不能现金充值,只能使用L站的LDC充值,但是如果只使用grok搜索每天签到积分完全够用。
项目结构
openclaw-grok-search/
├─ SKILL.md
├─ config.example.json
└─ scripts/
├─ configure.py
└─ grok_search.py隐私与安全
1. API Key 保存在本地配置文件中,请勿提交到公开仓库。 2. 建议把真实密钥写入 config.local.json,并加入忽略列表。 3. 第三方中转站属于外部服务,请注意账号和密钥安全。
开源协议
沿用上游项目协议(MIT)
作者
- 作者:橙家厨子
- 邮箱:tomography2308@163.com
求个 Star
如果这个项目对你有帮助,欢迎 Star 支持,也欢迎提 Issue / PR 一起完善。
#!/usr/bin/env python3
"""
Author: 橙家厨子
Email: tomography2308@163.com
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from typing import Any
def read_json(path: Path) -> dict[str, Any]:
if not path.exists():
return {}
data = json.loads(path.read_text(encoding="utf-8-sig"))
if not isinstance(data, dict):
raise ValueError("config must be a JSON object")
return data
def ask(prompt: str, default: str) -> str:
suffix = f" [{default}]" if default else ""
value = input(f"{prompt}{suffix}: ").strip()
return value or default
def default_config_path() -> Path:
custom = (os.environ.get("GROK_CONFIG_PATH") or "").strip()
if custom:
return Path(custom).expanduser()
return Path(__file__).resolve().parent.parent / "config.json"
def main() -> int:
parser = argparse.ArgumentParser(description="Interactive config for openclaw-grok-search (project-local)")
parser.add_argument("--config", default="", help="Explicit config output path")
args = parser.parse_args()
config_path = Path(args.config).expanduser() if args.config else default_config_path()
config_path.parent.mkdir(parents=True, exist_ok=True)
existing = {}
try:
existing = read_json(config_path)
except Exception:
existing = {}
base_url = ask("Grok base URL", str(existing.get("base_url") or "https://your-grok-endpoint.example"))
api_key = ask("Grok API key", str(existing.get("api_key") or ""))
model = ask("Model", str(existing.get("model") or "grok-2-latest"))
timeout_raw = ask("Timeout seconds", str(existing.get("timeout_seconds") or "60"))
try:
timeout_seconds = int(timeout_raw)
except ValueError:
timeout_seconds = 60
config = {
"author": "橙家厨子",
"email": "tomography2308@163.com",
"base_url": base_url,
"api_key": api_key,
"model": model,
"timeout_seconds": timeout_seconds,
"extra_body": existing.get("extra_body") if isinstance(existing.get("extra_body"), dict) else {},
"extra_headers": existing.get("extra_headers") if isinstance(existing.get("extra_headers"), dict) else {},
}
config_path.write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"Wrote config: {config_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
import argparse
import json
import os
import re
import sys
import time
import urllib.error
import urllib.request
from typing import Any
from subprocess import CalledProcessError, run
"""
Author: 橙家厨子
Email: tomography2308@163.com
"""
def _compact_json(data: Any) -> str:
return json.dumps(data, ensure_ascii=False, separators=(",", ":"), sort_keys=False)
def _skill_root() -> str:
return os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
def _default_skill_config_paths() -> list[str]:
root = _skill_root()
return [
os.path.join(root, "config.json"),
os.path.join(root, "config.local.json"),
]
def _normalize_api_key(api_key: str) -> str:
api_key = api_key.strip()
if not api_key:
return ""
placeholder = {"YOUR_API_KEY", "API_KEY", "CHANGE_ME", "REPLACE_ME"}
if api_key.upper() in placeholder:
return ""
return api_key
def _normalize_base_url_value(base_url: str) -> str:
base_url = base_url.strip()
if not base_url:
return ""
placeholder = {
"https://your-grok-endpoint.example",
"YOUR_BASE_URL",
"BASE_URL",
"CHANGE_ME",
"REPLACE_ME",
}
if base_url.upper() in placeholder:
return ""
return base_url
def _load_json_file(path: str) -> dict[str, Any]:
try:
with open(path, "r", encoding="utf-8-sig") as f:
value = json.load(f)
except FileNotFoundError:
return {}
if not isinstance(value, dict):
raise ValueError("config must be a JSON object")
return value
def _has_any_config_file() -> bool:
return any(os.path.exists(p) for p in _default_skill_config_paths())
def _should_run_configure(args: argparse.Namespace) -> bool:
if args.base_url.strip() or args.api_key.strip():
return False
if (os.environ.get("GROK_BASE_URL", "").strip() and os.environ.get("GROK_API_KEY", "").strip()):
return False
if os.environ.get("GROK_CONFIG_PATH", "").strip():
return os.path.exists(os.environ["GROK_CONFIG_PATH"].strip())
return not _has_any_config_file()
def _run_configure() -> bool:
configure_path = os.path.join(_skill_root(), "scripts", "configure.py")
if not os.path.exists(configure_path):
sys.stderr.write("configure.py not found; cannot auto-configure.\n")
return False
try:
run([sys.executable, configure_path], check=True)
except CalledProcessError:
sys.stderr.write("configure.py failed; aborting.\n")
return False
return True
def _normalize_base_url(base_url: str) -> str:
base_url = base_url.strip().rstrip("/")
if base_url.endswith("/v1"):
return base_url[: -len("/v1")]
return base_url
def _coerce_json_object(text: str) -> dict[str, Any] | None:
text = text.strip()
if not text:
return None
if text.startswith("{") and text.endswith("}"):
try:
value = json.loads(text)
return value if isinstance(value, dict) else None
except json.JSONDecodeError:
return None
return None
def _extract_embedded_json_object(text: str) -> dict[str, Any] | None:
decoder = json.JSONDecoder()
candidates: list[dict[str, Any]] = []
for i, ch in enumerate(text):
if ch != "{":
continue
try:
value, end = decoder.raw_decode(text[i:])
except json.JSONDecodeError:
continue
if i + end < len(text):
tail = text[i + end :].strip()
if tail and tail not in {"```", "```json", "```JSON"}:
continue
if isinstance(value, dict):
candidates.append(value)
if not candidates:
return None
for item in reversed(candidates):
if "content" in item or "sources" in item:
return item
return candidates[-1]
def _extract_urls(text: str) -> list[str]:
urls = re.findall(r"https?://[^\s)\]}>\"']+", text)
seen: set[str] = set()
out: list[str] = []
for url in urls:
url = url.rstrip(".,;:!?'\"")
if url and url not in seen:
seen.add(url)
out.append(url)
return out
def _load_json_env(var_name: str) -> dict[str, Any]:
raw = os.environ.get(var_name, "").strip()
if not raw:
return {}
value = json.loads(raw)
if not isinstance(value, dict):
raise ValueError(f"{var_name} must be a JSON object")
return value
def _parse_json_object(raw: str, *, label: str) -> dict[str, Any]:
raw = raw.strip()
if not raw:
return {}
value = json.loads(raw)
if not isinstance(value, dict):
raise ValueError(f"{label} must be a JSON object")
return value
def _request_chat_completions(
*,
base_url: str,
api_key: str,
model: str,
query: str,
timeout_seconds: float,
extra_headers: dict[str, Any],
extra_body: dict[str, Any],
) -> dict[str, Any]:
url = f"{_normalize_base_url(base_url)}/v1/chat/completions"
system = (
"You are a web research assistant. Use live web search/browsing when answering. "
"Return ONLY a single JSON object with keys: "
"content (string), sources (array of objects with url/title/snippet when possible). "
"Keep content concise and evidence-backed."
)
body: dict[str, Any] = {
"model": model,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": query},
],
"temperature": 0.2,
"stream": False,
}
body.update(extra_body)
headers: dict[str, str] = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
for key, value in extra_headers.items():
headers[str(key)] = str(value)
req = urllib.request.Request(
url=url,
data=_compact_json(body).encode("utf-8"),
headers=headers,
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout_seconds) as resp:
raw = resp.read().decode("utf-8", errors="replace")
try:
return json.loads(raw)
except json.JSONDecodeError:
# Some OpenAI-compatible proxies return SSE chunks even when stream=false.
content_parts: list[str] = []
model_name = model
created = int(time.time())
for line in raw.splitlines():
line = line.strip()
if not line.startswith("data:"):
continue
payload = line[len("data:") :].strip()
if not payload or payload == "[DONE]":
continue
try:
chunk = json.loads(payload)
except json.JSONDecodeError:
continue
if isinstance(chunk.get("model"), str) and chunk.get("model"):
model_name = chunk["model"]
if isinstance(chunk.get("created"), int):
created = chunk["created"]
choices = chunk.get("choices")
if not isinstance(choices, list):
continue
for choice in choices:
if not isinstance(choice, dict):
continue
delta = choice.get("delta")
if isinstance(delta, dict):
piece = delta.get("content")
if isinstance(piece, str) and piece:
content_parts.append(piece)
if content_parts:
return {
"id": "chatcmpl-sse-normalized",
"object": "chat.completion",
"created": created,
"model": model_name,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "".join(content_parts)},
"finish_reason": "stop",
}
],
}
raise
def main() -> int:
parser = argparse.ArgumentParser(description="Aggressive web research via OpenAI-compatible Grok endpoint.")
parser.add_argument("--query", required=True, help="Search query / research task.")
parser.add_argument("--config", default="", help="Path to config JSON file.")
parser.add_argument("--base-url", default="", help="Override base URL.")
parser.add_argument("--api-key", default="", help="Override API key.")
parser.add_argument("--model", default="", help="Override model.")
parser.add_argument("--timeout-seconds", type=float, default=0.0, help="Override timeout (seconds).")
parser.add_argument(
"--extra-body-json",
default="",
help="Extra JSON object merged into request body.",
)
parser.add_argument(
"--extra-headers-json",
default="",
help="Extra JSON object merged into request headers.",
)
args = parser.parse_args()
if _should_run_configure(args):
if not _run_configure():
return 2
env_config_path = os.environ.get("GROK_CONFIG_PATH", "").strip()
explicit_config_path = args.config.strip() or env_config_path
config_path = ""
config: dict[str, Any] = {}
if explicit_config_path:
config_path = explicit_config_path
try:
config = _load_json_file(config_path)
except Exception as e:
sys.stderr.write(f"Invalid config ({config_path}): {e}\n")
return 2
else:
fallback_path = ""
fallback_config: dict[str, Any] = {}
for candidate in _default_skill_config_paths():
if not os.path.exists(candidate):
continue
try:
candidate_config = _load_json_file(candidate)
except Exception as e:
sys.stderr.write(f"Invalid config ({candidate}): {e}\n")
return 2
if not fallback_path:
fallback_path = candidate
fallback_config = candidate_config
candidate_key = _normalize_api_key(str(candidate_config.get("api_key") or ""))
if candidate_key:
config_path = candidate
config = candidate_config
break
if not config_path and fallback_path:
config_path = fallback_path
config = fallback_config
if not config_path:
config_path = _default_skill_config_paths()[0]
base_url = _normalize_base_url_value(
args.base_url.strip() or os.environ.get("GROK_BASE_URL", "").strip() or str(config.get("base_url") or "").strip()
)
api_key = _normalize_api_key(
args.api_key.strip() or os.environ.get("GROK_API_KEY", "").strip() or str(config.get("api_key") or "").strip()
)
model = args.model.strip() or os.environ.get("GROK_MODEL", "").strip() or str(config.get("model") or "").strip() or "grok-2-latest"
timeout_seconds = args.timeout_seconds
if not timeout_seconds:
timeout_seconds = float(os.environ.get("GROK_TIMEOUT_SECONDS", "0") or "0")
if not timeout_seconds:
timeout_seconds = float(config.get("timeout_seconds") or 0) or 60.0
if not base_url:
sys.stderr.write(
"Missing base URL: set GROK_BASE_URL, write it to config, or pass --base-url\n"
f"Config path: {config_path}\n"
)
return 2
if not api_key:
sys.stderr.write(
"Missing API key: set GROK_API_KEY, write it to config, or pass --api-key\n"
f"Config path: {config_path}\n"
)
return 2
try:
extra_body: dict[str, Any] = {}
cfg_extra_body = config.get("extra_body")
if isinstance(cfg_extra_body, dict):
extra_body.update(cfg_extra_body)
extra_body.update(_load_json_env("GROK_EXTRA_BODY_JSON"))
extra_body.update(_parse_json_object(args.extra_body_json, label="--extra-body-json"))
extra_headers: dict[str, Any] = {}
cfg_extra_headers = config.get("extra_headers")
if isinstance(cfg_extra_headers, dict):
extra_headers.update(cfg_extra_headers)
extra_headers.update(_load_json_env("GROK_EXTRA_HEADERS_JSON"))
extra_headers.update(_parse_json_object(args.extra_headers_json, label="--extra-headers-json"))
except Exception as e:
sys.stderr.write(f"Invalid JSON: {e}\n")
return 2
started = time.time()
try:
resp = _request_chat_completions(
base_url=base_url,
api_key=api_key,
model=model,
query=args.query,
timeout_seconds=timeout_seconds,
extra_headers=extra_headers,
extra_body=extra_body,
)
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", errors="replace") if hasattr(e, "read") else ""
out = {
"ok": False,
"error": f"HTTP {getattr(e, 'code', None)}",
"detail": raw or str(e),
"config_path": config_path,
"base_url": base_url,
"model": model,
"elapsed_ms": int((time.time() - started) * 1000),
}
sys.stdout.write(_compact_json(out))
return 1
except Exception as e:
out = {
"ok": False,
"error": "request_failed",
"detail": str(e),
"config_path": config_path,
"base_url": base_url,
"model": model,
"elapsed_ms": int((time.time() - started) * 1000),
}
sys.stdout.write(_compact_json(out))
return 1
message = ""
try:
choice0 = (resp.get("choices") or [{}])[0]
msg = choice0.get("message") or {}
message = msg.get("content") or ""
except Exception:
message = ""
parsed = _coerce_json_object(message)
if parsed is None:
parsed = _extract_embedded_json_object(message)
sources: list[dict[str, Any]] = []
content = ""
raw = ""
if parsed is not None:
content = str(parsed.get("content") or "")
src = parsed.get("sources")
if isinstance(src, list):
for item in src:
if isinstance(item, dict) and item.get("url"):
sources.append(
{
"url": str(item.get("url")),
"title": str(item.get("title") or ""),
"snippet": str(item.get("snippet") or ""),
}
)
if not sources:
for url in _extract_urls(content):
sources.append({"url": url, "title": "", "snippet": ""})
else:
raw = message
for url in _extract_urls(message):
sources.append({"url": url, "title": "", "snippet": ""})
out = {
"ok": True,
"query": args.query,
"config_path": config_path,
"base_url": base_url,
"model": resp.get("model") or model,
"content": content,
"sources": sources,
"raw": raw,
"usage": resp.get("usage") or {},
"elapsed_ms": int((time.time() - started) * 1000),
}
sys.stdout.write(_compact_json(out))
return 0
if __name__ == "__main__":
raise SystemExit(main())