fix: Gemini CLI agent loading errors (#347)

* fix: convert agent frontmatter for Gemini CLI compatibility

- Add claudeToGeminiTools mapping (Read→read_file, Bash→run_shell_command, etc.)
- Add convertGeminiToolName() to map tools and filter out mcp__*/Task
- Add convertClaudeToGeminiAgent() to convert agent YAML frontmatter
- Hook conversion into agent copy logic for Gemini installs
- Fixes: tools as YAML array (not string), color field removed, mcp__* excluded

* fix: enable experimental.enableAgents for Gemini CLI installs

* fix: strip HTML <sub> tags for Gemini CLI terminal output

* fix: replace ${VAR} with $VAR in agent templates to avoid Gemini CLI template parsing
This commit is contained in:
Cristian Uibar
2026-01-30 03:43:01 +02:00
committed by GitHub
parent beca9faead
commit 5660b6fc0b
5 changed files with 172 additions and 31 deletions

View File

@@ -227,6 +227,21 @@ const claudeToOpencodeTools = {
WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
};
// Tool name mapping from Claude Code to Gemini CLI
// Gemini CLI uses snake_case built-in tool names
const claudeToGeminiTools = {
Read: 'read_file',
Write: 'write_file',
Edit: 'replace',
Bash: 'run_shell_command',
Glob: 'glob',
Grep: 'search_file_content',
WebSearch: 'google_web_search',
WebFetch: 'web_fetch',
TodoWrite: 'write_todos',
AskUserQuestion: 'ask_user',
};
/**
* Convert a Claude Code tool name to OpenCode format
* - Applies special mappings (AskUserQuestion -> question, etc.)
@@ -245,6 +260,118 @@ function convertToolName(claudeTool) {
return claudeTool.toLowerCase();
}
/**
* Convert a Claude Code tool name to Gemini CLI format
* - Applies Claude→Gemini mapping (Read→read_file, Bash→run_shell_command, etc.)
* - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Gemini
* - Filters out Task — agents are auto-registered as tools in Gemini
* @returns {string|null} Gemini tool name, or null if tool should be excluded
*/
function convertGeminiToolName(claudeTool) {
// MCP tools: exclude — auto-discovered from mcpServers config at runtime
if (claudeTool.startsWith('mcp__')) {
return null;
}
// Task: exclude — agents are auto-registered as callable tools
if (claudeTool === 'Task') {
return null;
}
// Check for explicit mapping
if (claudeToGeminiTools[claudeTool]) {
return claudeToGeminiTools[claudeTool];
}
// Default: lowercase
return claudeTool.toLowerCase();
}
/**
* Strip HTML <sub> tags for Gemini CLI output
* Terminals don't support subscript — Gemini renders these as raw HTML.
* Converts <sub>text</sub> to italic *(text)* for readable terminal output.
*/
function stripSubTags(content) {
return content.replace(/<sub>(.*?)<\/sub>/g, '*($1)*');
}
/**
* Convert Claude Code agent frontmatter to Gemini CLI format
* Gemini agents use .md files with YAML frontmatter, same as Claude,
* but with different field names and formats:
* - tools: must be a YAML array (not comma-separated string)
* - tool names: must use Gemini built-in names (read_file, not Read)
* - color: must be removed (causes validation error)
* - mcp__* tools: must be excluded (auto-discovered at runtime)
*/
function convertClaudeToGeminiAgent(content) {
if (!content.startsWith('---')) return content;
const endIndex = content.indexOf('---', 3);
if (endIndex === -1) return content;
const frontmatter = content.substring(3, endIndex).trim();
const body = content.substring(endIndex + 3);
const lines = frontmatter.split('\n');
const newLines = [];
let inAllowedTools = false;
const tools = [];
for (const line of lines) {
const trimmed = line.trim();
// Convert allowed-tools YAML array to tools list
if (trimmed.startsWith('allowed-tools:')) {
inAllowedTools = true;
continue;
}
// Handle inline tools: field (comma-separated string)
if (trimmed.startsWith('tools:')) {
const toolsValue = trimmed.substring(6).trim();
if (toolsValue) {
const parsed = toolsValue.split(',').map(t => t.trim()).filter(t => t);
for (const t of parsed) {
const mapped = convertGeminiToolName(t);
if (mapped) tools.push(mapped);
}
} else {
// tools: with no value means YAML array follows
inAllowedTools = true;
}
continue;
}
// Strip color field (not supported by Gemini CLI, causes validation error)
if (trimmed.startsWith('color:')) continue;
// Collect allowed-tools/tools array items
if (inAllowedTools) {
if (trimmed.startsWith('- ')) {
const mapped = convertGeminiToolName(trimmed.substring(2).trim());
if (mapped) tools.push(mapped);
continue;
} else if (trimmed && !trimmed.startsWith('-')) {
inAllowedTools = false;
}
}
if (!inAllowedTools) {
newLines.push(line);
}
}
// Add tools as YAML array (Gemini requires array format)
if (tools.length > 0) {
newLines.push('tools:');
for (const tool of tools) {
newLines.push(` - ${tool}`);
}
}
const newFrontmatter = newLines.join('\n').trim();
return `---\n${newFrontmatter}\n---${stripSubTags(body)}`;
}
function convertClaudeToOpencodeFrontmatter(content) {
// Replace tool name references in content (applies to all files)
let convertedContent = content;
@@ -477,7 +604,8 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
content = convertClaudeToOpencodeFrontmatter(content);
fs.writeFileSync(destPath, content);
} else if (runtime === 'gemini') {
// Convert to TOML for Gemini
// Convert to TOML for Gemini (strip <sub> tags — terminals can't render subscript)
content = stripSubTags(content);
const tomlContent = convertClaudeToGeminiToml(content);
// Replace extension with .toml
const tomlPath = destPath.replace(/\.md$/, '.toml');
@@ -953,9 +1081,11 @@ function install(isGlobal, runtime = 'claude') {
// Always replace ~/.claude/ as it is the source of truth in the repo
const dirRegex = /~\/\.claude\//g;
content = content.replace(dirRegex, pathPrefix);
// Convert frontmatter for opencode compatibility
// Convert frontmatter for runtime compatibility
if (isOpencode) {
content = convertClaudeToOpencodeFrontmatter(content);
} else if (isGemini) {
content = convertClaudeToGeminiAgent(content);
}
fs.writeFileSync(path.join(agentsDest, entry.name), content);
}
@@ -1024,6 +1154,17 @@ function install(isGlobal, runtime = 'claude') {
? buildHookCommand(targetDir, 'gsd-check-update.js')
: 'node ' + dirName + '/hooks/gsd-check-update.js';
// Enable experimental agents for Gemini CLI (required for custom sub-agents)
if (isGemini) {
if (!settings.experimental) {
settings.experimental = {};
}
if (!settings.experimental.enableAgents) {
settings.experimental.enableAgents = true;
console.log(` ${green}✓${reset} Enabled experimental agents`);
}
}
// Configure SessionStart hook for update checking (skip for opencode)
if (!isOpencode) {
if (!settings.hooks) {