Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "agent-search"
version = "0.9.0"
edition = "2021"
description = "Unified multi-provider search CLI for AI agents — 13 providers, 13 modes, email verification, one binary"
description = "Unified multi-provider search CLI for AI agents — 14 providers, 13 modes, email verification, one binary"
license = "MIT"
repository = "https://github.com/paperfoot/search-cli"
homepage = "https://github.com/paperfoot/search-cli"
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Search CLI — Web Search for AI Agents

**One binary, 13 providers, 13 modes, rank-fused results. The web search tool your AI agent is missing.**
**One binary, 14 providers, 13 modes, rank-fused results. The web search tool your AI agent is missing.**

<br />

Expand All @@ -19,7 +19,7 @@

---

A single Rust binary that aggregates Brave, Serper, Exa, Linkup, Jina, Firecrawl, Tavily, SerpApi, Perplexity, Parallel, xAI, and more into one search interface. Built for AI agents from day one: structured JSON, semantic exit codes, self-describing `agent-info`, reciprocal rank fusion across providers, and a `usage` command that reports remaining API credits.
A single Rust binary that aggregates Brave, Serper, Exa, Linkup, Jina, You.com, Firecrawl, Tavily, SerpApi, Perplexity, Parallel, xAI, and more into one search interface. Built for AI agents from day one: structured JSON, semantic exit codes, self-describing `agent-info`, reciprocal rank fusion across providers, and a `usage` command that reports remaining API credits.

