[Feat]: Add gemini cli plugin#912
[Feat]: Add gemini cli plugin#912priyanshuharshbodhi1 wants to merge 10 commits intoComposioHQ:mainfrom
Conversation
Add @composio/ao-plugin-agent-gemini workspace package with package.json and tsconfig.json. Register it in: - packages/cli/package.json (workspace dependency) - packages/core/src/plugin-registry.ts (BUILTIN_PLUGINS) - packages/cli/src/lib/plugins.ts (static import map) - packages/cli/src/lib/detect-agent.ts (ao init detection list) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full plugin implementation (~450 lines):
- promptDelivery: "post-launch" — Gemini runs as interactive REPL in tmux.
Using -p causes Gemini to exit after one response (AO marks session killed).
- getLaunchCommand: resolves absolute binary path at creation time so tmux
shells without nvm in PATH still find the real binary, not a guard wrapper.
- getActivityState: reads lastUpdated from session JSON (more accurate than
file mtime on network filesystems). 30-second session file cache prevents
redundant readdir per poll cycle.
- getSessionInfo: extracts summary and per-message token cost from session
JSON. Falls back to first user message as summary if no .summary field.
- getRestoreCommand: gemini --resume <session-id>, AO sends task post-launch.
- postLaunchSetup: writes workspace hooks, pre-trusts hook in
trusted_hooks.json, polls tmux pane for REPL prompt (❯) before the
session manager sends the task — prevents prompt injection into bash.
- detect(): execFileSync("gemini", ["--version"]) binary presence check.
Session path: ~/.gemini/tmp/<sha256(projectRoot)>/chats/session-*.json
Project hash: full 64-char SHA-256 hex, verified against Gemini CLI source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Coverage: - manifest shape and plugin export - getGeminiProjectHash / getGeminiChatsDir path construction - getLaunchCommand: base, permissionless/auto-edit/default, model, system prompt, no -p flag (post-launch delivery) - getActivityState: active/ready/idle/exited, process check, session cache - getSessionInfo: summary, cost extraction, first-message fallback - getRestoreCommand: uses resolved binary + --resume flag - setupWorkspaceHooks: creates dir, writes script, registers AfterTool hook, merges existing settings, no duplicate hooks, pre-trusts in trusted_hooks.json - postLaunchSetup: writes hooks, polls for ❯ REPL prompt, early exit on guard message or tmux failure, uses relative path in settings.json - METADATA_UPDATER_SCRIPT: bash shebang, run_shell_command filter, gh pr create / git checkout / gh pr merge detection, path traversal guard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
b68b4f4 to
06d75cd
Compare
| if (config.issueId) { | ||
| env["AO_ISSUE_ID"] = config.issueId; | ||
| } | ||
| return env; |
There was a problem hiding this comment.
Missing buildAgentPath — every non-Claude-Code agent (codex, aider, opencode) prepends ~/.ao/bin to PATH so the AO gh/git wrappers intercept commands and update session metadata. Without this, PRs created by Gemini won't reliably appear in the dashboard.
import { buildAgentPath, setupPathWrapperWorkspace, ... } from "@composio/ao-core";
// in getEnvironment:
env["PATH"] = buildAgentPath(process.env["PATH"]);
See agent-codex/src/index.ts:389, agent-aider/src/index.ts:149, agent-opencode/src/index.ts:291 for the
pattern.| }, | ||
|
|
||
| async setupWorkspaceHooks(workspacePath: string, _config: WorkspaceHooksConfig): Promise<void> { | ||
| await writeGeminiHooks(workspacePath); |
There was a problem hiding this comment.
Also needs setupPathWrapperWorkspace(workspacePath) here alongside writeGeminiHooks. This installs the ~/.ao/bin/gh and ~/.ao/bin/git wrappers and writes .ao/AGENTS.md context. Every other PATH-wrapper agent calls both their native hooks AND this function in setupWorkspaceHooks and postLaunchSetup.
async setupWorkspaceHooks(workspacePath: string, _config: WorkspaceHooksConfig): Promise {
await writeGeminiHooks(workspacePath);
await setupPathWrapperWorkspace(workspacePath); // <-- add this
},
Same addition needed in postLaunchSetup at line 773. See agent-codex/src/index.ts:616,633 for the pattern.
| try { | ||
| let content = pendingSystemPrompt ?? ""; | ||
| if (pendingSystemPromptFile) { | ||
| content = readFileSync(pendingSystemPromptFile, "utf-8"); |
There was a problem hiding this comment.
This uses readFileSync (sync, blocking) inside an async function. The async readFile from node:fs/promises is already imported at line 23, use that instead:
content = await readFile(pendingSystemPromptFile, "utf-8");
This keeps the async function non-blocking and consistent with the rest of the file.
…and verifying process status
…tem prompt handling for Gemini agent
…huharshbodhi1/agent-orchestrator into feat/agent-gemini-plugin
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4e2d06a. Configure here.
| for (const msg of session.messages) { | ||
| if (msg.type === "gemini" && msg.tokens) { | ||
| inputTokens += msg.tokens.input ?? 0; | ||
| inputTokens += msg.tokens.cached ?? 0; |
There was a problem hiding this comment.
Cached tokens double-counted inflating cost estimates
Medium Severity
extractCost adds msg.tokens.cached to inputTokens on line 303, but per the Gemini API, cached_content_token_count is a subset of prompt_token_count (input), not additive. This double-counts cached tokens, inflating both the reported inputTokens and estimatedCostUsd. The test data uses cached: 0 which masks the bug — any session with non-zero cached usage will report incorrect values.
Reviewed by Cursor Bugbot for commit 4e2d06a. Configure here.
| const lastLine = lines[lines.length - 1]?.trim() ?? ""; | ||
|
|
||
| // REPL prompt visible → agent is idle and waiting for input | ||
| if (/^(gemini\s+)?[❯>]\s*$/.test(lastLine)) return "idle"; |
There was a problem hiding this comment.
Activity classifier matches bare > unlike REPL detector
Low Severity
classifyGeminiOutput matches [❯>] at line 446, treating bare > as an idle REPL prompt. However, postLaunchSetup at line 837 intentionally matches only ❯, with a comment explaining that > causes false positives from bash heredocs and PS1 prompts. The same false-positive risk applies to ongoing activity detection — terminal output ending with > would be misclassified as idle.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 4e2d06a. Configure here.
There was a problem hiding this comment.
Pull request overview
Adds a new built-in Agent Orchestrator agent plugin that integrates Google Gemini CLI, enabling projects to configure agent: gemini and providing session/activity detection, restore support, and workspace hook setup.
Changes:
- Introduces
@composio/ao-plugin-agent-geminiwith launch/env wiring, activity detection, session metadata extraction, and Gemini hook setup. - Registers Gemini as a built-in agent across core + CLI (plugin registry, plugin wiring, agent detection).
- Updates Codex agent session-meta parsing to support newer Codex JSONL shape (
payload.cwd).
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds workspace link entries for the new agent-gemini package. |
| packages/plugins/agent-gemini/package.json | New Gemini agent plugin package metadata, scripts, and deps. |
| packages/plugins/agent-gemini/tsconfig.json | TypeScript build config for the new plugin package. |
| packages/plugins/agent-gemini/src/index.ts | Implements Gemini agent behavior, session parsing, activity detection, and Gemini AfterTool hook script generation. |
| packages/plugins/agent-gemini/src/index.test.ts | Unit tests covering manifest, launch/env, activity/session parsing, hook setup, and post-launch readiness polling. |
| packages/plugins/agent-codex/src/index.ts | Updates session meta matching logic to support payload.cwd in newer Codex CLI versions. |
| packages/core/src/plugin-registry.ts | Registers gemini in the built-in plugin list for automatic discovery/loading. |
| packages/cli/src/lib/plugins.ts | Adds static import + registration mapping for Gemini agent in the CLI. |
| packages/cli/src/lib/detect-agent.ts | Adds Gemini to the CLI agent auto-detection list. |
| packages/cli/package.json | Adds the Gemini agent plugin as a workspace dependency. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| else | ||
| tool_name=\$(echo "\$input" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 || echo "") | ||
| command=\$(echo "\$input" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 || echo "") | ||
| output=\$(echo "\$input" | grep -o '"llmContent"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 || echo "") |
There was a problem hiding this comment.
In the jq-absent fallback parser, output is extracted only from the llmContent field. When Gemini’s hook payload provides the command output under tool_response (string) without llmContent, this fallback will silently miss PR URLs and metadata won’t update. Consider expanding the fallback to also extract tool_response (and/or tool_response.llmContent) similarly to the jq path.
| output=\$(echo "\$input" | grep -o '"llmContent"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 || echo "") | |
| output=\$(echo "\$input" | grep -o '"llmContent"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 || echo "") | |
| if [[ -z "\$output" ]]; then | |
| output=\$(echo "\$input" | grep -o '"tool_response"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 || echo "") | |
| fi |
| # Input : JSON on stdin (session_id, tool_name, tool_input, tool_response, …) | ||
| # Output: JSON on stdout (empty {} by default, systemMessage on updates) | ||
|
|
||
| set -euo pipefail | ||
|
|
There was a problem hiding this comment.
The hook script updates AO metadata based on the detected command, but it never checks the tool’s exit status. This can incorrectly mark status=merged / status=pr_open or update branch metadata even when the underlying gh/git command failed. Consider parsing exit_code from the hook JSON (Gemini includes it) and only applying metadata updates when it is 0 (similar to the existing wrapper/hook logic elsewhere in the repo).
| # Confirm metadata file is still inside ao_dir after symlink resolution | ||
| real_ao_dir=\$(cd "\$AO_DATA_DIR" 2>/dev/null && pwd -P) || { echo '{}'; exit 0; } | ||
| real_dir=\$(cd "\$(dirname "\$metadata_file")" 2>/dev/null && pwd -P) || { echo '{}'; exit 0; } | ||
| [[ "\$real_dir" == "\$real_ao_dir"* ]] || { echo '{}'; exit 0; } | ||
| [[ -f "\$metadata_file" ]] || { echo '{}'; exit 0; } |
There was a problem hiding this comment.
The path allowlist for AO_DATA_DIR is checked before canonicalization, but after pwd -P the script does not re-validate the canonical real_ao_dir against the allowlist. This leaves a bypass where a value like /tmp/../../home/user/.agent-orchestrator/... passes the initial /tmp/* check but resolves outside /tmp. Consider re-validating real_ao_dir after canonicalization (as done in packages/core/src/agent-workspace-hooks.ts) before allowing metadata writes.
| { slot: "agent", name: "claude-code", pkg: "@composio/ao-plugin-agent-claude-code" }, | ||
| { slot: "agent", name: "codex", pkg: "@composio/ao-plugin-agent-codex" }, | ||
| { slot: "agent", name: "aider", pkg: "@composio/ao-plugin-agent-aider" }, | ||
| { slot: "agent", name: "gemini", pkg: "@composio/ao-plugin-agent-gemini" }, | ||
| { slot: "agent", name: "opencode", pkg: "@composio/ao-plugin-agent-opencode" }, |
There was a problem hiding this comment.
Adding Gemini to BUILTIN_PLUGINS means loadBuiltins() will now attempt to import @composio/ao-plugin-agent-gemini. The existing unit test packages/core/src/__tests__/plugin-registry.test.ts stubs importFn for only claude-code/codex/opencode and will start failing unless it’s updated to handle the new package. Also note that the Next.js web app registers a hardcoded subset of agent plugins in packages/web/src/lib/services.ts and transpiles a hardcoded list in packages/web/next.config.js; if the web dashboard is expected to support agent: gemini, those lists need to include the new plugin as well.


Fixes #931
What
Adds
@composio/ao-plugin-agent-gemini- full support for Google Gemini CLI as an agent in Agent Orchestrator. Any project can now setagent: geminiinagent-orchestrator.yaml.Why
Gemini CLI is a first-class AI coding agent alongside Claude Code, Codex, Aider, and OpenCode. This rounds out the default agent roster.
Key Design Decisions
promptDelivery: "post-launch"- Gemini's-pflag exits after one response; AO would mark the session killed. Instead, AO launches Gemini as an interactive REPL in tmux and sends the task prompt after startup viaruntime.sendMessage().Absolute binary resolution -
resolveGeminiBinarySync()callswhich geminiinside the CLI process (which has nvm in PATH). The resolved absolute path is used in the launch command so tmux shells without nvm still find the real binary.Hook trust pre-population - AO writes
~/.gemini/trusted_hooks.jsonwhen setting up workspace hooks so Gemini skips its "trust these hooks?" warning dialog before the REPL is ready.Session file path -
~/.gemini/tmp/<sha256(projectRoot)>/chats/session-*.json. Hash is the full 64-char SHA-256 hex, verified againstgoogle-gemini/gemini-clisource.Metadata hooks -
AfterToolhook in.gemini/settings.jsonfires after everyrun_shell_command. Detectsgh pr create,git checkout -b, andgh pr mergeto auto-update AO session metadata. Uses a relative path for symlink safety across worktrees.Activity classification - reads
lastUpdatedfrom session JSON (more accurate than file mtime). 30-second session file cache prevents doublereaddirper poll cycle.Testing
84 unit tests covering: manifest, binary resolution, launch command flags, activity states, session info/cost extraction, restore command, workspace hooks (write, merge, no-duplicate, hook trust),
postLaunchSetupREPL readiness polling.