Rationalize different Extension typings (#10435)

This commit is contained in:
Zack Birkenbuel
2025-10-08 07:31:41 -07:00
committed by GitHub
parent 5d09ab7eb3
commit 8980276b20
19 changed files with 300 additions and 256 deletions
+15 -14
View File
@@ -20,16 +20,16 @@ import {
GEMINI_CONFIG_DIR,
DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_GEMINI_MODEL,
type GeminiCLIExtension,
} from '@google/gemini-cli-core';
import { logger } from '../utils/logger.js';
import type { Settings } from './settings.js';
import type { Extension } from './extension.js';
import { type AgentSettings, CoderAgentEvent } from '../types.js';
export async function loadConfig(
settings: Settings,
extensions: Extension[],
extensions: GeminiCLIExtension[],
taskId: string,
): Promise<Config> {
const mcpServers = mergeMcpServers(settings, extensions);
@@ -118,20 +118,21 @@ export async function loadConfig(
return config;
}
export function mergeMcpServers(settings: Settings, extensions: Extension[]) {
export function mergeMcpServers(
settings: Settings,
extensions: GeminiCLIExtension[],
) {
const mcpServers = { ...(settings.mcpServers || {}) };
for (const extension of extensions) {
Object.entries(extension.config.mcpServers || {}).forEach(
([key, server]) => {
if (mcpServers[key]) {
console.warn(
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
);
return;
}
mcpServers[key] = server;
},
);
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
if (mcpServers[key]) {
console.warn(
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
);
return;
}
mcpServers[key] = server;
});
}
return mcpServers;
}
+49 -17
View File
@@ -6,7 +6,11 @@
// Copied exactly from packages/cli/src/config/extension.ts, last PR #1026
import type { MCPServerConfig } from '@google/gemini-cli-core';
import type {
MCPServerConfig,
ExtensionInstallMetadata,
GeminiCLIExtension,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
@@ -14,47 +18,51 @@ import { logger } from '../utils/logger.js';
export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
export interface Extension {
config: ExtensionConfig;
contextFiles: string[];
}
export interface ExtensionConfig {
/**
* Extension definition as written to disk in gemini-extension.json files.
* This should *not* be referenced outside of the logic for reading files.
* If information is required for manipulating extensions (load, unload, update)
* outside of the loading process that data needs to be stored on the
* GeminiCLIExtension class defined in Core.
*/
interface ExtensionConfig {
name: string;
version: string;
mcpServers?: Record<string, MCPServerConfig>;
contextFileName?: string | string[];
excludeTools?: string[];
}
export function loadExtensions(workspaceDir: string): Extension[] {
export function loadExtensions(workspaceDir: string): GeminiCLIExtension[] {
const allExtensions = [
...loadExtensionsFromDir(workspaceDir),
...loadExtensionsFromDir(os.homedir()),
];
const uniqueExtensions: Extension[] = [];
const uniqueExtensions: GeminiCLIExtension[] = [];
const seenNames = new Set<string>();
for (const extension of allExtensions) {
if (!seenNames.has(extension.config.name)) {
if (!seenNames.has(extension.name)) {
logger.info(
`Loading extension: ${extension.config.name} (version: ${extension.config.version})`,
`Loading extension: ${extension.name} (version: ${extension.version})`,
);
uniqueExtensions.push(extension);
seenNames.add(extension.config.name);
seenNames.add(extension.name);
}
}
return uniqueExtensions;
}
function loadExtensionsFromDir(dir: string): Extension[] {
function loadExtensionsFromDir(dir: string): GeminiCLIExtension[] {
const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME);
if (!fs.existsSync(extensionsDir)) {
return [];
}
const extensions: Extension[] = [];
const extensions: GeminiCLIExtension[] = [];
for (const subdir of fs.readdirSync(extensionsDir)) {
const extensionDir = path.join(extensionsDir, subdir);
@@ -66,7 +74,7 @@ function loadExtensionsFromDir(dir: string): Extension[] {
return extensions;
}
function loadExtension(extensionDir: string): Extension | null {
function loadExtension(extensionDir: string): GeminiCLIExtension | null {
if (!fs.statSync(extensionDir).isDirectory()) {
logger.error(
`Warning: unexpected file ${extensionDir} in extensions directory.`,
@@ -92,14 +100,22 @@ function loadExtension(extensionDir: string): Extension | null {
return null;
}
const installMetadata = loadInstallMetadata(extensionDir);
const contextFiles = getContextFileNames(config)
.map((contextFileName) => path.join(extensionDir, contextFileName))
.filter((contextFilePath) => fs.existsSync(contextFilePath));
return {
config,
name: config.name,
version: config.version,
path: extensionDir,
contextFiles,
};
installMetadata,
mcpServers: config.mcpServers,
excludeTools: config.excludeTools,
isActive: true, // Barring any other signals extensions should be considered Active.
} as GeminiCLIExtension;
} catch (e) {
logger.error(
`Warning: error parsing extension config in ${configFilePath}: ${e}`,
@@ -116,3 +132,19 @@ function getContextFileNames(config: ExtensionConfig): string[] {
}
return config.contextFileName;
}
export function loadInstallMetadata(
extensionDir: string,
): ExtensionInstallMetadata | undefined {
const metadataFilePath = path.join(extensionDir, INSTALL_METADATA_FILENAME);
try {
const configContent = fs.readFileSync(metadataFilePath, 'utf-8');
const metadata = JSON.parse(configContent) as ExtensionInstallMetadata;
return metadata;
} catch (e) {
logger.warn(
`Failed to load or parse extension install metadata at ${metadataFilePath}: ${e}`,
);
return undefined;
}
}