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:
145
bin/install.js
145
bin/install.js
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user