feat: add gemini agent adapter

This commit is contained in:
Affaan Mustafa
2026-04-08 15:38:49 -07:00
parent e363c54057
commit e226772a72
3 changed files with 326 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const TOOL_NAME_MAP = new Map([
['Read', 'read_file'],
['Write', 'write_file'],
['Edit', 'replace'],
['Bash', 'run_shell_command'],
['Grep', 'grep_search'],
['Glob', 'glob'],
['WebSearch', 'google_web_search'],
['WebFetch', 'web_fetch'],
]);
function usage() {
return [
'Adapt ECC agent frontmatter for Gemini CLI.',
'',
'Usage:',
' node scripts/gemini-adapt-agents.js [agents-dir]',
'',
'Defaults to .gemini/agents under the current working directory.',
'Rewrites tools: to Gemini-compatible tool names and removes unsupported color: metadata.'
].join('\n');
}
function parseArgs(argv) {
if (argv.includes('--help') || argv.includes('-h')) {
return { help: true };
}
const positional = argv.filter(arg => !arg.startsWith('-'));
if (positional.length > 1) {
throw new Error('Expected at most one agents directory argument');
}
return {
help: false,
agentsDir: path.resolve(positional[0] || path.join(process.cwd(), '.gemini', 'agents')),
};
}
function ensureDirectory(dirPath) {
if (!fs.existsSync(dirPath)) {
throw new Error(`Agents directory not found: ${dirPath}`);
}
if (!fs.statSync(dirPath).isDirectory()) {
throw new Error(`Expected a directory: ${dirPath}`);
}
}
function stripQuotes(value) {
return value.trim().replace(/^['"]|['"]$/g, '');
}
function parseToolList(line) {
const match = line.match(/^(\s*tools\s*:\s*)\[(.*)\]\s*$/);
if (!match) {
return null;
}
const rawItems = match[2].trim();
if (!rawItems) {
return [];
}
return rawItems
.split(',')
.map(part => stripQuotes(part))
.filter(Boolean);
}
function adaptToolName(toolName) {
const mapped = TOOL_NAME_MAP.get(toolName);
if (mapped) {
return mapped;
}
if (toolName.startsWith('mcp__')) {
return toolName
.replace(/^mcp__/, 'mcp_')
.replace(/__/g, '_')
.replace(/[^A-Za-z0-9_]/g, '_')
.toLowerCase();
}
return toolName;
}
function formatToolLine(tools) {
return `tools: [${tools.map(tool => JSON.stringify(tool)).join(', ')}]`;
}
function adaptFrontmatter(text) {
const match = text.match(/^---\n([\s\S]*?)\n---(\n|$)/);
if (!match) {
return { text, changed: false };
}
let changed = false;
const updatedLines = [];
for (const line of match[1].split('\n')) {
if (/^\s*color\s*:/.test(line)) {
changed = true;
continue;
}
const tools = parseToolList(line);
if (tools) {
const adaptedTools = [];
const seen = new Set();
for (const tool of tools.map(adaptToolName)) {
if (seen.has(tool)) {
continue;
}
seen.add(tool);
adaptedTools.push(tool);
}
const updatedLine = formatToolLine(adaptedTools);
if (updatedLine !== line) {
changed = true;
}
updatedLines.push(updatedLine);
continue;
}
updatedLines.push(line);
}
if (!changed) {
return { text, changed: false };
}
return {
text: `---\n${updatedLines.join('\n')}\n---${match[2]}${text.slice(match[0].length)}`,
changed: true,
};
}
function adaptAgents(dirPath) {
ensureDirectory(dirPath);
let updated = 0;
let unchanged = 0;
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith('.md')) {
continue;
}
const filePath = path.join(dirPath, entry.name);
const original = fs.readFileSync(filePath, 'utf8');
const adapted = adaptFrontmatter(original);
if (adapted.changed) {
fs.writeFileSync(filePath, adapted.text);
updated += 1;
} else {
unchanged += 1;
}
}
return { updated, unchanged };
}
function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
console.log(usage());
return;
}
const result = adaptAgents(options.agentsDir);
console.log(`Updated ${result.updated} agent file(s); ${result.unchanged} already compatible`);
}
try {
main();
} catch (error) {
console.error(error.message);
process.exit(1);
}