8cc0dcb167
- Moved 4 skills: browser-testing, claude-multimedia, exa-search, gravl-research - Moved 14 agents: architect, backend-dev, browser-tester, coach, data, flight, frontend-dev, gravl-pm, gravl-researcher, nutritionist, research, reviewer, staging, update - Created symlinks from ~/clawd/skills and ~/clawd/agents back to hub - Single source of truth in claude-agents-skills repo
222 lines
6.1 KiB
JavaScript
Executable File
222 lines
6.1 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Exa Search CLI - Wrapper for Exa MCP Server
|
|
*
|
|
* Usage:
|
|
* ./exa-cli.mjs search "query"
|
|
* ./exa-cli.mjs code "Python async patterns"
|
|
* ./exa-cli.mjs company "Anthropic"
|
|
* ./exa-cli.mjs people "CTO AI startups"
|
|
* ./exa-cli.mjs crawl "https://example.com"
|
|
* ./exa-cli.mjs research "topic for deep research"
|
|
* ./exa-cli.mjs research-check <taskId>
|
|
*/
|
|
|
|
const MCP_BASE = 'https://mcp.exa.ai/mcp';
|
|
const TOOLS_PARAM = 'tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check';
|
|
|
|
function getMcpUrl() {
|
|
const apiKey = process.env.EXA_API_KEY;
|
|
let url = `${MCP_BASE}?${TOOLS_PARAM}`;
|
|
if (apiKey) {
|
|
url += `&exaApiKey=${apiKey}`;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
async function callMcp(toolName, args) {
|
|
const url = getMcpUrl();
|
|
|
|
const request = {
|
|
jsonrpc: '2.0',
|
|
id: Date.now(),
|
|
method: 'tools/call',
|
|
params: {
|
|
name: toolName,
|
|
arguments: args
|
|
}
|
|
};
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json, text/event-stream',
|
|
},
|
|
body: JSON.stringify(request)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') || '';
|
|
|
|
// Handle SSE streaming response
|
|
if (contentType.includes('text/event-stream')) {
|
|
const text = await response.text();
|
|
const lines = text.split('\n');
|
|
let result = null;
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('data: ')) {
|
|
const data = line.slice(6);
|
|
if (data && data !== '[DONE]') {
|
|
try {
|
|
const parsed = JSON.parse(data);
|
|
if (parsed.result) {
|
|
result = parsed.result;
|
|
} else if (parsed.error) {
|
|
throw new Error(`MCP Error: ${JSON.stringify(parsed.error)}`);
|
|
}
|
|
} catch (e) {
|
|
// Skip non-JSON lines
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Handle regular JSON response
|
|
const result = await response.json();
|
|
|
|
if (result.error) {
|
|
throw new Error(`MCP Error: ${JSON.stringify(result.error)}`);
|
|
}
|
|
|
|
return result.result;
|
|
}
|
|
|
|
function parseArgs(args) {
|
|
const parsed = { _: [] };
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg.startsWith('--')) {
|
|
const key = arg.slice(2);
|
|
const next = args[i + 1];
|
|
if (next && !next.startsWith('--')) {
|
|
parsed[key] = next;
|
|
i++;
|
|
} else {
|
|
parsed[key] = true;
|
|
}
|
|
} else {
|
|
parsed._.push(arg);
|
|
}
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function formatContent(content) {
|
|
if (!content) return '';
|
|
if (Array.isArray(content)) {
|
|
return content.map(c => c.text || JSON.stringify(c)).join('\n');
|
|
}
|
|
if (typeof content === 'string') return content;
|
|
return JSON.stringify(content, null, 2);
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const command = args._[0];
|
|
const query = args._.slice(1).join(' ');
|
|
const numResults = parseInt(args.num) || 10;
|
|
|
|
if (!command) {
|
|
console.log(`Exa Search CLI
|
|
|
|
Commands:
|
|
search <query> Web search
|
|
search-advanced <query> Advanced search with filters
|
|
code <query> Code/documentation search
|
|
crawl <url> Get full page content
|
|
company <name> Company research
|
|
people <query> People/profile search
|
|
research <topic> Start deep research
|
|
research-check <taskId> Check research status
|
|
|
|
Options:
|
|
--num <n> Number of results (default: 10)
|
|
--domains <list> Comma-separated domains (advanced search)
|
|
--after <date> Results after date YYYY-MM-DD (advanced search)
|
|
--before <date> Results before date YYYY-MM-DD (advanced search)
|
|
--tokens <n> Token limit for code search (default: 5000)
|
|
|
|
Environment:
|
|
EXA_API_KEY Your Exa API key (optional, for higher rate limits)
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
|
|
try {
|
|
let result;
|
|
|
|
switch (command) {
|
|
case 'search':
|
|
if (!query) throw new Error('Query required');
|
|
result = await callMcp('web_search_exa', { query, numResults });
|
|
break;
|
|
|
|
case 'search-advanced':
|
|
if (!query) throw new Error('Query required');
|
|
const advancedArgs = { query, numResults };
|
|
if (args.domains) {
|
|
advancedArgs.includeDomains = args.domains.split(',');
|
|
}
|
|
if (args.after) {
|
|
advancedArgs.startPublishedDate = args.after;
|
|
}
|
|
if (args.before) {
|
|
advancedArgs.endPublishedDate = args.before;
|
|
}
|
|
result = await callMcp('web_search_advanced_exa', advancedArgs);
|
|
break;
|
|
|
|
case 'code':
|
|
if (!query) throw new Error('Query required');
|
|
const tokensNum = parseInt(args.tokens) || 5000;
|
|
result = await callMcp('get_code_context_exa', { query, tokensNum });
|
|
break;
|
|
|
|
case 'crawl':
|
|
if (!query) throw new Error('URL required');
|
|
result = await callMcp('crawling_exa', { url: query });
|
|
break;
|
|
|
|
case 'company':
|
|
if (!query) throw new Error('Company name required');
|
|
result = await callMcp('company_research_exa', { companyName: query });
|
|
break;
|
|
|
|
case 'people':
|
|
if (!query) throw new Error('Query required');
|
|
result = await callMcp('people_search_exa', { query, numResults });
|
|
break;
|
|
|
|
case 'research':
|
|
if (!query) throw new Error('Research topic required');
|
|
result = await callMcp('deep_researcher_start', { query });
|
|
break;
|
|
|
|
case 'research-check':
|
|
if (!query) throw new Error('Task ID required');
|
|
result = await callMcp('deep_researcher_check', { taskId: query });
|
|
break;
|
|
|
|
default:
|
|
console.error(`Unknown command: ${command}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(formatContent(result?.content || result));
|
|
|
|
} catch (error) {
|
|
console.error(`Error: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|