From cc7e1472f9c51338c5ed1ce15226764caf8ced33 Mon Sep 17 00:00:00 2001 From: Zack Birkenbuel Date: Mon, 20 Oct 2025 16:15:23 -0700 Subject: [PATCH] Pass whole extensions rather than just context files (#10910) Co-authored-by: Jake Macdonald --- packages/a2a-server/src/config/config.ts | 3 +- .../cli/src/commands/extensions/disable.ts | 16 +- .../cli/src/commands/extensions/enable.ts | 12 +- .../cli/src/commands/extensions/update.ts | 16 +- packages/cli/src/commands/mcp/list.ts | 4 +- .../cli/src/config/config.integration.test.ts | 32 - packages/cli/src/config/config.test.ts | 708 ++---------------- packages/cli/src/config/config.ts | 43 +- packages/cli/src/config/extension.test.ts | 272 ++----- packages/cli/src/config/extension.ts | 58 +- .../cli/src/config/extensions/github.test.ts | 26 +- packages/cli/src/config/extensions/github.ts | 3 + .../cli/src/config/extensions/update.test.ts | 137 ++-- packages/cli/src/config/extensions/update.ts | 18 +- .../src/config/extensions/variableSchema.ts | 3 + packages/cli/src/config/settings.test.ts | 17 +- packages/cli/src/config/settings.ts | 9 +- packages/cli/src/gemini.tsx | 4 +- packages/cli/src/ui/AppContainer.tsx | 7 +- .../src/ui/commands/directoryCommand.test.tsx | 1 - .../cli/src/ui/commands/directoryCommand.tsx | 2 +- .../cli/src/ui/commands/memoryCommand.test.ts | 2 +- packages/cli/src/ui/commands/memoryCommand.ts | 2 +- .../components/views/ExtensionsList.test.tsx | 10 +- .../ui/components/views/ExtensionsList.tsx | 7 +- .../cli/src/ui/components/views/McpStatus.tsx | 4 +- .../src/ui/hooks/useExtensionUpdates.test.ts | 86 ++- .../cli/src/ui/hooks/useExtensionUpdates.ts | 6 + .../cli/src/zed-integration/zedIntegration.ts | 2 - packages/core/src/config/config.ts | 20 +- .../core/src/tools/mcp-client-manager.test.ts | 38 +- packages/core/src/tools/mcp-client-manager.ts | 42 +- packages/core/src/tools/tool-registry.ts | 10 +- .../core/src/utils/memoryDiscovery.test.ts | 40 +- packages/core/src/utils/memoryDiscovery.ts | 20 +- 35 files changed, 487 insertions(+), 1193 deletions(-) diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts index 46d3329e2a..2a56f4cd52 100644 --- a/packages/a2a-server/src/config/config.ts +++ b/packages/a2a-server/src/config/config.ts @@ -73,13 +73,12 @@ export async function loadConfig( }; const fileService = new FileDiscoveryService(workspaceDir); - const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles); const { memoryContent, fileCount } = await loadServerHierarchicalMemory( workspaceDir, [workspaceDir], false, fileService, - extensionContextFilePaths, + extensions, settings.folderTrust === true, ); configParams.userMemory = memoryContent; diff --git a/packages/cli/src/commands/extensions/disable.ts b/packages/cli/src/commands/extensions/disable.ts index b79691c0d0..53f8f145fd 100644 --- a/packages/cli/src/commands/extensions/disable.ts +++ b/packages/cli/src/commands/extensions/disable.ts @@ -8,6 +8,7 @@ import { type CommandModule } from 'yargs'; import { disableExtension } from '../../config/extension.js'; import { SettingScope } from '../../config/settings.js'; import { getErrorMessage } from '../../utils/errors.js'; +import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js'; import { debugLogger } from '@google/gemini-cli-core'; interface DisableArgs { @@ -16,11 +17,20 @@ interface DisableArgs { } export function handleDisable(args: DisableArgs) { + const extensionEnablementManager = new ExtensionEnablementManager(); try { if (args.scope?.toLowerCase() === 'workspace') { - disableExtension(args.name, SettingScope.Workspace); + disableExtension( + args.name, + SettingScope.Workspace, + extensionEnablementManager, + ); } else { - disableExtension(args.name, SettingScope.User); + disableExtension( + args.name, + SettingScope.User, + extensionEnablementManager, + ); } debugLogger.log( `Extension "${args.name}" successfully disabled for scope "${args.scope}".`, @@ -41,7 +51,7 @@ export const disableCommand: CommandModule = { type: 'string', }) .option('scope', { - describe: 'The scope to disable the extenison in.', + describe: 'The scope to disable the extension in.', type: 'string', default: SettingScope.User, }) diff --git a/packages/cli/src/commands/extensions/enable.ts b/packages/cli/src/commands/extensions/enable.ts index 0691b86a60..8337fbe468 100644 --- a/packages/cli/src/commands/extensions/enable.ts +++ b/packages/cli/src/commands/extensions/enable.ts @@ -8,6 +8,7 @@ import { type CommandModule } from 'yargs'; import { FatalConfigError, getErrorMessage } from '@google/gemini-cli-core'; import { enableExtension } from '../../config/extension.js'; import { SettingScope } from '../../config/settings.js'; +import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js'; interface EnableArgs { name: string; @@ -15,11 +16,16 @@ interface EnableArgs { } export function handleEnable(args: EnableArgs) { + const extensionEnablementManager = new ExtensionEnablementManager(); try { if (args.scope?.toLowerCase() === 'workspace') { - enableExtension(args.name, SettingScope.Workspace); + enableExtension( + args.name, + SettingScope.Workspace, + extensionEnablementManager, + ); } else { - enableExtension(args.name, SettingScope.User); + enableExtension(args.name, SettingScope.User, extensionEnablementManager); } if (args.scope) { console.log( @@ -46,7 +52,7 @@ export const enableCommand: CommandModule = { }) .option('scope', { describe: - 'The scope to enable the extenison in. If not set, will be enabled in all scopes.', + 'The scope to enable the extension in. If not set, will be enabled in all scopes.', type: 'string', }) .check((argv) => { diff --git a/packages/cli/src/commands/extensions/update.ts b/packages/cli/src/commands/extensions/update.ts index 6f8ceb13a7..39b9e174e3 100644 --- a/packages/cli/src/commands/extensions/update.ts +++ b/packages/cli/src/commands/extensions/update.ts @@ -7,7 +7,6 @@ import type { CommandModule } from 'yargs'; import { loadExtensions, - annotateActiveExtensions, requestConsentNonInteractive, } from '../../config/extension.js'; import { @@ -37,12 +36,7 @@ export async function handleUpdate(args: UpdateArgs) { // ones. args.name ? [args.name] : [], ); - const allExtensions = loadExtensions(extensionEnablementManager); - const extensions = annotateActiveExtensions( - allExtensions, - workingDir, - extensionEnablementManager, - ); + const extensions = loadExtensions(extensionEnablementManager); if (args.name) { try { const extension = extensions.find( @@ -58,7 +52,10 @@ export async function handleUpdate(args: UpdateArgs) { ); return; } - const updateState = await checkForExtensionUpdate(extension); + const updateState = await checkForExtensionUpdate( + extension, + extensionEnablementManager, + ); if (updateState !== ExtensionUpdateState.UPDATE_AVAILABLE) { debugLogger.log(`Extension "${args.name}" is already up to date.`); return; @@ -66,6 +63,7 @@ export async function handleUpdate(args: UpdateArgs) { // TODO(chrstnb): we should list extensions if the requested extension is not installed. const updatedExtensionInfo = (await updateExtension( extension, + extensionEnablementManager, workingDir, requestConsentNonInteractive, updateState, @@ -90,6 +88,7 @@ export async function handleUpdate(args: UpdateArgs) { const extensionState = new Map(); await checkForAllExtensionUpdates( extensions, + extensionEnablementManager, (action) => { if (action.type === 'SET_STATE') { extensionState.set(action.payload.name, { @@ -104,6 +103,7 @@ export async function handleUpdate(args: UpdateArgs) { requestConsentNonInteractive, extensions, extensionState, + extensionEnablementManager, () => {}, ); updateInfos = updateInfos.filter( diff --git a/packages/cli/src/commands/mcp/list.ts b/packages/cli/src/commands/mcp/list.ts index 125fdab41c..fbfae2d913 100644 --- a/packages/cli/src/commands/mcp/list.ts +++ b/packages/cli/src/commands/mcp/list.ts @@ -31,7 +31,7 @@ async function getMcpServersFromConfig(): Promise< } mcpServers[key] = { ...server, - extensionName: extension.name, + extension, }; }); } @@ -115,7 +115,7 @@ export async function listMcpServers(): Promise { let serverInfo = serverName + - (server.extensionName ? ` (from ${server.extensionName})` : '') + + (server.extension?.name ? ` (from ${server.extension.name})` : '') + ': '; if (server.httpUrl) { serverInfo += `${server.httpUrl} (http)`; diff --git a/packages/cli/src/config/config.integration.test.ts b/packages/cli/src/config/config.integration.test.ts index f4c22c0121..24010033f4 100644 --- a/packages/cli/src/config/config.integration.test.ts +++ b/packages/cli/src/config/config.integration.test.ts @@ -243,38 +243,6 @@ describe('Configuration Integration Tests', () => { }); }); - describe('Extension Context Files', () => { - it('should have an empty array for extension context files by default', () => { - const configParams: ConfigParameters = { - sessionId: 'test-session', - cwd: '/tmp', - model: 'test-model', - embeddingModel: 'test-embedding-model', - sandbox: undefined, - targetDir: tempDir, - debugMode: false, - }; - const config = new Config(configParams); - expect(config.getExtensionContextFilePaths()).toEqual([]); - }); - - it('should correctly store and return extension context file paths', () => { - const contextFiles = ['/path/to/file1.txt', '/path/to/file2.js']; - const configParams: ConfigParameters = { - sessionId: 'test-session', - cwd: '/tmp', - model: 'test-model', - embeddingModel: 'test-embedding-model', - sandbox: undefined, - targetDir: tempDir, - debugMode: false, - extensionContextFilePaths: contextFiles, - }; - const config = new Config(configParams); - expect(config.getExtensionContextFilePaths()).toEqual(contextFiles); - }); - }); - describe('Approval Mode Integration Tests', () => { let parseArguments: typeof import('./config.js').parseArguments; diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index e3b0d7a793..95f3e6c778 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -20,7 +20,6 @@ import { loadCliConfig, parseArguments, type CliArgs } from './config.js'; import type { Settings } from './settings.js'; import * as ServerConfig from '@google/gemini-cli-core'; import { isWorkspaceTrusted } from './trustedFolders.js'; -import { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; vi.mock('./trustedFolders.js', () => ({ isWorkspaceTrusted: vi @@ -538,13 +537,7 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getProxy()).toBeFalsy(); }); @@ -585,13 +578,7 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getProxy()).toBe(expected); }); }); @@ -639,24 +626,13 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { }, ]; const argv = await parseArguments({} as Settings); - await loadCliConfig( - settings, - extensions, - - new ExtensionEnablementManager(argv.extensions), - 'session-id', - argv, - ); + await loadCliConfig(settings, extensions, 'session-id', argv); expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith( expect.any(String), [], false, expect.any(Object), - [ - '/path/to/ext1/GEMINI.md', - '/path/to/ext3/context1.md', - '/path/to/ext3/context2.md', - ], + extensions, true, 'tree', { @@ -727,13 +703,7 @@ describe('mergeMcpServers', () => { const originalSettings = JSON.parse(JSON.stringify(settings)); process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - await loadCliConfig( - settings, - extensions, - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + await loadCliConfig(settings, extensions, 'test-session', argv); expect(settings).toEqual(originalSettings); }); }); @@ -779,7 +749,6 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -806,7 +775,6 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -841,7 +809,6 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -860,7 +827,6 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -876,7 +842,6 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -891,7 +856,6 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -918,7 +882,6 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -943,13 +906,7 @@ describe('mergeExcludeTools', () => { const originalSettings = JSON.parse(JSON.stringify(settings)); process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - await loadCliConfig( - settings, - extensions, - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + await loadCliConfig(settings, extensions, 'test-session', argv); expect(settings).toEqual(originalSettings); }); }); @@ -978,7 +935,6 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1005,7 +961,6 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1032,7 +987,6 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1059,7 +1013,6 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1079,7 +1032,6 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1110,7 +1062,6 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1138,7 +1089,6 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1165,7 +1115,6 @@ describe('Approval mode tool exclusion logic', () => { loadCliConfig( settings, extensions, - new ExtensionEnablementManager(invalidArgv.extensions), 'test-session', invalidArgv as CliArgs, ), @@ -1201,13 +1150,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow all MCP servers if the flag is not provided', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - baseSettings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual(baseSettings.mcpServers); }); @@ -1219,13 +1162,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { 'server1', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - baseSettings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -1241,13 +1178,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { 'server3', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - baseSettings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, server3: { url: 'http://localhost:8082' }, @@ -1264,13 +1195,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { 'server4', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - baseSettings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -1279,13 +1204,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow no MCP servers if the flag is provided but empty', async () => { process.argv = ['node', 'script.js', '--allowed-mcp-server-names', '']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - baseSettings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({}); }); @@ -1296,13 +1215,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { ...baseSettings, mcp: { allowed: ['server1', 'server2'] }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, server2: { url: 'http://localhost:8081' }, @@ -1316,13 +1229,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { ...baseSettings, mcp: { excluded: ['server1', 'server2'] }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server3: { url: 'http://localhost:8082' }, }); @@ -1338,13 +1245,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { allowed: ['server1', 'server2'], }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server2: { url: 'http://localhost:8081' }, }); @@ -1365,13 +1266,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { allowed: ['server2'], }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -1394,13 +1289,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { excluded: ['server3'], // Should be ignored }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server2: { url: 'http://localhost:8081' }, server3: { url: 'http://localhost:8082' }, @@ -1408,56 +1297,6 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { }); }); -describe('loadCliConfig extensions', () => { - const mockExtensions: GeminiCLIExtension[] = [ - { - path: '/path/to/ext1', - name: 'ext1', - version: '1.0.0', - contextFiles: ['/path/to/ext1.md'], - isActive: true, - }, - { - path: '/path/to/ext2', - name: 'ext2', - version: '1.0.0', - contextFiles: ['/path/to/ext2.md'], - isActive: true, - }, - ]; - - it('should not filter extensions if --extensions flag is not used', async () => { - process.argv = ['node', 'script.js']; - const argv = await parseArguments({} as Settings); - const settings: Settings = {}; - const config = await loadCliConfig( - settings, - mockExtensions, - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); - expect(config.getExtensionContextFilePaths()).toEqual([ - '/path/to/ext1.md', - '/path/to/ext2.md', - ]); - }); - - it('should filter extensions if --extensions flag is used', async () => { - process.argv = ['node', 'script.js', '--extensions', 'ext1']; - const argv = await parseArguments({} as Settings); - const settings: Settings = {}; - const config = await loadCliConfig( - settings, - mockExtensions, - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); - expect(config.getExtensionContextFilePaths()).toEqual(['/path/to/ext1.md']); - }); -}); - describe('loadCliConfig model selection', () => { it('selects a model from settings.json if provided', async () => { process.argv = ['node', 'script.js']; @@ -1469,7 +1308,6 @@ describe('loadCliConfig model selection', () => { }, }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1485,7 +1323,6 @@ describe('loadCliConfig model selection', () => { // No model set. }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1493,7 +1330,7 @@ describe('loadCliConfig model selection', () => { expect(config.getModel()).toBe('auto'); }); - it('always prefers model from argvs', async () => { + it('always prefers model from argv', async () => { process.argv = ['node', 'script.js', '--model', 'gemini-8675309-ultra']; const argv = await parseArguments({} as Settings); const config = await loadCliConfig( @@ -1503,7 +1340,6 @@ describe('loadCliConfig model selection', () => { }, }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1511,7 +1347,7 @@ describe('loadCliConfig model selection', () => { expect(config.getModel()).toBe('gemini-8675309-ultra'); }); - it('selects the model from argvs if provided', async () => { + it('selects the model from argv if provided', async () => { process.argv = ['node', 'script.js', '--model', 'gemini-8675309-ultra']; const argv = await parseArguments({} as Settings); const config = await loadCliConfig( @@ -1519,7 +1355,6 @@ describe('loadCliConfig model selection', () => { // No model provided via settings. }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1539,7 +1374,6 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1557,7 +1391,6 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1575,7 +1408,6 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1596,7 +1428,6 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1615,7 +1446,6 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -1649,13 +1479,7 @@ describe('loadCliConfig folderTrust', () => { }, }; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrust()).toBe(false); }); @@ -1669,13 +1493,7 @@ describe('loadCliConfig folderTrust', () => { }, }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrust()).toBe(true); }); @@ -1683,13 +1501,7 @@ describe('loadCliConfig folderTrust', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrust()).toBe(false); }); }); @@ -1730,13 +1542,7 @@ describe('loadCliConfig with includeDirectories', () => { ], }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); const expected = [ mockCwd, path.resolve(path.sep, 'cli', 'path1'), @@ -1779,13 +1585,7 @@ describe('loadCliConfig chatCompression', () => { }, }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getChatCompression()).toEqual({ contextPercentageThreshold: 0.5, }); @@ -1795,13 +1595,7 @@ describe('loadCliConfig chatCompression', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getChatCompression()).toBeUndefined(); }); }); @@ -1825,13 +1619,7 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getUseRipgrep()).toBe(true); }); @@ -1839,13 +1627,7 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { tools: { useRipgrep: false } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getUseRipgrep()).toBe(false); }); @@ -1853,28 +1635,16 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { tools: { useRipgrep: true } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getUseRipgrep()).toBe(true); }); describe('loadCliConfig useModelRouter', () => { - it('should be false by default when useModelRouter is not set in settings', async () => { + it('should be true by default when useModelRouter is not set in settings', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getUseModelRouter()).toBe(true); }); @@ -1882,13 +1652,7 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { experimental: { useModelRouter: true } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getUseModelRouter()).toBe(true); }); @@ -1896,13 +1660,7 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { experimental: { useModelRouter: false } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getUseModelRouter()).toBe(false); }); }); @@ -1929,13 +1687,7 @@ describe('screenReader configuration', () => { const settings: Settings = { ui: { accessibility: { screenReader: true } }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getScreenReader()).toBe(true); }); @@ -1945,13 +1697,7 @@ describe('screenReader configuration', () => { const settings: Settings = { ui: { accessibility: { screenReader: false } }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getScreenReader()).toBe(false); }); @@ -1961,13 +1707,7 @@ describe('screenReader configuration', () => { const settings: Settings = { ui: { accessibility: { screenReader: false } }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getScreenReader()).toBe(true); }); @@ -1975,13 +1715,7 @@ describe('screenReader configuration', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getScreenReader()).toBe(false); }); }); @@ -2012,13 +1746,7 @@ describe('loadCliConfig tool exclusions', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).not.toContain('run_shell_command'); expect(config.getExcludeTools()).not.toContain('replace'); expect(config.getExcludeTools()).not.toContain('write_file'); @@ -2028,13 +1756,7 @@ describe('loadCliConfig tool exclusions', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).not.toContain('run_shell_command'); expect(config.getExcludeTools()).not.toContain('replace'); expect(config.getExcludeTools()).not.toContain('write_file'); @@ -2044,13 +1766,7 @@ describe('loadCliConfig tool exclusions', () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '-p', 'test']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).toContain('run_shell_command'); expect(config.getExcludeTools()).toContain('replace'); expect(config.getExcludeTools()).toContain('write_file'); @@ -2060,13 +1776,7 @@ describe('loadCliConfig tool exclusions', () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '-p', 'test', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).not.toContain('run_shell_command'); expect(config.getExcludeTools()).not.toContain('replace'); expect(config.getExcludeTools()).not.toContain('write_file'); @@ -2083,13 +1793,7 @@ describe('loadCliConfig tool exclusions', () => { 'ShellTool', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).not.toContain(SHELL_TOOL_NAME); }); @@ -2104,13 +1808,7 @@ describe('loadCliConfig tool exclusions', () => { 'run_shell_command', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).not.toContain(SHELL_TOOL_NAME); }); @@ -2125,13 +1823,7 @@ describe('loadCliConfig tool exclusions', () => { 'ShellTool(wc)', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).not.toContain(SHELL_TOOL_NAME); }); }); @@ -2158,13 +1850,7 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(true); }); @@ -2172,13 +1858,7 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '--prompt-interactive', 'test']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(true); }); @@ -2186,13 +1866,7 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); }); @@ -2200,13 +1874,7 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--prompt', 'test']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); }); @@ -2214,13 +1882,7 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--model', 'gemini-1.5-pro', 'Hello']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); }); @@ -2235,13 +1897,7 @@ describe('loadCliConfig interactive', () => { 'Hello world', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); // Verify the question is preserved for one-shot execution expect(argv.prompt).toBe('Hello world'); @@ -2252,13 +1908,7 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '-e', 'none', 'hello']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); expect(argv.query).toBe('hello'); expect(argv.extensions).toEqual(['none']); @@ -2268,13 +1918,7 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', 'hello world how are you']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); expect(argv.query).toBe('hello world how are you'); expect(argv.prompt).toBe('hello world how are you'); @@ -2295,13 +1939,7 @@ describe('loadCliConfig interactive', () => { 'array', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); expect(argv.query).toBe('write a function to sort array'); expect(argv.model).toBe('gemini-1.5-pro'); @@ -2311,13 +1949,7 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(true); expect(argv.query).toBeUndefined(); }); @@ -2336,13 +1968,7 @@ describe('loadCliConfig interactive', () => { 'you', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); expect(argv.query).toBe('hello world how are you'); expect(argv.extensions).toEqual(['none']); @@ -2352,13 +1978,7 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--model', 'gemini-1.5-pro']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(true); }); }); @@ -2386,78 +2006,42 @@ describe('loadCliConfig approval mode', () => { it('should default to DEFAULT approval mode when no flags are set', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should set YOLO approval mode when --yolo flag is used', async () => { process.argv = ['node', 'script.js', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); it('should set YOLO approval mode when -y flag is used', async () => { process.argv = ['node', 'script.js', '-y']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); it('should set DEFAULT approval mode when --approval-mode=default', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'default']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT); }); it('should set YOLO approval mode when --approval-mode=yolo', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); @@ -2468,26 +2052,14 @@ describe('loadCliConfig approval mode', () => { const argv = await parseArguments({} as Settings); // Manually set yolo to true to simulate what would happen if validation didn't prevent it argv.yolo = true; - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should fall back to --yolo behavior when --approval-mode is not set', async () => { process.argv = ['node', 'script.js', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); @@ -2503,52 +2075,28 @@ describe('loadCliConfig approval mode', () => { it('should override --approval-mode=yolo to DEFAULT', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should override --approval-mode=auto_edit to DEFAULT', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should override --yolo flag to DEFAULT', async () => { process.argv = ['node', 'script.js', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should remain DEFAULT when --approval-mode=default', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'default']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); }); @@ -2629,13 +2177,7 @@ describe('loadCliConfig fileFiltering', () => { }, }; const argv = await parseArguments(settings); - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(getter(config)).toBe(value); }, ); @@ -2645,13 +2187,7 @@ describe('Output format', () => { it('should default to TEXT', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getOutputFormat()).toBe(OutputFormat.TEXT); }); @@ -2661,7 +2197,6 @@ describe('Output format', () => { const config = await loadCliConfig( { output: { format: OutputFormat.JSON } }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -2674,7 +2209,6 @@ describe('Output format', () => { const config = await loadCliConfig( { output: { format: OutputFormat.JSON } }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -2684,13 +2218,7 @@ describe('Output format', () => { it('should accept stream-json as a valid output format', async () => { process.argv = ['node', 'script.js', '--output-format', 'stream-json']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getOutputFormat()).toBe(OutputFormat.STREAM_JSON); }); @@ -2784,13 +2312,7 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: false } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); }); @@ -2801,13 +2323,7 @@ describe('Telemetry configuration via environment variables', () => { const settings: Settings = { telemetry: { target: ServerConfig.TelemetryTarget.LOCAL }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryTarget()).toBe('gcp'); }); @@ -2819,13 +2335,7 @@ describe('Telemetry configuration via environment variables', () => { telemetry: { target: ServerConfig.TelemetryTarget.GCP }, }; await expect( - loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ), + loadCliConfig(settings, [], 'test-session', argv), ).rejects.toThrow( /Invalid telemetry configuration: .*Invalid telemetry target/i, ); @@ -2840,13 +2350,7 @@ describe('Telemetry configuration via environment variables', () => { const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.com' }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpEndpoint()).toBe('http://gemini.env.com'); }); @@ -2855,13 +2359,7 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { otlpProtocol: 'grpc' } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpProtocol()).toBe('http'); }); @@ -2870,13 +2368,7 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { logPrompts: true } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); }); @@ -2887,13 +2379,7 @@ describe('Telemetry configuration via environment variables', () => { const settings: Settings = { telemetry: { outfile: '/settings/telemetry.log' }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOutfile()).toBe('/gemini/env/telemetry.log'); }); @@ -2902,13 +2388,7 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { useCollector: false } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryUseCollector()).toBe(true); }); @@ -2917,13 +2397,7 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); }); @@ -2934,13 +2408,7 @@ describe('Telemetry configuration via environment variables', () => { const settings: Settings = { telemetry: { target: ServerConfig.TelemetryTarget.LOCAL }, }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryTarget()).toBe('local'); }); @@ -2948,13 +2416,7 @@ describe('Telemetry configuration via environment variables', () => { vi.stubEnv('GEMINI_TELEMETRY_ENABLED', '1'); process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); }); @@ -2965,7 +2427,6 @@ describe('Telemetry configuration via environment variables', () => { const config = await loadCliConfig( { telemetry: { enabled: true } }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); @@ -2976,13 +2437,7 @@ describe('Telemetry configuration via environment variables', () => { vi.stubEnv('GEMINI_TELEMETRY_LOG_PROMPTS', '1'); process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig( - {}, - [], - new ExtensionEnablementManager(argv.extensions), - 'test-session', - argv, - ); + const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); }); @@ -2993,7 +2448,6 @@ describe('Telemetry configuration via environment variables', () => { const config = await loadCliConfig( { telemetry: { logPrompts: true } }, [], - new ExtensionEnablementManager(argv.extensions), 'test-session', argv, ); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index e44026b272..1bb1a3b70d 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -40,7 +40,6 @@ import { } from '@google/gemini-cli-core'; import type { Settings } from './settings.js'; -import { annotateActiveExtensions } from './extension.js'; import { getCliVersion } from '../utils/version.js'; import { loadSandboxConfig } from './sandboxConfig.js'; import { resolvePath } from '../utils/resolvePath.js'; @@ -48,7 +47,6 @@ import { appEvents } from '../utils/events.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; import { createPolicyEngineConfig } from './policy.js'; -import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; export interface CliArgs { query: string | undefined; @@ -289,7 +287,7 @@ export async function loadHierarchicalGeminiMemory( debugMode: boolean, fileService: FileDiscoveryService, settings: Settings, - extensionContextFilePaths: string[] = [], + extensions: GeminiCLIExtension[], folderTrust: boolean, memoryImportFormat: 'flat' | 'tree' = 'tree', fileFilteringOptions?: FileFilteringOptions, @@ -315,7 +313,7 @@ export async function loadHierarchicalGeminiMemory( includeDirectoriesToReadGemini, debugMode, fileService, - extensionContextFilePaths, + extensions, folderTrust, memoryImportFormat, fileFilteringOptions, @@ -364,8 +362,7 @@ export function isDebugMode(argv: CliArgs): boolean { export async function loadCliConfig( settings: Settings, - extensions: GeminiCLIExtension[], - extensionEnablementManager: ExtensionEnablementManager, + allExtensions: GeminiCLIExtension[], sessionId: string, argv: CliArgs, cwd: string = process.cwd(), @@ -379,16 +376,6 @@ export async function loadCliConfig( const folderTrust = settings.security?.folderTrust?.enabled ?? false; const trustedFolder = isWorkspaceTrusted(settings)?.isTrusted ?? true; - const allExtensions = annotateActiveExtensions( - extensions, - cwd, - extensionEnablementManager, - ); - - const activeExtensions = extensions.filter( - (_, i) => allExtensions[i].isActive, - ); - // 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. @@ -400,10 +387,6 @@ export async function loadCliConfig( setServerGeminiMdFilename(getCurrentGeminiMdFilename()); } - const extensionContextFilePaths = activeExtensions.flatMap( - (e) => e.contextFiles, - ); - const fileService = new FileDiscoveryService(cwd); const fileFiltering = { @@ -425,13 +408,13 @@ export async function loadCliConfig( debugMode, fileService, settings, - extensionContextFilePaths, + allExtensions, trustedFolder, memoryImportFormat, fileFiltering, ); - let mcpServers = mergeMcpServers(settings, activeExtensions); + let mcpServers = mergeMcpServers(settings, allExtensions); const question = argv.promptInteractive || argv.prompt || ''; // Determine approval mode with backward compatibility @@ -527,7 +510,7 @@ export async function loadCliConfig( const excludeTools = mergeExcludeTools( settings, - activeExtensions, + allExtensions, extraExcludes.length > 0 ? extraExcludes : undefined, ); const blockedMcpServers: Array<{ name: string; extensionName: string }> = []; @@ -618,10 +601,10 @@ export async function loadCliConfig( fileDiscoveryService: fileService, bugCommand: settings.advanced?.bugCommand, model: resolvedModel, - extensionContextFilePaths, maxSessionTurns: settings.model?.maxSessionTurns ?? -1, experimentalZedIntegration: argv.experimentalAcp || false, listExtensions: argv.listExtensions || false, + enabledExtensions: argv.extensions, extensions: allExtensions, blockedMcpServers, noBrowser: !!process.env['NO_BROWSER'], @@ -668,7 +651,7 @@ function allowedMcpServers( if (!isAllowed) { blockedMcpServers.push({ name: key, - extensionName: server.extensionName || '', + extensionName: server.extension?.name || '', }); } return isAllowed; @@ -678,7 +661,7 @@ function allowedMcpServers( blockedMcpServers.push( ...Object.entries(mcpServers).map(([key, server]) => ({ name: key, - extensionName: server.extensionName || '', + extensionName: server.extension?.name || '', })), ); mcpServers = {}; @@ -689,6 +672,9 @@ function allowedMcpServers( function mergeMcpServers(settings: Settings, extensions: GeminiCLIExtension[]) { const mcpServers = { ...(settings.mcpServers || {}) }; for (const extension of extensions) { + if (!extension.isActive) { + continue; + } Object.entries(extension.mcpServers || {}).forEach(([key, server]) => { if (mcpServers[key]) { debugLogger.warn( @@ -698,7 +684,7 @@ function mergeMcpServers(settings: Settings, extensions: GeminiCLIExtension[]) { } mcpServers[key] = { ...server, - extensionName: extension.name, + extension, }; }); } @@ -715,6 +701,9 @@ function mergeExcludeTools( ...(extraExcludes || []), ]); for (const extension of extensions) { + if (!extension.isActive) { + continue; + } for (const tool of extension.excludeTools || []) { allExcludeTools.add(tool); } diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index d1f80c6c30..18b92fc36c 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -14,7 +14,6 @@ import { ExtensionStorage, INSTALL_METADATA_FILENAME, INSTALL_WARNING_MESSAGE, - annotateActiveExtensions, disableExtension, enableExtension, installOrUpdateExtension, @@ -202,7 +201,7 @@ describe('extension tests', () => { ]); }); - it('should filter out disabled extensions', () => { + it('should annotate disabled extensions', () => { createExtension({ extensionsDir: userExtensionsDir, name: 'disabled-extension', @@ -213,20 +212,19 @@ describe('extension tests', () => { name: 'enabled-extension', version: '2.0.0', }); + const manager = new ExtensionEnablementManager(); disableExtension( 'disabled-extension', SettingScope.User, + manager, tempWorkspaceDir, ); - const manager = new ExtensionEnablementManager(); const extensions = loadExtensions(manager); - const activeExtensions = annotateActiveExtensions( - extensions, - tempWorkspaceDir, - manager, - ).filter((e) => e.isActive); - expect(activeExtensions).toHaveLength(1); - expect(activeExtensions[0].name).toBe('enabled-extension'); + expect(extensions).toHaveLength(2); + expect(extensions[0].name).toBe('disabled-extension'); + expect(extensions[0].isActive).toBe(false); + expect(extensions[1].name).toBe('enabled-extension'); + expect(extensions[1].isActive).toBe(true); }); it('should hydrate variables', () => { @@ -477,6 +475,7 @@ describe('extension tests', () => { const extension = loadExtension({ extensionDir: badExtDir, workspaceDir: tempWorkspaceDir, + extensionEnablementManager: new ExtensionEnablementManager(), }); expect(extension).toBeNull(); @@ -501,6 +500,7 @@ describe('extension tests', () => { const extension = loadExtension({ extensionDir, workspaceDir: tempWorkspaceDir, + extensionEnablementManager: new ExtensionEnablementManager(), }); const expectedHash = createHash('sha256') @@ -523,6 +523,7 @@ describe('extension tests', () => { const extension = loadExtension({ extensionDir, workspaceDir: tempWorkspaceDir, + extensionEnablementManager: new ExtensionEnablementManager(), }); const expectedHash = createHash('sha256') @@ -545,6 +546,7 @@ describe('extension tests', () => { const extension = loadExtension({ extensionDir, workspaceDir: tempWorkspaceDir, + extensionEnablementManager: new ExtensionEnablementManager(), }); const expectedHash = createHash('sha256') @@ -567,6 +569,7 @@ describe('extension tests', () => { const extension = loadExtension({ extensionDir, workspaceDir: tempWorkspaceDir, + extensionEnablementManager: new ExtensionEnablementManager(), }); const expectedHash = createHash('sha256') @@ -589,6 +592,7 @@ describe('extension tests', () => { const extension = loadExtension({ extensionDir, workspaceDir: tempWorkspaceDir, + extensionEnablementManager: new ExtensionEnablementManager(), }); const expectedHash = createHash('sha256') @@ -616,6 +620,7 @@ describe('extension tests', () => { const extension = loadExtension({ extensionDir: new ExtensionStorage(extensionName).getExtensionDir(), workspaceDir: tempWorkspaceDir, + extensionEnablementManager: new ExtensionEnablementManager(), }); const expectedHash = createHash('sha256') @@ -634,6 +639,7 @@ describe('extension tests', () => { const extension = loadExtension({ extensionDir, workspaceDir: tempWorkspaceDir, + extensionEnablementManager: new ExtensionEnablementManager(), }); const expectedHash = createHash('sha256') @@ -644,182 +650,6 @@ describe('extension tests', () => { }); }); - describe('annotateActiveExtensions', () => { - const extensions: GeminiCLIExtension[] = [ - { - path: '/path/to/ext1', - name: 'ext1', - version: '1.0.0', - contextFiles: [], - isActive: true, - }, - { - path: '/path/to/ext2', - name: 'ext2', - version: '1.0.0', - contextFiles: [], - isActive: true, - }, - { - path: '/path/to/ext3', - name: 'ext3', - version: '1.0.0', - contextFiles: [], - isActive: true, - }, - ]; - - it('should mark all extensions as active if no enabled extensions are provided', () => { - const activeExtensions = annotateActiveExtensions( - extensions, - '/path/to/workspace', - new ExtensionEnablementManager(), - ); - expect(activeExtensions).toHaveLength(3); - expect(activeExtensions.every((e) => e.isActive)).toBe(true); - }); - - it('should mark only the enabled extensions as active', () => { - const activeExtensions = annotateActiveExtensions( - extensions, - '/path/to/workspace', - new ExtensionEnablementManager(['ext1', 'ext3']), - ); - expect(activeExtensions).toHaveLength(3); - expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe( - true, - ); - expect(activeExtensions.find((e) => e.name === 'ext2')?.isActive).toBe( - false, - ); - expect(activeExtensions.find((e) => e.name === 'ext3')?.isActive).toBe( - true, - ); - }); - - it('should mark all extensions as inactive when "none" is provided', () => { - const activeExtensions = annotateActiveExtensions( - extensions, - '/path/to/workspace', - new ExtensionEnablementManager(['none']), - ); - expect(activeExtensions).toHaveLength(3); - expect(activeExtensions.every((e) => !e.isActive)).toBe(true); - }); - - it('should handle case-insensitivity', () => { - const activeExtensions = annotateActiveExtensions( - extensions, - '/path/to/workspace', - new ExtensionEnablementManager(['EXT1']), - ); - expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe( - true, - ); - }); - - it('should log an error for unknown extensions', () => { - const consoleSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - annotateActiveExtensions( - extensions, - '/path/to/workspace', - new ExtensionEnablementManager(['ext4']), - ); - expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4'); - consoleSpy.mockRestore(); - }); - - describe('autoUpdate', () => { - it('should be false if autoUpdate is not set in install metadata', () => { - const activeExtensions = annotateActiveExtensions( - extensions, - tempHomeDir, - new ExtensionEnablementManager(), - ); - expect( - activeExtensions.every( - (e) => e.installMetadata?.autoUpdate === false, - ), - ).toBe(false); - }); - - it('should be true if autoUpdate is true in install metadata', () => { - const extensionsWithAutoUpdate: GeminiCLIExtension[] = extensions.map( - (e) => ({ - ...e, - installMetadata: { - ...e.installMetadata!, - autoUpdate: true, - }, - }), - ); - const activeExtensions = annotateActiveExtensions( - extensionsWithAutoUpdate, - tempHomeDir, - new ExtensionEnablementManager(), - ); - expect( - activeExtensions.every((e) => e.installMetadata?.autoUpdate === true), - ).toBe(true); - }); - - it('should respect the per-extension settings from install metadata', () => { - const extensionsWithAutoUpdate: GeminiCLIExtension[] = [ - { - path: '/path/to/ext1', - name: 'ext1', - version: '1.0.0', - contextFiles: [], - installMetadata: { - source: 'test', - type: 'local', - autoUpdate: true, - }, - isActive: true, - }, - { - path: '/path/to/ext2', - name: 'ext2', - version: '1.0.0', - contextFiles: [], - installMetadata: { - source: 'test', - type: 'local', - autoUpdate: false, - }, - isActive: true, - }, - { - path: '/path/to/ext3', - name: 'ext3', - version: '1.0.0', - contextFiles: [], - isActive: true, - }, - ]; - const activeExtensions = annotateActiveExtensions( - extensionsWithAutoUpdate, - tempHomeDir, - new ExtensionEnablementManager(), - ); - expect( - activeExtensions.find((e) => e.name === 'ext1')?.installMetadata - ?.autoUpdate, - ).toBe(true); - expect( - activeExtensions.find((e) => e.name === 'ext2')?.installMetadata - ?.autoUpdate, - ).toBe(false); - expect( - activeExtensions.find((e) => e.name === 'ext3')?.installMetadata - ?.autoUpdate, - ).toBe(undefined); - }); - }); - }); - describe('installExtension', () => { it('should install an extension from a local path', async () => { const sourceExtDir = createExtension({ @@ -1194,6 +1024,7 @@ This extension will run the following MCP servers: await loadExtensionConfig({ extensionDir: sourceExtDir, workspaceDir: process.cwd(), + extensionEnablementManager: new ExtensionEnablementManager(), }), ), ).resolves.toBe('my-local-extension'); @@ -1512,7 +1343,11 @@ This extension will run the following MCP servers: version: '1.0.0', }); - disableExtension('my-extension', SettingScope.User); + disableExtension( + 'my-extension', + SettingScope.User, + new ExtensionEnablementManager(), + ); expect( isEnabled({ name: 'my-extension', @@ -1531,6 +1366,7 @@ This extension will run the following MCP servers: disableExtension( 'my-extension', SettingScope.Workspace, + new ExtensionEnablementManager(), tempWorkspaceDir, ); expect( @@ -1554,8 +1390,16 @@ This extension will run the following MCP servers: version: '1.0.0', }); - disableExtension('my-extension', SettingScope.User); - disableExtension('my-extension', SettingScope.User); + disableExtension( + 'my-extension', + SettingScope.User, + new ExtensionEnablementManager(), + ); + disableExtension( + 'my-extension', + SettingScope.User, + new ExtensionEnablementManager(), + ); expect( isEnabled({ name: 'my-extension', @@ -1566,7 +1410,11 @@ This extension will run the following MCP servers: it('should throw an error if you request system scope', () => { expect(() => - disableExtension('my-extension', SettingScope.System), + disableExtension( + 'my-extension', + SettingScope.System, + new ExtensionEnablementManager(), + ), ).toThrow('System and SystemDefaults scopes are not supported.'); }); @@ -1577,7 +1425,11 @@ This extension will run the following MCP servers: version: '1.0.0', }); - disableExtension('ext1', SettingScope.Workspace); + disableExtension( + 'ext1', + SettingScope.Workspace, + new ExtensionEnablementManager(), + ); expect(mockLogExtensionDisable).toHaveBeenCalled(); expect(ExtensionDisableEvent).toHaveBeenCalledWith( @@ -1595,12 +1447,7 @@ This extension will run the following MCP servers: const getActiveExtensions = (): GeminiCLIExtension[] => { const manager = new ExtensionEnablementManager(); const extensions = loadExtensions(manager); - const activeExtensions = annotateActiveExtensions( - extensions, - tempWorkspaceDir, - manager, - ); - return activeExtensions.filter((e) => e.isActive); + return extensions.filter((e) => e.isActive); }; it('should enable an extension at the user scope', () => { @@ -1609,11 +1456,12 @@ This extension will run the following MCP servers: name: 'ext1', version: '1.0.0', }); - disableExtension('ext1', SettingScope.User); + const extensionEnablementManager = new ExtensionEnablementManager(); + disableExtension('ext1', SettingScope.User, extensionEnablementManager); let activeExtensions = getActiveExtensions(); expect(activeExtensions).toHaveLength(0); - enableExtension('ext1', SettingScope.User); + enableExtension('ext1', SettingScope.User, extensionEnablementManager); activeExtensions = getActiveExtensions(); expect(activeExtensions).toHaveLength(1); expect(activeExtensions[0].name).toBe('ext1'); @@ -1625,11 +1473,20 @@ This extension will run the following MCP servers: name: 'ext1', version: '1.0.0', }); - disableExtension('ext1', SettingScope.Workspace); + const extensionEnablementManager = new ExtensionEnablementManager(); + disableExtension( + 'ext1', + SettingScope.Workspace, + extensionEnablementManager, + ); let activeExtensions = getActiveExtensions(); expect(activeExtensions).toHaveLength(0); - enableExtension('ext1', SettingScope.Workspace); + enableExtension( + 'ext1', + SettingScope.Workspace, + extensionEnablementManager, + ); activeExtensions = getActiveExtensions(); expect(activeExtensions).toHaveLength(1); expect(activeExtensions[0].name).toBe('ext1'); @@ -1641,8 +1498,17 @@ This extension will run the following MCP servers: name: 'ext1', version: '1.0.0', }); - disableExtension('ext1', SettingScope.Workspace); - enableExtension('ext1', SettingScope.Workspace); + const extensionEnablementManager = new ExtensionEnablementManager(); + disableExtension( + 'ext1', + SettingScope.Workspace, + extensionEnablementManager, + ); + enableExtension( + 'ext1', + SettingScope.Workspace, + extensionEnablementManager, + ); expect(mockLogExtensionEnable).toHaveBeenCalled(); expect(ExtensionEnableEvent).toHaveBeenCalledWith( diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index a3c4624ed4..2328f5426f 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -142,6 +142,7 @@ export function loadExtensions( const extension = loadExtension({ extensionDir, workspaceDir, + extensionEnablementManager, }); if (extension != null) { extensions.push(extension); @@ -151,10 +152,7 @@ export function loadExtensions( const uniqueExtensions = new Map(); for (const extension of extensions) { - if ( - !uniqueExtensions.has(extension.name) && - extensionEnablementManager.isEnabled(extension.name, workspaceDir) - ) { + if (!uniqueExtensions.has(extension.name)) { uniqueExtensions.set(extension.name, extension); } } @@ -165,7 +163,7 @@ export function loadExtensions( export function loadExtension( context: LoadExtensionContext, ): GeminiCLIExtension | null { - const { extensionDir, workspaceDir } = context; + const { extensionDir, workspaceDir, extensionEnablementManager } = context; if (!fs.statSync(extensionDir).isDirectory()) { return null; } @@ -181,6 +179,7 @@ export function loadExtension( let config = loadExtensionConfig({ extensionDir: effectiveExtensionPath, workspaceDir, + extensionEnablementManager, }); config = resolveEnvVarsInObject(config); @@ -230,7 +229,7 @@ export function loadExtension( installMetadata, mcpServers: config.mcpServers, excludeTools: config.excludeTools, - isActive: true, // Barring any other signals extensions should be considered Active. + isActive: extensionEnablementManager.isEnabled(config.name, workspaceDir), id, }; } catch (e) { @@ -245,6 +244,7 @@ export function loadExtension( export function loadExtensionByName( name: string, + extensionEnablementManager: ExtensionEnablementManager, workspaceDir: string = process.cwd(), ): GeminiCLIExtension | null { const userExtensionsDir = ExtensionStorage.getUserExtensionsDir(); @@ -257,7 +257,11 @@ export function loadExtensionByName( if (!fs.statSync(extensionDir).isDirectory()) { continue; } - const extension = loadExtension({ extensionDir, workspaceDir }); + const extension = loadExtension({ + extensionDir, + workspaceDir, + extensionEnablementManager, + }); if (extension && extension.name.toLowerCase() === name.toLowerCase()) { return extension; } @@ -294,25 +298,6 @@ function getContextFileNames(config: ExtensionConfig): string[] { return config.contextFileName; } -/** - * Returns an annotated list of extensions. If an extension is listed in enabledExtensionNames, it will be active. - * If enabledExtensionNames is empty, an extension is active unless it is disabled. - * @param extensions The base list of extensions. - * @param enabledExtensionNames The names of explicitly enabled extensions. - * @param workspaceDir The current workspace directory. - */ -export function annotateActiveExtensions( - extensions: GeminiCLIExtension[], - workspaceDir: string, - manager: ExtensionEnablementManager, -): GeminiCLIExtension[] { - manager.validateExtensionOverrides(extensions); - return extensions.map((extension) => ({ - ...extension, - isActive: manager.isEnabled(extension.name, workspaceDir), - })); -} - /** * Requests consent from the user to perform an action, by reading a Y/n * character from stdin. @@ -409,6 +394,7 @@ export async function installOrUpdateExtension( const telemetryConfig = getTelemetryConfig(cwd); let newExtensionConfig: ExtensionConfig | null = null; let localSourcePath: string | undefined; + const extensionEnablementManager = new ExtensionEnablementManager(); try { const settings = loadSettings(cwd).merged; @@ -480,6 +466,7 @@ export async function installOrUpdateExtension( newExtensionConfig = loadExtensionConfig({ extensionDir: localSourcePath, workspaceDir: cwd, + extensionEnablementManager, }); const newExtensionName = newExtensionConfig.name; @@ -555,7 +542,11 @@ export async function installOrUpdateExtension( 'success', ), ); - enableExtension(newExtensionConfig.name, SettingScope.User); + enableExtension( + newExtensionConfig.name, + SettingScope.User, + extensionEnablementManager, + ); } return newExtensionConfig!.name; @@ -567,6 +558,7 @@ export async function installOrUpdateExtension( newExtensionConfig = loadExtensionConfig({ extensionDir: localSourcePath, workspaceDir: cwd, + extensionEnablementManager, }); } catch { // Ignore error, this is just for logging. @@ -791,38 +783,38 @@ export function toOutputString( export function disableExtension( name: string, scope: SettingScope, + extensionEnablementManager: ExtensionEnablementManager, cwd: string = process.cwd(), ) { const config = getTelemetryConfig(cwd); if (scope === SettingScope.System || scope === SettingScope.SystemDefaults) { throw new Error('System and SystemDefaults scopes are not supported.'); } - const extension = loadExtensionByName(name, cwd); + const extension = loadExtensionByName(name, extensionEnablementManager, cwd); if (!extension) { throw new Error(`Extension with name ${name} does not exist.`); } - const manager = new ExtensionEnablementManager([name]); const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir(); - manager.disable(name, true, scopePath); + extensionEnablementManager.disable(name, true, scopePath); logExtensionDisable(config, new ExtensionDisableEvent(name, scope)); } export function enableExtension( name: string, scope: SettingScope, + extensionEnablementManager: ExtensionEnablementManager, cwd: string = process.cwd(), ) { if (scope === SettingScope.System || scope === SettingScope.SystemDefaults) { throw new Error('System and SystemDefaults scopes are not supported.'); } - const extension = loadExtensionByName(name, cwd); + const extension = loadExtensionByName(name, extensionEnablementManager, cwd); if (!extension) { throw new Error(`Extension with name ${name} does not exist.`); } - const manager = new ExtensionEnablementManager(); const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir(); - manager.enable(name, true, scopePath); + extensionEnablementManager.enable(name, true, scopePath); const config = getTelemetryConfig(cwd); logExtensionEnable(config, new ExtensionEnableEvent(name, scope)); } diff --git a/packages/cli/src/config/extensions/github.test.ts b/packages/cli/src/config/extensions/github.test.ts index c4874aa08b..43c69e4302 100644 --- a/packages/cli/src/config/extensions/github.test.ts +++ b/packages/cli/src/config/extensions/github.test.ts @@ -22,6 +22,7 @@ import * as path from 'node:path'; import * as tar from 'tar'; import * as archiver from 'archiver'; import type { GeminiCLIExtension } from '@google/gemini-cli-core'; +import { ExtensionEnablementManager } from './extensionEnablement.js'; const mockPlatform = vi.hoisted(() => vi.fn()); const mockArch = vi.hoisted(() => vi.fn()); @@ -149,7 +150,10 @@ describe('git extension helpers', () => { }, contextFiles: [], }; - const result = await checkForExtensionUpdate(extension); + const result = await checkForExtensionUpdate( + extension, + new ExtensionEnablementManager(), + ); expect(result).toBe(ExtensionUpdateState.NOT_UPDATABLE); }); @@ -166,7 +170,10 @@ describe('git extension helpers', () => { contextFiles: [], }; mockGit.getRemotes.mockResolvedValue([]); - const result = await checkForExtensionUpdate(extension); + const result = await checkForExtensionUpdate( + extension, + new ExtensionEnablementManager(), + ); expect(result).toBe(ExtensionUpdateState.ERROR); }); @@ -188,7 +195,10 @@ describe('git extension helpers', () => { mockGit.listRemote.mockResolvedValue('remote-hash\tHEAD'); mockGit.revparse.mockResolvedValue('local-hash'); - const result = await checkForExtensionUpdate(extension); + const result = await checkForExtensionUpdate( + extension, + new ExtensionEnablementManager(), + ); expect(result).toBe(ExtensionUpdateState.UPDATE_AVAILABLE); }); @@ -210,7 +220,10 @@ describe('git extension helpers', () => { mockGit.listRemote.mockResolvedValue('same-hash\tHEAD'); mockGit.revparse.mockResolvedValue('same-hash'); - const result = await checkForExtensionUpdate(extension); + const result = await checkForExtensionUpdate( + extension, + new ExtensionEnablementManager(), + ); expect(result).toBe(ExtensionUpdateState.UP_TO_DATE); }); @@ -228,7 +241,10 @@ describe('git extension helpers', () => { }; mockGit.getRemotes.mockRejectedValue(new Error('git error')); - const result = await checkForExtensionUpdate(extension); + const result = await checkForExtensionUpdate( + extension, + new ExtensionEnablementManager(), + ); expect(result).toBe(ExtensionUpdateState.ERROR); }); }); diff --git a/packages/cli/src/config/extensions/github.ts b/packages/cli/src/config/extensions/github.ts index 70b4cd94d5..367c80501f 100644 --- a/packages/cli/src/config/extensions/github.ts +++ b/packages/cli/src/config/extensions/github.ts @@ -20,6 +20,7 @@ import { EXTENSIONS_CONFIG_FILENAME, loadExtension } from '../extension.js'; import * as tar from 'tar'; import extract from 'extract-zip'; import { fetchJson, getGitHubToken } from './github_fetch.js'; +import { type ExtensionEnablementManager } from './extensionEnablement.js'; /** * Clones a Git repository to a specified local path. @@ -152,6 +153,7 @@ export async function fetchReleaseFromGithub( export async function checkForExtensionUpdate( extension: GeminiCLIExtension, + extensionEnablementManager: ExtensionEnablementManager, cwd: string = process.cwd(), ): Promise { const installMetadata = extension.installMetadata; @@ -159,6 +161,7 @@ export async function checkForExtensionUpdate( const newExtension = loadExtension({ extensionDir: installMetadata.source, workspaceDir: cwd, + extensionEnablementManager, }); if (!newExtension) { debugLogger.error( diff --git a/packages/cli/src/config/extensions/update.test.ts b/packages/cli/src/config/extensions/update.test.ts index 99a83737f3..f0ba5ba982 100644 --- a/packages/cli/src/config/extensions/update.test.ts +++ b/packages/cli/src/config/extensions/update.test.ts @@ -11,7 +11,6 @@ import * as path from 'node:path'; import { EXTENSIONS_CONFIG_FILENAME, INSTALL_METADATA_FILENAME, - annotateActiveExtensions, loadExtension, } from '../extension.js'; import { checkForAllExtensionUpdates, updateExtension } from './update.js'; @@ -128,18 +127,15 @@ describe('update tests', () => { ); }); mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]); - const extension = annotateActiveExtensions( - [ - loadExtension({ - extensionDir: targetExtDir, - workspaceDir: tempWorkspaceDir, - })!, - ], - process.cwd(), - new ExtensionEnablementManager(), - )[0]; + const extensionEnablementManager = new ExtensionEnablementManager(); + const extension = loadExtension({ + extensionDir: targetExtDir, + workspaceDir: tempWorkspaceDir, + extensionEnablementManager, + })!; const updateInfo = await updateExtension( extension, + extensionEnablementManager, tempHomeDir, async (_) => true, ExtensionUpdateState.UPDATE_AVAILABLE, @@ -185,18 +181,15 @@ describe('update tests', () => { mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]); const dispatch = vi.fn(); - const extension = annotateActiveExtensions( - [ - loadExtension({ - extensionDir, - workspaceDir: tempWorkspaceDir, - })!, - ], - process.cwd(), - new ExtensionEnablementManager(), - )[0]; + const extensionEnablementManager = new ExtensionEnablementManager(); + const extension = loadExtension({ + extensionDir, + workspaceDir: tempWorkspaceDir, + extensionEnablementManager, + })!; await updateExtension( extension, + extensionEnablementManager, tempHomeDir, async (_) => true, ExtensionUpdateState.UPDATE_AVAILABLE, @@ -235,19 +228,16 @@ describe('update tests', () => { mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]); const dispatch = vi.fn(); - const extension = annotateActiveExtensions( - [ - loadExtension({ - extensionDir, - workspaceDir: tempWorkspaceDir, - })!, - ], - process.cwd(), - new ExtensionEnablementManager(), - )[0]; + const extensionEnablementManager = new ExtensionEnablementManager(); + const extension = loadExtension({ + extensionDir, + workspaceDir: tempWorkspaceDir, + extensionEnablementManager, + })!; await expect( updateExtension( extension, + extensionEnablementManager, tempHomeDir, async (_) => true, ExtensionUpdateState.UPDATE_AVAILABLE, @@ -283,16 +273,12 @@ describe('update tests', () => { type: 'git', }, }); - const extension = annotateActiveExtensions( - [ - loadExtension({ - extensionDir, - workspaceDir: tempWorkspaceDir, - })!, - ], - process.cwd(), - new ExtensionEnablementManager(), - )[0]; + const extensionEnablementManager = new ExtensionEnablementManager(); + const extension = loadExtension({ + extensionDir, + workspaceDir: tempWorkspaceDir, + extensionEnablementManager, + })!; mockGit.getRemotes.mockResolvedValue([ { name: 'origin', refs: { fetch: 'https://some.git/repo' } }, @@ -303,6 +289,7 @@ describe('update tests', () => { const dispatch = vi.fn(); await checkForAllExtensionUpdates( [extension], + extensionEnablementManager, dispatch, tempWorkspaceDir, ); @@ -325,16 +312,12 @@ describe('update tests', () => { type: 'git', }, }); - const extension = annotateActiveExtensions( - [ - loadExtension({ - extensionDir, - workspaceDir: tempWorkspaceDir, - })!, - ], - process.cwd(), - new ExtensionEnablementManager(), - )[0]; + const extensionEnablementManager = new ExtensionEnablementManager(); + const extension = loadExtension({ + extensionDir, + workspaceDir: tempWorkspaceDir, + extensionEnablementManager, + })!; mockGit.getRemotes.mockResolvedValue([ { name: 'origin', refs: { fetch: 'https://some.git/repo' } }, @@ -345,6 +328,7 @@ describe('update tests', () => { const dispatch = vi.fn(); await checkForAllExtensionUpdates( [extension], + extensionEnablementManager, dispatch, tempWorkspaceDir, ); @@ -371,19 +355,16 @@ describe('update tests', () => { version: '1.0.0', installMetadata: { source: sourceExtensionDir, type: 'local' }, }); - const extension = annotateActiveExtensions( - [ - loadExtension({ - extensionDir: installedExtensionDir, - workspaceDir: tempWorkspaceDir, - })!, - ], - process.cwd(), - new ExtensionEnablementManager(), - )[0]; + const extensionEnablementManager = new ExtensionEnablementManager(); + const extension = loadExtension({ + extensionDir: installedExtensionDir, + workspaceDir: tempWorkspaceDir, + extensionEnablementManager, + })!; const dispatch = vi.fn(); await checkForAllExtensionUpdates( [extension], + extensionEnablementManager, dispatch, tempWorkspaceDir, ); @@ -410,19 +391,16 @@ describe('update tests', () => { version: '1.0.0', installMetadata: { source: sourceExtensionDir, type: 'local' }, }); - const extension = annotateActiveExtensions( - [ - loadExtension({ - extensionDir: installedExtensionDir, - workspaceDir: tempWorkspaceDir, - })!, - ], - process.cwd(), - new ExtensionEnablementManager(), - )[0]; + const extensionEnablementManager = new ExtensionEnablementManager(); + const extension = loadExtension({ + extensionDir: installedExtensionDir, + workspaceDir: tempWorkspaceDir, + extensionEnablementManager, + })!; const dispatch = vi.fn(); await checkForAllExtensionUpdates( [extension], + extensionEnablementManager, dispatch, tempWorkspaceDir, ); @@ -445,22 +423,19 @@ describe('update tests', () => { type: 'git', }, }); - const extension = annotateActiveExtensions( - [ - loadExtension({ - extensionDir, - workspaceDir: tempWorkspaceDir, - })!, - ], - process.cwd(), - new ExtensionEnablementManager(), - )[0]; + const extensionEnablementManager = new ExtensionEnablementManager(); + const extension = loadExtension({ + extensionDir, + workspaceDir: tempWorkspaceDir, + extensionEnablementManager, + })!; mockGit.getRemotes.mockRejectedValue(new Error('Git error')); const dispatch = vi.fn(); await checkForAllExtensionUpdates( [extension], + extensionEnablementManager, dispatch, tempWorkspaceDir, ); diff --git a/packages/cli/src/config/extensions/update.ts b/packages/cli/src/config/extensions/update.ts index 744d2b15a8..6f09fd7703 100644 --- a/packages/cli/src/config/extensions/update.ts +++ b/packages/cli/src/config/extensions/update.ts @@ -21,6 +21,7 @@ import { checkForExtensionUpdate } from './github.js'; import { debugLogger, type GeminiCLIExtension } from '@google/gemini-cli-core'; import * as fs from 'node:fs'; import { getErrorMessage } from '../../utils/errors.js'; +import { type ExtensionEnablementManager } from './extensionEnablement.js'; export interface ExtensionUpdateInfo { name: string; @@ -30,6 +31,7 @@ export interface ExtensionUpdateInfo { export async function updateExtension( extension: GeminiCLIExtension, + extensionEnablementManager: ExtensionEnablementManager, cwd: string = process.cwd(), requestConsent: (consent: string) => Promise, currentState: ExtensionUpdateState, @@ -67,6 +69,7 @@ export async function updateExtension( const previousExtensionConfig = await loadExtensionConfig({ extensionDir: extension.path, workspaceDir: cwd, + extensionEnablementManager, }); await installOrUpdateExtension( installMetadata, @@ -79,6 +82,7 @@ export async function updateExtension( const updatedExtension = loadExtension({ extensionDir: updatedExtensionStorage.getExtensionDir(), workspaceDir: cwd, + extensionEnablementManager, }); if (!updatedExtension) { dispatchExtensionStateUpdate({ @@ -120,6 +124,7 @@ export async function updateAllUpdatableExtensions( requestConsent: (consent: string) => Promise, extensions: GeminiCLIExtension[], extensionsState: Map, + extensionEnablementManager: ExtensionEnablementManager, dispatch: (action: ExtensionUpdateAction) => void, ): Promise { return ( @@ -133,6 +138,7 @@ export async function updateAllUpdatableExtensions( .map((extension) => updateExtension( extension, + extensionEnablementManager, cwd, requestConsent, extensionsState.get(extension.name)!.status, @@ -150,6 +156,7 @@ export interface ExtensionUpdateCheckResult { export async function checkForAllExtensionUpdates( extensions: GeminiCLIExtension[], + extensionEnablementManager: ExtensionEnablementManager, dispatch: (action: ExtensionUpdateAction) => void, cwd: string = process.cwd(), ): Promise { @@ -174,11 +181,12 @@ export async function checkForAllExtensionUpdates( }, }); promises.push( - checkForExtensionUpdate(extension, cwd).then((state) => - dispatch({ - type: 'SET_STATE', - payload: { name: extension.name, state }, - }), + checkForExtensionUpdate(extension, extensionEnablementManager, cwd).then( + (state) => + dispatch({ + type: 'SET_STATE', + payload: { name: extension.name, state }, + }), ), ); } diff --git a/packages/cli/src/config/extensions/variableSchema.ts b/packages/cli/src/config/extensions/variableSchema.ts index f38e1b1f81..4e0adf3582 100644 --- a/packages/cli/src/config/extensions/variableSchema.ts +++ b/packages/cli/src/config/extensions/variableSchema.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { ExtensionEnablementManager } from './extensionEnablement.js'; + export interface VariableDefinition { type: 'string'; description: string; @@ -18,6 +20,7 @@ export interface VariableSchema { export interface LoadExtensionContext { extensionDir: string; workspaceDir: string; + extensionEnablementManager: ExtensionEnablementManager; } const PATH_SEPARATOR_DEFINITION = { diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index d9bb8dfa92..5f0f07cfe4 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -50,7 +50,7 @@ import { import * as fs from 'node:fs'; // fs will be mocked separately import stripJsonComments from 'strip-json-comments'; // Will be mocked separately import { isWorkspaceTrusted } from './trustedFolders.js'; -import { disableExtension } from './extension.js'; +import { disableExtension, ExtensionStorage } from './extension.js'; // These imports will get the versions from the vi.mock('./settings.js', ...) factory. import { @@ -65,7 +65,8 @@ import { migrateDeprecatedSettings, SettingScope, } from './settings.js'; -import { FatalConfigError, GEMINI_DIR } from '@google/gemini-cli-core'; +import { FatalConfigError, GEMINI_DIR, Storage } from '@google/gemini-cli-core'; +import { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; const MOCK_WORKSPACE_DIR = '/mock/workspace'; // Use the (mocked) GEMINI_DIR for consistency @@ -93,9 +94,7 @@ vi.mock('fs', async (importOriginal) => { }; }); -vi.mock('./extension.js', () => ({ - disableExtension: vi.fn(), -})); +vi.mock('./extension.js'); vi.mock('strip-json-comments', () => ({ default: vi.fn((content) => content), @@ -2349,7 +2348,9 @@ describe('Settings Loading and Merging', () => { mockFsExistsSync = vi.mocked(fs.existsSync); mockFsReadFileSync = vi.mocked(fs.readFileSync); mockDisableExtension = vi.mocked(disableExtension); - + vi.mocked(ExtensionStorage.getUserExtensionsDir).mockReturnValue( + new Storage(osActual.homedir()).getExtensionsDir(), + ); (mockFsExistsSync as Mock).mockReturnValue(true); vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, @@ -2392,11 +2393,13 @@ describe('Settings Loading and Merging', () => { expect(mockDisableExtension).toHaveBeenCalledWith( 'user-ext-1', SettingScope.User, + expect.any(ExtensionEnablementManager), MOCK_WORKSPACE_DIR, ); expect(mockDisableExtension).toHaveBeenCalledWith( 'shared-ext', SettingScope.User, + expect.any(ExtensionEnablementManager), MOCK_WORKSPACE_DIR, ); @@ -2404,11 +2407,13 @@ describe('Settings Loading and Merging', () => { expect(mockDisableExtension).toHaveBeenCalledWith( 'workspace-ext-1', SettingScope.Workspace, + expect.any(ExtensionEnablementManager), MOCK_WORKSPACE_DIR, ); expect(mockDisableExtension).toHaveBeenCalledWith( 'shared-ext', SettingScope.Workspace, + expect.any(ExtensionEnablementManager), MOCK_WORKSPACE_DIR, ); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 59ffb42ce6..5311a370bc 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -31,6 +31,7 @@ import { resolveEnvVarsInObject } from '../utils/envVarResolver.js'; import { customDeepMerge, type MergeableObject } from '../utils/deepMerge.js'; import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js'; import { disableExtension } from './extension.js'; +import { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined { let current: SettingDefinition | undefined = undefined; @@ -755,8 +756,14 @@ export function migrateDeprecatedSettings( console.log( `Migrating deprecated extensions.disabled settings from ${scope} settings...`, ); + const extensionEnablementManager = new ExtensionEnablementManager(); for (const extension of settings.extensions.disabled ?? []) { - disableExtension(extension, scope, workspaceDir); + disableExtension( + extension, + scope, + extensionEnablementManager, + workspaceDir, + ); } const newExtensionsValue = { ...settings.extensions }; diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index a0fa434685..1f43ab7694 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -279,7 +279,7 @@ export async function main() { ? getNodeMemoryArgs(isDebugMode) : []; const sandboxConfig = await loadSandboxConfig(settings.merged, argv); - // We intentially omit the list of extensions here because extensions + // We intentionally omit the list of extensions here because extensions // should not impact auth or setting up the sandbox. // TODO(jacobr): refactor loadCliConfig so there is a minimal version // that only initializes enough config to enable refreshAuth or find @@ -289,7 +289,6 @@ export async function main() { const partialConfig = await loadCliConfig( settings.merged, [], - new ExtensionEnablementManager(), sessionId, argv, ); @@ -367,7 +366,6 @@ export async function main() { const config = await loadCliConfig( settings.merged, extensions, - extensionEnablementManager, sessionId, argv, ); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 03210c29d2..8216e2e925 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -90,6 +90,7 @@ import { useSessionStats } from './contexts/SessionContext.js'; import { useGitBranchName } from './hooks/useGitBranchName.js'; import { useExtensionUpdates } from './hooks/useExtensionUpdates.js'; import { ShellFocusContext } from './contexts/ShellFocusContext.js'; +import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js'; const CTRL_EXIT_PROMPT_DURATION_MS = 1000; const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000; @@ -159,6 +160,9 @@ export const AppContainer = (props: AppContainerProps) => { ); const extensions = config.getExtensions(); + const [extensionEnablementManager] = useState( + new ExtensionEnablementManager(config.getEnabledExtensions()), + ); const { extensionsUpdateState, extensionsUpdateStateInternal, @@ -167,6 +171,7 @@ export const AppContainer = (props: AppContainerProps) => { addConfirmUpdateExtensionRequest, } = useExtensionUpdates( extensions, + extensionEnablementManager, historyManager.addItem, config.getWorkingDir(), ); @@ -529,7 +534,7 @@ Logging in with Google... Please restart Gemini CLI to continue. config.getDebugMode(), config.getFileService(), settings.merged, - config.getExtensionContextFilePaths(), + config.getExtensions(), config.isTrustedFolder(), settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' config.getFileFilteringOptions(), diff --git a/packages/cli/src/ui/commands/directoryCommand.test.tsx b/packages/cli/src/ui/commands/directoryCommand.test.tsx index 12e20bff76..cfcb00d8ec 100644 --- a/packages/cli/src/ui/commands/directoryCommand.test.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.test.tsx @@ -44,7 +44,6 @@ describe('directoryCommand', () => { shouldLoadMemoryFromIncludeDirectories: () => false, getDebugMode: () => false, getFileService: () => ({}), - getExtensionContextFilePaths: () => [], getFileFilteringOptions: () => ({ ignore: [], include: [] }), setUserMemory: vi.fn(), setGeminiMdFileCount: vi.fn(), diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index 966ad2eb1d..b174b1d8d5 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -103,7 +103,7 @@ export const directoryCommand: SlashCommand = { ], config.getDebugMode(), config.getFileService(), - config.getExtensionContextFilePaths(), + config.getExtensions(), config.getFolderTrust(), context.services.settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' diff --git a/packages/cli/src/ui/commands/memoryCommand.test.ts b/packages/cli/src/ui/commands/memoryCommand.test.ts index 2a4ea30364..b1f65a8a5f 100644 --- a/packages/cli/src/ui/commands/memoryCommand.test.ts +++ b/packages/cli/src/ui/commands/memoryCommand.test.ts @@ -176,7 +176,7 @@ describe('memoryCommand', () => { getWorkingDir: () => '/test/dir', getDebugMode: () => false, getFileService: () => ({}) as FileDiscoveryService, - getExtensionContextFilePaths: () => [], + getExtensions: () => [], shouldLoadMemoryFromIncludeDirectories: () => false, getWorkspaceContext: () => ({ getDirectories: () => [], diff --git a/packages/cli/src/ui/commands/memoryCommand.ts b/packages/cli/src/ui/commands/memoryCommand.ts index 0095cfd227..988c611291 100644 --- a/packages/cli/src/ui/commands/memoryCommand.ts +++ b/packages/cli/src/ui/commands/memoryCommand.ts @@ -91,7 +91,7 @@ export const memoryCommand: SlashCommand = { config.getDebugMode(), config.getFileService(), settings.merged, - config.getExtensionContextFilePaths(), + config.getExtensions(), config.isTrustedFolder(), settings.merged.context?.importFormat || 'tree', config.getFileFilteringOptions(), diff --git a/packages/cli/src/ui/components/views/ExtensionsList.test.tsx b/packages/cli/src/ui/components/views/ExtensionsList.test.tsx index 9067e34738..7e7da01e39 100644 --- a/packages/cli/src/ui/components/views/ExtensionsList.test.tsx +++ b/packages/cli/src/ui/components/views/ExtensionsList.test.tsx @@ -29,7 +29,6 @@ describe('', () => { const mockUIState = ( extensions: unknown[], extensionsUpdateState: Map, - disabledExtensions: string[] = [], ) => { mockUseUIState.mockReturnValue({ commandContext: createMockCommandContext({ @@ -37,13 +36,6 @@ describe('', () => { config: { getExtensions: () => extensions, }, - settings: { - merged: { - extensions: { - disabled: disabledExtensions, - }, - }, - }, }, }), extensionsUpdateState, @@ -58,7 +50,7 @@ describe('', () => { }); it('should render a list of extensions with their version and status', () => { - mockUIState(mockExtensions, new Map(), ['ext-disabled']); + mockUIState(mockExtensions, new Map()); const { lastFrame } = render(); const output = lastFrame(); expect(output).toContain('ext-one (v1.0.0) - active'); diff --git a/packages/cli/src/ui/components/views/ExtensionsList.tsx b/packages/cli/src/ui/components/views/ExtensionsList.tsx index 742bee1304..3a78518c8f 100644 --- a/packages/cli/src/ui/components/views/ExtensionsList.tsx +++ b/packages/cli/src/ui/components/views/ExtensionsList.tsx @@ -11,8 +11,6 @@ import { ExtensionUpdateState } from '../../state/extensions.js'; export const ExtensionsList = () => { const { commandContext, extensionsUpdateState } = useUIState(); const allExtensions = commandContext.services.config!.getExtensions(); - const settings = commandContext.services.settings; - const disabledExtensions = settings.merged.extensions?.disabled ?? []; if (allExtensions.length === 0) { return No extensions installed.; @@ -24,8 +22,9 @@ export const ExtensionsList = () => { {allExtensions.map((ext) => { const state = extensionsUpdateState.get(ext.name); - const isActive = !disabledExtensions.includes(ext.name); + const isActive = ext.isActive; const activeString = isActive ? 'active' : 'disabled'; + const activeColor = isActive ? 'green' : 'grey'; let stateColor = 'gray'; const stateText = state || 'unknown state'; @@ -55,7 +54,7 @@ export const ExtensionsList = () => { {`${ext.name} (v${ext.version})`} - {` - ${activeString}`} + {` - ${activeString}`} {{` (${stateText})`}} diff --git a/packages/cli/src/ui/components/views/McpStatus.tsx b/packages/cli/src/ui/components/views/McpStatus.tsx index d865915de7..f34b7e3662 100644 --- a/packages/cli/src/ui/components/views/McpStatus.tsx +++ b/packages/cli/src/ui/components/views/McpStatus.tsx @@ -115,8 +115,8 @@ export const McpStatus: React.FC = ({ } let serverDisplayName = serverName; - if (server.extensionName) { - serverDisplayName += ` (from ${server.extensionName})`; + if (server.extension?.name) { + serverDisplayName += ` (from ${server.extension?.name})`; } const toolCount = serverTools.length; diff --git a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts index 770c060e8a..8ec0689cc6 100644 --- a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts +++ b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts @@ -8,21 +8,18 @@ import { vi } from 'vitest'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { - annotateActiveExtensions, - loadExtension, -} from '../../config/extension.js'; +import { loadExtension } from '../../config/extension.js'; import { createExtension } from '../../test-utils/createExtension.js'; import { useExtensionUpdates } from './useExtensionUpdates.js'; import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core'; import { renderHook, waitFor } from '@testing-library/react'; import { MessageType } from '../types.js'; -import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js'; import { checkForAllExtensionUpdates, updateExtension, } from '../../config/extensions/update.js'; import { ExtensionUpdateState } from '../state/extensions.js'; +import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js'; vi.mock('os', async (importOriginal) => { const mockedOs = await importOriginal(); @@ -76,7 +73,7 @@ describe('useExtensionUpdates', () => { const cwd = '/test/cwd'; vi.mocked(checkForAllExtensionUpdates).mockImplementation( - async (_extensions, dispatch, _cwd) => { + async (_extensions, _extensionEnablementManager, dispatch, _cwd) => { dispatch({ type: 'SET_STATE', payload: { @@ -88,7 +85,12 @@ describe('useExtensionUpdates', () => { ); renderHook(() => - useExtensionUpdates(extensions as GeminiCLIExtension[], addItem, cwd), + useExtensionUpdates( + extensions as GeminiCLIExtension[], + new ExtensionEnablementManager(), + addItem, + cwd, + ), ); await waitFor(() => { @@ -113,16 +115,17 @@ describe('useExtensionUpdates', () => { autoUpdate: true, }, }); - const extension = annotateActiveExtensions( - [loadExtension({ extensionDir, workspaceDir: tempHomeDir })!], - tempHomeDir, - new ExtensionEnablementManager(), - )[0]; + const extensionEnablementManager = new ExtensionEnablementManager(); + const extension = loadExtension({ + extensionDir, + workspaceDir: tempHomeDir, + extensionEnablementManager, + })!; const addItem = vi.fn(); vi.mocked(checkForAllExtensionUpdates).mockImplementation( - async (_extensions, dispatch, _cwd) => { + async (_extensions, _extensionEnablementManager, dispatch, _cwd) => { dispatch({ type: 'SET_STATE', payload: { @@ -139,7 +142,14 @@ describe('useExtensionUpdates', () => { name: '', }); - renderHook(() => useExtensionUpdates([extension], addItem, tempHomeDir)); + renderHook(() => + useExtensionUpdates( + [extension], + extensionEnablementManager, + addItem, + tempHomeDir, + ), + ); await waitFor( () => { @@ -177,25 +187,24 @@ describe('useExtensionUpdates', () => { }, }); - const extensions = annotateActiveExtensions( - [ - loadExtension({ - extensionDir: extensionDir1, - workspaceDir: tempHomeDir, - })!, - loadExtension({ - extensionDir: extensionDir2, - workspaceDir: tempHomeDir, - })!, - ], - tempHomeDir, - new ExtensionEnablementManager(), - ); + const extensionEnablementManager = new ExtensionEnablementManager(); + const extensions = [ + loadExtension({ + extensionDir: extensionDir1, + workspaceDir: tempHomeDir, + extensionEnablementManager, + })!, + loadExtension({ + extensionDir: extensionDir2, + workspaceDir: tempHomeDir, + extensionEnablementManager, + })!, + ]; const addItem = vi.fn(); vi.mocked(checkForAllExtensionUpdates).mockImplementation( - async (_extensions, dispatch, _cwd) => { + async (_extensions, _extensionEnablementManager, dispatch, _cwd) => { dispatch({ type: 'SET_STATE', payload: { @@ -225,7 +234,14 @@ describe('useExtensionUpdates', () => { name: '', }); - renderHook(() => useExtensionUpdates(extensions, addItem, tempHomeDir)); + renderHook(() => + useExtensionUpdates( + extensions, + extensionEnablementManager, + addItem, + tempHomeDir, + ), + ); await waitFor( () => { @@ -282,7 +298,7 @@ describe('useExtensionUpdates', () => { const cwd = '/test/cwd'; vi.mocked(checkForAllExtensionUpdates).mockImplementation( - async (_extensions, dispatch, _cwd) => { + async (_extensions, _extensionEnablementManager, dispatch, _cwd) => { dispatch({ type: 'BATCH_CHECK_START' }); dispatch({ type: 'SET_STATE', @@ -303,8 +319,14 @@ describe('useExtensionUpdates', () => { }, ); + const extensionEnablementManager = new ExtensionEnablementManager(); renderHook(() => - useExtensionUpdates(extensions as GeminiCLIExtension[], addItem, cwd), + useExtensionUpdates( + extensions as GeminiCLIExtension[], + extensionEnablementManager, + addItem, + cwd, + ), ); await waitFor(() => { diff --git a/packages/cli/src/ui/hooks/useExtensionUpdates.ts b/packages/cli/src/ui/hooks/useExtensionUpdates.ts index 5908e298b1..5103cde875 100644 --- a/packages/cli/src/ui/hooks/useExtensionUpdates.ts +++ b/packages/cli/src/ui/hooks/useExtensionUpdates.ts @@ -23,6 +23,7 @@ import { type ExtensionUpdateInfo, } from '../../config/extension.js'; import { checkExhaustive } from '../../utils/checks.js'; +import type { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js'; type ConfirmationRequestWrapper = { prompt: React.ReactNode; @@ -49,6 +50,7 @@ function confirmationRequestsReducer( export const useExtensionUpdates = ( extensions: GeminiCLIExtension[], + extensionEnablementManager: ExtensionEnablementManager, addItem: UseHistoryManagerReturn['addItem'], cwd: string, ) => { @@ -93,11 +95,13 @@ export const useExtensionUpdates = ( if (extensionsToCheck.length === 0) return; checkForAllExtensionUpdates( extensionsToCheck, + extensionEnablementManager, dispatchExtensionStateUpdate, cwd, ); }, [ extensions, + extensionEnablementManager, extensionsUpdateState.extensionStatuses, cwd, dispatchExtensionStateUpdate, @@ -154,6 +158,7 @@ export const useExtensionUpdates = ( } else { const updatePromise = updateExtension( extension, + extensionEnablementManager, cwd, (description) => requestConsentInteractive( @@ -210,6 +215,7 @@ export const useExtensionUpdates = ( } }, [ extensions, + extensionEnablementManager, extensionsUpdateState, addConfirmUpdateExtensionRequest, addItem, diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts index 9cc13a6512..33d1d596ba 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/zed-integration/zedIntegration.ts @@ -44,7 +44,6 @@ import { z } from 'zod'; import { randomUUID } from 'node:crypto'; import type { CliArgs } from '../config/config.js'; import { loadCliConfig } from '../config/config.js'; -import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js'; /** * Resolves the model to use based on the current configuration. @@ -206,7 +205,6 @@ class GeminiAgent { const config = await loadCliConfig( settings, this.extensions, - new ExtensionEnablementManager(this.argv.extensions), sessionId, this.argv, cwd, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 1b72f19e89..584e928cb0 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -189,7 +189,7 @@ export class MCPServerConfig { readonly description?: string, readonly includeTools?: string[], readonly excludeTools?: string[], - readonly extensionName?: string, + readonly extension?: GeminiCLIExtension, // OAuth configuration readonly oauth?: MCPOAuthConfig, readonly authProviderType?: AuthProviderType, @@ -249,11 +249,11 @@ export interface ConfigParameters { includeDirectories?: string[]; bugCommand?: BugCommandSettings; model: string; - extensionContextFilePaths?: string[]; maxSessionTurns?: number; experimentalZedIntegration?: boolean; listExtensions?: boolean; extensions?: GeminiCLIExtension[]; + enabledExtensions?: string[]; blockedMcpServers?: Array<{ name: string; extensionName: string }>; noBrowser?: boolean; summarizeToolOutput?: Record; @@ -332,7 +332,6 @@ export class Config { private readonly cwd: string; private readonly bugCommand: BugCommandSettings | undefined; private model: string; - private readonly extensionContextFilePaths: string[]; private readonly noBrowser: boolean; private readonly folderTrust: boolean; private ideMode: boolean; @@ -341,6 +340,7 @@ export class Config { private readonly maxSessionTurns: number; private readonly listExtensions: boolean; private readonly _extensions: GeminiCLIExtension[]; + private readonly _enabledExtensions: string[]; private readonly _blockedMcpServers: Array<{ name: string; extensionName: string; @@ -436,12 +436,12 @@ export class Config { this.fileDiscoveryService = params.fileDiscoveryService ?? null; this.bugCommand = params.bugCommand; this.model = params.model; - this.extensionContextFilePaths = params.extensionContextFilePaths ?? []; this.maxSessionTurns = params.maxSessionTurns ?? -1; this.experimentalZedIntegration = params.experimentalZedIntegration ?? false; this.listExtensions = params.listExtensions ?? false; this._extensions = params.extensions ?? []; + this._enabledExtensions = params.enabledExtensions ?? []; this._blockedMcpServers = params.blockedMcpServers ?? []; this.noBrowser = params.noBrowser ?? false; this.summarizeToolOutput = params.summarizeToolOutput; @@ -542,7 +542,7 @@ export class Config { async refreshAuth(authMethod: AuthType) { // Vertex and Genai have incompatible encryption and sending history with - // throughtSignature from Genai to Vertex will fail, we need to strip them + // thoughtSignature from Genai to Vertex will fail, we need to strip them if ( this.contentGeneratorConfig?.authType === AuthType.USE_GEMINI && authMethod === AuthType.LOGIN_WITH_GOOGLE @@ -869,10 +869,6 @@ export class Config { return this.usageStatisticsEnabled; } - getExtensionContextFilePaths(): string[] { - return this.extensionContextFilePaths; - } - getExperimentalZedIntegration(): boolean { return this.experimentalZedIntegration; } @@ -889,6 +885,12 @@ export class Config { return this._extensions; } + // The list of explicitly enabled extensions, if any were given, may contain + // the string "none". + getEnabledExtensions(): string[] { + return this._enabledExtensions; + } + getBlockedMcpServers(): Array<{ name: string; extensionName: string }> { return this._blockedMcpServers; } diff --git a/packages/core/src/tools/mcp-client-manager.test.ts b/packages/core/src/tools/mcp-client-manager.test.ts index 9611f823be..dc0560107f 100644 --- a/packages/core/src/tools/mcp-client-manager.test.ts +++ b/packages/core/src/tools/mcp-client-manager.test.ts @@ -8,8 +8,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { McpClientManager } from './mcp-client-manager.js'; import { McpClient } from './mcp-client.js'; import type { ToolRegistry } from './tool-registry.js'; -import type { PromptRegistry } from '../prompts/prompt-registry.js'; -import type { WorkspaceContext } from '../utils/workspaceContext.js'; import type { Config } from '../config/config.js'; vi.mock('./mcp-client.js', async () => { @@ -38,18 +36,16 @@ describe('McpClientManager', () => { vi.mocked(McpClient).mockReturnValue( mockedMcpClient as unknown as McpClient, ); - const manager = new McpClientManager( - { - 'test-server': {}, - }, - '', - {} as ToolRegistry, - {} as PromptRegistry, - false, - {} as WorkspaceContext, - ); + const manager = new McpClientManager({} as ToolRegistry); await manager.discoverAllMcpTools({ isTrustedFolder: () => true, + getMcpServers: () => ({ + 'test-server': {}, + }), + getMcpServerCommand: () => '', + getPromptRegistry: () => {}, + getDebugMode: () => false, + getWorkspaceContext: () => {}, } as unknown as Config); expect(mockedMcpClient.connect).toHaveBeenCalledOnce(); expect(mockedMcpClient.discover).toHaveBeenCalledOnce(); @@ -65,18 +61,16 @@ describe('McpClientManager', () => { vi.mocked(McpClient).mockReturnValue( mockedMcpClient as unknown as McpClient, ); - const manager = new McpClientManager( - { - 'test-server': {}, - }, - '', - {} as ToolRegistry, - {} as PromptRegistry, - false, - {} as WorkspaceContext, - ); + const manager = new McpClientManager({} as ToolRegistry); await manager.discoverAllMcpTools({ isTrustedFolder: () => false, + getMcpServers: () => ({ + 'test-server': {}, + }), + getMcpServerCommand: () => '', + getPromptRegistry: () => {}, + getDebugMode: () => false, + getWorkspaceContext: () => {}, } as unknown as Config); expect(mockedMcpClient.connect).not.toHaveBeenCalled(); expect(mockedMcpClient.discover).not.toHaveBeenCalled(); diff --git a/packages/core/src/tools/mcp-client-manager.ts b/packages/core/src/tools/mcp-client-manager.ts index 93e25ea8b2..ec05563d3d 100644 --- a/packages/core/src/tools/mcp-client-manager.ts +++ b/packages/core/src/tools/mcp-client-manager.ts @@ -4,9 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { Config, MCPServerConfig } from '../config/config.js'; +import type { Config } from '../config/config.js'; import type { ToolRegistry } from './tool-registry.js'; -import type { PromptRegistry } from '../prompts/prompt-registry.js'; import { McpClient, MCPDiscoveryState, @@ -14,7 +13,6 @@ import { } from './mcp-client.js'; import { getErrorMessage } from '../utils/errors.js'; import type { EventEmitter } from 'node:events'; -import type { WorkspaceContext } from '../utils/workspaceContext.js'; /** * Manages the lifecycle of multiple MCP clients, including local child processes. @@ -23,30 +21,12 @@ import type { WorkspaceContext } from '../utils/workspaceContext.js'; */ export class McpClientManager { private clients: Map = new Map(); - private readonly mcpServers: Record; - private readonly mcpServerCommand: string | undefined; private readonly toolRegistry: ToolRegistry; - private readonly promptRegistry: PromptRegistry; - private readonly debugMode: boolean; - private readonly workspaceContext: WorkspaceContext; private discoveryState: MCPDiscoveryState = MCPDiscoveryState.NOT_STARTED; private readonly eventEmitter?: EventEmitter; - constructor( - mcpServers: Record, - mcpServerCommand: string | undefined, - toolRegistry: ToolRegistry, - promptRegistry: PromptRegistry, - debugMode: boolean, - workspaceContext: WorkspaceContext, - eventEmitter?: EventEmitter, - ) { - this.mcpServers = mcpServers; - this.mcpServerCommand = mcpServerCommand; + constructor(toolRegistry: ToolRegistry, eventEmitter?: EventEmitter) { this.toolRegistry = toolRegistry; - this.promptRegistry = promptRegistry; - this.debugMode = debugMode; - this.workspaceContext = workspaceContext; this.eventEmitter = eventEmitter; } @@ -62,22 +42,23 @@ export class McpClientManager { await this.stop(); const servers = populateMcpServerCommand( - this.mcpServers, - this.mcpServerCommand, + cliConfig.getMcpServers() || {}, + cliConfig.getMcpServerCommand(), ); this.discoveryState = MCPDiscoveryState.IN_PROGRESS; this.eventEmitter?.emit('mcp-client-update', this.clients); - const discoveryPromises = Object.entries(servers).map( - async ([name, config]) => { + const discoveryPromises = Object.entries(servers) + .filter(([_, config]) => !config.extension || config.extension.isActive) + .map(async ([name, config]) => { const client = new McpClient( name, config, this.toolRegistry, - this.promptRegistry, - this.workspaceContext, - this.debugMode, + cliConfig.getPromptRegistry(), + cliConfig.getWorkspaceContext(), + cliConfig.getDebugMode(), ); this.clients.set(name, client); @@ -95,8 +76,7 @@ export class McpClientManager { )}`, ); } - }, - ); + }); await Promise.all(discoveryPromises); this.discoveryState = MCPDiscoveryState.COMPLETED; diff --git a/packages/core/src/tools/tool-registry.ts b/packages/core/src/tools/tool-registry.ts index 627412f38a..213efc4b4b 100644 --- a/packages/core/src/tools/tool-registry.ts +++ b/packages/core/src/tools/tool-registry.ts @@ -174,15 +174,7 @@ export class ToolRegistry { constructor(config: Config, eventEmitter?: EventEmitter) { this.config = config; - this.mcpClientManager = new McpClientManager( - this.config.getMcpServers() ?? {}, - this.config.getMcpServerCommand(), - this, - this.config.getPromptRegistry(), - this.config.getDebugMode(), - this.config.getWorkspaceContext(), - eventEmitter, - ); + this.mcpClientManager = new McpClientManager(this, eventEmitter); } /** diff --git a/packages/core/src/utils/memoryDiscovery.test.ts b/packages/core/src/utils/memoryDiscovery.test.ts index 0d928b76aa..6d7d4da971 100644 --- a/packages/core/src/utils/memoryDiscovery.test.ts +++ b/packages/core/src/utils/memoryDiscovery.test.ts @@ -15,6 +15,7 @@ import { } from '../tools/memoryTool.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { GEMINI_DIR } from './paths.js'; +import type { GeminiCLIExtension } from '../config/config.js'; vi.mock('os', async (importOriginal) => { const actualOs = await importOriginal(); @@ -87,7 +88,7 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions false, // untrusted ); @@ -116,7 +117,7 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions false, // untrusted ); @@ -132,7 +133,7 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -154,7 +155,7 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -181,7 +182,7 @@ default context content [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -212,7 +213,7 @@ custom context content [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -247,7 +248,7 @@ cwd context content [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -279,7 +280,7 @@ Subdir custom memory [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -311,7 +312,7 @@ Src directory memory [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -355,7 +356,7 @@ Subdir memory [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -408,7 +409,7 @@ Subdir memory [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, 'tree', { @@ -444,7 +445,7 @@ My code memory [], true, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, 'tree', // importFormat { @@ -466,7 +467,7 @@ My code memory [], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -488,7 +489,12 @@ My code memory [], false, new FileDiscoveryService(projectRoot), - [extensionFilePath], + [ + { + contextFiles: [extensionFilePath], + isActive: true, + } as GeminiCLIExtension, + ], // extensions DEFAULT_FOLDER_TRUST, ); @@ -515,7 +521,7 @@ Extension memory content [includedDir], false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -550,7 +556,7 @@ included directory memory createdFiles.map((f) => path.dirname(f)), false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); @@ -585,7 +591,7 @@ included directory memory [childDir, parentDir], // Deliberately include duplicates false, new FileDiscoveryService(projectRoot), - [], + [], // extensions DEFAULT_FOLDER_TRUST, ); diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts index a25309cefd..a1f8bd2dfe 100644 --- a/packages/core/src/utils/memoryDiscovery.ts +++ b/packages/core/src/utils/memoryDiscovery.ts @@ -15,6 +15,7 @@ import { processImports } from './memoryImportProcessor.js'; import type { FileFilteringOptions } from '../config/constants.js'; import { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS } from '../config/constants.js'; import { GEMINI_DIR } from './paths.js'; +import type { GeminiCLIExtension } from '../config/config.js'; // Simple console logger, similar to the one previously in CLI's config.ts // TODO: Integrate with a more robust server-side logger if available/appropriate. @@ -84,7 +85,6 @@ async function getGeminiMdFilePathsInternal( userHomePath: string, debugMode: boolean, fileService: FileDiscoveryService, - extensionContextFilePaths: string[] = [], folderTrust: boolean, fileFilteringOptions: FileFilteringOptions, maxDirs: number, @@ -107,7 +107,6 @@ async function getGeminiMdFilePathsInternal( userHomePath, debugMode, fileService, - extensionContextFilePaths, folderTrust, fileFilteringOptions, maxDirs, @@ -137,7 +136,6 @@ async function getGeminiMdFilePathsInternalForEachDir( userHomePath: string, debugMode: boolean, fileService: FileDiscoveryService, - extensionContextFilePaths: string[] = [], folderTrust: boolean, fileFilteringOptions: FileFilteringOptions, maxDirs: number, @@ -226,11 +224,6 @@ async function getGeminiMdFilePathsInternalForEachDir( } } - // Add extension context file paths. - for (const extensionPath of extensionContextFilePaths) { - allPaths.add(extensionPath); - } - const finalPaths = Array.from(allPaths); if (debugMode) @@ -343,7 +336,7 @@ export async function loadServerHierarchicalMemory( includeDirectoriesToReadGemini: readonly string[], debugMode: boolean, fileService: FileDiscoveryService, - extensionContextFilePaths: string[] = [], + extensions: GeminiCLIExtension[], folderTrust: boolean, importFormat: 'flat' | 'tree' = 'tree', fileFilteringOptions?: FileFilteringOptions, @@ -363,11 +356,18 @@ export async function loadServerHierarchicalMemory( userHomePath, debugMode, fileService, - extensionContextFilePaths, folderTrust, fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, maxDirs, ); + + // Add extension file paths separately since they may be conditionally enabled. + filePaths.push( + ...extensions + .filter((ext) => ext.isActive) + .flatMap((ext) => ext.contextFiles), + ); + if (filePaths.length === 0) { if (debugMode) logger.debug('No GEMINI.md files found in hierarchy of the workspace.');