Sync upstream Gemini-CLI v0.8.2 (#838)

This commit is contained in:
tanzhenxin
2025-10-23 09:27:04 +08:00
committed by GitHub
parent 096fabb5d6
commit eb95c131be
644 changed files with 70389 additions and 23709 deletions

View File

@@ -5,16 +5,16 @@
*/
import type {
ConfigParameters,
FileFilteringOptions,
MCPServerConfig,
TelemetryTarget,
OutputFormat,
} from '@qwen-code/qwen-code-core';
import { extensionsCommand } from '../commands/extensions.js';
import {
ApprovalMode,
Config,
DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_GEMINI_MODEL,
DEFAULT_QWEN_MODEL,
DEFAULT_QWEN_EMBEDDING_MODEL,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
EditTool,
FileDiscoveryService,
@@ -23,24 +23,26 @@ import {
setGeminiMdFilename as setServerGeminiMdFilename,
ShellTool,
WriteFileTool,
resolveTelemetrySettings,
FatalConfigError,
} from '@qwen-code/qwen-code-core';
import * as fs from 'node:fs';
import { homedir } from 'node:os';
import * as path from 'node:path';
import process from 'node:process';
import { hideBin } from 'yargs/helpers';
import yargs from 'yargs/yargs';
import { extensionsCommand } from '../commands/extensions.js';
import { mcpCommand } from '../commands/mcp.js';
import type { Settings } from './settings.js';
import yargs, { type Argv } from 'yargs';
import { hideBin } from 'yargs/helpers';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';
import { resolvePath } from '../utils/resolvePath.js';
import { getCliVersion } from '../utils/version.js';
import type { Extension } from './extension.js';
import { annotateActiveExtensions } from './extension.js';
import { loadSandboxConfig } from './sandboxConfig.js';
import { appEvents } from '../utils/events.js';
import { mcpCommand } from '../commands/mcp.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
// Simple console logger for now - replace with actual logger if available
const logger = {
@@ -86,6 +88,7 @@ function parseApprovalModeValue(value: string): ApprovalMode {
}
export interface CliArgs {
query: string | undefined;
model: string | undefined;
sandbox: boolean | string | undefined;
sandboxImage: string | undefined;
@@ -116,23 +119,98 @@ export interface CliArgs {
tavilyApiKey: string | undefined;
screenReader: boolean | undefined;
vlmSwitchMode: string | undefined;
useSmartEdit: boolean | undefined;
outputFormat: string | undefined;
}
export async function parseArguments(settings: Settings): Promise<CliArgs> {
const yargsInstance = yargs(hideBin(process.argv))
// Set locale to English for consistent output, especially in tests
const rawArgv = hideBin(process.argv);
const yargsInstance = yargs(rawArgv)
.locale('en')
.scriptName('qwen')
.usage(
'Usage: qwen [options] [command]\n\nQwen Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode',
)
.command('$0', 'Launch Qwen Code', (yargsInstance) =>
.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.',
})
.deprecateOption(
'telemetry',
'Use the "telemetry.enabled" setting in settings.json instead. This flag will be removed in a future version.',
)
.deprecateOption(
'telemetry-target',
'Use the "telemetry.target" setting in settings.json instead. This flag will be removed in a future version.',
)
.deprecateOption(
'telemetry-otlp-endpoint',
'Use the "telemetry.otlpEndpoint" setting in settings.json instead. This flag will be removed in a future version.',
)
.deprecateOption(
'telemetry-otlp-protocol',
'Use the "telemetry.otlpProtocol" setting in settings.json instead. This flag will be removed in a future version.',
)
.deprecateOption(
'telemetry-log-prompts',
'Use the "telemetry.logPrompts" setting in settings.json instead. This flag will be removed in a future version.',
)
.deprecateOption(
'telemetry-outfile',
'Use the "telemetry.outfile" setting in settings.json instead. This flag will be removed in a future version.',
)
.option('debug', {
alias: 'd',
type: 'boolean',
description: 'Run in debug mode?',
default: false,
})
.option('proxy', {
type: 'string',
description:
'Proxy for gemini client, like schema://user:password@host:port',
})
.deprecateOption(
'proxy',
'Use the "proxy" setting in settings.json instead. This flag will be removed in a future version.',
)
.command('$0 [query..]', 'Launch Gemini CLI', (yargsInstance: Argv) =>
yargsInstance
.positional('query', {
description:
'Positional prompt. Defaults to one-shot; use -i/--prompt-interactive for interactive.',
})
.option('model', {
alias: 'm',
type: 'string',
description: `Model`,
default: process.env['GEMINI_MODEL'],
})
.option('prompt', {
alias: 'p',
@@ -154,12 +232,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
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',
@@ -184,37 +256,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
description:
'Set the approval mode: plan (plan only), 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',
@@ -229,11 +270,19 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
type: 'array',
string: true,
description: 'Allowed MCP server names',
coerce: (mcpServerNames: string[]) =>
// Handle comma-separated values
mcpServerNames.flatMap((mcpServerName) =>
mcpServerName.split(',').map((m) => m.trim()),
),
})
.option('allowed-tools', {
type: 'array',
string: true,
description: 'Tools that are allowed to run without confirmation',
coerce: (tools: string[]) =>
// Handle comma-separated values
tools.flatMap((tool) => tool.split(',').map((t) => t.trim())),
})
.option('extensions', {
alias: 'e',
@@ -241,17 +290,17 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
string: true,
description:
'A list of extensions to use. If not provided, all extensions are used.',
coerce: (extensions: string[]) =>
// Handle comma-separated values
extensions.flatMap((extension) =>
extension.split(',').map((e) => e.trim()),
),
})
.option('list-extensions', {
alias: 'l',
type: 'boolean',
description: 'List all available extensions and exit.',
})
.option('proxy', {
type: 'string',
description:
'Proxy for qwen client, like schema://user:password@host:port',
})
.option('include-directories', {
type: 'array',
string: true,
@@ -281,7 +330,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
.option('screen-reader', {
type: 'boolean',
description: 'Enable screen reader mode for accessibility.',
default: false,
})
.option('vlm-switch-mode', {
type: 'string',
@@ -290,16 +338,54 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
'Default behavior when images are detected in input. Values: once (one-time switch), session (switch for entire session), persist (continue with current model). Overrides settings files.',
default: process.env['VLM_SWITCH_MODE'],
})
.check((argv) => {
if (argv.prompt && argv['promptInteractive']) {
throw new Error(
'Cannot use both --prompt (-p) and --prompt-interactive (-i) together',
);
.option('output-format', {
alias: 'o',
type: 'string',
description: 'The format of the CLI output.',
choices: ['text', 'json'],
})
.deprecateOption(
'show-memory-usage',
'Use the "ui.showMemoryUsage" setting in settings.json instead. This flag will be removed in a future version.',
)
.deprecateOption(
'sandbox-image',
'Use the "tools.sandbox" setting in settings.json instead. This flag will be removed in a future version.',
)
.deprecateOption(
'checkpointing',
'Use the "general.checkpointing.enabled" setting in settings.json instead. This flag will be removed in a future version.',
)
.deprecateOption(
'all-files',
'Use @ includes in the application instead. This flag will be removed in a future version.',
)
.deprecateOption(
'prompt',
'Use the positional prompt instead. This flag will be removed in a future version.',
)
// Ensure validation flows through .fail() for clean UX
.fail((msg: string, err: Error | undefined, yargs: Argv) => {
console.error(msg || err?.message || 'Unknown error');
yargs.showHelp();
process.exit(1);
})
.check((argv: { [x: string]: unknown }) => {
// The 'query' positional can be a string (for one arg) or string[] (for multiple).
// This guard safely checks if any positional argument was provided.
const query = argv['query'] as string | string[] | undefined;
const hasPositionalQuery = Array.isArray(query)
? query.length > 0
: !!query;
if (argv['prompt'] && hasPositionalQuery) {
return 'Cannot use both a positional prompt and the --prompt (-p) flag together';
}
if (argv.yolo && argv['approvalMode']) {
throw new Error(
'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.',
);
if (argv['prompt'] && argv['promptInteractive']) {
return 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together';
}
if (argv['yolo'] && argv['approvalMode']) {
return 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.';
}
return true;
}),
@@ -307,7 +393,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
// Register MCP subcommands
.command(mcpCommand);
if (settings?.experimental?.extensionManagement ?? false) {
if (settings?.experimental?.extensionManagement ?? true) {
yargsInstance.command(extensionsCommand);
}
@@ -322,6 +408,8 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
yargsInstance.wrap(yargsInstance.terminalWidth());
const result = await yargsInstance.parse();
// If yargs handled --help/--version it will have exited; nothing to do here.
// Handle case where MCP subcommands are executed - they should exit the process
// and not return to main CLI logic
if (
@@ -332,6 +420,26 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
process.exit(0);
}
// Normalize query args: handle both quoted "@path file" and unquoted @path file
const queryArg = (result as { query?: string | string[] | undefined }).query;
const q: string | undefined = Array.isArray(queryArg)
? queryArg.join(' ')
: queryArg;
// Route positional args: explicit -i flag -> interactive; else -> one-shot (even for @commands)
if (q && !result['prompt']) {
const hasExplicitInteractive =
result['promptInteractive'] === '' || !!result['promptInteractive'];
if (hasExplicitInteractive) {
result['promptInteractive'] = q;
} else {
result['prompt'] = q;
}
}
// Keep CliArgs.query as a string for downstream typing
(result as Record<string, unknown>)['query'] = q || undefined;
// 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;
@@ -347,6 +455,7 @@ export async function loadHierarchicalGeminiMemory(
fileService: FileDiscoveryService,
settings: Settings,
extensionContextFilePaths: string[] = [],
folderTrust: boolean,
memoryImportFormat: 'flat' | 'tree' = 'tree',
fileFilteringOptions?: FileFilteringOptions,
): Promise<{ memoryContent: string; fileCount: number }> {
@@ -372,58 +481,48 @@ export async function loadHierarchicalGeminiMemory(
debugMode,
fileService,
extensionContextFilePaths,
folderTrust,
memoryImportFormat,
fileFilteringOptions,
settings.context?.discoveryMaxDirs,
);
}
export function isDebugMode(argv: CliArgs): boolean {
return (
argv.debug ||
[process.env['DEBUG'], process.env['DEBUG_MODE']].some(
(v) => v === 'true' || v === '1',
)
);
}
export async function loadCliConfig(
settings: Settings,
extensions: Extension[],
extensionEnablementManager: ExtensionEnablementManager,
sessionId: string,
argv: CliArgs,
cwd: string = process.cwd(),
): Promise<Config> {
const debugMode =
argv.debug ||
[process.env['DEBUG'], process.env['DEBUG_MODE']].some(
(v) => v === 'true' || v === '1',
) ||
false;
const debugMode = isDebugMode(argv);
const memoryImportFormat = settings.context?.importFormat || 'tree';
const ideMode = settings.ide?.enabled ?? false;
const folderTrustFeature =
settings.security?.folderTrust?.featureEnabled ?? false;
const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true;
const folderTrust = folderTrustFeature && folderTrustSetting;
const trustedFolder = isWorkspaceTrusted(settings);
const folderTrust = settings.security?.folderTrust?.enabled ?? false;
const trustedFolder = isWorkspaceTrusted(settings)?.isTrusted ?? true;
const allExtensions = annotateActiveExtensions(
extensions,
argv.extensions || [],
cwd,
extensionEnablementManager,
);
const activeExtensions = extensions.filter(
(_, i) => allExtensions[i].isActive,
);
// Handle OpenAI API key from command line
if (argv.openaiApiKey) {
process.env['OPENAI_API_KEY'] = argv.openaiApiKey;
}
// Handle OpenAI base URL from command line
if (argv.openaiBaseUrl) {
process.env['OPENAI_BASE_URL'] = argv.openaiBaseUrl;
}
// Handle Tavily API key from command line
if (argv.tavilyApiKey) {
process.env['TAVILY_API_KEY'] = argv.tavilyApiKey;
}
// 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
@@ -461,6 +560,7 @@ export async function loadCliConfig(
fileService,
settings,
extensionContextFilePaths,
trustedFolder,
memoryImportFormat,
fileFiltering,
);
@@ -474,8 +574,8 @@ export async function loadCliConfig(
approvalMode = parseApprovalModeValue(argv.approvalMode);
} else if (argv.yolo) {
approvalMode = ApprovalMode.YOLO;
} else if (settings.approvalMode) {
approvalMode = parseApprovalModeValue(settings.approvalMode);
} else if (settings.tools?.approvalMode) {
approvalMode = parseApprovalModeValue(settings.tools.approvalMode);
} else {
approvalMode = ApprovalMode.DEFAULT;
}
@@ -492,8 +592,27 @@ export async function loadCliConfig(
approvalMode = ApprovalMode.DEFAULT;
}
let telemetrySettings;
try {
telemetrySettings = await resolveTelemetrySettings({
argv,
env: process.env as unknown as Record<string, string | undefined>,
settings: settings.telemetry,
});
} catch (err) {
if (err instanceof FatalConfigError) {
throw new FatalConfigError(
`Invalid telemetry configuration: ${err.message}.`,
);
}
throw err;
}
// Interactive mode: explicit -i flag or (TTY + no args + no -p flag)
const hasQuery = !!argv.query;
const interactive =
!!argv.promptInteractive || (process.stdin.isTTY && question.length === 0);
!!argv.promptInteractive ||
(process.stdin.isTTY && !hasQuery && !argv.prompt);
// In non-interactive mode, exclude tools that require a prompt.
const extraExcludes: string[] = [];
if (!interactive && !argv.experimentalAcp) {
@@ -550,9 +669,15 @@ export async function loadCliConfig(
);
}
const sandboxConfig = await loadSandboxConfig(settings, argv);
const cliVersion = await getCliVersion();
const defaultModel = DEFAULT_QWEN_MODEL;
const resolvedModel: string =
argv.model ||
process.env['OPENAI_MODEL'] ||
process.env['QWEN_MODEL'] ||
settings.model?.name ||
defaultModel;
const sandboxConfig = await loadSandboxConfig(settings, argv);
const screenReader =
argv.screenReader !== undefined
? argv.screenReader
@@ -562,7 +687,7 @@ export async function loadCliConfig(
argv.vlmSwitchMode || settings.experimental?.vlmSwitchMode;
return new Config({
sessionId,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
embeddingModel: DEFAULT_QWEN_EMBEDDING_MODEL,
sandbox: sandboxConfig,
targetDir: cwd,
includeDirectories,
@@ -587,31 +712,9 @@ export async function loadCliConfig(
...settings.ui?.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,
},
telemetry: telemetrySettings,
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true,
// Git-aware file filtering settings
fileFiltering: {
respectGitIgnore: settings.context?.fileFiltering?.respectGitIgnore,
respectGeminiIgnore: settings.context?.fileFiltering?.respectGeminiIgnore,
enableRecursiveFileSearch:
settings.context?.fileFiltering?.enableRecursiveFileSearch,
disableFuzzySearch: settings.context?.fileFiltering?.disableFuzzySearch,
},
fileFiltering: settings.context?.fileFiltering,
checkpointing:
argv.checkpointing || settings.general?.checkpointing?.enabled,
proxy:
@@ -623,50 +726,51 @@ export async function loadCliConfig(
cwd,
fileDiscoveryService: fileService,
bugCommand: settings.advanced?.bugCommand,
model: argv.model || settings.model?.name || DEFAULT_GEMINI_MODEL,
model: resolvedModel,
extensionContextFilePaths,
sessionTokenLimit: settings.sessionTokenLimit ?? -1,
sessionTokenLimit: settings.model?.sessionTokenLimit ?? -1,
maxSessionTurns: settings.model?.maxSessionTurns ?? -1,
experimentalZedIntegration: argv.experimentalAcp || false,
listExtensions: argv.listExtensions || false,
extensions: allExtensions,
blockedMcpServers,
noBrowser: !!process.env['NO_BROWSER'],
enableOpenAILogging:
(typeof argv.openaiLogging === 'undefined'
? settings.enableOpenAILogging
: argv.openaiLogging) ?? false,
systemPromptMappings: (settings.systemPromptMappings ?? [
{
baseUrls: [
'https://dashscope.aliyuncs.com/compatible-mode/v1/',
'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/',
],
modelNames: ['qwen3-coder-plus'],
template:
'SYSTEM_TEMPLATE:{"name":"qwen3_coder","params":{"is_git_repository":{RUNTIME_VARS_IS_GIT_REPO},"sandbox":"{RUNTIME_VARS_SANDBOX}"}}',
},
]) as ConfigParameters['systemPromptMappings'],
authType: settings.security?.auth?.selectedType,
contentGenerator: settings.contentGenerator,
cliVersion,
generationConfig: {
...(settings.model?.generationConfig || {}),
model: resolvedModel,
apiKey: argv.openaiApiKey || process.env['OPENAI_API_KEY'],
baseUrl: argv.openaiBaseUrl || process.env['OPENAI_BASE_URL'],
enableOpenAILogging:
(typeof argv.openaiLogging === 'undefined'
? settings.model?.enableOpenAILogging
: argv.openaiLogging) ?? false,
},
cliVersion: await getCliVersion(),
tavilyApiKey:
argv.tavilyApiKey ||
settings.tavilyApiKey ||
settings.advanced?.tavilyApiKey ||
process.env['TAVILY_API_KEY'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode,
chatCompression: settings.model?.chatCompression,
folderTrustFeature,
folderTrust,
interactive,
trustedFolder,
useRipgrep: settings.tools?.useRipgrep,
shouldUseNodePtyShell: settings.tools?.usePty,
shouldUseNodePtyShell: settings.tools?.shell?.enableInteractiveShell,
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
enablePromptCompletion: settings.general?.enablePromptCompletion ?? false,
skipLoopDetection: settings.skipLoopDetection ?? false,
skipLoopDetection: settings.model?.skipLoopDetection ?? false,
vlmSwitchMode,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
truncateToolOutputLines: settings.tools?.truncateToolOutputLines,
enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation,
eventEmitter: appEvents,
useSmartEdit: argv.useSmartEdit ?? settings.useSmartEdit,
output: {
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
},
});
}