Merge tag 'v0.1.15' into feature/yiheng/sync-gemini-cli-0.1.15

This commit is contained in:
奕桁
2025-08-01 23:06:11 +08:00
340 changed files with 36528 additions and 22931 deletions

View File

@@ -18,13 +18,12 @@ import {
} from '../core/contentGenerator.js';
import { GeminiClient } from '../core/client.js';
import { GitService } from '../services/gitService.js';
import { loadServerHierarchicalMemory } from '../utils/memoryDiscovery.js';
// Mock dependencies that might be called during Config construction or createServerConfig
vi.mock('../tools/tool-registry', () => {
const ToolRegistryMock = vi.fn();
ToolRegistryMock.prototype.registerTool = vi.fn();
ToolRegistryMock.prototype.discoverTools = vi.fn();
ToolRegistryMock.prototype.discoverAllTools = vi.fn();
ToolRegistryMock.prototype.getAllTools = vi.fn(() => []); // Mock methods if needed
ToolRegistryMock.prototype.getTool = vi.fn();
ToolRegistryMock.prototype.getFunctionDeclarations = vi.fn(() => []);
@@ -48,9 +47,9 @@ vi.mock('../tools/read-many-files');
vi.mock('../tools/memoryTool', () => ({
MemoryTool: vi.fn(),
setGeminiMdFilename: vi.fn(),
getCurrentGeminiMdFilename: vi.fn(() => 'QWEN.md'), // Mock the original filename
DEFAULT_CONTEXT_FILENAME: 'QWEN.md',
GEMINI_CONFIG_DIR: '.qwen',
getCurrentGeminiMdFilename: vi.fn(() => 'GEMINI.md'), // Mock the original filename
DEFAULT_CONTEXT_FILENAME: 'GEMINI.md',
GEMINI_CONFIG_DIR: '.gemini',
}));
vi.mock('../core/contentGenerator.js', async (importOriginal) => {
@@ -93,7 +92,7 @@ describe('Server Config (config.ts)', () => {
const QUESTION = 'test question';
const FULL_CONTEXT = false;
const USER_MEMORY = 'Test User Memory';
const TELEMETRY_SETTINGS = { enabled: true };
const TELEMETRY_SETTINGS = { enabled: false };
const EMBEDDING_MODEL = 'gemini-embedding';
const SESSION_ID = 'test-session-id';
const baseParams: ConfigParameters = {
@@ -151,14 +150,12 @@ describe('Server Config (config.ts)', () => {
apiKey: 'test-key',
};
(createContentGeneratorConfig as Mock).mockResolvedValue(
mockContentConfig,
);
(createContentGeneratorConfig as Mock).mockReturnValue(mockContentConfig);
await config.refreshAuth(authType);
expect(createContentGeneratorConfig).toHaveBeenCalledWith(
MODEL, // Should be called with the original model 'gemini-pro'
config,
authType,
);
// Verify that contentGeneratorConfig is updated with the new model
@@ -234,11 +231,11 @@ describe('Server Config (config.ts)', () => {
expect(config.getTelemetryEnabled()).toBe(false);
});
it('Config constructor should default telemetry to false if not provided', () => {
it('Config constructor should default telemetry to default value if not provided', () => {
const paramsWithoutTelemetry: ConfigParameters = { ...baseParams };
delete paramsWithoutTelemetry.telemetry;
const config = new Config(paramsWithoutTelemetry);
expect(config.getTelemetryEnabled()).toBe(false);
expect(config.getTelemetryEnabled()).toBe(TELEMETRY_SETTINGS.enabled);
});
it('should have a getFileService method that returns FileDiscoveryService', () => {
@@ -285,20 +282,20 @@ describe('Server Config (config.ts)', () => {
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
});
it('should return default logPrompts setting (false) if not provided', () => {
it('should return default logPrompts setting (true) if not provided', () => {
const params: ConfigParameters = {
...baseParams,
telemetry: { enabled: true },
};
const config = new Config(params);
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
});
it('should return default logPrompts setting (false) if telemetry object is not provided', () => {
it('should return default logPrompts setting (true) if telemetry object is not provided', () => {
const paramsWithoutTelemetry: ConfigParameters = { ...baseParams };
delete paramsWithoutTelemetry.telemetry;
const config = new Config(paramsWithoutTelemetry);
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
});
it('should return default telemetry target if telemetry object is not provided', () => {
@@ -315,38 +312,4 @@ describe('Server Config (config.ts)', () => {
expect(config.getTelemetryOtlpEndpoint()).toBe(DEFAULT_OTLP_ENDPOINT);
});
});
describe('refreshMemory', () => {
it('should update memory and file count on successful refresh', async () => {
const config = new Config(baseParams);
const mockMemoryData = {
memoryContent: 'new memory content',
fileCount: 5,
};
(loadServerHierarchicalMemory as Mock).mockResolvedValue(mockMemoryData);
const result = await config.refreshMemory();
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
config.getWorkingDir(),
config.getDebugMode(),
config.getFileService(),
config.getExtensionContextFilePaths(),
);
expect(config.getUserMemory()).toBe(mockMemoryData.memoryContent);
expect(config.getGeminiMdFileCount()).toBe(mockMemoryData.fileCount);
expect(result).toEqual(mockMemoryData);
});
it('should propagate errors from loadServerHierarchicalMemory', async () => {
const config = new Config(baseParams);
const testError = new Error('Failed to load memory');
(loadServerHierarchicalMemory as Mock).mockRejectedValue(testError);
await expect(config.refreshMemory()).rejects.toThrow(testError);
});
});
});

View File

@@ -11,7 +11,7 @@ import {
ContentGeneratorConfig,
createContentGeneratorConfig,
} from '../core/contentGenerator.js';
import { UserTierId } from '../code_assist/types.js';
import { PromptRegistry } from '../prompts/prompt-registry.js';
import { ToolRegistry } from '../tools/tool-registry.js';
import { LSTool } from '../tools/ls.js';
import { ReadFileTool } from '../tools/read-file.js';
@@ -27,21 +27,29 @@ import {
setGeminiMdFilename,
GEMINI_CONFIG_DIR as GEMINI_DIR,
} from '../tools/memoryTool.js';
import { WebSearchTool } from '../tools/web-search.js';
import { GeminiClient } from '../core/client.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { GitService } from '../services/gitService.js';
import { loadServerHierarchicalMemory } from '../utils/memoryDiscovery.js';
import { getProjectTempDir } from '../utils/paths.js';
import {
initializeTelemetry,
DEFAULT_TELEMETRY_TARGET,
DEFAULT_OTLP_ENDPOINT,
TelemetryTarget,
StartSessionEvent,
} from '../telemetry/index.js';
import {
DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
} from './models.js';
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
import { MCPOAuthConfig } from '../mcp/oauth-provider.js';
import { IdeClient } from '../ide/ide-client.js';
// Re-export OAuth config type
export type { MCPOAuthConfig };
export enum ApprovalMode {
DEFAULT = 'default',
@@ -57,18 +65,37 @@ export interface BugCommandSettings {
urlTemplate: string;
}
export interface SummarizeToolOutputSettings {
tokenBudget?: number;
}
export interface TelemetrySettings {
enabled?: boolean;
target?: TelemetryTarget;
otlpEndpoint?: string;
logPrompts?: boolean;
outfile?: string;
}
export interface ActiveExtension {
export interface GeminiCLIExtension {
name: string;
version: string;
isActive: boolean;
}
export interface FileFilteringOptions {
respectGitIgnore: boolean;
respectGeminiIgnore: boolean;
}
// For memory files
export const DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: FileFilteringOptions = {
respectGitIgnore: false,
respectGeminiIgnore: true,
};
// For all other files
export const DEFAULT_FILE_FILTERING_OPTIONS: FileFilteringOptions = {
respectGitIgnore: true,
respectGeminiIgnore: true,
};
export class MCPServerConfig {
constructor(
// For stdio transport
@@ -90,9 +117,18 @@ export class MCPServerConfig {
readonly description?: string,
readonly includeTools?: string[],
readonly excludeTools?: string[],
readonly extensionName?: string,
// OAuth configuration
readonly oauth?: MCPOAuthConfig,
readonly authProviderType?: AuthProviderType,
) {}
}
export enum AuthProviderType {
DYNAMIC_DISCOVERY = 'dynamic_discovery',
GOOGLE_CREDENTIALS = 'google_credentials',
}
export interface SandboxConfig {
command: 'docker' | 'podman' | 'sandbox-exec';
image: string;
@@ -128,6 +164,7 @@ export interface ConfigParameters {
usageStatisticsEnabled?: boolean;
fileFiltering?: {
respectGitIgnore?: boolean;
respectGeminiIgnore?: boolean;
enableRecursiveFileSearch?: boolean;
};
checkpointing?: boolean;
@@ -138,31 +175,26 @@ export interface ConfigParameters {
model: string;
extensionContextFilePaths?: string[];
maxSessionTurns?: number;
sessionTokenLimit?: number;
maxFolderItems?: number;
experimentalAcp?: boolean;
listExtensions?: boolean;
activeExtensions?: ActiveExtension[];
extensions?: GeminiCLIExtension[];
blockedMcpServers?: Array<{ name: string; extensionName: string }>;
noBrowser?: boolean;
summarizeToolOutput?: Record<string, SummarizeToolOutputSettings>;
ideMode?: boolean;
ideClient?: IdeClient;
enableOpenAILogging?: boolean;
sampling_params?: {
top_p?: number;
top_k?: number;
repetition_penalty?: number;
presence_penalty?: number;
frequency_penalty?: number;
temperature?: number;
max_tokens?: number;
};
sampling_params?: Record<string, unknown>;
systemPromptMappings?: Array<{
baseUrls?: string[];
modelNames?: string[];
template?: string;
baseUrls: string[];
modelNames: string[];
template: string;
}>;
}
export class Config {
private toolRegistry!: ToolRegistry;
private promptRegistry!: PromptRegistry;
private readonly sessionId: string;
private contentGeneratorConfig!: ContentGeneratorConfig;
private readonly embeddingModel: string;
@@ -187,6 +219,7 @@ export class Config {
private geminiClient!: GeminiClient;
private readonly fileFiltering: {
respectGitIgnore: boolean;
respectGeminiIgnore: boolean;
enableRecursiveFileSearch: boolean;
};
private fileDiscoveryService: FileDiscoveryService | null = null;
@@ -199,29 +232,21 @@ export class Config {
private readonly extensionContextFilePaths: string[];
private readonly noBrowser: boolean;
private readonly ideMode: boolean;
private readonly enableOpenAILogging: boolean;
private readonly sampling_params?: {
top_p?: number;
top_k?: number;
repetition_penalty?: number;
presence_penalty?: number;
frequency_penalty?: number;
temperature?: number;
max_tokens?: number;
};
private readonly systemPromptMappings?: Array<{
baseUrls?: string[];
modelNames?: string[];
template?: string;
}>;
private readonly ideClient: IdeClient | undefined;
private modelSwitchedDuringSession: boolean = false;
private readonly maxSessionTurns: number;
private readonly sessionTokenLimit: number;
private readonly maxFolderItems: number;
private readonly listExtensions: boolean;
private readonly _activeExtensions: ActiveExtension[];
private readonly _extensions: GeminiCLIExtension[];
private readonly _blockedMcpServers: Array<{
name: string;
extensionName: string;
}>;
flashFallbackHandler?: FlashFallbackHandler;
private quotaErrorOccurred: boolean = false;
private readonly summarizeToolOutput:
| Record<string, SummarizeToolOutputSettings>
| undefined;
private readonly experimentalAcp: boolean = false;
constructor(params: ConfigParameters) {
this.sessionId = params.sessionId;
@@ -247,12 +272,14 @@ export class Config {
enabled: params.telemetry?.enabled ?? false,
target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET,
otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT,
logPrompts: params.telemetry?.logPrompts ?? false,
logPrompts: params.telemetry?.logPrompts ?? true,
outfile: params.telemetry?.outfile,
};
this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true;
this.fileFiltering = {
respectGitIgnore: params.fileFiltering?.respectGitIgnore ?? true,
respectGeminiIgnore: params.fileFiltering?.respectGeminiIgnore ?? true,
enableRecursiveFileSearch:
params.fileFiltering?.enableRecursiveFileSearch ?? true,
};
@@ -264,15 +291,14 @@ export class Config {
this.model = params.model;
this.extensionContextFilePaths = params.extensionContextFilePaths ?? [];
this.maxSessionTurns = params.maxSessionTurns ?? -1;
this.sessionTokenLimit = params.sessionTokenLimit ?? 32000;
this.maxFolderItems = params.maxFolderItems ?? 20;
this.experimentalAcp = params.experimentalAcp ?? false;
this.listExtensions = params.listExtensions ?? false;
this._activeExtensions = params.activeExtensions ?? [];
this._extensions = params.extensions ?? [];
this._blockedMcpServers = params.blockedMcpServers ?? [];
this.noBrowser = params.noBrowser ?? false;
this.summarizeToolOutput = params.summarizeToolOutput;
this.ideMode = params.ideMode ?? false;
this.enableOpenAILogging = params.enableOpenAILogging ?? false;
this.sampling_params = params.sampling_params;
this.systemPromptMappings = params.systemPromptMappings;
this.ideClient = params.ideClient;
if (params.contextFileName) {
setGeminiMdFilename(params.contextFileName);
@@ -283,10 +309,9 @@ export class Config {
}
if (this.getUsageStatisticsEnabled()) {
// ClearcutLogger.getInstance(this)?.logStartSessionEvent(
// new StartSessionEvent(this),
// );
console.log('ClearcutLogger disabled - no data collection.');
ClearcutLogger.getInstance(this)?.logStartSessionEvent(
new StartSessionEvent(this),
);
} else {
console.log('Data collection is disabled.');
}
@@ -298,20 +323,15 @@ export class Config {
if (this.getCheckpointingEnabled()) {
await this.getGitService();
}
this.promptRegistry = new PromptRegistry();
this.toolRegistry = await this.createToolRegistry();
}
async refreshAuth(authMethod: AuthType) {
this.contentGeneratorConfig = await createContentGeneratorConfig(
this.model,
this.contentGeneratorConfig = createContentGeneratorConfig(
this,
authMethod,
);
this.contentGeneratorConfig.enableOpenAILogging = this.enableOpenAILogging;
// Set sampling parameters from config if available
if (this.sampling_params) {
this.contentGeneratorConfig.samplingParams = this.sampling_params;
}
this.geminiClient = new GeminiClient(this);
await this.geminiClient.initialize(this.contentGeneratorConfig);
@@ -358,14 +378,6 @@ export class Config {
return this.maxSessionTurns;
}
getSessionTokenLimit(): number {
return this.sessionTokenLimit;
}
getMaxFolderItems(): number {
return this.maxFolderItems;
}
setQuotaErrorOccurred(value: boolean): void {
this.quotaErrorOccurred = value;
}
@@ -374,14 +386,6 @@ export class Config {
return this.quotaErrorOccurred;
}
async getUserTier(): Promise<UserTierId | undefined> {
if (!this.geminiClient) {
return undefined;
}
const generator = this.geminiClient.getContentGenerator();
return await generator.getTier?.();
}
getEmbeddingModel(): string {
return this.embeddingModel;
}
@@ -402,6 +406,10 @@ export class Config {
return Promise.resolve(this.toolRegistry);
}
getPromptRegistry(): PromptRegistry {
return this.promptRegistry;
}
getDebugMode(): boolean {
return this.debugMode;
}
@@ -474,7 +482,7 @@ export class Config {
}
getTelemetryLogPromptsEnabled(): boolean {
return this.telemetrySettings.logPrompts ?? false;
return this.telemetrySettings.logPrompts ?? true;
}
getTelemetryOtlpEndpoint(): string {
@@ -485,6 +493,10 @@ export class Config {
return this.telemetrySettings.target ?? DEFAULT_TELEMETRY_TARGET;
}
getTelemetryOutfile(): string | undefined {
return this.telemetrySettings.outfile;
}
getGeminiClient(): GeminiClient {
return this.geminiClient;
}
@@ -504,6 +516,16 @@ export class Config {
getFileFilteringRespectGitIgnore(): boolean {
return this.fileFiltering.respectGitIgnore;
}
getFileFilteringRespectGeminiIgnore(): boolean {
return this.fileFiltering.respectGeminiIgnore;
}
getFileFilteringOptions(): FileFilteringOptions {
return {
respectGitIgnore: this.fileFiltering.respectGitIgnore,
respectGeminiIgnore: this.fileFiltering.respectGeminiIgnore,
};
}
getCheckpointingEnabled(): boolean {
return this.checkpointing;
@@ -536,22 +558,44 @@ export class Config {
return this.extensionContextFilePaths;
}
getExperimentalAcp(): boolean {
return this.experimentalAcp;
}
getListExtensions(): boolean {
return this.listExtensions;
}
getActiveExtensions(): ActiveExtension[] {
return this._activeExtensions;
getExtensions(): GeminiCLIExtension[] {
return this._extensions;
}
getBlockedMcpServers(): Array<{ name: string; extensionName: string }> {
return this._blockedMcpServers;
}
getNoBrowser(): boolean {
return this.noBrowser;
}
isBrowserLaunchSuppressed(): boolean {
return this.getNoBrowser() || !shouldAttemptBrowserLaunch();
}
getSummarizeToolOutputConfig():
| Record<string, SummarizeToolOutputSettings>
| undefined {
return this.summarizeToolOutput;
}
getIdeMode(): boolean {
return this.ideMode;
}
getIdeClient(): IdeClient | undefined {
return this.ideClient;
}
async getGitService(): Promise<GitService> {
if (!this.gitService) {
this.gitService = new GitService(this.targetDir);
@@ -560,34 +604,6 @@ export class Config {
return this.gitService;
}
getEnableOpenAILogging(): boolean {
return this.enableOpenAILogging;
}
getSystemPromptMappings():
| Array<{
baseUrls?: string[];
modelNames?: string[];
template?: string;
}>
| undefined {
return this.systemPromptMappings;
}
async refreshMemory(): Promise<{ memoryContent: string; fileCount: number }> {
const { memoryContent, fileCount } = await loadServerHierarchicalMemory(
this.getWorkingDir(),
this.getDebugMode(),
this.getFileService(),
this.getExtensionContextFilePaths(),
);
this.setUserMemory(memoryContent);
this.setGeminiMdFileCount(fileCount);
return { memoryContent, fileCount };
}
async createToolRegistry(): Promise<ToolRegistry> {
const registry = new ToolRegistry(this);
@@ -634,9 +650,9 @@ export class Config {
registerCoreTool(ReadManyFilesTool, this);
registerCoreTool(ShellTool, this);
registerCoreTool(MemoryTool);
// registerCoreTool(WebSearchTool, this); // Temporarily disabled
registerCoreTool(WebSearchTool, this);
await registry.discoverTools();
await registry.discoverAllTools();
return registry;
}
}

View File

@@ -72,7 +72,7 @@ describe('Flash Model Fallback Configuration', () => {
expect(config.getModel()).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
it('should fallback to initial model if contentGeneratorConfig is not available', () => {
it('should fall back to initial model if contentGeneratorConfig is not available', () => {
// Test with fresh config where contentGeneratorConfig might not be set
const newConfig = new Config({
sessionId: 'test-session-2',