diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index d44f1e3a..14f0e135 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -121,20 +121,34 @@ export interface GrokWebSearchConfig { timeout?: number; } +/** + * OpenCode CLI WebSearch configuration. + */ +export interface OpenCodeWebSearchConfig { + /** Enable OpenCode CLI for WebSearch (default: false) */ + enabled?: boolean; + /** Model to use (default: opencode/gpt-5-nano) */ + model?: string; + /** Timeout in seconds (default: 60) */ + timeout?: number; +} + /** * WebSearch providers configuration. - * Currently supports Gemini CLI and Grok CLI. + * Supports Gemini CLI, Grok CLI, and OpenCode. */ export interface WebSearchProvidersConfig { /** Gemini CLI - uses google_web_search tool (FREE tier: 1000 req/day) */ gemini?: GeminiWebSearchConfig; /** Grok CLI - xAI web search (requires GROK_API_KEY) */ grok?: GrokWebSearchConfig; + /** OpenCode - built-in web search (FREE via OpenCode Zen) */ + opencode?: OpenCodeWebSearchConfig; } /** * WebSearch configuration. - * Uses CLI tools (Gemini CLI, Grok CLI) to provide WebSearch for third-party profiles. + * Uses CLI tools (Gemini CLI, Grok CLI, OpenCode) for third-party profiles. * Third-party providers don't have server-side WebSearch access. */ export interface WebSearchConfig { diff --git a/src/utils/websearch-manager.ts b/src/utils/websearch-manager.ts index 3ce1e8d5..2c119b38 100644 --- a/src/utils/websearch-manager.ts +++ b/src/utils/websearch-manager.ts @@ -204,12 +204,99 @@ export function clearGrokCliCache(): void { grokCliCache = null; } +// ========== OpenCode CLI Detection ========== + +/** + * OpenCode CLI installation status + */ +export interface OpenCodeCliStatus { + installed: boolean; + path: string | null; + version: string | null; +} + +// Cache for OpenCode CLI status (per process) +let opencodeCliCache: OpenCodeCliStatus | null = null; + +/** + * Check if OpenCode CLI is installed globally + * + * OpenCode provides built-in web search via opencode/gpt-5-nano model. + * Install: curl -fsSL https://opencode.ai/install | bash + * + * @returns OpenCode CLI status with path and version + */ +export function getOpenCodeCliStatus(): OpenCodeCliStatus { + // Return cached result if available + if (opencodeCliCache) { + return opencodeCliCache; + } + + const result: OpenCodeCliStatus = { + installed: false, + path: null, + version: null, + }; + + try { + const isWindows = process.platform === 'win32'; + const whichCmd = isWindows ? 'where opencode' : 'which opencode'; + + const pathResult = execSync(whichCmd, { + encoding: 'utf8', + timeout: 5000, + stdio: ['pipe', 'pipe', 'pipe'], + }); + + const opencodePath = pathResult.trim().split('\n')[0]; // First result on Windows + + if (opencodePath) { + result.installed = true; + result.path = opencodePath; + + // Try to get version + try { + const versionResult = execSync('opencode --version', { + encoding: 'utf8', + timeout: 5000, + stdio: ['pipe', 'pipe', 'pipe'], + }); + result.version = versionResult.trim(); + } catch { + // Version check failed, but CLI is installed + result.version = 'unknown'; + } + } + } catch { + // Command not found - OpenCode CLI not installed + } + + // Cache result + opencodeCliCache = result; + return result; +} + +/** + * Check if OpenCode CLI is available (quick boolean check) + */ +export function hasOpenCodeCli(): boolean { + return getOpenCodeCliStatus().installed; +} + +/** + * Clear OpenCode CLI cache (for testing or after installation) + */ +export function clearOpenCodeCliCache(): void { + opencodeCliCache = null; +} + /** * Clear all CLI caches */ export function clearAllCliCaches(): void { geminiCliCache = null; grokCliCache = null; + opencodeCliCache = null; } // ========== CLI Provider Info ========== @@ -219,7 +306,7 @@ export function clearAllCliCaches(): void { */ export interface WebSearchCliInfo { /** Provider ID */ - id: 'gemini' | 'grok'; + id: 'gemini' | 'grok' | 'opencode'; /** Display name */ name: string; /** CLI command name */ @@ -248,6 +335,7 @@ export interface WebSearchCliInfo { export function getWebSearchCliProviders(): WebSearchCliInfo[] { const geminiStatus = getGeminiCliStatus(); const grokStatus = getGrokCliStatus(); + const opencodeStatus = getOpenCodeCliStatus(); return [ { @@ -262,6 +350,18 @@ export function getWebSearchCliProviders(): WebSearchCliInfo[] { description: 'Google Gemini with web search (FREE tier: 1000 req/day)', freeTier: true, }, + { + id: 'opencode', + name: 'OpenCode', + command: 'opencode', + installed: opencodeStatus.installed, + version: opencodeStatus.version, + 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, + }, { id: 'grok', name: 'Grok CLI', @@ -282,7 +382,7 @@ export function getWebSearchCliProviders(): WebSearchCliInfo[] { * Check if any WebSearch CLI is available */ export function hasAnyWebSearchCli(): boolean { - return hasGeminiCli() || hasGrokCli(); + return hasGeminiCli() || hasGrokCli() || hasOpenCodeCli(); } /** @@ -296,6 +396,7 @@ export function getCliInstallHints(): string[] { return [ '[i] 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', ]; } @@ -520,6 +621,7 @@ export interface WebSearchStatus { readiness: WebSearchReadiness; geminiCli: boolean; grokCli: boolean; + opencodeCli: boolean; message: string; } @@ -537,38 +639,29 @@ export function getWebSearchReadiness(): WebSearchStatus { readiness: 'unavailable', geminiCli: false, grokCli: false, + opencodeCli: false, message: 'Disabled in config', }; } - // Check both CLIs + // Check all CLIs const geminiInstalled = hasGeminiCli(); const grokInstalled = hasGrokCli(); + const opencodeInstalled = hasOpenCodeCli(); - if (geminiInstalled && grokInstalled) { + // Build message based on installed CLIs + const installedClis: string[] = []; + if (geminiInstalled) installedClis.push('Gemini'); + if (grokInstalled) installedClis.push('Grok'); + if (opencodeInstalled) installedClis.push('OpenCode'); + + if (installedClis.length > 0) { return { readiness: 'ready', - geminiCli: true, - grokCli: true, - message: 'Ready (Gemini + Grok CLI)', - }; - } - - if (geminiInstalled) { - return { - readiness: 'ready', - geminiCli: true, - grokCli: false, - message: 'Ready (Gemini CLI)', - }; - } - - if (grokInstalled) { - return { - readiness: 'ready', - geminiCli: false, - grokCli: true, - message: 'Ready (Grok CLI)', + geminiCli: geminiInstalled, + grokCli: grokInstalled, + opencodeCli: opencodeInstalled, + message: `Ready (${installedClis.join(' + ')})`, }; } @@ -576,6 +669,7 @@ export function getWebSearchReadiness(): WebSearchStatus { readiness: 'unavailable', geminiCli: false, grokCli: false, + opencodeCli: false, message: 'Install: npm i -g @google/gemini-cli', }; } diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 921b8751..93328eca 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -62,6 +62,7 @@ import { getWebSearchReadiness, getGeminiCliStatus, getGrokCliStatus, + getOpenCodeCliStatus, } from '../utils/websearch-manager'; export const apiRoutes = Router(); @@ -1489,6 +1490,20 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => { timeout: providers.grok?.timeout ?? existingConfig.websearch?.providers?.grok?.timeout ?? 55, }, + opencode: { + enabled: + providers.opencode?.enabled ?? + existingConfig.websearch?.providers?.opencode?.enabled ?? + false, + model: + providers.opencode?.model ?? + existingConfig.websearch?.providers?.opencode?.model ?? + 'opencode/gpt-5-nano', + timeout: + providers.opencode?.timeout ?? + existingConfig.websearch?.providers?.opencode?.timeout ?? + 60, + }, } : existingConfig.websearch?.providers, }; @@ -1506,12 +1521,13 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => { /** * GET /api/websearch/status - Get WebSearch status - * Returns: { geminiCli, grokCli, readiness } + * Returns: { geminiCli, grokCli, opencodeCli, readiness } */ apiRoutes.get('/websearch/status', (_req: Request, res: Response): void => { try { const geminiCli = getGeminiCliStatus(); const grokCli = getGrokCliStatus(); + const opencodeCli = getOpenCodeCliStatus(); const readiness = getWebSearchReadiness(); res.json({ @@ -1525,6 +1541,11 @@ apiRoutes.get('/websearch/status', (_req: Request, res: Response): void => { path: grokCli.path, version: grokCli.version, }, + opencodeCli: { + installed: opencodeCli.installed, + path: opencodeCli.path, + version: opencodeCli.version, + }, readiness: { status: readiness.readiness, message: readiness.message, diff --git a/ui/src/pages/settings.tsx b/ui/src/pages/settings.tsx index b66e0ab8..ea4b2b1c 100644 --- a/ui/src/pages/settings.tsx +++ b/ui/src/pages/settings.tsx @@ -28,9 +28,14 @@ interface ProviderConfig { timeout?: number; } +interface OpenCodeProviderConfig extends ProviderConfig { + model?: string; +} + interface WebSearchProvidersConfig { gemini?: ProviderConfig; grok?: ProviderConfig; + opencode?: OpenCodeProviderConfig; } interface WebSearchConfig { @@ -47,6 +52,7 @@ interface CliStatus { interface WebSearchStatus { geminiCli: CliStatus; grokCli: CliStatus; + opencodeCli: CliStatus; readiness: { status: 'ready' | 'unavailable'; message: string; @@ -136,9 +142,10 @@ export function SettingsPage() { const providers = config?.providers || {}; const currentState = providers.gemini?.enabled ?? false; const grokState = providers.grok?.enabled ?? false; + const opencodeState = providers.opencode?.enabled ?? false; saveConfig({ - enabled: !currentState || grokState, // Enable WebSearch if any provider is enabled + enabled: !currentState || grokState || opencodeState, // Enable WebSearch if any provider is enabled providers: { ...providers, gemini: { @@ -192,6 +199,7 @@ export function SettingsPage() { const isGeminiEnabled = config?.providers?.gemini?.enabled ?? false; const isGrokEnabled = config?.providers?.grok?.enabled ?? false; + const isOpenCodeEnabled = config?.providers?.opencode?.enabled ?? false; // Toggle Grok provider const toggleGrok = () => { @@ -199,7 +207,7 @@ export function SettingsPage() { const currentState = providers.grok?.enabled ?? false; saveConfig({ - enabled: isGeminiEnabled || !currentState, // Enable WebSearch if any provider is enabled + enabled: isGeminiEnabled || !currentState || isOpenCodeEnabled, // Enable WebSearch if any provider is enabled providers: { ...providers, grok: { @@ -210,6 +218,23 @@ export function SettingsPage() { }); }; + // Toggle OpenCode provider + const toggleOpenCode = () => { + const providers = config?.providers || {}; + const currentState = providers.opencode?.enabled ?? false; + + saveConfig({ + enabled: isGeminiEnabled || isGrokEnabled || !currentState, // Enable WebSearch if any provider is enabled + providers: { + ...providers, + opencode: { + ...providers.opencode, + enabled: !currentState, + }, + }, + }); + }; + if (loading) { return (
@@ -352,6 +377,72 @@ export function SettingsPage() {
)} + {/* OpenCode CLI Provider */} +
+
+ +
+
+

opencode

+ + FREE + + {status?.opencodeCli?.installed ? ( + + installed + + ) : ( + + not installed + + )} +
+

+ OpenCode (web search via Zen) +

+
+
+ +
+ + {/* OpenCode Installation hint when not installed */} + {!status?.opencodeCli?.installed && !statusLoading && ( +
+

+ OpenCode not installed +

+

+ Install globally (FREE tier available): +

+ + curl -fsSL https://opencode.ai/install | bash + +
+ + + View documentation + +
+
+ )} + {/* Grok CLI Provider */}