
Grok Search
- 5 installs
- 61 repo stars
- Updated March 16, 2026
- kirkluokun/awesome-a-stock-openclawskills
Helps with ai & agent building tasks.
About
grok-search is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- grok-search
- AI & Agent Building
- AI-coding skill
Grok Search by the numbers
- 5 all-time installs (skills.sh)
- Ranked #13,065 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kirkluokun/awesome-a-stock-openclawskills --skill grok-searchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 61 |
| Last updated | March 16, 2026 |
| Repository | kirkluokun/awesome-a-stock-openclawskills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Run xAI Grok locally via bundled scripts (search + chat + model listing). Default output for search is pretty JSON (agent-friendly) with citations.
API key
The script looks for an xAI API key in this order:
XAI_API_KEYenv var~/.clawdbot/clawdbot.json→env.XAI_API_KEY~/.clawdbot/clawdbot.json→skills.entries["grok-search"].apiKey- fallback:
skills.entries["search-x"].apiKeyorskills.entries.xai.apiKey
Run
Use {baseDir} so the command works regardless of workspace layout.
Search
- Web search (JSON):
node {baseDir}/scripts/grok_search.mjs "<query>" --web
- X/Twitter search (JSON):
node {baseDir}/scripts/grok_search.mjs "<query>" --x
Chat
- Chat (text):
node {baseDir}/scripts/chat.mjs "<prompt>"
- Chat (vision):
node {baseDir}/scripts/chat.mjs --image /path/to/image.jpg "<prompt>"
Models
- List models:
node {baseDir}/scripts/models.mjs
Useful flags
Output:
--links-onlyprint just citation URLs--texthide the citations section in pretty output--rawinclude the raw Responses API payload on stderr (debug)
Common:
--max <n>limit results (default 8)--model <id>(defaultgrok-4-1-fast)
X-only filters (server-side via x_search tool params):
--days <n>(e.g. 7)--from YYYY-MM-DD/--to YYYY-MM-DD--handles @a,@b(limit to these handles)--exclude @bots,@spam(exclude handles)
Output shape (JSON)
{
"query": "...",
"mode": "web" | "x",
"results": [
{
"title": "...",
"url": "...",
"snippet": "...",
"author": "...",
"posted_at": "..."
}
],
"citations": ["https://..."]
}Notes
citationsare merged/validated from xAI response annotations where possible (more reliable than trusting the model’s JSON blindly).- Prefer
--xfor tweets/threads,--webfor general research. - Default to English queries for both web and X searches unless the user explicitly requests a different language.
{
"owner": "notabhay",
"slug": "grok-search",
"displayName": "grok-search",
"latest": {
"version": "0.2.1",
"publishedAt": 1769443868217,
"commit": "https://github.com/clawdbot/skills/commit/1aa64124d9f3f9bb137d1ad15fd6512f117ff15e"
},
"history": [
{
"version": "0.1.0",
"publishedAt": 1769439975447,
"commit": "https://github.com/clawdbot/skills/commit/d715469b769c291216d96ef136a4259e0fe53ed5"
}
]
}
# xAI (Grok) API 密钥(capture-grok-search 核心依赖,用于 web_search / x_search)
# 获取地址:https://console.x.ai/
# 文档:https://docs.x.ai/docs/api-reference
XAI_API_KEY=your_xai_api_key_here
xAI tools docs (quick links)
- Search tools (web_search + x_search): https://docs.x.ai/docs/guides/tools/search-tools
- Tools overview: https://docs.x.ai/docs/guides/tools/overview
- API reference (base URL, endpoints): https://docs.x.ai/docs/api-reference
#!/usr/bin/env node
/**
* chat.mjs
*
* Chat with xAI Grok via Responses API.
* Supports optional image attachments.
*
* Examples:
* node {baseDir}/scripts/chat.mjs "What is xAI?"
* node {baseDir}/scripts/chat.mjs --model grok-4-1-fast "Summarize today's AI news"
* node {baseDir}/scripts/chat.mjs --image ./pic.jpg "What's in this image?"
* node {baseDir}/scripts/chat.mjs --json "Return a JSON object with keys a,b"
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
function usage(msg) {
if (msg) console.error(msg);
console.error(
"Usage: chat.mjs [--model <id>] [--json] [--raw] [--image <path>]... <prompt>"
);
process.exit(2);
}
function readKeyFromClawdbotConfig() {
try {
const p = path.join(os.homedir(), ".clawdbot", "clawdbot.json");
const raw = fs.readFileSync(p, "utf8");
const j = JSON.parse(raw);
return (
process.env.XAI_API_KEY ||
j?.env?.XAI_API_KEY ||
j?.env?.vars?.XAI_API_KEY ||
j?.skills?.entries?.["grok-search"]?.apiKey ||
j?.skills?.entries?.xai?.apiKey ||
null
);
} catch {
return process.env.XAI_API_KEY || null;
}
}
function mimeFor(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
if (ext === ".png") return "image/png";
if (ext === ".webp") return "image/webp";
if (ext === ".gif") return "image/gif";
return null;
}
function toDataUrl(filePath) {
const mime = mimeFor(filePath);
if (!mime) throw new Error(`Unsupported image type: ${filePath}`);
const buf = fs.readFileSync(filePath);
return `data:${mime};base64,${buf.toString("base64")}`;
}
function collectCitations(resp) {
const out = new Set();
if (Array.isArray(resp?.citations)) {
for (const u of resp.citations) if (typeof u === "string" && u) out.add(u);
}
if (Array.isArray(resp?.output)) {
for (const item of resp.output) {
const content = Array.isArray(item?.content) ? item.content : [];
for (const c of content) {
const ann = Array.isArray(c?.annotations) ? c.annotations : [];
for (const a of ann) {
const url = a?.url || a?.web_citation?.url;
if (typeof url === "string" && url) out.add(url);
}
}
}
}
return [...out];
}
const args = process.argv.slice(2);
if (!args.length) usage();
let model = process.env.GROK_MODEL || "grok-4-1-fast";
let jsonOut = false;
let rawOut = false;
let images = [];
let promptParts = [];
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === "--model") {
const v = args[++i];
if (!v) usage("Missing value for --model");
model = v;
} else if (a === "--json") jsonOut = true;
else if (a === "--raw") rawOut = true;
else if (a === "--image") {
const v = args[++i];
if (!v) usage("Missing value for --image");
images.push(v);
} else if (a.startsWith("-")) usage(`Unknown flag: ${a}`);
else promptParts.push(a);
}
const prompt = promptParts.join(" ").trim();
if (!prompt) usage("Missing <prompt>");
const apiKey = readKeyFromClawdbotConfig();
if (!apiKey) {
console.error("Missing XAI_API_KEY.");
process.exit(1);
}
const content = [{ type: "input_text", text: prompt }];
for (const img of images) {
content.push({ type: "input_image", image_url: toDataUrl(img) });
}
const body = {
model,
input: [{ role: "user", content }],
store: false,
};
const res = await fetch("https://api.x.ai/v1/responses", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
console.error(`xAI API error: ${res.status} ${res.statusText}`);
console.error(t.slice(0, 4000));
process.exit(1);
}
const data = await res.json();
const text =
data.output_text ||
data?.output
?.flatMap((o) => (Array.isArray(o?.content) ? o.content : []))
?.find((c) => c?.type === "output_text" && typeof c?.text === "string")
?.text ||
"";
if (jsonOut) {
console.log(JSON.stringify({ model, prompt, text, citations: collectCitations(data) }, null, 2));
if (rawOut) console.error(JSON.stringify(data, null, 2));
process.exit(0);
}
console.log(text.trim());
const cites = collectCitations(data);
if (cites.length) {
console.log("\nCitations:");
for (const c of cites) console.log(`- ${c}`);
}
if (rawOut) {
console.error("\n--- RAW RESPONSE (debug) ---\n");
console.error(JSON.stringify(data, null, 2));
}
#!/usr/bin/env node
/**
* grok_search.mjs
*
* Minimal xAI (Grok) search wrapper.
* - Uses xAI Responses API (OpenAI-compatible)
* - Lets Grok run server-side tools:
* - web_search (web)
* - x_search (X/Twitter)
*
* Usage:
* node scripts/grok_search.mjs "query" --web --json
* node scripts/grok_search.mjs "query" --x --days 7 --handles @clawdbot --json
* node scripts/grok_search.mjs "query" --x --from 2026-01-01 --to 2026-01-27 --json
* node scripts/grok_search.mjs "query" --x --links-only
* node scripts/grok_search.mjs "query" --x --raw --json
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
function usage(msg) {
if (msg) console.error(msg);
console.error(
"Usage: grok_search.mjs <query> (--web|--x) [--json] [--text|--links-only] [--raw] [--model <id>] [--max <n>] [--days <n>] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--handles a,b] [--exclude a,b]"
);
process.exit(2);
}
function readKeyFromClawdbotConfig() {
// Priority:
// 1) env var
// 2) clawdbot.json env.XAI_API_KEY
// 3) clawdbot.json skills.entries.grok-search.apiKey
// 4) clawdbot.json skills.entries.search-x.apiKey / skills.entries.xai.apiKey (fallback)
try {
const p = path.join(os.homedir(), ".clawdbot", "clawdbot.json");
const raw = fs.readFileSync(p, "utf8");
const j = JSON.parse(raw);
return (
process.env.XAI_API_KEY ||
j?.env?.XAI_API_KEY ||
j?.env?.vars?.XAI_API_KEY ||
j?.skills?.entries?.["grok-search"]?.apiKey ||
j?.skills?.entries?.["search-x"]?.apiKey ||
j?.skills?.entries?.xai?.apiKey ||
null
);
} catch {
return process.env.XAI_API_KEY || null;
}
}
const args = process.argv.slice(2);
if (args.length === 0) usage();
let queryParts = [];
let mode = null; // 'web' | 'x'
let jsonOut = false;
let rawOut = false;
let format = "json"; // json|text|links
let model = "grok-4-1-fast";
let maxResults = 8;
// X-search filters
let days = null; // number
let fromDate = null; // YYYY-MM-DD
let toDate = null; // YYYY-MM-DD
let handles = []; // array of handles (no @)
let excludeHandles = [];
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === "--web") mode = "web";
else if (a === "--x") mode = "x";
else if (a === "--json") jsonOut = true;
else if (a === "--raw") rawOut = true;
else if (a === "--links-only") format = "links";
else if (a === "--text") format = "text";
else if (a === "--model") {
const v = args[++i];
if (!v) usage("Missing value for --model");
model = v;
} else if (a === "--max") {
const v = Number(args[++i]);
if (!Number.isFinite(v) || v <= 0) usage("Bad value for --max");
maxResults = Math.floor(v);
} else if (a === "--days") {
const v = Number(args[++i]);
if (!Number.isFinite(v) || v <= 0) usage("Bad value for --days");
days = Math.floor(v);
} else if (a === "--from") {
const v = args[++i];
if (!v) usage("Missing value for --from");
fromDate = v;
} else if (a === "--to") {
const v = args[++i];
if (!v) usage("Missing value for --to");
toDate = v;
} else if (a === "--handles") {
const v = args[++i];
if (!v) usage("Missing value for --handles");
handles = v
.split(",")
.map((h) => h.trim().replace(/^@/, ""))
.filter(Boolean);
} else if (a === "--exclude") {
const v = args[++i];
if (!v) usage("Missing value for --exclude");
excludeHandles = v
.split(",")
.map((h) => h.trim().replace(/^@/, ""))
.filter(Boolean);
} else if (a.startsWith("-")) {
usage(`Unknown flag: ${a}`);
} else {
queryParts.push(a);
}
}
// default output is JSON (agent-friendly)
if (!jsonOut && format === "json") jsonOut = true;
const query = queryParts.join(" ").trim();
if (!query) usage("Missing <query>");
if (!mode) usage("Must specify --web or --x");
const apiKey = readKeyFromClawdbotConfig();
if (!apiKey) {
console.error(
"Missing XAI_API_KEY. Set env var or add env.XAI_API_KEY in ~/.clawdbot/clawdbot.json"
);
process.exit(1);
}
const toolType = mode === "x" ? "x_search" : "web_search";
function isoDate(d) {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function computeDateRange() {
if (fromDate || toDate) {
return {
from_date: fromDate || undefined,
to_date: toDate || undefined,
};
}
if (days) {
const to = new Date();
const from = new Date();
from.setDate(from.getDate() - days);
return { from_date: isoDate(from), to_date: isoDate(to) };
}
return {};
}
const dateRange = computeDateRange();
// Prefer server-side tool params for X filtering (days/handles/exclude)
const tools =
mode === "x"
? [
{
type: "x_search",
x_search: {
...(dateRange.from_date ? { from_date: dateRange.from_date } : {}),
...(dateRange.to_date ? { to_date: dateRange.to_date } : {}),
...(handles.length ? { allowed_x_handles: handles } : {}),
...(excludeHandles.length
? { excluded_x_handles: excludeHandles }
: {}),
},
},
]
: [{ type: "web_search" }];
// We ask Grok to use the tool, and return strict JSON.
const prompt = `Use the provided ${toolType} tool to research: ${JSON.stringify(
query
)}
Return ONLY valid JSON (no markdown) in this schema:
{
"query": string,
"mode": "${mode}",
"results": [
{
"title": string|null,
"url": string|null,
"snippet": string|null,
"author": string|null,
"posted_at": string|null
}
],
"citations": [string]
}
Rules:
- results length <= ${maxResults}
- citations must be unique URLs
- for mode="x":
- urls should be x.com links to the posts whenever possible
- title can be "@handle" and snippet should contain the tweet text
- posted_at should be ISO date/time if you can infer it, else null
- if you cannot find anything, return empty arrays (still valid JSON).`;
const body = {
model,
input: [
{
role: "user",
content: [{ type: "input_text", text: prompt }],
},
],
tools,
store: false,
temperature: 0,
};
const res = await fetch("https://api.x.ai/v1/responses", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
console.error(`xAI API error: ${res.status} ${res.statusText}`);
console.error(t.slice(0, 4000));
process.exit(1);
}
const data = await res.json();
// Try to extract the model's text.
const text =
data.output_text ||
// xAI responses: output is an array of events; the assistant message is usually later.
data?.output
?.flatMap((o) => (Array.isArray(o?.content) ? o.content : []))
?.find((c) => c?.type === "output_text" && typeof c?.text === "string")
?.text ||
null;
if (!text) {
// fallback: emit whole response
if (jsonOut) {
console.log(JSON.stringify({ query, mode, raw: data }, null, 2));
} else {
console.log(JSON.stringify(data, null, 2));
}
process.exit(0);
}
function dedupeXCitations(urls) {
// Dedupe by tweet id. Prefer canonical /<handle>/status/<id> over /i/status/<id>.
function tweetInfo(u) {
const m1 = u.match(/https?:\/\/(?:x\.com|twitter\.com)\/[^\/]+\/status\/(\d+)/i);
if (m1) return { id: m1[1], kind: "status" };
const m2 = u.match(/https?:\/\/(?:x\.com|twitter\.com)\/i\/status\/(\d+)/i);
if (m2) return { id: m2[1], kind: "i" };
return null;
}
// First pass: pick best URL per tweet id (first status wins; else first i/status).
const best = new Map(); // id -> { url, kind }
for (const u of urls) {
const info = tweetInfo(u);
if (!info) continue;
const cur = best.get(info.id);
if (!cur) best.set(info.id, { url: u, kind: info.kind });
else if (cur.kind === "i" && info.kind === "status") best.set(info.id, { url: u, kind: info.kind });
}
// Second pass: preserve original order, emit only best url per id.
const out = [];
const seen = new Set();
for (const u of urls) {
const info = tweetInfo(u);
if (info) {
const b = best.get(info.id);
if (b?.url === u && !seen.has(b.url)) {
out.push(u);
seen.add(b.url);
}
continue;
}
if (!seen.has(u)) {
out.push(u);
seen.add(u);
}
}
return out;
}
function collectCitations(resp) {
const out = new Set();
// Some responses include a top-level citations array.
if (Array.isArray(resp?.citations)) {
for (const u of resp.citations) {
if (typeof u === "string" && u) out.add(u);
}
}
// Annotations inside output_text.
if (Array.isArray(resp?.output)) {
for (const item of resp.output) {
if (!item) continue;
const content = Array.isArray(item.content) ? item.content : [];
for (const c of content) {
const ann = Array.isArray(c?.annotations) ? c.annotations : [];
for (const a of ann) {
const url = a?.url || a?.web_citation?.url;
if (typeof url === "string" && url) out.add(url);
}
}
}
}
// Prefer X links for x_search when available.
if (mode === "x") {
const xLinks = [...out].filter((u) => /https?:\/\/(x\.com|twitter\.com)\//i.test(u));
if (xLinks.length) return dedupeXCitations(xLinks);
}
return [...out];
}
function normalizeParsed(parsedObj) {
const resultsRaw = Array.isArray(parsedObj?.results) ? parsedObj.results : [];
const results = resultsRaw.slice(0, maxResults).map((r) => {
const obj = r && typeof r === "object" ? r : {};
return {
title: obj.title ?? null,
url: obj.url ?? null,
snippet: obj.snippet ?? null,
author: obj.author ?? null,
posted_at: obj.posted_at ?? null,
};
});
const citationsRaw = Array.isArray(parsedObj?.citations) ? parsedObj.citations : [];
const citationsFromResp = collectCitations(data);
let citationsMerged = [...new Set([...citationsRaw, ...citationsFromResp].filter(Boolean))];
// Prefer citations that correspond to returned results, and cap the list.
const resultUrls = results.map((r) => r?.url).filter((u) => typeof u === "string" && u);
if (mode === "x") citationsMerged = dedupeXCitations(citationsMerged);
const citations = [];
const seen = new Set();
for (const u of resultUrls) {
if (!seen.has(u)) {
citations.push(u);
seen.add(u);
}
}
for (const u of citationsMerged) {
if (!seen.has(u)) {
citations.push(u);
seen.add(u);
}
}
const cap = Math.max(12, maxResults * 3);
const capped = citations.slice(0, cap);
return {
query: parsedObj?.query ?? query,
mode: parsedObj?.mode ?? mode,
results,
citations: capped,
};
}
// If the model complied, `text` should be JSON.
let parsed;
try {
parsed = JSON.parse(text);
} catch {
parsed = null;
}
if (jsonOut) {
if (parsed) {
console.log(JSON.stringify(normalizeParsed(parsed), null, 2));
} else {
// fallback: pass through
console.log(text.trim());
}
if (rawOut) {
console.error("\n--- RAW RESPONSE (debug) ---\n");
console.error(JSON.stringify(data, null, 2));
}
process.exit(0);
}
if (!parsed) {
console.log(text.trim());
if (rawOut) console.error(JSON.stringify(data, null, 2));
process.exit(0);
}
const normalized = normalizeParsed(parsed);
const citations = normalized.citations;
const results = normalized.results;
if (format === "links") {
for (const c of citations) console.log(c);
process.exit(0);
}
// Pretty, human output for terminal usage.
const lines = [];
lines.push(`Query: ${normalized.query}`);
lines.push(`Mode: ${normalized.mode}`);
lines.push("");
if (results.length) {
lines.push("Results:");
for (const r of results) {
const title = r?.title || (r?.author ? String(r.author) : "(no title)");
const url = r?.url || "";
const snip = r?.snippet || "";
const when = r?.posted_at ? `\n ${r.posted_at}` : "";
lines.push(`- ${title}${when}${url ? `\n ${url}` : ""}${snip ? `\n ${snip}` : ""}`);
}
} else {
lines.push("Results: (none)");
}
if (format !== "text" && citations.length) {
lines.push("");
lines.push("Citations:");
for (const c of citations) lines.push(`- ${c}`);
}
console.log(lines.join("\n"));
if (rawOut) {
console.error("\n--- RAW RESPONSE (debug) ---\n");
console.error(JSON.stringify(data, null, 2));
}
#!/usr/bin/env node
/**
* models.mjs
*
* List available xAI models.
*
* Examples:
* node {baseDir}/scripts/models.mjs
* node {baseDir}/scripts/models.mjs --json
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
function usage(msg) {
if (msg) console.error(msg);
console.error("Usage: models.mjs [--json] [--raw]");
process.exit(2);
}
function readKeyFromClawdbotConfig() {
try {
const p = path.join(os.homedir(), ".clawdbot", "clawdbot.json");
const raw = fs.readFileSync(p, "utf8");
const j = JSON.parse(raw);
return (
process.env.XAI_API_KEY ||
j?.env?.XAI_API_KEY ||
j?.env?.vars?.XAI_API_KEY ||
j?.skills?.entries?.["grok-search"]?.apiKey ||
j?.skills?.entries?.xai?.apiKey ||
null
);
} catch {
return process.env.XAI_API_KEY || null;
}
}
const args = process.argv.slice(2);
let jsonOut = false;
let rawOut = false;
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === "--json") jsonOut = true;
else if (a === "--raw") rawOut = true;
else if (a.startsWith("-")) usage(`Unknown flag: ${a}`);
}
const apiKey = readKeyFromClawdbotConfig();
if (!apiKey) {
console.error("Missing XAI_API_KEY.");
process.exit(1);
}
async function fetchJson(url) {
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const t = await res.text();
if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${t.slice(0, 400)}`);
return JSON.parse(t);
}
let data;
try {
data = await fetchJson("https://api.x.ai/v1/language-models");
} catch {
data = await fetchJson("https://api.x.ai/v1/models");
}
if (jsonOut) {
console.log(JSON.stringify(data, null, 2));
process.exit(0);
}
const rows = [];
if (Array.isArray(data?.data)) {
for (const m of data.data) rows.push({ id: m.id, owned_by: m.owned_by, object: m.object });
} else if (Array.isArray(data)) {
for (const m of data) rows.push({ id: m.id, owned_by: m.owned_by, object: m.object });
} else if (Array.isArray(data?.models)) {
for (const m of data.models) rows.push({ id: m.id ?? m.model ?? m.name, owned_by: m.owned_by, object: m.object });
}
rows.sort((a, b) => String(a.id).localeCompare(String(b.id)));
for (const r of rows) console.log(r.id);
if (rawOut) {
console.error("\n--- RAW ---\n");
console.error(JSON.stringify(data, null, 2));
}
#!/usr/bin/env node
/**
* selftest.mjs
*
* Lightweight sanity tests for grok-search scripts.
* Makes a handful of real API calls.
*/
import { spawn } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function runNode(args, { timeoutMs = 180000 } = {}) {
return new Promise((resolve) => {
const p = spawn(process.execPath, args, {
cwd: __dirname,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
p.stdout.on("data", (d) => (stdout += d.toString("utf8")));
p.stderr.on("data", (d) => (stderr += d.toString("utf8")));
const t = setTimeout(() => {
p.kill("SIGKILL");
resolve({ code: 124, stdout, stderr: stderr + "\n(timeout)" });
}, timeoutMs);
p.on("close", (code) => {
clearTimeout(t);
resolve({ code, stdout, stderr });
});
});
}
function assert(cond, msg) {
if (!cond) throw new Error(msg);
}
function assertJsonShape(obj, { mode, max }) {
assert(obj && typeof obj === "object", "output is not an object");
assert(typeof obj.query === "string", "missing query string");
assert(obj.mode === mode, `mode mismatch: expected ${mode}, got ${obj.mode}`);
assert(Array.isArray(obj.results), "results is not an array");
assert(Array.isArray(obj.citations), "citations is not an array");
assert(obj.results.length <= max, `results length > max (${obj.results.length} > ${max})`);
// citations unique
const set = new Set(obj.citations);
assert(set.size === obj.citations.length, "citations contain duplicates");
}
async function testJson({ name, argv, mode, max }) {
const { code, stdout, stderr } = await runNode(argv);
assert(code === 0, `${name}: non-zero exit code ${code}\nstderr: ${stderr}`);
let parsed;
try {
parsed = JSON.parse(stdout);
} catch (e) {
throw new Error(`${name}: stdout is not valid JSON\nstdout: ${stdout.slice(0, 400)}\nstderr: ${stderr}`);
}
assertJsonShape(parsed, { mode, max });
// For X searches, citations should (usually) be X links when present.
if (mode === "x" && parsed.citations.length) {
const hasX = parsed.citations.some((u) => /https?:\/\/(x\.com|twitter\.com)\//i.test(u));
assert(hasX, `${name}: expected at least one X link citation`);
}
return { stdout, stderr, parsed };
}
async function testLinksOnly({ name, argv }) {
const { code, stdout, stderr } = await runNode(argv);
assert(code === 0, `${name}: non-zero exit code ${code}\nstderr: ${stderr}`);
const lines = stdout
.split(/\r?\n/)
.map((l) => l.trim())
.filter(Boolean);
assert(lines.length > 0, `${name}: expected some links on stdout`);
for (const l of lines) assert(/^https?:\/\//.test(l), `${name}: non-url line: ${l}`);
return { stdout, stderr };
}
async function testText({ name, argv }) {
const { code, stdout, stderr } = await runNode(argv);
assert(code === 0, `${name}: non-zero exit code ${code}\nstderr: ${stderr}`);
assert(stdout.includes("Results:"), `${name}: expected pretty output with Results:`);
return { stdout, stderr };
}
async function testModels() {
const { code, stdout, stderr } = await runNode(["./models.mjs"]);
assert(code === 0, `models: non-zero exit code ${code}\nstderr: ${stderr}`);
const lines = stdout
.split(/\r?\n/)
.map((l) => l.trim())
.filter(Boolean);
assert(lines.length > 0, "models: expected some model ids");
assert(lines.some((l) => l.includes("grok")), "models: expected at least one grok* model");
}
async function testChat() {
const { code, stdout, stderr } = await runNode(["./chat.mjs", "say", "hi", "in", "one", "word"]);
assert(code === 0, `chat: non-zero exit code ${code}\nstderr: ${stderr}`);
assert(stdout.trim().length > 0, "chat: expected non-empty response");
}
async function main() {
const tests = [];
// Search JSON (default)
tests.push(
testJson({
name: "web-json",
argv: ["./grok_search.mjs", "clawdbot", "--web", "--max", "3"],
mode: "web",
max: 3,
})
);
tests.push(
testJson({
name: "x-json-days-handles",
argv: [
"./grok_search.mjs",
"clawdbot",
"--x",
"--days",
"14",
"--handles",
"@clawdbot",
"--max",
"3",
],
mode: "x",
max: 3,
})
);
// Links only
tests.push(
testLinksOnly({
name: "x-links-only",
argv: ["./grok_search.mjs", "clawdbot", "--x", "--days", "30", "--links-only"],
})
);
// Pretty text output
tests.push(
testText({
name: "web-pretty-text",
argv: ["./grok_search.mjs", "xai", "search", "tools", "--web", "--text", "--max", "3"],
})
);
// Debug raw (ensure it doesn't break JSON)
tests.push(
testJson({
name: "x-json-raw",
argv: ["./grok_search.mjs", "clawdbot", "--x", "--days", "7", "--max", "2", "--raw"],
mode: "x",
max: 2,
})
);
// Chat + models
tests.push(testChat());
tests.push(testModels());
// Run sequentially (avoid hammering API)
for (const t of tests) {
// eslint-disable-next-line no-await-in-loop
await t;
}
console.log("OK: grok-search selftest passed");
}
main().catch((e) => {
console.error("SELFTEST FAILED:");
console.error(e?.stack || String(e));
process.exit(1);
});
Related skills
AI & Agent Buildingagents