[Install](#install) | [How It Works](#how-it-works) | [Features](#features) | [Providers](#providers) | [Contributing](#contributing)

Expand Down Expand Up @@ -125,8 +125,8 @@ same routing registry the engine uses.

| Mode | Use when | `-q` is | Providers used |
|------|----------|---------|----------------|
| `general` | Any web lookup not covered below (default) | query | Parallel + Brave + Serper + Exa + Jina + Linkup + Tavily + Perplexity |
| `news` | Current events; add `-f day`/`-f week` | query | Parallel + Brave + Serper + Linkup + Tavily + Perplexity (news endpoints) |
| `general` | Any web lookup not covered below (default) | query | Parallel + Brave + Serper + Exa + Jina + Linkup + You.com + Tavily + Perplexity |
| `news` | Current events; add `-f day`/`-f week` | query | Parallel + Brave + Serper + Linkup + You.com + Tavily + Perplexity (news endpoints) |
| `academic` | Papers/studies by topic (semantic + web) | query | Exa + Serper + Tavily + Perplexity |
| `scholar` | Google Scholar records: citations, PDFs | query | Serper + SerpApi |
| `deep` | Max coverage; waits for all providers — use `-c 30` | query | Parallel + Brave (web + LLM Context) + Serper + Exa + Linkup + Tavily + Perplexity + xAI |
Expand Down Expand Up @@ -226,6 +226,7 @@ search "query" 2>/dev/null # suppress diagnostics
| **[Exa](https://exa.ai/)** | Neural/semantic search, category filters | Research papers, people search, similar sites |
| **[Jina](https://jina.ai/)** | Fast URL-to-markdown, 500 RPM free tier | Reading article content, quick extraction |
| **[Linkup](https://www.linkup.so/)** | High-accuracy agent search (leads the SimpleQA benchmark) | Factual lookups where accuracy matters most |
| **You.com** | Current web + news snippets | Fast general search and news-grounded results |
| **[Firecrawl](https://firecrawl.dev/)** | JavaScript rendering, structured extraction | Dynamic pages, SPAs, data extraction |
| **[Tavily](https://tavily.com/)** | General + deep search, research-focused | Broad coverage, research queries |
| **[SerpApi](https://serpapi.com/)** | Many engines: Google, Bing, YouTube, Baidu | Multi-engine coverage; only provider with a real balance API |
Expand Down
5 changes: 3 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use clap::{Parser, Subcommand};
name = "search",
version,
about = "Agent-friendly multi-provider search CLI",
long_about = "Aggregates 13 search providers across 13 explicit search modes.\n\
long_about = "Aggregates 14 search providers across 13 explicit search modes.\n\
You choose the mode (-m) and/or providers (-p); the CLI does not guess\n\
intent. Run `search agent-info` for the machine-readable capability map.\n\
Outputs colored tables for humans, JSON when piped to other tools.\n\n\
Expand All @@ -17,6 +17,7 @@ use clap::{Parser, Subcommand};
exa Neural/semantic search, LinkedIn people, find-similar\n \
jina Fast web search + URL-to-markdown reader\n \
linkup High-accuracy agent search (SimpleQA leader)\n \
youcom You.com web search + news snippets\n \
firecrawl JS-rendered page scraping + structured extraction\n \
tavily RAG-oriented search: general, news, academic, deep\n \
serpapi Many engines (Google, Bing, YouTube, Baidu, Scholar)\n \
Expand Down Expand Up @@ -182,7 +183,7 @@ pub struct SearchArgs {
#[arg(short, long)]
pub count: Option<usize>,

/// Use only specific providers (comma-separated: parallel,brave,serper,exa,jina,linkup,firecrawl,tavily,serpapi,perplexity,browserless,stealth,xai)
/// Use only specific providers (comma-separated: parallel,brave,serper,exa,jina,linkup,youcom,firecrawl,tavily,serpapi,perplexity,browserless,stealth,xai)
#[arg(short, long, value_delimiter = ',')]
pub providers: Option<Vec<String>>,

Expand Down
11 changes: 11 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub struct ApiKeys {
#[serde(default)]
pub linkup: String,
#[serde(default)]
pub youcom: String,
#[serde(default)]
pub firecrawl: String,
#[serde(default)]
pub tavily: String,
Expand Down Expand Up @@ -86,6 +88,7 @@ impl Default for AppConfig {
exa: String::new(),
jina: String::new(),
linkup: String::new(),
youcom: String::new(),
firecrawl: String::new(),
tavily: String::new(),
serpapi: String::new(),
Expand Down Expand Up @@ -151,6 +154,7 @@ pub const PROVIDER_KEYS: &[&str] = &[
"exa",
"jina",
"linkup",
"youcom",
"firecrawl",
"tavily",
"serpapi",
Expand Down Expand Up @@ -206,6 +210,7 @@ pub fn config_show(config: &AppConfig) {
("exa", &config.keys.exa, "EXA_API_KEY"),
("jina", &config.keys.jina, "JINA_API_KEY"),
("linkup", &config.keys.linkup, "LINKUP_API_KEY"),
("youcom", &config.keys.youcom, "YDC_API_KEY"),
("firecrawl", &config.keys.firecrawl, "FIRECRAWL_API_KEY"),
("tavily", &config.keys.tavily, "TAVILY_API_KEY"),
("serpapi", &config.keys.serpapi, "SERPAPI_API_KEY"),
Expand Down Expand Up @@ -477,6 +482,12 @@ pub fn config_check(config: &AppConfig) {
"LINKUP_API_KEY",
"High-accuracy agent search (SimpleQA leader)",
),
(
"youcom",
&config.keys.youcom,
"YDC_API_KEY",
"Web search and news snippets",
),
(
"firecrawl",
&config.keys.firecrawl,
Expand Down
18 changes: 13 additions & 5 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,7 @@ impl SearchError {

pub fn suggestion(&self) -> Option<String> {
match self {
Self::AuthMissing { provider } => Some(format!(
"Set {}_API_KEY env var, or: echo YOUR_KEY | search config set keys.{} -",
provider.to_uppercase(),
provider
)),
Self::AuthMissing { provider } => Some(auth_missing_suggestion(provider)),
Self::NoProviders(mode) => Some(format!(
"No providers configured for mode '{}'. Run: search config check",
mode
Expand Down Expand Up @@ -187,6 +183,18 @@ impl SearchError {
}
}

fn auth_missing_suggestion(provider: &str) -> String {
match provider {
"youcom" => "Set YDC_API_KEY env var, or: echo YOUR_KEY | search config set keys.youcom -"
.to_string(),
_ => format!(
"Set {}_API_KEY env var, or: echo YOUR_KEY | search config set keys.{} -",
provider.to_uppercase(),
provider
),
}
}

/// Scrub credential values from user-visible strings. Transport errors can
/// embed full request URLs (SerpApi authenticates via `?api_key=` in the
/// query string), and provider error bodies sometimes echo the caller's key.
Expand Down
8 changes: 5 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ async fn main() {
"google.serper.dev:443",
"api.exa.ai:443",
"api.jina.ai:443",
"ydc-index.io:443",
"api.tavily.com:443",
"api.perplexity.ai:443",
];
Expand Down Expand Up @@ -192,6 +193,7 @@ async fn main() {
"https://api.search.brave.com/res/v1/web/search",
"https://google.serper.dev/search",
"https://api.exa.ai/search",
"https://ydc-index.io/v1/search",
];
for url in urls {
let _ = app_c.client.head(url).send().await;
Expand Down Expand Up @@ -537,7 +539,7 @@ async fn run(cli: Cli, ctx: &Ctx, app: Arc<AppContext>) -> Result<i32, errors::S
ConfigAction::Show => {
if ctx.is_json() {
// Use the same resolver as `config check` (is_configured ->
// resolve_key) so env-only keys count and all 12 providers
// resolve_key) so env-only keys count and all providers
// are covered — the old hardcoded list missed parallel +
// stealth and ignored env vars.
let all = providers::build_providers(&app);
Expand Down Expand Up @@ -689,7 +691,7 @@ async fn run(cli: Cli, ctx: &Ctx, app: Arc<AppContext>) -> Result<i32, errors::S
"description": "Search mode — chosen explicitly by the caller; the CLI does NOT infer intent from the query"},
{"name": "-c/--count", "type": "integer", "required": false, "description": "Number of results"},
{"name": "-p/--providers", "type": "string[]", "required": false,
"values": ["parallel","brave","serper","exa","jina","linkup","firecrawl","tavily","serpapi","perplexity","browserless","stealth","xai"],
"values": ["parallel","brave","serper","exa","jina","linkup","youcom","firecrawl","tavily","serpapi","perplexity","browserless","stealth","xai"],
"description": "Comma-separated provider list"},
{"name": "-d/--domain", "type": "string[]", "required": false, "description": "Include only these domains"},
{"name": "--exclude-domain", "type": "string[]", "required": false, "description": "Exclude these domains"},
Expand Down Expand Up @@ -724,7 +726,7 @@ async fn run(cli: Cli, ctx: &Ctx, app: Arc<AppContext>) -> Result<i32, errors::S
"config set": {
"description": "Set a configuration value",
"args": [
{"name": "key", "type": "string", "required": true, "description": "Config key (e.g. keys.brave, settings.timeout)"},
{"name": "key", "type": "string", "required": true, "description": "Config key (e.g. keys.brave, keys.youcom, settings.timeout)"},
{"name": "value", "type": "string", "required": true, "description": "Value to set"},
],
"options": []
Expand Down
2 changes: 2 additions & 0 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod parallel;
pub mod perplexity;
pub mod serpapi;
pub mod serper;
pub mod youcom;
#[cfg(feature = "stealth")]
pub mod stealth;
pub mod tavily;
Expand Down Expand Up @@ -188,6 +189,7 @@ pub fn build_providers(ctx: &Arc<AppContext>) -> Vec<Box<dyn Provider>> {
Box::new(exa::Exa::new(ctx.clone())),
Box::new(jina::Jina::new(ctx.clone())),
Box::new(linkup::Linkup::new(ctx.clone())),
Box::new(youcom::YouCom::new(ctx.clone())),
#[cfg(feature = "stealth")]
Box::new(stealth::Stealth::new(ctx.clone())),
Box::new(firecrawl::Firecrawl::new(ctx.clone())),
Expand Down
Loading