@jonnyhoo/ccs 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ccs-delegation/SKILL.md +7 -7
- package/.claude/skills/ccs-delegation/references/troubleshooting.md +20 -20
- package/config/base-codex.settings.json +3 -3
- package/dist/api/services/profile-writer.d.ts +1 -1
- package/dist/api/services/profile-writer.d.ts.map +1 -1
- package/dist/api/services/profile-writer.js +8 -9
- package/dist/api/services/profile-writer.js.map +1 -1
- package/dist/auth/profile-detector.d.ts +3 -1
- package/dist/auth/profile-detector.d.ts.map +1 -1
- package/dist/auth/profile-detector.js +16 -17
- package/dist/auth/profile-detector.js.map +1 -1
- package/dist/ccs.js +303 -44
- package/dist/ccs.js.map +1 -1
- package/dist/cliproxy/anthropic-to-openai-proxy.d.ts +15 -1
- package/dist/cliproxy/anthropic-to-openai-proxy.d.ts.map +1 -1
- package/dist/cliproxy/anthropic-to-openai-proxy.js +1100 -296
- package/dist/cliproxy/anthropic-to-openai-proxy.js.map +1 -1
- package/dist/cliproxy/cliproxy-executor.d.ts.map +1 -1
- package/dist/cliproxy/cliproxy-executor.js +58 -44
- package/dist/cliproxy/cliproxy-executor.js.map +1 -1
- package/dist/cliproxy/config-generator.d.ts.map +1 -1
- package/dist/cliproxy/config-generator.js +16 -2
- package/dist/cliproxy/config-generator.js.map +1 -1
- package/dist/cliproxy/model-catalog.d.ts.map +1 -1
- package/dist/cliproxy/model-catalog.js +12 -1
- package/dist/cliproxy/model-catalog.js.map +1 -1
- package/dist/cliproxy/services/variant-settings.d.ts.map +1 -1
- package/dist/cliproxy/services/variant-settings.js +0 -5
- package/dist/cliproxy/services/variant-settings.js.map +1 -1
- package/dist/cliproxy/tool-name-sanitizer.d.ts +17 -21
- package/dist/cliproxy/tool-name-sanitizer.d.ts.map +1 -1
- package/dist/cliproxy/tool-name-sanitizer.js +46 -33
- package/dist/cliproxy/tool-name-sanitizer.js.map +1 -1
- package/dist/cliproxy/tool-sanitization-proxy.d.ts.map +1 -1
- package/dist/cliproxy/tool-sanitization-proxy.js +18 -1
- package/dist/cliproxy/tool-sanitization-proxy.js.map +1 -1
- package/dist/commands/api-command.d.ts.map +1 -1
- package/dist/commands/api-command.js +21 -1
- package/dist/commands/api-command.js.map +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +15 -107
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/index.d.ts +0 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -3
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/install-command.d.ts.map +1 -1
- package/dist/commands/install-command.js +2 -9
- package/dist/commands/install-command.js.map +1 -1
- package/dist/commands/router.js +1 -1
- package/dist/commands/router.js.map +1 -1
- package/dist/config/unified-config-loader.d.ts +0 -32
- package/dist/config/unified-config-loader.d.ts.map +1 -1
- package/dist/config/unified-config-loader.js +6 -95
- package/dist/config/unified-config-loader.js.map +1 -1
- package/dist/config/unified-config-types.d.ts +3 -75
- package/dist/config/unified-config-types.d.ts.map +1 -1
- package/dist/config/unified-config-types.js +0 -20
- package/dist/config/unified-config-types.js.map +1 -1
- package/dist/delegation/background-monitor.d.ts +80 -0
- package/dist/delegation/background-monitor.d.ts.map +1 -0
- package/dist/delegation/background-monitor.js +295 -0
- package/dist/delegation/background-monitor.js.map +1 -0
- package/dist/delegation/delegation-handler.d.ts +2 -1
- package/dist/delegation/delegation-handler.d.ts.map +1 -1
- package/dist/delegation/delegation-handler.js +48 -30
- package/dist/delegation/delegation-handler.js.map +1 -1
- package/dist/delegation/executor/types.d.ts +2 -0
- package/dist/delegation/executor/types.d.ts.map +1 -1
- package/dist/delegation/headless-executor.d.ts +3 -0
- package/dist/delegation/headless-executor.d.ts.map +1 -1
- package/dist/delegation/headless-executor.js +162 -14
- package/dist/delegation/headless-executor.js.map +1 -1
- package/dist/hello.d.ts +7 -0
- package/dist/hello.d.ts.map +1 -0
- package/dist/hello.js +13 -0
- package/dist/hello.js.map +1 -0
- package/dist/management/checks/image-analysis-check.d.ts.map +1 -1
- package/dist/management/checks/image-analysis-check.js.map +1 -1
- package/dist/router/index.d.ts +3 -3
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/scenario-router.d.ts +2 -8
- package/dist/router/scenario-router.d.ts.map +1 -1
- package/dist/router/scenario-router.js +3 -22
- package/dist/router/scenario-router.js.map +1 -1
- package/dist/router/scenario-routing-proxy.d.ts.map +1 -1
- package/dist/router/scenario-routing-proxy.js +19 -4
- package/dist/router/scenario-routing-proxy.js.map +1 -1
- package/dist/router/settings-routing-executor.d.ts.map +1 -1
- package/dist/router/settings-routing-executor.js.map +1 -1
- package/dist/router/types.d.ts +1 -1
- package/dist/router/types.d.ts.map +1 -1
- package/dist/utils/shell-executor.d.ts.map +1 -1
- package/dist/utils/shell-executor.js +1 -6
- package/dist/utils/shell-executor.js.map +1 -1
- package/dist/web-server/health/index.d.ts +0 -1
- package/dist/web-server/health/index.d.ts.map +1 -1
- package/dist/web-server/health/index.js +1 -4
- package/dist/web-server/health/index.js.map +1 -1
- package/dist/web-server/health-service.d.ts.map +1 -1
- package/dist/web-server/health-service.js +0 -4
- package/dist/web-server/health-service.js.map +1 -1
- package/dist/web-server/routes/index.d.ts.map +1 -1
- package/dist/web-server/routes/index.js +0 -3
- package/dist/web-server/routes/index.js.map +1 -1
- package/dist/web-server/routes/settings-routes.d.ts.map +1 -1
- package/dist/web-server/routes/settings-routes.js +0 -4
- package/dist/web-server/routes/settings-routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/background-redis-bridge.js +320 -0
- package/scripts/monitor-task.js +313 -0
- package/dist/commands/cliproxy-command.d.ts +0 -21
- package/dist/commands/cliproxy-command.d.ts.map +0 -1
- package/dist/commands/cliproxy-command.js +0 -1099
- package/dist/commands/cliproxy-command.js.map +0 -1
- package/dist/commands/cliproxy-sync-handler.d.ts +0 -19
- package/dist/commands/cliproxy-sync-handler.d.ts.map +0 -1
- package/dist/commands/cliproxy-sync-handler.js +0 -99
- package/dist/commands/cliproxy-sync-handler.js.map +0 -1
- package/dist/utils/websearch/gemini-cli.d.ts +0 -36
- package/dist/utils/websearch/gemini-cli.d.ts.map +0 -1
- package/dist/utils/websearch/gemini-cli.js +0 -132
- package/dist/utils/websearch/gemini-cli.js.map +0 -1
- package/dist/utils/websearch/grok-cli.d.ts +0 -26
- package/dist/utils/websearch/grok-cli.d.ts.map +0 -1
- package/dist/utils/websearch/grok-cli.js +0 -81
- package/dist/utils/websearch/grok-cli.js.map +0 -1
- package/dist/utils/websearch/hook-config.d.ts +0 -27
- package/dist/utils/websearch/hook-config.d.ts.map +0 -1
- package/dist/utils/websearch/hook-config.js +0 -280
- package/dist/utils/websearch/hook-config.js.map +0 -1
- package/dist/utils/websearch/hook-env.d.ts +0 -16
- package/dist/utils/websearch/hook-env.d.ts.map +0 -1
- package/dist/utils/websearch/hook-env.js +0 -62
- package/dist/utils/websearch/hook-env.js.map +0 -1
- package/dist/utils/websearch/hook-installer.d.ts +0 -30
- package/dist/utils/websearch/hook-installer.d.ts.map +0 -1
- package/dist/utils/websearch/hook-installer.js +0 -148
- package/dist/utils/websearch/hook-installer.js.map +0 -1
- package/dist/utils/websearch/hook-utils.d.ts +0 -18
- package/dist/utils/websearch/hook-utils.d.ts.map +0 -1
- package/dist/utils/websearch/hook-utils.js +0 -59
- package/dist/utils/websearch/hook-utils.js.map +0 -1
- package/dist/utils/websearch/index.d.ts +0 -17
- package/dist/utils/websearch/index.d.ts.map +0 -1
- package/dist/utils/websearch/index.js +0 -51
- package/dist/utils/websearch/index.js.map +0 -1
- package/dist/utils/websearch/opencode-cli.d.ts +0 -26
- package/dist/utils/websearch/opencode-cli.d.ts.map +0 -1
- package/dist/utils/websearch/opencode-cli.js +0 -81
- package/dist/utils/websearch/opencode-cli.js.map +0 -1
- package/dist/utils/websearch/profile-hook-injector.d.ts +0 -20
- package/dist/utils/websearch/profile-hook-injector.d.ts.map +0 -1
- package/dist/utils/websearch/profile-hook-injector.js +0 -250
- package/dist/utils/websearch/profile-hook-injector.js.map +0 -1
- package/dist/utils/websearch/status.d.ts +0 -36
- package/dist/utils/websearch/status.d.ts.map +0 -1
- package/dist/utils/websearch/status.js +0 -192
- package/dist/utils/websearch/status.js.map +0 -1
- package/dist/utils/websearch/types.d.ts +0 -85
- package/dist/utils/websearch/types.d.ts.map +0 -1
- package/dist/utils/websearch/types.js +0 -10
- package/dist/utils/websearch/types.js.map +0 -1
- package/dist/utils/websearch-manager.d.ts +0 -31
- package/dist/utils/websearch-manager.d.ts.map +0 -1
- package/dist/utils/websearch-manager.js +0 -72
- package/dist/utils/websearch-manager.js.map +0 -1
- package/dist/web-server/health/websearch-checks.d.ts +0 -11
- package/dist/web-server/health/websearch-checks.d.ts.map +0 -1
- package/dist/web-server/health/websearch-checks.js +0 -53
- package/dist/web-server/health/websearch-checks.js.map +0 -1
- package/dist/web-server/routes/websearch-routes.d.ts +0 -6
- package/dist/web-server/routes/websearch-routes.d.ts.map +0 -1
- package/dist/web-server/routes/websearch-routes.js +0 -130
- package/dist/web-server/routes/websearch-routes.js.map +0 -1
- package/lib/hooks/block-websearch.cjs +0 -75
- package/lib/hooks/websearch-transformer.cjs +0 -632
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* CCS WebSearch Blocking Hook
|
|
4
|
-
*
|
|
5
|
-
* Blocks Claude's native WebSearch tool and redirects to MCP alternative.
|
|
6
|
-
* This is a PreToolUse hook that runs BEFORE the tool is executed.
|
|
7
|
-
*
|
|
8
|
-
* WebSearch is a server-side tool executed by Anthropic's API.
|
|
9
|
-
* Third-party providers (gemini, agy, codex, qwen) don't have access.
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* Configured in ~/.claude/settings.json:
|
|
13
|
-
* {
|
|
14
|
-
* "hooks": {
|
|
15
|
-
* "PreToolUse": [{
|
|
16
|
-
* "matcher": "WebSearch",
|
|
17
|
-
* "hooks": [{
|
|
18
|
-
* "type": "command",
|
|
19
|
-
* "command": "node ~/.ccs/hooks/block-websearch.cjs",
|
|
20
|
-
* "timeout": 5
|
|
21
|
-
* }]
|
|
22
|
-
* }]
|
|
23
|
-
* }
|
|
24
|
-
* }
|
|
25
|
-
*
|
|
26
|
-
* Exit codes:
|
|
27
|
-
* 0 - Allow tool (pass-through)
|
|
28
|
-
* 2 - Block tool (deny with message)
|
|
29
|
-
*
|
|
30
|
-
* @module hooks/block-websearch
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
// Read input from stdin
|
|
34
|
-
let input = '';
|
|
35
|
-
process.stdin.setEncoding('utf8');
|
|
36
|
-
process.stdin.on('data', (chunk) => {
|
|
37
|
-
input += chunk;
|
|
38
|
-
});
|
|
39
|
-
process.stdin.on('end', () => {
|
|
40
|
-
try {
|
|
41
|
-
const data = JSON.parse(input);
|
|
42
|
-
|
|
43
|
-
// Only block WebSearch tool
|
|
44
|
-
if (data.tool_name === 'WebSearch') {
|
|
45
|
-
const query = data.tool_input?.query || '';
|
|
46
|
-
|
|
47
|
-
const output = {
|
|
48
|
-
decision: 'block',
|
|
49
|
-
reason: 'WebSearch unavailable with current provider',
|
|
50
|
-
hookSpecificOutput: {
|
|
51
|
-
hookEventName: 'PreToolUse',
|
|
52
|
-
permissionDecision: 'deny',
|
|
53
|
-
permissionDecisionReason: `WebSearch is not available with third-party providers. Use mcp__web-search-prime__webSearchPrime tool instead with the same query: "${query}"`,
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
console.log(JSON.stringify(output));
|
|
58
|
-
process.exit(2); // Exit code 2 = block
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Allow all other tools
|
|
62
|
-
process.exit(0);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
// Don't block on parse errors - allow tool to proceed
|
|
65
|
-
if (process.env.CCS_DEBUG) {
|
|
66
|
-
console.error('[CCS Hook] Parse error:', err.message);
|
|
67
|
-
}
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Handle stdin not being available
|
|
73
|
-
process.stdin.on('error', () => {
|
|
74
|
-
process.exit(0);
|
|
75
|
-
});
|
|
@@ -1,632 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* CCS WebSearch Hook - CLI Tool Executor with Fallback Chain
|
|
4
|
-
*
|
|
5
|
-
* Intercepts Claude's WebSearch tool and executes search via CLI tools.
|
|
6
|
-
* Respects provider enabled states from config.yaml.
|
|
7
|
-
* Supports automatic fallback: Gemini CLI → OpenCode → Grok CLI
|
|
8
|
-
*
|
|
9
|
-
* Environment Variables (set by CCS):
|
|
10
|
-
* CCS_WEBSEARCH_SKIP=1 - Skip this hook entirely (for official Claude)
|
|
11
|
-
* CCS_WEBSEARCH_ENABLED=1 - Enable WebSearch (default: 1)
|
|
12
|
-
* CCS_WEBSEARCH_TIMEOUT=55 - Timeout in seconds (default: 55)
|
|
13
|
-
* CCS_WEBSEARCH_GEMINI=1 - Enable Gemini CLI provider
|
|
14
|
-
* CCS_WEBSEARCH_GEMINI_MODEL - Gemini model (default: gemini-2.5-flash)
|
|
15
|
-
* CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider
|
|
16
|
-
* CCS_WEBSEARCH_GROK=1 - Enable Grok CLI provider
|
|
17
|
-
* CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/grok-code)
|
|
18
|
-
* CCS_DEBUG=1 - Enable debug output
|
|
19
|
-
*
|
|
20
|
-
* Exit codes:
|
|
21
|
-
* 0 - Allow tool (pass-through to native WebSearch)
|
|
22
|
-
* 2 - Block tool (deny with results/message)
|
|
23
|
-
*
|
|
24
|
-
* @module hooks/websearch-transformer
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const { spawnSync } = require('child_process');
|
|
28
|
-
|
|
29
|
-
// ============================================================================
|
|
30
|
-
// PLATFORM DETECTION
|
|
31
|
-
// ============================================================================
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Windows platform flag - used for shell option in spawnSync calls.
|
|
35
|
-
* On Windows, globally installed CLI tools via npm/pnpm are .cmd/.bat files,
|
|
36
|
-
* which require shell: true to execute properly.
|
|
37
|
-
* @see https://github.com/kaitranntt/ccs/issues/378
|
|
38
|
-
*/
|
|
39
|
-
const isWindows = process.platform === 'win32';
|
|
40
|
-
|
|
41
|
-
// ============================================================================
|
|
42
|
-
// CONFIGURATION - Edit these for prompt engineering
|
|
43
|
-
// ============================================================================
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* SHARED INSTRUCTIONS - Applied to ALL providers
|
|
47
|
-
* Edit here to change behavior across all CLI tools at once.
|
|
48
|
-
*/
|
|
49
|
-
const SHARED_INSTRUCTIONS = `Instructions:
|
|
50
|
-
1. Search the web for current, up-to-date information
|
|
51
|
-
2. Provide a comprehensive summary of the search results
|
|
52
|
-
3. Include relevant URLs/sources when available
|
|
53
|
-
4. Be concise but thorough - prioritize key facts
|
|
54
|
-
5. Focus on factual information from reliable sources
|
|
55
|
-
6. If results conflict, note the discrepancy
|
|
56
|
-
7. Format output clearly with sections if the topic is complex`;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* PROVIDER-SPECIFIC CONFIG - Only tool-use differences and quirks
|
|
60
|
-
* Each provider may have unique capabilities or invocation methods.
|
|
61
|
-
*/
|
|
62
|
-
const PROVIDER_CONFIG = {
|
|
63
|
-
gemini: {
|
|
64
|
-
// Model to use (passed via --model flag)
|
|
65
|
-
model: 'gemini-2.5-flash',
|
|
66
|
-
// Alternative free models: gemini-2.0-flash, gemini-1.5-flash
|
|
67
|
-
|
|
68
|
-
// Provider-specific: How to invoke web search (Gemini has google_web_search tool)
|
|
69
|
-
toolInstruction: 'Use the google_web_search tool to find current information.',
|
|
70
|
-
|
|
71
|
-
// Optional quirks (null if none)
|
|
72
|
-
quirks: null,
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
opencode: {
|
|
76
|
-
// Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var)
|
|
77
|
-
model: 'opencode/grok-code',
|
|
78
|
-
// Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet, opencode/gpt-5-nano
|
|
79
|
-
|
|
80
|
-
// Provider-specific: OpenCode has built-in web search via Zen
|
|
81
|
-
toolInstruction: 'Search the web using your built-in capabilities.',
|
|
82
|
-
|
|
83
|
-
// Optional quirks
|
|
84
|
-
quirks: null,
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
grok: {
|
|
88
|
-
// Model to use (Grok CLI uses default model)
|
|
89
|
-
model: 'grok-3',
|
|
90
|
-
// Note: Grok CLI doesn't support model selection via CLI
|
|
91
|
-
|
|
92
|
-
// Provider-specific: Grok has web + X/Twitter search
|
|
93
|
-
toolInstruction: 'Use your web search capabilities to find information.',
|
|
94
|
-
|
|
95
|
-
// Grok-specific: Can also search X for real-time info
|
|
96
|
-
quirks: 'For breaking news or real-time events, also check X/Twitter if relevant.',
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Build the complete prompt for a provider
|
|
102
|
-
* Combines: query + tool instruction + shared instructions + quirks
|
|
103
|
-
*/
|
|
104
|
-
function buildPrompt(providerId, query) {
|
|
105
|
-
const config = PROVIDER_CONFIG[providerId];
|
|
106
|
-
const parts = [
|
|
107
|
-
`Search the web for: ${query}`,
|
|
108
|
-
'',
|
|
109
|
-
config.toolInstruction,
|
|
110
|
-
'',
|
|
111
|
-
SHARED_INSTRUCTIONS,
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
if (config.quirks) {
|
|
115
|
-
parts.push('', `Note: ${config.quirks}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return parts.join('\n');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Minimum response length to consider valid
|
|
122
|
-
const MIN_VALID_RESPONSE_LENGTH = 20;
|
|
123
|
-
|
|
124
|
-
// Default timeout in seconds
|
|
125
|
-
const DEFAULT_TIMEOUT_SEC = 55;
|
|
126
|
-
|
|
127
|
-
// ============================================================================
|
|
128
|
-
// HOOK LOGIC - Generally no need to edit below
|
|
129
|
-
// ============================================================================
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Determine if hook should skip and pass through to native WebSearch.
|
|
133
|
-
* Returns true for native Claude accounts where WebSearch works server-side.
|
|
134
|
-
*/
|
|
135
|
-
function shouldSkipHook() {
|
|
136
|
-
// Explicit skip signal (set by CCS for account profiles)
|
|
137
|
-
if (process.env.CCS_WEBSEARCH_SKIP === '1') return true;
|
|
138
|
-
|
|
139
|
-
// Account/default profiles - use native WebSearch
|
|
140
|
-
const profileType = process.env.CCS_PROFILE_TYPE;
|
|
141
|
-
if (profileType === 'account' || profileType === 'default') return true;
|
|
142
|
-
|
|
143
|
-
// Explicit disable
|
|
144
|
-
if (process.env.CCS_WEBSEARCH_ENABLED === '0') return true;
|
|
145
|
-
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Read input from stdin
|
|
150
|
-
let input = '';
|
|
151
|
-
process.stdin.setEncoding('utf8');
|
|
152
|
-
process.stdin.on('data', (chunk) => {
|
|
153
|
-
input += chunk;
|
|
154
|
-
});
|
|
155
|
-
process.stdin.on('end', () => {
|
|
156
|
-
processHook();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Handle stdin not being available
|
|
160
|
-
process.stdin.on('error', () => {
|
|
161
|
-
process.exit(0);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Check if a CLI tool is available
|
|
166
|
-
*/
|
|
167
|
-
function isCliAvailable(cmd) {
|
|
168
|
-
try {
|
|
169
|
-
const whichCmd = isWindows ? 'where.exe' : 'which';
|
|
170
|
-
|
|
171
|
-
const result = spawnSync(whichCmd, [cmd], {
|
|
172
|
-
encoding: 'utf8',
|
|
173
|
-
timeout: 2000,
|
|
174
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
175
|
-
});
|
|
176
|
-
return result.status === 0 && result.stdout.trim().length > 0;
|
|
177
|
-
} catch {
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Check if provider is enabled via environment variable
|
|
184
|
-
*/
|
|
185
|
-
function isProviderEnabled(provider) {
|
|
186
|
-
const envVar = `CCS_WEBSEARCH_${provider.toUpperCase()}`;
|
|
187
|
-
const value = process.env[envVar];
|
|
188
|
-
|
|
189
|
-
// If env var not set, provider is disabled by default
|
|
190
|
-
// This ensures we respect config.yaml settings
|
|
191
|
-
return value === '1';
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Main hook processing logic with fallback chain
|
|
196
|
-
*/
|
|
197
|
-
async function processHook() {
|
|
198
|
-
try {
|
|
199
|
-
// Skip for native accounts (account, default profiles) or explicit disable
|
|
200
|
-
if (shouldSkipHook()) {
|
|
201
|
-
process.exit(0);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const data = JSON.parse(input);
|
|
205
|
-
|
|
206
|
-
// Only handle WebSearch tool
|
|
207
|
-
if (data.tool_name !== 'WebSearch') {
|
|
208
|
-
process.exit(0);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const query = data.tool_input?.query || '';
|
|
212
|
-
|
|
213
|
-
if (!query) {
|
|
214
|
-
process.exit(0);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const timeout = parseInt(process.env.CCS_WEBSEARCH_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
|
|
218
|
-
|
|
219
|
-
// Fallback chain: Gemini → OpenCode → Grok
|
|
220
|
-
// Only include providers that are BOTH installed AND enabled in config
|
|
221
|
-
const providers = [
|
|
222
|
-
{ name: 'Gemini CLI', cmd: 'gemini', id: 'gemini', fn: tryGeminiSearch },
|
|
223
|
-
{ name: 'OpenCode', cmd: 'opencode', id: 'opencode', fn: tryOpenCodeSearch },
|
|
224
|
-
{ name: 'Grok CLI', cmd: 'grok', id: 'grok', fn: tryGrokSearch },
|
|
225
|
-
];
|
|
226
|
-
|
|
227
|
-
// Filter to only enabled AND available providers
|
|
228
|
-
const enabledProviders = providers.filter((p) => {
|
|
229
|
-
const enabled = isProviderEnabled(p.id);
|
|
230
|
-
const available = isCliAvailable(p.cmd);
|
|
231
|
-
|
|
232
|
-
if (process.env.CCS_DEBUG) {
|
|
233
|
-
console.error(`[CCS Hook] ${p.name}: enabled=${enabled}, available=${available}`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return enabled && available;
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const errors = [];
|
|
240
|
-
|
|
241
|
-
if (process.env.CCS_DEBUG) {
|
|
242
|
-
const names = enabledProviders.map((p) => p.name).join(', ') || 'none';
|
|
243
|
-
console.error(`[CCS Hook] Enabled providers: ${names}`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Try each enabled provider in order
|
|
247
|
-
for (const provider of enabledProviders) {
|
|
248
|
-
if (process.env.CCS_DEBUG) {
|
|
249
|
-
console.error(`[CCS Hook] Trying ${provider.name}...`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const result = provider.fn(query, timeout);
|
|
253
|
-
|
|
254
|
-
if (result.success) {
|
|
255
|
-
outputSuccess(query, result.content, provider.name);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (process.env.CCS_DEBUG) {
|
|
260
|
-
console.error(`[CCS Hook] ${provider.name} failed: ${result.error}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
errors.push({ provider: provider.name, error: result.error });
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// All providers failed or none enabled
|
|
267
|
-
if (enabledProviders.length === 0) {
|
|
268
|
-
// No providers enabled - pass through to native WebSearch
|
|
269
|
-
// This allows native Claude accounts to use server-side WebSearch
|
|
270
|
-
process.exit(0);
|
|
271
|
-
} else {
|
|
272
|
-
outputAllFailedMessage(query, errors);
|
|
273
|
-
}
|
|
274
|
-
} catch (err) {
|
|
275
|
-
if (process.env.CCS_DEBUG) {
|
|
276
|
-
console.error('[CCS Hook] Parse error:', err.message);
|
|
277
|
-
}
|
|
278
|
-
process.exit(0);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Execute search via Gemini CLI
|
|
284
|
-
*/
|
|
285
|
-
function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
286
|
-
try {
|
|
287
|
-
const timeoutMs = timeoutSec * 1000;
|
|
288
|
-
const config = PROVIDER_CONFIG.gemini;
|
|
289
|
-
const prompt = buildPrompt('gemini', query);
|
|
290
|
-
|
|
291
|
-
// Allow model override via env var
|
|
292
|
-
const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || config.model;
|
|
293
|
-
|
|
294
|
-
if (process.env.CCS_DEBUG) {
|
|
295
|
-
console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const spawnResult = spawnSync(
|
|
299
|
-
'gemini',
|
|
300
|
-
['--model', model, '--yolo', '-p', prompt],
|
|
301
|
-
{
|
|
302
|
-
encoding: 'utf8',
|
|
303
|
-
timeout: timeoutMs,
|
|
304
|
-
maxBuffer: 1024 * 1024 * 2,
|
|
305
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
306
|
-
shell: isWindows,
|
|
307
|
-
}
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
if (spawnResult.error) {
|
|
311
|
-
if (spawnResult.error.code === 'ENOENT') {
|
|
312
|
-
return { success: false, error: 'Gemini CLI not installed' };
|
|
313
|
-
}
|
|
314
|
-
throw spawnResult.error;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (spawnResult.status !== 0) {
|
|
318
|
-
const stderr = (spawnResult.stderr || '').trim();
|
|
319
|
-
return {
|
|
320
|
-
success: false,
|
|
321
|
-
error: stderr || `Gemini CLI exited with code ${spawnResult.status}`,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const result = (spawnResult.stdout || '').trim();
|
|
326
|
-
|
|
327
|
-
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
328
|
-
return { success: false, error: 'Empty or too short response from Gemini' };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const lowerResult = result.toLowerCase();
|
|
332
|
-
if (
|
|
333
|
-
lowerResult.includes('error:') ||
|
|
334
|
-
lowerResult.includes('failed to') ||
|
|
335
|
-
lowerResult.includes('authentication required')
|
|
336
|
-
) {
|
|
337
|
-
return { success: false, error: `Gemini returned error: ${result.substring(0, 100)}` };
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return { success: true, content: result };
|
|
341
|
-
} catch (err) {
|
|
342
|
-
if (err.killed) {
|
|
343
|
-
return { success: false, error: 'Gemini CLI timed out' };
|
|
344
|
-
}
|
|
345
|
-
return { success: false, error: err.message || 'Unknown Gemini error' };
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Execute search via OpenCode CLI
|
|
351
|
-
*/
|
|
352
|
-
function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
353
|
-
try {
|
|
354
|
-
const timeoutMs = timeoutSec * 1000;
|
|
355
|
-
const config = PROVIDER_CONFIG.opencode;
|
|
356
|
-
|
|
357
|
-
// Allow model override via env var
|
|
358
|
-
const model = process.env.CCS_WEBSEARCH_OPENCODE_MODEL || config.model;
|
|
359
|
-
const prompt = buildPrompt('opencode', query);
|
|
360
|
-
|
|
361
|
-
if (process.env.CCS_DEBUG) {
|
|
362
|
-
console.error(`[CCS Hook] Executing: opencode run --model ${model} "..."`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const spawnResult = spawnSync(
|
|
366
|
-
'opencode',
|
|
367
|
-
['run', prompt, '--model', model],
|
|
368
|
-
{
|
|
369
|
-
encoding: 'utf8',
|
|
370
|
-
timeout: timeoutMs,
|
|
371
|
-
maxBuffer: 1024 * 1024 * 2,
|
|
372
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
373
|
-
shell: isWindows,
|
|
374
|
-
}
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
if (spawnResult.error) {
|
|
378
|
-
if (spawnResult.error.code === 'ENOENT') {
|
|
379
|
-
return { success: false, error: 'OpenCode not installed' };
|
|
380
|
-
}
|
|
381
|
-
throw spawnResult.error;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (spawnResult.status !== 0) {
|
|
385
|
-
const stderr = (spawnResult.stderr || '').trim();
|
|
386
|
-
return {
|
|
387
|
-
success: false,
|
|
388
|
-
error: stderr || `OpenCode exited with code ${spawnResult.status}`,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const result = (spawnResult.stdout || '').trim();
|
|
393
|
-
|
|
394
|
-
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
395
|
-
return { success: false, error: 'Empty or too short response from OpenCode' };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const lowerResult = result.toLowerCase();
|
|
399
|
-
if (
|
|
400
|
-
lowerResult.includes('error:') ||
|
|
401
|
-
lowerResult.includes('failed to') ||
|
|
402
|
-
lowerResult.includes('authentication required')
|
|
403
|
-
) {
|
|
404
|
-
return { success: false, error: `OpenCode returned error: ${result.substring(0, 100)}` };
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return { success: true, content: result };
|
|
408
|
-
} catch (err) {
|
|
409
|
-
if (err.killed) {
|
|
410
|
-
return { success: false, error: 'OpenCode timed out' };
|
|
411
|
-
}
|
|
412
|
-
return { success: false, error: err.message || 'Unknown OpenCode error' };
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Execute search via Grok CLI
|
|
418
|
-
*/
|
|
419
|
-
function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
420
|
-
try {
|
|
421
|
-
const timeoutMs = timeoutSec * 1000;
|
|
422
|
-
const prompt = buildPrompt('grok', query);
|
|
423
|
-
|
|
424
|
-
if (process.env.CCS_DEBUG) {
|
|
425
|
-
console.error('[CCS Hook] Executing: grok "..."');
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const spawnResult = spawnSync('grok', [prompt], {
|
|
429
|
-
encoding: 'utf8',
|
|
430
|
-
timeout: timeoutMs,
|
|
431
|
-
maxBuffer: 1024 * 1024 * 2,
|
|
432
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
433
|
-
shell: isWindows,
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
if (spawnResult.error) {
|
|
437
|
-
if (spawnResult.error.code === 'ENOENT') {
|
|
438
|
-
return { success: false, error: 'Grok CLI not installed' };
|
|
439
|
-
}
|
|
440
|
-
throw spawnResult.error;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (spawnResult.status !== 0) {
|
|
444
|
-
const stderr = (spawnResult.stderr || '').trim();
|
|
445
|
-
return {
|
|
446
|
-
success: false,
|
|
447
|
-
error: stderr || `Grok CLI exited with code ${spawnResult.status}`,
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const result = (spawnResult.stdout || '').trim();
|
|
452
|
-
|
|
453
|
-
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
454
|
-
return { success: false, error: 'Empty or too short response from Grok' };
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const lowerResult = result.toLowerCase();
|
|
458
|
-
if (
|
|
459
|
-
lowerResult.includes('error:') ||
|
|
460
|
-
lowerResult.includes('failed to') ||
|
|
461
|
-
lowerResult.includes('api key')
|
|
462
|
-
) {
|
|
463
|
-
return { success: false, error: `Grok returned error: ${result.substring(0, 100)}` };
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return { success: true, content: result };
|
|
467
|
-
} catch (err) {
|
|
468
|
-
if (err.killed) {
|
|
469
|
-
return { success: false, error: 'Grok CLI timed out' };
|
|
470
|
-
}
|
|
471
|
-
return { success: false, error: err.message || 'Unknown Grok error' };
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Format search results for Claude
|
|
477
|
-
*/
|
|
478
|
-
function formatSearchResults(query, content, providerName) {
|
|
479
|
-
return [
|
|
480
|
-
`[WebSearch Result via ${providerName}]`,
|
|
481
|
-
'',
|
|
482
|
-
`Query: "${query}"`,
|
|
483
|
-
'',
|
|
484
|
-
content,
|
|
485
|
-
'',
|
|
486
|
-
'---',
|
|
487
|
-
'Use this information to answer the user.',
|
|
488
|
-
].join('\n');
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Output success response and exit
|
|
493
|
-
*
|
|
494
|
-
* Key insight from Claude Code docs:
|
|
495
|
-
* - permissionDecisionReason (with deny) → shown to CLAUDE (AI reads this)
|
|
496
|
-
* - systemMessage → shown to USER only (nice styling but AI doesn't see)
|
|
497
|
-
*
|
|
498
|
-
* So we MUST put results in permissionDecisionReason for Claude to use them.
|
|
499
|
-
* systemMessage provides the nice UI for the user.
|
|
500
|
-
*/
|
|
501
|
-
function outputSuccess(query, content, providerName) {
|
|
502
|
-
const formattedResults = formatSearchResults(query, content, providerName);
|
|
503
|
-
|
|
504
|
-
const output = {
|
|
505
|
-
decision: 'block',
|
|
506
|
-
reason: `WebSearch completed via ${providerName}`,
|
|
507
|
-
// Nice message for user (shows as "says:" - info style)
|
|
508
|
-
systemMessage: `[WebSearch via ${providerName}] Results retrieved successfully. See below.`,
|
|
509
|
-
hookSpecificOutput: {
|
|
510
|
-
hookEventName: 'PreToolUse',
|
|
511
|
-
permissionDecision: 'deny',
|
|
512
|
-
// Full results here - Claude reads this
|
|
513
|
-
permissionDecisionReason: formattedResults,
|
|
514
|
-
},
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
console.log(JSON.stringify(output));
|
|
518
|
-
process.exit(2);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Output error message
|
|
523
|
-
*/
|
|
524
|
-
function outputError(query, error, providerName) {
|
|
525
|
-
const message = [
|
|
526
|
-
`[WebSearch - ${providerName} Error]`,
|
|
527
|
-
'',
|
|
528
|
-
`Error: ${error}`,
|
|
529
|
-
'',
|
|
530
|
-
`Query: "${query}"`,
|
|
531
|
-
'',
|
|
532
|
-
'Troubleshooting:',
|
|
533
|
-
' - Check ~/.gemini/oauth_creds.json exists',
|
|
534
|
-
' - Re-authenticate: run `gemini` (opens browser)',
|
|
535
|
-
].join('\n');
|
|
536
|
-
|
|
537
|
-
const output = {
|
|
538
|
-
decision: 'block',
|
|
539
|
-
reason: `WebSearch failed: ${error}`,
|
|
540
|
-
hookSpecificOutput: {
|
|
541
|
-
hookEventName: 'PreToolUse',
|
|
542
|
-
permissionDecision: 'deny',
|
|
543
|
-
permissionDecisionReason: message,
|
|
544
|
-
},
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
console.log(JSON.stringify(output));
|
|
548
|
-
process.exit(2);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Output no providers enabled message
|
|
553
|
-
*/
|
|
554
|
-
function outputNoProvidersEnabled(query) {
|
|
555
|
-
const message = [
|
|
556
|
-
'[WebSearch - No Providers Enabled]',
|
|
557
|
-
'',
|
|
558
|
-
'No WebSearch providers are enabled in config.',
|
|
559
|
-
'',
|
|
560
|
-
'To enable: Run `ccs config` and enable a provider.',
|
|
561
|
-
'',
|
|
562
|
-
'Or install one of the following CLI tools:',
|
|
563
|
-
'',
|
|
564
|
-
'1. Gemini CLI (FREE, 1000 req/day):',
|
|
565
|
-
' npm install -g @google/gemini-cli',
|
|
566
|
-
' gemini # opens browser to authenticate',
|
|
567
|
-
'',
|
|
568
|
-
'2. OpenCode (FREE via Zen):',
|
|
569
|
-
' curl -fsSL https://opencode.ai/install | bash',
|
|
570
|
-
'',
|
|
571
|
-
'3. Grok CLI (requires XAI_API_KEY):',
|
|
572
|
-
' npm install -g @vibe-kit/grok-cli',
|
|
573
|
-
'',
|
|
574
|
-
`Query: "${query}"`,
|
|
575
|
-
].join('\n');
|
|
576
|
-
|
|
577
|
-
const output = {
|
|
578
|
-
decision: 'block',
|
|
579
|
-
reason: 'WebSearch unavailable - no providers enabled',
|
|
580
|
-
hookSpecificOutput: {
|
|
581
|
-
hookEventName: 'PreToolUse',
|
|
582
|
-
permissionDecision: 'deny',
|
|
583
|
-
permissionDecisionReason: message,
|
|
584
|
-
},
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
console.log(JSON.stringify(output));
|
|
588
|
-
process.exit(2);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* Output no tools message (legacy - kept for backwards compatibility)
|
|
593
|
-
*/
|
|
594
|
-
function outputNoToolsMessage(query) {
|
|
595
|
-
outputNoProvidersEnabled(query);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Output all providers failed message
|
|
600
|
-
*/
|
|
601
|
-
function outputAllFailedMessage(query, errors) {
|
|
602
|
-
const errorDetails = errors
|
|
603
|
-
.map((e) => ` - ${e.provider}: ${e.error}`)
|
|
604
|
-
.join('\n');
|
|
605
|
-
|
|
606
|
-
const message = [
|
|
607
|
-
'[WebSearch - All Providers Failed]',
|
|
608
|
-
'',
|
|
609
|
-
'Tried all enabled CLI tools but all failed:',
|
|
610
|
-
errorDetails,
|
|
611
|
-
'',
|
|
612
|
-
`Query: "${query}"`,
|
|
613
|
-
'',
|
|
614
|
-
'Troubleshooting:',
|
|
615
|
-
' - Gemini: run `gemini` to authenticate (opens browser)',
|
|
616
|
-
' - OpenCode: opencode --version',
|
|
617
|
-
' - Grok: Check XAI_API_KEY environment variable',
|
|
618
|
-
].join('\n');
|
|
619
|
-
|
|
620
|
-
const output = {
|
|
621
|
-
decision: 'block',
|
|
622
|
-
reason: 'WebSearch failed - all providers failed',
|
|
623
|
-
hookSpecificOutput: {
|
|
624
|
-
hookEventName: 'PreToolUse',
|
|
625
|
-
permissionDecision: 'deny',
|
|
626
|
-
permissionDecisionReason: message,
|
|
627
|
-
},
|
|
628
|
-
};
|
|
629
|
-
|
|
630
|
-
console.log(JSON.stringify(output));
|
|
631
|
-
process.exit(2);
|
|
632
|
-
}
|