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.
This commit is contained in:
kaitranntt
2025-12-16 22:30:37 -05:00
parent 110925e72e
commit e71cb6227c
2 changed files with 156 additions and 48 deletions
+124 -46
View File
@@ -3,13 +3,18 @@
* CCS WebSearch Hook - CLI Tool Executor with Fallback Chain * CCS WebSearch Hook - CLI Tool Executor with Fallback Chain
* *
* Intercepts Claude's WebSearch tool and executes search via CLI tools. * 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 * Supports automatic fallback: Gemini CLI → OpenCode → Grok CLI
* *
* Environment Variables (set by CCS): * Environment Variables (set by CCS):
* CCS_WEBSEARCH_SKIP=1 - Skip this hook entirely (for official Claude) * CCS_WEBSEARCH_SKIP=1 - Skip this hook entirely (for official Claude)
* CCS_WEBSEARCH_ENABLED=1 - Enable WebSearch (default: 1) * CCS_WEBSEARCH_ENABLED=1 - Enable WebSearch (default: 1)
* CCS_WEBSEARCH_TIMEOUT=55 - Timeout in seconds (default: 55) * CCS_WEBSEARCH_TIMEOUT=55 - Timeout in seconds (default: 55)
* CCS_DEBUG=1 - Enable debug output * 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: * Exit codes:
* 0 - Allow tool (pass-through to native WebSearch) * 0 - Allow tool (pass-through to native WebSearch)
@@ -20,12 +25,64 @@
const { spawnSync } = require('child_process'); 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 // Minimum response length to consider valid
const MIN_VALID_RESPONSE_LENGTH = 20; const MIN_VALID_RESPONSE_LENGTH = 20;
// Default timeout in seconds // Default timeout in seconds
const DEFAULT_TIMEOUT_SEC = 55; const DEFAULT_TIMEOUT_SEC = 55;
// ============================================================================
// HOOK LOGIC - Generally no need to edit below
// ============================================================================
// Read input from stdin // Read input from stdin
let input = ''; let input = '';
process.stdin.setEncoding('utf8'); 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 * 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); const timeout = parseInt(process.env.CCS_WEBSEARCH_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
// Fallback chain: Gemini → OpenCode → Grok // Fallback chain: Gemini → OpenCode → Grok
// Only include providers that are BOTH installed AND enabled in config
const providers = [ const providers = [
{ name: 'Gemini CLI', cmd: 'gemini', fn: tryGeminiSearch }, { name: 'Gemini CLI', cmd: 'gemini', id: 'gemini', fn: tryGeminiSearch },
{ name: 'OpenCode', cmd: 'opencode', fn: tryOpenCodeSearch }, { name: 'OpenCode', cmd: 'opencode', id: 'opencode', fn: tryOpenCodeSearch },
{ name: 'Grok CLI', cmd: 'grok', fn: tryGrokSearch }, { 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 = []; const errors = [];
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
const available = availableProviders.map((p) => p.name).join(', ') || 'none'; const names = enabledProviders.map((p) => p.name).join(', ') || 'none';
console.error(`[CCS Hook] Available providers: ${available}`); console.error(`[CCS Hook] Enabled providers: ${names}`);
} }
// Try each available provider in order // Try each enabled provider in order
for (const provider of availableProviders) { for (const provider of enabledProviders) {
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
console.error(`[CCS Hook] Trying ${provider.name}...`); console.error(`[CCS Hook] Trying ${provider.name}...`);
} }
@@ -122,9 +203,9 @@ async function processHook() {
errors.push({ provider: provider.name, error: result.error }); errors.push({ provider: provider.name, error: result.error });
} }
// All providers failed or none available // All providers failed or none enabled
if (availableProviders.length === 0) { if (enabledProviders.length === 0) {
outputNoToolsMessage(query); outputNoProvidersEnabled(query);
} else { } else {
outputAllFailedMessage(query, errors); outputAllFailedMessage(query, errors);
} }
@@ -142,25 +223,16 @@ async function processHook() {
function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
try { try {
const timeoutMs = timeoutSec * 1000; const timeoutMs = timeoutSec * 1000;
const config = PROVIDER_CONFIG.gemini;
const prompt = [ const prompt = config.prompt.replace('{query}', query);
`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');
if (process.env.CCS_DEBUG) { 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( const spawnResult = spawnSync(
'gemini', 'gemini',
['--model', 'gemini-2.5-flash', '--yolo', '-p', prompt], ['--model', config.model, '--yolo', '-p', prompt],
{ {
encoding: 'utf8', encoding: 'utf8',
timeout: timeoutMs, timeout: timeoutMs,
@@ -210,23 +282,23 @@ function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
/** /**
* Execute search via OpenCode CLI * Execute search via OpenCode CLI
* Uses opencode run with gpt-5-nano model for web search
*/ */
function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
try { try {
const timeoutMs = timeoutSec * 1000; const timeoutMs = timeoutSec * 1000;
const config = PROVIDER_CONFIG.opencode;
const prompt = `Search the web for: ${query} // Allow model override via env var
const model = process.env.CCS_WEBSEARCH_OPENCODE_MODEL || config.model;
Provide a comprehensive summary with relevant URLs/sources.`; const prompt = config.prompt.replace('{query}', query);
if (process.env.CCS_DEBUG) { 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( const spawnResult = spawnSync(
'opencode', 'opencode',
['run', prompt, '--model', 'opencode/gpt-5-nano'], ['run', prompt, '--model', model],
{ {
encoding: 'utf8', encoding: 'utf8',
timeout: timeoutMs, timeout: timeoutMs,
@@ -276,15 +348,12 @@ Provide a comprehensive summary with relevant URLs/sources.`;
/** /**
* Execute search via Grok CLI * Execute search via Grok CLI
* Uses grok command for web search (requires GROK_API_KEY)
*/ */
function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
try { try {
const timeoutMs = timeoutSec * 1000; const timeoutMs = timeoutSec * 1000;
const config = PROVIDER_CONFIG.grok;
const prompt = `Search the web for: ${query} const prompt = config.prompt.replace('{query}', query);
Provide a comprehensive summary with relevant URLs/sources.`;
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
console.error('[CCS Hook] Executing: grok "..."'); 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 = [ 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):', '1. Gemini CLI (FREE, 1000 req/day):',
' npm install -g @google/gemini-cli', ' npm install -g @google/gemini-cli',
@@ -427,7 +498,7 @@ function outputNoToolsMessage(query) {
const output = { const output = {
decision: 'block', decision: 'block',
reason: 'WebSearch unavailable - no CLI tools installed', reason: 'WebSearch unavailable - no providers enabled',
hookSpecificOutput: { hookSpecificOutput: {
hookEventName: 'PreToolUse', hookEventName: 'PreToolUse',
permissionDecision: 'deny', permissionDecision: 'deny',
@@ -439,6 +510,13 @@ function outputNoToolsMessage(query) {
process.exit(2); process.exit(2);
} }
/**
* Output no tools message (legacy - kept for backwards compatibility)
*/
function outputNoToolsMessage(query) {
outputNoProvidersEnabled(query);
}
/** /**
* Output all providers failed message * Output all providers failed message
*/ */
@@ -450,7 +528,7 @@ function outputAllFailedMessage(query, errors) {
const message = [ const message = [
'[WebSearch - All Providers Failed]', '[WebSearch - All Providers Failed]',
'', '',
'Tried all available CLI tools but all failed:', 'Tried all enabled CLI tools but all failed:',
errorDetails, errorDetails,
'', '',
`Query: "${query}"`, `Query: "${query}"`,
+32 -2
View File
@@ -612,9 +612,39 @@ export function getWebSearchHookEnv(): Record<string, string> {
return env; return env;
} }
// Pass simple config to hook // Pass master switch
env.CCS_WEBSEARCH_ENABLED = '1'; 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; return env;
} }