feat(websearch): add Grok CLI support and improve install guidance

- Add Grok CLI detection (grok-4-cli by lalomorales22) alongside Gemini CLI
- Add WebSearch health check group to health-service.ts
- Update settings UI to show both Gemini and Grok CLI providers
- Add detailed install hints when no WebSearch CLI is installed
- Update API routes to return grokCli status
- Both CLI and dashboard users now see clear installation guidance

Providers:
- Gemini CLI: FREE tier (1000 req/day), no API key needed
- Grok CLI: Requires xAI API key (XAI_API_KEY), web + X search
This commit is contained in:
kaitranntt
2025-12-16 21:00:35 -05:00
parent 66c3edc7e9
commit c0938e1c82
12 changed files with 1309 additions and 1726 deletions
+54
View File
@@ -23,6 +23,7 @@ import { getPortProcess, isCLIProxyProcess } from '../utils/port-utils';
import packageJson from '../../package.json';
import { getEnvironmentDiagnostics } from '../management/environment-diagnostics';
import { checkAuthCodePorts } from '../management/oauth-port-diagnostics';
import { getWebSearchCliProviders, hasAnyWebSearchCli } from '../utils/websearch-manager';
export interface HealthCheck {
id: string;
@@ -136,6 +137,16 @@ export async function runHealthChecks(): Promise<HealthReport> {
checks: oauthReadinessChecks,
});
// Group 8: WebSearch CLI Providers
const websearchChecks: HealthCheck[] = [];
websearchChecks.push(...checkWebSearchClis());
groups.push({
id: 'websearch',
name: 'WebSearch',
icon: 'Search',
checks: websearchChecks,
});
// Flatten all checks for backward compatibility
const allChecks = groups.flatMap((g) => g.checks);
@@ -816,6 +827,49 @@ async function checkOAuthPortsForDashboard(): Promise<HealthCheck[]> {
});
}
// Check 18: WebSearch CLI Providers (Gemini CLI, Grok CLI)
function checkWebSearchClis(): HealthCheck[] {
const providers = getWebSearchCliProviders();
const checks: HealthCheck[] = [];
for (const provider of providers) {
if (provider.installed) {
const freeTag = provider.freeTier ? ' (FREE)' : '';
checks.push({
id: `websearch-${provider.id}`,
name: provider.name,
status: 'ok',
message: `v${provider.version || 'unknown'}${freeTag}`,
details: provider.description,
});
} else {
const keyNote = provider.requiresApiKey ? ` (needs ${provider.apiKeyEnvVar})` : ' (FREE)';
checks.push({
id: `websearch-${provider.id}`,
name: provider.name,
status: 'info',
message: `Not installed${keyNote}`,
fix: provider.installCommand,
details: provider.description,
});
}
}
// Add summary check if no providers installed
if (!hasAnyWebSearchCli()) {
checks.push({
id: 'websearch-summary',
name: 'WebSearch Status',
status: 'warning',
message: 'No CLI tools installed',
fix: 'npm install -g @google/gemini-cli (FREE)',
details: 'Install a WebSearch CLI for real-time web access',
});
}
return checks;
}
/**
* Fix a health issue by its check ID
*/
+30 -67
View File
@@ -60,9 +60,9 @@ import { isUnifiedConfig } from '../config/unified-config-types';
import { isSensitiveKey, maskSensitiveValue } from '../utils/sensitive-keys';
import {
getWebSearchReadiness,
getMcpWebSearchStatus,
getGeminiCliStatus,
} from '../utils/mcp-manager';
getGrokCliStatus,
} from '../utils/websearch-manager';
export const apiRoutes = Router();
@@ -1440,19 +1440,11 @@ apiRoutes.get('/websearch', (_req: Request, res: Response): void => {
/**
* PUT /api/websearch - Update WebSearch configuration
* Body: WebSearchConfig fields (enabled, provider, fallback, gemini, mode, selectedProviders, customMcp)
* Body: WebSearchConfig fields (enabled, providers)
* Dashboard is the source of truth for provider selection.
*/
apiRoutes.put('/websearch', (req: Request, res: Response): void => {
const {
enabled,
provider,
fallback,
webSearchPrimeUrl,
gemini,
mode,
selectedProviders,
customMcp,
} = req.body as Partial<WebSearchConfig>;
const { enabled, providers } = req.body as Partial<WebSearchConfig>;
// Validate enabled
if (enabled !== undefined && typeof enabled !== 'boolean') {
@@ -1460,45 +1452,9 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => {
return;
}
// Validate fallback
if (fallback !== undefined && typeof fallback !== 'boolean') {
res.status(400).json({ error: 'Invalid value for fallback. Must be a boolean.' });
return;
}
// Validate provider if specified
const validProviders = ['auto', 'web-search-prime', 'brave', 'tavily'];
if (provider && !validProviders.includes(provider)) {
res.status(400).json({
error: `Invalid provider. Must be one of: ${validProviders.join(', ')}`,
});
return;
}
// Validate webSearchPrimeUrl if specified
if (webSearchPrimeUrl !== undefined && typeof webSearchPrimeUrl !== 'string') {
res.status(400).json({ error: 'Invalid value for webSearchPrimeUrl. Must be a string.' });
return;
}
// Validate mode if specified
const validModes = ['sequential', 'parallel'];
if (mode && !validModes.includes(mode)) {
res.status(400).json({
error: `Invalid mode. Must be one of: ${validModes.join(', ')}`,
});
return;
}
// Validate selectedProviders if specified
if (selectedProviders !== undefined && !Array.isArray(selectedProviders)) {
res.status(400).json({ error: 'Invalid value for selectedProviders. Must be an array.' });
return;
}
// Validate customMcp if specified
if (customMcp !== undefined && !Array.isArray(customMcp)) {
res.status(400).json({ error: 'Invalid value for customMcp. Must be an array.' });
// Validate providers if specified
if (providers !== undefined && typeof providers !== 'object') {
res.status(400).json({ error: 'Invalid value for providers. Must be an object.' });
return;
}
@@ -1510,16 +1466,23 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => {
return;
}
// Merge updates - preserve all existing fields
// Merge updates - simple structure (Gemini CLI only for now)
existingConfig.websearch = {
enabled: enabled ?? existingConfig.websearch?.enabled ?? true,
provider: provider ?? existingConfig.websearch?.provider ?? 'auto',
fallback: fallback ?? existingConfig.websearch?.fallback ?? true,
webSearchPrimeUrl: webSearchPrimeUrl ?? existingConfig.websearch?.webSearchPrimeUrl,
gemini: gemini ?? existingConfig.websearch?.gemini ?? { enabled: true, timeout: 55 },
mode: mode ?? existingConfig.websearch?.mode ?? 'sequential',
selectedProviders: selectedProviders ?? existingConfig.websearch?.selectedProviders ?? [],
customMcp: customMcp ?? existingConfig.websearch?.customMcp ?? [],
providers: providers
? {
gemini: {
enabled:
providers.gemini?.enabled ??
existingConfig.websearch?.providers?.gemini?.enabled ??
true,
timeout:
providers.gemini?.timeout ??
existingConfig.websearch?.providers?.gemini?.timeout ??
55,
},
}
: existingConfig.websearch?.providers,
};
saveUnifiedConfig(existingConfig);
@@ -1534,13 +1497,13 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => {
});
/**
* GET /api/websearch/status - Get comprehensive WebSearch status
* Returns: { geminiCli, mcpServers, readiness }
* GET /api/websearch/status - Get WebSearch status
* Returns: { geminiCli, grokCli, readiness }
*/
apiRoutes.get('/websearch/status', (_req: Request, res: Response): void => {
try {
const geminiCli = getGeminiCliStatus();
const mcpStatus = getMcpWebSearchStatus();
const grokCli = getGrokCliStatus();
const readiness = getWebSearchReadiness();
res.json({
@@ -1549,10 +1512,10 @@ apiRoutes.get('/websearch/status', (_req: Request, res: Response): void => {
path: geminiCli.path,
version: geminiCli.version,
},
mcpServers: {
configured: mcpStatus.configured,
ccsManaged: mcpStatus.ccsManaged,
userAdded: mcpStatus.userAdded,
grokCli: {
installed: grokCli.installed,
path: grokCli.path,
version: grokCli.version,
},
readiness: {
status: readiness.readiness,