Initial commit with translated description
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
72
README.md
Normal file
72
README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# xAI / Grok Skill for Clawdbot
|
||||
|
||||
Chat with xAI's Grok models from Clawdbot. Supports text chat, vision, and all Grok models including Grok-4.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
clawdhub install xai
|
||||
# or
|
||||
cd ~/clawd/skills && git clone https://github.com/mvanhorn/clawdbot-skill-xai xai
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
Get your API key from [console.x.ai](https://console.x.ai), then:
|
||||
|
||||
```bash
|
||||
clawdbot config set skills.entries.xai.apiKey "xai-YOUR-KEY"
|
||||
```
|
||||
|
||||
Or set environment variable:
|
||||
```bash
|
||||
export XAI_API_KEY="xai-YOUR-KEY"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Chat with Grok
|
||||
```bash
|
||||
node scripts/chat.js "What is the meaning of life?"
|
||||
```
|
||||
|
||||
### Use specific model
|
||||
```bash
|
||||
node scripts/chat.js --model grok-4-0709 "Complex question here"
|
||||
node scripts/chat.js --model grok-3-mini "Quick question"
|
||||
```
|
||||
|
||||
### Vision (analyze images)
|
||||
```bash
|
||||
node scripts/chat.js --image photo.jpg "What's in this image?"
|
||||
```
|
||||
|
||||
### System prompts
|
||||
```bash
|
||||
node scripts/chat.js --system "You are a pirate" "Tell me about ships"
|
||||
```
|
||||
|
||||
### List available models
|
||||
```bash
|
||||
node scripts/models.js
|
||||
```
|
||||
|
||||
## Available Models
|
||||
|
||||
- `grok-3` - Capable general model
|
||||
- `grok-3-mini` - Fast and efficient
|
||||
- `grok-4-0709` - Latest Grok 4
|
||||
- `grok-4-fast-reasoning` - Fast with reasoning
|
||||
- `grok-2-vision-1212` - Image understanding
|
||||
- `grok-2-image-1212` - Image generation
|
||||
|
||||
## Clawdbot Integration
|
||||
|
||||
Once installed, you can just say:
|
||||
- "Ask Grok about [topic]"
|
||||
- "Use Grok to analyze this image"
|
||||
- "Have Grok 4 explain [concept]"
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
108
SKILL.md
Normal file
108
SKILL.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
name: xai
|
||||
description: "通过xAI API与Grok模型聊天。支持Grok-3、Grok-3-mini、视觉等。"
|
||||
homepage: https://docs.x.ai
|
||||
user-invocable: true
|
||||
disable-model-invocation: true
|
||||
triggers:
|
||||
- grok
|
||||
- xai
|
||||
- ask grok
|
||||
metadata:
|
||||
clawdbot:
|
||||
emoji: "🤖"
|
||||
primaryEnv: XAI_API_KEY
|
||||
requires:
|
||||
bins: [node]
|
||||
env: [XAI_API_KEY]
|
||||
---
|
||||
|
||||
# xAI / Grok
|
||||
|
||||
Chat with xAI's Grok models. Supports text and vision.
|
||||
|
||||
## Setup
|
||||
|
||||
[INSTALLATION AND USE INSTRUCTIONS](https://claude.ai/public/artifacts/5bb1d4ca-68ef-4d74-900c-d14fefa5c094)
|
||||
|
||||
```bash
|
||||
export XAI_API_KEY="xai-YOUR-KEY"
|
||||
```
|
||||
|
||||
Get your API key at: https://console.x.ai
|
||||
|
||||
## Commands
|
||||
|
||||
### Chat with Grok
|
||||
```bash
|
||||
node {baseDir}/scripts/chat.js "What is the meaning of life?"
|
||||
```
|
||||
|
||||
### Use a specific model
|
||||
```bash
|
||||
node {baseDir}/scripts/chat.js --model grok-3-mini "Quick question: 2+2?"
|
||||
```
|
||||
|
||||
### Vision (analyze images)
|
||||
```bash
|
||||
node {baseDir}/scripts/chat.js --image /path/to/image.jpg "What's in this image?"
|
||||
```
|
||||
|
||||
### 🔍 Search X/Twitter (Real-time!)
|
||||
```bash
|
||||
node {baseDir}/scripts/search-x.js "Remotion video framework"
|
||||
node {baseDir}/scripts/search-x.js --days 7 "Claude AI tips"
|
||||
node {baseDir}/scripts/search-x.js --handles @remotion_dev "updates"
|
||||
```
|
||||
|
||||
Uses xAI Responses API with x_search tool for real X posts with citations.
|
||||
|
||||
### List available models
|
||||
```bash
|
||||
node {baseDir}/scripts/models.js
|
||||
```
|
||||
|
||||
## Available Models
|
||||
|
||||
- `grok-3` - Most capable, best for complex tasks
|
||||
- `grok-3-mini` - Fast and efficient
|
||||
- `grok-3-fast` - Optimized for speed
|
||||
- `grok-2-vision-1212` - Vision model for image understanding
|
||||
|
||||
## Example Usage
|
||||
|
||||
**User:** "Ask Grok what it thinks about AI safety"
|
||||
**Action:** Run chat.js with the prompt
|
||||
|
||||
**User:** "Use Grok to analyze this image" (with attached image)
|
||||
**Action:** Run chat.js with --image flag
|
||||
|
||||
**User:** "What Grok models are available?"
|
||||
**Action:** Run models.js
|
||||
|
||||
## API Reference
|
||||
|
||||
xAI API Docs: https://docs.x.ai/api
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `XAI_API_KEY` - Your xAI API key (required)
|
||||
- `XAI_MODEL` - Default model (optional, defaults to grok-3)
|
||||
|
||||
## Security & Permissions
|
||||
|
||||
**What this skill does:**
|
||||
- Sends chat prompts to xAI's API at `api.x.ai`
|
||||
- Vision mode sends images to xAI for analysis
|
||||
- `scripts/models.js` lists available models (read-only)
|
||||
|
||||
**What this skill does NOT do:**
|
||||
- Does not read arbitrary local files — `--image` only accepts files with image extensions (`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`)
|
||||
- Does not read config files or access the filesystem beyond the specified image path
|
||||
- Does not store conversation history or logs
|
||||
- Does not send credentials to any endpoint other than `api.x.ai`
|
||||
- Cannot be invoked autonomously by the agent (`disable-model-invocation: true`)
|
||||
|
||||
**Bundled scripts:** `scripts/chat.js` (chat), `scripts/models.js` (list models), `scripts/search-x.js` (X search)
|
||||
|
||||
Review scripts before first use to verify behavior.
|
||||
6
_meta.json
Normal file
6
_meta.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7fstfjar4k2jzf4a39jb64qd819q1p",
|
||||
"slug": "x-ai",
|
||||
"version": "1.0.2",
|
||||
"publishedAt": 1771690964816
|
||||
}
|
||||
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "clawdbot-skill-xai",
|
||||
"version": "1.0.0",
|
||||
"description": "xAI/Grok integration for Clawdbot",
|
||||
"keywords": ["clawdbot-skill", "xai", "grok", "ai", "chat"],
|
||||
"author": "Lobster",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"chat": "node scripts/chat.js",
|
||||
"models": "node scripts/models.js"
|
||||
}
|
||||
}
|
||||
240
scripts/chat.js
Normal file
240
scripts/chat.js
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* xAI/Grok Chat Script
|
||||
*
|
||||
* Usage:
|
||||
* node chat.js "Your prompt here"
|
||||
* node chat.js --model grok-3-mini "Quick question"
|
||||
* node chat.js --image /path/to/image.jpg "Describe this"
|
||||
* node chat.js --system "You are a pirate" "Tell me about ships"
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const API_BASE = 'api.x.ai';
|
||||
const DEFAULT_MODEL = 'grok-3';
|
||||
|
||||
function getApiKey() {
|
||||
return process.env.XAI_API_KEY || null;
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const result = {
|
||||
model: process.env.XAI_MODEL || DEFAULT_MODEL,
|
||||
system: null,
|
||||
image: null,
|
||||
prompt: '',
|
||||
stream: false,
|
||||
json: false,
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
while (i < args.length) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '--model' || arg === '-m') {
|
||||
result.model = args[++i];
|
||||
} else if (arg === '--system' || arg === '-s') {
|
||||
result.system = args[++i];
|
||||
} else if (arg === '--image' || arg === '-i') {
|
||||
result.image = args[++i];
|
||||
} else if (arg === '--stream') {
|
||||
result.stream = true;
|
||||
} else if (arg === '--json' || arg === '-j') {
|
||||
result.json = true;
|
||||
} else if (!arg.startsWith('-')) {
|
||||
result.prompt = args.slice(i).join(' ');
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function imageToBase64(imagePath) {
|
||||
const absolutePath = path.resolve(imagePath);
|
||||
|
||||
// Validate file extension is an allowed image type
|
||||
const ext = path.extname(absolutePath).toLowerCase();
|
||||
const mimeTypes = {
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
};
|
||||
|
||||
if (!mimeTypes[ext]) {
|
||||
throw new Error(`Unsupported image format: ${ext}. Allowed: ${Object.keys(mimeTypes).join(', ')}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
throw new Error(`Image not found: ${absolutePath}`);
|
||||
}
|
||||
|
||||
const imageData = fs.readFileSync(absolutePath);
|
||||
const base64 = imageData.toString('base64');
|
||||
const mimeType = mimeTypes[ext];
|
||||
|
||||
return `data:${mimeType};base64,${base64}`;
|
||||
}
|
||||
|
||||
async function chat(options) {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
console.error('❌ No API key found. Set XAI_API_KEY or configure in clawdbot.');
|
||||
console.error(' Get your key at: https://console.x.ai');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build messages
|
||||
const messages = [];
|
||||
|
||||
if (options.system) {
|
||||
messages.push({ role: 'system', content: options.system });
|
||||
}
|
||||
|
||||
// Build user message content
|
||||
let userContent;
|
||||
if (options.image) {
|
||||
// Vision request
|
||||
const imageUrl = imageToBase64(options.image);
|
||||
userContent = [
|
||||
{ type: 'image_url', image_url: { url: imageUrl } },
|
||||
{ type: 'text', text: options.prompt || 'Describe this image.' },
|
||||
];
|
||||
// Use vision model if not specified
|
||||
if (options.model === DEFAULT_MODEL) {
|
||||
options.model = 'grok-2-vision-1212';
|
||||
}
|
||||
} else {
|
||||
userContent = options.prompt;
|
||||
}
|
||||
|
||||
messages.push({ role: 'user', content: userContent });
|
||||
|
||||
const payload = {
|
||||
model: options.model,
|
||||
messages: messages,
|
||||
stream: options.stream,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request({
|
||||
hostname: API_BASE,
|
||||
path: '/v1/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', chunk => {
|
||||
data += chunk;
|
||||
|
||||
if (options.stream) {
|
||||
// Handle streaming responses
|
||||
const lines = data.split('\n');
|
||||
data = lines.pop(); // Keep incomplete line
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
|
||||
try {
|
||||
const json = JSON.parse(line.slice(6));
|
||||
const content = json.choices?.[0]?.delta?.content;
|
||||
if (content) {
|
||||
process.stdout.write(content);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors in stream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.error(`❌ API Error (${res.statusCode}):`, data);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!options.stream) {
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
const content = response.choices?.[0]?.message?.content;
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(response, null, 2));
|
||||
} else {
|
||||
console.log(content || '(no response)');
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
} catch (e) {
|
||||
console.error('❌ Failed to parse response:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log(); // Newline after streaming
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error('❌ Request failed:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.write(JSON.stringify(payload));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Main
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`
|
||||
🤖 xAI/Grok Chat
|
||||
|
||||
Usage:
|
||||
node chat.js [options] "Your prompt"
|
||||
|
||||
Options:
|
||||
--model, -m <model> Model to use (default: grok-3)
|
||||
--system, -s <text> System prompt
|
||||
--image, -i <path> Image for vision analysis
|
||||
--stream Stream the response
|
||||
--json, -j Output full JSON response
|
||||
--help, -h Show this help
|
||||
|
||||
Models:
|
||||
grok-3 Most capable
|
||||
grok-3-mini Fast and efficient
|
||||
grok-3-fast Speed optimized
|
||||
grok-2-vision-1212 Vision/image analysis
|
||||
|
||||
Examples:
|
||||
node chat.js "What is quantum computing?"
|
||||
node chat.js -m grok-3-mini "Quick: capital of France?"
|
||||
node chat.js -i photo.jpg "What's in this image?"
|
||||
node chat.js -s "You are a poet" "Write about the moon"
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const options = parseArgs(args);
|
||||
|
||||
if (!options.prompt && !options.image) {
|
||||
console.error('❌ Please provide a prompt');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
chat(options);
|
||||
74
scripts/models.js
Normal file
74
scripts/models.js
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* List available xAI models
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const API_BASE = 'api.x.ai';
|
||||
|
||||
function getApiKey() {
|
||||
return process.env.XAI_API_KEY || null;
|
||||
}
|
||||
|
||||
async function listModels() {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
console.error('❌ No API key found. Set XAI_API_KEY or configure in clawdbot.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request({
|
||||
hostname: API_BASE,
|
||||
path: '/v1/models',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.error(`❌ API Error (${res.statusCode}):`, data);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
const models = response.data || [];
|
||||
|
||||
console.log('🤖 Available xAI Models:\n');
|
||||
|
||||
for (const model of models) {
|
||||
console.log(` • ${model.id}`);
|
||||
if (model.description) {
|
||||
console.log(` ${model.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
console.log(' (no models available)');
|
||||
}
|
||||
|
||||
console.log('\nUsage: node chat.js --model <model-id> "Your prompt"');
|
||||
|
||||
resolve(models);
|
||||
} catch (e) {
|
||||
console.error('❌ Failed to parse response:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error('❌ Request failed:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
listModels();
|
||||
242
scripts/search-x.js
Normal file
242
scripts/search-x.js
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* xAI X Search Script - Uses Responses API with x_search tool
|
||||
*
|
||||
* Usage:
|
||||
* node search-x.js "Remotion best practices"
|
||||
* node search-x.js --days 30 "AI video creation"
|
||||
* node search-x.js --handles @remotion_dev,@jonnyburger "updates"
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const API_BASE = 'api.x.ai';
|
||||
const DEFAULT_MODEL = 'grok-4-1-fast'; // Optimized for agentic search
|
||||
|
||||
function getApiKey() {
|
||||
return process.env.XAI_API_KEY || null;
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const result = {
|
||||
model: DEFAULT_MODEL,
|
||||
query: '',
|
||||
days: 30,
|
||||
handles: [],
|
||||
excludeHandles: [],
|
||||
json: false,
|
||||
requireCitations: true,
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
while (i < args.length) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '--model' || arg === '-m') {
|
||||
result.model = args[++i];
|
||||
} else if (arg === '--days' || arg === '-d') {
|
||||
result.days = parseInt(args[++i], 10);
|
||||
} else if (arg === '--handles' || arg === '-h') {
|
||||
result.handles = args[++i].split(',').map(h => h.trim());
|
||||
} else if (arg === '--exclude') {
|
||||
result.excludeHandles = args[++i].split(',').map(h => h.trim());
|
||||
} else if (arg === '--json' || arg === '-j') {
|
||||
result.json = true;
|
||||
} else if (arg === '--no-require-citations') {
|
||||
result.requireCitations = false;
|
||||
} else if (!arg.startsWith('-')) {
|
||||
result.query = args.slice(i).join(' ');
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getDateRange(days) {
|
||||
const to = new Date();
|
||||
const from = new Date();
|
||||
from.setDate(from.getDate() - days);
|
||||
|
||||
return {
|
||||
from_date: from.toISOString().split('T')[0],
|
||||
to_date: to.toISOString().split('T')[0],
|
||||
};
|
||||
}
|
||||
|
||||
async function searchX(options) {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
console.error('❌ No API key found. Set XAI_API_KEY or configure in clawdbot.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dateRange = getDateRange(options.days);
|
||||
|
||||
// Build x_search tool config
|
||||
const xSearchTool = {
|
||||
type: 'x_search',
|
||||
x_search: {
|
||||
from_date: dateRange.from_date,
|
||||
to_date: dateRange.to_date,
|
||||
}
|
||||
};
|
||||
|
||||
if (options.handles.length > 0) {
|
||||
xSearchTool.x_search.allowed_x_handles = options.handles;
|
||||
}
|
||||
if (options.excludeHandles.length > 0) {
|
||||
xSearchTool.x_search.excluded_x_handles = options.excludeHandles;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: options.model,
|
||||
input: `Search X/Twitter and find real posts about: ${options.query}
|
||||
|
||||
Give me actual tweets with:
|
||||
- Username/handle
|
||||
- The actual tweet content
|
||||
- Date if available
|
||||
- Link to the tweet
|
||||
|
||||
Only include REAL posts you find. If you can't find any, say so.`,
|
||||
tools: [xSearchTool],
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request({
|
||||
hostname: API_BASE,
|
||||
path: '/v1/responses',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', chunk => data += chunk);
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.error(`❌ API Error (${res.statusCode}):`, data);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(response, null, 2));
|
||||
resolve(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract content from Responses API format
|
||||
let content = '(no response)';
|
||||
if (response.output) {
|
||||
for (const item of response.output) {
|
||||
if (item.type === 'message' && item.content) {
|
||||
for (const c of item.content) {
|
||||
if (c.type === 'output_text' && c.text) {
|
||||
content = c.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract citations from annotations
|
||||
let xCitations = [];
|
||||
if (response.output) {
|
||||
for (const item of response.output) {
|
||||
if (item.content) {
|
||||
for (const c of item.content) {
|
||||
if (c.annotations) {
|
||||
for (const ann of c.annotations) {
|
||||
if (ann.type === 'url_citation' && ann.url) {
|
||||
if (ann.url.includes('x.com') || ann.url.includes('twitter.com')) {
|
||||
xCitations.push(ann);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dedupe by URL
|
||||
xCitations = [...new Map(xCitations.map(c => [c.url, c])).values()];
|
||||
|
||||
if (options.requireCitations && xCitations.length === 0) {
|
||||
console.error('⚠️ No X/Twitter citations found in response.');
|
||||
console.error(' Grok may not have found relevant posts, or X search may not be enabled.');
|
||||
console.error('\nResponse anyway:\n');
|
||||
}
|
||||
|
||||
console.log(content);
|
||||
|
||||
if (xCitations.length > 0) {
|
||||
console.log('\n📎 Citations:');
|
||||
for (const cite of xCitations) {
|
||||
console.log(` ${cite.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
} catch (e) {
|
||||
console.error('❌ Failed to parse response:', e.message);
|
||||
console.error('Raw:', data.slice(0, 500));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error('❌ Request failed:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.write(JSON.stringify(payload));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Main
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes('--help')) {
|
||||
console.log(`
|
||||
🔍 xAI X Search (Responses API)
|
||||
|
||||
Usage:
|
||||
node search-x.js [options] "Your search query"
|
||||
|
||||
Options:
|
||||
--model, -m <model> Model (default: grok-4-1-fast)
|
||||
--days, -d <n> Search last N days (default: 30)
|
||||
--handles <list> Only search these handles (comma-separated)
|
||||
--exclude <list> Exclude these handles (comma-separated)
|
||||
--json, -j Output full JSON response
|
||||
--no-require-citations Don't warn if no X citations found
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
node search-x.js "Remotion video framework tips"
|
||||
node search-x.js --days 7 "Claude AI"
|
||||
node search-x.js --handles @remotion_dev "new features"
|
||||
node search-x.js --days 30 "AI video creation best practices"
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const options = parseArgs(args);
|
||||
|
||||
if (!options.query) {
|
||||
console.error('❌ Please provide a search query');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.error(`🔍 Searching X for: "${options.query}" (last ${options.days} days)...\n`);
|
||||
searchX(options);
|
||||
Reference in New Issue
Block a user