mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-04 10:16:34 +00:00
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.
This commit is contained in:
@@ -99,7 +99,9 @@ function processHook() {
|
|||||||
const geminiResult = tryGeminiSearch(query);
|
const geminiResult = tryGeminiSearch(query);
|
||||||
|
|
||||||
if (geminiResult.success) {
|
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 = {
|
const output = {
|
||||||
hookSpecificOutput: {
|
hookSpecificOutput: {
|
||||||
hookEventName: 'PreToolUse',
|
hookEventName: 'PreToolUse',
|
||||||
@@ -143,12 +145,18 @@ function tryGeminiSearch(query) {
|
|||||||
const prompt = buildGeminiPrompt(query);
|
const prompt = buildGeminiPrompt(query);
|
||||||
|
|
||||||
if (process.env.CCS_DEBUG) {
|
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
|
// Execute gemini CLI with required flags:
|
||||||
// Note: gemini CLI uses OAuth auth, no API key needed
|
// --model gemini-2.5-flash: Use the flash model for fast responses
|
||||||
const spawnResult = spawnSync('gemini', ['-p', prompt], {
|
// --yolo: Skip confirmation prompts for tool use
|
||||||
|
// -p: Provide prompt
|
||||||
|
const spawnResult = spawnSync('gemini', [
|
||||||
|
'--model', 'gemini-2.5-flash',
|
||||||
|
'--yolo',
|
||||||
|
'-p', prompt
|
||||||
|
], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
maxBuffer: 1024 * 1024 * 2, // 2MB buffer for large responses
|
maxBuffer: 1024 * 1024 * 2, // 2MB buffer for large responses
|
||||||
@@ -209,7 +217,7 @@ function tryGeminiSearch(query) {
|
|||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
return {
|
return {
|
||||||
success: false,
|
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) {
|
function formatGeminiResults(query, content) {
|
||||||
return [
|
return [
|
||||||
'[WebSearch Results via Gemini]',
|
'=== WEBSEARCH COMPLETED SUCCESSFULLY ===',
|
||||||
|
'(via Gemini CLI - this is NOT an error)',
|
||||||
'',
|
'',
|
||||||
`Query: "${query}"`,
|
`Query: "${query}"`,
|
||||||
'',
|
'',
|
||||||
'---',
|
|
||||||
'',
|
|
||||||
content,
|
content,
|
||||||
'',
|
'',
|
||||||
'---',
|
'=========================================',
|
||||||
'',
|
'Use this information to answer the user. Search again if needed.',
|
||||||
'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.',
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +290,10 @@ function getMcpFallbackMessage(query, error) {
|
|||||||
'',
|
'',
|
||||||
`Gemini CLI failed: ${error || 'Unknown 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.',
|
'The native WebSearch tool is not available with your current provider.',
|
||||||
'Please use one of the following MCP tools instead:',
|
'Please use one of the following MCP tools instead:',
|
||||||
'',
|
'',
|
||||||
|
|||||||
+54
-5
@@ -4,13 +4,20 @@
|
|||||||
#
|
#
|
||||||
# Options:
|
# Options:
|
||||||
# --skip-validate Skip validation (faster, use when you're sure code is good)
|
# --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
|
set -e
|
||||||
|
|
||||||
SKIP_VALIDATE=false
|
SKIP_VALIDATE=false
|
||||||
|
FORCE_NPM=false
|
||||||
|
FORCE_BUN=false
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case $arg in
|
case $arg in
|
||||||
--skip-validate) SKIP_VALIDATE=true ;;
|
--skip-validate) SKIP_VALIDATE=true ;;
|
||||||
|
--npm) FORCE_NPM=true ;;
|
||||||
|
--bun) FORCE_BUN=true ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -19,6 +26,42 @@ echo "[i] CCS Dev Install - Starting..."
|
|||||||
# Get to the right directory
|
# Get to the right directory
|
||||||
cd "$(dirname "$0")/.."
|
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
|
# Build TypeScript first
|
||||||
echo "[i] Building TypeScript..."
|
echo "[i] Building TypeScript..."
|
||||||
bun run build
|
bun run build
|
||||||
@@ -27,10 +70,10 @@ bun run build
|
|||||||
echo "[i] Creating package..."
|
echo "[i] Creating package..."
|
||||||
if [ "$SKIP_VALIDATE" = true ]; then
|
if [ "$SKIP_VALIDATE" = true ]; then
|
||||||
# Skip validation, just pack
|
# Skip validation, just pack
|
||||||
bun pm pack --ignore-scripts
|
npm pack --ignore-scripts 2>/dev/null || bun pm pack --ignore-scripts
|
||||||
else
|
else
|
||||||
# Full pack with validation (runs prepublishOnly)
|
# Full pack with validation (runs prepublishOnly)
|
||||||
bun pm pack
|
npm pack 2>/dev/null || bun pm pack
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Find the tarball
|
# Find the tarball
|
||||||
@@ -43,9 +86,15 @@ fi
|
|||||||
|
|
||||||
echo "[i] Found tarball: $TARBALL"
|
echo "[i] Found tarball: $TARBALL"
|
||||||
|
|
||||||
# Install globally using npm (handles bin linking correctly)
|
# Install globally using detected package manager
|
||||||
echo "[i] Installing globally with npm..."
|
echo "[i] Installing globally with $PKG_MANAGER..."
|
||||||
npm install -g "$TARBALL"
|
|
||||||
|
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
|
# Clean up
|
||||||
echo "[i] Cleaning up..."
|
echo "[i] Cleaning up..."
|
||||||
|
|||||||
+8
-1
@@ -14,7 +14,11 @@ import { detectClaudeCli } from './utils/claude-detector';
|
|||||||
import { getSettingsPath } from './utils/config-manager';
|
import { getSettingsPath } from './utils/config-manager';
|
||||||
import { ErrorManager } from './utils/error-manager';
|
import { ErrorManager } from './utils/error-manager';
|
||||||
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
|
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 extracted command handlers
|
||||||
import { handleVersionCommand } from './commands/version-command';
|
import { handleVersionCommand } from './commands/version-command';
|
||||||
@@ -384,6 +388,9 @@ async function main(): Promise<void> {
|
|||||||
ensureMcpWebSearch();
|
ensureMcpWebSearch();
|
||||||
installWebSearchHook();
|
installWebSearchHook();
|
||||||
|
|
||||||
|
// Display WebSearch status (single line, equilibrium UX)
|
||||||
|
displayWebSearchStatus();
|
||||||
|
|
||||||
// Check if this is GLMT profile (requires proxy)
|
// Check if this is GLMT profile (requires proxy)
|
||||||
if (profileInfo.name === 'glmt') {
|
if (profileInfo.name === 'glmt') {
|
||||||
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ import {
|
|||||||
} from './account-manager';
|
} from './account-manager';
|
||||||
import { getPortCheckCommand, getCatCommand, killProcessOnPort } from '../utils/platform-commands';
|
import { getPortCheckCommand, getCatCommand, killProcessOnPort } from '../utils/platform-commands';
|
||||||
import { getPortProcess, isCLIProxyProcess } from '../utils/port-utils';
|
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 */
|
/** Default executor configuration */
|
||||||
const DEFAULT_CONFIG: ExecutorConfig = {
|
const DEFAULT_CONFIG: ExecutorConfig = {
|
||||||
@@ -122,6 +126,9 @@ export async function execClaudeWithCLIProxy(
|
|||||||
// Hook intercepts WebSearch, tries Gemini CLI first, falls back to MCP
|
// Hook intercepts WebSearch, tries Gemini CLI first, falls back to MCP
|
||||||
installWebSearchHook();
|
installWebSearchHook();
|
||||||
|
|
||||||
|
// Display WebSearch status (single line, equilibrium UX)
|
||||||
|
displayWebSearchStatus();
|
||||||
|
|
||||||
// Validate provider
|
// Validate provider
|
||||||
const providerConfig = getProviderConfig(provider);
|
const providerConfig = getProviderConfig(provider);
|
||||||
log(`Provider: ${providerConfig.displayName}`);
|
log(`Provider: ${providerConfig.displayName}`);
|
||||||
|
|||||||
+232
-1
@@ -14,7 +14,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { info, warn } from './ui';
|
import { ok, info, warn, fail } from './ui';
|
||||||
import { getWebSearchConfig } from '../config/unified-config-loader';
|
import { getWebSearchConfig } from '../config/unified-config-loader';
|
||||||
|
|
||||||
// MCP configuration file path
|
// MCP configuration file path
|
||||||
@@ -30,6 +30,7 @@ interface McpServerConfig {
|
|||||||
command?: string;
|
command?: string;
|
||||||
args?: string[];
|
args?: string[];
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
|
_managedBy?: 'ccs'; // CCS-managed marker (Option C hybrid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +49,7 @@ const WEB_SEARCH_PRIME_CONFIG: McpServerConfig = {
|
|||||||
type: 'http',
|
type: 'http',
|
||||||
url: 'https://api.z.ai/api/mcp/web_search_prime/mcp',
|
url: 'https://api.z.ai/api/mcp/web_search_prime/mcp',
|
||||||
headers: {},
|
headers: {},
|
||||||
|
_managedBy: 'ccs',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,6 +63,7 @@ const BRAVE_SEARCH_CONFIG: McpServerConfig = {
|
|||||||
command: 'npx',
|
command: 'npx',
|
||||||
args: ['-y', '@modelcontextprotocol/server-brave-search'],
|
args: ['-y', '@modelcontextprotocol/server-brave-search'],
|
||||||
env: {},
|
env: {},
|
||||||
|
_managedBy: 'ccs',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,6 +77,7 @@ const TAVILY_CONFIG: McpServerConfig = {
|
|||||||
command: 'npx',
|
command: 'npx',
|
||||||
args: ['-y', '@tavily/mcp-server'],
|
args: ['-y', '@tavily/mcp-server'],
|
||||||
env: {},
|
env: {},
|
||||||
|
_managedBy: 'ccs',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -571,3 +575,230 @@ function ensureWebSearchHookConfigInternal(): boolean {
|
|||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ import { getWebSearchConfig } from '../config/unified-config-loader';
|
|||||||
import type { WebSearchConfig } from '../config/unified-config-types';
|
import type { WebSearchConfig } from '../config/unified-config-types';
|
||||||
import { isUnifiedConfig } from '../config/unified-config-types';
|
import { isUnifiedConfig } from '../config/unified-config-types';
|
||||||
import { isSensitiveKey, maskSensitiveValue } from '../utils/sensitive-keys';
|
import { isSensitiveKey, maskSensitiveValue } from '../utils/sensitive-keys';
|
||||||
|
import {
|
||||||
|
getWebSearchReadiness,
|
||||||
|
getMcpWebSearchStatus,
|
||||||
|
getGeminiCliStatus,
|
||||||
|
} from '../utils/mcp-manager';
|
||||||
|
|
||||||
export const apiRoutes = Router();
|
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 });
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
+122
-1
@@ -23,16 +23,36 @@ interface WebSearchConfig {
|
|||||||
fallback: boolean;
|
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() {
|
export function SettingsPage() {
|
||||||
const [config, setConfig] = useState<WebSearchConfig | null>(null);
|
const [config, setConfig] = useState<WebSearchConfig | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [status, setStatus] = useState<WebSearchStatus | null>(null);
|
||||||
|
const [statusLoading, setStatusLoading] = useState(true);
|
||||||
|
|
||||||
// Load config on mount
|
// Load config and status on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
|
fetchStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
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<WebSearchConfig>) => {
|
const saveConfig = async (updates: Partial<WebSearchConfig>) => {
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
|
|
||||||
@@ -131,6 +165,93 @@ export function SettingsPage() {
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
{/* WebSearch Status Panel */}
|
||||||
|
<div className="space-y-4 pb-4 border-b">
|
||||||
|
<h4 className="font-medium flex items-center gap-2">
|
||||||
|
Status
|
||||||
|
<Button variant="ghost" size="sm" onClick={fetchStatus} disabled={statusLoading}>
|
||||||
|
<RefreshCw className={`w-3 h-3 ${statusLoading ? 'animate-spin' : ''}`} />
|
||||||
|
</Button>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{statusLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||||
|
Checking status...
|
||||||
|
</div>
|
||||||
|
) : status ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Overall Readiness */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{status.readiness.status === 'ready' && (
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-600" />
|
||||||
|
)}
|
||||||
|
{status.readiness.status === 'mcp-only' && (
|
||||||
|
<Info className="w-4 h-4 text-blue-600" />
|
||||||
|
)}
|
||||||
|
{status.readiness.status === 'unavailable' && (
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-600" />
|
||||||
|
)}
|
||||||
|
<span className="font-medium">{status.readiness.message}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gemini CLI Status */}
|
||||||
|
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||||
|
<div
|
||||||
|
className={`w-2 h-2 rounded-full mt-2 ${
|
||||||
|
status.geminiCli.installed ? 'bg-green-500' : 'bg-gray-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">Gemini CLI</p>
|
||||||
|
{status.geminiCli.installed ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Installed {status.geminiCli.version && `(${status.geminiCli.version})`}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<p>Not installed</p>
|
||||||
|
<code className="text-xs bg-muted px-1 py-0.5 rounded">
|
||||||
|
npm install -g @google/gemini-cli
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* MCP Servers */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">MCP Servers</p>
|
||||||
|
{status.mcpServers.ccsManaged.length === 0 &&
|
||||||
|
status.mcpServers.userAdded.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">No web search MCP configured</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{status.mcpServers.ccsManaged.map((name) => (
|
||||||
|
<div key={name} className="flex items-center gap-2 text-sm">
|
||||||
|
<span className="px-1.5 py-0.5 rounded text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300">
|
||||||
|
Managed by CCS
|
||||||
|
</span>
|
||||||
|
<span>{name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{status.mcpServers.userAdded.map((name) => (
|
||||||
|
<div key={name} className="flex items-center gap-2 text-sm">
|
||||||
|
<span className="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||||
|
User-added
|
||||||
|
</span>
|
||||||
|
<span>{name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">Status unavailable</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Enable/Disable */}
|
{/* Enable/Disable */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user