From f7a1a40b42a17e18a69c241614cf3f5853deace3 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 16 Dec 2025 02:49:07 -0500 Subject: [PATCH] feat(websearch): enhance Gemini CLI integration, package manager detection, and WebSearch status - Improve Gemini CLI integration in websearch hook: use gemini-2.5-flash & --yolo. - Update `dev-install.sh` for better package manager (npm/bun) auto-detection. - Introduce `displayWebSearchStatus` for improved user experience. - Refactor `mcp-manager.ts` to track CCS-managed MCP servers. --- lib/hooks/websearch-gemini-transformer.cjs | 35 ++-- scripts/dev-install.sh | 59 +++++- src/ccs.ts | 9 +- src/cliproxy/cliproxy-executor.ts | 9 +- src/utils/mcp-manager.ts | 233 ++++++++++++++++++++- src/web-server/routes.ts | 36 ++++ ui/src/pages/settings.tsx | 123 ++++++++++- 7 files changed, 482 insertions(+), 22 deletions(-) diff --git a/lib/hooks/websearch-gemini-transformer.cjs b/lib/hooks/websearch-gemini-transformer.cjs index 06fd43e3..3b2756f6 100644 --- a/lib/hooks/websearch-gemini-transformer.cjs +++ b/lib/hooks/websearch-gemini-transformer.cjs @@ -99,7 +99,9 @@ function processHook() { const geminiResult = tryGeminiSearch(query); if (geminiResult.success) { - // Success! Return results via deny with reason + // Success! Use deny with clear success messaging + // Note: "deny" prevents native WebSearch from running (which would fail anyway) + // The permissionDecisionReason contains the actual search results const output = { hookSpecificOutput: { hookEventName: 'PreToolUse', @@ -143,12 +145,18 @@ function tryGeminiSearch(query) { const prompt = buildGeminiPrompt(query); if (process.env.CCS_DEBUG) { - console.error(`[CCS Hook] Executing: gemini -p "${prompt.substring(0, 100)}..."`); + console.error(`[CCS Hook] Executing: gemini --model gemini-2.5-flash --yolo -p "..."`); } - // Execute gemini CLI using spawnSync to avoid shell injection - // Note: gemini CLI uses OAuth auth, no API key needed - const spawnResult = spawnSync('gemini', ['-p', prompt], { + // Execute gemini CLI with required flags: + // --model gemini-2.5-flash: Use the flash model for fast responses + // --yolo: Skip confirmation prompts for tool use + // -p: Provide prompt + const spawnResult = spawnSync('gemini', [ + '--model', 'gemini-2.5-flash', + '--yolo', + '-p', prompt + ], { encoding: 'utf8', timeout: timeoutMs, maxBuffer: 1024 * 1024 * 2, // 2MB buffer for large responses @@ -209,7 +217,7 @@ function tryGeminiSearch(query) { if (err.code === 'ENOENT') { return { success: false, - error: 'Gemini CLI not found (not installed or not in PATH)', + error: 'Gemini CLI not installed. Install with: npm install -g @google/gemini-cli', }; } @@ -257,18 +265,15 @@ function buildGeminiPrompt(query) { */ function formatGeminiResults(query, content) { return [ - '[WebSearch Results via Gemini]', + '=== WEBSEARCH COMPLETED SUCCESSFULLY ===', + '(via Gemini CLI - this is NOT an error)', '', `Query: "${query}"`, '', - '---', - '', content, '', - '---', - '', - 'Search completed successfully. You can use this information to answer the user\'s question.', - 'If you need more specific information, you can search again with a refined query.', + '=========================================', + 'Use this information to answer the user. Search again if needed.', ].join('\n'); } @@ -285,6 +290,10 @@ function getMcpFallbackMessage(query, error) { '', `Gemini CLI failed: ${error || 'Unknown error'}`, '', + 'To enable Gemini CLI WebSearch:', + ' 1. Install: npm install -g @google/gemini-cli', + ' 2. Authenticate: gemini auth', + '', 'The native WebSearch tool is not available with your current provider.', 'Please use one of the following MCP tools instead:', '', diff --git a/scripts/dev-install.sh b/scripts/dev-install.sh index 6c4f3c5b..41d9f38f 100755 --- a/scripts/dev-install.sh +++ b/scripts/dev-install.sh @@ -4,13 +4,20 @@ # # Options: # --skip-validate Skip validation (faster, use when you're sure code is good) +# --npm Force npm install (default: auto-detect, fallback to bun) +# --bun Force bun install set -e SKIP_VALIDATE=false +FORCE_NPM=false +FORCE_BUN=false + for arg in "$@"; do case $arg in --skip-validate) SKIP_VALIDATE=true ;; + --npm) FORCE_NPM=true ;; + --bun) FORCE_BUN=true ;; esac done @@ -19,6 +26,42 @@ echo "[i] CCS Dev Install - Starting..." # Get to the right directory cd "$(dirname "$0")/.." +# Detect installation method +# Priority: CLI flags > existing global install location > bun (default) +detect_pkg_manager() { + if [ "$FORCE_NPM" = true ]; then + echo "npm" + return + fi + + if [ "$FORCE_BUN" = true ]; then + echo "bun" + return + fi + + # Check existing ccs installation location + CCS_PATH=$(which ccs 2>/dev/null || true) + + if [ -n "$CCS_PATH" ]; then + # Check if installed via bun + if [[ "$CCS_PATH" == *".bun"* ]]; then + echo "bun" + return + fi + # Check if installed via npm + if [[ "$CCS_PATH" == *"npm"* ]] || [[ "$CCS_PATH" == *"node_modules"* ]]; then + echo "npm" + return + fi + fi + + # Default fallback: bun (preferred) + echo "bun" +} + +PKG_MANAGER=$(detect_pkg_manager) +echo "[i] Detected package manager: $PKG_MANAGER" + # Build TypeScript first echo "[i] Building TypeScript..." bun run build @@ -27,10 +70,10 @@ bun run build echo "[i] Creating package..." if [ "$SKIP_VALIDATE" = true ]; then # Skip validation, just pack - bun pm pack --ignore-scripts + npm pack --ignore-scripts 2>/dev/null || bun pm pack --ignore-scripts else # Full pack with validation (runs prepublishOnly) - bun pm pack + npm pack 2>/dev/null || bun pm pack fi # Find the tarball @@ -43,9 +86,15 @@ fi echo "[i] Found tarball: $TARBALL" -# Install globally using npm (handles bin linking correctly) -echo "[i] Installing globally with npm..." -npm install -g "$TARBALL" +# Install globally using detected package manager +echo "[i] Installing globally with $PKG_MANAGER..." + +if [ "$PKG_MANAGER" = "bun" ]; then + # Bun requires file: protocol for local tarballs + bun add -g "file:$(pwd)/$TARBALL" +else + npm install -g "$TARBALL" +fi # Clean up echo "[i] Cleaning up..." diff --git a/src/ccs.ts b/src/ccs.ts index af79374a..176e845c 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -14,7 +14,11 @@ import { detectClaudeCli } from './utils/claude-detector'; import { getSettingsPath } from './utils/config-manager'; import { ErrorManager } from './utils/error-manager'; import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy'; -import { ensureMcpWebSearch, installWebSearchHook } from './utils/mcp-manager'; +import { + ensureMcpWebSearch, + installWebSearchHook, + displayWebSearchStatus, +} from './utils/mcp-manager'; // Import extracted command handlers import { handleVersionCommand } from './commands/version-command'; @@ -384,6 +388,9 @@ async function main(): Promise { ensureMcpWebSearch(); installWebSearchHook(); + // Display WebSearch status (single line, equilibrium UX) + displayWebSearchStatus(); + // Check if this is GLMT profile (requires proxy) if (profileInfo.name === 'glmt') { // GLMT FLOW: Settings-based with embedded proxy for thinking support diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index d81a1d19..0ef7ff65 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -39,7 +39,11 @@ import { } from './account-manager'; import { getPortCheckCommand, getCatCommand, killProcessOnPort } from '../utils/platform-commands'; import { getPortProcess, isCLIProxyProcess } from '../utils/port-utils'; -import { ensureMcpWebSearch, installWebSearchHook } from '../utils/mcp-manager'; +import { + ensureMcpWebSearch, + installWebSearchHook, + displayWebSearchStatus, +} from '../utils/mcp-manager'; /** Default executor configuration */ const DEFAULT_CONFIG: ExecutorConfig = { @@ -122,6 +126,9 @@ export async function execClaudeWithCLIProxy( // Hook intercepts WebSearch, tries Gemini CLI first, falls back to MCP installWebSearchHook(); + // Display WebSearch status (single line, equilibrium UX) + displayWebSearchStatus(); + // Validate provider const providerConfig = getProviderConfig(provider); log(`Provider: ${providerConfig.displayName}`); diff --git a/src/utils/mcp-manager.ts b/src/utils/mcp-manager.ts index 855d1686..25c14071 100644 --- a/src/utils/mcp-manager.ts +++ b/src/utils/mcp-manager.ts @@ -14,7 +14,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { info, warn } from './ui'; +import { ok, info, warn, fail } from './ui'; import { getWebSearchConfig } from '../config/unified-config-loader'; // MCP configuration file path @@ -30,6 +30,7 @@ interface McpServerConfig { command?: string; args?: string[]; env?: Record; + _managedBy?: 'ccs'; // CCS-managed marker (Option C hybrid) } /** @@ -48,6 +49,7 @@ const WEB_SEARCH_PRIME_CONFIG: McpServerConfig = { type: 'http', url: 'https://api.z.ai/api/mcp/web_search_prime/mcp', headers: {}, + _managedBy: 'ccs', }; /** @@ -61,6 +63,7 @@ const BRAVE_SEARCH_CONFIG: McpServerConfig = { command: 'npx', args: ['-y', '@modelcontextprotocol/server-brave-search'], env: {}, + _managedBy: 'ccs', }; /** @@ -74,6 +77,7 @@ const TAVILY_CONFIG: McpServerConfig = { command: 'npx', args: ['-y', '@tavily/mcp-server'], env: {}, + _managedBy: 'ccs', }; /** @@ -571,3 +575,230 @@ function ensureWebSearchHookConfigInternal(): boolean { return false; } } + +// ========== Phase 1: MCP Status Functions ========== + +/** + * MCP web search status with ownership tracking + */ +export interface McpWebSearchStatus { + configured: boolean; + ccsManaged: string[]; // CCS-managed server names + userAdded: string[]; // User-added server names +} + +/** + * Get comprehensive MCP web search status + * + * Distinguishes CCS-managed vs user-added MCP servers + * based on _managedBy marker. + */ +export function getMcpWebSearchStatus(): McpWebSearchStatus { + const result: McpWebSearchStatus = { + configured: false, + ccsManaged: [], + userAdded: [], + }; + + try { + if (!fs.existsSync(MCP_CONFIG_PATH)) return result; + + const content = fs.readFileSync(MCP_CONFIG_PATH, 'utf8'); + const config: McpConfig = JSON.parse(content); + + if (!config.mcpServers) return result; + + for (const [name, server] of Object.entries(config.mcpServers)) { + // Check if it's a web search server + const lowerName = name.toLowerCase(); + const isWebSearch = + lowerName.includes('web-search') || + lowerName.includes('websearch') || + lowerName.includes('tavily') || + lowerName.includes('brave'); + + if (!isWebSearch) continue; + + if (server._managedBy === 'ccs') { + result.ccsManaged.push(name); + } else { + result.userAdded.push(name); + } + } + + result.configured = result.ccsManaged.length > 0 || result.userAdded.length > 0; + return result; + } catch { + return result; + } +} + +/** + * Check if CCS-managed web search MCP is configured + */ +export function hasCcsManagedWebSearch(): boolean { + const status = getMcpWebSearchStatus(); + return status.ccsManaged.length > 0; +} + +// ========== Phase 2: Gemini CLI Detection ========== + +import { execSync } from 'child_process'; + +/** + * Gemini CLI installation status + */ +export interface GeminiCliStatus { + installed: boolean; + path: string | null; + version: string | null; +} + +// Cache for Gemini CLI status (per process) +let geminiCliCache: GeminiCliStatus | null = null; + +/** + * Check if Gemini CLI is installed globally + * + * Requires global install: `npm install -g @google/gemini-cli` + * No npx fallback - must be in PATH + * + * @returns Gemini CLI status with path and version + */ +export function getGeminiCliStatus(): GeminiCliStatus { + // Return cached result if available + if (geminiCliCache) { + return geminiCliCache; + } + + const result: GeminiCliStatus = { + installed: false, + path: null, + version: null, + }; + + try { + const isWindows = process.platform === 'win32'; + const whichCmd = isWindows ? 'where gemini' : 'which gemini'; + + const pathResult = execSync(whichCmd, { + encoding: 'utf8', + timeout: 5000, + stdio: ['pipe', 'pipe', 'pipe'], + }); + + const geminiPath = pathResult.trim().split('\n')[0]; // First result on Windows + + if (geminiPath) { + result.installed = true; + result.path = geminiPath; + + // Try to get version + try { + const versionResult = execSync('gemini --version', { + encoding: 'utf8', + timeout: 5000, + stdio: ['pipe', 'pipe', 'pipe'], + }); + result.version = versionResult.trim(); + } catch { + // Version check failed, but CLI is installed + result.version = 'unknown'; + } + } + } catch { + // Command not found - Gemini CLI not installed + } + + // Cache result + geminiCliCache = result; + return result; +} + +/** + * Check if Gemini CLI is available (quick boolean check) + */ +export function hasGeminiCli(): boolean { + return getGeminiCliStatus().installed; +} + +/** + * Clear Gemini CLI cache (for testing or after installation) + */ +export function clearGeminiCliCache(): void { + geminiCliCache = null; +} + +// ========== Phase 3: WebSearch Readiness Status ========== + +/** + * WebSearch availability status for third-party profiles + */ +export type WebSearchReadiness = 'ready' | 'mcp-only' | 'unavailable'; + +/** + * WebSearch status for display + */ +export interface WebSearchStatus { + readiness: WebSearchReadiness; + geminiCli: boolean; + mcpConfigured: boolean; + message: string; +} + +/** + * Get WebSearch readiness status for display + * + * Called on third-party profile startup to inform user. + */ +export function getWebSearchReadiness(): WebSearchStatus { + const geminiCli = hasGeminiCli(); + const mcpStatus = getMcpWebSearchStatus(); + const mcpConfigured = mcpStatus.configured; + + if (geminiCli) { + return { + readiness: 'ready', + geminiCli: true, + mcpConfigured, + message: 'Ready (Gemini CLI)', + }; + } + + if (mcpConfigured) { + return { + readiness: 'mcp-only', + geminiCli: false, + mcpConfigured: true, + message: 'MCP fallback only', + }; + } + + return { + readiness: 'unavailable', + geminiCli: false, + mcpConfigured: false, + message: 'Unavailable (run: ccs config)', + }; +} + +/** + * Display WebSearch status (single line, equilibrium UX) + * + * Only call for third-party profiles. + */ +export function displayWebSearchStatus(): void { + const status = getWebSearchReadiness(); + + switch (status.readiness) { + case 'ready': + console.error(ok(`WebSearch: ${status.message}`)); + break; + case 'mcp-only': + console.error(info(`WebSearch: ${status.message}`)); + break; + case 'unavailable': + console.error(fail(`WebSearch: ${status.message}`)); + break; + } +} diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index df30f5cf..708132b7 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -57,6 +57,11 @@ import { getWebSearchConfig } from '../config/unified-config-loader'; import type { WebSearchConfig } from '../config/unified-config-types'; import { isUnifiedConfig } from '../config/unified-config-types'; import { isSensitiveKey, maskSensitiveValue } from '../utils/sensitive-keys'; +import { + getWebSearchReadiness, + getMcpWebSearchStatus, + getGeminiCliStatus, +} from '../utils/mcp-manager'; export const apiRoutes = Router(); @@ -1475,3 +1480,34 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => { res.status(500).json({ error: (error as Error).message }); } }); + +/** + * GET /api/websearch/status - Get comprehensive WebSearch status + * Returns: { geminiCli, mcpServers, readiness } + */ +apiRoutes.get('/websearch/status', (_req: Request, res: Response): void => { + try { + const geminiCli = getGeminiCliStatus(); + const mcpStatus = getMcpWebSearchStatus(); + const readiness = getWebSearchReadiness(); + + res.json({ + geminiCli: { + installed: geminiCli.installed, + path: geminiCli.path, + version: geminiCli.version, + }, + mcpServers: { + configured: mcpStatus.configured, + ccsManaged: mcpStatus.ccsManaged, + userAdded: mcpStatus.userAdded, + }, + readiness: { + status: readiness.readiness, + message: readiness.message, + }, + }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); diff --git a/ui/src/pages/settings.tsx b/ui/src/pages/settings.tsx index 92a36d16..5e5d3d23 100644 --- a/ui/src/pages/settings.tsx +++ b/ui/src/pages/settings.tsx @@ -23,16 +23,36 @@ interface WebSearchConfig { fallback: boolean; } +interface WebSearchStatus { + geminiCli: { + installed: boolean; + path: string | null; + version: string | null; + }; + mcpServers: { + configured: boolean; + ccsManaged: string[]; + userAdded: string[]; + }; + readiness: { + status: 'ready' | 'mcp-only' | 'unavailable'; + message: string; + }; +} + export function SettingsPage() { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); + const [status, setStatus] = useState(null); + const [statusLoading, setStatusLoading] = useState(true); - // Load config on mount + // Load config and status on mount useEffect(() => { fetchConfig(); + fetchStatus(); }, []); const fetchConfig = async () => { @@ -50,6 +70,20 @@ export function SettingsPage() { } }; + const fetchStatus = async () => { + try { + setStatusLoading(true); + const res = await fetch('/api/websearch/status'); + if (!res.ok) throw new Error('Failed to load status'); + const data = await res.json(); + setStatus(data); + } catch (err) { + console.error('Failed to fetch WebSearch status:', err); + } finally { + setStatusLoading(false); + } + }; + const saveConfig = async (updates: Partial) => { if (!config) return; @@ -131,6 +165,93 @@ export function SettingsPage() { + {/* WebSearch Status Panel */} +
+

+ Status + +

+ + {statusLoading ? ( +
+ + Checking status... +
+ ) : status ? ( +
+ {/* Overall Readiness */} +
+ {status.readiness.status === 'ready' && ( + + )} + {status.readiness.status === 'mcp-only' && ( + + )} + {status.readiness.status === 'unavailable' && ( + + )} + {status.readiness.message} +
+ + {/* Gemini CLI Status */} +
+
+
+

Gemini CLI

+ {status.geminiCli.installed ? ( +

+ Installed {status.geminiCli.version && `(${status.geminiCli.version})`} +

+ ) : ( +
+

Not installed

+ + npm install -g @google/gemini-cli + +
+ )} +
+
+ + {/* MCP Servers */} +
+

MCP Servers

+ {status.mcpServers.ccsManaged.length === 0 && + status.mcpServers.userAdded.length === 0 ? ( +

No web search MCP configured

+ ) : ( +
+ {status.mcpServers.ccsManaged.map((name) => ( +
+ + Managed by CCS + + {name} +
+ ))} + {status.mcpServers.userAdded.map((name) => ( +
+ + User-added + + {name} +
+ ))} +
+ )} +
+
+ ) : ( +

Status unavailable

+ )} +
+
{/* Enable/Disable */}