feat(websearch): add real provider chain

This commit is contained in:
Tam Nhu Tran committed 2026-03-23 15:26:05 -04:00
1 parent 84fd2b001c
commit 6c7d215ecc
18 files changed
+2105 -1203

No files matched your search

+5 -4
View File
@@ -3,12 +3,13 @@
*
* WebSearch is a server-side tool executed by Anthropic's API.
* Third-party providers (gemini, agy, codex, qwen) don't have access.
* This manager installs a hook that uses CLI tools (Gemini CLI) as fallback.
* This manager installs a hook that uses deterministic local search backends,
* with legacy AI CLI tools kept only as optional fallback.
*
* Simplified Architecture:
* - No MCP complexity
* - Uses CLI tools (currently Gemini CLI)
* - Easy to extend for future CLI tools (opencode, etc.)
* - No MCP dependency for the default path
* - Uses real search providers first (DuckDuckGo, Brave)
* - Keeps Gemini/OpenCode/Grok as compatibility fallback only
*
* @module utils/websearch-manager
*/
+23 -1
View File
@@ -29,7 +29,29 @@ export function getWebSearchHookEnv(): Record<string, string> {
env.CCS_WEBSEARCH_ENABLED = '1';
// Pass individual provider enabled states
// Hook will only use providers that are BOTH enabled AND installed
// Hook will only use providers that are BOTH enabled AND ready.
if (wsConfig.providers?.exa?.enabled) {
env.CCS_WEBSEARCH_EXA = '1';
env.CCS_WEBSEARCH_EXA_MAX_RESULTS = String(wsConfig.providers.exa.max_results || 5);
}
if (wsConfig.providers?.tavily?.enabled) {
env.CCS_WEBSEARCH_TAVILY = '1';
env.CCS_WEBSEARCH_TAVILY_MAX_RESULTS = String(wsConfig.providers.tavily.max_results || 5);
}
if (wsConfig.providers?.duckduckgo?.enabled) {
env.CCS_WEBSEARCH_DUCKDUCKGO = '1';
env.CCS_WEBSEARCH_DUCKDUCKGO_MAX_RESULTS = String(
wsConfig.providers.duckduckgo.max_results || 5
);
}
if (wsConfig.providers?.brave?.enabled) {
env.CCS_WEBSEARCH_BRAVE = '1';
env.CCS_WEBSEARCH_BRAVE_MAX_RESULTS = String(wsConfig.providers.brave.max_results || 5);
}
if (wsConfig.providers?.gemini?.enabled) {
env.CCS_WEBSEARCH_GEMINI = '1';
if (wsConfig.providers.gemini.model) {
+139 -90
View File
@@ -8,70 +8,164 @@
import { ok, warn, fail, info } from '../ui';
import { getWebSearchConfig } from '../../config/unified-config-loader';
import { getGeminiCliStatus, hasGeminiCli, isGeminiAuthenticated } from './gemini-cli';
import { getGrokCliStatus, hasGrokCli } from './grok-cli';
import { getOpenCodeCliStatus, hasOpenCodeCli } from './opencode-cli';
import { getGeminiCliStatus, isGeminiAuthenticated } from './gemini-cli';
import { getGrokCliStatus } from './grok-cli';
import { getOpenCodeCliStatus } from './opencode-cli';
import type { WebSearchCliInfo, WebSearchStatus } from './types';
/**
* Get all WebSearch CLI providers with their status
*/
export function getWebSearchCliProviders(): WebSearchCliInfo[] {
function hasEnvValue(name: string): boolean {
return (process.env[name] || '').trim().length > 0;
}
function hasAnyEnvValue(names: string[]): boolean {
return names.some((name) => hasEnvValue(name));
}
function getLegacyProviderStatuses(): WebSearchCliInfo[] {
const wsConfig = getWebSearchConfig();
const geminiStatus = getGeminiCliStatus();
const grokStatus = getGrokCliStatus();
const opencodeStatus = getOpenCodeCliStatus();
const geminiAuthed = geminiStatus.installed && isGeminiAuthenticated();
return [
{
id: 'gemini',
kind: 'legacy-cli',
name: 'Gemini CLI',
command: 'gemini',
installed: geminiStatus.installed,
enabled: wsConfig.providers?.gemini?.enabled ?? false,
available: geminiAuthed,
version: geminiStatus.version ?? null,
installCommand: 'npm install -g @google/gemini-cli',
docsUrl: 'https://github.com/google-gemini/gemini-cli',
requiresApiKey: false,
description: 'Google Gemini with web search (FREE tier: 1000 req/day)',
freeTier: true,
description: 'Optional legacy LLM fallback with Google web search.',
detail: geminiStatus.installed
? geminiAuthed
? 'Authenticated'
: "Run 'gemini' to login"
: 'Not installed',
},
{
id: 'opencode',
kind: 'legacy-cli',
name: 'OpenCode',
command: 'opencode',
installed: opencodeStatus.installed,
enabled: wsConfig.providers?.opencode?.enabled ?? false,
available: opencodeStatus.installed,
version: opencodeStatus.version ?? null,
installCommand: 'curl -fsSL https://opencode.ai/install | bash',
docsUrl: 'https://github.com/sst/opencode',
requiresApiKey: false,
description: 'OpenCode with built-in web search (FREE via Zen)',
freeTier: true,
description: 'Optional legacy LLM fallback via OpenCode.',
detail: opencodeStatus.installed ? 'Installed' : 'Not installed',
},
{
id: 'grok',
kind: 'legacy-cli',
name: 'Grok CLI',
command: 'grok',
installed: grokStatus.installed,
enabled: wsConfig.providers?.grok?.enabled ?? false,
available: grokStatus.installed && hasEnvValue('GROK_API_KEY'),
version: grokStatus.version ?? null,
installCommand: 'npm install -g @vibe-kit/grok-cli',
docsUrl: 'https://github.com/superagent-ai/grok-cli',
requiresApiKey: true,
apiKeyEnvVar: 'GROK_API_KEY',
description: 'xAI Grok CLI with AI coding agent capabilities',
freeTier: false,
description: 'Optional legacy LLM fallback with xAI Grok.',
detail: grokStatus.installed
? hasEnvValue('GROK_API_KEY')
? 'Ready'
: 'Set GROK_API_KEY'
: 'Not installed',
},
];
}
/**
* Check if any WebSearch CLI is available
* Get all WebSearch providers with their current status.
*/
export function hasAnyWebSearchCli(): boolean {
return hasGeminiCli() || hasGrokCli() || hasOpenCodeCli();
export function getWebSearchCliProviders(): WebSearchCliInfo[] {
const wsConfig = getWebSearchConfig();
const providers: WebSearchCliInfo[] = [
{
id: 'exa',
kind: 'backend',
name: 'Exa',
enabled: wsConfig.providers?.exa?.enabled ?? false,
available:
(wsConfig.providers?.exa?.enabled ?? false) &&
hasAnyEnvValue(['EXA_API_KEY', 'CCS_WEBSEARCH_EXA_API_KEY']),
version: null,
docsUrl: 'https://docs.exa.ai/reference/search',
requiresApiKey: true,
apiKeyEnvVar: 'EXA_API_KEY',
description: 'API-backed search with strong relevance and content extraction.',
detail: hasAnyEnvValue(['EXA_API_KEY', 'CCS_WEBSEARCH_EXA_API_KEY'])
? `API key detected (${wsConfig.providers?.exa?.max_results ?? 5} results)`
: 'Set EXA_API_KEY',
},
{
id: 'tavily',
kind: 'backend',
name: 'Tavily',
enabled: wsConfig.providers?.tavily?.enabled ?? false,
available:
(wsConfig.providers?.tavily?.enabled ?? false) &&
hasAnyEnvValue(['TAVILY_API_KEY', 'CCS_WEBSEARCH_TAVILY_API_KEY']),
version: null,
docsUrl: 'https://docs.tavily.com/documentation/api-reference/endpoint/search',
requiresApiKey: true,
apiKeyEnvVar: 'TAVILY_API_KEY',
description: 'Search API optimized for agent workflows and concise web result synthesis.',
detail: hasAnyEnvValue(['TAVILY_API_KEY', 'CCS_WEBSEARCH_TAVILY_API_KEY'])
? `API key detected (${wsConfig.providers?.tavily?.max_results ?? 5} results)`
: 'Set TAVILY_API_KEY',
},
{
id: 'duckduckgo',
kind: 'backend',
name: 'DuckDuckGo',
enabled: wsConfig.providers?.duckduckgo?.enabled ?? true,
available: wsConfig.providers?.duckduckgo?.enabled ?? true,
version: null,
docsUrl: 'https://duckduckgo.com',
requiresApiKey: false,
description: 'Default built-in HTML search backend. Zero setup.',
detail: `Built-in (${wsConfig.providers?.duckduckgo?.max_results ?? 5} results)`,
},
{
id: 'brave',
kind: 'backend',
name: 'Brave Search',
enabled: wsConfig.providers?.brave?.enabled ?? false,
available:
(wsConfig.providers?.brave?.enabled ?? false) &&
hasAnyEnvValue(['BRAVE_API_KEY', 'CCS_WEBSEARCH_BRAVE_API_KEY']),
version: null,
docsUrl: 'https://brave.com/search/api/',
requiresApiKey: true,
apiKeyEnvVar: 'BRAVE_API_KEY',
description: 'API-backed web search with cleaner result metadata.',
detail: hasAnyEnvValue(['BRAVE_API_KEY', 'CCS_WEBSEARCH_BRAVE_API_KEY'])
? `API key detected (${wsConfig.providers?.brave?.max_results ?? 5} results)`
: 'Set BRAVE_API_KEY',
},
];
return [...providers, ...getLegacyProviderStatuses()];
}
/**
* Get install hints for CLI-only users when no WebSearch CLI is installed
* Returns raw message strings (without indicator prefix) for display
* Check if any WebSearch provider is currently ready.
*/
export function hasAnyWebSearchCli(): boolean {
return getWebSearchCliProviders().some((provider) => provider.enabled && provider.available);
}
/**
* Get setup hints when no providers are ready.
*/
export function getCliInstallHints(): string[] {
if (hasAnyWebSearchCli()) {
@@ -79,95 +173,58 @@ export function getCliInstallHints(): string[] {
}
return [
'WebSearch: No CLI tools installed',
' Gemini CLI (FREE): npm i -g @google/gemini-cli',
' OpenCode (FREE): curl -fsSL https://opencode.ai/install | bash',
' Grok CLI (paid): npm i -g @vibe-kit/grok-cli',
'WebSearch: no ready providers',
' Enable DuckDuckGo in Settings > WebSearch for zero-setup search',
' Or export EXA_API_KEY, TAVILY_API_KEY, or BRAVE_API_KEY for API-backed search',
' Optional legacy fallback: npm i -g @google/gemini-cli',
];
}
/**
* Get WebSearch readiness status for display
*
* Called on third-party profile startup to inform user.
* Checks both installation AND authentication status for Gemini CLI.
* Get WebSearch readiness status for display.
*/
export function getWebSearchReadiness(): WebSearchStatus {
const wsConfig = getWebSearchConfig();
const providers = getWebSearchCliProviders();
// Check if WebSearch is disabled entirely
if (!wsConfig.enabled) {
return {
readiness: 'unavailable',
geminiCli: false,
geminiAuthenticated: false,
grokCli: false,
opencodeCli: false,
message: 'Disabled in config',
providers,
};
}
// Check all CLIs
const geminiInstalled = hasGeminiCli();
const geminiAuthed = geminiInstalled && isGeminiAuthenticated();
const grokInstalled = hasGrokCli();
const opencodeInstalled = hasOpenCodeCli();
const enabledProviders = providers.filter((provider) => provider.enabled);
const readyProviders = enabledProviders.filter((provider) => provider.available);
// Build message based on installed + authenticated CLIs
const readyClis: string[] = [];
const needsAuthClis: string[] = [];
// Gemini requires auth check
if (geminiInstalled) {
if (geminiAuthed) {
readyClis.push('Gemini');
} else {
needsAuthClis.push('Gemini');
}
}
// Other CLIs don't require auth check (for now)
if (grokInstalled) readyClis.push('Grok');
if (opencodeInstalled) readyClis.push('OpenCode');
// Determine overall status
if (readyClis.length > 0) {
if (readyProviders.length > 0) {
return {
readiness: 'ready',
geminiCli: geminiInstalled,
geminiAuthenticated: geminiAuthed,
grokCli: grokInstalled,
opencodeCli: opencodeInstalled,
message: `Ready (${readyClis.join(' + ')})`,
message: `Ready (${readyProviders.map((provider) => provider.name).join(' + ')})`,
providers,
};
}
if (needsAuthClis.length > 0) {
if (enabledProviders.length > 0) {
return {
readiness: 'needs_auth',
geminiCli: geminiInstalled,
geminiAuthenticated: false,
grokCli: grokInstalled,
opencodeCli: opencodeInstalled,
message: `Gemini: run 'gemini' to login`,
readiness: 'needs_setup',
message: enabledProviders
.map((provider) => `${provider.name}: ${provider.detail}`)
.join(' | '),
providers,
};
}
return {
readiness: 'unavailable',
geminiCli: false,
geminiAuthenticated: false,
grokCli: false,
opencodeCli: false,
message: 'Install: npm i -g @google/gemini-cli',
message: 'Enable at least one provider in Settings > WebSearch',
providers,
};
}
/**
* Display WebSearch status (single line, equilibrium UX)
*
* Only call for third-party profiles.
* Shows detailed install hints when no CLI is installed.
* Display WebSearch status (single line, equilibrium UX).
*/
export function displayWebSearchStatus(): void {
const status = getWebSearchReadiness();
@@ -176,21 +233,13 @@ export function displayWebSearchStatus(): void {
case 'ready':
console.error(ok(`WebSearch: ${status.message}`));
break;
case 'needs_auth':
case 'needs_setup':
console.error(warn(`WebSearch: ${status.message}`));
break;
case 'unavailable':
console.error(fail(`WebSearch: ${status.message}`));
const hints = getCliInstallHints();
if (hints.length > 0) {
// First line gets [i] prefix, rest are continuation (indented, no prefix)
for (let i = 0; i < hints.length; i++) {
if (i === 0) {
console.error(info(hints[i]));
} else {
console.error(hints[i]);
}
}
for (const [index, hint] of getCliInstallHints().entries()) {
console.error(index === 0 ? info(hint) : hint);
}
break;
}
+46 -23
View File
@@ -1,7 +1,7 @@
/**
* WebSearch Type Definitions
*
* Contains all type definitions for WebSearch CLI providers and status.
* Contains all type definitions for WebSearch providers and status.
*
* @module utils/websearch/types
*/
@@ -29,46 +29,64 @@ export type OpenCodeCliStatus = ComponentStatus;
/**
* WebSearch availability status for third-party profiles
*/
export type WebSearchReadiness = 'ready' | 'needs_auth' | 'unavailable';
export type WebSearchReadiness = 'ready' | 'needs_setup' | 'unavailable';
/**
* WebSearch status for display
* WebSearch provider identifier
*/
export interface WebSearchStatus {
readiness: WebSearchReadiness;
geminiCli: boolean;
geminiAuthenticated: boolean;
grokCli: boolean;
opencodeCli: boolean;
message: string;
}
export type WebSearchProviderId =
| 'exa'
| 'tavily'
| 'duckduckgo'
| 'brave'
| 'gemini'
| 'grok'
| 'opencode';
/**
* WebSearch CLI provider information for health checks and UI
* Provider execution class.
*/
export type WebSearchProviderKind = 'backend' | 'legacy-cli';
/**
* WebSearch provider information for health checks and UI
*/
export interface WebSearchCliInfo {
/** Provider ID */
id: 'gemini' | 'grok' | 'opencode';
id: WebSearchProviderId;
/** Backend vs legacy CLI */
kind: WebSearchProviderKind;
/** Display name */
name: string;
/** CLI command name */
command: string;
/** Whether CLI is installed */
installed: boolean;
/** CLI version if installed */
/** Command name for legacy providers */
command?: string;
/** Whether the provider is enabled in config */
enabled: boolean;
/** Whether the provider is ready right now */
available: boolean;
/** CLI version if applicable */
version: string | null;
/** Install command */
installCommand: string;
/** Install or setup command when applicable */
installCommand?: string;
/** Docs URL */
docsUrl: string;
docsUrl?: string;
/** Whether this provider requires an API key */
requiresApiKey: boolean;
/** API key environment variable name */
apiKeyEnvVar?: string;
/** Brief description */
description: string;
/** Free tier available? */
freeTier: boolean;
/** Summary detail shown in status UIs */
detail: string;
}
/**
* WebSearch status for display
*/
export interface WebSearchStatus {
readiness: WebSearchReadiness;
message: string;
providers: WebSearchCliInfo[];
}
/**
@@ -78,6 +96,7 @@ export interface WebSearchProviderConfig {
enabled?: boolean;
model?: string;
timeout?: number;
max_results?: number;
}
/**
@@ -86,6 +105,10 @@ export interface WebSearchProviderConfig {
export interface WebSearchConfig {
enabled: boolean;
providers?: {
exa?: WebSearchProviderConfig;
tavily?: WebSearchProviderConfig;
duckduckgo?: WebSearchProviderConfig;
brave?: WebSearchProviderConfig;
gemini?: WebSearchProviderConfig;
opencode?: WebSearchProviderConfig;
grok?: WebSearchProviderConfig;