@kaitranntt/ccs 7.56.0-dev.4 → 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.
Files changed (64) hide show
  1. package/README.md +23 -17
  2. package/dist/config/unified-config-loader.d.ts +16 -0
  3. package/dist/config/unified-config-loader.d.ts.map +1 -1
  4. package/dist/config/unified-config-loader.js +56 -12
  5. package/dist/config/unified-config-loader.js.map +1 -1
  6. package/dist/config/unified-config-types.d.ts +56 -10
  7. package/dist/config/unified-config-types.d.ts.map +1 -1
  8. package/dist/config/unified-config-types.js +20 -2
  9. package/dist/config/unified-config-types.js.map +1 -1
  10. package/dist/ui/assets/{accounts-BSknYWh4.js → accounts-CcKtNJpZ.js} +1 -1
  11. package/dist/ui/assets/{alert-dialog-BsXoK0Gt.js → alert-dialog-D9ixkKjG.js} +1 -1
  12. package/dist/ui/assets/{api-C-ZXD7TW.js → api-ClWqAKR1.js} +1 -1
  13. package/dist/ui/assets/{auth-section-CjGg1fit.js → auth-section-BQ4Kogh5.js} +1 -1
  14. package/dist/ui/assets/{backups-section-fHW6gGBE.js → backups-section-fPK5pSgx.js} +1 -1
  15. package/dist/ui/assets/{checkbox-K93r237H.js → checkbox-Cr_w2dBr.js} +1 -1
  16. package/dist/ui/assets/{claude-extension-feiyhDgC.js → claude-extension-Blq0eREl.js} +1 -1
  17. package/dist/ui/assets/{cliproxy-CVv_-PsP.js → cliproxy-CPWxG68T.js} +1 -1
  18. package/dist/ui/assets/{cliproxy-ai-providers-0JQCQ6YS.js → cliproxy-ai-providers-CWIwmTNM.js} +1 -1
  19. package/dist/ui/assets/{cliproxy-control-panel-CrHA_sjo.js → cliproxy-control-panel-CgEMwXOj.js} +1 -1
  20. package/dist/ui/assets/{confirm-dialog-_wN3tR_n.js → confirm-dialog-BLoDbdRn.js} +1 -1
  21. package/dist/ui/assets/{copilot-Cao_05p3.js → copilot-tFEndl-V.js} +1 -1
  22. package/dist/ui/assets/{cursor-Cbe1L5vq.js → cursor-d_8EDsFB.js} +1 -1
  23. package/dist/ui/assets/{droid-Cu3rERcX.js → droid-BIbPZCVp.js} +1 -1
  24. package/dist/ui/assets/{globalenv-section-CtzXlKoL.js → globalenv-section-C1OjUAhr.js} +1 -1
  25. package/dist/ui/assets/{health-CjuQlQLu.js → health-BAy1igW2.js} +1 -1
  26. package/dist/ui/assets/{icons-DR-ORtNe.js → icons-BwsSbo2z.js} +1 -1
  27. package/dist/ui/assets/index-Bp4s-Led.css +1 -0
  28. package/dist/ui/assets/{index-17YWpyV1.js → index-D-xh-rUY.js} +1 -1
  29. package/dist/ui/assets/index-DVgRJ--R.js +1 -0
  30. package/dist/ui/assets/{index-CtS8lmbp.js → index-DjsOxm87.js} +1 -1
  31. package/dist/ui/assets/{index-gjtWXRc2.js → index-KQ7UM0Zx.js} +3 -3
  32. package/dist/ui/assets/{index-B4MTOnn6.js → index-L-WMPeHP.js} +1 -1
  33. package/dist/ui/assets/{proxy-status-widget-9CJglirI.js → proxy-status-widget-CibAyvji.js} +1 -1
  34. package/dist/ui/assets/{searchable-select-CGOND84a.js → searchable-select-BCt9hVab.js} +1 -1
  35. package/dist/ui/assets/{separator-2e8ySql0.js → separator-B-7tcT70.js} +1 -1
  36. package/dist/ui/assets/{shared-598yR3xg.js → shared-B5lECoNe.js} +1 -1
  37. package/dist/ui/assets/{switch-BUqDuRxm.js → switch-ofCljueo.js} +1 -1
  38. package/dist/ui/assets/{updates-CQW2-eYw.js → updates-CDdcIRgb.js} +1 -1
  39. package/dist/ui/index.html +3 -3
  40. package/dist/utils/websearch/hook-env.d.ts.map +1 -1
  41. package/dist/utils/websearch/hook-env.js +17 -1
  42. package/dist/utils/websearch/hook-env.js.map +1 -1
  43. package/dist/utils/websearch/status.d.ts +5 -12
  44. package/dist/utils/websearch/status.d.ts.map +1 -1
  45. package/dist/utils/websearch/status.js +128 -87
  46. package/dist/utils/websearch/status.js.map +1 -1
  47. package/dist/utils/websearch/types.d.ts +37 -23
  48. package/dist/utils/websearch/types.d.ts.map +1 -1
  49. package/dist/utils/websearch/types.js +1 -1
  50. package/dist/utils/websearch-manager.d.ts +5 -4
  51. package/dist/utils/websearch-manager.d.ts.map +1 -1
  52. package/dist/utils/websearch-manager.js +5 -4
  53. package/dist/utils/websearch-manager.js.map +1 -1
  54. package/dist/web-server/health/websearch-checks.d.ts +2 -2
  55. package/dist/web-server/health/websearch-checks.d.ts.map +1 -1
  56. package/dist/web-server/health/websearch-checks.js +9 -11
  57. package/dist/web-server/health/websearch-checks.js.map +1 -1
  58. package/dist/web-server/routes/websearch-routes.d.ts.map +1 -1
  59. package/dist/web-server/routes/websearch-routes.js +33 -22
  60. package/dist/web-server/routes/websearch-routes.js.map +1 -1
  61. package/lib/hooks/websearch-transformer.cjs +488 -462
  62. package/package.json +1 -1
  63. package/dist/ui/assets/index-CKpdevWy.css +0 -1
  64. package/dist/ui/assets/index-DRuiCZZ-.js +0 -1
