/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import yargs from 'yargs/yargs'; import { hideBin } from 'yargs/helpers'; import process from 'node:process'; import { Config, loadServerHierarchicalMemory, setGeminiMdFilename as setServerGeminiMdFilename, getCurrentGeminiMdFilename, ApprovalMode, GEMINI_CONFIG_DIR as GEMINI_DIR, DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL, FileDiscoveryService, TelemetryTarget, } from '@gemini-cli/core'; import { Settings } from './settings.js'; import { Extension } from './extension.js'; import { getCliVersion } from '../utils/version.js'; import * as dotenv from 'dotenv'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { loadSandboxConfig } from './sandboxConfig.js'; // Simple console logger for now - replace with actual logger if available const logger = { // eslint-disable-next-line @typescript-eslint/no-explicit-any debug: (...args: any[]) => console.debug('[DEBUG]', ...args), // eslint-disable-next-line @typescript-eslint/no-explicit-any warn: (...args: any[]) => console.warn('[WARN]', ...args), // eslint-disable-next-line @typescript-eslint/no-explicit-any error: (...args: any[]) => console.error('[ERROR]', ...args), }; interface CliArgs { model: string | undefined; sandbox: boolean | string | undefined; 'sandbox-image': string | undefined; debug: boolean | undefined; prompt: string | undefined; all_files: boolean | undefined; show_memory_usage: boolean | undefined; yolo: boolean | undefined; telemetry: boolean | undefined; checkpointing: boolean | undefined; telemetryTarget: string | undefined; telemetryOtlpEndpoint: string | undefined; telemetryLogPrompts: boolean | undefined; } async function parseArguments(): Promise { const argv = await yargs(hideBin(process.argv)) .option('model', { alias: 'm', type: 'string', description: `Model`, default: process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL, }) .option('prompt', { alias: 'p', type: 'string', description: 'Prompt. Appended to input on stdin (if any).', }) .option('sandbox', { alias: 's', type: 'boolean', description: 'Run in sandbox?', }) .option('sandbox-image', { type: 'string', description: 'Sandbox image URI.', }) .option('debug', { alias: 'd', type: 'boolean', description: 'Run in debug mode?', default: false, }) .option('all_files', { alias: 'a', type: 'boolean', description: 'Include ALL files in context?', default: false, }) .option('show_memory_usage', { type: 'boolean', description: 'Show memory usage in status bar', default: false, }) .option('yolo', { alias: 'y', type: 'boolean', description: 'Automatically accept all actions (aka YOLO mode, see https://www.youtube.com/watch?v=xvFZjo5PgG0 for more details)?', default: false, }) .option('telemetry', { type: 'boolean', description: 'Enable telemetry? This flag specifically controls if telemetry is sent. Other --telemetry-* flags set specific values but do not enable telemetry on their own.', }) .option('telemetry-target', { type: 'string', choices: ['local', 'gcp'], description: 'Set the telemetry target (local or gcp). Overrides settings files.', }) .option('telemetry-otlp-endpoint', { type: 'string', description: 'Set the OTLP endpoint for telemetry. Overrides environment variables and settings files.', }) .option('telemetry-log-prompts', { type: 'boolean', description: 'Enable or disable logging of user prompts for telemetry. Overrides settings files.', }) .option('checkpointing', { alias: 'c', type: 'boolean', description: 'Enables checkpointing of file edits', default: false, }) .version(await getCliVersion()) // This will enable the --version flag based on package.json .alias('v', 'version') .help() .alias('h', 'help') .strict().argv; return argv; } // This function is now a thin wrapper around the server's implementation. // It's kept in the CLI for now as App.tsx directly calls it for memory refresh. // TODO: Consider if App.tsx should get memory via a server call or if Config should refresh itself. export async function loadHierarchicalGeminiMemory( currentWorkingDirectory: string, debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], ): Promise<{ memoryContent: string; fileCount: number }> { if (debugMode) { logger.debug( `CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory}`, ); } // Directly call the server function. // The server function will use its own homedir() for the global path. return loadServerHierarchicalMemory( currentWorkingDirectory, debugMode, fileService, extensionContextFilePaths, ); } export async function loadCliConfig( settings: Settings, extensions: Extension[], sessionId: string, ): Promise { loadEnvironment(); const argv = await parseArguments(); const debugMode = argv.debug || false; // Set the context filename in the server's memoryTool module BEFORE loading memory // TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed // directly to the Config constructor in core, and have core handle setGeminiMdFilename. // However, loadHierarchicalGeminiMemory is called *before* createServerConfig. if (settings.contextFileName) { setServerGeminiMdFilename(settings.contextFileName); } else { // Reset to default if not provided in settings. setServerGeminiMdFilename(getCurrentGeminiMdFilename()); } const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles); const fileService = new FileDiscoveryService(process.cwd()); // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( process.cwd(), debugMode, fileService, extensionContextFilePaths, ); const mcpServers = mergeMcpServers(settings, extensions); const sandboxConfig = await loadSandboxConfig(settings, argv); return new Config({ sessionId, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, sandbox: sandboxConfig, targetDir: process.cwd(), debugMode, question: argv.prompt || '', fullContext: argv.all_files || false, coreTools: settings.coreTools || undefined, excludeTools: settings.excludeTools || undefined, toolDiscoveryCommand: settings.toolDiscoveryCommand, toolCallCommand: settings.toolCallCommand, mcpServerCommand: settings.mcpServerCommand, mcpServers, userMemory: memoryContent, geminiMdFileCount: fileCount, approvalMode: argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT, showMemoryUsage: argv.show_memory_usage || settings.showMemoryUsage || false, accessibility: settings.accessibility, telemetry: { enabled: argv.telemetry ?? settings.telemetry?.enabled, target: (argv.telemetryTarget ?? settings.telemetry?.target) as TelemetryTarget, otlpEndpoint: argv.telemetryOtlpEndpoint ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? settings.telemetry?.otlpEndpoint, logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts, disableDataCollection: settings.telemetry?.disableDataCollection ?? true, }, // Git-aware file filtering settings fileFiltering: { respectGitIgnore: settings.fileFiltering?.respectGitIgnore, enableRecursiveFileSearch: settings.fileFiltering?.enableRecursiveFileSearch, }, checkpointing: argv.checkpointing || settings.checkpointing?.enabled, proxy: process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy, cwd: process.cwd(), fileDiscoveryService: fileService, bugCommand: settings.bugCommand, model: argv.model!, }); } function mergeMcpServers(settings: Settings, extensions: Extension[]) { const mcpServers = { ...(settings.mcpServers || {}) }; for (const extension of extensions) { Object.entries(extension.config.mcpServers || {}).forEach( ([key, server]) => { if (mcpServers[key]) { logger.warn( `Skipping extension MCP config for server with key "${key}" as it already exists.`, ); return; } mcpServers[key] = server; }, ); } return mcpServers; } function findEnvFile(startDir: string): string | null { let currentDir = path.resolve(startDir); while (true) { // prefer gemini-specific .env under GEMINI_DIR const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env'); if (fs.existsSync(geminiEnvPath)) { return geminiEnvPath; } const envPath = path.join(currentDir, '.env'); if (fs.existsSync(envPath)) { return envPath; } const parentDir = path.dirname(currentDir); if (parentDir === currentDir || !parentDir) { // check .env under home as fallback, again preferring gemini-specific .env const homeGeminiEnvPath = path.join(os.homedir(), GEMINI_DIR, '.env'); if (fs.existsSync(homeGeminiEnvPath)) { return homeGeminiEnvPath; } const homeEnvPath = path.join(os.homedir(), '.env'); if (fs.existsSync(homeEnvPath)) { return homeEnvPath; } return null; } currentDir = parentDir; } } export function loadEnvironment(): void { const envFilePath = findEnvFile(process.cwd()); if (envFilePath) { dotenv.config({ path: envFilePath }); } }