Add Gemini CLI support to Orchestrator
Register Gemini CLI with auth detection and a verified headless workflow. Expose its settings and status metadata, update the plugin contracts and tests, bump the plugin version, and replace the thumbnail.
Alessandro committed
Jul 10, 2026 at 18:41 UTC
8aad52784b8e639582de555e3ea9b725e0628519
14 files changed
+176
-12
plugins/_orchestrator/AGENTS.md
+1
-1
@@ -4,7 +4,7 @@
4
5
- Provide a bundled Agent Zero plugin for delegating repository and coding work to external terminal/headless agents.
6
- Keep heavy delegation instructions out of the always-loaded prompt by exposing the `orchestrator` skill instead of a `terminal_agent` tool.
7
-- Own adapter status metadata, settings UI, Codex device login APIs, and skill instructions for Agent Zero headless, Codex CLI, Claude Code, Cursor CLI, Grok Build, Hermes Agent, OpenCode, and future terminal agents.
7
+- Own adapter status metadata, settings UI, Codex device login APIs, and skill instructions for Agent Zero headless, Codex CLI, Claude Code, Cursor CLI, Gemini CLI, Grok Build, Hermes Agent, OpenCode, and future terminal agents.
8
9
## Ownership
10
plugins/_orchestrator/README.md
+5
-1
@@ -10,7 +10,7 @@ The plugin provides:
10
- a Settings > External Services status screen for configured binaries and
11
detected auth state;
12
- adapter metadata for Agent Zero headless, OpenAI Codex CLI, Claude Code,
13
- Cursor CLI, Grok Build, Hermes Agent, OpenCode, and future terminal agents.
13
+ Cursor CLI, Gemini CLI, Grok Build, Hermes Agent, OpenCode, and future terminal agents.
14
15
There is intentionally no `terminal_agent` tool and no settings-screen install
16
button. When the user explicitly asks for a terminal agent, the skill first
@@ -30,6 +30,7 @@ Docker runtime.
30
| OpenAI Codex | `codex` | ChatGPT device login from settings or external CLI login |
31
| Claude Code | `claude` | External `claude` login or `ANTHROPIC_API_KEY` |
32
| Cursor CLI | `agent` | `CURSOR_API_KEY`, `NO_OPEN_BROWSER=1 agent login`, or cached Cursor login |
33
+| Gemini CLI | `gemini` | Cached Google login, `GEMINI_API_KEY`, or Vertex AI credentials |
34
| Grok Build | `grok` | `XAI_API_KEY`, `grok login --device-auth`, or cached Grok login |
35
| Hermes Agent | `hermes` | External Hermes/provider setup, `~/.hermes/.env`, `~/.hermes/auth.json`, or provider env vars |
36
| OpenCode | `opencode` | External `opencode auth login`, provider env vars, or `~/.local/share/opencode/auth.json` |
@@ -152,6 +153,9 @@ cursor:
153
binary: agent
154
output_format: text
155
force: true
156
+gemini:
157
+ binary: gemini
158
+ model: ""
159
grok:
160
binary: grok
161
model: ""
plugins/_orchestrator/default_config.yaml
+3
@@ -15,6 +15,9 @@ cursor:
15
binary: agent
16
output_format: text
17
force: true
18
+gemini:
19
+ binary: gemini
20
+ model: ""
21
grok:
22
binary: grok
23
model: ""
plugins/_orchestrator/helpers/adapters/AGENTS.md
+4
-1
@@ -7,7 +7,7 @@
7
8
## Ownership
9
10
-- Owns `base.py` plus adapter modules for A0 Headless, Codex CLI, Claude Code, Cursor CLI, Grok Build, Hermes Agent, OpenCode, and future status adapters.
10
+- Owns `base.py` plus adapter modules for A0 Headless, Codex CLI, Claude Code, Cursor CLI, Gemini CLI, Grok Build, Hermes Agent, OpenCode, and future status adapters.
11
- Owns credential-path detection and safe disconnect behavior only when the adapter can identify the exact credential store.
12
13
## Local Contracts
@@ -42,6 +42,9 @@
42
- Treat `XAI_API_KEY` as environment auth.
43
- Detect `~/.grok/config.toml`, `~/.grok/auth.json`, and `~/.grok/auth/` without returning secret contents.
44
- Do not model the full-screen TUI as a status API flow.
45
+- Gemini CLI:
46
+ - Detect `GEMINI_API_KEY`, `GOOGLE_API_KEY`, service-account/ADC files, and current or legacy Gemini credential files without returning secret contents.
47
+ - Do not model Gemini's interactive sign-in TUI as a status API flow.
48
- Hermes Agent and OpenCode:
49
- Detect known provider environment variables and known auth files.
50
- Secret detection should answer yes/no without returning secret values.
plugins/_orchestrator/helpers/adapters/gemini.py
new
+78
@@ -0,0 +1,78 @@
1
+from __future__ import annotations
2
+
3
+import os
4
+from pathlib import Path
5
+from typing import Any
6
+
7
+from plugins._orchestrator.helpers.adapters.base import TerminalAgentAdapter
8
+
9
+
10
+_API_KEY_ENV_VARS = ("GEMINI_API_KEY", "GOOGLE_API_KEY")
11
+
12
+
13
+class GeminiCliAdapter(TerminalAgentAdapter):
14
+ id = "gemini"
15
+ title = "Gemini CLI"
16
+ binary = "gemini"
17
+ install_hint = "npm install -g @google/gemini-cli"
18
+ description = "Google Gemini CLI in headless single-prompt mode."
19
+
20
+ def _home(self) -> Path:
21
+ configured = os.environ.get("GEMINI_CLI_HOME", "").strip()
22
+ root = Path(configured).expanduser() if configured else Path.home()
23
+ return root / ".gemini"
24
+
25
+ def auth_status(self, config: dict[str, Any] | None = None) -> dict[str, Any]:
26
+ env_var = next((name for name in _API_KEY_ENV_VARS if os.environ.get(name)), "")
27
+ if env_var:
28
+ return {"connected": True, "mode": "env", "auth_path": env_var}
29
+
30
+ credentials = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", "").strip()
31
+ if credentials and _nonempty_file(Path(credentials).expanduser()):
32
+ return {
33
+ "connected": True,
34
+ "mode": "env",
35
+ "auth_path": "GOOGLE_APPLICATION_CREDENTIALS",
36
+ }
37
+
38
+ home = self._home()
39
+ for path in (home / "gemini-credentials.json", home / "oauth_creds.json"):
40
+ if _nonempty_file(path):
41
+ return {"connected": True, "mode": "external", "auth_path": str(path)}
42
+
43
+ if _env_file_has_key(home / ".env"):
44
+ return {"connected": True, "mode": "external", "auth_path": str(home / ".env")}
45
+
46
+ adc = _adc_path()
47
+ if _nonempty_file(adc):
48
+ return {"connected": True, "mode": "external", "auth_path": str(adc)}
49
+
50
+ return {"connected": False, "mode": "", "auth_path": str(home)}
51
+
52
+
53
+def _adc_path() -> Path:
54
+ configured = os.environ.get("CLOUDSDK_CONFIG", "").strip()
55
+ root = Path(configured).expanduser() if configured else Path.home() / ".config" / "gcloud"
56
+ return root / "application_default_credentials.json"
57
+
58
+
59
+def _nonempty_file(path: Path) -> bool:
60
+ try:
61
+ return path.is_file() and path.stat().st_size > 0
62
+ except OSError:
63
+ return False
64
+
65
+
66
+def _env_file_has_key(path: Path) -> bool:
67
+ try:
68
+ lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
69
+ except OSError:
70
+ return False
71
+ for line in lines:
72
+ raw = line.strip()
73
+ if not raw or raw.startswith("#") or "=" not in raw:
74
+ continue
75
+ key, value = raw.split("=", 1)
76
+ if key.strip() in _API_KEY_ENV_VARS and value.strip().strip("'\""):
77
+ return True
78
+ return False
plugins/_orchestrator/helpers/registry.py
+2
@@ -7,6 +7,7 @@ from plugins._orchestrator.helpers.adapters.base import TerminalAgentAdapter
7
from plugins._orchestrator.helpers.adapters.claude import ClaudeCodeAdapter
8
from plugins._orchestrator.helpers.adapters.codex import CodexAdapter
9
from plugins._orchestrator.helpers.adapters.cursor import CursorCliAdapter
10
+from plugins._orchestrator.helpers.adapters.gemini import GeminiCliAdapter
11
from plugins._orchestrator.helpers.adapters.grok import GrokBuildAdapter
12
from plugins._orchestrator.helpers.adapters.hermes import HermesAgentAdapter
13
from plugins._orchestrator.helpers.adapters.opencode import OpenCodeAdapter
@@ -19,6 +20,7 @@ _ADAPTERS: dict[str, TerminalAgentAdapter] = {
20
CodexAdapter(),
21
ClaudeCodeAdapter(),
22
CursorCliAdapter(),
23
+ GeminiCliAdapter(),
24
GrokBuildAdapter(),
25
HermesAgentAdapter(),
26
OpenCodeAdapter(),
plugins/_orchestrator/plugin.yaml
+2
-2
@@ -1,7 +1,7 @@
1
name: _orchestrator
2
title: Orchestrator
3
-description: Load-on-demand skill and status UI for external headless terminal coding agents such as Agent Zero, Codex, Claude Code, Cursor CLI, Grok Build, Hermes Agent, and OpenCode.
4
-version: 0.1.0
3
+description: Load-on-demand skill and status UI for external headless terminal coding agents such as Agent Zero, Codex, Claude Code, Cursor CLI, Gemini CLI, Grok Build, Hermes Agent, and OpenCode.
4
+version: 0.2.0
5
settings_sections:
6
- external
7
per_project_config: false
plugins/_orchestrator/skills/orchestrator/AGENTS.md
+1
-1
@@ -4,7 +4,7 @@
4
5
- Teach Agent Zero how to delegate work to external terminal/headless coding agents only after the skill is loaded.
6
- Keep generic orchestration rules in `SKILL.md` and agent-specific command runbooks in `references/`.
7
-- Provide copy-ready command patterns for A0 Headless, Codex CLI, Claude Code, Cursor CLI, Grok Build, Hermes Agent, and OpenCode through reference files.
7
+- Provide copy-ready command patterns for A0 Headless, Codex CLI, Claude Code, Cursor CLI, Gemini CLI, Grok Build, Hermes Agent, and OpenCode through reference files.
8
9
## Ownership
10
plugins/_orchestrator/skills/orchestrator/SKILL.md
+6
-4
@@ -1,6 +1,6 @@
1
---
2
name: orchestrator
3
-description: Use when delegating coding or repository work to external terminal coding agents such as the user's host Claude Code/Codex/Cursor CLI or container-installed pal agents.
3
+description: Use when delegating coding or repository work to external terminal coding agents such as the user's host Claude Code/Codex/Cursor/Gemini CLI or container-installed pal agents.
4
triggers:
5
- "terminal agent"
6
- "external coding agent"
@@ -8,6 +8,7 @@ triggers:
8
- "delegate to claude code"
9
- "delegate to cursor"
10
- "delegate to cursor cli"
11
+ - "delegate to gemini cli"
12
- "delegate to grok build"
13
- "delegate to a0 headless"
14
- "delegate to hermes"
@@ -27,12 +28,12 @@ Prefer the user's own host-machine CLI when that is what they mean. Container-in
28
29
## Rules
30
30
-- For Codex, Claude Code, Cursor CLI, Grok Build, Hermes Agent, and OpenCode, first decide the execution place: the user's local machine through A0 CLI, or the Agent Zero Docker/container shell.
31
+- For Codex, Claude Code, Cursor CLI, Gemini CLI, Grok Build, Hermes Agent, and OpenCode, first decide the execution place: the user's local machine through A0 CLI, or the Agent Zero Docker/container shell.
32
- If the user did not specify local/host versus container, check memory for this coding agent's execution-place preference. If no current preference is known, ask: "Do you want me to use your own local <agent> through A0 CLI, or the <agent> installed inside the Agent Zero container?"
33
- After the user chooses, save a stable per-agent preference with `memory_save`, for example: "For orchestrator, the user prefers Claude Code to run on the host machine through A0 CLI by default." If memory tools are unavailable, continue without saving.
34
- Never use Computer Use to drive coding-agent terminals, menus, or TUIs. Use headless CLI commands through `code_execution_remote` or `code_execution_tool`; if that is not possible, stop and ask.
35
- ACP may be available as a community plugin, but do not assume it is installed. Mention it only if the user explicitly asks for ACP or the direct CLI path is unsuitable.
35
-- For Codex, Claude Code, Cursor CLI, Grok Build, Hermes Agent, and OpenCode, setup is part of the workflow: check whether the CLI is installed, install only the requested CLI if missing, probe its version/help, run a tiny smoke prompt, then run the real task.
36
+- For Codex, Claude Code, Cursor CLI, Gemini CLI, Grok Build, Hermes Agent, and OpenCode, setup is part of the workflow: check whether the CLI is installed, install only the requested CLI if missing, probe its version/help, run a tiny smoke prompt, then run the real task.
37
- Keep authentication human-in-the-loop. You may start the CLI login/setup command, relay the exact URL, device code, browser step, or prompt to the user, then wait for the user to confirm completion before retrying the smoke prompt.
38
- Never start a full-screen CLI/TUI as a login fallback. If you accidentally opened one and see welcome, theme, provider, or unreadable menu output, reset the terminal session instead of sending keys into it.
39
- If login/setup shows a menu or provider choices, show those choices to the user in chat and ask which one to select. Keep the terminal session open, then send the user's selected number/key back to that session.
@@ -45,7 +46,7 @@ Prefer the user's own host-machine CLI when that is what they mean. Container-in
46
47
## Host CLI Flow
48
48
-Use this when the user wants their own local Claude Code, Codex, Cursor CLI, Grok Build, Hermes Agent, or OpenCode.
49
+Use this when the user wants their own local Claude Code, Codex, Cursor CLI, Gemini CLI, Grok Build, Hermes Agent, or OpenCode.
50
51
1. Use `code_execution_remote`, not `code_execution_tool`, because paths, shell, login, and installed CLIs belong to the A0 CLI host machine.
52
2. If `code_execution_remote` is unavailable or reports no connected CLI / remote execution disabled, tell the user exactly:
@@ -98,6 +99,7 @@ You can consult both host and container agents in one workflow. Keep their shell
99
- Codex CLI: `references/codex.md`
100
- Claude Code: `references/claude.md`
101
- Cursor CLI: `references/cursor.md`
102
+- Gemini CLI: `references/gemini.md`
103
- Grok Build: `references/grok.md`
104
- Hermes Agent: `references/hermes.md`
105
- OpenCode: `references/opencode.md`
plugins/_orchestrator/skills/orchestrator/references/AGENTS.md
+1
-1
@@ -7,7 +7,7 @@
7
8
## Ownership
9
10
-- Owns one markdown file per agent id, currently `a0.md`, `codex.md`, `claude.md`, `cursor.md`, `grok.md`, `hermes.md`, and `opencode.md`.
10
+- Owns one markdown file per agent id, currently `a0.md`, `codex.md`, `claude.md`, `cursor.md`, `gemini.md`, `grok.md`, `hermes.md`, and `opencode.md`.
11
- Does not own adapter status code, settings UI, or global orchestration rules.
12
13
## Local Contracts
plugins/_orchestrator/skills/orchestrator/references/gemini.md
new
+41
@@ -0,0 +1,41 @@
1
+# Gemini CLI
2
+
3
+Use Gemini CLI for headless Google Gemini coding tasks. Always pass `-p`; bare `gemini` opens the interactive TUI.
4
+
5
+## Install And Probe
6
+
7
+```bash
8
+command -v gemini >/dev/null || npm install -g @google/gemini-cli
9
+gemini --version
10
+gemini --help
11
+```
12
+
13
+## Smoke Prompt
14
+
15
+```bash
16
+cd "$WORKDIR"
17
+gemini -p "Respond exactly: TERMINAL_AGENT_SMOKE_OK" --output-format json --approval-mode=yolo --skip-trust
18
+```
19
+
20
+## Login
21
+
22
+Headless mode uses existing cached Google credentials, a Gemini API key, or Vertex AI credentials. Do not start bare `gemini` through Agent Zero for login; it opens a TUI. For local browser sign-in, ask the user to run `gemini` in their own terminal, select **Sign in with Google**, finish in the browser, and then retry the smoke prompt.
23
+
24
+For container automation, prefer `GEMINI_API_KEY`. Ask the user to add it through **Settings > External Services > Secrets Management**, then source `/a0/usr/.env` without printing it:
25
+
26
+```bash
27
+set -a
28
+. /a0/usr/.env
29
+set +a
30
+```
31
+
32
+Vertex AI may instead use `GOOGLE_API_KEY`, `GOOGLE_APPLICATION_CREDENTIALS`, or cached Application Default Credentials. It also requires `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`. Never ask the user to paste keys or service-account JSON into chat.
33
+
34
+## Real Task
35
+
36
+```bash
37
+cd "$WORKDIR"
38
+gemini -p "$TASK" --output-format json --approval-mode=yolo --skip-trust
39
+```
40
+
41
+Add `-m "$MODEL"` only when the user or settings provide a model override. Use `--approval-mode=plan` instead of `yolo` when the user explicitly asks for read-only analysis.
plugins/_orchestrator/tests/test_status_adapters.py
+31
@@ -9,6 +9,8 @@ repo_root = next(
9
sys.path.insert(0, str(repo_root))
10
11
from plugins._orchestrator.helpers.adapters.cursor import CursorCliAdapter
12
+from plugins._orchestrator.helpers.adapters.gemini import _env_file_has_key
13
+from plugins._orchestrator.helpers.adapters.gemini import GeminiCliAdapter
14
from plugins._orchestrator.helpers.adapters.grok import _toml_has_secret
15
from plugins._orchestrator.helpers.adapters.grok import GrokBuildAdapter
16
from plugins._orchestrator.helpers.adapters.base import TerminalAgentAdapter
@@ -41,6 +43,7 @@ def test_registry_order_puts_a0_first():
43
"codex",
44
"claude",
45
"cursor",
46
+ "gemini",
47
"grok",
48
"hermes",
49
"opencode",
@@ -100,6 +103,23 @@ def test_cursor_detects_agent_zero_cursor_env_key():
103
os.environ["API_KEY_CURSOR"] = old_a0_cursor
104
105
106
+def test_gemini_detects_supported_auth_sources():
107
+ old_value = os.environ.get("GEMINI_API_KEY")
108
+ try:
109
+ os.environ["GEMINI_API_KEY"] = "secret"
110
+ assert GeminiCliAdapter().auth_status()["auth_path"] == "GEMINI_API_KEY"
111
+ finally:
112
+ if old_value is None:
113
+ os.environ.pop("GEMINI_API_KEY", None)
114
+ else:
115
+ os.environ["GEMINI_API_KEY"] = old_value
116
+
117
+ with tempfile.TemporaryDirectory() as tmp:
118
+ env_path = Path(tmp) / ".env"
119
+ env_path.write_text('GEMINI_API_KEY="secret"\n')
120
+ assert _env_file_has_key(env_path)
121
+
122
+
123
def test_grok_env_key_requires_present_environment_value():
124
old_value = os.environ.pop("GROK_TEST_KEY", None)
125
try:
@@ -136,6 +156,7 @@ def test_skill_documents_human_setup_loop_and_a0_exception():
156
codex_text = (references / "codex.md").read_text()
157
claude_text = (references / "claude.md").read_text()
158
cursor_text = (references / "cursor.md").read_text()
159
+ gemini_text = (references / "gemini.md").read_text()
160
grok_text = (references / "grok.md").read_text()
161
hermes_text = (references / "hermes.md").read_text()
162
opencode_text = (references / "opencode.md").read_text()
@@ -168,6 +189,7 @@ def test_skill_documents_human_setup_loop_and_a0_exception():
189
assert "references/codex.md" in skill_text
190
assert "references/claude.md" in skill_text
191
assert "references/cursor.md" in skill_text
192
+ assert "references/gemini.md" in skill_text
193
assert "references/grok.md" in skill_text
194
assert "references/hermes.md" in skill_text
195
assert "references/opencode.md" in skill_text
@@ -203,6 +225,14 @@ def test_skill_documents_human_setup_loop_and_a0_exception():
225
assert "agent status" in cursor_text
226
assert "not with an invented flag" in cursor_text
227
228
+ assert 'gemini -p "Respond exactly: TERMINAL_AGENT_SMOKE_OK"' in gemini_text
229
+ assert "--output-format json" in gemini_text
230
+ assert "--approval-mode=yolo" in gemini_text
231
+ assert "--skip-trust" in gemini_text
232
+ assert "GEMINI_API_KEY" in gemini_text
233
+ assert "GOOGLE_APPLICATION_CREDENTIALS" in gemini_text
234
+ assert "Do not start bare `gemini`" in gemini_text
235
+
236
assert "grok --no-auto-update --cwd \"$WORKDIR\" -p" in grok_text
237
assert "--output-format json" in grok_text
238
assert "--always-approve" in grok_text
@@ -224,6 +254,7 @@ if __name__ == "__main__":
254
test_claude_defaults_skip_permissions()
255
test_cursor_defaults_headless_automation()
256
test_cursor_detects_agent_zero_cursor_env_key()
257
+ test_gemini_detects_supported_auth_sources()
258
test_grok_defaults_headless_automation()
259
test_grok_env_key_requires_present_environment_value()
260
test_grok_detects_agent_zero_xai_env_key()
plugins/_orchestrator/thumbnail.png
Binary files a/plugins/_orchestrator/thumbnail.png and b/plugins/_orchestrator/thumbnail.png differ
plugins/_orchestrator/webui/config.html
+1
-1
@@ -302,7 +302,7 @@
302
</div>
303
</div>
304
305
- <div class="ta-field" x-show="agent.id === 'codex' || agent.id === 'claude' || agent.id === 'grok' || agent.id === 'hermes' || agent.id === 'opencode'">
305
+ <div class="ta-field" x-show="agent.id === 'codex' || agent.id === 'claude' || agent.id === 'gemini' || agent.id === 'grok' || agent.id === 'hermes' || agent.id === 'opencode'">
306
<div class="field-label">
307
<div class="field-title">Model</div>
308
<div class="field-description">Optional CLI model override.</div>