@@ -1,51 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * CCS WebSearch Hook - CLI Tool Executor with Fallback Chain
3
+ * CCS WebSearch Hook - deterministic search backends with legacy CLI fallback
4
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
5
+ * Primary providers:
6
+ * - Exa Search API
7
+ * - Tavily Search API
8
+ * - Brave Search API
9
+ * - DuckDuckGo HTML search
8
10
  *
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
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
- * 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
- ];
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
- if (config.quirks) {
115
- parts.push('', `Note: ${config.quirks}`);
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
- const envVar = `CCS_WEBSEARCH_${provider.toUpperCase()}`;
187
- const value = process.env[envVar];
90
+ return process.env[`CCS_WEBSEARCH_${provider.toUpperCase()}`] === '1';
91
+ }
188
92
 
189
- // If env var not set, provider is disabled by default
190
- // This ensures we respect config.yaml settings
191
- return value === '1';
93
+ function hasEnvValue(name) {
94
+ return (process.env[name] || '').trim().length > 0;
192
95
  }
193
96
 
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);
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
- const data = JSON.parse(input);
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
- // Only handle WebSearch tool
207
- if (data.tool_name !== 'WebSearch') {
208
- process.exit(0);
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
- const query = data.tool_input?.query || '';
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
- if (!query) {
214
- process.exit(0);
140
+ function decodeHtml(value) {
141
+ return value
142
+ .replace(/&amp;/g, '&')
143
+ .replace(/&quot;/g, '"')
144
+ .replace(/&#39;/g, "'")
145
+ .replace(/&lt;/g, '<')
146
+ .replace(/&gt;/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
- const timeout = parseInt(process.env.CCS_WEBSEARCH_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
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
- // 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
- ];
185
+ function formatStructuredSearchResults(query, providerName, results) {
186
+ if (!results.length) {
187
+ return `No search results found for "${query}" via ${providerName}.`;
188
+ }
226
189
 
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);
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
- if (process.env.CCS_DEBUG) {
233
- console.error(`[CCS Hook] ${p.name}: enabled=${enabled}, available=${available}`);
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
- return enabled && available;
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
- const errors = [];
219
+ const params = new URLSearchParams({
220
+ q: query,
221
+ count: String(getResultCount('brave')),
222
+ });
240
223
 
241
- if (process.env.CCS_DEBUG) {
242
- const names = enabledProviders.map((p) => p.name).join(', ') || 'none';
243
- console.error(`[CCS Hook] Enabled providers: ${names}`);
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
- // 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);
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
- if (result.success) {
255
- outputSuccess(query, result.content, provider.name);
256
- return;
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
- if (process.env.CCS_DEBUG) {
260
- console.error(`[CCS Hook] ${provider.name} failed: ${result.error}`);
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
- errors.push({ provider: provider.name, error: result.error });
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
- // 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);
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
- } catch (err) {
275
- if (process.env.CCS_DEBUG) {
276
- console.error('[CCS Hook] Parse error:', err.message);
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
- process.exit(0);
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 lowerError = (errorMessage || '').toLowerCase();
287
-
400
+ const lower = (errorMessage || '').toLowerCase();
288
401
  return (
289
- lowerError.includes('unknown option') ||
290
- lowerError.includes('unknown argument') ||
291
- lowerError.includes('unrecognized option') ||
292
- lowerError.includes('usage: gemini') ||
293
- lowerError.includes('use --prompt') ||
294
- lowerError.includes('using the --prompt option')
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 spawnResult = spawnSync('gemini', args, {
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 (spawnResult.error) {
308
- if (spawnResult.error.code === 'ENOENT') {
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 ${spawnResult.status}`,
428
+ error: (result.stderr || '').trim() || `Gemini CLI exited with code ${result.status}`,
319
429
  };
320
430
  }
321
431
 
322
- const result = (spawnResult.stdout || '').trim();
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 config = PROVIDER_CONFIG.gemini;
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
- // Current Gemini CLI prefers positional prompts and deprecates -p/--prompt.
356
- // Retry once with -p for legacy CLIs that still require it.
357
- const positionalResult = runGeminiCommand(positionalArgs, timeoutMs);
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
- if (!shouldRetryGeminiWithLegacyPrompt(positionalResult.error)) {
363
- return positionalResult;
364
- }
365
-
366
- if (process.env.CCS_DEBUG) {
367
- console.error('[CCS Hook] Positional Gemini prompt failed; retrying with -p for legacy CLI');
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 timeoutMs = timeoutSec * 1000;
387
- const config = PROVIDER_CONFIG.opencode;
388
-
389
- // Allow model override via env var
390
- const model = process.env.CCS_WEBSEARCH_OPENCODE_MODEL || config.model;
391
- const prompt = buildPrompt('opencode', query);
392
-
393
- if (process.env.CCS_DEBUG) {
394
- console.error(`[CCS Hook] Executing: opencode run --model ${model} "..."`);
395
- }
396
-
397
- const spawnResult = spawnSync('opencode', ['run', prompt, '--model', model], {
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 (spawnResult.error) {
406
- if (spawnResult.error.code === 'ENOENT') {
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 ${spawnResult.status}`,
484
+ error: (result.stderr || '').trim() || `OpenCode exited with code ${result.status}`,
417
485
  };
418
486
  }
419
487
 
420
- const result = (spawnResult.stdout || '').trim();
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
- const lowerResult = result.toLowerCase();
427
- if (
428
- lowerResult.includes('error:') ||
429
- lowerResult.includes('failed to') ||
430
- lowerResult.includes('authentication required')
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 timeoutMs = timeoutSec * 1000;
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: timeoutMs,
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 (spawnResult.error) {
465
- if (spawnResult.error.code === 'ENOENT') {
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 ${spawnResult.status}`,
519
+ error: (result.stderr || '').trim() || `Grok CLI exited with code ${result.status}`,
476
520
  };
477
521
  }
478
522
 
479
- const result = (spawnResult.stdout || '').trim();
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
- const lowerResult = result.toLowerCase();
486
- if (
487
- lowerResult.includes('error:') ||
488
- lowerResult.includes('failed to') ||
489
- lowerResult.includes('api key')
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 completed via ${providerName}`,
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
- // Full results here - Claude reads this
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
- * Output error message
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: `WebSearch failed: ${error}`,
555
+ reason: 'WebSearch fallback failed',
568
556
  hookSpecificOutput: {
569
557
  hookEventName: 'PreToolUse',
570
558
  permissionDecision: 'deny',
571
- permissionDecisionReason: message,
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
- * Output no providers enabled message
581
- */
582
- function outputNoProvidersEnabled(query) {
583
- const message = [
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
- const output = {
606
- decision: 'block',
607
- reason: 'WebSearch unavailable - no providers enabled',
608
- hookSpecificOutput: {
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
- console.log(JSON.stringify(output));
616
- process.exit(2);
617
- }
578
+ const query = data.tool_input?.query || '';
579
+ if (!query) {
580
+ process.exit(0);
581
+ }
618
582
 
619
- /**
620
- * Output no tools message (legacy - kept for backwards compatibility)
621
- */
622
- function outputNoToolsMessage(query) {
623
- outputNoProvidersEnabled(query);
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
- * Output all providers failed message
628
- */
629
- function outputAllFailedMessage(query, errors) {
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
- const message = [
633
- '[WebSearch - All Providers Failed]',
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
- const output = {
647
- decision: 'block',
648
- reason: 'WebSearch failed - all providers failed',
649
- hookSpecificOutput: {
650
- hookEventName: 'PreToolUse',
651
- permissionDecision: 'deny',
652
- permissionDecisionReason: message,
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
- console.log(JSON.stringify(output));
657
- process.exit(2);
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
+ };