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,
|
|
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 });
|
|
}
|
|
}
|