@kaitranntt/ccs 7.56.0-dev.3 → 7.56.0-dev.5
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/README.md +23 -17
- package/dist/config/unified-config-loader.d.ts +16 -0
- package/dist/config/unified-config-loader.d.ts.map +1 -1
- package/dist/config/unified-config-loader.js +56 -12
- package/dist/config/unified-config-loader.js.map +1 -1
- package/dist/config/unified-config-types.d.ts +56 -10
- package/dist/config/unified-config-types.d.ts.map +1 -1
- package/dist/config/unified-config-types.js +20 -2
- package/dist/config/unified-config-types.js.map +1 -1
- package/dist/ui/assets/{accounts-_HKzUYeW.js → accounts-CcKtNJpZ.js} +1 -1
- package/dist/ui/assets/{alert-dialog-7NHGF_Xe.js → alert-dialog-D9ixkKjG.js} +1 -1
- package/dist/ui/assets/{api-DOfVbQ1z.js → api-ClWqAKR1.js} +2 -2
- package/dist/ui/assets/{auth-section-DqMIwU7c.js → auth-section-BQ4Kogh5.js} +1 -1
- package/dist/ui/assets/{backups-section-D4um8aUY.js → backups-section-fPK5pSgx.js} +1 -1
- package/dist/ui/assets/{checkbox-BOeXuNDh.js → checkbox-Cr_w2dBr.js} +1 -1
- package/dist/ui/assets/claude-extension-Blq0eREl.js +1 -0
- package/dist/ui/assets/{cliproxy-CzJfXgOZ.js → cliproxy-CPWxG68T.js} +2 -2
- package/dist/ui/assets/{cliproxy-ai-providers-BYWaYXvO.js → cliproxy-ai-providers-CWIwmTNM.js} +12 -12
- package/dist/ui/assets/{cliproxy-control-panel-77ZZmJI4.js → cliproxy-control-panel-CgEMwXOj.js} +1 -1
- package/dist/ui/assets/{confirm-dialog-Bz3IbucG.js → confirm-dialog-BLoDbdRn.js} +1 -1
- package/dist/ui/assets/{copilot-DzskqpA-.js → copilot-tFEndl-V.js} +2 -2
- package/dist/ui/assets/cursor-d_8EDsFB.js +1 -0
- package/dist/ui/assets/{droid-DR0b5dXu.js → droid-BIbPZCVp.js} +2 -2
- package/dist/ui/assets/{globalenv-section-O2tusa4k.js → globalenv-section-C1OjUAhr.js} +1 -1
- package/dist/ui/assets/{health-BPuHhvGU.js → health-BAy1igW2.js} +1 -1
- package/dist/ui/assets/{icons-DR-ORtNe.js → icons-BwsSbo2z.js} +1 -1
- package/dist/ui/assets/index-Bp4s-Led.css +1 -0
- package/dist/ui/assets/{index-eQOk8v6I.js → index-D-xh-rUY.js} +1 -1
- package/dist/ui/assets/index-DVgRJ--R.js +1 -0
- package/dist/ui/assets/{index-3azdVT3y.js → index-DjsOxm87.js} +1 -1
- package/dist/ui/assets/{index-ZlUC3TcJ.js → index-KQ7UM0Zx.js} +3 -3
- package/dist/ui/assets/{index-D0r4vaLi.js → index-L-WMPeHP.js} +1 -1
- package/dist/ui/assets/{proxy-status-widget-CU9RtUUZ.js → proxy-status-widget-CibAyvji.js} +1 -1
- package/dist/ui/assets/{searchable-select-DltMdKve.js → searchable-select-BCt9hVab.js} +1 -1
- package/dist/ui/assets/{separator-DvmULQuv.js → separator-B-7tcT70.js} +1 -1
- package/dist/ui/assets/{shared-3KjBmAma.js → shared-B5lECoNe.js} +1 -1
- package/dist/ui/assets/{switch-WfgHsT03.js → switch-ofCljueo.js} +1 -1
- package/dist/ui/assets/{updates-DEb1kb0C.js → updates-CDdcIRgb.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/websearch/hook-env.d.ts.map +1 -1
- package/dist/utils/websearch/hook-env.js +17 -1
- package/dist/utils/websearch/hook-env.js.map +1 -1
- package/dist/utils/websearch/status.d.ts +5 -12
- package/dist/utils/websearch/status.d.ts.map +1 -1
- package/dist/utils/websearch/status.js +128 -87
- package/dist/utils/websearch/status.js.map +1 -1
- package/dist/utils/websearch/types.d.ts +37 -23
- package/dist/utils/websearch/types.d.ts.map +1 -1
- package/dist/utils/websearch/types.js +1 -1
- package/dist/utils/websearch-manager.d.ts +5 -4
- package/dist/utils/websearch-manager.d.ts.map +1 -1
- package/dist/utils/websearch-manager.js +5 -4
- package/dist/utils/websearch-manager.js.map +1 -1
- package/dist/web-server/health/websearch-checks.d.ts +2 -2
- package/dist/web-server/health/websearch-checks.d.ts.map +1 -1
- package/dist/web-server/health/websearch-checks.js +9 -11
- package/dist/web-server/health/websearch-checks.js.map +1 -1
- package/dist/web-server/routes/websearch-routes.d.ts.map +1 -1
- package/dist/web-server/routes/websearch-routes.js +33 -22
- package/dist/web-server/routes/websearch-routes.js.map +1 -1
- package/lib/hooks/websearch-transformer.cjs +488 -462
- package/package.json +1 -1
- package/dist/ui/assets/claude-extension-CelZ8ku0.js +0 -1
- package/dist/ui/assets/cursor-C8f14wrT.js +0 -1
- package/dist/ui/assets/index-BxTb3XSE.js +0 -1
- package/dist/ui/assets/index-kGiBvBM-.css +0 -1
|
@@ -1,51 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* CCS WebSearch Hook -
|
|
3
|
+
* CCS WebSearch Hook - deterministic search backends with legacy CLI fallback
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Primary providers:
|
|
6
|
+
* - Exa Search API
|
|
7
|
+
* - Tavily Search API
|
|
8
|
+
* - Brave Search API
|
|
9
|
+
* - DuckDuckGo HTML search
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
11
|
+
* Legacy compatibility fallback:
|
|
12
|
+
* - Gemini CLI
|
|
13
|
+
* - OpenCode
|
|
14
|
+
* - Grok CLI
|
|
25
15
|
*/
|
|
26
16
|
|
|
27
17
|
const { spawnSync } = require('child_process');
|
|
28
18
|
|
|
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
19
|
const isWindows = process.platform === 'win32';
|
|
20
|
+
const DEFAULT_TIMEOUT_SEC = 55;
|
|
21
|
+
const DEFAULT_RESULT_COUNT = 5;
|
|
22
|
+
const MIN_VALID_RESPONSE_LENGTH = 20;
|
|
23
|
+
const EXA_URL = 'https://api.exa.ai/search';
|
|
24
|
+
const TAVILY_URL = 'https://api.tavily.com/search';
|
|
25
|
+
const DDG_URL = 'https://html.duckduckgo.com/html/';
|
|
26
|
+
const BRAVE_URL = 'https://api.search.brave.com/res/v1/web/search';
|
|
27
|
+
const USER_AGENT =
|
|
28
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
40
29
|
|
|
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
30
|
const SHARED_INSTRUCTIONS = `Instructions:
|
|
50
31
|
1. Search the web for current, up-to-date information
|
|
51
32
|
2. Provide a comprehensive summary of the search results
|
|
@@ -55,119 +36,45 @@ const SHARED_INSTRUCTIONS = `Instructions:
|
|
|
55
36
|
6. If results conflict, note the discrepancy
|
|
56
37
|
7. Format output clearly with sections if the topic is complex`;
|
|
57
38
|
|
|
58
|
-
/**
|
|
59
|
-
* PROVIDER-SPECIFIC CONFIG - Only tool-use differences and quirks
|
|
60
|
-
* Each provider may have unique capabilities or invocation methods.
|
|
61
|
-
*/
|
|
62
39
|
const PROVIDER_CONFIG = {
|
|
63
40
|
gemini: {
|
|
64
|
-
// Model to use (passed via --model flag)
|
|
65
41
|
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
42
|
toolInstruction: 'Use the google_web_search tool to find current information.',
|
|
70
|
-
|
|
71
|
-
// Optional quirks (null if none)
|
|
72
43
|
quirks: null,
|
|
73
44
|
},
|
|
74
|
-
|
|
75
45
|
opencode: {
|
|
76
|
-
// Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var)
|
|
77
46
|
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
47
|
toolInstruction: 'Search the web using your built-in capabilities.',
|
|
82
|
-
|
|
83
|
-
// Optional quirks
|
|
84
48
|
quirks: null,
|
|
85
49
|
},
|
|
86
|
-
|
|
87
50
|
grok: {
|
|
88
|
-
// Model to use (Grok CLI uses default model)
|
|
89
51
|
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
52
|
toolInstruction: 'Use your web search capabilities to find information.',
|
|
94
|
-
|
|
95
|
-
// Grok-specific: Can also search X for real-time info
|
|
96
53
|
quirks: 'For breaking news or real-time events, also check X/Twitter if relevant.',
|
|
97
54
|
},
|
|
98
55
|
};
|
|
99
56
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
];
|
|
57
|
+
const ddgLinkRe = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
|
|
58
|
+
const ddgSnippetRe = /<a class="result__snippet[^"]*".*?>([\s\S]*?)<\/a>/g;
|
|
59
|
+
const htmlTagRe = /<[^>]+>/g;
|
|
113
60
|
|
|
114
|
-
|
|
115
|
-
|
|
61
|
+
function debug(message) {
|
|
62
|
+
if (process.env.CCS_DEBUG) {
|
|
63
|
+
console.error(`[CCS Hook] ${message}`);
|
|
116
64
|
}
|
|
117
|
-
|
|
118
|
-
return parts.join('\n');
|
|
119
65
|
}
|
|
120
66
|
|
|
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
67
|
function shouldSkipHook() {
|
|
136
|
-
// Explicit skip signal (set by CCS for account profiles)
|
|
137
68
|
if (process.env.CCS_WEBSEARCH_SKIP === '1') return true;
|
|
138
|
-
|
|
139
|
-
// Account/default profiles - use native WebSearch
|
|
140
69
|
const profileType = process.env.CCS_PROFILE_TYPE;
|
|
141
70
|
if (profileType === 'account' || profileType === 'default') return true;
|
|
142
|
-
|
|
143
|
-
// Explicit disable
|
|
144
71
|
if (process.env.CCS_WEBSEARCH_ENABLED === '0') return true;
|
|
145
|
-
|
|
146
72
|
return false;
|
|
147
73
|
}
|
|
148
74
|
|
|
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
75
|
function isCliAvailable(cmd) {
|
|
168
76
|
try {
|
|
169
77
|
const whichCmd = isWindows ? 'where.exe' : 'which';
|
|
170
|
-
|
|
171
78
|
const result = spawnSync(whichCmd, [cmd], {
|
|
172
79
|
encoding: 'utf8',
|
|
173
80
|
timeout: 2000,
|
|
@@ -179,124 +86,330 @@ function isCliAvailable(cmd) {
|
|
|
179
86
|
}
|
|
180
87
|
}
|
|
181
88
|
|
|
182
|
-
/**
|
|
183
|
-
* Check if provider is enabled via environment variable
|
|
184
|
-
*/
|
|
185
89
|
function isProviderEnabled(provider) {
|
|
186
|
-
|
|
187
|
-
|
|
90
|
+
return process.env[`CCS_WEBSEARCH_${provider.toUpperCase()}`] === '1';
|
|
91
|
+
}
|
|
188
92
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return value === '1';
|
|
93
|
+
function hasEnvValue(name) {
|
|
94
|
+
return (process.env[name] || '').trim().length > 0;
|
|
192
95
|
}
|
|
193
96
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
// Skip for native accounts (account, default profiles) or explicit disable
|
|
200
|
-
if (shouldSkipHook()) {
|
|
201
|
-
process.exit(0);
|
|
97
|
+
function getFirstEnvValue(names) {
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
if (hasEnvValue(name)) {
|
|
100
|
+
return process.env[name].trim();
|
|
202
101
|
}
|
|
102
|
+
}
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
203
105
|
|
|
204
|
-
|
|
106
|
+
function getProviderApiKey(providerId) {
|
|
107
|
+
switch (providerId) {
|
|
108
|
+
case 'brave':
|
|
109
|
+
return getFirstEnvValue(['BRAVE_API_KEY', 'CCS_WEBSEARCH_BRAVE_API_KEY']);
|
|
110
|
+
case 'exa':
|
|
111
|
+
return getFirstEnvValue(['EXA_API_KEY', 'CCS_WEBSEARCH_EXA_API_KEY']);
|
|
112
|
+
case 'tavily':
|
|
113
|
+
return getFirstEnvValue(['TAVILY_API_KEY', 'CCS_WEBSEARCH_TAVILY_API_KEY']);
|
|
114
|
+
default:
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
205
118
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
119
|
+
function getResultCount(provider) {
|
|
120
|
+
const raw = process.env[`CCS_WEBSEARCH_${provider.toUpperCase()}_MAX_RESULTS`];
|
|
121
|
+
const parsed = Number.parseInt(raw || '', 10);
|
|
122
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 10) : DEFAULT_RESULT_COUNT;
|
|
123
|
+
}
|
|
210
124
|
|
|
211
|
-
|
|
125
|
+
function buildPrompt(providerId, query) {
|
|
126
|
+
const config = PROVIDER_CONFIG[providerId];
|
|
127
|
+
const parts = [
|
|
128
|
+
`Search the web for: ${query}`,
|
|
129
|
+
'',
|
|
130
|
+
config.toolInstruction,
|
|
131
|
+
'',
|
|
132
|
+
SHARED_INSTRUCTIONS,
|
|
133
|
+
];
|
|
134
|
+
if (config.quirks) {
|
|
135
|
+
parts.push('', `Note: ${config.quirks}`);
|
|
136
|
+
}
|
|
137
|
+
return parts.join('\n');
|
|
138
|
+
}
|
|
212
139
|
|
|
213
|
-
|
|
214
|
-
|
|
140
|
+
function decodeHtml(value) {
|
|
141
|
+
return value
|
|
142
|
+
.replace(/&/g, '&')
|
|
143
|
+
.replace(/"/g, '"')
|
|
144
|
+
.replace(/'/g, "'")
|
|
145
|
+
.replace(/</g, '<')
|
|
146
|
+
.replace(/>/g, '>');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function compactText(value, maxLength = 280) {
|
|
150
|
+
const text = String(value || '')
|
|
151
|
+
.replace(/\s+/g, ' ')
|
|
152
|
+
.trim();
|
|
153
|
+
if (text.length <= maxLength) {
|
|
154
|
+
return text;
|
|
155
|
+
}
|
|
156
|
+
return `${text.slice(0, maxLength - 3).trimEnd()}...`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractDuckDuckGoResults(html, count) {
|
|
160
|
+
const links = [...html.matchAll(ddgLinkRe)].slice(0, count + 5);
|
|
161
|
+
const snippets = [...html.matchAll(ddgSnippetRe)].slice(0, count + 5);
|
|
162
|
+
|
|
163
|
+
return links.slice(0, count).map((match, index) => {
|
|
164
|
+
let url = match[1];
|
|
165
|
+
if (url.includes('uddg=')) {
|
|
166
|
+
try {
|
|
167
|
+
const decoded = decodeURIComponent(url);
|
|
168
|
+
const uddgIndex = decoded.indexOf('uddg=');
|
|
169
|
+
if (uddgIndex !== -1) {
|
|
170
|
+
url = decoded.slice(uddgIndex + 5).split('&')[0];
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// keep original url
|
|
174
|
+
}
|
|
215
175
|
}
|
|
216
176
|
|
|
217
|
-
|
|
177
|
+
return {
|
|
178
|
+
title: decodeHtml(match[2].replace(htmlTagRe, '').trim()),
|
|
179
|
+
url,
|
|
180
|
+
description: decodeHtml((snippets[index]?.[1] || '').replace(htmlTagRe, '').trim()),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
}
|
|
218
184
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
{ name: 'OpenCode', cmd: 'opencode', id: 'opencode', fn: tryOpenCodeSearch },
|
|
224
|
-
{ name: 'Grok CLI', cmd: 'grok', id: 'grok', fn: tryGrokSearch },
|
|
225
|
-
];
|
|
185
|
+
function formatStructuredSearchResults(query, providerName, results) {
|
|
186
|
+
if (!results.length) {
|
|
187
|
+
return `No search results found for "${query}" via ${providerName}.`;
|
|
188
|
+
}
|
|
226
189
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
190
|
+
const lines = [`Search results for "${query}" via ${providerName}:`, ''];
|
|
191
|
+
for (const [index, result] of results.entries()) {
|
|
192
|
+
lines.push(`${index + 1}. ${result.title}`);
|
|
193
|
+
lines.push(` ${result.url}`);
|
|
194
|
+
if (result.description) {
|
|
195
|
+
lines.push(` ${result.description}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push('');
|
|
198
|
+
}
|
|
199
|
+
lines.push('Use these results to answer the user directly.');
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
231
202
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
203
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
204
|
+
const controller = new AbortController();
|
|
205
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
206
|
+
try {
|
|
207
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
235
212
|
|
|
236
|
-
|
|
237
|
-
|
|
213
|
+
async function tryBraveSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
214
|
+
const apiKey = getProviderApiKey('brave');
|
|
215
|
+
if (!apiKey) {
|
|
216
|
+
return { success: false, error: 'BRAVE_API_KEY is not set' };
|
|
217
|
+
}
|
|
238
218
|
|
|
239
|
-
|
|
219
|
+
const params = new URLSearchParams({
|
|
220
|
+
q: query,
|
|
221
|
+
count: String(getResultCount('brave')),
|
|
222
|
+
});
|
|
240
223
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
224
|
+
try {
|
|
225
|
+
const response = await fetchWithTimeout(
|
|
226
|
+
`${BRAVE_URL}?${params.toString()}`,
|
|
227
|
+
{
|
|
228
|
+
headers: {
|
|
229
|
+
Accept: 'application/json',
|
|
230
|
+
'User-Agent': USER_AGENT,
|
|
231
|
+
'X-Subscription-Token': apiKey,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
timeoutSec * 1000
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
const body = await response.text();
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
error: `Brave Search returned ${response.status}: ${body.slice(0, 160)}`,
|
|
242
|
+
};
|
|
244
243
|
}
|
|
245
244
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const result = provider.fn(query, timeout);
|
|
245
|
+
const body = await response.json();
|
|
246
|
+
const results = (body.web?.results || []).map((result) => ({
|
|
247
|
+
title: result.title || 'Untitled',
|
|
248
|
+
url: result.url || '',
|
|
249
|
+
description: result.description || '',
|
|
250
|
+
}));
|
|
253
251
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
content: formatStructuredSearchResults(query, 'Brave Search', results),
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
error: error.name === 'AbortError' ? 'Brave Search timed out' : error.message,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
258
263
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
264
|
+
async function tryExaSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
265
|
+
const apiKey = getProviderApiKey('exa');
|
|
266
|
+
if (!apiKey) {
|
|
267
|
+
return { success: false, error: 'EXA_API_KEY is not set' };
|
|
268
|
+
}
|
|
262
269
|
|
|
263
|
-
|
|
270
|
+
try {
|
|
271
|
+
const response = await fetchWithTimeout(
|
|
272
|
+
EXA_URL,
|
|
273
|
+
{
|
|
274
|
+
method: 'POST',
|
|
275
|
+
headers: {
|
|
276
|
+
Accept: 'application/json',
|
|
277
|
+
'Content-Type': 'application/json',
|
|
278
|
+
'User-Agent': USER_AGENT,
|
|
279
|
+
'x-api-key': apiKey,
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
query,
|
|
283
|
+
type: 'auto',
|
|
284
|
+
numResults: getResultCount('exa'),
|
|
285
|
+
text: true,
|
|
286
|
+
}),
|
|
287
|
+
},
|
|
288
|
+
timeoutSec * 1000
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
const body = await response.text();
|
|
293
|
+
return { success: false, error: `Exa returned ${response.status}: ${body.slice(0, 160)}` };
|
|
264
294
|
}
|
|
265
295
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
296
|
+
const body = await response.json();
|
|
297
|
+
const results = (body.results || []).map((result) => ({
|
|
298
|
+
title: compactText(result.title || result.url || 'Untitled', 120),
|
|
299
|
+
url: result.url || '',
|
|
300
|
+
description: compactText(result.text || result.summary || '', 240),
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
success: true,
|
|
305
|
+
content: formatStructuredSearchResults(query, 'Exa', results),
|
|
306
|
+
};
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return {
|
|
309
|
+
success: false,
|
|
310
|
+
error: error.name === 'AbortError' ? 'Exa timed out' : error.message,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function tryTavilySearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
316
|
+
const apiKey = getProviderApiKey('tavily');
|
|
317
|
+
if (!apiKey) {
|
|
318
|
+
return { success: false, error: 'TAVILY_API_KEY is not set' };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetchWithTimeout(
|
|
323
|
+
TAVILY_URL,
|
|
324
|
+
{
|
|
325
|
+
method: 'POST',
|
|
326
|
+
headers: {
|
|
327
|
+
Accept: 'application/json',
|
|
328
|
+
Authorization: `Bearer ${apiKey}`,
|
|
329
|
+
'Content-Type': 'application/json',
|
|
330
|
+
'User-Agent': USER_AGENT,
|
|
331
|
+
},
|
|
332
|
+
body: JSON.stringify({
|
|
333
|
+
query,
|
|
334
|
+
search_depth: 'basic',
|
|
335
|
+
max_results: getResultCount('tavily'),
|
|
336
|
+
include_answer: false,
|
|
337
|
+
include_raw_content: false,
|
|
338
|
+
}),
|
|
339
|
+
},
|
|
340
|
+
timeoutSec * 1000
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
const body = await response.text();
|
|
345
|
+
return { success: false, error: `Tavily returned ${response.status}: ${body.slice(0, 160)}` };
|
|
273
346
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
347
|
+
|
|
348
|
+
const body = await response.json();
|
|
349
|
+
const results = (body.results || []).map((result) => ({
|
|
350
|
+
title: compactText(result.title || result.url || 'Untitled', 120),
|
|
351
|
+
url: result.url || '',
|
|
352
|
+
description: compactText(result.content || '', 240),
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
success: true,
|
|
357
|
+
content: formatStructuredSearchResults(query, 'Tavily', results),
|
|
358
|
+
};
|
|
359
|
+
} catch (error) {
|
|
360
|
+
return {
|
|
361
|
+
success: false,
|
|
362
|
+
error: error.name === 'AbortError' ? 'Tavily timed out' : error.message,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function tryDuckDuckGoSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
368
|
+
try {
|
|
369
|
+
const params = new URLSearchParams({ q: query });
|
|
370
|
+
const response = await fetchWithTimeout(
|
|
371
|
+
`${DDG_URL}?${params.toString()}`,
|
|
372
|
+
{
|
|
373
|
+
headers: {
|
|
374
|
+
Accept: 'text/html',
|
|
375
|
+
'User-Agent': USER_AGENT,
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
timeoutSec * 1000
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
return { success: false, error: `DuckDuckGo returned ${response.status}` };
|
|
277
383
|
}
|
|
278
|
-
|
|
384
|
+
|
|
385
|
+
const html = await response.text();
|
|
386
|
+
const results = extractDuckDuckGoResults(html, getResultCount('duckduckgo'));
|
|
387
|
+
return {
|
|
388
|
+
success: true,
|
|
389
|
+
content: formatStructuredSearchResults(query, 'DuckDuckGo', results),
|
|
390
|
+
};
|
|
391
|
+
} catch (error) {
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
error: error.name === 'AbortError' ? 'DuckDuckGo timed out' : error.message,
|
|
395
|
+
};
|
|
279
396
|
}
|
|
280
397
|
}
|
|
281
398
|
|
|
282
|
-
/**
|
|
283
|
-
* Execute search via Gemini CLI
|
|
284
|
-
*/
|
|
285
399
|
function shouldRetryGeminiWithLegacyPrompt(errorMessage) {
|
|
286
|
-
const
|
|
287
|
-
|
|
400
|
+
const lower = (errorMessage || '').toLowerCase();
|
|
288
401
|
return (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
402
|
+
lower.includes('unknown option') ||
|
|
403
|
+
lower.includes('unknown argument') ||
|
|
404
|
+
lower.includes('unrecognized option') ||
|
|
405
|
+
lower.includes('usage: gemini') ||
|
|
406
|
+
lower.includes('use --prompt') ||
|
|
407
|
+
lower.includes('using the --prompt option')
|
|
295
408
|
);
|
|
296
409
|
}
|
|
297
410
|
|
|
298
411
|
function runGeminiCommand(args, timeoutMs) {
|
|
299
|
-
const
|
|
412
|
+
const result = spawnSync('gemini', args, {
|
|
300
413
|
encoding: 'utf8',
|
|
301
414
|
timeout: timeoutMs,
|
|
302
415
|
maxBuffer: 1024 * 1024 * 2,
|
|
@@ -304,241 +417,130 @@ function runGeminiCommand(args, timeoutMs) {
|
|
|
304
417
|
shell: isWindows,
|
|
305
418
|
});
|
|
306
419
|
|
|
307
|
-
if (
|
|
308
|
-
if (
|
|
420
|
+
if (result.error) {
|
|
421
|
+
if (result.error.code === 'ENOENT')
|
|
309
422
|
return { success: false, error: 'Gemini CLI not installed' };
|
|
310
|
-
|
|
311
|
-
throw spawnResult.error;
|
|
423
|
+
throw result.error;
|
|
312
424
|
}
|
|
313
|
-
|
|
314
|
-
if (spawnResult.status !== 0) {
|
|
315
|
-
const stderr = (spawnResult.stderr || '').trim();
|
|
425
|
+
if (result.status !== 0) {
|
|
316
426
|
return {
|
|
317
427
|
success: false,
|
|
318
|
-
error: stderr || `Gemini CLI exited with code ${
|
|
428
|
+
error: (result.stderr || '').trim() || `Gemini CLI exited with code ${result.status}`,
|
|
319
429
|
};
|
|
320
430
|
}
|
|
321
431
|
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
432
|
+
const output = (result.stdout || '').trim();
|
|
433
|
+
if (!output || output.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
325
434
|
return { success: false, error: 'Empty or too short response from Gemini' };
|
|
326
435
|
}
|
|
327
|
-
|
|
328
|
-
const lowerResult = result.toLowerCase();
|
|
329
|
-
if (
|
|
330
|
-
lowerResult.includes('error:') ||
|
|
331
|
-
lowerResult.includes('failed to') ||
|
|
332
|
-
lowerResult.includes('authentication required')
|
|
333
|
-
) {
|
|
334
|
-
return { success: false, error: `Gemini returned error: ${result.substring(0, 100)}` };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return { success: true, content: result };
|
|
436
|
+
return { success: true, content: output };
|
|
338
437
|
}
|
|
339
438
|
|
|
340
439
|
function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
341
440
|
try {
|
|
342
441
|
const timeoutMs = timeoutSec * 1000;
|
|
343
|
-
const
|
|
442
|
+
const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || PROVIDER_CONFIG.gemini.model;
|
|
344
443
|
const prompt = buildPrompt('gemini', query);
|
|
345
|
-
|
|
346
|
-
// Allow model override via env var
|
|
347
|
-
const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || config.model;
|
|
348
444
|
const baseArgs = ['--model', model, '--yolo'];
|
|
349
|
-
const positionalArgs = [...baseArgs, prompt];
|
|
350
|
-
|
|
351
|
-
if (process.env.CCS_DEBUG) {
|
|
352
|
-
console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo "..."`);
|
|
353
|
-
}
|
|
354
445
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (positionalResult.success) {
|
|
446
|
+
debug(`Executing Gemini legacy fallback with model ${model}`);
|
|
447
|
+
const positionalResult = runGeminiCommand([...baseArgs, prompt], timeoutMs);
|
|
448
|
+
if (positionalResult.success || !shouldRetryGeminiWithLegacyPrompt(positionalResult.error)) {
|
|
359
449
|
return positionalResult;
|
|
360
450
|
}
|
|
361
451
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const legacyPromptArgs = [...baseArgs, '-p', prompt];
|
|
372
|
-
return runGeminiCommand(legacyPromptArgs, timeoutMs);
|
|
373
|
-
} catch (err) {
|
|
374
|
-
if (err.killed) {
|
|
375
|
-
return { success: false, error: 'Gemini CLI timed out' };
|
|
376
|
-
}
|
|
377
|
-
return { success: false, error: err.message || 'Unknown Gemini error' };
|
|
452
|
+
return runGeminiCommand([...baseArgs, '-p', prompt], timeoutMs);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
return {
|
|
455
|
+
success: false,
|
|
456
|
+
error: error.killed ? 'Gemini CLI timed out' : error.message || 'Unknown Gemini error',
|
|
457
|
+
};
|
|
378
458
|
}
|
|
379
459
|
}
|
|
380
460
|
|
|
381
|
-
/**
|
|
382
|
-
* Execute search via OpenCode CLI
|
|
383
|
-
*/
|
|
384
461
|
function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
385
462
|
try {
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
encoding: 'utf8',
|
|
399
|
-
timeout: timeoutMs,
|
|
400
|
-
maxBuffer: 1024 * 1024 * 2,
|
|
401
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
402
|
-
shell: isWindows,
|
|
403
|
-
});
|
|
463
|
+
const model = process.env.CCS_WEBSEARCH_OPENCODE_MODEL || PROVIDER_CONFIG.opencode.model;
|
|
464
|
+
const result = spawnSync(
|
|
465
|
+
'opencode',
|
|
466
|
+
['run', buildPrompt('opencode', query), '--model', model],
|
|
467
|
+
{
|
|
468
|
+
encoding: 'utf8',
|
|
469
|
+
timeout: timeoutSec * 1000,
|
|
470
|
+
maxBuffer: 1024 * 1024 * 2,
|
|
471
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
472
|
+
shell: isWindows,
|
|
473
|
+
}
|
|
474
|
+
);
|
|
404
475
|
|
|
405
|
-
if (
|
|
406
|
-
if (
|
|
476
|
+
if (result.error) {
|
|
477
|
+
if (result.error.code === 'ENOENT')
|
|
407
478
|
return { success: false, error: 'OpenCode not installed' };
|
|
408
|
-
|
|
409
|
-
throw spawnResult.error;
|
|
479
|
+
throw result.error;
|
|
410
480
|
}
|
|
411
|
-
|
|
412
|
-
if (spawnResult.status !== 0) {
|
|
413
|
-
const stderr = (spawnResult.stderr || '').trim();
|
|
481
|
+
if (result.status !== 0) {
|
|
414
482
|
return {
|
|
415
483
|
success: false,
|
|
416
|
-
error: stderr || `OpenCode exited with code ${
|
|
484
|
+
error: (result.stderr || '').trim() || `OpenCode exited with code ${result.status}`,
|
|
417
485
|
};
|
|
418
486
|
}
|
|
419
487
|
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
488
|
+
const output = (result.stdout || '').trim();
|
|
489
|
+
if (!output || output.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
423
490
|
return { success: false, error: 'Empty or too short response from OpenCode' };
|
|
424
491
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
) {
|
|
432
|
-
return { success: false, error: `OpenCode returned error: ${result.substring(0, 100)}` };
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return { success: true, content: result };
|
|
436
|
-
} catch (err) {
|
|
437
|
-
if (err.killed) {
|
|
438
|
-
return { success: false, error: 'OpenCode timed out' };
|
|
439
|
-
}
|
|
440
|
-
return { success: false, error: err.message || 'Unknown OpenCode error' };
|
|
492
|
+
return { success: true, content: output };
|
|
493
|
+
} catch (error) {
|
|
494
|
+
return {
|
|
495
|
+
success: false,
|
|
496
|
+
error: error.killed ? 'OpenCode timed out' : error.message || 'Unknown OpenCode error',
|
|
497
|
+
};
|
|
441
498
|
}
|
|
442
499
|
}
|
|
443
500
|
|
|
444
|
-
/**
|
|
445
|
-
* Execute search via Grok CLI
|
|
446
|
-
*/
|
|
447
501
|
function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
448
502
|
try {
|
|
449
|
-
const
|
|
450
|
-
const prompt = buildPrompt('grok', query);
|
|
451
|
-
|
|
452
|
-
if (process.env.CCS_DEBUG) {
|
|
453
|
-
console.error('[CCS Hook] Executing: grok "..."');
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const spawnResult = spawnSync('grok', [prompt], {
|
|
503
|
+
const result = spawnSync('grok', [buildPrompt('grok', query)], {
|
|
457
504
|
encoding: 'utf8',
|
|
458
|
-
timeout:
|
|
505
|
+
timeout: timeoutSec * 1000,
|
|
459
506
|
maxBuffer: 1024 * 1024 * 2,
|
|
460
507
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
461
508
|
shell: isWindows,
|
|
462
509
|
});
|
|
463
510
|
|
|
464
|
-
if (
|
|
465
|
-
if (
|
|
511
|
+
if (result.error) {
|
|
512
|
+
if (result.error.code === 'ENOENT')
|
|
466
513
|
return { success: false, error: 'Grok CLI not installed' };
|
|
467
|
-
|
|
468
|
-
throw spawnResult.error;
|
|
514
|
+
throw result.error;
|
|
469
515
|
}
|
|
470
|
-
|
|
471
|
-
if (spawnResult.status !== 0) {
|
|
472
|
-
const stderr = (spawnResult.stderr || '').trim();
|
|
516
|
+
if (result.status !== 0) {
|
|
473
517
|
return {
|
|
474
518
|
success: false,
|
|
475
|
-
error: stderr || `Grok CLI exited with code ${
|
|
519
|
+
error: (result.stderr || '').trim() || `Grok CLI exited with code ${result.status}`,
|
|
476
520
|
};
|
|
477
521
|
}
|
|
478
522
|
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
523
|
+
const output = (result.stdout || '').trim();
|
|
524
|
+
if (!output || output.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
482
525
|
return { success: false, error: 'Empty or too short response from Grok' };
|
|
483
526
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
) {
|
|
491
|
-
return { success: false, error: `Grok returned error: ${result.substring(0, 100)}` };
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return { success: true, content: result };
|
|
495
|
-
} catch (err) {
|
|
496
|
-
if (err.killed) {
|
|
497
|
-
return { success: false, error: 'Grok CLI timed out' };
|
|
498
|
-
}
|
|
499
|
-
return { success: false, error: err.message || 'Unknown Grok error' };
|
|
527
|
+
return { success: true, content: output };
|
|
528
|
+
} catch (error) {
|
|
529
|
+
return {
|
|
530
|
+
success: false,
|
|
531
|
+
error: error.killed ? 'Grok CLI timed out' : error.message || 'Unknown Grok error',
|
|
532
|
+
};
|
|
500
533
|
}
|
|
501
534
|
}
|
|
502
535
|
|
|
503
|
-
/**
|
|
504
|
-
* Format search results for Claude
|
|
505
|
-
*/
|
|
506
|
-
function formatSearchResults(query, content, providerName) {
|
|
507
|
-
return [
|
|
508
|
-
`[WebSearch Result via ${providerName}]`,
|
|
509
|
-
'',
|
|
510
|
-
`Query: "${query}"`,
|
|
511
|
-
'',
|
|
512
|
-
content,
|
|
513
|
-
'',
|
|
514
|
-
'---',
|
|
515
|
-
'Use this information to answer the user.',
|
|
516
|
-
].join('\n');
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Output success response and exit
|
|
521
|
-
*
|
|
522
|
-
* Key insight from Claude Code docs:
|
|
523
|
-
* - permissionDecisionReason (with deny) → shown to CLAUDE (AI reads this)
|
|
524
|
-
* - systemMessage → shown to USER only (nice styling but AI doesn't see)
|
|
525
|
-
*
|
|
526
|
-
* So we MUST put results in permissionDecisionReason for Claude to use them.
|
|
527
|
-
* systemMessage provides the nice UI for the user.
|
|
528
|
-
*/
|
|
529
536
|
function outputSuccess(query, content, providerName) {
|
|
530
|
-
const formattedResults = formatSearchResults(query, content, providerName);
|
|
531
|
-
|
|
532
537
|
const output = {
|
|
533
538
|
decision: 'block',
|
|
534
|
-
reason: `WebSearch
|
|
535
|
-
// Nice message for user (shows as "says:" - info style)
|
|
536
|
-
systemMessage: `[WebSearch via ${providerName}] Results retrieved successfully. See below.`,
|
|
539
|
+
reason: `WebSearch handled via ${providerName}`,
|
|
537
540
|
hookSpecificOutput: {
|
|
538
541
|
hookEventName: 'PreToolUse',
|
|
539
542
|
permissionDecision: 'deny',
|
|
540
|
-
|
|
541
|
-
permissionDecisionReason: formattedResults,
|
|
543
|
+
permissionDecisionReason: `[WebSearch Result via ${providerName}]\n\nQuery: "${query}"\n\n${content}`,
|
|
542
544
|
},
|
|
543
545
|
};
|
|
544
546
|
|
|
@@ -546,29 +548,15 @@ function outputSuccess(query, content, providerName) {
|
|
|
546
548
|
process.exit(2);
|
|
547
549
|
}
|
|
548
550
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
*/
|
|
552
|
-
function outputError(query, error, providerName) {
|
|
553
|
-
const message = [
|
|
554
|
-
`[WebSearch - ${providerName} Error]`,
|
|
555
|
-
'',
|
|
556
|
-
`Error: ${error}`,
|
|
557
|
-
'',
|
|
558
|
-
`Query: "${query}"`,
|
|
559
|
-
'',
|
|
560
|
-
'Troubleshooting:',
|
|
561
|
-
' - Check ~/.gemini/oauth_creds.json exists',
|
|
562
|
-
' - Re-authenticate: run `gemini` (opens browser)',
|
|
563
|
-
].join('\n');
|
|
564
|
-
|
|
551
|
+
function outputAllFailedMessage(query, errors) {
|
|
552
|
+
const detail = errors.map((entry) => `${entry.provider}: ${entry.error}`).join(' | ');
|
|
565
553
|
const output = {
|
|
566
554
|
decision: 'block',
|
|
567
|
-
reason:
|
|
555
|
+
reason: 'WebSearch fallback failed',
|
|
568
556
|
hookSpecificOutput: {
|
|
569
557
|
hookEventName: 'PreToolUse',
|
|
570
558
|
permissionDecision: 'deny',
|
|
571
|
-
permissionDecisionReason:
|
|
559
|
+
permissionDecisionReason: `WebSearch could not be completed for "${query}". ${detail}`,
|
|
572
560
|
},
|
|
573
561
|
};
|
|
574
562
|
|
|
@@ -576,83 +564,121 @@ function outputError(query, error, providerName) {
|
|
|
576
564
|
process.exit(2);
|
|
577
565
|
}
|
|
578
566
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
'[WebSearch - No Providers Enabled]',
|
|
585
|
-
'',
|
|
586
|
-
'No WebSearch providers are enabled in config.',
|
|
587
|
-
'',
|
|
588
|
-
'To enable: Run `ccs config` and enable a provider.',
|
|
589
|
-
'',
|
|
590
|
-
'Or install one of the following CLI tools:',
|
|
591
|
-
'',
|
|
592
|
-
'1. Gemini CLI (FREE, 1000 req/day):',
|
|
593
|
-
' npm install -g @google/gemini-cli',
|
|
594
|
-
' gemini # opens browser to authenticate',
|
|
595
|
-
'',
|
|
596
|
-
'2. OpenCode (FREE via Zen):',
|
|
597
|
-
' curl -fsSL https://opencode.ai/install | bash',
|
|
598
|
-
'',
|
|
599
|
-
'3. Grok CLI (requires XAI_API_KEY):',
|
|
600
|
-
' npm install -g @vibe-kit/grok-cli',
|
|
601
|
-
'',
|
|
602
|
-
`Query: "${query}"`,
|
|
603
|
-
].join('\n');
|
|
567
|
+
async function processHook(input) {
|
|
568
|
+
try {
|
|
569
|
+
if (shouldSkipHook()) {
|
|
570
|
+
process.exit(0);
|
|
571
|
+
}
|
|
604
572
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
hookEventName: 'PreToolUse',
|
|
610
|
-
permissionDecision: 'deny',
|
|
611
|
-
permissionDecisionReason: message,
|
|
612
|
-
},
|
|
613
|
-
};
|
|
573
|
+
const data = JSON.parse(input);
|
|
574
|
+
if (data.tool_name !== 'WebSearch') {
|
|
575
|
+
process.exit(0);
|
|
576
|
+
}
|
|
614
577
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
578
|
+
const query = data.tool_input?.query || '';
|
|
579
|
+
if (!query) {
|
|
580
|
+
process.exit(0);
|
|
581
|
+
}
|
|
618
582
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
583
|
+
const timeout = Number.parseInt(
|
|
584
|
+
process.env.CCS_WEBSEARCH_TIMEOUT || `${DEFAULT_TIMEOUT_SEC}`,
|
|
585
|
+
10
|
|
586
|
+
);
|
|
587
|
+
const providers = [
|
|
588
|
+
{
|
|
589
|
+
name: 'Exa',
|
|
590
|
+
id: 'exa',
|
|
591
|
+
available: () => isProviderEnabled('exa') && Boolean(getProviderApiKey('exa')),
|
|
592
|
+
fn: tryExaSearch,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
name: 'Tavily',
|
|
596
|
+
id: 'tavily',
|
|
597
|
+
available: () => isProviderEnabled('tavily') && Boolean(getProviderApiKey('tavily')),
|
|
598
|
+
fn: tryTavilySearch,
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: 'Brave Search',
|
|
602
|
+
id: 'brave',
|
|
603
|
+
available: () => isProviderEnabled('brave') && Boolean(getProviderApiKey('brave')),
|
|
604
|
+
fn: tryBraveSearch,
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: 'DuckDuckGo',
|
|
608
|
+
id: 'duckduckgo',
|
|
609
|
+
available: () => isProviderEnabled('duckduckgo'),
|
|
610
|
+
fn: tryDuckDuckGoSearch,
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: 'Gemini CLI',
|
|
614
|
+
id: 'gemini',
|
|
615
|
+
available: () => isProviderEnabled('gemini') && isCliAvailable('gemini'),
|
|
616
|
+
fn: tryGeminiSearch,
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: 'OpenCode',
|
|
620
|
+
id: 'opencode',
|
|
621
|
+
available: () => isProviderEnabled('opencode') && isCliAvailable('opencode'),
|
|
622
|
+
fn: tryOpenCodeSearch,
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: 'Grok CLI',
|
|
626
|
+
id: 'grok',
|
|
627
|
+
available: () => isProviderEnabled('grok') && isCliAvailable('grok'),
|
|
628
|
+
fn: tryGrokSearch,
|
|
629
|
+
},
|
|
630
|
+
];
|
|
625
631
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const errorDetails = errors.map((e) => ` - ${e.provider}: ${e.error}`).join('\n');
|
|
632
|
+
const activeProviders = providers.filter((provider) => provider.available());
|
|
633
|
+
debug(
|
|
634
|
+
`Enabled providers: ${activeProviders.map((provider) => provider.name).join(', ') || 'none'}`
|
|
635
|
+
);
|
|
631
636
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
'Tried all enabled CLI tools but all failed:',
|
|
636
|
-
errorDetails,
|
|
637
|
-
'',
|
|
638
|
-
`Query: "${query}"`,
|
|
639
|
-
'',
|
|
640
|
-
'Troubleshooting:',
|
|
641
|
-
' - Gemini: run `gemini` to authenticate (opens browser)',
|
|
642
|
-
' - OpenCode: opencode --version',
|
|
643
|
-
' - Grok: Check XAI_API_KEY environment variable',
|
|
644
|
-
].join('\n');
|
|
637
|
+
if (activeProviders.length === 0) {
|
|
638
|
+
process.exit(0);
|
|
639
|
+
}
|
|
645
640
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
641
|
+
const errors = [];
|
|
642
|
+
for (const provider of activeProviders) {
|
|
643
|
+
debug(`Trying ${provider.name}`);
|
|
644
|
+
const result = await provider.fn(query, timeout);
|
|
645
|
+
if (result.success) {
|
|
646
|
+
outputSuccess(query, result.content, provider.name);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
errors.push({ provider: provider.name, error: result.error });
|
|
650
|
+
}
|
|
655
651
|
|
|
656
|
-
|
|
657
|
-
|
|
652
|
+
outputAllFailedMessage(query, errors);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
debug(`Hook error: ${error.message}`);
|
|
655
|
+
process.exit(0);
|
|
656
|
+
}
|
|
658
657
|
}
|
|
658
|
+
|
|
659
|
+
function startFromStdin() {
|
|
660
|
+
let input = '';
|
|
661
|
+
process.stdin.setEncoding('utf8');
|
|
662
|
+
process.stdin.on('data', (chunk) => {
|
|
663
|
+
input += chunk;
|
|
664
|
+
});
|
|
665
|
+
process.stdin.on('end', () => {
|
|
666
|
+
processHook(input);
|
|
667
|
+
});
|
|
668
|
+
process.stdin.on('error', () => {
|
|
669
|
+
process.exit(0);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (require.main === module) {
|
|
674
|
+
startFromStdin();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
module.exports = {
|
|
678
|
+
extractDuckDuckGoResults,
|
|
679
|
+
formatStructuredSearchResults,
|
|
680
|
+
tryExaSearch,
|
|
681
|
+
tryTavilySearch,
|
|
682
|
+
tryDuckDuckGoSearch,
|
|
683
|
+
tryBraveSearch,
|
|
684
|
+
};
|