From 0d09604bb98cf8e9cfbe2f35adec65277fcf1003 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 22:12:16 +0700 Subject: [PATCH] fix(websearch): support Gemini positional prompt mode --- docs/websearch.md | 6 +- lib/hooks/websearch-transformer.cjs | 140 +++++++++++++++++----------- 2 files changed, 86 insertions(+), 60 deletions(-) diff --git a/docs/websearch.md b/docs/websearch.md index 06e93141..c1b44841 100644 --- a/docs/websearch.md +++ b/docs/websearch.md @@ -1,6 +1,6 @@ # WebSearch Configuration Guide -Last Updated: 2026-02-04 +Last Updated: 2026-02-26 CCS provides automatic web search capability for all profiles, including third-party providers that cannot access Anthropic's native WebSearch API. @@ -19,7 +19,7 @@ Third-party profiles (OAuth and API-based) cannot use Anthropic's WebSearch beca CCS solves this with a hybrid fallback approach: -1. **Gemini CLI Transformer** (Primary) - Uses `gemini -p` with `google_web_search` tool +1. **Gemini CLI Transformer** (Primary) - Uses positional Gemini prompt mode (with legacy `-p` fallback) and `google_web_search` tool 2. **MCP Fallback Chain** (Secondary) - MCP-based web search servers ## Architecture @@ -53,7 +53,7 @@ The **ultimate solution** for third-party WebSearch. Uses `gemini` CLI with OAut ### How It Works 1. A PreToolUse hook intercepts WebSearch tool calls -2. Executes `gemini -p` with explicit google_web_search instruction +2. Executes `gemini ""` (positional mode) with explicit google_web_search instruction 3. Returns search results directly to Claude via the hook's deny reason 4. Claude receives full search results and continues the conversation diff --git a/lib/hooks/websearch-transformer.cjs b/lib/hooks/websearch-transformer.cjs index 2df55450..8d8a99ab 100644 --- a/lib/hooks/websearch-transformer.cjs +++ b/lib/hooks/websearch-transformer.cjs @@ -282,6 +282,61 @@ async function processHook() { /** * Execute search via Gemini CLI */ +function shouldRetryGeminiWithLegacyPrompt(errorMessage) { + const lowerError = (errorMessage || '').toLowerCase(); + + return ( + lowerError.includes('unknown option') || + lowerError.includes('unknown argument') || + lowerError.includes('unrecognized option') || + lowerError.includes('usage: gemini') || + lowerError.includes('use --prompt') || + lowerError.includes('using the --prompt option') + ); +} + +function runGeminiCommand(args, timeoutMs) { + const spawnResult = spawnSync('gemini', args, { + encoding: 'utf8', + timeout: timeoutMs, + maxBuffer: 1024 * 1024 * 2, + stdio: ['pipe', 'pipe', 'pipe'], + shell: isWindows, + }); + + if (spawnResult.error) { + if (spawnResult.error.code === 'ENOENT') { + return { success: false, error: 'Gemini CLI not installed' }; + } + throw spawnResult.error; + } + + if (spawnResult.status !== 0) { + const stderr = (spawnResult.stderr || '').trim(); + return { + success: false, + error: stderr || `Gemini CLI exited with code ${spawnResult.status}`, + }; + } + + const result = (spawnResult.stdout || '').trim(); + + if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) { + return { success: false, error: 'Empty or too short response from Gemini' }; + } + + const lowerResult = result.toLowerCase(); + if ( + lowerResult.includes('error:') || + lowerResult.includes('failed to') || + lowerResult.includes('authentication required') + ) { + return { success: false, error: `Gemini returned error: ${result.substring(0, 100)}` }; + } + + return { success: true, content: result }; +} + function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { try { const timeoutMs = timeoutSec * 1000; @@ -290,54 +345,31 @@ function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { // Allow model override via env var const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || config.model; + const baseArgs = ['--model', model, '--yolo']; + const positionalArgs = [...baseArgs, prompt]; if (process.env.CCS_DEBUG) { + console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo "..."`); + } + + // Current Gemini CLI prefers positional prompts and deprecates -p/--prompt. + // Retry once with -p for legacy CLIs that still require it. + const positionalResult = runGeminiCommand(positionalArgs, timeoutMs); + if (positionalResult.success) { + return positionalResult; + } + + if (!shouldRetryGeminiWithLegacyPrompt(positionalResult.error)) { + return positionalResult; + } + + if (process.env.CCS_DEBUG) { + console.error('[CCS Hook] Positional Gemini prompt failed; retrying with -p for legacy CLI'); console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`); } - const spawnResult = spawnSync( - 'gemini', - ['--model', model, '--yolo', '-p', prompt], - { - encoding: 'utf8', - timeout: timeoutMs, - maxBuffer: 1024 * 1024 * 2, - stdio: ['pipe', 'pipe', 'pipe'], - shell: isWindows, - } - ); - - if (spawnResult.error) { - if (spawnResult.error.code === 'ENOENT') { - return { success: false, error: 'Gemini CLI not installed' }; - } - throw spawnResult.error; - } - - if (spawnResult.status !== 0) { - const stderr = (spawnResult.stderr || '').trim(); - return { - success: false, - error: stderr || `Gemini CLI exited with code ${spawnResult.status}`, - }; - } - - const result = (spawnResult.stdout || '').trim(); - - if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) { - return { success: false, error: 'Empty or too short response from Gemini' }; - } - - const lowerResult = result.toLowerCase(); - if ( - lowerResult.includes('error:') || - lowerResult.includes('failed to') || - lowerResult.includes('authentication required') - ) { - return { success: false, error: `Gemini returned error: ${result.substring(0, 100)}` }; - } - - return { success: true, content: result }; + const legacyPromptArgs = [...baseArgs, '-p', prompt]; + return runGeminiCommand(legacyPromptArgs, timeoutMs); } catch (err) { if (err.killed) { return { success: false, error: 'Gemini CLI timed out' }; @@ -362,17 +394,13 @@ function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { console.error(`[CCS Hook] Executing: opencode run --model ${model} "..."`); } - const spawnResult = spawnSync( - 'opencode', - ['run', prompt, '--model', model], - { - encoding: 'utf8', - timeout: timeoutMs, - maxBuffer: 1024 * 1024 * 2, - stdio: ['pipe', 'pipe', 'pipe'], - shell: isWindows, - } - ); + const spawnResult = spawnSync('opencode', ['run', prompt, '--model', model], { + encoding: 'utf8', + timeout: timeoutMs, + maxBuffer: 1024 * 1024 * 2, + stdio: ['pipe', 'pipe', 'pipe'], + shell: isWindows, + }); if (spawnResult.error) { if (spawnResult.error.code === 'ENOENT') { @@ -599,9 +627,7 @@ function outputNoToolsMessage(query) { * Output all providers failed message */ function outputAllFailedMessage(query, errors) { - const errorDetails = errors - .map((e) => ` - ${e.provider}: ${e.error}`) - .join('\n'); + const errorDetails = errors.map((e) => ` - ${e.provider}: ${e.error}`).join('\n'); const message = [ '[WebSearch - All Providers Failed]',