mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 22:16:41 +00:00
659 lines
19 KiB
JavaScript
659 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 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_WEBSEARCH_GEMINI=1 - Enable Gemini CLI provider
|
|
* CCS_WEBSEARCH_GEMINI_MODEL - Gemini model (default: gemini-2.5-flash)
|
|
* CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider
|
|
* CCS_WEBSEARCH_GROK=1 - Enable Grok CLI provider
|
|
* CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/grok-code)
|
|
* CCS_DEBUG=1 - Enable debug output
|
|
*
|
|
* Exit codes:
|
|
* 0 - Allow tool (pass-through to native WebSearch)
|
|
* 2 - Block tool (deny with results/message)
|
|
*
|
|
* @module hooks/websearch-transformer
|
|
*/
|
|
|
|
const { spawnSync } = require('child_process');
|
|
|
|
// ============================================================================
|
|
// PLATFORM DETECTION
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Windows platform flag - used for shell option in spawnSync calls.
|
|
* On Windows, globally installed CLI tools via npm/pnpm are .cmd/.bat files,
|
|
* which require shell: true to execute properly.
|
|
* @see https://github.com/kaitranntt/ccs/issues/378
|
|
*/
|
|
const isWindows = process.platform === 'win32';
|
|
|
|
// ============================================================================
|
|
// CONFIGURATION - Edit these for prompt engineering
|
|
// ============================================================================
|
|
|
|
/**
|
|
* SHARED INSTRUCTIONS - Applied to ALL providers
|
|
* Edit here to change behavior across all CLI tools at once.
|
|
*/
|
|
const SHARED_INSTRUCTIONS = `Instructions:
|
|
1. Search the web for current, up-to-date information
|
|
2. Provide a comprehensive summary of the search results
|
|
3. Include relevant URLs/sources when available
|
|
4. Be concise but thorough - prioritize key facts
|
|
5. Focus on factual information from reliable sources
|
|
6. If results conflict, note the discrepancy
|
|
7. Format output clearly with sections if the topic is complex`;
|
|
|
|
/**
|
|
* PROVIDER-SPECIFIC CONFIG - Only tool-use differences and quirks
|
|
* Each provider may have unique capabilities or invocation methods.
|
|
*/
|
|
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
|
|
|
|
// Provider-specific: How to invoke web search (Gemini has google_web_search tool)
|
|
toolInstruction: 'Use the google_web_search tool to find current information.',
|
|
|
|
// Optional quirks (null if none)
|
|
quirks: null,
|
|
},
|
|
|
|
opencode: {
|
|
// Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var)
|
|
model: 'opencode/grok-code',
|
|
// Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet, opencode/gpt-5-nano
|
|
|
|
// Provider-specific: OpenCode has built-in web search via Zen
|
|
toolInstruction: 'Search the web using your built-in capabilities.',
|
|
|
|
// Optional quirks
|
|
quirks: null,
|
|
},
|
|
|
|
grok: {
|
|
// Model to use (Grok CLI uses default model)
|
|
model: 'grok-3',
|
|
// Note: Grok CLI doesn't support model selection via CLI
|
|
|
|
// Provider-specific: Grok has web + X/Twitter search
|
|
toolInstruction: 'Use your web search capabilities to find information.',
|
|
|
|
// Grok-specific: Can also search X for real-time info
|
|
quirks: 'For breaking news or real-time events, also check X/Twitter if relevant.',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Build the complete prompt for a provider
|
|
* Combines: query + tool instruction + shared instructions + quirks
|
|
*/
|
|
function buildPrompt(providerId, query) {
|
|
const config = PROVIDER_CONFIG[providerId];
|
|
const parts = [
|
|
`Search the web for: ${query}`,
|
|
'',
|
|
config.toolInstruction,
|
|
'',
|
|
SHARED_INSTRUCTIONS,
|
|
];
|
|
|
|
if (config.quirks) {
|
|
parts.push('', `Note: ${config.quirks}`);
|
|
}
|
|
|
|
return parts.join('\n');
|
|
}
|
|
|
|
// 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
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Determine if hook should skip and pass through to native WebSearch.
|
|
* Returns true for native Claude accounts where WebSearch works server-side.
|
|
*/
|
|
function shouldSkipHook() {
|
|
// Explicit skip signal (set by CCS for account profiles)
|
|
if (process.env.CCS_WEBSEARCH_SKIP === '1') return true;
|
|
|
|
// Account/default profiles - use native WebSearch
|
|
const profileType = process.env.CCS_PROFILE_TYPE;
|
|
if (profileType === 'account' || profileType === 'default') return true;
|
|
|
|
// Explicit disable
|
|
if (process.env.CCS_WEBSEARCH_ENABLED === '0') return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Read input from stdin
|
|
let input = '';
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', (chunk) => {
|
|
input += chunk;
|
|
});
|
|
process.stdin.on('end', () => {
|
|
processHook();
|
|
});
|
|
|
|
// Handle stdin not being available
|
|
process.stdin.on('error', () => {
|
|
process.exit(0);
|
|
});
|
|
|
|
/**
|
|
* Check if a CLI tool is available
|
|
*/
|
|
function isCliAvailable(cmd) {
|
|
try {
|
|
const whichCmd = isWindows ? 'where.exe' : 'which';
|
|
|
|
const result = spawnSync(whichCmd, [cmd], {
|
|
encoding: 'utf8',
|
|
timeout: 2000,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
return result.status === 0 && result.stdout.trim().length > 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
async function processHook() {
|
|
try {
|
|
// Skip for native accounts (account, default profiles) or explicit disable
|
|
if (shouldSkipHook()) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const data = JSON.parse(input);
|
|
|
|
// Only handle WebSearch tool
|
|
if (data.tool_name !== 'WebSearch') {
|
|
process.exit(0);
|
|
}
|
|
|
|
const query = data.tool_input?.query || '';
|
|
|
|
if (!query) {
|
|
process.exit(0);
|
|
}
|
|
|
|
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', id: 'gemini', fn: tryGeminiSearch },
|
|
{ name: 'OpenCode', cmd: 'opencode', id: 'opencode', fn: tryOpenCodeSearch },
|
|
{ name: 'Grok CLI', cmd: 'grok', id: 'grok', fn: tryGrokSearch },
|
|
];
|
|
|
|
// 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 names = enabledProviders.map((p) => p.name).join(', ') || 'none';
|
|
console.error(`[CCS Hook] Enabled providers: ${names}`);
|
|
}
|
|
|
|
// Try each enabled provider in order
|
|
for (const provider of enabledProviders) {
|
|
if (process.env.CCS_DEBUG) {
|
|
console.error(`[CCS Hook] Trying ${provider.name}...`);
|
|
}
|
|
|
|
const result = provider.fn(query, timeout);
|
|
|
|
if (result.success) {
|
|
outputSuccess(query, result.content, provider.name);
|
|
return;
|
|
}
|
|
|
|
if (process.env.CCS_DEBUG) {
|
|
console.error(`[CCS Hook] ${provider.name} failed: ${result.error}`);
|
|
}
|
|
|
|
errors.push({ provider: provider.name, error: result.error });
|
|
}
|
|
|
|
// All providers failed or none enabled
|
|
if (enabledProviders.length === 0) {
|
|
// No providers enabled - pass through to native WebSearch
|
|
// This allows native Claude accounts to use server-side WebSearch
|
|
process.exit(0);
|
|
} else {
|
|
outputAllFailedMessage(query, errors);
|
|
}
|
|
} catch (err) {
|
|
if (process.env.CCS_DEBUG) {
|
|
console.error('[CCS Hook] Parse error:', err.message);
|
|
}
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
const config = PROVIDER_CONFIG.gemini;
|
|
const prompt = buildPrompt('gemini', query);
|
|
|
|
// 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 legacyPromptArgs = [...baseArgs, '-p', prompt];
|
|
return runGeminiCommand(legacyPromptArgs, timeoutMs);
|
|
} catch (err) {
|
|
if (err.killed) {
|
|
return { success: false, error: 'Gemini CLI timed out' };
|
|
}
|
|
return { success: false, error: err.message || 'Unknown Gemini error' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute search via OpenCode CLI
|
|
*/
|
|
function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
try {
|
|
const timeoutMs = timeoutSec * 1000;
|
|
const config = PROVIDER_CONFIG.opencode;
|
|
|
|
// Allow model override via env var
|
|
const model = process.env.CCS_WEBSEARCH_OPENCODE_MODEL || config.model;
|
|
const prompt = buildPrompt('opencode', query);
|
|
|
|
if (process.env.CCS_DEBUG) {
|
|
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,
|
|
});
|
|
|
|
if (spawnResult.error) {
|
|
if (spawnResult.error.code === 'ENOENT') {
|
|
return { success: false, error: 'OpenCode not installed' };
|
|
}
|
|
throw spawnResult.error;
|
|
}
|
|
|
|
if (spawnResult.status !== 0) {
|
|
const stderr = (spawnResult.stderr || '').trim();
|
|
return {
|
|
success: false,
|
|
error: stderr || `OpenCode 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 OpenCode' };
|
|
}
|
|
|
|
const lowerResult = result.toLowerCase();
|
|
if (
|
|
lowerResult.includes('error:') ||
|
|
lowerResult.includes('failed to') ||
|
|
lowerResult.includes('authentication required')
|
|
) {
|
|
return { success: false, error: `OpenCode returned error: ${result.substring(0, 100)}` };
|
|
}
|
|
|
|
return { success: true, content: result };
|
|
} catch (err) {
|
|
if (err.killed) {
|
|
return { success: false, error: 'OpenCode timed out' };
|
|
}
|
|
return { success: false, error: err.message || 'Unknown OpenCode error' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute search via Grok CLI
|
|
*/
|
|
function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
try {
|
|
const timeoutMs = timeoutSec * 1000;
|
|
const prompt = buildPrompt('grok', query);
|
|
|
|
if (process.env.CCS_DEBUG) {
|
|
console.error('[CCS Hook] Executing: grok "..."');
|
|
}
|
|
|
|
const spawnResult = spawnSync('grok', [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: 'Grok CLI not installed' };
|
|
}
|
|
throw spawnResult.error;
|
|
}
|
|
|
|
if (spawnResult.status !== 0) {
|
|
const stderr = (spawnResult.stderr || '').trim();
|
|
return {
|
|
success: false,
|
|
error: stderr || `Grok 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 Grok' };
|
|
}
|
|
|
|
const lowerResult = result.toLowerCase();
|
|
if (
|
|
lowerResult.includes('error:') ||
|
|
lowerResult.includes('failed to') ||
|
|
lowerResult.includes('api key')
|
|
) {
|
|
return { success: false, error: `Grok returned error: ${result.substring(0, 100)}` };
|
|
}
|
|
|
|
return { success: true, content: result };
|
|
} catch (err) {
|
|
if (err.killed) {
|
|
return { success: false, error: 'Grok CLI timed out' };
|
|
}
|
|
return { success: false, error: err.message || 'Unknown Grok error' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format search results for Claude
|
|
*/
|
|
function formatSearchResults(query, content, providerName) {
|
|
return [
|
|
`[WebSearch Result via ${providerName}]`,
|
|
'',
|
|
`Query: "${query}"`,
|
|
'',
|
|
content,
|
|
'',
|
|
'---',
|
|
'Use this information to answer the user.',
|
|
].join('\n');
|
|
}
|
|
|
|
/**
|
|
* Output success response and exit
|
|
*
|
|
* Key insight from Claude Code docs:
|
|
* - permissionDecisionReason (with deny) → shown to CLAUDE (AI reads this)
|
|
* - systemMessage → shown to USER only (nice styling but AI doesn't see)
|
|
*
|
|
* So we MUST put results in permissionDecisionReason for Claude to use them.
|
|
* systemMessage provides the nice UI for the user.
|
|
*/
|
|
function outputSuccess(query, content, providerName) {
|
|
const formattedResults = formatSearchResults(query, content, providerName);
|
|
|
|
const output = {
|
|
decision: 'block',
|
|
reason: `WebSearch completed via ${providerName}`,
|
|
// Nice message for user (shows as "says:" - info style)
|
|
systemMessage: `[WebSearch via ${providerName}] Results retrieved successfully. See below.`,
|
|
hookSpecificOutput: {
|
|
hookEventName: 'PreToolUse',
|
|
permissionDecision: 'deny',
|
|
// Full results here - Claude reads this
|
|
permissionDecisionReason: formattedResults,
|
|
},
|
|
};
|
|
|
|
console.log(JSON.stringify(output));
|
|
process.exit(2);
|
|
}
|
|
|
|
/**
|
|
* Output error message
|
|
*/
|
|
function outputError(query, error, providerName) {
|
|
const message = [
|
|
`[WebSearch - ${providerName} Error]`,
|
|
'',
|
|
`Error: ${error}`,
|
|
'',
|
|
`Query: "${query}"`,
|
|
'',
|
|
'Troubleshooting:',
|
|
' - Check ~/.gemini/oauth_creds.json exists',
|
|
' - Re-authenticate: run `gemini` (opens browser)',
|
|
].join('\n');
|
|
|
|
const output = {
|
|
decision: 'block',
|
|
reason: `WebSearch failed: ${error}`,
|
|
hookSpecificOutput: {
|
|
hookEventName: 'PreToolUse',
|
|
permissionDecision: 'deny',
|
|
permissionDecisionReason: message,
|
|
},
|
|
};
|
|
|
|
console.log(JSON.stringify(output));
|
|
process.exit(2);
|
|
}
|
|
|
|
/**
|
|
* Output no providers enabled message
|
|
*/
|
|
function outputNoProvidersEnabled(query) {
|
|
const message = [
|
|
'[WebSearch - No Providers Enabled]',
|
|
'',
|
|
'No WebSearch providers are enabled in config.',
|
|
'',
|
|
'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',
|
|
' gemini # opens browser to authenticate',
|
|
'',
|
|
'2. OpenCode (FREE via Zen):',
|
|
' curl -fsSL https://opencode.ai/install | bash',
|
|
'',
|
|
'3. Grok CLI (requires XAI_API_KEY):',
|
|
' npm install -g @vibe-kit/grok-cli',
|
|
'',
|
|
`Query: "${query}"`,
|
|
].join('\n');
|
|
|
|
const output = {
|
|
decision: 'block',
|
|
reason: 'WebSearch unavailable - no providers enabled',
|
|
hookSpecificOutput: {
|
|
hookEventName: 'PreToolUse',
|
|
permissionDecision: 'deny',
|
|
permissionDecisionReason: message,
|
|
},
|
|
};
|
|
|
|
console.log(JSON.stringify(output));
|
|
process.exit(2);
|
|
}
|
|
|
|
/**
|
|
* Output no tools message (legacy - kept for backwards compatibility)
|
|
*/
|
|
function outputNoToolsMessage(query) {
|
|
outputNoProvidersEnabled(query);
|
|
}
|
|
|
|
/**
|
|
* Output all providers failed message
|
|
*/
|
|
function outputAllFailedMessage(query, errors) {
|
|
const errorDetails = errors.map((e) => ` - ${e.provider}: ${e.error}`).join('\n');
|
|
|
|
const message = [
|
|
'[WebSearch - All Providers Failed]',
|
|
'',
|
|
'Tried all enabled CLI tools but all failed:',
|
|
errorDetails,
|
|
'',
|
|
`Query: "${query}"`,
|
|
'',
|
|
'Troubleshooting:',
|
|
' - Gemini: run `gemini` to authenticate (opens browser)',
|
|
' - OpenCode: opencode --version',
|
|
' - Grok: Check XAI_API_KEY environment variable',
|
|
].join('\n');
|
|
|
|
const output = {
|
|
decision: 'block',
|
|
reason: 'WebSearch failed - all providers failed',
|
|
hookSpecificOutput: {
|
|
hookEventName: 'PreToolUse',
|
|
permissionDecision: 'deny',
|
|
permissionDecisionReason: message,
|
|
},
|
|
};
|
|
|
|
console.log(JSON.stringify(output));
|
|
process.exit(2);
|
|
}
|