mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 02:16:43 +00:00
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:
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user