- Add code-interpreter, kokoro-tts, remotion-best-practices, research-to-paper-slides, summarize, tavily-tool to source repo - skill-review: add main/xiaoming agent mapping in handler.ts + SKILL.md - tts-voice: handler.ts updates from agent workspace Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
162 lines
4.4 KiB
JavaScript
162 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Tavily Search CLI
|
|
*
|
|
* - Reads TAVILY_API_KEY from env only.
|
|
* - Prints full JSON response to stdout.
|
|
* - Prints a simple list of URLs to stderr by default (can be disabled).
|
|
*/
|
|
|
|
const TAVILY_ENDPOINT = 'https://api.tavily.com/search';
|
|
|
|
function usage(msg) {
|
|
if (msg) console.error(`Error: ${msg}\n`);
|
|
console.error(`Usage:
|
|
tavily_search.js --query "..." [--max_results 5] [--include_domains a.com,b.com] [--exclude_domains x.com,y.com]
|
|
|
|
Options:
|
|
--query, -q Search query (required)
|
|
--max_results, -n Max results (default: 5; clamped to 0..20)
|
|
--include_domains Comma-separated domains to include
|
|
--exclude_domains Comma-separated domains to exclude
|
|
--urls-stderr Print URL list to stderr (default: true)
|
|
--no-urls-stderr Disable URL list to stderr
|
|
--urls-only Print URLs (one per line) to stdout instead of JSON
|
|
--help, -h Show help
|
|
|
|
Env:
|
|
TAVILY_API_KEY (required) Tavily API key
|
|
|
|
Exit codes:
|
|
0 success
|
|
2 usage / missing required inputs
|
|
3 network / HTTP error
|
|
4 invalid JSON response
|
|
`);
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const out = {
|
|
query: null,
|
|
max_results: 5,
|
|
include_domains: null,
|
|
exclude_domains: null,
|
|
urls_stderr: true,
|
|
urls_only: false,
|
|
help: false,
|
|
};
|
|
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--help' || a === '-h') out.help = true;
|
|
else if (a === '--query' || a === '-q') out.query = argv[++i];
|
|
else if (a === '--max_results' || a === '-n') out.max_results = Number(argv[++i]);
|
|
else if (a === '--include_domains') out.include_domains = argv[++i];
|
|
else if (a === '--exclude_domains') out.exclude_domains = argv[++i];
|
|
else if (a === '--urls-stderr') out.urls_stderr = true;
|
|
else if (a === '--no-urls-stderr') out.urls_stderr = false;
|
|
else if (a === '--urls-only') out.urls_only = true;
|
|
else return { error: `Unknown arg: ${a}` };
|
|
}
|
|
|
|
if (Number.isNaN(out.max_results) || !Number.isFinite(out.max_results)) {
|
|
return { error: `--max_results must be a number` };
|
|
}
|
|
// Tavily allows 0..20; clamp to stay in range.
|
|
out.max_results = Math.max(0, Math.min(20, Math.trunc(out.max_results)));
|
|
|
|
const csvToArray = (s) => {
|
|
if (!s) return null;
|
|
const arr = s.split(',').map(x => x.trim()).filter(Boolean);
|
|
return arr.length ? arr : null;
|
|
};
|
|
|
|
out.include_domains = csvToArray(out.include_domains);
|
|
out.exclude_domains = csvToArray(out.exclude_domains);
|
|
|
|
return out;
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.error) {
|
|
usage(args.error);
|
|
process.exit(2);
|
|
}
|
|
if (args.help) {
|
|
usage();
|
|
process.exit(0);
|
|
}
|
|
|
|
const apiKey = process.env.TAVILY_API_KEY;
|
|
if (!apiKey) {
|
|
usage('TAVILY_API_KEY env var is required');
|
|
process.exit(2);
|
|
}
|
|
|
|
if (!args.query) {
|
|
usage('--query is required');
|
|
process.exit(2);
|
|
}
|
|
|
|
const payload = {
|
|
query: args.query,
|
|
max_results: args.max_results,
|
|
};
|
|
if (args.include_domains) payload.include_domains = args.include_domains;
|
|
if (args.exclude_domains) payload.exclude_domains = args.exclude_domains;
|
|
|
|
let res;
|
|
try {
|
|
res = await fetch(TAVILY_ENDPOINT, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${apiKey}`,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
} catch (e) {
|
|
console.error(`Network error calling Tavily: ${e?.message || String(e)}`);
|
|
process.exit(3);
|
|
}
|
|
|
|
if (!res.ok) {
|
|
let bodyText = '';
|
|
try { bodyText = await res.text(); } catch {}
|
|
console.error(`Tavily HTTP error: ${res.status} ${res.statusText}`);
|
|
if (bodyText) console.error(bodyText);
|
|
process.exit(3);
|
|
}
|
|
|
|
let data;
|
|
try {
|
|
data = await res.json();
|
|
} catch (e) {
|
|
console.error(`Invalid JSON response from Tavily: ${e?.message || String(e)}`);
|
|
process.exit(4);
|
|
}
|
|
|
|
const urls = Array.isArray(data?.results)
|
|
? data.results.map(r => r?.url).filter(Boolean)
|
|
: [];
|
|
|
|
if (args.urls_only) {
|
|
for (const u of urls) process.stdout.write(`${u}\n`);
|
|
process.exit(0);
|
|
}
|
|
|
|
process.stdout.write(JSON.stringify(data, null, 2));
|
|
process.stdout.write('\n');
|
|
|
|
if (args.urls_stderr && urls.length) {
|
|
console.error('\nURLs:');
|
|
for (const u of urls) console.error(u);
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(`Unexpected error: ${e?.stack || e?.message || String(e)}`);
|
|
process.exit(1);
|
|
});
|