fix(websearch): honor disabled steering per launch

This commit is contained in:
Tam Nhu Tran committed 2026-07-29 13:22:21 -04:00
1 parent 09757b418e
commit abc3002e89
18 files changed
+165 -76

No files matched your search

+10 -2
View File
@@ -15,6 +15,7 @@ import {
appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext,
} from '../../utils/websearch-manager';
import type { WebSearchLaunchState } from '../../utils/websearch-manager';
import { appendThirdPartyImageAnalysisToolArgs } from '../../utils/image-analysis';
import { appendBrowserToolArgs } from '../../utils/browser';
import { getDefaultAccount } from '../accounts/account-manager';
@@ -51,6 +52,8 @@ export interface ClaudeLaunchContext {
sessionId: string | undefined;
/** Whether image analysis MCP is ready */
imageAnalysisMcpReady: boolean;
/** WebSearch state captured when launch provisioning ran */
webSearchLaunch: WebSearchLaunchState;
/** Browser runtime environment variables (undefined if browser not active) */
browserRuntimeEnv: NodeJS.ProcessEnv | undefined;
/** Inherited Claude config dir for continuity */
@@ -82,6 +85,7 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
skipLocalAuth,
sessionId,
imageAnalysisMcpReady,
webSearchLaunch,
browserRuntimeEnv,
inheritedClaudeConfigDir,
codexReasoningProxy,
@@ -124,8 +128,12 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
? appendBrowserToolArgs(imageAnalysisArgs)
: imageAnalysisArgs;
const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(browserArgs)
: ['--settings', settingsPath, ...appendThirdPartyWebSearchToolArgs(browserArgs)];
? appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled)
: [
'--settings',
settingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled),
];
// Inject web search trace context into env
const traceEnv = createWebSearchTraceContext({
+3 -2
View File
@@ -19,7 +19,6 @@ import {
import { applyExtendedContextConfig } from '../config/extended-context-config';
import { CLIProxyProvider } from '../types';
import { CompositeTierConfig } from '../../config/unified-config-types';
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
import {
applyImageAnalysisRuntimeOverrides,
getImageAnalysisHookEnv,
@@ -78,6 +77,8 @@ export interface ProxyChainConfig {
claudeConfigDir?: string;
/** Execution-aware image analysis env prepared by the caller */
imageAnalysisEnv?: Record<string, string>;
/** WebSearch hook env captured from the launch configuration snapshot. */
webSearchEnv?: Record<string, string>;
/** Optional browser runtime env for Claude browser MCP reuse. */
browserRuntimeEnv?: Record<string, string>;
}
@@ -246,6 +247,7 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
compositeDefaultTier,
claudeConfigDir,
imageAnalysisEnv: resolvedImageAnalysisEnv,
webSearchEnv = {},
browserRuntimeEnv,
} = config;
@@ -386,7 +388,6 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
};
// Add hook environment variables
const webSearchEnv = getWebSearchHookEnv();
const imageAnalysisEnv = resolvedImageAnalysisEnv ?? getImageAnalysisHookEnv(provider);
// Merge all environment variables (filter undefined values)
+8 -6
View File
@@ -52,7 +52,7 @@ import {
resolveCliproxyImageAnalysisEnv,
} from './env-resolver';
import { checkOrJoinProxy, registerProxySession } from './session-bridge';
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
import { resolveWebSearchLaunchState } from '../../utils/websearch-manager';
import {
handleLogout,
handleImport,
@@ -276,10 +276,9 @@ export async function execClaudeWithCLIProxy(
await handleImport(authCtx);
// Setup first-class CCS WebSearch runtime for non-strict user launches.
const shouldDisplayWebSearchStatus = ensureWebSearchMcpForLaunch();
if (shouldDisplayWebSearchStatus) {
displayWebSearchStatus();
}
const webSearchLaunch = resolveWebSearchLaunchState();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
displayWebSearchStatus(webSearchLaunch.config);
// 3. Ensure OAuth completed (if provider requires it)
const remoteAuthToken = proxyConfig.authToken?.trim();
@@ -483,6 +482,7 @@ export async function execClaudeWithCLIProxy(
compositeDefaultTier: cfg.compositeDefaultTier,
claudeConfigDir: inheritedClaudeConfigDir,
imageAnalysisEnv,
webSearchEnv: webSearchLaunch.hookEnv,
});
// 9b. Build env-dependent proxy chain (tool-sanitization + codex-reasoning)
@@ -527,6 +527,7 @@ export async function execClaudeWithCLIProxy(
compositeDefaultTier: cfg.compositeDefaultTier,
claudeConfigDir: inheritedClaudeConfigDir,
imageAnalysisEnv,
webSearchEnv: webSearchLaunch.hookEnv,
browserRuntimeEnv,
});
@@ -542,7 +543,7 @@ export async function execClaudeWithCLIProxy(
);
}
const webSearchEnv = getWebSearchHookEnv();
const webSearchEnv = webSearchLaunch.hookEnv;
if (process.env.CCS_DEBUG) {
logger.info('browser-env-keys', 'CCS_BROWSER_* keys in environment', {
keys: Object.keys(env)
@@ -580,6 +581,7 @@ export async function execClaudeWithCLIProxy(
skipLocalAuth,
sessionId,
imageAnalysisMcpReady,
webSearchLaunch,
browserRuntimeEnv,
inheritedClaudeConfigDir,
codexReasoningProxy,
+20 -7
View File
@@ -18,11 +18,11 @@ import { normalizeCopilotConfigWithWarnings } from './copilot-model-normalizer';
import { CopilotStatus } from './types';
import { fail, info, ok, warn } from '../utils/ui';
import {
getWebSearchHookEnv,
appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext,
syncWebSearchMcpToConfigDir,
} from '../utils/websearch-manager';
import type { WebSearchLaunchState } from '../utils/websearch-manager';
import {
appendThirdPartyImageAnalysisToolArgs,
ensureImageAnalysisMcpOrThrow,
@@ -71,6 +71,17 @@ export async function getCopilotStatus(config: CopilotConfig): Promise<CopilotSt
};
}
export function buildCopilotClaudeLaunchArgs(
claudeArgs: string[],
imageAnalysisMcpReady: boolean,
webSearchEnabled: boolean
): string[] {
const imageAnalysisArgs = imageAnalysisMcpReady
? appendThirdPartyImageAnalysisToolArgs(claudeArgs)
: claudeArgs;
return appendThirdPartyWebSearchToolArgs(imageAnalysisArgs, webSearchEnabled);
}
/**
* Generate environment variables for Claude Code to use copilot-api.
* Uses model mapping for opus/sonnet/haiku tiers if configured.
@@ -185,7 +196,8 @@ export async function executeCopilotProfile(
config: CopilotConfig,
claudeArgs: string[],
claudeConfigDir?: string,
claudeCliPath: string = 'claude'
claudeCliPath: string = 'claude',
webSearchLaunch?: WebSearchLaunchState
): Promise<number> {
const { config: normalizedConfig, warnings } = normalizeCopilotConfigWithWarnings(config);
@@ -276,7 +288,7 @@ export async function executeCopilotProfile(
syncImageAnalysisMcpToConfigDir(claudeConfigDir);
// Merge with current environment (global env first, copilot overrides, then hook env vars)
const webSearchEnv = getWebSearchHookEnv();
const webSearchEnv = webSearchLaunch?.hookEnv ?? {};
const imageAnalysisResolution = await resolveCopilotImageAnalysisEnv();
const imageAnalysisProvisioningFailed =
!imageAnalysisMcpReady && imageAnalysisResolution.env.CCS_IMAGE_ANALYSIS_ENABLED === '1';
@@ -305,10 +317,11 @@ export async function executeCopilotProfile(
// Spawn Claude CLI
const spawnStartedAt = Date.now();
return new Promise((resolve) => {
const imageAnalysisArgs = imageAnalysisMcpReady
? appendThirdPartyImageAnalysisToolArgs(claudeArgs)
: claudeArgs;
const launchArgs = appendThirdPartyWebSearchToolArgs(imageAnalysisArgs);
const launchArgs = buildCopilotClaudeLaunchArgs(
claudeArgs,
imageAnalysisMcpReady,
webSearchLaunch?.enabled ?? true
);
const traceEnv = createWebSearchTraceContext({
launcher: 'copilot.executor',
args: launchArgs,
+12 -4
View File
@@ -9,9 +9,9 @@ import { fail, info, ok } from '../utils/ui';
import {
appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext,
getWebSearchHookEnv,
syncWebSearchMcpToConfigDir,
} from '../utils/websearch-manager';
import type { WebSearchLaunchState } from '../utils/websearch-manager';
import { getImageAnalysisHookEnv, resolveImageAnalysisRuntimeStatus } from '../utils/hooks';
import { stripClaudeCodeEnv } from '../utils/shell-executor';
import { checkAuthStatus } from './cursor-auth';
@@ -106,11 +106,19 @@ export async function resolveCursorImageAnalysisEnv(
return { env, warning: null };
}
export function buildCursorClaudeLaunchArgs(
claudeArgs: string[],
webSearchEnabled: boolean
): string[] {
return appendThirdPartyWebSearchToolArgs(claudeArgs, webSearchEnabled);
}
export async function executeCursorProfile(
config: CursorConfig,
claudeArgs: string[],
claudeConfigDir?: string,
claudeCliPath = 'claude'
claudeCliPath = 'claude',
webSearchLaunch?: WebSearchLaunchState
): Promise<number> {
if (!config.enabled) {
process.stderr.write(fail('Cursor integration is not enabled.') + '\n');
@@ -163,7 +171,7 @@ export async function executeCursorProfile(
const cursorEnv = generateCursorEnv(config, daemonToken, claudeConfigDir);
const globalEnvConfig = getGlobalEnvConfig();
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
const webSearchEnv = getWebSearchHookEnv();
const webSearchEnv = webSearchLaunch?.hookEnv ?? {};
const { env: imageAnalysisEnv, warning: imageAnalysisWarning } =
await resolveCursorImageAnalysisEnv();
const env = stripClaudeCodeEnv({
@@ -184,7 +192,7 @@ export async function executeCursorProfile(
syncWebSearchMcpToConfigDir(claudeConfigDir);
return new Promise((resolve) => {
const launchArgs = appendThirdPartyWebSearchToolArgs(claudeArgs);
const launchArgs = buildCursorClaudeLaunchArgs(claudeArgs, webSearchLaunch?.enabled ?? true);
const traceEnv = createWebSearchTraceContext({
launcher: 'cursor.executor',
args: launchArgs,
+8 -4
View File
@@ -55,8 +55,8 @@ import {
appendWebSearchTrace,
createWebSearchTraceContext,
ensureWebSearchMcpForLaunch,
getWebSearchHookEnv,
readWebSearchTraceRecords,
resolveWebSearchLaunchState,
syncWebSearchMcpToConfigDir,
} from '../utils/websearch-manager';
import { getCcsDir, getGlobalEnvConfig, loadSettings } from '../config/config-loader-facade';
@@ -159,7 +159,8 @@ export class HeadlessExecutor {
);
}
ensureWebSearchMcpForLaunch();
const webSearchLaunch = resolveWebSearchLaunchState();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
@@ -257,6 +258,7 @@ export class HeadlessExecutor {
let runtimeEnvVars: NodeJS.ProcessEnv = {
...stripAnthropicRoutingEnv({ ...globalEnv, ...settingsEnv }, settingsEnv),
...webSearchLaunch.hookEnv,
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
CCS_PROFILE_TYPE: 'settings',
CCS_STRIP_INHERITED_ANTHROPIC_ENV: '1',
@@ -380,7 +382,10 @@ export class HeadlessExecutor {
const imageAnalysisArgs = imageAnalysisMcpReady
? appendThirdPartyImageAnalysisToolArgs(args)
: args;
const launchArgs = appendThirdPartyWebSearchToolArgs(imageAnalysisArgs);
const launchArgs = appendThirdPartyWebSearchToolArgs(
imageAnalysisArgs,
webSearchLaunch.enabled
);
const traceEnv = createWebSearchTraceContext({
launcher: 'delegation.headless-executor',
args: launchArgs,
@@ -467,7 +472,6 @@ export class HeadlessExecutor {
const cleanEnv = stripClaudeCodeEnv({
...stripAnthropicRoutingEnv(process.env),
...getClaudeLaunchEnvOverrides(),
...getWebSearchHookEnv(),
...runtimeEnvVars,
...imageAnalysisEnv,
...traceEnv,
+8 -3
View File
@@ -5,7 +5,10 @@
*/
import { fail, info } from '../../utils/ui';
import { ensureWebSearchMcpForLaunch } from '../../utils/websearch-manager';
import {
ensureWebSearchMcpForLaunch,
resolveWebSearchLaunchState,
} from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import {
ensureProfileHooks as ensureImageAnalyzerHooks,
@@ -23,7 +26,8 @@ export async function runCopilotFlow(ctx: ProfileDispatchContext): Promise<void>
resolveProfileContinuityInheritance,
} = ctx;
ensureWebSearchMcpForLaunch();
const webSearchLaunch = resolveWebSearchLaunchState();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
if (resolvedTarget === 'claude') {
if (imageAnalysisMcpReady) {
@@ -60,7 +64,8 @@ export async function runCopilotFlow(ctx: ProfileDispatchContext): Promise<void>
copilotConfig,
remainingArgs,
continuityInheritance.claudeConfigDir,
claudeCli
claudeCli,
webSearchLaunch
);
process.exit(exitCode);
}
+8 -3
View File
@@ -5,7 +5,10 @@
*/
import { fail, info } from '../../utils/ui';
import { ensureWebSearchMcpForLaunch } from '../../utils/websearch-manager';
import {
ensureWebSearchMcpForLaunch,
resolveWebSearchLaunchState,
} from '../../utils/websearch-manager';
import { ensureProfileHooks as ensureImageAnalyzerHooks } from '../../utils/hooks/image-analyzer-profile-hook-injector';
import { installImageAnalyzerHook } from '../../utils/hooks';
import type { ProfileDispatchContext } from '../dispatcher-context';
@@ -19,7 +22,8 @@ export async function runCursorFlow(ctx: ProfileDispatchContext): Promise<void>
resolveProfileContinuityInheritance,
} = ctx;
ensureWebSearchMcpForLaunch();
const webSearchLaunch = resolveWebSearchLaunchState();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
installImageAnalyzerHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
@@ -48,7 +52,8 @@ export async function runCursorFlow(ctx: ProfileDispatchContext): Promise<void>
cursorConfig,
remainingArgs,
continuityInheritance.claudeConfigDir,
claudeCli
claudeCli,
webSearchLaunch
);
process.exit(exitCode);
}
+13 -11
View File
@@ -16,10 +16,10 @@ import {
import {
ensureWebSearchMcpForLaunch,
displayWebSearchStatus,
getWebSearchHookEnv,
syncWebSearchMcpToConfigDir,
appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext,
resolveWebSearchLaunchState,
} from '../../utils/websearch-manager';
import {
ensureImageAnalysisMcpOrThrow,
@@ -94,18 +94,16 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
if (browserAttachRuntime?.warning) {
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
}
let shouldDisplayWebSearchStatus = true;
const webSearchLaunch = resolveWebSearchLaunchState();
if (resolvedTarget === 'claude') {
shouldDisplayWebSearchStatus = ensureWebSearchMcpForLaunch();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow();
}
}
// Display WebSearch status (single line, equilibrium UX)
if (shouldDisplayWebSearchStatus) {
displayWebSearchStatus();
}
displayWebSearchStatus(webSearchLaunch.config);
const continuityInheritance =
resolvedTarget === 'claude'
@@ -275,7 +273,7 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
targetRemainingArgs,
});
const webSearchEnv = getWebSearchHookEnv();
const webSearchEnv = webSearchLaunch.hookEnv;
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
const globalEnvConfig = getGlobalEnvConfig();
@@ -395,11 +393,11 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
? stripClaudeSubcommandSessionArgs(browserArgs)
: browserArgs;
const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(subcommandArgs)
? appendThirdPartyWebSearchToolArgs(subcommandArgs, webSearchLaunch.enabled)
: [
'--settings',
launchSettings.settingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs),
...appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled),
];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile.proxy',
@@ -418,8 +416,12 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
const isSubcommand = isClaudeSubcommandInvocation(browserArgs);
const subcommandArgs = isSubcommand ? stripClaudeSubcommandSessionArgs(browserArgs) : browserArgs;
const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(subcommandArgs)
: ['--settings', expandedSettingsPath, ...appendThirdPartyWebSearchToolArgs(browserArgs)];
? appendThirdPartyWebSearchToolArgs(subcommandArgs, webSearchLaunch.enabled)
: [
'--settings',
expandedSettingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled),
];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile',
args: launchArgs,
+2
View File
@@ -73,6 +73,8 @@ export {
// Re-export Claude launch arg helpers
export { appendThirdPartyWebSearchToolArgs } from './websearch/claude-tool-args';
export { resolveWebSearchLaunchState } from './websearch/launch-state';
export type { WebSearchConfigSnapshot, WebSearchLaunchState } from './websearch/launch-state';
// Re-export trace helpers
export {
+7 -2
View File
@@ -135,9 +135,14 @@ function ensureWebSearchSteeringPrompt(args: string[]): string[] {
return [...optionArgs, ...steeringArgs, ...trailingArgs];
}
export function appendThirdPartyWebSearchToolArgs(args: string[]): string[] {
export function appendThirdPartyWebSearchToolArgs(
args: string[],
webSearchEnabled = true
): string[] {
// Claude subcommands (agents, doctor, mcp, ...) reject top-level session flags
// like `--append-system-prompt` and `--disallowedTools`. Issue #1218.
if (isClaudeSubcommandInvocation(args)) return args;
return ensureWebSearchSteeringPrompt(ensureDisallowedNativeWebSearchTool(args));
const disallowedArgs = ensureDisallowedNativeWebSearchTool(args);
return webSearchEnabled ? ensureWebSearchSteeringPrompt(disallowedArgs) : disallowedArgs;
}
+4 -2
View File
@@ -14,6 +14,7 @@ import { getCcsHooksDir } from '../config-manager';
import { getClaudeSettingsPath } from '../claude-config-path';
import { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils';
import { getWebSearchConfig } from '../../config/config-loader-facade';
import type { WebSearchConfigSnapshot } from './launch-state';
// Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
@@ -35,9 +36,10 @@ export function getHookPath(): string {
* Get WebSearch hook configuration for settings.json
* Timeout is computed from max provider timeout in config.yaml + buffer
*/
export function getWebSearchHookConfig(): Record<string, unknown> {
export function getWebSearchHookConfig(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): Record<string, unknown> {
const hookPath = getHookPath();
const wsConfig = getWebSearchConfig();
// Compute max timeout from enabled providers
const timeouts: number[] = [];
+4 -2
View File
@@ -9,6 +9,7 @@
import { normalizeSearxngBaseUrl } from './types';
import { resolveAllowedWebSearchTraceFile } from './trace';
import { getWebSearchConfig } from '../../config/config-loader-facade';
import type { WebSearchConfigSnapshot } from './launch-state';
/**
* Get environment variables for WebSearch hook configuration.
@@ -17,8 +18,9 @@ import { getWebSearchConfig } from '../../config/config-loader-facade';
*
* @returns Record of environment variables to set before spawning Claude
*/
export function getWebSearchHookEnv(): Record<string, string> {
const wsConfig = getWebSearchConfig();
export function getWebSearchHookEnv(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): Record<string, string> {
const env: Record<string, string> = {
CCS_WEBSEARCH_ENABLED: '0',
CCS_WEBSEARCH_SKIP: '0',
+4 -3
View File
@@ -13,6 +13,7 @@ import { info, warn } from '../ui';
import { getCcsDir, getCcsHooksDir } from '../config-manager';
import { getHookPath } from './hook-config';
import { getWebSearchConfig } from '../../config/config-loader-facade';
import type { WebSearchConfigSnapshot } from './launch-state';
// Re-export from hook-config for backward compatibility
export { getHookPath, getWebSearchHookConfig } from './hook-config';
@@ -76,10 +77,10 @@ export function hasWebSearchHook(): boolean {
*
* @returns true if hook installed successfully
*/
export function installWebSearchHook(): boolean {
export function installWebSearchHook(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): boolean {
try {
const wsConfig = getWebSearchConfig();
// Skip if disabled
if (!wsConfig.enabled) {
if (process.env.CCS_DEBUG) {
+2
View File
@@ -71,6 +71,8 @@ export {
// Claude launch args
export { appendThirdPartyWebSearchToolArgs } from './claude-tool-args';
export { resolveWebSearchLaunchState } from './launch-state';
export type { WebSearchConfigSnapshot, WebSearchLaunchState } from './launch-state';
// Trace helpers
export {
+19
View File
@@ -0,0 +1,19 @@
import { getWebSearchConfig } from '../../config/config-loader-facade';
import { getWebSearchHookEnv } from './hook-env';
export type WebSearchConfigSnapshot = ReturnType<typeof getWebSearchConfig>;
export interface WebSearchLaunchState {
config: WebSearchConfigSnapshot;
enabled: boolean;
hookEnv: Record<string, string>;
}
export function resolveWebSearchLaunchState(): WebSearchLaunchState {
const config = getWebSearchConfig();
return {
config,
enabled: config.enabled,
hookEnv: getWebSearchHookEnv(config),
};
}
+19 -15
View File
@@ -16,6 +16,7 @@ import {
isClaudeUserConfigLockUnavailableError as isLockUnavailableError,
withClaudeUserConfigLock,
} from '../claude-user-config-lock';
import type { WebSearchConfigSnapshot } from './launch-state';
const WEBSEARCH_MCP_SERVER = 'ccs-websearch-server.cjs';
const WEBSEARCH_MCP_SERVER_NAME = 'ccs-websearch';
@@ -189,14 +190,15 @@ function removeManagedServerConfig(configPath: string): boolean {
}
}
export function installWebSearchMcpServer(): boolean {
const wsConfig = getWebSearchConfig();
export function installWebSearchMcpServer(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): boolean {
if (!wsConfig.enabled) {
appendWebSearchTrace('websearch_mcp_install_skipped', { reason: 'disabled' });
return false;
}
if (!installWebSearchHook()) {
if (!installWebSearchHook(wsConfig)) {
appendWebSearchTrace('websearch_mcp_install_failed', { reason: 'hook_unavailable' });
if (process.env.CCS_DEBUG) {
process.stderr.write(
@@ -267,8 +269,9 @@ export function installWebSearchMcpServer(): boolean {
}
}
export function ensureWebSearchMcpConfig(): boolean {
const wsConfig = getWebSearchConfig();
export function ensureWebSearchMcpConfig(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): boolean {
if (!wsConfig.enabled) {
appendWebSearchTrace('websearch_mcp_config_skipped', { reason: 'disabled' });
return false;
@@ -366,15 +369,16 @@ export function ensureWebSearchMcpConfig(): boolean {
}
}
export function ensureWebSearchMcp(): boolean {
const wsConfig = getWebSearchConfig();
export function ensureWebSearchMcp(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): boolean {
if (!wsConfig.enabled) {
appendWebSearchTrace('websearch_mcp_ensure_skipped', { reason: 'disabled' });
return false;
}
const installed = installWebSearchMcpServer();
const configured = installed && ensureWebSearchMcpConfig();
const installed = installWebSearchMcpServer(wsConfig);
const configured = installed && ensureWebSearchMcpConfig(wsConfig);
appendWebSearchTrace('websearch_mcp_ensure_result', { installed, configured });
return installed && configured;
}
@@ -428,13 +432,14 @@ export function uninstallWebSearchMcp(): boolean {
return removedConfig || removedServer;
}
export function ensureWebSearchMcpOrThrow(): void {
const wsConfig = getWebSearchConfig();
export function ensureWebSearchMcpOrThrow(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): void {
if (!wsConfig.enabled) {
return;
}
if (!ensureWebSearchMcp()) {
if (!ensureWebSearchMcp(wsConfig)) {
throw new Error('WebSearch is enabled, but CCS could not prepare the local WebSearch tool.');
}
}
@@ -447,7 +452,6 @@ export function ensureWebSearchMcpOrThrow(): void {
* native WebSearch and inject fallback steering while the constrained MCP
* search path is unavailable.
*/
export function ensureWebSearchMcpForLaunch(): boolean {
ensureWebSearchMcpOrThrow();
return true;
export function ensureWebSearchMcpForLaunch(wsConfig: WebSearchConfigSnapshot): void {
ensureWebSearchMcpOrThrow(wsConfig);
}
+14 -10
View File
@@ -18,6 +18,7 @@ import { getOpenCodeCliStatus } from './opencode-cli';
import { getWebSearchApiKeyStates } from './provider-secrets';
import { normalizeSearxngBaseUrl, type WebSearchCliInfo, type WebSearchStatus } from './types';
import { getWebSearchConfig } from '../../config/config-loader-facade';
import type { WebSearchConfigSnapshot } from './launch-state';
const PROVIDER_STATE_FILE = 'websearch-provider-state.json';
@@ -112,8 +113,7 @@ function applyCooldownStatus(
};
}
function getLegacyProviderStatuses(): WebSearchCliInfo[] {
const wsConfig = getWebSearchConfig();
function getLegacyProviderStatuses(wsConfig: WebSearchConfigSnapshot): WebSearchCliInfo[] {
const agyStatus = getAgyCliStatus();
const geminiStatus = getGeminiCliStatus();
const grokStatus = getGrokCliStatus();
@@ -197,8 +197,9 @@ function getLegacyProviderStatuses(): WebSearchCliInfo[] {
/**
* Get all WebSearch providers with their current status.
*/
export function getWebSearchCliProviders(): WebSearchCliInfo[] {
const wsConfig = getWebSearchConfig();
export function getWebSearchCliProviders(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): WebSearchCliInfo[] {
const apiKeyStates = getWebSearchApiKeyStates();
const cooldowns = readProviderCooldowns();
const providers: WebSearchCliInfo[] = [
@@ -283,7 +284,7 @@ export function getWebSearchCliProviders(): WebSearchCliInfo[] {
},
];
return [...providers, ...getLegacyProviderStatuses()].map((provider) =>
return [...providers, ...getLegacyProviderStatuses(wsConfig)].map((provider) =>
applyCooldownStatus(provider, cooldowns)
);
}
@@ -355,17 +356,20 @@ export function buildWebSearchReadiness(
/**
* Get WebSearch readiness status for display.
*/
export function getWebSearchReadiness(): WebSearchStatus {
const wsConfig = getWebSearchConfig();
const providers = getWebSearchCliProviders();
export function getWebSearchReadiness(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): WebSearchStatus {
const providers = getWebSearchCliProviders(wsConfig);
return buildWebSearchReadiness(wsConfig.enabled, providers);
}
/**
* Display WebSearch status (single line, equilibrium UX).
*/
export function displayWebSearchStatus(): void {
const status = getWebSearchReadiness();
export function displayWebSearchStatus(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): void {
const status = getWebSearchReadiness(wsConfig);
switch (status.readiness) {
case 'ready':