/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import * as fs from 'fs'; import * as path from 'path'; import { homedir } from 'node:os'; import yargs from 'yargs/yargs'; import { hideBin } from 'yargs/helpers'; import process from 'node:process'; import { mcpCommand } from '../commands/mcp.js'; import { Config, loadServerHierarchicalMemory, setGeminiMdFilename as setServerGeminiMdFilename, getCurrentGeminiMdFilename, ApprovalMode, DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL, DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, FileDiscoveryService, TelemetryTarget, FileFilteringOptions, ShellTool, EditTool, WriteFileTool, MCPServerConfig, } from '@google/gemini-cli-core'; import { Settings } from './settings.js'; import { Extension, annotateActiveExtensions } from './extension.js'; import { getCliVersion } from '../utils/version.js'; import { loadSandboxConfig } from './sandboxConfig.js'; import { resolvePath } from '../utils/resolvePath.js'; import { isWorkspaceTrusted } from './trustedFolders.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), }; export interface CliArgs { model: string | undefined; sandbox: boolean | string | undefined; sandboxImage: string | undefined; debug: boolean | undefined; prompt: string | undefined; promptInteractive: string | undefined; allFiles: boolean | undefined; all_files: boolean | undefined; showMemoryUsage: boolean | undefined; show_memory_usage: boolean | undefined; yolo: boolean | undefined; approvalMode: string | undefined; telemetry: boolean | undefined; checkpointing: boolean | undefined; telemetryTarget: string | undefined; telemetryOtlpEndpoint: string | undefined; telemetryOtlpProtocol: string | undefined; telemetryLogPrompts: boolean | undefined; telemetryOutfile: string | undefined; allowedMcpServerNames: string[] | undefined; experimentalAcp: boolean | undefined; extensions: string[] | undefined; listExtensions: boolean | undefined; proxy: string | undefined; includeDirectories: string[] | undefined; screenReader: boolean | undefined; } export async function parseArguments(): Promise { const yargsInstance = yargs(hideBin(process.argv)) .scriptName('gemini') .usage( 'Usage: gemini [options] [command]\n\nGemini CLI - Launch an interactive CLI, use -p/--prompt for non-interactive mode', ) .command('$0', 'Launch Gemini CLI', (yargsInstance) => yargsInstance .option('model', { alias: 'm', type: 'string', description: `Model`, default: process.env['GEMINI_MODEL'], }) .option('prompt', { alias: 'p', type: 'string', description: 'Prompt. Appended to input on stdin (if any).', }) .option('prompt-interactive', { alias: 'i', type: 'string', description: 'Execute the provided prompt and continue in interactive mode', }) .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('all_files', { type: 'boolean', description: 'Include ALL files in context?', default: false, }) .deprecateOption( 'all_files', 'Use --all-files instead. We will be removing --all_files in the coming weeks.', ) .option('show-memory-usage', { type: 'boolean', description: 'Show memory usage in status bar', default: false, }) .option('show_memory_usage', { type: 'boolean', description: 'Show memory usage in status bar', default: false, }) .deprecateOption( 'show_memory_usage', 'Use --show-memory-usage instead. We will be removing --show_memory_usage in the coming weeks.', ) .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('approval-mode', { type: 'string', choices: ['default', 'auto_edit', 'yolo'], description: 'Set the approval mode: default (prompt for approval), auto_edit (auto-approve edit tools), yolo (auto-approve all tools)', }) .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-otlp-protocol', { type: 'string', choices: ['grpc', 'http'], description: 'Set the OTLP protocol for telemetry (grpc or http). Overrides settings files.', }) .option('telemetry-log-prompts', { type: 'boolean', description: 'Enable or disable logging of user prompts for telemetry. Overrides settings files.', }) .option('telemetry-outfile', { type: 'string', description: 'Redirect all telemetry output to the specified file.', }) .option('checkpointing', { alias: 'c', type: 'boolean', description: 'Enables checkpointing of file edits', default: false, }) .option('experimental-acp', { type: 'boolean', description: 'Starts the agent in ACP mode', }) .option('allowed-mcp-server-names', { type: 'array', string: true, description: 'Allowed MCP server names', }) .option('extensions', { alias: 'e', type: 'array', string: true, description: 'A list of extensions to use. If not provided, all extensions are used.', }) .option('list-extensions', { alias: 'l', type: 'boolean', description: 'List all available extensions and exit.', }) .option('proxy', { type: 'string', description: 'Proxy for gemini client, like schema://user:password@host:port', }) .option('include-directories', { type: 'array', string: true, description: 'Additional directories to include in the workspace (comma-separated or multiple --include-directories)', coerce: (dirs: string[]) => // Handle comma-separated values dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())), }) .option('screen-reader', { type: 'boolean', description: 'Enable screen reader mode for accessibility.', default: false, }) .check((argv) => { if (argv.prompt && argv['promptInteractive']) { throw new Error( 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together', ); } if (argv.yolo && argv['approvalMode']) { throw new Error( 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.', ); } return true; }), ) // Register MCP subcommands .command(mcpCommand) .version(await getCliVersion()) // This will enable the --version flag based on package.json .alias('v', 'version') .help() .alias('h', 'help') .strict() .demandCommand(0, 0); // Allow base command to run with no subcommands yargsInstance.wrap(yargsInstance.terminalWidth()); const result = await yargsInstance.parse(); // Handle case where MCP subcommands are executed - they should exit the process // and not return to main CLI logic if (result._.length > 0 && result._[0] === 'mcp') { // MCP commands handle their own execution and process exit process.exit(0); } // The import format is now only controlled by settings.memoryImportFormat // We no longer accept it as a CLI argument return result as unknown as CliArgs; } // 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, includeDirectoriesToReadGemini: readonly string[] = [], debugMode: boolean, fileService: FileDiscoveryService, settings: Settings, extensionContextFilePaths: string[] = [], memoryImportFormat: 'flat' | 'tree' = 'tree', fileFilteringOptions?: FileFilteringOptions, ): Promise<{ memoryContent: string; fileCount: number }> { // FIX: Use real, canonical paths for a reliable comparison to handle symlinks. const realCwd = fs.realpathSync(path.resolve(currentWorkingDirectory)); const realHome = fs.realpathSync(path.resolve(homedir())); const isHomeDirectory = realCwd === realHome; // If it is the home directory, pass an empty string to the core memory // function to signal that it should skip the workspace search. const effectiveCwd = isHomeDirectory ? '' : currentWorkingDirectory; if (debugMode) { logger.debug( `CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory} (memoryImportFormat: ${memoryImportFormat})`, ); } // Directly call the server function with the corrected path. return loadServerHierarchicalMemory( effectiveCwd, includeDirectoriesToReadGemini, debugMode, fileService, extensionContextFilePaths, memoryImportFormat, fileFilteringOptions, settings.memoryDiscoveryMaxDirs, ); } export async function loadCliConfig( settings: Settings, extensions: Extension[], sessionId: string, argv: CliArgs, cwd: string = process.cwd(), ): Promise { const debugMode = argv.debug || [process.env['DEBUG'], process.env['DEBUG_MODE']].some( (v) => v === 'true' || v === '1', ) || false; const memoryImportFormat = settings.memoryImportFormat || 'tree'; const ideMode = settings.ideMode ?? false; const folderTrustFeature = settings.folderTrustFeature ?? false; const folderTrustSetting = settings.folderTrust ?? true; const folderTrust = folderTrustFeature && folderTrustSetting; const trustedFolder = isWorkspaceTrusted(settings); const allExtensions = annotateActiveExtensions( extensions, argv.extensions || [], ); const activeExtensions = extensions.filter( (_, i) => allExtensions[i].isActive, ); // 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 = activeExtensions.flatMap( (e) => e.contextFiles, ); const fileService = new FileDiscoveryService(cwd); const fileFiltering = { ...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, ...settings.fileFiltering, }; const includeDirectories = (settings.includeDirectories || []) .map(resolvePath) .concat((argv.includeDirectories || []).map(resolvePath)); // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( cwd, settings.loadMemoryFromIncludeDirectories ? includeDirectories : [], debugMode, fileService, settings, extensionContextFilePaths, memoryImportFormat, fileFiltering, ); let mcpServers = mergeMcpServers(settings, activeExtensions); const question = argv.promptInteractive || argv.prompt || ''; // Determine approval mode with backward compatibility let approvalMode: ApprovalMode; if (argv.approvalMode) { // New --approval-mode flag takes precedence switch (argv.approvalMode) { case 'yolo': approvalMode = ApprovalMode.YOLO; break; case 'auto_edit': approvalMode = ApprovalMode.AUTO_EDIT; break; case 'default': approvalMode = ApprovalMode.DEFAULT; break; default: throw new Error( `Invalid approval mode: ${argv.approvalMode}. Valid values are: yolo, auto_edit, default`, ); } } else { // Fallback to legacy --yolo flag behavior approvalMode = argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT; } const interactive = !!argv.promptInteractive || (process.stdin.isTTY && question.length === 0); // In non-interactive mode, exclude tools that require a prompt. const extraExcludes: string[] = []; if (!interactive && !argv.experimentalAcp) { switch (approvalMode) { case ApprovalMode.DEFAULT: // In default non-interactive mode, all tools that require approval are excluded. extraExcludes.push(ShellTool.Name, EditTool.Name, WriteFileTool.Name); break; case ApprovalMode.AUTO_EDIT: // In auto-edit non-interactive mode, only tools that still require a prompt are excluded. extraExcludes.push(ShellTool.Name); break; case ApprovalMode.YOLO: // No extra excludes for YOLO mode. break; default: // This should never happen due to validation earlier, but satisfies the linter break; } } const excludeTools = mergeExcludeTools( settings, activeExtensions, extraExcludes.length > 0 ? extraExcludes : undefined, ); const blockedMcpServers: Array<{ name: string; extensionName: string }> = []; if (!argv.allowedMcpServerNames) { if (settings.allowMCPServers) { mcpServers = allowedMcpServers( mcpServers, settings.allowMCPServers, blockedMcpServers, ); } if (settings.excludeMCPServers) { const excludedNames = new Set(settings.excludeMCPServers.filter(Boolean)); if (excludedNames.size > 0) { mcpServers = Object.fromEntries( Object.entries(mcpServers).filter(([key]) => !excludedNames.has(key)), ); } } } if (argv.allowedMcpServerNames) { mcpServers = allowedMcpServers( mcpServers, argv.allowedMcpServerNames, blockedMcpServers, ); } const sandboxConfig = await loadSandboxConfig(settings, argv); // The screen reader argument takes precedence over the accessibility setting. const screenReader = argv.screenReader ?? settings.accessibility?.screenReader ?? false; return new Config({ sessionId, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, sandbox: sandboxConfig, targetDir: cwd, includeDirectories, loadMemoryFromIncludeDirectories: settings.loadMemoryFromIncludeDirectories || false, debugMode, question, fullContext: argv.allFiles || argv.all_files || false, coreTools: settings.coreTools || undefined, excludeTools, toolDiscoveryCommand: settings.toolDiscoveryCommand, toolCallCommand: settings.toolCallCommand, mcpServerCommand: settings.mcpServerCommand, mcpServers, userMemory: memoryContent, geminiMdFileCount: fileCount, approvalMode, showMemoryUsage: argv.showMemoryUsage || argv.show_memory_usage || settings.showMemoryUsage || false, accessibility: { ...settings.accessibility, screenReader, }, 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, otlpProtocol: (['grpc', 'http'] as const).find( (p) => p === (argv.telemetryOtlpProtocol ?? settings.telemetry?.otlpProtocol), ), logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts, outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile, }, usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true, // Git-aware file filtering settings fileFiltering: { respectGitIgnore: settings.fileFiltering?.respectGitIgnore, respectGeminiIgnore: settings.fileFiltering?.respectGeminiIgnore, enableRecursiveFileSearch: settings.fileFiltering?.enableRecursiveFileSearch, }, checkpointing: argv.checkpointing || settings.checkpointing?.enabled, proxy: argv.proxy || process.env['HTTPS_PROXY'] || process.env['https_proxy'] || process.env['HTTP_PROXY'] || process.env['http_proxy'], cwd, fileDiscoveryService: fileService, bugCommand: settings.bugCommand, model: argv.model || settings.model || DEFAULT_GEMINI_MODEL, extensionContextFilePaths, maxSessionTurns: settings.maxSessionTurns ?? -1, experimentalZedIntegration: argv.experimentalAcp || false, listExtensions: argv.listExtensions || false, extensions: allExtensions, blockedMcpServers, noBrowser: !!process.env['NO_BROWSER'], summarizeToolOutput: settings.summarizeToolOutput, ideMode, chatCompression: settings.chatCompression, folderTrustFeature, folderTrust, interactive, trustedFolder, shouldUseNodePtyShell: settings.shouldUseNodePtyShell, skipNextSpeakerCheck: settings.skipNextSpeakerCheck, enablePromptCompletion: settings.enablePromptCompletion ?? false, }); } function allowedMcpServers( mcpServers: { [x: string]: MCPServerConfig }, allowMCPServers: string[], blockedMcpServers: Array<{ name: string; extensionName: string }>, ) { const allowedNames = new Set(allowMCPServers.filter(Boolean)); if (allowedNames.size > 0) { mcpServers = Object.fromEntries( Object.entries(mcpServers).filter(([key, server]) => { const isAllowed = allowedNames.has(key); if (!isAllowed) { blockedMcpServers.push({ name: key, extensionName: server.extensionName || '', }); } return isAllowed; }), ); } else { blockedMcpServers.push( ...Object.entries(mcpServers).map(([key, server]) => ({ name: key, extensionName: server.extensionName || '', })), ); mcpServers = {}; } return mcpServers; } 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, extensionName: extension.config.name, }; }, ); } return mcpServers; } function mergeExcludeTools( settings: Settings, extensions: Extension[], extraExcludes?: string[] | undefined, ): string[] { const allExcludeTools = new Set([ ...(settings.excludeTools || []), ...(extraExcludes || []), ]); for (const extension of extensions) { for (const tool of extension.config.excludeTools || []) { allExcludeTools.add(tool); } } return [...allExcludeTools]; }