|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
|
DEFAULT_QWEN_MODEL, |
|
|
DEFAULT_LOCAL_ENDPOINT, |
|
|
FileDiscoveryService, |
|
|
TelemetryTarget, |
|
|
isLocalModel, |
|
|
} from '@google/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'; |
|
|
|
|
|
|
|
|
const logger = { |
|
|
|
|
|
debug: (...args: any[]) => console.debug('[DEBUG]', ...args), |
|
|
|
|
|
warn: (...args: any[]) => console.warn('[WARN]', ...args), |
|
|
|
|
|
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; |
|
|
'local-endpoint': string | undefined; |
|
|
} |
|
|
|
|
|
async function parseArguments(): Promise<CliArgs> { |
|
|
const argv = await yargs(hideBin(process.argv)) |
|
|
.option('model', { |
|
|
alias: 'm', |
|
|
type: 'string', |
|
|
description: `Model (default: ${DEFAULT_QWEN_MODEL} for local, or specify gemini-2.5-pro for Gemini)`, |
|
|
default: process.env.GEMINI_MODEL || process.env.LOCAL_MODEL || DEFAULT_QWEN_MODEL, |
|
|
}) |
|
|
.option('local-endpoint', { |
|
|
type: 'string', |
|
|
description: 'Local model endpoint (default: http://127.0.0.1:1234)', |
|
|
default: process.env.LOCAL_MODEL_ENDPOINT || DEFAULT_LOCAL_ENDPOINT, |
|
|
}) |
|
|
.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()) |
|
|
.alias('v', 'version') |
|
|
.help() |
|
|
.alias('h', 'help') |
|
|
.strict().argv; |
|
|
|
|
|
return argv; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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}`, |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (settings.contextFileName) { |
|
|
setServerGeminiMdFilename(settings.contextFileName); |
|
|
} else { |
|
|
|
|
|
setServerGeminiMdFilename(getCurrentGeminiMdFilename()); |
|
|
} |
|
|
|
|
|
const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles); |
|
|
|
|
|
|
|
|
const userCwd = process.env.OPENCLI_USER_CWD || process.cwd(); |
|
|
|
|
|
const fileService = new FileDiscoveryService(userCwd); |
|
|
|
|
|
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( |
|
|
userCwd, |
|
|
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: userCwd, |
|
|
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: (() => { |
|
|
const yoloMode = argv.yolo || false ? ApprovalMode.YOLO : |
|
|
|
|
|
(argv.model && isLocalModel(argv.model)) ? ApprovalMode.YOLO : ApprovalMode.DEFAULT; |
|
|
console.log(`🔧 DEBUG: Setting approval mode to: ${yoloMode} (yolo flag: ${argv.yolo}, model: ${argv.model}, isLocal: ${argv.model && isLocalModel(argv.model)})`); |
|
|
return yoloMode; |
|
|
})(), |
|
|
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, |
|
|
|
|
|
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: userCwd, |
|
|
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) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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 userCwd = process.env.OPENCLI_USER_CWD || process.cwd(); |
|
|
const envFilePath = findEnvFile(userCwd); |
|
|
if (envFilePath) { |
|
|
dotenv.config({ path: envFilePath }); |
|
|
} |
|
|
} |
|
|
|