@kaitranntt/ccs 5.20.0 → 6.0.0-dev.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.
Files changed (76) hide show
  1. package/README.md +39 -0
  2. package/VERSION +1 -1
  3. package/dist/ccs.js +9 -1
  4. package/dist/ccs.js.map +1 -1
  5. package/dist/cliproxy/account-manager.d.ts.map +1 -1
  6. package/dist/cliproxy/account-manager.js +3 -1
  7. package/dist/cliproxy/account-manager.js.map +1 -1
  8. package/dist/cliproxy/cliproxy-executor.d.ts.map +1 -1
  9. package/dist/cliproxy/cliproxy-executor.js +145 -76
  10. package/dist/cliproxy/cliproxy-executor.js.map +1 -1
  11. package/dist/cliproxy/session-tracker.d.ts +54 -0
  12. package/dist/cliproxy/session-tracker.d.ts.map +1 -0
  13. package/dist/cliproxy/session-tracker.js +228 -0
  14. package/dist/cliproxy/session-tracker.js.map +1 -0
  15. package/dist/cliproxy/stats-fetcher.d.ts +19 -0
  16. package/dist/cliproxy/stats-fetcher.d.ts.map +1 -1
  17. package/dist/cliproxy/stats-fetcher.js +41 -3
  18. package/dist/cliproxy/stats-fetcher.js.map +1 -1
  19. package/dist/config/unified-config-loader.d.ts +33 -0
  20. package/dist/config/unified-config-loader.d.ts.map +1 -1
  21. package/dist/config/unified-config-loader.js +105 -3
  22. package/dist/config/unified-config-loader.js.map +1 -1
  23. package/dist/config/unified-config-types.d.ts +75 -1
  24. package/dist/config/unified-config-types.d.ts.map +1 -1
  25. package/dist/config/unified-config-types.js +21 -1
  26. package/dist/config/unified-config-types.js.map +1 -1
  27. package/dist/ui/assets/{accounts-achdtDUJ.js → accounts-BfRSUzvC.js} +1 -1
  28. package/dist/ui/assets/{analytics-BVlC8Y7-.js → analytics-ClDxRP9x.js} +34 -34
  29. package/dist/ui/assets/api-DyJHhCv0.js +1 -0
  30. package/dist/ui/assets/card-CrGAcUkx.js +1 -0
  31. package/dist/ui/assets/cliproxy-Zr4pFy6O.js +1 -0
  32. package/dist/ui/assets/cliproxy-control-panel-Cu7DgEPx.js +1 -0
  33. package/dist/ui/assets/code-editor-D0YjMRQO.js +2 -0
  34. package/dist/ui/assets/{form-utils-DKkU3nz7.js → form-utils-CKETQmt9.js} +1 -1
  35. package/dist/ui/assets/{health-Xbq8eUat.js → health-Rretog4H.js} +1 -1
  36. package/dist/ui/assets/icons-D6DaLbNh.js +1 -0
  37. package/dist/ui/assets/index-B2PFll-u.js +46 -0
  38. package/dist/ui/assets/index-DTVwRqgB.css +1 -0
  39. package/dist/ui/assets/providers/iflow.png +0 -0
  40. package/dist/ui/assets/{radix-ui-OFtPgiRV.js → radix-ui-Bhyw9pC9.js} +2 -2
  41. package/dist/ui/assets/{react-vendor-CjrBBxxX.js → react-vendor-DadlvKzT.js} +1 -1
  42. package/dist/ui/assets/settings-B7OFU9lZ.js +1 -0
  43. package/dist/ui/assets/shared-qGCa7mu2.js +1 -0
  44. package/dist/ui/assets/{tanstack-DMWkeNzM.js → tanstack-DZ3a6J0N.js} +1 -1
  45. package/dist/ui/index.html +7 -7
  46. package/dist/utils/shell-executor.d.ts.map +1 -1
  47. package/dist/utils/shell-executor.js +6 -1
  48. package/dist/utils/shell-executor.js.map +1 -1
  49. package/dist/utils/websearch-manager.d.ts +197 -0
  50. package/dist/utils/websearch-manager.d.ts.map +1 -0
  51. package/dist/utils/websearch-manager.js +685 -0
  52. package/dist/utils/websearch-manager.js.map +1 -0
  53. package/dist/web-server/health-service.d.ts.map +1 -1
  54. package/dist/web-server/health-service.js +50 -0
  55. package/dist/web-server/health-service.js.map +1 -1
  56. package/dist/web-server/routes.d.ts.map +1 -1
  57. package/dist/web-server/routes.js +169 -1
  58. package/dist/web-server/routes.js.map +1 -1
  59. package/dist/web-server/shutdown.d.ts +4 -4
  60. package/dist/web-server/shutdown.d.ts.map +1 -1
  61. package/dist/web-server/shutdown.js +9 -17
  62. package/dist/web-server/shutdown.js.map +1 -1
  63. package/lib/hooks/block-websearch.cjs +75 -0
  64. package/lib/hooks/websearch-transformer.cjs +600 -0
  65. package/package.json +2 -1
  66. package/scripts/dev-install.sh +57 -5
  67. package/scripts/preinstall.js +59 -0
  68. package/dist/ui/assets/api-BwWsFLoC.js +0 -1
  69. package/dist/ui/assets/cliproxy-DuaxYcVk.js +0 -1
  70. package/dist/ui/assets/cliproxy-control-panel-DTiBzA5u.js +0 -1
  71. package/dist/ui/assets/code-editor-D7CYoILm.js +0 -36
  72. package/dist/ui/assets/icons-Alnq4BWm.js +0 -1
  73. package/dist/ui/assets/index-B1gvMo-b.css +0 -1
  74. package/dist/ui/assets/index-C2RMogI8.js +0 -12
  75. package/dist/ui/assets/settings-95g3F5Gk.js +0 -1
  76. package/dist/ui/assets/shared-Cg5XjdQM.js +0 -1
