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
*
* 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}"`,
+32 -2
View File
@@ -612,9 +612,39 @@ export function getWebSearchHookEnv(): Record<string, string> {
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;
}