This commit refactors the `usageStatisticsEnabled` setting from a sub-property of the `telemetry` configuration to a top-level setting. This change simplifies the configuration by decoupling usage statistics from the telemetry settings. The documentation has also been updated to reflect this change.
302 lines
9.8 KiB
TypeScript
302 lines
9.8 KiB
TypeScript
/**
|
|
* @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<CliArgs> {
|
|
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<Config> {
|
|
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,
|
|
},
|
|
usageStatisticsEnabled: settings.usageStatisticsEnabled ?? 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!,
|
|
extensionContextFilePaths,
|
|
});
|
|
}
|
|
|
|
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 });
|
|
}
|
|
}
|