@@ -0,0 +1,685 @@
1
+ "use strict";
2
+ /**
3
+ * WebSearch Manager - Manages WebSearch hook for CCS
4
+ *
5
+ * WebSearch is a server-side tool executed by Anthropic's API.
6
+ * Third-party providers (gemini, agy, codex, qwen) don't have access.
7
+ * This manager installs a hook that uses CLI tools (Gemini CLI) as fallback.
8
+ *
9
+ * Simplified Architecture:
10
+ * - No MCP complexity
11
+ * - Uses CLI tools (currently Gemini CLI)
12
+ * - Easy to extend for future CLI tools (opencode, etc.)
13
+ *
14
+ * @module utils/websearch-manager
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.getMcpConfigPath = exports.hasMcpWebSearch = exports.ensureMcpWebSearch = exports.displayWebSearchStatus = exports.getWebSearchReadiness = exports.getWebSearchHookEnv = exports.getWebSearchHookConfig = exports.hasWebSearchHook = exports.installWebSearchHook = exports.getCliInstallHints = exports.hasAnyWebSearchCli = exports.getWebSearchCliProviders = exports.clearAllCliCaches = exports.clearOpenCodeCliCache = exports.hasOpenCodeCli = exports.getOpenCodeCliStatus = exports.clearGrokCliCache = exports.hasGrokCli = exports.getGrokCliStatus = exports.clearGeminiCliCache = exports.hasGeminiCli = exports.getGeminiCliStatus = void 0;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const os = __importStar(require("os"));
44
+ const child_process_1 = require("child_process");
45
+ const ui_1 = require("./ui");
46
+ const unified_config_loader_1 = require("../config/unified-config-loader");
47
+ // CCS hooks directory
48
+ const CCS_HOOKS_DIR = path.join(os.homedir(), '.ccs', 'hooks');
49
+ // Hook file name
50
+ const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
51
+ // Buffer time added to max provider timeout for hook timeout (seconds)
52
+ const HOOK_TIMEOUT_BUFFER = 30;
53
+ // Minimum hook timeout in seconds (fallback if no providers configured)
54
+ const MIN_HOOK_TIMEOUT = 60;
55
+ // Path to Claude settings.json
56
+ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
57
+ // Cache for Gemini CLI status (per process)
58
+ let geminiCliCache = null;
59
+ /**
60
+ * Check if Gemini CLI is installed globally
61
+ *
62
+ * Requires global install: `npm install -g @google/gemini-cli`
63
+ * No npx fallback - must be in PATH
64
+ *
65
+ * @returns Gemini CLI status with path and version
66
+ */
67
+ function getGeminiCliStatus() {
68
+ // Return cached result if available
69
+ if (geminiCliCache) {
70
+ return geminiCliCache;
71
+ }
72
+ const result = {
73
+ installed: false,
74
+ path: null,
75
+ version: null,
76
+ };
77
+ try {
78
+ const isWindows = process.platform === 'win32';
79
+ const whichCmd = isWindows ? 'where gemini' : 'which gemini';
80
+ const pathResult = (0, child_process_1.execSync)(whichCmd, {
81
+ encoding: 'utf8',
82
+ timeout: 5000,
83
+ stdio: ['pipe', 'pipe', 'pipe'],
84
+ });
85
+ const geminiPath = pathResult.trim().split('\n')[0]; // First result on Windows
86
+ if (geminiPath) {
87
+ result.installed = true;
88
+ result.path = geminiPath;
89
+ // Try to get version
90
+ try {
91
+ const versionResult = (0, child_process_1.execSync)('gemini --version', {
92
+ encoding: 'utf8',
93
+ timeout: 5000,
94
+ stdio: ['pipe', 'pipe', 'pipe'],
95
+ });
96
+ result.version = versionResult.trim();
97
+ }
98
+ catch {
99
+ // Version check failed, but CLI is installed
100
+ result.version = 'unknown';
101
+ }
102
+ }
103
+ }
104
+ catch {
105
+ // Command not found - Gemini CLI not installed
106
+ }
107
+ // Cache result
108
+ geminiCliCache = result;
109
+ return result;
110
+ }
111
+ exports.getGeminiCliStatus = getGeminiCliStatus;
112
+ /**
113
+ * Check if Gemini CLI is available (quick boolean check)
114
+ */
115
+ function hasGeminiCli() {
116
+ return getGeminiCliStatus().installed;
117
+ }
118
+ exports.hasGeminiCli = hasGeminiCli;
119
+ /**
120
+ * Clear Gemini CLI cache (for testing or after installation)
121
+ */
122
+ function clearGeminiCliCache() {
123
+ geminiCliCache = null;
124
+ }
125
+ exports.clearGeminiCliCache = clearGeminiCliCache;
126
+ // Cache for Grok CLI status (per process)
127
+ let grokCliCache = null;
128
+ /**
129
+ * Check if Grok CLI is installed globally
130
+ *
131
+ * Grok CLI (grok-4-cli by lalomorales22) provides web search + X search.
132
+ * Requires: `npm install -g grok-cli` and XAI_API_KEY env var.
133
+ *
134
+ * @returns Grok CLI status with path and version
135
+ */
136
+ function getGrokCliStatus() {
137
+ // Return cached result if available
138
+ if (grokCliCache) {
139
+ return grokCliCache;
140
+ }
141
+ const result = {
142
+ installed: false,
143
+ path: null,
144
+ version: null,
145
+ };
146
+ try {
147
+ const isWindows = process.platform === 'win32';
148
+ const whichCmd = isWindows ? 'where grok' : 'which grok';
149
+ const pathResult = (0, child_process_1.execSync)(whichCmd, {
150
+ encoding: 'utf8',
151
+ timeout: 5000,
152
+ stdio: ['pipe', 'pipe', 'pipe'],
153
+ });
154
+ const grokPath = pathResult.trim().split('\n')[0]; // First result on Windows
155
+ if (grokPath) {
156
+ result.installed = true;
157
+ result.path = grokPath;
158
+ // Try to get version
159
+ try {
160
+ const versionResult = (0, child_process_1.execSync)('grok --version', {
161
+ encoding: 'utf8',
162
+ timeout: 5000,
163
+ stdio: ['pipe', 'pipe', 'pipe'],
164
+ });
165
+ result.version = versionResult.trim();
166
+ }
167
+ catch {
168
+ // Version check failed, but CLI is installed
169
+ result.version = 'unknown';
170
+ }
171
+ }
172
+ }
173
+ catch {
174
+ // Command not found - Grok CLI not installed
175
+ }
176
+ // Cache result
177
+ grokCliCache = result;
178
+ return result;
179
+ }
180
+ exports.getGrokCliStatus = getGrokCliStatus;
181
+ /**
182
+ * Check if Grok CLI is available (quick boolean check)
183
+ */
184
+ function hasGrokCli() {
185
+ return getGrokCliStatus().installed;
186
+ }
187
+ exports.hasGrokCli = hasGrokCli;
188
+ /**
189
+ * Clear Grok CLI cache (for testing or after installation)
190
+ */
191
+ function clearGrokCliCache() {
192
+ grokCliCache = null;
193
+ }
194
+ exports.clearGrokCliCache = clearGrokCliCache;
195
+ // Cache for OpenCode CLI status (per process)
196
+ let opencodeCliCache = null;
197
+ /**
198
+ * Check if OpenCode CLI is installed globally
199
+ *
200
+ * OpenCode provides built-in web search via opencode/grok-code model.
201
+ * Install: curl -fsSL https://opencode.ai/install | bash
202
+ *
203
+ * @returns OpenCode CLI status with path and version
204
+ */
205
+ function getOpenCodeCliStatus() {
206
+ // Return cached result if available
207
+ if (opencodeCliCache) {
208
+ return opencodeCliCache;
209
+ }
210
+ const result = {
211
+ installed: false,
212
+ path: null,
213
+ version: null,
214
+ };
215
+ try {
216
+ const isWindows = process.platform === 'win32';
217
+ const whichCmd = isWindows ? 'where opencode' : 'which opencode';
218
+ const pathResult = (0, child_process_1.execSync)(whichCmd, {
219
+ encoding: 'utf8',
220
+ timeout: 5000,
221
+ stdio: ['pipe', 'pipe', 'pipe'],
222
+ });
223
+ const opencodePath = pathResult.trim().split('\n')[0]; // First result on Windows
224
+ if (opencodePath) {
225
+ result.installed = true;
226
+ result.path = opencodePath;
227
+ // Try to get version
228
+ try {
229
+ const versionResult = (0, child_process_1.execSync)('opencode --version', {
230
+ encoding: 'utf8',
231
+ timeout: 5000,
232
+ stdio: ['pipe', 'pipe', 'pipe'],
233
+ });
234
+ result.version = versionResult.trim();
235
+ }
236
+ catch {
237
+ // Version check failed, but CLI is installed
238
+ result.version = 'unknown';
239
+ }
240
+ }
241
+ }
242
+ catch {
243
+ // Command not found - OpenCode CLI not installed
244
+ }
245
+ // Cache result
246
+ opencodeCliCache = result;
247
+ return result;
248
+ }
249
+ exports.getOpenCodeCliStatus = getOpenCodeCliStatus;
250
+ /**
251
+ * Check if OpenCode CLI is available (quick boolean check)
252
+ */
253
+ function hasOpenCodeCli() {
254
+ return getOpenCodeCliStatus().installed;
255
+ }
256
+ exports.hasOpenCodeCli = hasOpenCodeCli;
257
+ /**
258
+ * Clear OpenCode CLI cache (for testing or after installation)
259
+ */
260
+ function clearOpenCodeCliCache() {
261
+ opencodeCliCache = null;
262
+ }
263
+ exports.clearOpenCodeCliCache = clearOpenCodeCliCache;
264
+ /**
265
+ * Clear all CLI caches
266
+ */
267
+ function clearAllCliCaches() {
268
+ geminiCliCache = null;
269
+ grokCliCache = null;
270
+ opencodeCliCache = null;
271
+ }
272
+ exports.clearAllCliCaches = clearAllCliCaches;
273
+ /**
274
+ * Get all WebSearch CLI providers with their status
275
+ */
276
+ function getWebSearchCliProviders() {
277
+ const geminiStatus = getGeminiCliStatus();
278
+ const grokStatus = getGrokCliStatus();
279
+ const opencodeStatus = getOpenCodeCliStatus();
280
+ return [
281
+ {
282
+ id: 'gemini',
283
+ name: 'Gemini CLI',
284
+ command: 'gemini',
285
+ installed: geminiStatus.installed,
286
+ version: geminiStatus.version,
287
+ installCommand: 'npm install -g @google/gemini-cli',
288
+ docsUrl: 'https://github.com/google-gemini/gemini-cli',
289
+ requiresApiKey: false,
290
+ description: 'Google Gemini with web search (FREE tier: 1000 req/day)',
291
+ freeTier: true,
292
+ },
293
+ {
294
+ id: 'opencode',
295
+ name: 'OpenCode',
296
+ command: 'opencode',
297
+ installed: opencodeStatus.installed,
298
+ version: opencodeStatus.version,
299
+ installCommand: 'curl -fsSL https://opencode.ai/install | bash',
300
+ docsUrl: 'https://github.com/sst/opencode',
301
+ requiresApiKey: false,
302
+ description: 'OpenCode with built-in web search (FREE via Zen)',
303
+ freeTier: true,
304
+ },
305
+ {
306
+ id: 'grok',
307
+ name: 'Grok CLI',
308
+ command: 'grok',
309
+ installed: grokStatus.installed,
310
+ version: grokStatus.version,
311
+ installCommand: 'npm install -g @vibe-kit/grok-cli',
312
+ docsUrl: 'https://github.com/superagent-ai/grok-cli',
313
+ requiresApiKey: true,
314
+ apiKeyEnvVar: 'GROK_API_KEY',
315
+ description: 'xAI Grok CLI with AI coding agent capabilities',
316
+ freeTier: false,
317
+ },
318
+ ];
319
+ }
320
+ exports.getWebSearchCliProviders = getWebSearchCliProviders;
321
+ /**
322
+ * Check if any WebSearch CLI is available
323
+ */
324
+ function hasAnyWebSearchCli() {
325
+ return hasGeminiCli() || hasGrokCli() || hasOpenCodeCli();
326
+ }
327
+ exports.hasAnyWebSearchCli = hasAnyWebSearchCli;
328
+ /**
329
+ * Get install hints for CLI-only users when no WebSearch CLI is installed
330
+ */
331
+ function getCliInstallHints() {
332
+ if (hasAnyWebSearchCli()) {
333
+ return [];
334
+ }
335
+ return [
336
+ '[i] WebSearch: No CLI tools installed',
337
+ ' Gemini CLI (FREE): npm i -g @google/gemini-cli',
338
+ ' OpenCode (FREE): curl -fsSL https://opencode.ai/install | bash',
339
+ ' Grok CLI (paid): npm i -g @vibe-kit/grok-cli',
340
+ ];
341
+ }
342
+ exports.getCliInstallHints = getCliInstallHints;
343
+ // ========== Hook Management ==========
344
+ /**
345
+ * Install WebSearch hook to ~/.ccs/hooks/
346
+ *
347
+ * This hook intercepts WebSearch and executes via Gemini CLI.
348
+ *
349
+ * @returns true if hook installed successfully
350
+ */
351
+ function installWebSearchHook() {
352
+ try {
353
+ const wsConfig = (0, unified_config_loader_1.getWebSearchConfig)();
354
+ // Skip if disabled
355
+ if (!wsConfig.enabled) {
356
+ if (process.env.CCS_DEBUG) {
357
+ console.error((0, ui_1.info)('WebSearch disabled - skipping hook install'));
358
+ }
359
+ return false;
360
+ }
361
+ // Ensure hooks directory exists
362
+ if (!fs.existsSync(CCS_HOOKS_DIR)) {
363
+ fs.mkdirSync(CCS_HOOKS_DIR, { recursive: true, mode: 0o700 });
364
+ }
365
+ const hookPath = path.join(CCS_HOOKS_DIR, WEBSEARCH_HOOK);
366
+ // Find the bundled hook script
367
+ // In npm package: node_modules/ccs/lib/hooks/
368
+ // In development: lib/hooks/
369
+ const possiblePaths = [
370
+ path.join(__dirname, '..', '..', 'lib', 'hooks', WEBSEARCH_HOOK),
371
+ path.join(__dirname, '..', 'lib', 'hooks', WEBSEARCH_HOOK),
372
+ ];
373
+ let sourcePath = null;
374
+ for (const p of possiblePaths) {
375
+ if (fs.existsSync(p)) {
376
+ sourcePath = p;
377
+ break;
378
+ }
379
+ }
380
+ if (!sourcePath) {
381
+ if (process.env.CCS_DEBUG) {
382
+ console.error((0, ui_1.warn)(`WebSearch hook source not found: ${WEBSEARCH_HOOK}`));
383
+ }
384
+ return false;
385
+ }
386
+ // Copy hook to ~/.ccs/hooks/
387
+ fs.copyFileSync(sourcePath, hookPath);
388
+ fs.chmodSync(hookPath, 0o755);
389
+ if (process.env.CCS_DEBUG) {
390
+ console.error((0, ui_1.info)(`Installed WebSearch hook: ${hookPath}`));
391
+ }
392
+ // Ensure hook is configured in settings.json
393
+ ensureHookConfig();
394
+ return true;
395
+ }
396
+ catch (error) {
397
+ if (process.env.CCS_DEBUG) {
398
+ console.error((0, ui_1.warn)(`Failed to install WebSearch hook: ${error.message}`));
399
+ }
400
+ return false;
401
+ }
402
+ }
403
+ exports.installWebSearchHook = installWebSearchHook;
404
+ /**
405
+ * Check if WebSearch hook is installed
406
+ */
407
+ function hasWebSearchHook() {
408
+ const hookPath = path.join(CCS_HOOKS_DIR, WEBSEARCH_HOOK);
409
+ return fs.existsSync(hookPath);
410
+ }
411
+ exports.hasWebSearchHook = hasWebSearchHook;
412
+ /**
413
+ * Get WebSearch hook configuration for settings.json
414
+ * Timeout is computed from max provider timeout in config.yaml + buffer
415
+ */
416
+ function getWebSearchHookConfig() {
417
+ const hookPath = path.join(CCS_HOOKS_DIR, WEBSEARCH_HOOK);
418
+ const wsConfig = (0, unified_config_loader_1.getWebSearchConfig)();
419
+ // Compute max timeout from enabled providers
420
+ const timeouts = [];
421
+ if (wsConfig.providers?.gemini?.enabled && wsConfig.providers.gemini.timeout) {
422
+ timeouts.push(wsConfig.providers.gemini.timeout);
423
+ }
424
+ if (wsConfig.providers?.opencode?.enabled && wsConfig.providers.opencode.timeout) {
425
+ timeouts.push(wsConfig.providers.opencode.timeout);
426
+ }
427
+ if (wsConfig.providers?.grok?.enabled && wsConfig.providers.grok.timeout) {
428
+ timeouts.push(wsConfig.providers.grok.timeout);
429
+ }
430
+ // Hook timeout = max provider timeout + buffer (or minimum if none configured)
431
+ const maxProviderTimeout = timeouts.length > 0 ? Math.max(...timeouts) : MIN_HOOK_TIMEOUT;
432
+ const hookTimeout = maxProviderTimeout + HOOK_TIMEOUT_BUFFER;
433
+ return {
434
+ PreToolUse: [
435
+ {
436
+ matcher: 'WebSearch',
437
+ hooks: [
438
+ {
439
+ type: 'command',
440
+ command: `node "${hookPath}"`,
441
+ timeout: hookTimeout,
442
+ },
443
+ ],
444
+ },
445
+ ],
446
+ };
447
+ }
448
+ exports.getWebSearchHookConfig = getWebSearchHookConfig;
449
+ /**
450
+ * Ensure WebSearch hook is configured in ~/.claude/settings.json
451
+ */
452
+ function ensureHookConfig() {
453
+ try {
454
+ const wsConfig = (0, unified_config_loader_1.getWebSearchConfig)();
455
+ if (!wsConfig.enabled) {
456
+ return false;
457
+ }
458
+ // Read existing settings or start fresh
459
+ let settings = {};
460
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
461
+ try {
462
+ const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
463
+ settings = JSON.parse(content);
464
+ }
465
+ catch {
466
+ if (process.env.CCS_DEBUG) {
467
+ console.error((0, ui_1.warn)('Malformed settings.json - will merge carefully'));
468
+ }
469
+ }
470
+ }
471
+ // Check if WebSearch hook already configured
472
+ const hooks = settings.hooks;
473
+ const expectedHookPath = path.join(CCS_HOOKS_DIR, WEBSEARCH_HOOK);
474
+ const expectedCommand = `node "${expectedHookPath}"`;
475
+ if (hooks?.PreToolUse) {
476
+ const webSearchHookIndex = hooks.PreToolUse.findIndex((h) => {
477
+ const hook = h;
478
+ return hook.matcher === 'WebSearch';
479
+ });
480
+ if (webSearchHookIndex !== -1) {
481
+ // Hook exists - check if it needs updating (different command path or timeout)
482
+ const existingHook = hooks.PreToolUse[webSearchHookIndex];
483
+ const existingHooks = existingHook.hooks;
484
+ const currentHookConfig = getWebSearchHookConfig();
485
+ const expectedHooks = currentHookConfig.PreToolUse[0]
486
+ .hooks;
487
+ const expectedTimeout = expectedHooks[0].timeout;
488
+ let needsUpdate = false;
489
+ if (existingHooks?.[0]?.command !== expectedCommand) {
490
+ existingHooks[0].command = expectedCommand;
491
+ needsUpdate = true;
492
+ }
493
+ if (existingHooks?.[0]?.timeout !== expectedTimeout) {
494
+ existingHooks[0].timeout = expectedTimeout;
495
+ needsUpdate = true;
496
+ }
497
+ if (needsUpdate) {
498
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8');
499
+ if (process.env.CCS_DEBUG) {
500
+ console.error((0, ui_1.info)('Updated WebSearch hook config in settings.json'));
501
+ }
502
+ }
503
+ return true;
504
+ }
505
+ }
506
+ // Get hook config
507
+ const hookConfig = getWebSearchHookConfig();
508
+ // Merge hook config into settings
509
+ if (!settings.hooks) {
510
+ settings.hooks = {};
511
+ }
512
+ const settingsHooks = settings.hooks;
513
+ if (!settingsHooks.PreToolUse) {
514
+ settingsHooks.PreToolUse = [];
515
+ }
516
+ // Add our hook config
517
+ const preToolUseHooks = hookConfig.PreToolUse;
518
+ settingsHooks.PreToolUse.push(...preToolUseHooks);
519
+ // Ensure ~/.claude directory exists
520
+ const claudeDir = path.dirname(CLAUDE_SETTINGS_PATH);
521
+ if (!fs.existsSync(claudeDir)) {
522
+ fs.mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
523
+ }
524
+ // Write updated settings
525
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8');
526
+ if (process.env.CCS_DEBUG) {
527
+ console.error((0, ui_1.info)('Added WebSearch hook to settings.json'));
528
+ }
529
+ return true;
530
+ }
531
+ catch (error) {
532
+ if (process.env.CCS_DEBUG) {
533
+ console.error((0, ui_1.warn)(`Failed to configure WebSearch hook: ${error.message}`));
534
+ }
535
+ return false;
536
+ }
537
+ }
538
+ // ========== Environment Variables for Hook ==========
539
+ /**
540
+ * Get environment variables for WebSearch hook configuration.
541
+ *
542
+ * Simple env vars - hook reads these to control behavior.
543
+ *
544
+ * @returns Record of environment variables to set before spawning Claude
545
+ */
546
+ function getWebSearchHookEnv() {
547
+ const wsConfig = (0, unified_config_loader_1.getWebSearchConfig)();
548
+ const env = {};
549
+ // Skip hook entirely if disabled
550
+ if (!wsConfig.enabled) {
551
+ env.CCS_WEBSEARCH_SKIP = '1';
552
+ return env;
553
+ }
554
+ // Pass master switch
555
+ env.CCS_WEBSEARCH_ENABLED = '1';
556
+ // Pass individual provider enabled states
557
+ // Hook will only use providers that are BOTH enabled AND installed
558
+ if (wsConfig.providers?.gemini?.enabled) {
559
+ env.CCS_WEBSEARCH_GEMINI = '1';
560
+ if (wsConfig.providers.gemini.model) {
561
+ env.CCS_WEBSEARCH_GEMINI_MODEL = wsConfig.providers.gemini.model;
562
+ }
563
+ env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.gemini.timeout || 55);
564
+ }
565
+ if (wsConfig.providers?.opencode?.enabled) {
566
+ env.CCS_WEBSEARCH_OPENCODE = '1';
567
+ if (wsConfig.providers.opencode.model) {
568
+ env.CCS_WEBSEARCH_OPENCODE_MODEL = wsConfig.providers.opencode.model;
569
+ }
570
+ // Use opencode timeout if no gemini timeout set
571
+ if (!env.CCS_WEBSEARCH_TIMEOUT) {
572
+ env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.opencode.timeout || 90);
573
+ }
574
+ }
575
+ if (wsConfig.providers?.grok?.enabled) {
576
+ env.CCS_WEBSEARCH_GROK = '1';
577
+ // Use grok timeout if no other timeout set
578
+ if (!env.CCS_WEBSEARCH_TIMEOUT) {
579
+ env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.grok.timeout || 55);
580
+ }
581
+ }
582
+ // Default timeout if none set
583
+ if (!env.CCS_WEBSEARCH_TIMEOUT) {
584
+ env.CCS_WEBSEARCH_TIMEOUT = '55';
585
+ }
586
+ return env;
587
+ }
588
+ exports.getWebSearchHookEnv = getWebSearchHookEnv;
589
+ /**
590
+ * Get WebSearch readiness status for display
591
+ *
592
+ * Called on third-party profile startup to inform user.
593
+ */
594
+ function getWebSearchReadiness() {
595
+ const wsConfig = (0, unified_config_loader_1.getWebSearchConfig)();
596
+ // Check if WebSearch is disabled entirely
597
+ if (!wsConfig.enabled) {
598
+ return {
599
+ readiness: 'unavailable',
600
+ geminiCli: false,
601
+ grokCli: false,
602
+ opencodeCli: false,
603
+ message: 'Disabled in config',
604
+ };
605
+ }
606
+ // Check all CLIs
607
+ const geminiInstalled = hasGeminiCli();
608
+ const grokInstalled = hasGrokCli();
609
+ const opencodeInstalled = hasOpenCodeCli();
610
+ // Build message based on installed CLIs
611
+ const installedClis = [];
612
+ if (geminiInstalled)
613
+ installedClis.push('Gemini');
614
+ if (grokInstalled)
615
+ installedClis.push('Grok');
616
+ if (opencodeInstalled)
617
+ installedClis.push('OpenCode');
618
+ if (installedClis.length > 0) {
619
+ return {
620
+ readiness: 'ready',
621
+ geminiCli: geminiInstalled,
622
+ grokCli: grokInstalled,
623
+ opencodeCli: opencodeInstalled,
624
+ message: `Ready (${installedClis.join(' + ')})`,
625
+ };
626
+ }
627
+ return {
628
+ readiness: 'unavailable',
629
+ geminiCli: false,
630
+ grokCli: false,
631
+ opencodeCli: false,
632
+ message: 'Install: npm i -g @google/gemini-cli',
633
+ };
634
+ }
635
+ exports.getWebSearchReadiness = getWebSearchReadiness;
636
+ /**
637
+ * Display WebSearch status (single line, equilibrium UX)
638
+ *
639
+ * Only call for third-party profiles.
640
+ * Shows detailed install hints when no CLI is installed.
641
+ */
642
+ function displayWebSearchStatus() {
643
+ const status = getWebSearchReadiness();
644
+ switch (status.readiness) {
645
+ case 'ready':
646
+ console.error((0, ui_1.ok)(`WebSearch: ${status.message}`));
647
+ break;
648
+ case 'unavailable':
649
+ console.error((0, ui_1.fail)(`WebSearch: ${status.message}`));
650
+ // Show install hints for CLI-only users
651
+ const hints = getCliInstallHints();
652
+ if (hints.length > 0) {
653
+ for (const hint of hints) {
654
+ console.error((0, ui_1.info)(hint));
655
+ }
656
+ }
657
+ break;
658
+ }
659
+ }
660
+ exports.displayWebSearchStatus = displayWebSearchStatus;
661
+ // ========== Backward Compatibility Exports ==========
662
+ // These are kept for imports that haven't been updated yet
663
+ /**
664
+ * @deprecated Use installWebSearchHook instead - MCP is no longer used
665
+ */
666
+ function ensureMcpWebSearch() {
667
+ // No-op - MCP is no longer used
668
+ return false;
669
+ }
670
+ exports.ensureMcpWebSearch = ensureMcpWebSearch;
671
+ /**
672
+ * @deprecated MCP is no longer used
673
+ */
674
+ function hasMcpWebSearch() {
675
+ return false;
676
+ }
677
+ exports.hasMcpWebSearch = hasMcpWebSearch;
678
+ /**
679
+ * @deprecated MCP is no longer used
680
+ */
681
+ function getMcpConfigPath() {
682
+ return path.join(os.homedir(), '.claude', '.mcp.json');
683
+ }
684
+ exports.getMcpConfigPath = getMcpConfigPath;
685
+ //# sourceMappingURL=websearch-manager.js.map