mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-06 08:13:49 +00:00
feat(websearch): add agy provider and deprecate gemini cli fallback (#1607)
Google retired the gemini CLI on 2026-06-18, so the gemini websearch fallback no longer works. Add agy (Antigravity) as the primary CLI websearch provider end to end: runtime spawn in websearch-transformer.cjs (agy --dangerously-skip-permissions --print-timeout Ns -p), detection (agy.ts), config schema and defaults, hook env, status, dashboard persistence, and docs. The legacy gemini provider stays present but is marked deprecated for enterprise users who retain access. Image analysis already supported the agy provider (no change). The CLIProxy gemini quota fetcher (Gemini API OAuth, not the cli binary) is untouched. Validation: typecheck, lint, and the websearch + config + web-server unit suites pass.
This commit is contained in:
1 parent
608ff97e35
commit
9dd9bf2978
18 files changed
+395
-43
No files matched your search
+8
-3
@@ -70,9 +70,10 @@ That shared launch helper applies to normal third-party settings profiles, CLIPr
|
||||
| Brave Search | HTTP API | `BRAVE_API_KEY` | No | Cleaner snippets and metadata |
|
||||
| SearXNG | JSON API | `providers.searxng.url` | No | Self-hosted/public SearXNG backend via `/search?format=json` |
|
||||
| DuckDuckGo | HTML fetch | None | Yes | Built-in zero-setup fallback |
|
||||
| Gemini CLI | Legacy CLI | `npm i -g @google/gemini-cli` | No | Optional compatibility fallback |
|
||||
| OpenCode | Legacy CLI | `curl -fsSL https://opencode.ai/install \| bash` | No | Optional compatibility fallback |
|
||||
| Grok CLI | Legacy CLI | `npm i -g @vibe-kit/grok-cli` + `GROK_API_KEY` | No | Optional compatibility fallback |
|
||||
| Antigravity (agy) | LLM CLI | `curl -fsSL https://antigravity.google/cli/install.sh \| bash` | No | Recommended LLM CLI fallback (Gemini CLI successor) |
|
||||
| Gemini CLI | LLM CLI | Deprecated, use Antigravity (agy) | No | Deprecated. Google retired the gemini CLI on 2026-06-18 |
|
||||
| OpenCode | LLM CLI | `curl -fsSL https://opencode.ai/install \| bash` | No | Optional compatibility fallback |
|
||||
| Grok CLI | LLM CLI | `npm i -g @vibe-kit/grok-cli` + `GROK_API_KEY` | No | Optional compatibility fallback |
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -111,6 +112,10 @@ websearch:
|
||||
duckduckgo:
|
||||
enabled: true
|
||||
max_results: 5
|
||||
agy:
|
||||
enabled: false
|
||||
model: gemini-2.5-flash
|
||||
timeout: 90
|
||||
gemini:
|
||||
enabled: false
|
||||
model: gemini-2.5-flash
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
* - SearXNG JSON API
|
||||
* - DuckDuckGo HTML search
|
||||
*
|
||||
* Legacy compatibility fallback:
|
||||
* - Gemini CLI
|
||||
* Optional LLM CLI fallback:
|
||||
* - Antigravity CLI (agy) - recommended (Gemini CLI successor)
|
||||
* - Gemini CLI - deprecated (Google retired the gemini CLI on 2026-06-18)
|
||||
* - OpenCode
|
||||
* - Grok CLI
|
||||
*/
|
||||
@@ -49,6 +50,11 @@ const SHARED_INSTRUCTIONS = `Instructions:
|
||||
7. Format output clearly with sections if the topic is complex`;
|
||||
|
||||
const PROVIDER_CONFIG = {
|
||||
agy: {
|
||||
model: 'gemini-2.5-flash',
|
||||
toolInstruction: 'Use your web search tool to find current information.',
|
||||
quirks: null,
|
||||
},
|
||||
gemini: {
|
||||
model: 'gemini-2.5-flash',
|
||||
toolInstruction: 'Use the google_web_search tool to find current information.',
|
||||
@@ -124,7 +130,10 @@ function readProviderState() {
|
||||
|
||||
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
||||
const cooldowns =
|
||||
parsed && typeof parsed === 'object' && parsed.cooldowns && typeof parsed.cooldowns === 'object'
|
||||
parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
parsed.cooldowns &&
|
||||
typeof parsed.cooldowns === 'object'
|
||||
? parsed.cooldowns
|
||||
: {};
|
||||
return { cooldowns };
|
||||
@@ -856,6 +865,66 @@ async function tryDuckDuckGoSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
||||
}
|
||||
}
|
||||
|
||||
function runAgyCommand(args, timeoutMs) {
|
||||
const result = spawnSync('agy', args, {
|
||||
encoding: 'utf8',
|
||||
timeout: timeoutMs,
|
||||
maxBuffer: 1024 * 1024 * 2,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
// Never route query-derived prompts through a shell. Node concatenates
|
||||
// arguments for shell-backed Windows spawns, which lets shell metacharacters
|
||||
// in WebSearch queries escape the intended CLI invocation.
|
||||
shell: false,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
if (result.error.code === 'ENOENT')
|
||||
return { success: false, error: 'Antigravity CLI (agy) not installed' };
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: (result.stderr || '').trim() || `Antigravity CLI exited with code ${result.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const output = (result.stdout || '').trim();
|
||||
if (!output || output.length < MIN_VALID_RESPONSE_LENGTH) {
|
||||
return { success: false, error: 'Empty or too short response from Antigravity CLI' };
|
||||
}
|
||||
return { success: true, content: output };
|
||||
}
|
||||
|
||||
function tryAgySearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
||||
try {
|
||||
const timeoutMs = timeoutSec * 1000;
|
||||
const model = process.env.CCS_WEBSEARCH_AGY_MODEL || PROVIDER_CONFIG.agy.model;
|
||||
const prompt = buildPrompt('agy', query);
|
||||
// gemini flag mapping: `--yolo` -> `--dangerously-skip-permissions`, `-m` -> `--model`,
|
||||
// shell `timeout N` -> native `--print-timeout Ns`. The prompt is passed to `-p` (print mode).
|
||||
const args = [
|
||||
'--model',
|
||||
model,
|
||||
'--dangerously-skip-permissions',
|
||||
'--print-timeout',
|
||||
`${timeoutSec}s`,
|
||||
'-p',
|
||||
prompt,
|
||||
];
|
||||
|
||||
debug(`Executing Antigravity (agy) fallback with model ${model}`);
|
||||
return runAgyCommand(args, timeoutMs);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.killed
|
||||
? 'Antigravity CLI timed out'
|
||||
: error.message || 'Unknown Antigravity error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRetryGeminiWithLegacyPrompt(errorMessage) {
|
||||
const lower = (errorMessage || '').toLowerCase();
|
||||
return (
|
||||
@@ -1042,6 +1111,12 @@ function getConfiguredProviders() {
|
||||
available: () => isProviderEnabled('duckduckgo'),
|
||||
fn: tryDuckDuckGoSearch,
|
||||
},
|
||||
{
|
||||
name: 'Antigravity CLI',
|
||||
id: 'agy',
|
||||
available: () => isProviderEnabled('agy') && isCliAvailable('agy'),
|
||||
fn: tryAgySearch,
|
||||
},
|
||||
{
|
||||
name: 'Gemini CLI',
|
||||
id: 'gemini',
|
||||
@@ -1230,7 +1305,9 @@ async function runProviderWithPolicy(provider, query, timeoutSec, fingerprint) {
|
||||
}
|
||||
|
||||
function getActiveProviders() {
|
||||
return getConfiguredProviders().filter((provider) => !getProviderCooldown(provider.id) && provider.available());
|
||||
return getConfiguredProviders().filter(
|
||||
(provider) => !getProviderCooldown(provider.id) && provider.available()
|
||||
);
|
||||
}
|
||||
|
||||
function getActiveProviderIds() {
|
||||
|
||||
@@ -90,6 +90,7 @@ export function getWebSearchConfig(): {
|
||||
brave?: { enabled?: boolean; max_results?: number };
|
||||
searxng?: { enabled?: boolean; url?: string; max_results?: number };
|
||||
duckduckgo?: { enabled?: boolean; max_results?: number };
|
||||
agy?: { enabled?: boolean; model?: string; timeout?: number };
|
||||
gemini?: GeminiWebSearchInfo;
|
||||
opencode?: { enabled?: boolean; model?: string; timeout?: number };
|
||||
grok?: { enabled?: boolean; timeout?: number };
|
||||
@@ -126,6 +127,12 @@ export function getWebSearchConfig(): {
|
||||
max_results: config.websearch?.providers?.searxng?.max_results ?? 5,
|
||||
};
|
||||
|
||||
const agyConfig = {
|
||||
enabled: config.websearch?.providers?.agy?.enabled ?? false,
|
||||
model: config.websearch?.providers?.agy?.model ?? 'gemini-2.5-flash',
|
||||
timeout: config.websearch?.providers?.agy?.timeout ?? 90,
|
||||
};
|
||||
|
||||
const geminiConfig: GeminiWebSearchInfo = {
|
||||
enabled:
|
||||
config.websearch?.providers?.gemini?.enabled ?? config.websearch?.gemini?.enabled ?? false,
|
||||
@@ -152,6 +159,7 @@ export function getWebSearchConfig(): {
|
||||
braveConfig.enabled ||
|
||||
searxngConfig.enabled ||
|
||||
duckDuckGoConfig.enabled ||
|
||||
agyConfig.enabled ||
|
||||
geminiConfig.enabled ||
|
||||
opencodeConfig.enabled ||
|
||||
grokConfig.enabled;
|
||||
@@ -165,6 +173,7 @@ export function getWebSearchConfig(): {
|
||||
brave: braveConfig,
|
||||
searxng: searxngConfig,
|
||||
duckduckgo: duckDuckGoConfig,
|
||||
agy: agyConfig,
|
||||
gemini: geminiConfig,
|
||||
opencode: opencodeConfig,
|
||||
grok: grokConfig,
|
||||
|
||||
@@ -160,6 +160,11 @@ export function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfi
|
||||
enabled: partial.websearch?.providers?.duckduckgo?.enabled ?? true,
|
||||
max_results: partial.websearch?.providers?.duckduckgo?.max_results ?? 5,
|
||||
},
|
||||
agy: {
|
||||
enabled: partial.websearch?.providers?.agy?.enabled ?? false,
|
||||
model: partial.websearch?.providers?.agy?.model ?? 'gemini-2.5-flash',
|
||||
timeout: partial.websearch?.providers?.agy?.timeout ?? 90,
|
||||
},
|
||||
gemini: {
|
||||
enabled:
|
||||
partial.websearch?.providers?.gemini?.enabled ??
|
||||
|
||||
@@ -149,8 +149,11 @@ export function generateYamlWithComments(config: UnifiedConfig): string {
|
||||
lines.push('# Brave requires BRAVE_API_KEY in your environment.');
|
||||
lines.push('# DuckDuckGo works with zero extra setup and is enabled by default.');
|
||||
lines.push('#');
|
||||
lines.push('# Legacy LLM fallbacks remain optional if you still want them:');
|
||||
lines.push('# gemini: npm i -g @google/gemini-cli');
|
||||
lines.push('# Optional LLM CLI fallbacks:');
|
||||
lines.push(
|
||||
'# agy (recommended): curl -fsSL https://antigravity.google/cli/install.sh | bash'
|
||||
);
|
||||
lines.push('# gemini (deprecated, retired upstream 2026-06-18): use agy instead');
|
||||
lines.push('# opencode: curl -fsSL https://opencode.ai/install | bash');
|
||||
lines.push('# grok: npm i -g @vibe-kit/grok-cli');
|
||||
lines.push('# ----------------------------------------------------------------------------');
|
||||
|
||||
@@ -66,6 +66,7 @@ export type {
|
||||
ExaWebSearchConfig,
|
||||
TavilyWebSearchConfig,
|
||||
SearxngWebSearchConfig,
|
||||
AgyWebSearchConfig,
|
||||
GeminiWebSearchConfig,
|
||||
GrokWebSearchConfig,
|
||||
OpenCodeWebSearchConfig,
|
||||
|
||||
@@ -154,6 +154,11 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
||||
enabled: true,
|
||||
max_results: 5,
|
||||
},
|
||||
agy: {
|
||||
enabled: false,
|
||||
model: 'gemini-2.5-flash',
|
||||
timeout: 90,
|
||||
},
|
||||
gemini: {
|
||||
enabled: false,
|
||||
model: 'gemini-2.5-flash',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* - API-backed: Exa, Tavily, Brave
|
||||
* - Self-hosted: SearXNG
|
||||
* - Zero-setup: DuckDuckGo
|
||||
* - Legacy CLI fallbacks: Gemini, Grok, OpenCode
|
||||
* - LLM CLI fallbacks: Antigravity (agy, recommended), Gemini (deprecated), Grok, OpenCode
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -60,8 +60,25 @@ export interface SearxngWebSearchConfig {
|
||||
max_results?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Antigravity CLI (agy) WebSearch configuration.
|
||||
*
|
||||
* Recommended LLM CLI fallback. `agy` is Google's successor to the retired
|
||||
* `gemini` CLI. Install: `curl -fsSL https://antigravity.google/cli/install.sh | bash`.
|
||||
*/
|
||||
export interface AgyWebSearchConfig {
|
||||
/** Enable Antigravity CLI fallback (default: false) */
|
||||
enabled?: boolean;
|
||||
/** Model to use (default: gemini-2.5-flash; accepts legacy gemini ids) */
|
||||
model?: string;
|
||||
/** Timeout in seconds (default: 90) */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini CLI WebSearch configuration.
|
||||
*
|
||||
* @deprecated Google retired the gemini CLI on 2026-06-18. Use Antigravity (agy) instead.
|
||||
*/
|
||||
export interface GeminiWebSearchConfig {
|
||||
/** Enable Gemini CLI legacy fallback (default: false) */
|
||||
@@ -109,7 +126,9 @@ export interface WebSearchProvidersConfig {
|
||||
searxng?: SearxngWebSearchConfig;
|
||||
/** DuckDuckGo HTML search - zero setup default backend */
|
||||
duckduckgo?: DuckDuckGoWebSearchConfig;
|
||||
/** Gemini CLI - optional legacy LLM fallback */
|
||||
/** Antigravity CLI (agy) - recommended LLM CLI fallback (Gemini CLI successor) */
|
||||
agy?: AgyWebSearchConfig;
|
||||
/** Gemini CLI - deprecated legacy LLM fallback (retired upstream) */
|
||||
gemini?: GeminiWebSearchConfig;
|
||||
/** Grok CLI - optional legacy LLM fallback */
|
||||
grok?: GrokWebSearchConfig;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
// Re-export types
|
||||
export type {
|
||||
AgyCliStatus,
|
||||
GeminiCliStatus,
|
||||
GrokCliStatus,
|
||||
OpenCodeCliStatus,
|
||||
@@ -25,6 +26,8 @@ export type {
|
||||
} from './websearch/types';
|
||||
|
||||
// Re-export CLI detection functions
|
||||
export { getAgyCliStatus, hasAgyCli, clearAgyCliCache } from './websearch/agy';
|
||||
|
||||
export {
|
||||
getGeminiCliStatus,
|
||||
hasGeminiCli,
|
||||
@@ -92,12 +95,18 @@ export {
|
||||
export { ensureProfileHooks, ensureProfileHooksOrThrow } from './websearch/profile-hook-injector';
|
||||
|
||||
// Import for local use
|
||||
import { clearGeminiCliCache, clearGrokCliCache, clearOpenCodeCliCache } from './websearch';
|
||||
import {
|
||||
clearAgyCliCache,
|
||||
clearGeminiCliCache,
|
||||
clearGrokCliCache,
|
||||
clearOpenCodeCliCache,
|
||||
} from './websearch';
|
||||
|
||||
/**
|
||||
* Clear all CLI caches
|
||||
*/
|
||||
export function clearAllCliCaches(): void {
|
||||
clearAgyCliCache();
|
||||
clearGeminiCliCache();
|
||||
clearGrokCliCache();
|
||||
clearOpenCodeCliCache();
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Antigravity CLI (agy) Detection
|
||||
*
|
||||
* Detects and manages Antigravity CLI installation status. `agy` is Google's
|
||||
* successor to the retired `gemini` CLI (Google retired the gemini CLI on
|
||||
* 2026-06-18) and is the recommended LLM CLI WebSearch fallback.
|
||||
*
|
||||
* @module utils/websearch/agy
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import type { AgyCliStatus } from './types';
|
||||
|
||||
// Cache for Antigravity CLI status (per process)
|
||||
let agyCliCache: AgyCliStatus | null = null;
|
||||
|
||||
/**
|
||||
* Check if Antigravity CLI (agy) is installed globally.
|
||||
*
|
||||
* Install: `curl -fsSL https://antigravity.google/cli/install.sh | bash`
|
||||
* (Unix installs to ~/.local/bin/agy). Must be in PATH.
|
||||
*
|
||||
* @returns Antigravity CLI status with path and version
|
||||
*/
|
||||
export function getAgyCliStatus(): AgyCliStatus {
|
||||
// Return cached result if available
|
||||
if (agyCliCache) {
|
||||
return agyCliCache;
|
||||
}
|
||||
|
||||
const result: AgyCliStatus = {
|
||||
installed: false,
|
||||
path: undefined,
|
||||
version: undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const whichCmd = isWindows ? 'where agy' : 'which agy';
|
||||
|
||||
const pathResult = execSync(whichCmd, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const agyPath = pathResult.trim().split('\n')[0]; // First result on Windows
|
||||
|
||||
if (agyPath) {
|
||||
result.installed = true;
|
||||
result.path = agyPath;
|
||||
|
||||
// Try to get version
|
||||
try {
|
||||
const versionResult = execSync('agy --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 - Antigravity CLI not installed
|
||||
}
|
||||
|
||||
// Cache result
|
||||
agyCliCache = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Antigravity CLI is available (quick boolean check)
|
||||
*/
|
||||
export function hasAgyCli(): boolean {
|
||||
return getAgyCliStatus().installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear Antigravity CLI cache (for testing or after installation)
|
||||
*/
|
||||
export function clearAgyCliCache(): void {
|
||||
agyCliCache = null;
|
||||
}
|
||||
@@ -41,6 +41,9 @@ export function getWebSearchHookConfig(): Record<string, unknown> {
|
||||
|
||||
// Compute max timeout from enabled providers
|
||||
const timeouts: number[] = [];
|
||||
if (wsConfig.providers?.agy?.enabled && wsConfig.providers.agy.timeout) {
|
||||
timeouts.push(wsConfig.providers.agy.timeout);
|
||||
}
|
||||
if (wsConfig.providers?.gemini?.enabled && wsConfig.providers.gemini.timeout) {
|
||||
timeouts.push(wsConfig.providers.gemini.timeout);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export function getWebSearchHookEnv(): Record<string, string> {
|
||||
CCS_WEBSEARCH_BRAVE: '0',
|
||||
CCS_WEBSEARCH_SEARXNG: '0',
|
||||
CCS_WEBSEARCH_DUCKDUCKGO: '0',
|
||||
CCS_WEBSEARCH_AGY: '0',
|
||||
CCS_WEBSEARCH_GEMINI: '0',
|
||||
CCS_WEBSEARCH_OPENCODE: '0',
|
||||
CCS_WEBSEARCH_GROK: '0',
|
||||
@@ -80,12 +81,24 @@ export function getWebSearchHookEnv(): Record<string, string> {
|
||||
env.CCS_WEBSEARCH_SEARXNG_MAX_RESULTS = String(wsConfig.providers.searxng.max_results || 5);
|
||||
}
|
||||
|
||||
if (wsConfig.providers?.agy?.enabled) {
|
||||
env.CCS_WEBSEARCH_AGY = '1';
|
||||
if (wsConfig.providers.agy.model) {
|
||||
env.CCS_WEBSEARCH_AGY_MODEL = wsConfig.providers.agy.model;
|
||||
}
|
||||
// Antigravity is the primary CLI fallback, so its timeout wins.
|
||||
env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.agy.timeout || 90);
|
||||
}
|
||||
|
||||
if (wsConfig.providers?.gemini?.enabled) {
|
||||
env.CCS_WEBSEARCH_GEMINI = '1';
|
||||
if (wsConfig.providers.gemini.model) {
|
||||
env.CCS_WEBSEARCH_GEMINI_MODEL = wsConfig.providers.gemini.model;
|
||||
}
|
||||
env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.gemini.timeout || 55);
|
||||
// Only set if Antigravity (primary) has not already chosen the timeout.
|
||||
if (!env.CCS_WEBSEARCH_TIMEOUT) {
|
||||
env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.gemini.timeout || 55);
|
||||
}
|
||||
}
|
||||
|
||||
if (wsConfig.providers?.opencode?.enabled) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
// Types
|
||||
export type {
|
||||
AgyCliStatus,
|
||||
GeminiCliStatus,
|
||||
GrokCliStatus,
|
||||
OpenCodeCliStatus,
|
||||
@@ -20,7 +21,10 @@ export type {
|
||||
|
||||
export type { WebSearchApiKeyState } from './provider-secrets';
|
||||
|
||||
// Gemini CLI
|
||||
// Antigravity CLI (agy) - recommended Gemini CLI successor
|
||||
export { getAgyCliStatus, hasAgyCli, clearAgyCliCache } from './agy';
|
||||
|
||||
// Gemini CLI (deprecated)
|
||||
export {
|
||||
getGeminiCliStatus,
|
||||
hasGeminiCli,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { join } from 'path';
|
||||
import { ok, warn, fail, info } from '../ui';
|
||||
|
||||
import { getCcsDir } from '../config-manager';
|
||||
import { getAgyCliStatus } from './agy';
|
||||
import { getGeminiCliStatus, isGeminiAuthenticated } from './gemini-cli';
|
||||
import { getGrokCliStatus } from './grok-cli';
|
||||
import { getOpenCodeCliStatus } from './opencode-cli';
|
||||
@@ -113,12 +114,31 @@ function applyCooldownStatus(
|
||||
|
||||
function getLegacyProviderStatuses(): WebSearchCliInfo[] {
|
||||
const wsConfig = getWebSearchConfig();
|
||||
const agyStatus = getAgyCliStatus();
|
||||
const geminiStatus = getGeminiCliStatus();
|
||||
const grokStatus = getGrokCliStatus();
|
||||
const opencodeStatus = getOpenCodeCliStatus();
|
||||
const geminiAuthed = geminiStatus.installed && isGeminiAuthenticated();
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'agy',
|
||||
kind: 'legacy-cli',
|
||||
name: 'Antigravity CLI',
|
||||
command: 'agy',
|
||||
enabled: wsConfig.providers?.agy?.enabled ?? false,
|
||||
available: agyStatus.installed,
|
||||
version: agyStatus.version ?? null,
|
||||
installCommand: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
|
||||
docsUrl: 'https://antigravity.google/cli',
|
||||
requiresApiKey: false,
|
||||
description: 'Recommended LLM CLI fallback with Google web search (Gemini CLI successor).',
|
||||
detail: agyStatus.installed
|
||||
? agyStatus.version
|
||||
? `Installed (${agyStatus.version})`
|
||||
: 'Installed'
|
||||
: 'Not installed',
|
||||
},
|
||||
{
|
||||
id: 'gemini',
|
||||
kind: 'legacy-cli',
|
||||
@@ -127,15 +147,16 @@ function getLegacyProviderStatuses(): WebSearchCliInfo[] {
|
||||
enabled: wsConfig.providers?.gemini?.enabled ?? false,
|
||||
available: geminiAuthed,
|
||||
version: geminiStatus.version ?? null,
|
||||
installCommand: 'npm install -g @google/gemini-cli',
|
||||
docsUrl: 'https://github.com/google-gemini/gemini-cli',
|
||||
installCommand: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
|
||||
docsUrl: 'https://antigravity.google/cli',
|
||||
requiresApiKey: false,
|
||||
description: 'Optional legacy LLM fallback with Google web search.',
|
||||
description:
|
||||
'Deprecated legacy fallback (Google retired the gemini CLI). Prefer Antigravity.',
|
||||
detail: geminiStatus.installed
|
||||
? geminiAuthed
|
||||
? 'Authenticated'
|
||||
: "Run 'gemini' to login"
|
||||
: 'Not installed',
|
||||
: 'Not installed (retired - use Antigravity)',
|
||||
},
|
||||
{
|
||||
id: 'opencode',
|
||||
@@ -287,7 +308,7 @@ export function getCliInstallHints(): string[] {
|
||||
' Enable DuckDuckGo in Settings > WebSearch for zero-setup search',
|
||||
' Or enable SearXNG and set a valid base URL (must support /search?format=json)',
|
||||
' Or export EXA_API_KEY, TAVILY_API_KEY, or BRAVE_API_KEY for API-backed search',
|
||||
' Optional legacy fallback: npm i -g @google/gemini-cli',
|
||||
' Optional LLM CLI fallback: curl -fsSL https://antigravity.google/cli/install.sh | bash',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
import type { ComponentStatus } from '../../types/utils';
|
||||
|
||||
/**
|
||||
* Antigravity CLI (agy) installation status
|
||||
* @deprecated Use ComponentStatus directly
|
||||
*/
|
||||
export type AgyCliStatus = ComponentStatus;
|
||||
|
||||
/**
|
||||
* Gemini CLI installation status
|
||||
* @deprecated Use ComponentStatus directly
|
||||
@@ -40,6 +46,7 @@ export type WebSearchProviderId =
|
||||
| 'brave'
|
||||
| 'searxng'
|
||||
| 'duckduckgo'
|
||||
| 'agy'
|
||||
| 'gemini'
|
||||
| 'grok'
|
||||
| 'opencode';
|
||||
@@ -112,6 +119,7 @@ export interface WebSearchConfig {
|
||||
brave?: WebSearchProviderConfig;
|
||||
searxng?: WebSearchProviderConfig;
|
||||
duckduckgo?: WebSearchProviderConfig;
|
||||
agy?: WebSearchProviderConfig;
|
||||
gemini?: WebSearchProviderConfig;
|
||||
opencode?: WebSearchProviderConfig;
|
||||
grok?: WebSearchProviderConfig;
|
||||
|
||||
@@ -197,6 +197,15 @@ router.put('/', (req: Request, res: Response): void => {
|
||||
config.websearch?.providers?.searxng?.max_results ?? DEFAULT_WEBSEARCH_MAX_RESULTS
|
||||
),
|
||||
},
|
||||
agy: {
|
||||
enabled:
|
||||
providers.agy?.enabled ?? config.websearch?.providers?.agy?.enabled ?? false,
|
||||
model:
|
||||
providers.agy?.model ??
|
||||
config.websearch?.providers?.agy?.model ??
|
||||
'gemini-2.5-flash',
|
||||
timeout: providers.agy?.timeout ?? config.websearch?.providers?.agy?.timeout ?? 90,
|
||||
},
|
||||
gemini: {
|
||||
enabled:
|
||||
providers.gemini?.enabled ??
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
@@ -32,11 +25,7 @@ const hook = require('../../../lib/hooks/websearch-transformer.cjs') as {
|
||||
query: string,
|
||||
errors: Array<{ provider: string; error: string }>
|
||||
) => HookOutput;
|
||||
buildSuccessHookOutput: (
|
||||
query: string,
|
||||
providerName: string,
|
||||
content: string
|
||||
) => HookOutput;
|
||||
buildSuccessHookOutput: (query: string, providerName: string, content: string) => HookOutput;
|
||||
classifyDuckDuckGoHtml: (
|
||||
html: string,
|
||||
count: number
|
||||
@@ -45,7 +34,10 @@ const hook = require('../../../lib/hooks/websearch-transformer.cjs') as {
|
||||
kind: 'results' | 'no_results' | 'non_result_html';
|
||||
results: Array<{ title: string; url: string; description: string }>;
|
||||
};
|
||||
extractDuckDuckGoResults: (html: string, count: number) => Array<{
|
||||
extractDuckDuckGoResults: (
|
||||
html: string,
|
||||
count: number
|
||||
) => Array<{
|
||||
title: string;
|
||||
url: string;
|
||||
description: string;
|
||||
@@ -62,7 +54,10 @@ const hook = require('../../../lib/hooks/websearch-transformer.cjs') as {
|
||||
results: Array<{ title: string; url: string; description: string }>
|
||||
) => string;
|
||||
parseRetryAfterSeconds: (rawValue: string) => number | null;
|
||||
trySearxngSearch: (query: string, timeoutSec?: number) => Promise<{
|
||||
trySearxngSearch: (
|
||||
query: string,
|
||||
timeoutSec?: number
|
||||
) => Promise<{
|
||||
content?: string;
|
||||
error?: string;
|
||||
statusCode?: number;
|
||||
@@ -136,7 +131,7 @@ describe('websearch-transformer legacy CLI safety', () => {
|
||||
const source = readFileSync(hookPath, 'utf8');
|
||||
|
||||
expect(source).not.toContain('shell: isWindows');
|
||||
expect(source.match(/shell: false/g) || []).toHaveLength(3);
|
||||
expect(source.match(/shell: false/g) || []).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -411,9 +406,7 @@ describe('websearch-transformer hook helpers', () => {
|
||||
const output = JSON.parse(result.stdout.trim()) as HookOutput;
|
||||
expect(output.hookSpecificOutput.hookEventName).toBe('PreToolUse');
|
||||
expect(output.hookSpecificOutput.permissionDecision).toBe('deny');
|
||||
expect(output.hookSpecificOutput.additionalContext).toContain(
|
||||
'CCS local WebSearch evidence'
|
||||
);
|
||||
expect(output.hookSpecificOutput.additionalContext).toContain('CCS local WebSearch evidence');
|
||||
expect(output.hookSpecificOutput.additionalContext).toContain('Provider: DuckDuckGo');
|
||||
expect(output.hookSpecificOutput.additionalContext).toContain(
|
||||
'URL: https://example.com/article'
|
||||
@@ -782,8 +775,7 @@ global.fetch = async (url) => {
|
||||
expect(
|
||||
traceEvents.some(
|
||||
(event) =>
|
||||
event.event === 'websearch_provider_success' &&
|
||||
event.providerId === 'duckduckgo'
|
||||
event.event === 'websearch_provider_success' && event.providerId === 'duckduckgo'
|
||||
)
|
||||
).toBe(true);
|
||||
} finally {
|
||||
@@ -994,8 +986,7 @@ global.fetch = async (url) => {
|
||||
).toBe(true);
|
||||
expect(
|
||||
traceEvents.some(
|
||||
(event) =>
|
||||
event.event === 'websearch_provider_success' && event.providerId === 'exa'
|
||||
(event) => event.event === 'websearch_provider_success' && event.providerId === 'exa'
|
||||
)
|
||||
).toBe(true);
|
||||
} finally {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it, spyOn } from 'bun:test';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import * as agyCli from '../../../../src/utils/websearch/agy';
|
||||
import * as geminiCli from '../../../../src/utils/websearch/gemini-cli';
|
||||
import * as grokCli from '../../../../src/utils/websearch/grok-cli';
|
||||
import * as opencodeCli from '../../../../src/utils/websearch/opencode-cli';
|
||||
@@ -13,7 +14,9 @@ import {
|
||||
} from '../../../../src/utils/websearch/status';
|
||||
import type { WebSearchCliInfo } from '../../../../src/utils/websearch/types';
|
||||
|
||||
function provider(overrides: Partial<WebSearchCliInfo> & Pick<WebSearchCliInfo, 'id' | 'name'>): WebSearchCliInfo {
|
||||
function provider(
|
||||
overrides: Partial<WebSearchCliInfo> & Pick<WebSearchCliInfo, 'id' | 'name'>
|
||||
): WebSearchCliInfo {
|
||||
return {
|
||||
id: overrides.id,
|
||||
kind: overrides.kind ?? 'backend',
|
||||
@@ -151,6 +154,10 @@ describe('websearch readiness', () => {
|
||||
installed: false,
|
||||
version: null,
|
||||
} as any);
|
||||
const agyStatusSpy = spyOn(agyCli, 'getAgyCliStatus').mockReturnValue({
|
||||
installed: false,
|
||||
version: null,
|
||||
} as any);
|
||||
|
||||
try {
|
||||
const providers = getWebSearchCliProviders();
|
||||
@@ -166,6 +173,7 @@ describe('websearch readiness', () => {
|
||||
geminiAuthSpy.mockRestore();
|
||||
grokStatusSpy.mockRestore();
|
||||
opencodeStatusSpy.mockRestore();
|
||||
agyStatusSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -205,6 +213,10 @@ describe('websearch readiness', () => {
|
||||
installed: false,
|
||||
version: null,
|
||||
} as any);
|
||||
const agyStatusSpy = spyOn(agyCli, 'getAgyCliStatus').mockReturnValue({
|
||||
installed: false,
|
||||
version: null,
|
||||
} as any);
|
||||
|
||||
try {
|
||||
const providers = getWebSearchCliProviders();
|
||||
@@ -220,6 +232,72 @@ describe('websearch readiness', () => {
|
||||
geminiAuthSpy.mockRestore();
|
||||
grokStatusSpy.mockRestore();
|
||||
opencodeStatusSpy.mockRestore();
|
||||
agyStatusSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('exposes Antigravity (agy) as a recommended CLI provider when enabled and installed', () => {
|
||||
const getConfigSpy = spyOn(unifiedConfigLoader, 'getWebSearchConfig').mockReturnValue({
|
||||
enabled: true,
|
||||
providers: {
|
||||
exa: { enabled: false, max_results: 5 },
|
||||
tavily: { enabled: false, max_results: 5 },
|
||||
brave: { enabled: false, max_results: 5 },
|
||||
searxng: { enabled: false, url: '', max_results: 5 },
|
||||
duckduckgo: { enabled: false, max_results: 5 },
|
||||
agy: { enabled: true, model: 'gemini-2.5-flash', timeout: 90 },
|
||||
gemini: { enabled: false },
|
||||
grok: { enabled: false },
|
||||
opencode: { enabled: false },
|
||||
},
|
||||
} as any);
|
||||
const apiKeySpy = spyOn(providerSecrets, 'getWebSearchApiKeyStates').mockReturnValue({
|
||||
exa: { envVar: 'EXA_API_KEY', configured: false, available: false, source: 'none' },
|
||||
tavily: { envVar: 'TAVILY_API_KEY', configured: false, available: false, source: 'none' },
|
||||
brave: { envVar: 'BRAVE_API_KEY', configured: false, available: false, source: 'none' },
|
||||
});
|
||||
const agyStatusSpy = spyOn(agyCli, 'getAgyCliStatus').mockReturnValue({
|
||||
installed: true,
|
||||
version: '1.0.13',
|
||||
} as any);
|
||||
const geminiStatusSpy = spyOn(geminiCli, 'getGeminiCliStatus').mockReturnValue({
|
||||
installed: false,
|
||||
version: null,
|
||||
} as any);
|
||||
const geminiAuthSpy = spyOn(geminiCli, 'isGeminiAuthenticated').mockReturnValue(false);
|
||||
const grokStatusSpy = spyOn(grokCli, 'getGrokCliStatus').mockReturnValue({
|
||||
installed: false,
|
||||
version: null,
|
||||
} as any);
|
||||
const opencodeStatusSpy = spyOn(opencodeCli, 'getOpenCodeCliStatus').mockReturnValue({
|
||||
installed: false,
|
||||
version: null,
|
||||
} as any);
|
||||
|
||||
try {
|
||||
const providers = getWebSearchCliProviders();
|
||||
const agy = providers.find((entry) => entry.id === 'agy');
|
||||
|
||||
expect(agy?.name).toBe('Antigravity CLI');
|
||||
expect(agy?.kind).toBe('legacy-cli');
|
||||
expect(agy?.command).toBe('agy');
|
||||
expect(agy?.enabled).toBe(true);
|
||||
expect(agy?.available).toBe(true);
|
||||
expect(agy?.requiresApiKey).toBe(false);
|
||||
expect(agy?.installCommand).toContain('antigravity.google');
|
||||
expect(agy?.detail).toContain('1.0.13');
|
||||
|
||||
const readiness = buildWebSearchReadiness(true, providers);
|
||||
expect(readiness.readiness).toBe('ready');
|
||||
expect(readiness.message).toContain('Antigravity CLI');
|
||||
} finally {
|
||||
getConfigSpy.mockRestore();
|
||||
apiKeySpy.mockRestore();
|
||||
agyStatusSpy.mockRestore();
|
||||
geminiStatusSpy.mockRestore();
|
||||
geminiAuthSpy.mockRestore();
|
||||
grokStatusSpy.mockRestore();
|
||||
opencodeStatusSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -293,6 +371,10 @@ describe('websearch readiness', () => {
|
||||
installed: false,
|
||||
version: null,
|
||||
} as any);
|
||||
const agyStatusSpy = spyOn(agyCli, 'getAgyCliStatus').mockReturnValue({
|
||||
installed: false,
|
||||
version: null,
|
||||
} as any);
|
||||
|
||||
try {
|
||||
const providers = getWebSearchCliProviders();
|
||||
@@ -313,6 +395,7 @@ describe('websearch readiness', () => {
|
||||
geminiAuthSpy.mockRestore();
|
||||
grokStatusSpy.mockRestore();
|
||||
opencodeStatusSpy.mockRestore();
|
||||
agyStatusSpy.mockRestore();
|
||||
|
||||
if (originalCcsHome === undefined) {
|
||||
delete process.env.CCS_HOME;
|
||||
|
||||
Reference in new issue
Block a user