From e71cb6227cfb114ca1864eea5198854604d83c52 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 16 Dec 2025 22:30:37 -0500 Subject: [PATCH] feat(websearch): respect config provider settings and consolidate prompts Major refactor of WebSearch hook to: 1. **Respect config.yaml settings**: Hook now reads CCS_WEBSEARCH_GEMINI, CCS_WEBSEARCH_OPENCODE, CCS_WEBSEARCH_GROK env vars to determine which providers to use. Only enabled AND installed providers are tried. 2. **Consolidate prompts/models**: Added PROVIDER_CONFIG section at top of hook file for easy prompt engineering: - gemini: model + prompt template - opencode: model (overridable via env) + prompt template - grok: model + prompt template 3. **Pass provider states via env**: Updated getWebSearchHookEnv() to pass individual provider enabled states as env vars. Breaking change: Hook no longer falls back to all installed CLIs. It now strictly respects config.yaml settings. --- lib/hooks/websearch-transformer.cjs | 170 ++++++++++++++++++++-------- src/utils/websearch-manager.ts | 34 +++++- 2 files changed, 156 insertions(+), 48 deletions(-) diff --git a/lib/hooks/websearch-transformer.cjs b/lib/hooks/websearch-transformer.cjs index 26dc974d..38e92593 100644 --- a/lib/hooks/websearch-transformer.cjs +++ b/lib/hooks/websearch-transformer.cjs @@ -3,13 +3,18 @@ * CCS WebSearch Hook - CLI Tool Executor with Fallback Chain * * Intercepts Claude's WebSearch tool and executes search via CLI tools. + * Respects provider enabled states from config.yaml. * Supports automatic fallback: Gemini CLI → OpenCode → Grok CLI * * Environment Variables (set by CCS): - * CCS_WEBSEARCH_SKIP=1 - Skip this hook entirely (for official Claude) - * CCS_WEBSEARCH_ENABLED=1 - Enable WebSearch (default: 1) - * CCS_WEBSEARCH_TIMEOUT=55 - Timeout in seconds (default: 55) - * CCS_DEBUG=1 - Enable debug output + * CCS_WEBSEARCH_SKIP=1 - Skip this hook entirely (for official Claude) + * CCS_WEBSEARCH_ENABLED=1 - Enable WebSearch (default: 1) + * CCS_WEBSEARCH_TIMEOUT=55 - Timeout in seconds (default: 55) + * CCS_WEBSEARCH_GEMINI=1 - Enable Gemini CLI provider + * CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider + * CCS_WEBSEARCH_GROK=1 - Enable Grok CLI provider + * CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/gpt-5-nano) + * CCS_DEBUG=1 - Enable debug output * * Exit codes: * 0 - Allow tool (pass-through to native WebSearch) @@ -20,12 +25,64 @@ const { spawnSync } = require('child_process'); +// ============================================================================ +// CONFIGURATION - Edit these for prompt engineering +// ============================================================================ + +/** + * Provider configurations - models and prompts for each CLI tool. + * Edit these to customize search behavior. + */ +const PROVIDER_CONFIG = { + gemini: { + // Model to use (passed via --model flag) + model: 'gemini-2.5-flash', + // Alternative free models: gemini-2.0-flash, gemini-1.5-flash + + // Prompt template - {query} will be replaced with search query + prompt: `Search the web for: {query} + +Instructions: +1. Use the google_web_search tool to find current information +2. Provide a comprehensive summary of the search results +3. Include relevant URLs/sources when available +4. Be concise but thorough +5. Focus on factual, up-to-date information`, + }, + + opencode: { + // Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var) + model: 'opencode/gpt-5-nano', + // Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet + + // Prompt template + prompt: `Search the web for: {query} + +Provide a comprehensive summary with relevant URLs/sources.`, + }, + + grok: { + // Model to use (Grok CLI uses default model) + model: 'grok-3', + // Note: Grok CLI doesn't support model selection via CLI + + // Prompt template + prompt: `Search the web for: {query} + +Provide a comprehensive summary with relevant URLs/sources.`, + }, +}; + // Minimum response length to consider valid const MIN_VALID_RESPONSE_LENGTH = 20; // Default timeout in seconds const DEFAULT_TIMEOUT_SEC = 55; +// ============================================================================ +// HOOK LOGIC - Generally no need to edit below +// ============================================================================ + // Read input from stdin let input = ''; process.stdin.setEncoding('utf8'); @@ -57,6 +114,18 @@ function isCliAvailable(cmd) { } } +/** + * Check if provider is enabled via environment variable + */ +function isProviderEnabled(provider) { + const envVar = `CCS_WEBSEARCH_${provider.toUpperCase()}`; + const value = process.env[envVar]; + + // If env var not set, provider is disabled by default + // This ensures we respect config.yaml settings + return value === '1'; +} + /** * Main hook processing logic with fallback chain */ @@ -88,22 +157,34 @@ async function processHook() { const timeout = parseInt(process.env.CCS_WEBSEARCH_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10); // Fallback chain: Gemini → OpenCode → Grok + // Only include providers that are BOTH installed AND enabled in config const providers = [ - { name: 'Gemini CLI', cmd: 'gemini', fn: tryGeminiSearch }, - { name: 'OpenCode', cmd: 'opencode', fn: tryOpenCodeSearch }, - { name: 'Grok CLI', cmd: 'grok', fn: tryGrokSearch }, + { name: 'Gemini CLI', cmd: 'gemini', id: 'gemini', fn: tryGeminiSearch }, + { name: 'OpenCode', cmd: 'opencode', id: 'opencode', fn: tryOpenCodeSearch }, + { name: 'Grok CLI', cmd: 'grok', id: 'grok', fn: tryGrokSearch }, ]; - const availableProviders = providers.filter((p) => isCliAvailable(p.cmd)); + // Filter to only enabled AND available providers + const enabledProviders = providers.filter((p) => { + const enabled = isProviderEnabled(p.id); + const available = isCliAvailable(p.cmd); + + if (process.env.CCS_DEBUG) { + console.error(`[CCS Hook] ${p.name}: enabled=${enabled}, available=${available}`); + } + + return enabled && available; + }); + const errors = []; if (process.env.CCS_DEBUG) { - const available = availableProviders.map((p) => p.name).join(', ') || 'none'; - console.error(`[CCS Hook] Available providers: ${available}`); + const names = enabledProviders.map((p) => p.name).join(', ') || 'none'; + console.error(`[CCS Hook] Enabled providers: ${names}`); } - // Try each available provider in order - for (const provider of availableProviders) { + // Try each enabled provider in order + for (const provider of enabledProviders) { if (process.env.CCS_DEBUG) { console.error(`[CCS Hook] Trying ${provider.name}...`); } @@ -122,9 +203,9 @@ async function processHook() { errors.push({ provider: provider.name, error: result.error }); } - // All providers failed or none available - if (availableProviders.length === 0) { - outputNoToolsMessage(query); + // All providers failed or none enabled + if (enabledProviders.length === 0) { + outputNoProvidersEnabled(query); } else { outputAllFailedMessage(query, errors); } @@ -142,25 +223,16 @@ async function processHook() { function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { try { const timeoutMs = timeoutSec * 1000; - - const prompt = [ - `Search the web for: ${query}`, - '', - 'Instructions:', - '1. Use the google_web_search tool to find current information', - '2. Provide a comprehensive summary of the search results', - '3. Include relevant URLs/sources when available', - '4. Be concise but thorough', - '5. Focus on factual, up-to-date information', - ].join('\n'); + const config = PROVIDER_CONFIG.gemini; + const prompt = config.prompt.replace('{query}', query); if (process.env.CCS_DEBUG) { - console.error('[CCS Hook] Executing: gemini --model gemini-2.5-flash --yolo -p "..."'); + console.error(`[CCS Hook] Executing: gemini --model ${config.model} --yolo -p "..."`); } const spawnResult = spawnSync( 'gemini', - ['--model', 'gemini-2.5-flash', '--yolo', '-p', prompt], + ['--model', config.model, '--yolo', '-p', prompt], { encoding: 'utf8', timeout: timeoutMs, @@ -210,23 +282,23 @@ function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { /** * Execute search via OpenCode CLI - * Uses opencode run with gpt-5-nano model for web search */ function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { try { const timeoutMs = timeoutSec * 1000; + const config = PROVIDER_CONFIG.opencode; - const prompt = `Search the web for: ${query} - -Provide a comprehensive summary with relevant URLs/sources.`; + // Allow model override via env var + const model = process.env.CCS_WEBSEARCH_OPENCODE_MODEL || config.model; + const prompt = config.prompt.replace('{query}', query); if (process.env.CCS_DEBUG) { - console.error('[CCS Hook] Executing: opencode run --model opencode/gpt-5-nano "..."'); + console.error(`[CCS Hook] Executing: opencode run --model ${model} "..."`); } const spawnResult = spawnSync( 'opencode', - ['run', prompt, '--model', 'opencode/gpt-5-nano'], + ['run', prompt, '--model', model], { encoding: 'utf8', timeout: timeoutMs, @@ -276,15 +348,12 @@ Provide a comprehensive summary with relevant URLs/sources.`; /** * Execute search via Grok CLI - * Uses grok command for web search (requires GROK_API_KEY) */ function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { try { const timeoutMs = timeoutSec * 1000; - - const prompt = `Search the web for: ${query} - -Provide a comprehensive summary with relevant URLs/sources.`; + const config = PROVIDER_CONFIG.grok; + const prompt = config.prompt.replace('{query}', query); if (process.env.CCS_DEBUG) { console.error('[CCS Hook] Executing: grok "..."'); @@ -402,15 +471,17 @@ function outputError(query, error, providerName) { } /** - * Output no tools message + * Output no providers enabled message */ -function outputNoToolsMessage(query) { +function outputNoProvidersEnabled(query) { const message = [ - '[WebSearch - No CLI Tools Available]', + '[WebSearch - No Providers Enabled]', '', - 'WebSearch requires a CLI tool to be installed.', + 'No WebSearch providers are enabled in config.', '', - 'Install one of the following (in order of preference):', + 'To enable: Run `ccs config` and enable a provider.', + '', + 'Or install one of the following CLI tools:', '', '1. Gemini CLI (FREE, 1000 req/day):', ' npm install -g @google/gemini-cli', @@ -427,7 +498,7 @@ function outputNoToolsMessage(query) { const output = { decision: 'block', - reason: 'WebSearch unavailable - no CLI tools installed', + reason: 'WebSearch unavailable - no providers enabled', hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', @@ -439,6 +510,13 @@ function outputNoToolsMessage(query) { process.exit(2); } +/** + * Output no tools message (legacy - kept for backwards compatibility) + */ +function outputNoToolsMessage(query) { + outputNoProvidersEnabled(query); +} + /** * Output all providers failed message */ @@ -450,7 +528,7 @@ function outputAllFailedMessage(query, errors) { const message = [ '[WebSearch - All Providers Failed]', '', - 'Tried all available CLI tools but all failed:', + 'Tried all enabled CLI tools but all failed:', errorDetails, '', `Query: "${query}"`, diff --git a/src/utils/websearch-manager.ts b/src/utils/websearch-manager.ts index b7e61705..aae724cc 100644 --- a/src/utils/websearch-manager.ts +++ b/src/utils/websearch-manager.ts @@ -612,9 +612,39 @@ export function getWebSearchHookEnv(): Record { return env; } - // Pass simple config to hook + // Pass master switch env.CCS_WEBSEARCH_ENABLED = '1'; - env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers?.gemini?.timeout || 55); + + // Pass individual provider enabled states + // Hook will only use providers that are BOTH enabled AND installed + if (wsConfig.providers?.gemini?.enabled) { + env.CCS_WEBSEARCH_GEMINI = '1'; + env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.gemini.timeout || 55); + } + + if (wsConfig.providers?.opencode?.enabled) { + env.CCS_WEBSEARCH_OPENCODE = '1'; + if (wsConfig.providers.opencode.model) { + env.CCS_WEBSEARCH_OPENCODE_MODEL = wsConfig.providers.opencode.model; + } + // Use opencode timeout if no gemini timeout set + if (!env.CCS_WEBSEARCH_TIMEOUT) { + env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.opencode.timeout || 60); + } + } + + if (wsConfig.providers?.grok?.enabled) { + env.CCS_WEBSEARCH_GROK = '1'; + // Use grok timeout if no other timeout set + if (!env.CCS_WEBSEARCH_TIMEOUT) { + env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.grok.timeout || 55); + } + } + + // Default timeout if none set + if (!env.CCS_WEBSEARCH_TIMEOUT) { + env.CCS_WEBSEARCH_TIMEOUT = '55'; + } return env; }