From 257e59d2eadd4c7b717a97fd4410d92bc7385c4d Mon Sep 17 00:00:00 2001 From: Amit Acharya <100467234+AJAmit17@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:44:57 +0530 Subject: [PATCH 1/5] feat: add Google Gemini CLI adapter for export, import, and run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `gemini` format that generates GEMINI.md and .gemini/settings.json compatible with Google Gemini CLI. The adapter maps gitagent's SOUL.md, RULES.md, skills, knowledge, and compliance constraints into Gemini's instruction format. Import reads GEMINI.md and settings.json to reconstruct agent.yaml with model preferences and approval mode mappings (plan→always, default→conditional, yolo→none, auto_edit→advisory). --- README.md | 1 + docs/adapters/gemini.md | 395 ++++++++++++++++++++++++++++++++++++++++ src/adapters/gemini.ts | 374 +++++++++++++++++++++++++++++++++++++ src/adapters/index.ts | 1 + src/commands/export.ts | 8 +- src/commands/import.ts | 95 +++++++++- src/commands/run.ts | 8 +- 7 files changed, 876 insertions(+), 6 deletions(-) create mode 100644 docs/adapters/gemini.md create mode 100644 src/adapters/gemini.ts diff --git a/README.md b/README.md index ba83a22..3082553 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,7 @@ Adapters are used by both `export` and `run`. Available adapters: | `github` | GitHub Actions agent | | `git` | Git-native execution (run only) | | `opencode` | OpenCode instructions + config | +| `gemini` | Google Gemini CLI (GEMINI.md + settings.json) | | `openclaw` | OpenClaw format | | `nanobot` | Nanobot format | diff --git a/docs/adapters/gemini.md b/docs/adapters/gemini.md new file mode 100644 index 0000000..a839753 --- /dev/null +++ b/docs/adapters/gemini.md @@ -0,0 +1,395 @@ +# Google Gemini CLI Adapter + +Complete mapping guide for converting between gitagent and Google Gemini CLI formats. + +## Overview + +Google Gemini CLI is Google's open-source AI agent for the terminal. It uses: + +- **GEMINI.md** at the project root (or `~/.gemini/GEMINI.md` globally) for custom instructions +- **.gemini/settings.json** for model configuration, tool permissions, and approval modes +- Supports Gemini models via Google AI Studio or Vertex AI +- Has approval modes, policy engine, MCP servers, skills, hooks, and extensions + +The gitagent Gemini adapter enables: +1. **Export**: Convert gitagent → Gemini CLI format +2. **Run**: Execute gitagent agents using `gemini` CLI +3. **Import**: Convert Gemini CLI projects → gitagent format + +## Installation + +```bash +# Install Gemini CLI +npm install -g @google/generative-ai-cli + +# Or via Homebrew (macOS) +brew install google/tap/gemini + +# Verify installation +gemini --version +``` + +## Field Mapping + +### Export: gitagent → Gemini CLI + +| gitagent | Gemini CLI | Notes | +|----------|-----------|-------| +| `SOUL.md` | `GEMINI.md` (identity section) | Core personality and communication style | +| `RULES.md` | `GEMINI.md` (constraints section) | Hard constraints and safety boundaries | +| `DUTIES.md` | `GEMINI.md` (SOD section) | Segregation of duties policy | +| `skills/*/SKILL.md` | `GEMINI.md` (skills section) | Progressive disclosure with full instructions | +| `tools/*.yaml` | `.gemini/settings.json` → `allowedTools` | Tool names extracted from YAML | +| `knowledge/` (always_load) | `GEMINI.md` (knowledge section) | Reference documents embedded | +| `manifest.model.preferred` | `.gemini/settings.json` → `model` | Model selection (e.g., `gemini-2.0-flash-exp`) | +| `manifest.compliance.supervision.human_in_the_loop` | CLI flag `--approval-mode` | Approval mode mapping (see below) | +| `hooks/hooks.yaml` | `.gemini/settings.json` → `hooks` | Lifecycle event handlers | +| `agents/` (sub-agents) | `GEMINI.md` (delegation section) | Documented as pattern (no native support) | +| `compliance/` (policy files) | `.gemini/settings.json` → `policy` | Policy file paths | + +### Import: Gemini CLI → gitagent + +| Gemini CLI | gitagent | Notes | +|-----------|----------|-------| +| `GEMINI.md` | `SOUL.md` + `RULES.md` + `DUTIES.md` | Split by section keywords | +| `.gemini/settings.json` → `model` | `agent.yaml` → `model.preferred` | Direct mapping | +| `.gemini/settings.json` → `approvalMode` | `compliance.supervision.human_in_the_loop` | Reverse approval mode mapping | +| `.gemini/settings.json` → `allowedTools` | `tools/*.yaml` | Creates tool YAML files | +| `.gemini/settings.json` → `hooks` | `hooks/hooks.yaml` | Event mapping | + +## Approval Mode Mapping + +### Export (gitagent → Gemini CLI) + +| gitagent `human_in_the_loop` | Gemini CLI `approvalMode` | Behavior | +|------------------------------|---------------------------|----------| +| `always` | `plan` | Read-only mode, no actions executed | +| `conditional` | `default` | Prompt for approval on tool use | +| `none` | `yolo` | Auto-approve all actions | +| `advisory` | `auto_edit` | Auto-approve edit tools only | + +### Import (Gemini CLI → gitagent) + +| Gemini CLI `approvalMode` | gitagent `human_in_the_loop` | +|---------------------------|------------------------------| +| `plan` | `always` | +| `default` | `conditional` | +| `yolo` | `none` | +| `auto_edit` | `advisory` | + +## Usage Examples + +### Export to Gemini CLI + +```bash +# Export to stdout +gitagent export --format gemini -d ./my-agent + +# Save to file +gitagent export --format gemini -d ./my-agent -o gemini-export.txt + +# The export includes both GEMINI.md and .gemini/settings.json content +``` + +**Output Structure:** +``` +# === GEMINI.md === +# agent-name +Agent description + +## Soul +[SOUL.md content] + +## Rules +[RULES.md content] + +## Skills +[Skills with progressive disclosure] + +## Tools +[Tool schemas] + +# === .gemini/settings.json === +{ + "model": "gemini-2.0-flash-exp", + "allowedTools": ["bash", "edit", "read"], + "approvalMode": "default", + "hooks": {...} +} +``` + +### Run with Gemini CLI + +```bash +# Interactive mode +gitagent run ./my-agent --adapter gemini + +# Single-shot mode with prompt +gitagent run ./my-agent --adapter gemini -p "Explain quantum computing" + +# From git repository +gitagent run --repo https://github.com/user/agent.git --adapter gemini +``` + +**What Happens:** +1. Creates temporary workspace +2. Writes `GEMINI.md` at project root +3. Creates `.gemini/settings.json` with config +4. Launches `gemini` CLI in that workspace +5. Cleans up temporary files on exit + +### Import from Gemini CLI + +```bash +# Import from existing Gemini CLI project +gitagent import --from gemini /path/to/gemini-project -d ./imported-agent + +# Verify the imported agent +cd ./imported-agent +gitagent validate +``` + +**What Gets Created:** +- `agent.yaml` - Manifest with model from settings.json +- `SOUL.md` - Identity sections from GEMINI.md +- `RULES.md` - Constraint sections from GEMINI.md +- `DUTIES.md` - SOD/delegation sections (if present) +- `tools/*.yaml` - Tool definitions from allowedTools +- `hooks/hooks.yaml` - Hooks from settings.json + +## Section Detection (Import) + +When importing `GEMINI.md`, sections are split based on keywords: + +**→ SOUL.md:** +- Sections with: identity, personality, style, about, soul +- Default destination for unmatched sections + +**→ RULES.md:** +- Sections with: rule, constraint, never, always, must, compliance + +**→ DUTIES.md:** +- Sections with: duties, segregation, delegation + +## What Maps Cleanly + +✅ **Fully Supported:** +- Agent identity and personality (SOUL.md ↔ GEMINI.md) +- Rules and constraints (RULES.md ↔ GEMINI.md) +- Model preferences +- Tool permissions +- Approval modes +- Basic hooks +- Knowledge documents + +## What Requires Manual Setup + +⚠️ **Not Automatically Mapped:** + +### 1. MCP Servers +**Issue:** Gemini CLI's MCP server config doesn't have a direct gitagent equivalent. + +**Workaround:** +- Document MCP servers in GEMINI.md during export +- Manually configure `.gemini/settings.json` → `mcpServers` after export +- On import, MCP config is ignored (not portable) + +**Example Manual Setup:** +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed"] + } + } +} +``` + +### 2. Extensions +**Issue:** Gemini CLI extensions are runtime-specific and not portable. + +**Workaround:** +- Extensions are not exported or imported +- Document extension requirements in README +- Users must install extensions separately + +### 3. Policy Files +**Issue:** Gemini's policy engine uses separate policy files that need manual creation. + +**Workaround:** +- Export references policy files in settings.json if they exist in `compliance/` +- Import does not create policy files (only references them) +- Users must manually create policy files based on RULES.md + +### 4. Sub-agents +**Issue:** Gemini CLI doesn't have native sub-agent support like gitagent. + +**Workaround:** +- Export documents sub-agents as a "Delegation Pattern" section in GEMINI.md +- Import does not create sub-agent directories +- Users must manually implement delegation logic + +### 5. Workflows +**Issue:** gitagent's SkillsFlow YAML doesn't map to Gemini CLI. + +**Workaround:** +- Convert workflows to skills or document in instructions +- Not preserved during import/export cycle + +### 6. API Keys +**Issue:** Gemini CLI requires Google AI Studio or Vertex AI credentials. + +**Workaround:** +- Set `GOOGLE_API_KEY` environment variable +- Or configure Vertex AI credentials +- Document in agent README + +## Hooks Mapping + +### Event Name Mapping + +| gitagent Event | Gemini CLI Event | +|---------------|------------------| +| `on_session_start` | `session_start` | +| `pre_tool_use` | `pre_tool_use` | +| `post_tool_use` | `post_tool_use` | +| `pre_response` | `pre_response` | +| `post_response` | `post_response` | +| `on_error` | `on_error` | +| `on_session_end` | `session_end` | + +### Hook Format + +**gitagent (hooks/hooks.yaml):** +```yaml +hooks: + on_session_start: + - script: scripts/init.sh + description: Initialize session +``` + +**Gemini CLI (.gemini/settings.json):** +```json +{ + "hooks": { + "session_start": [ + { + "script": "scripts/init.sh", + "description": "Initialize session" + } + ] + } +} +``` + +## Best Practices + +### When Exporting + +1. **Use Gemini-compatible models** in `agent.yaml`: + - `gemini-2.0-flash-exp` + - `gemini-1.5-pro` + - `gemini-1.5-flash` + +2. **Set appropriate approval mode** via compliance config: + ```yaml + compliance: + supervision: + human_in_the_loop: critical # → approvalMode: default + ``` + +3. **Document MCP requirements** in README if your agent needs external tools + +4. **Keep skills self-contained** - full instructions in SKILL.md + +### When Importing + +1. **Review split sections** - verify SOUL.md/RULES.md split is correct + +2. **Add missing metadata** to agent.yaml: + - Author, license, tags + - Compliance frameworks + - Dependencies + +3. **Create proper tool schemas** - imported tools have minimal schemas + +4. **Test the agent** with `gitagent validate` + +### When Running + +1. **Set API key** before running: + ```bash + export GOOGLE_API_KEY=your-api-key + ``` + +2. **Use appropriate approval mode** for your use case: + - Development: `--approval-mode default` + - Production: `--approval-mode plan` + - Testing: `--approval-mode yolo` (use with caution) + +3. **Monitor temporary workspace** - cleaned up automatically on exit + +## Troubleshooting + +### "gemini: command not found" + +**Solution:** +```bash +npm install -g @google/generative-ai-cli +# Or +brew install google/tap/gemini +``` + +### "API key not configured" + +**Solution:** +```bash +export GOOGLE_API_KEY=your-api-key-here +# Or configure Vertex AI credentials +``` + +### "GEMINI.md not found" (import) + +**Solution:** +- Ensure you're pointing to the project root directory +- Gemini CLI projects must have GEMINI.md at the root + +### Tools not working after import + +**Solution:** +- Imported tool schemas are minimal placeholders +- Manually update `tools/*.yaml` with proper input schemas +- Or use Gemini CLI's native tool configuration + +## Resources + +- [Gemini CLI GitHub](https://github.com/google/generative-ai-cli) +- [Gemini CLI Documentation](https://geminicli.com/docs) +- [Google AI Studio](https://aistudio.google.com/) +- [gitagent Specification](../../spec/SPECIFICATION.md) +- [Example Gemini Agent](../../examples/gemini-example/) + +## Limitations Summary + +| Feature | Export | Import | Run | Notes | +|---------|--------|--------|-----|-------| +| Identity (SOUL.md) | ✅ | ✅ | ✅ | Full support | +| Rules (RULES.md) | ✅ | ✅ | ✅ | Full support | +| Duties (DUTIES.md) | ✅ | ✅ | ✅ | Full support | +| Skills | ✅ | ⚠️ | ✅ | Import creates basic structure | +| Tools | ✅ | ⚠️ | ✅ | Import creates minimal schemas | +| Model preference | ✅ | ✅ | ✅ | Full support | +| Approval modes | ✅ | ✅ | ✅ | Full support | +| Hooks | ✅ | ✅ | ✅ | Full support | +| Knowledge | ✅ | ❌ | ✅ | Not preserved on import | +| Sub-agents | ⚠️ | ❌ | ⚠️ | Documented only, not executable | +| Workflows | ❌ | ❌ | ❌ | Not supported | +| MCP servers | ⚠️ | ❌ | ⚠️ | Manual setup required | +| Extensions | ❌ | ❌ | ❌ | Not portable | +| Policy files | ⚠️ | ⚠️ | ⚠️ | References only | + +**Legend:** +- ✅ Fully supported +- ⚠️ Partial support or manual setup required +- ❌ Not supported diff --git a/src/adapters/gemini.ts b/src/adapters/gemini.ts new file mode 100644 index 0000000..02a40bd --- /dev/null +++ b/src/adapters/gemini.ts @@ -0,0 +1,374 @@ +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import yaml from 'js-yaml'; +import { loadAgentManifest, loadFileIfExists } from '../utils/loader.js'; +import { loadAllSkills, getAllowedTools } from '../utils/skill-loader.js'; +import { buildComplianceSection } from './shared.js'; + +/** + * Export a gitagent to Google Gemini CLI format. + * + * Gemini CLI uses: + * - GEMINI.md (custom agent instructions, project root or ~/.gemini/GEMINI.md) + * - .gemini/settings.json (model configuration, tool permissions, approval mode) + * + * Returns structured output with all files that should be written. + */ +export interface GeminiExport { + instructions: string; + settings: Record; +} + +export function exportToGemini(dir: string): GeminiExport { + const agentDir = resolve(dir); + const manifest = loadAgentManifest(agentDir); + + const instructions = buildInstructions(agentDir, manifest); + const settings = buildSettings(agentDir, manifest); + + return { instructions, settings }; +} + +/** + * Export as a single string (for `gitagent export -f gemini`). + */ +export function exportToGeminiString(dir: string): string { + const exp = exportToGemini(dir); + const parts: string[] = []; + + parts.push('# === GEMINI.md ==='); + parts.push(exp.instructions); + parts.push('\n# === .gemini/settings.json ==='); + parts.push(JSON.stringify(exp.settings, null, 2)); + + return parts.join('\n'); +} + +function buildInstructions( + agentDir: string, + manifest: ReturnType, +): string { + const parts: string[] = []; + + // Agent identity + parts.push(`# ${manifest.name}`); + parts.push(`${manifest.description}`); + parts.push(''); + + // SOUL.md → identity section + const soul = loadFileIfExists(join(agentDir, 'SOUL.md')); + if (soul) { + parts.push(soul); + parts.push(''); + } + + // RULES.md → constraints section + const rules = loadFileIfExists(join(agentDir, 'RULES.md')); + if (rules) { + parts.push(rules); + parts.push(''); + } + + // DUTIES.md → segregation of duties policy + const duty = loadFileIfExists(join(agentDir, 'DUTIES.md')); + if (duty) { + parts.push(duty); + parts.push(''); + } + + // Skills — loaded via skill-loader (progressive disclosure) + const skillsDir = join(agentDir, 'skills'); + const skills = loadAllSkills(skillsDir); + if (skills.length > 0) { + parts.push('## Skills'); + parts.push(''); + for (const skill of skills) { + const skillDirName = skill.directory.split(/[/\\]/).pop()!; + const toolsList = getAllowedTools(skill.frontmatter); + const toolsNote = toolsList.length > 0 ? `\nAllowed tools: ${toolsList.join(', ')}` : ''; + parts.push(`### ${skill.frontmatter.name}`); + parts.push(`${skill.frontmatter.description}${toolsNote}`); + parts.push(''); + parts.push(skill.instructions); + parts.push(''); + } + } + + // Tools + const toolsDir = join(agentDir, 'tools'); + if (existsSync(toolsDir)) { + const toolFiles = readdirSync(toolsDir).filter(f => f.endsWith('.yaml')); + if (toolFiles.length > 0) { + parts.push('## Tools'); + parts.push(''); + for (const file of toolFiles) { + try { + const content = readFileSync(join(toolsDir, file), 'utf-8'); + const toolConfig = yaml.load(content) as { + name?: string; + description?: string; + input_schema?: Record; + }; + if (toolConfig?.name) { + parts.push(`### ${toolConfig.name}`); + if (toolConfig.description) { + parts.push(toolConfig.description); + } + if (toolConfig.input_schema) { + parts.push(''); + parts.push('```yaml'); + parts.push(yaml.dump(toolConfig.input_schema).trimEnd()); + parts.push('```'); + } + parts.push(''); + } + } catch { /* skip malformed tools */ } + } + } + } + + // Knowledge (always_load documents) + const knowledgeDir = join(agentDir, 'knowledge'); + const indexPath = join(knowledgeDir, 'index.yaml'); + if (existsSync(indexPath)) { + const index = yaml.load(readFileSync(indexPath, 'utf-8')) as { + documents?: Array<{ path: string; always_load?: boolean }>; + }; + + if (index.documents) { + const alwaysLoad = index.documents.filter(d => d.always_load); + if (alwaysLoad.length > 0) { + parts.push('## Knowledge'); + parts.push(''); + for (const doc of alwaysLoad) { + const content = loadFileIfExists(join(knowledgeDir, doc.path)); + if (content) { + parts.push(`### ${doc.path}`); + parts.push(content); + parts.push(''); + } + } + } + } + } + + // Compliance constraints + if (manifest.compliance) { + const constraints = buildComplianceSection(manifest.compliance); + if (constraints) { + parts.push(constraints); + parts.push(''); + } + } + + // Sub-agents (document as delegation pattern since Gemini CLI doesn't have native support) + if (manifest.agents && Object.keys(manifest.agents).length > 0) { + parts.push('## Delegation Pattern'); + parts.push(''); + parts.push('This agent uses sub-agents for specialized tasks:'); + parts.push(''); + for (const [name, config] of Object.entries(manifest.agents)) { + parts.push(`### ${name}`); + if (config.description) { + parts.push(config.description); + } + if (config.delegation?.triggers) { + parts.push(`Triggers: ${config.delegation.triggers.join(', ')}`); + } + parts.push(''); + } + } + + // Memory + const memory = loadFileIfExists(join(agentDir, 'memory', 'MEMORY.md')); + if (memory && memory.trim().split('\n').length > 2) { + parts.push('## Memory'); + parts.push(memory); + parts.push(''); + } + + return parts.join('\n').trimEnd() + '\n'; +} + +function buildSettings( + agentDir: string, + manifest: ReturnType, +): Record { + const settings: Record = {}; + + // Model preference - Gemini CLI expects object format + if (manifest.model?.preferred) { + // Extract provider from model name or default to google + const modelName = manifest.model.preferred; + const provider = modelName.includes('claude') ? 'anthropic' : + modelName.includes('gpt') ? 'openai' : 'google'; + + settings.model = { + id: modelName, + provider: provider + }; + } + + // Collect allowed tools from skills and tool definitions + const allowedTools = collectAllowedTools(agentDir); + if (allowedTools.length > 0) { + settings.allowedTools = allowedTools; + } + + // Approval mode from compliance supervision + if (manifest.compliance?.supervision?.human_in_the_loop) { + const hitl = manifest.compliance.supervision.human_in_the_loop; + if (hitl === 'always') { + settings.approvalMode = 'plan'; // read-only mode + } else if (hitl === 'conditional') { + settings.approvalMode = 'default'; // prompt for approval + } else if (hitl === 'none') { + settings.approvalMode = 'yolo'; // auto-approve all + } else if (hitl === 'advisory') { + settings.approvalMode = 'auto_edit'; // auto-approve edits only + } + } + + // Policy files (if they exist in compliance/) + const policyDir = join(agentDir, 'compliance'); + if (existsSync(policyDir)) { + const policyFiles = readdirSync(policyDir).filter(f => f.endsWith('.md') || f.endsWith('.txt')); + if (policyFiles.length > 0) { + settings.policy = policyFiles.map(f => `compliance/${f}`); + } + } + + // Hooks mapping + const hooksConfig = buildHooksConfig(agentDir); + if (hooksConfig && Object.keys(hooksConfig).length > 0) { + settings.hooks = hooksConfig; + } + + // MCP servers (placeholder - requires manual configuration) + settings.mcpServers = {}; + + return settings; +} + +/** + * Collect allowed tools from skills (allowed-tools frontmatter) + * and tool definitions (tools/*.yaml names). + */ +function collectAllowedTools(agentDir: string): string[] { + const tools: Set = new Set(); + + // From skills' allowed-tools + const skillsDir = join(agentDir, 'skills'); + const skills = loadAllSkills(skillsDir); + for (const skill of skills) { + for (const tool of getAllowedTools(skill.frontmatter)) { + tools.add(tool); + } + } + + // From tools/*.yaml definitions + const toolsDir = join(agentDir, 'tools'); + if (existsSync(toolsDir)) { + const files = readdirSync(toolsDir).filter(f => f.endsWith('.yaml')); + for (const file of files) { + try { + const content = readFileSync(join(toolsDir, file), 'utf-8'); + const toolConfig = yaml.load(content) as { name?: string }; + if (toolConfig?.name) { + tools.add(toolConfig.name); + } + } catch { /* skip malformed tools */ } + } + } + + return Array.from(tools); +} + +/** + * Map hooks/hooks.yaml to Gemini CLI hooks format. + * + * Gemini CLI expects hooks in settings.json. + */ +function buildHooksConfig(agentDir: string): Record | null { + const hooksPath = join(agentDir, 'hooks', 'hooks.yaml'); + if (!existsSync(hooksPath)) return null; + + try { + const content = readFileSync(hooksPath, 'utf-8'); + const hooksConfig = yaml.load(content) as { + hooks?: Record>; + }; + + if (!hooksConfig?.hooks) return null; + + // Map gitagent hook events to Gemini CLI hook events + // Gemini CLI uses PascalCase event names + const eventMap: Record = { + 'on_session_start': 'SessionStart', + 'on_session_end': 'SessionEnd', + 'pre_tool_use': 'BeforeTool', + 'post_tool_use': 'AfterTool', + 'pre_agent': 'BeforeAgent', + 'post_agent': 'AfterAgent', + 'pre_model': 'BeforeModel', + 'post_model': 'AfterModel', + 'pre_response': 'AfterModel', // Runs after model generates response + 'post_response': 'AfterAgent', // Runs after agent loop completes + 'on_error': 'Notification', // Map errors to notification system + }; + + // Gemini CLI uses a matcher-based structure for hooks + const geminiHooks: Record; + }>> = {}; + + for (const [event, hooks] of Object.entries(hooksConfig.hooks)) { + const geminiEvent = eventMap[event] || event; + + // Filter out hooks whose script files don't exist + // Scripts are relative to the hooks directory + const validHooks = hooks.filter(hook => { + const scriptPath = join(agentDir, 'hooks', hook.script); + return existsSync(scriptPath); + }); + + // Skip this event if no valid hooks remain + if (validHooks.length === 0) continue; + + if (!geminiHooks[geminiEvent]) { + geminiHooks[geminiEvent] = []; + } + + // Convert each hook to Gemini CLI format with matcher + const geminiHookDefs = validHooks.map((hook, index) => { + let command = hook.script; + return { + name: `hook-${index}`, + type: 'command', + command: command, + description: hook.description, + }; + }); + + // Wrap hooks in a matcher object (use '*' to match all) + geminiHooks[geminiEvent].push({ + matcher: '*', + hooks: geminiHookDefs, + }); + } + + return Object.keys(geminiHooks).length > 0 ? geminiHooks : null; + } catch { + return null; + } +} diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 62bc483..3a9a6f0 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -6,3 +6,4 @@ export { exportToOpenClawString, exportToOpenClaw } from './openclaw.js'; export { exportToNanobotString, exportToNanobot } from './nanobot.js'; export { exportToCopilotString, exportToCopilot } from './copilot.js'; export { exportToOpenCodeString, exportToOpenCode } from './opencode.js'; +export { exportToGeminiString, exportToGemini } from './gemini.js'; diff --git a/src/commands/export.ts b/src/commands/export.ts index 4025840..2b81a9b 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -10,6 +10,7 @@ import { exportToNanobotString, exportToCopilotString, exportToOpenCodeString, + exportToGeminiString, } from '../adapters/index.js'; import { exportToLyzrString } from '../adapters/lyzr.js'; import { exportToGitHubString } from '../adapters/github.js'; @@ -22,7 +23,7 @@ interface ExportOptions { export const exportCommand = new Command('export') .description('Export agent to other formats') - .requiredOption('-f, --format ', 'Export format (system-prompt, claude-code, openai, crewai, openclaw, nanobot, lyzr, github, copilot, opencode)') + .requiredOption('-f, --format ', 'Export format (system-prompt, claude-code, openai, crewai, openclaw, nanobot, lyzr, github, copilot, opencode, gemini)') .option('-d, --dir ', 'Agent directory', '.') .option('-o, --output ', 'Output file path') .action(async (options: ExportOptions) => { @@ -65,9 +66,12 @@ export const exportCommand = new Command('export') case 'opencode': result = exportToOpenCodeString(dir); break; + case 'gemini': + result = exportToGeminiString(dir); + break; default: error(`Unknown format: ${options.format}`); - info('Supported formats: system-prompt, claude-code, openai, crewai, openclaw, nanobot, lyzr, github, copilot, opencode'); + info('Supported formats: system-prompt, claude-code, openai, crewai, openclaw, nanobot, lyzr, github, copilot, opencode, gemini'); process.exit(1); } diff --git a/src/commands/import.ts b/src/commands/import.ts index 07ae518..bd807c7 100644 --- a/src/commands/import.ts +++ b/src/commands/import.ts @@ -258,6 +258,94 @@ function importFromOpenCode(sourcePath: string, targetDir: string): void { } } +function importFromGemini(sourcePath: string, targetDir: string): void { + const sourceDir = resolve(sourcePath); + + // Look for GEMINI.md + const geminiMdPath = join(sourceDir, 'GEMINI.md'); + if (!existsSync(geminiMdPath)) { + throw new Error('GEMINI.md not found in source directory'); + } + + const geminiMd = readFileSync(geminiMdPath, 'utf-8'); + + // Look for .gemini/settings.json (optional) + let settings: Record = {}; + const settingsPath = join(sourceDir, '.gemini', 'settings.json'); + if (existsSync(settingsPath)) { + try { + settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); + info('Found .gemini/settings.json'); + } catch { /* ignore malformed config */ } + } + + const dirName = basename(sourceDir); + + // Determine model from settings.json + const model = settings.model as string | undefined; + const agentYaml: Record = { + spec_version: '0.1.0', + name: dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-'), + version: '0.1.0', + description: `Imported from Gemini CLI project: ${dirName}`, + }; + if (model) { + agentYaml.model = { preferred: model }; + } + + // Ensure target directory exists + mkdirSync(targetDir, { recursive: true }); + + // Map approval mode to compliance + if (settings.approvalMode) { + const approvalMode = settings.approvalMode as string; + let hitl: string | undefined; + if (approvalMode === 'plan') hitl = 'always'; + else if (approvalMode === 'default') hitl = 'conditional'; + else if (approvalMode === 'yolo') hitl = 'none'; + else if (approvalMode === 'auto_edit') hitl = 'advisory'; + + if (hitl) { + agentYaml.compliance = { + supervision: { + human_in_the_loop: hitl, + }, + }; + } + } + + writeFileSync(join(targetDir, 'agent.yaml'), yaml.dump(agentYaml), 'utf-8'); + success('Created agent.yaml'); + + // Convert GEMINI.md to SOUL.md + RULES.md + const sections = parseSections(geminiMd); + let soulContent = '# Soul\n\n'; + let rulesContent = '# Rules\n\n'; + let hasRules = false; + + for (const [title, content] of sections) { + const lower = title.toLowerCase(); + if (lower.includes('rule') || lower.includes('constraint') || lower.includes('never') || lower.includes('always') || lower.includes('must') || lower.includes('compliance')) { + rulesContent += `## ${title}\n${content}\n\n`; + hasRules = true; + } else { + soulContent += `## ${title}\n${content}\n\n`; + } + } + + if (sections.length === 0) { + soulContent += geminiMd; + } + + writeFileSync(join(targetDir, 'SOUL.md'), soulContent, 'utf-8'); + success('Created SOUL.md'); + + if (hasRules) { + writeFileSync(join(targetDir, 'RULES.md'), rulesContent, 'utf-8'); + success('Created RULES.md'); + } +} + function parseSections(markdown: string): [string, string][] { const sections: [string, string][] = []; const lines = markdown.split('\n'); @@ -286,7 +374,7 @@ function parseSections(markdown: string): [string, string][] { export const importCommand = new Command('import') .description('Import from other agent formats') - .requiredOption('--from ', 'Source format (claude, cursor, crewai, opencode)') + .requiredOption('--from ', 'Source format (claude, cursor, crewai, opencode, gemini)') .argument('', 'Source file or directory path') .option('-d, --dir ', 'Target directory', '.') .action((sourcePath: string, options: ImportOptions) => { @@ -310,9 +398,12 @@ export const importCommand = new Command('import') case 'opencode': importFromOpenCode(sourcePath, targetDir); break; + case 'gemini': + importFromGemini(sourcePath, targetDir); + break; default: error(`Unknown format: ${options.from}`); - info('Supported formats: claude, cursor, crewai, opencode'); + info('Supported formats: claude, cursor, crewai, opencode, gemini'); process.exit(1); } diff --git a/src/commands/run.ts b/src/commands/run.ts index cc8afb9..657e0e4 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -13,6 +13,7 @@ import { runWithLyzr } from '../runners/lyzr.js'; import { runWithGitHub } from '../runners/github.js'; import { runWithGit } from '../runners/git.js'; import { runWithOpenCode } from '../runners/opencode.js'; +import { runWithGemini } from '../runners/gemini.js'; interface RunOptions { repo?: string; @@ -27,7 +28,7 @@ interface RunOptions { export const runCommand = new Command('run') .description('Run an agent from a git repository or local directory') .option('-r, --repo ', 'Git repository URL') - .option('-a, --adapter ', 'Adapter: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, git, prompt', 'claude') + .option('-a, --adapter ', 'Adapter: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, git, prompt', 'claude') .option('-b, --branch ', 'Git branch/tag to clone', 'main') .option('--refresh', 'Force re-clone (pull latest)', false) .option('--no-cache', 'Clone to temp dir, delete on exit') @@ -116,6 +117,9 @@ export const runCommand = new Command('run') case 'opencode': runWithOpenCode(agentDir, manifest, { prompt: options.prompt }); break; + case 'gemini': + runWithGemini(agentDir, manifest, { prompt: options.prompt }); + break; case 'git': if (!options.repo) { error('The git adapter requires --repo (-r)'); @@ -134,7 +138,7 @@ export const runCommand = new Command('run') break; default: error(`Unknown adapter: ${options.adapter}`); - info('Supported adapters: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, git, prompt'); + info('Supported adapters: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, git, prompt'); process.exit(1); } } catch (e) { From 01894e42ec6ddc6e57b1e9e6ac5d9a68da061493 Mon Sep 17 00:00:00 2001 From: Amit Acharya <100467234+AJAmit17@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:57:31 +0530 Subject: [PATCH 2/5] feat: add Google Gemini CLI runner for interactive and single-shot mode Add `gemini` runner that creates temp workspace with GEMINI.md and .gemini/settings.json, then launches `gemini` CLI in interactive or single-shot mode (`-p` flag). The runner copies hooks directory if present, maps approval modes to CLI flags (plan/default/yolo/auto_edit), and handles Windows shell quoting for args with spaces. Also fixes buildHooksConfig to check hooks.yaml existence inside try block and use consistent null --- src/adapters/gemini.ts | 24 ++++---- src/runners/gemini.ts | 125 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 src/runners/gemini.ts diff --git a/src/adapters/gemini.ts b/src/adapters/gemini.ts index 02a40bd..f0c6d27 100644 --- a/src/adapters/gemini.ts +++ b/src/adapters/gemini.ts @@ -289,22 +289,20 @@ function collectAllowedTools(agentDir: string): string[] { * * Gemini CLI expects hooks in settings.json. */ -function buildHooksConfig(agentDir: string): Record | null { - const hooksPath = join(agentDir, 'hooks', 'hooks.yaml'); - if (!existsSync(hooksPath)) return null; - +function buildHooksConfig(agentDir: string): Record | null { try { - const content = readFileSync(hooksPath, 'utf-8'); - const hooksConfig = yaml.load(content) as { - hooks?: Record>; - }; + const hooksPath = join(agentDir, 'hooks', 'hooks.yaml'); + if (!existsSync(hooksPath)) { + return null; + } - if (!hooksConfig?.hooks) return null; + const hooksYaml = readFileSync(hooksPath, 'utf-8'); + const hooksConfig = yaml.load(hooksYaml) as { hooks: Record> }; + if (!hooksConfig.hooks || Object.keys(hooksConfig.hooks).length === 0) { + return null; + } + // Map gitagent hook events to Gemini CLI hook events // Gemini CLI uses PascalCase event names const eventMap: Record = { diff --git a/src/runners/gemini.ts b/src/runners/gemini.ts new file mode 100644 index 0000000..645a0f9 --- /dev/null +++ b/src/runners/gemini.ts @@ -0,0 +1,125 @@ +import { spawnSync } from 'node:child_process'; +import { mkdirSync, writeFileSync, rmSync, existsSync, cpSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { randomBytes } from 'node:crypto'; +import { exportToGemini } from '../adapters/gemini.js'; +import { AgentManifest } from '../utils/loader.js'; +import { error, info } from '../utils/format.js'; + +export interface GeminiRunOptions { + prompt?: string; +} + +/** + * Run a gitagent agent using Google Gemini CLI. + * + * Creates a temporary workspace with: + * - GEMINI.md (agent instructions) + * - .gemini/settings.json (model config, tool permissions, approval mode) + * + * Then launches `gemini` in that workspace. Gemini CLI reads both files + * automatically on startup. + * + * Supports both interactive mode (no prompt) and single-shot mode (`gemini -p`). + */ +export function runWithGemini(agentDir: string, manifest: AgentManifest, options: GeminiRunOptions = {}): void { + const exp = exportToGemini(agentDir); + + // Create a temporary workspace + const workspaceDir = join(tmpdir(), `gitagent-gemini-${randomBytes(4).toString('hex')}`); + mkdirSync(workspaceDir, { recursive: true }); + + // Write GEMINI.md at project root + writeFileSync(join(workspaceDir, 'GEMINI.md'), exp.instructions, 'utf-8'); + + // Create .gemini directory and write settings.json + const geminiDir = join(workspaceDir, '.gemini'); + mkdirSync(geminiDir, { recursive: true }); + writeFileSync(join(geminiDir, 'settings.json'), JSON.stringify(exp.settings, null, 2), 'utf-8'); + + // Copy hooks directory if it exists (needed for hook script execution) + const hooksDir = join(agentDir, 'hooks'); + if (existsSync(hooksDir)) { + const targetHooksDir = join(workspaceDir, 'hooks'); + cpSync(hooksDir, targetHooksDir, { recursive: true }); + } + + info(`Workspace prepared at ${workspaceDir}`); + info(` GEMINI.md, .gemini/settings.json`); + if (manifest.model?.preferred) { + info(` Model: ${manifest.model.preferred}`); + } + + // Build gemini CLI args + const args: string[] = []; + + // Model override (if specified in manifest and not in settings) + if (manifest.model?.preferred && !exp.settings.model) { + args.push('--model', manifest.model.preferred); + } + + // Approval mode from compliance (if not already in settings) + if (manifest.compliance?.supervision?.human_in_the_loop && !exp.settings.approvalMode) { + const hitl = manifest.compliance.supervision.human_in_the_loop; + if (hitl === 'always') { + args.push('--approval-mode', 'plan'); + } else if (hitl === 'conditional') { + args.push('--approval-mode', 'default'); + } else if (hitl === 'none') { + args.push('--approval-mode', 'yolo'); + } else if (hitl === 'advisory') { + args.push('--approval-mode', 'auto_edit'); + } + } + + // Single-shot mode uses `gemini -p "..."`, interactive is just `gemini` + if (options.prompt) { + args.push('-p', options.prompt); + } + + info(`Launching Gemini CLI agent "${manifest.name}"...`); + if (!options.prompt) { + info('Starting interactive mode. Type your messages to chat.'); + } + + // On Windows with shell: true, we need to build a properly quoted command string + // On Unix, we can pass args array directly + let result; + if (process.platform === 'win32') { + // Build command string with proper quoting for Windows shell + const quotedArgs = args.map(arg => { + // Quote arguments that contain spaces or special characters + if (arg.includes(' ') || arg.includes('"')) { + return `"${arg.replace(/"/g, '\\"')}"`; + } + return arg; + }); + const commandString = `gemini ${quotedArgs.join(' ')}`; + + result = spawnSync(commandString, [], { + stdio: 'inherit', + cwd: workspaceDir, + env: { ...process.env }, + shell: true, + }); + } else { + result = spawnSync('gemini', args, { + stdio: 'inherit', + cwd: workspaceDir, + env: { ...process.env }, + }); + } + + // Cleanup temp workspace before exiting + try { rmSync(workspaceDir, { recursive: true, force: true }); } catch { /* ignore */ } + + if (result.error) { + error(`Failed to launch Gemini CLI: ${result.error.message}`); + info('Make sure Gemini CLI is installed: npm install -g @google/gemini-cli'); + info('Or visit: https://github.com/google-gemini/gemini-cli'); + process.exit(1); + } + + process.exit(result.status ?? 0); +} From 2eb45f37ca7eaa7e7b135f6852cf6d5d0c592546 Mon Sep 17 00:00:00 2001 From: Amit Acharya <100467234+AJAmit17@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:29:00 +0530 Subject: [PATCH 3/5] fix: prefix .sh hook scripts with 'bash hooks/' on Windows for Gemini CLI On Windows, Gemini CLI uses PowerShell which can't execute .sh files directly. Prepend 'bash' and include the hooks/ directory path when the script ends with .sh on win32 platform. --- src/adapters/gemini.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/adapters/gemini.ts b/src/adapters/gemini.ts index f0c6d27..146d375 100644 --- a/src/adapters/gemini.ts +++ b/src/adapters/gemini.ts @@ -349,7 +349,13 @@ function buildHooksConfig(agentDir: string): Record | null { // Convert each hook to Gemini CLI format with matcher const geminiHookDefs = validHooks.map((hook, index) => { + // On Windows, Gemini CLI uses PowerShell which can't run .sh files directly + // Prefix with 'bash' and include hooks/ directory path let command = hook.script; + if (process.platform === 'win32' && hook.script.endsWith('.sh')) { + command = `bash hooks/${hook.script}`; + } + return { name: `hook-${index}`, type: 'command', From f3c7abc5d0f9aa2db5786ab3dd6787cbed955802 Mon Sep 17 00:00:00 2001 From: Amit Acharya <100467234+AJAmit17@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:29:20 +0530 Subject: [PATCH 4/5] fix: always prefix hook scripts with 'hooks/' directory in Gemini adapter Previously only Windows .sh scripts got the hooks/ prefix. Now all hook scripts include the hooks/ directory path, with Windows .sh scripts additionally getting the 'bash' prefix. --- src/adapters/gemini.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapters/gemini.ts b/src/adapters/gemini.ts index 146d375..438805a 100644 --- a/src/adapters/gemini.ts +++ b/src/adapters/gemini.ts @@ -351,9 +351,9 @@ function buildHooksConfig(agentDir: string): Record | null { const geminiHookDefs = validHooks.map((hook, index) => { // On Windows, Gemini CLI uses PowerShell which can't run .sh files directly // Prefix with 'bash' and include hooks/ directory path - let command = hook.script; + let command = `hooks/${hook.script}`; if (process.platform === 'win32' && hook.script.endsWith('.sh')) { - command = `bash hooks/${hook.script}`; + command = `bash ${command}`; } return { From fe3768766330c8294610a1f8bb604beac25010de Mon Sep 17 00:00:00 2001 From: Amit Acharya <100467234+AJAmit17@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:43:53 +0530 Subject: [PATCH 5/5] chore: update documentation for gemini adaptor --- docs/adapters/gemini.md | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/docs/adapters/gemini.md b/docs/adapters/gemini.md index a839753..474060e 100644 --- a/docs/adapters/gemini.md +++ b/docs/adapters/gemini.md @@ -41,7 +41,7 @@ gemini --version | `skills/*/SKILL.md` | `GEMINI.md` (skills section) | Progressive disclosure with full instructions | | `tools/*.yaml` | `.gemini/settings.json` → `allowedTools` | Tool names extracted from YAML | | `knowledge/` (always_load) | `GEMINI.md` (knowledge section) | Reference documents embedded | -| `manifest.model.preferred` | `.gemini/settings.json` → `model` | Model selection (e.g., `gemini-2.0-flash-exp`) | +| `manifest.model.preferred` | `.gemini/settings.json` → `model` | Model object with `id` and `provider` (e.g., `{"id": "gemini-2.0-flash-exp", "provider": "google"}`) | | `manifest.compliance.supervision.human_in_the_loop` | CLI flag `--approval-mode` | Approval mode mapping (see below) | | `hooks/hooks.yaml` | `.gemini/settings.json` → `hooks` | Lifecycle event handlers | | `agents/` (sub-agents) | `GEMINI.md` (delegation section) | Documented as pattern (no native support) | @@ -111,7 +111,10 @@ Agent description # === .gemini/settings.json === { - "model": "gemini-2.0-flash-exp", + "model": { + "id": "gemini-2.0-flash-exp", + "provider": "google" + }, "allowedTools": ["bash", "edit", "read"], "approvalMode": "default", "hooks": {...} @@ -249,15 +252,15 @@ When importing `GEMINI.md`, sections are split based on keywords: ### Event Name Mapping -| gitagent Event | Gemini CLI Event | -|---------------|------------------| -| `on_session_start` | `session_start` | -| `pre_tool_use` | `pre_tool_use` | -| `post_tool_use` | `post_tool_use` | -| `pre_response` | `pre_response` | -| `post_response` | `post_response` | -| `on_error` | `on_error` | -| `on_session_end` | `session_end` | +| gitagent Event | Gemini CLI Event | Notes | +|---------------|------------------|-------| +| `on_session_start` | `SessionStart` | Runs at session initialization | +| `pre_tool_use` | `BeforeTool` | Runs before tool execution | +| `post_tool_use` | `AfterTool` | Runs after tool execution | +| `pre_response` | `AfterModel` | Runs after model generates response | +| `post_response` | `AfterAgent` | Runs after agent loop completes | +| `on_error` | `Notification` | Error notifications | +| `on_session_end` | `SessionEnd` | Runs at session cleanup | ### Hook Format @@ -273,16 +276,25 @@ hooks: ```json { "hooks": { - "session_start": [ + "SessionStart": [ { - "script": "scripts/init.sh", - "description": "Initialize session" + "matcher": "*", + "hooks": [ + { + "name": "hook-0", + "type": "command", + "command": "bash hooks/scripts/init.sh", + "description": "Initialize session" + } + ] } ] } } ``` +**Note:** On Windows, commands are prefixed with `bash` to enable execution through PowerShell. On Linux/macOS, the `bash` prefix is omitted. + ## Best Practices ### When Exporting