add exa research search, use category in search card title, update readme

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Arjun 2026-02-14 14:07:24 +05:30
parent d1a2446cb3
commit 8ef538b8c8
5 changed files with 141 additions and 4 deletions

View file

@ -164,6 +164,7 @@ When a user asks for ANY task that might require external capabilities (web sear
- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\`, \`executeMcpTool\` - MCP server management and execution
- \`loadSkill\` - Skill loading
- \`slack-checkConnection\`, \`slack-listAvailableTools\`, \`slack-executeAction\` - Slack integration (requires Slack to be connected via Composio). Use \`slack-listAvailableTools\` first to discover available tool slugs, then \`slack-executeAction\` to execute them.
- \`web-search\` and \`research-search\` - Web and research search tools (available when configured). **You MUST load the \`web-search\` skill before using either of these tools.** It tells you which tool to pick and how many searches to do.
**Prefer these tools whenever possible** they work instantly with zero friction. For file operations inside \`~/.rowboat/\`, always use these instead of \`executeCommand\`.

View file

@ -1364,4 +1364,117 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
}
},
},
// ============================================================================
// Research Search (Exa Search API)
// ============================================================================
'research-search': {
description: 'Use this for finding articles, blog posts, papers, companies, people, or exploring a topic in depth. Best for discovery and research where you need quality sources, not a quick fact.',
inputSchema: z.object({
query: z.string().describe('The search query'),
numResults: z.number().optional().describe('Number of results to return (default: 5, max: 20)'),
category: z.enum(['company', 'research paper', 'news', 'tweet', 'personal site', 'financial report', 'people']).optional().describe('Filter results by category'),
}),
isAvailable: async () => {
try {
const homedir = process.env.HOME || process.env.USERPROFILE || '';
const exaConfigPath = path.join(homedir, '.rowboat', 'config', 'exa-search.json');
const raw = await fs.readFile(exaConfigPath, 'utf8');
const config = JSON.parse(raw);
return !!config.apiKey;
} catch {
return false;
}
},
execute: async ({ query, numResults, category }: { query: string; numResults?: number; category?: string }) => {
try {
const homedir = process.env.HOME || process.env.USERPROFILE || '';
const exaConfigPath = path.join(homedir, '.rowboat', 'config', 'exa-search.json');
let apiKey: string;
try {
const raw = await fs.readFile(exaConfigPath, 'utf8');
const config = JSON.parse(raw);
apiKey = config.apiKey;
} catch {
return {
success: false,
error: 'Exa Search API key not configured. Create ~/.rowboat/config/exa-search.json with { "apiKey": "<your-key>" }',
};
}
if (!apiKey) {
return {
success: false,
error: 'Exa Search API key is empty. Set "apiKey" in ~/.rowboat/config/exa-search.json',
};
}
const resultCount = Math.min(Math.max(numResults || 5, 1), 20);
const body: Record<string, unknown> = {
query,
numResults: resultCount,
type: 'auto',
contents: {
text: { maxCharacters: 1000 },
highlights: true,
},
};
if (category) {
body.category = category;
}
const response = await fetch('https://api.exa.ai/search', {
method: 'POST',
headers: {
'x-api-key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text();
return {
success: false,
error: `Exa Search API error (${response.status}): ${text}`,
};
}
const data = await response.json() as {
results?: Array<{
title?: string;
url?: string;
publishedDate?: string;
author?: string;
highlights?: string[];
text?: string;
}>;
};
const results = (data.results || []).map((r) => ({
title: r.title || '',
url: r.url || '',
publishedDate: r.publishedDate || '',
author: r.author || '',
highlights: r.highlights || [],
text: r.text || '',
}));
return {
success: true,
query,
results,
count: results.length,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
},
},
};