mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
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:
@@ -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}"`,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user