Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ The `gemini-extension.json` file contains the configuration for the extension. T
- `name`: The name of the extension. This is used to uniquely identify the extension and for conflict resolution when extension commands have the same name as user or project commands.
- `version`: The version of the extension.
- `mcpServers`: A map of MCP servers to configure. The key is the name of the server, and the value is the server configuration. These servers will be loaded on startup just like MCP servers configured in a [`settings.json` file](./cli/configuration.md). If both an extension and a `settings.json` file configure an MCP server with the same name, the server defined in the `settings.json` file takes precedence.
- `contextFileName`: The name of the file that contains the context for the extension. This will be used to load the context from the workspace. If this property is not used but a `GEMINI.md` file is present in your extension directory, then that file will be loaded.
- `contextFileNames`: An array of file names that contain the context for the extension. These will be used to load the context from the workspace. If this property is not used, `contextFileName` will be checked. If neither is present, `GEMINI.md` will be loaded if it exists in your extension directory.
- `contextFileName`: (Deprecated) The name of the file that contains the context for the extension. Use `contextFileNames` instead.
- `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command.

When Gemini CLI starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence.
Expand Down
64 changes: 63 additions & 1 deletion packages/cli/src/config/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,53 @@ describe('loadExtensions', () => {
]);
});

it('should load multiple context file paths from the extension config', () => {
createExtension({
extensionsDir: workspaceExtensionsDir,
name: 'ext1',
version: '1.0.0',
addContextFile: false,
contextFileNames: ['my-context-file.md', 'another-context.md'],
});

const extensions = loadExtensions(tempWorkspaceDir);

expect(extensions).toHaveLength(1);
const ext1 = extensions.find((e) => e.config.name === 'ext1');
expect(ext1?.contextFiles).toEqual([
path.join(workspaceExtensionsDir, 'ext1', 'my-context-file.md'),
path.join(workspaceExtensionsDir, 'ext1', 'another-context.md'),
]);
});

it('should maintain backward compatibility for contextFileName as an array', () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const extDir = path.join(workspaceExtensionsDir, 'ext-compat');
fs.mkdirSync(extDir, { recursive: true });
fs.writeFileSync(
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
JSON.stringify({
name: 'ext-compat',
version: '1.0.0',
contextFileName: ['compat-a.md', 'compat-b.md'],
}),
);
fs.writeFileSync(path.join(extDir, 'compat-a.md'), 'context');
fs.writeFileSync(path.join(extDir, 'compat-b.md'), 'context');

const extensions = loadExtensions(tempWorkspaceDir);
expect(extensions).toHaveLength(1);
const extCompat = extensions.find((e) => e.config.name === 'ext-compat');
expect(extCompat?.contextFiles).toEqual([
path.join(extDir, 'compat-a.md'),
path.join(extDir, 'compat-b.md'),
]);
expect(consoleSpy).toHaveBeenCalledWith(
`[DEPRECATION] The 'contextFileName' property with an array value in extension 'ext-compat' is deprecated. Please migrate to 'contextFileNames'.`,
);
consoleSpy.mockRestore();
});

it('should filter out disabled extensions', () => {
createExtension({
extensionsDir: workspaceExtensionsDir,
Expand Down Expand Up @@ -742,13 +789,20 @@ function createExtension({
version = '1.0.0',
addContextFile = false,
contextFileName = undefined as string | undefined,
contextFileNames = undefined as string[] | undefined,
mcpServers = {} as Record<string, MCPServerConfig>,
} = {}): string {
const extDir = path.join(extensionsDir, name);
fs.mkdirSync(extDir, { recursive: true });
fs.writeFileSync(
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
JSON.stringify({ name, version, contextFileName, mcpServers }),
JSON.stringify({
name,
version,
contextFileName,
contextFileNames,
mcpServers,
}),
);

if (addContextFile) {
Expand All @@ -760,6 +814,14 @@ function createExtension({
fs.mkdirSync(path.dirname(contextPath), { recursive: true });
fs.writeFileSync(contextPath, 'context');
}

if (contextFileNames) {
for (const fileName of contextFileNames) {
const contextPath = path.join(extDir, fileName);
fs.mkdirSync(path.dirname(contextPath), { recursive: true });
fs.writeFileSync(contextPath, 'context');
}
}
return extDir;
}

Expand Down
30 changes: 25 additions & 5 deletions packages/cli/src/config/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export interface ExtensionConfig {
name: string;
version: string;
mcpServers?: Record<string, MCPServerConfig>;
contextFileName?: string | string[];
/**
* @deprecated Use `contextFileNames` instead.
*/
contextFileName?: string;
contextFileNames?: string[];
excludeTools?: string[];
}

Expand Down Expand Up @@ -244,13 +248,29 @@ function loadInstallMetadata(
}
}

// This interface is used for backward compatibility with the old ExtensionConfig.
interface OldExtensionConfig {
name: string;
contextFileName?: string | string[];
contextFileNames?: string[];
}

function getContextFileNames(config: ExtensionConfig): string[] {
if (!config.contextFileName) {
return ['GEMINI.md'];
} else if (!Array.isArray(config.contextFileName)) {
if (config.contextFileNames) {
return config.contextFileNames;
}
// For backward compatibility, handle if an old config still passes an array.
const oldConfig = config as OldExtensionConfig;
if (Array.isArray(oldConfig.contextFileName)) {
console.warn(
`[DEPRECATION] The 'contextFileName' property with an array value in extension '${config.name}' is deprecated. Please migrate to 'contextFileNames'.`,
);
return oldConfig.contextFileName;
}
if (config.contextFileName) {
return [config.contextFileName];
}
return config.contextFileName;
return ['GEMINI.md'];
}

/**
Expand Down