Skip to content

feat(agents): add Gemini CLI adapter#34

Merged
rgdevme merged 11 commits into
mainfrom
feat/gemini-cli-adapter
Jul 1, 2026
Merged

feat(agents): add Gemini CLI adapter#34
rgdevme merged 11 commits into
mainfrom
feat/gemini-cli-adapter

Conversation

@rgdevme

@rgdevme rgdevme commented Jul 1, 2026

Copy link
Copy Markdown
Owner

What

Adapts Gemini CLI (Google's official terminal agent harness) into agnos as the third first-tier CLI harness, and reworks the hooks layer so agnos owns a central hook-event mapping registry that adapters map into, with complete, verified coverage of every event each agent documents and no events ever dropped from the registry.

Gemini CLI adapter

Domain Gemini CLI native Notes
rules GEMINI.md Mirrored from the canonical rules file
mcp .gemini/settings.json#mcpServers Shared file; httpUrl for HTTP, url for SSE
hooks .gemini/settings.json#hooks Same shared file; Gemini's own event names
skills .gemini/skills/ Directory link to the canonical skills dir

mcp and hooks share settings.json via per-key read-modify-write, so they never clobber each other or user-owned keys. That shared file is deliberately not claimed, matching Claude Code.

Central hook-event mapping registry

  • agnos owns the canonical vocabulary — the union of every event any agent exposes (33 events: Claude Code's 30 plus Gemini's model-loop trio BeforeModel/AfterModel/BeforeToolSelection).
  • Each adapter declares a hookEvents map (canonical → its native name); identityEventMap keeps the verbatim-naming agents (Claude Code, Codex) terse.
  • Shared machinery (renderNativeHooks/scrapeNativeHooks) drives both directions. Nothing is dropped from the registry — scraping preserves every event; a per-agent render omits only events that agent can't express.

Verified coverage (against current docs)

Agent Native events Mapped
Claude Code 30 ✅ all 30
Codex 10 ✅ all 10 (added SubagentStart, PermissionRequest, PostCompact)
Gemini CLI 11 ✅ all 11

Codex's docs also list a handler statusMessage, so its hooks now render with messages preserved.

Side-effect: add-time support warning

hooks add <event> resolves the installed agents and warns which don't support the event, naming each:

$ agnos hooks add WorktreeCreate wt.sh
[hooks] event "WorktreeCreate" is not supported by OpenAI Codex, Gemini CLI; the hook will not be rendered for them
[hooks] added WorktreeCreate hook

Tests

  • Gemini adapter: mcp (stdio + httpUrl/url), hooks translation, mcp+hooks shared-file compose, skills, claims
  • renderNativeHooks/scrapeNativeHooks round-trips (identity + rename maps); supportsHookEvent
  • Per-adapter coverage suite: enumerates every documented native event and asserts the adapter maps it, plus negative checks for events an agent genuinely lacks
  • hooks add warning fires (naming the agent) and stays silent when all support it
  • Widened enum kept in sync in schema.json

Full gate green: lint (0 errors) → typechecktest (257 passed) → build. Verified end-to-end with the CLI (add-time warnings, correct per-agent rendering of newly-mapped events like PermissionRequest and WorktreeCreate, and hooks migrate round-tripping without drops).

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add Gemini CLI adapter and include it in the default built-in agent set

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add a first-tier Gemini CLI adapter to materialize rules and MCP settings.
• Register Gemini CLI in the closed adapter set and curated init defaults.
• Add round-trip tests for MCP server rendering/scraping and settings preservation.
Diagram

graph TD
Core["core mcp helpers"] --> Adapters["adapters/index.ts"] --> Gemini[["gemini-cli adapter"]] --> Rules(["GEMINI.md"])
Gemini --> Settings([".gemini/settings.json"])
Adapters --> Defaults["DEFAULT_AGENT_IDS"]
Tests["adapter/init tests"] --> Gemini
Tests --> Defaults
subgraph Legend
direction LR
_mod["Module"] ~~~ _ad[["Adapter"]] ~~~ _file(["File"])
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Write MCP config to a dedicated Gemini MCP file (not settings.json)
  • ➕ Avoids interacting with a shared settings file and any non-MCP user preferences
  • ➕ Simplifies merge/delete semantics (can fully own the file)
  • ➖ Does not match Gemini CLI’s native configuration location/shape as closely
  • ➖ Introduces a new file convention users must learn and potentially manually wire up
2. Fail hard (or overwrite) when .gemini/settings.json is invalid JSON
  • ➕ Ensures MCP configuration is always applied deterministically
  • ➕ Avoids silently leaving stale MCP servers configured
  • ➖ Risky: could destroy or block on user-owned settings
  • ➖ Current behavior is safer for a shared preferences file and aligns with key-preserving intent

Recommendation: Keep the current approach: treat .gemini/settings.json as a shared file and only manage the mcpServers key, preserving all unrelated keys and deleting mcpServers when empty. The explicit skip-with-warn on invalid JSON is the least surprising/most conservative behavior for a user-owned settings file, and the adapter remains consistent with the existing 'native file path + minimal ownership' recipe used by other agents.

Files changed (6) +276 / -8

Enhancement (2) +166 / -2
index.tsAdd Gemini CLI adapter (rules mirror + MCP settings.json integration) +163/-0

Add Gemini CLI adapter (rules mirror + MCP settings.json integration)

• Introduces the gemini-cli adapter with rules mirroring to GEMINI.md and MCP rendering/scraping via .gemini/settings.json#mcpServers. Preserves unrelated settings keys, drops mcpServers when empty, and maps remote transports using Gemini’s httpUrl/url field-keyed shape.

src/agents/adapters/gemini-cli/index.ts

index.tsRegister Gemini CLI adapter and add it to curated defaults +3/-2

Register Gemini CLI adapter and add it to curated defaults

• Adds gemini-cli to the closed ADAPTERS array and includes it in DEFAULT_AGENT_IDS so it’s selected by default during non-interactive init.

src/agents/adapters/index.ts

Tests (2) +107 / -3
adapters.test.tsAdd Gemini CLI adapter test suite for MCP round-trip and claims +103/-1

Add Gemini CLI adapter test suite for MCP round-trip and claims

• Adds tests for MCP render→scrape round-trips (stdio/http/sse), preservation of unrelated settings.json keys, deletion of mcpServers when empty, and claims including GEMINI.md and .gemini/settings.json.

test/agents/adapters.test.ts

init-steps.test.tsUpdate init-step assertions for expanded default agent set +4/-2

Update init-step assertions for expanded default agent set

• Updates init multiselect step tests to expect the curated default selection to include gemini-cli in both written config and dry-run logs.

test/core/init-steps.test.ts

Documentation (2) +3 / -3
README.mdDocument Gemini CLI as a built-in agent and its materialized files +2/-2

Document Gemini CLI as a built-in agent and its materialized files

• Updates the project description to include Gemini CLI alongside Claude Code and Codex. Adds GEMINI.md and .gemini/settings.json to the materialization examples and built-in agent list.

README.md

index.tsUpdate agents CLI help text to include gemini-cli +1/-1

Update agents CLI help text to include gemini-cli

• Extends the agents domain argument description to list gemini-cli as a valid agent id alongside claude-code and codex.

src/domains/agents/index.ts

@qodo-code-review

qodo-code-review Bot commented Jul 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 64 rules

Grey Divider


Action required

1. Deletes Gemini settings file ✓ Resolved 🐞 Bug ≡ Correctness
Description
gemini-cli.claims() marks .gemini/settings.json as owned, so cleanupAgent() will delete the
entire file when the Gemini agent is removed, including unrelated user settings that the adapter
explicitly preserves during writes. This can cause user data loss (e.g., theme or other Gemini
settings) on agent removal.
Code

src/agents/adapters/gemini-cli/index.ts[R45-51]

+  async claims(ctx) {
+    const config = await readConfigOrDefault(ctx.configPath);
+    const canonical = Object.keys(config.rules?.files ?? {});
+    return [
+      ...ruleMirrorPaths(canonical, GEMINI_RULES, ctx.projectRoot),
+      path.join(ctx.projectRoot, GEMINI_SETTINGS),
+    ];
Relevance

⭐⭐⭐ High

Cleanup deletes claimed paths; shared settings preservation matters. Same cleanup flow used in PR25.

PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The adapter claims .gemini/settings.json, and the agents domain cleanup deletes claimed paths.
Because the adapter preserves other keys in that file, deleting it on cleanup can remove
user-managed settings.

src/agents/adapters/gemini-cli/index.ts[45-52]
src/agents/adapters/gemini-cli/index.ts[55-96]
src/domains/agents/index.ts[97-110]
src/agents/adapters/shared.ts[145-152]
src/agents/adapters/claude-code/index.ts[56-63]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Gemini CLI adapter treats `.gemini/settings.json` as a shared file and only manages the `mcpServers` key, but it currently *claims* the entire settings file. On agent removal, cleanup deletes claimed paths, which can erase user-managed settings.

## Issue Context
- `cleanupAgent()` uses `removed.claims()` to compute owned paths and then deletes them via `removePaths()`.
- Gemini’s adapter preserves unrelated keys in `.gemini/settings.json`, which implies it should not be treated as fully owned.

## Fix Focus Areas
- Update claims to avoid unsafe deletion:
 - Option A (simplest): remove `.gemini/settings.json` from `claims()`.
 - Option B (safer ownership): only claim `.gemini/settings.json` if it is safe to delete (e.g., file exists and is a JSON object containing *only* `mcpServers` or is empty), otherwise do not claim.
- Ensure behavior stays consistent with other adapters that write into shared settings files (e.g., Claude’s `.claude/settings.json` is not claimed).

### Code references
- src/agents/adapters/gemini-cli/index.ts[45-51]
- src/domains/agents/index.ts[97-110]
- src/agents/adapters/shared.ts[145-152]
- src/agents/adapters/gemini-cli/index.ts[55-96]
- src/agents/adapters/claude-code/index.ts[56-63]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Empty remote URL written ✓ Resolved 🐞 Bug ≡ Correctness
Description
toGeminiServer() writes httpUrl/url as decl.command ?? "", so a config entry missing
command will be rendered with an empty-string remote URL in .gemini/settings.json. Since
agnos.json schema allows MCP entries without command and the agents domain passes them through
unchanged, this can generate invalid Gemini MCP config.
Code

src/agents/adapters/gemini-cli/index.ts[R140-153]

+function toGeminiServer(decl: ResolvedMcp): Record<string, unknown> {
+  if (decl.transport === "http") {
+    return {
+      httpUrl: decl.command ?? "",
+      ...(decl.headers ? { headers: decl.headers } : {}),
+      ...(decl.env ? { env: decl.env } : {}),
+    };
+  }
+  if (decl.transport === "sse") {
+    return {
+      url: decl.command ?? "",
+      ...(decl.headers ? { headers: decl.headers } : {}),
+      ...(decl.env ? { env: decl.env } : {}),
+    };
Relevance

⭐⭐⭐ High

Codex adapter only writes command when defined (no empty strings). Team likely rejects empty URL
output.

PR-#24

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The schema permits MCP declarations without command, and resolveSlices forwards them to
adapters; therefore toGeminiServer can receive missing command and will write empty strings into
Gemini’s URL fields.

src/core/schema.ts[38-47]
src/domains/agents/index.ts[49-57]
src/agents/adapters/gemini-cli/index.ts[140-154]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
For Gemini remote MCP servers, the adapter serializes missing `command` as an empty string (`""`) for `httpUrl`/`url`. Because MCP declarations allow `command` to be omitted, this can produce invalid `.gemini/settings.json` output.

## Issue Context
- `agnos.json` validation allows `mcp[].command` to be absent.
- `resolveSlices()` forwards `config.mcp` directly to adapters.
- Gemini adapter writes remote URL fields as `decl.command ?? ""`.

## Fix Focus Areas
- In `toGeminiServer()`:
 - For `http`/`sse`, only include `httpUrl`/`url` when `decl.command` is a non-empty string.
 - If missing, return an empty object is not ideal; instead skip the server in `writeGeminiMcp()` (filter) and log a warning with the server name.
- Consider aligning behavior with other adapters (e.g., omit keys instead of writing empty strings).

### Code references
- src/agents/adapters/gemini-cli/index.ts[140-154]
- src/core/schema.ts[38-47]
- src/domains/agents/index.ts[49-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. gemini-cli uses default export 📘 Rule violation ⚙ Maintainability
Description
src/agents/adapters/gemini-cli/index.ts adds export default geminiCli, which violates the rule
disallowing default exports outside designated plugin entry files. This increases API inconsistency
and can complicate tooling/linting expectations around import style.
Code

src/agents/adapters/gemini-cli/index.ts[R162-163]

+export { removePaths };
+export default geminiCli;
Relevance

⭐ Low

Existing adapters use default exports and are imported as defaults; gemini-cli follows established
pattern (PR25/24).

PR-#25
PR-#24

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 704840 forbids export default except for designated plugin entry files. The new
adapter file ends with export default geminiCli, introducing a prohibited default export.

Rule 704840: Disallow default exports except in designated plugin entry files
src/agents/adapters/gemini-cli/index.ts[162-163]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new adapter module exports `geminiCli` via `export default`, but default exports are disallowed except in designated plugin entry files.

## Issue Context
`src/agents/adapters/index.ts` and tests currently import this adapter via a default import, so switching to a named export will require updating those call sites.

## Fix Focus Areas
- src/agents/adapters/gemini-cli/index.ts[162-163]
- src/agents/adapters/index.ts[4-7]
- test/agents/adapters.test.ts[7-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/agents/adapters/gemini-cli/index.ts
Comment thread src/agents/adapters/gemini-cli/index.ts
rgdevme added 11 commits July 1, 2026 12:24
Adapt Google's Gemini CLI harness into the closed agent set. It mirrors
canonical rules to GEMINI.md and renders MCP servers into the shared
.gemini/settings.json under the mcpServers key, preserving other settings.
Remote transports map to Gemini's field-keyed shape: httpUrl for HTTP,
url for SSE. Gemini has no hook or skills mechanism, so those slices are
omitted. Registered in ADAPTERS and added to the curated default set.
Add a round-trip suite for the gemini-cli adapter: stdio and remote
(httpUrl/url) MCP render → scrape, preservation of unrelated settings
keys, mcpServers removal when empty, and claims. Update the agents
init-step assertions to the expanded curated default set.
Note GEMINI.md + .gemini/settings.json in the materialization example
and add Gemini CLI to the fixed built-in agent set.
Gemini CLI does expose lifecycle hooks and agent skills, so wire up both
slices. Hooks live under the `hooks` key of the shared .gemini/settings.json
and use Gemini's own event names, so translate the closed agnos vocabulary
across the boundary (PreToolUse↔BeforeTool, Stop↔AfterAgent, PreCompact↔
PreCompress, etc.), dropping events with no counterpart. Skills link
.gemini/skills to the canonical skills directory.

Also stop claiming .gemini/settings.json: it is a shared user file (theme
and other settings), so removing the agent must not delete it — key-level
teardown happens through render, matching the Claude Code adapter.
Add coverage for the hook event-vocabulary translation and unsupported-
event drop, the mcp+hooks shared-settings compose path, and update the
claims assertion to the skills dir (settings.json is no longer claimed).
Promote the canonical event set to the union of every event any agent
exposes (adds BeforeModel, AfterModel, BeforeToolSelection) and give each
adapter a declared `hookEvents` map from canonical names to its own native
names. The shared machinery (renderNativeHooks/scrapeNativeHooks) drives
render and scrape off that map, replacing the per-adapter drop-set (Codex)
and hand-rolled rename table (Gemini). Nothing is dropped from the central
registry, so scraping preserves every event even when only one agent
supports it; a per-agent render simply omits events that agent can't
express. schema.json tracks the widened enum.
`hooks add` now resolves the installed agents and, using the central
hookEvents maps, warns which of them do not support the event being added
(naming each), so the hook silently not reaching an agent is surfaced up
front rather than only at render time.
Rewrite the hooks-map suite around renderNativeHooks/scrapeNativeHooks with
identity and rename maps, and add hooks-add tests asserting the unsupported-
agent warning fires (naming the agent) and stays silent when all support it.
Re-checking the three agents' current hook docs showed the maps were far
from complete: Claude Code exposes 30 events (we mapped 9), Codex 10 (we
mapped 7, missing SubagentStart, PermissionRequest, PostCompact), and
Gemini's 11 were already covered. Widen the canonical registry to the full
union — Claude's 30 names plus Gemini's three model-loop events (BeforeModel,
AfterModel, BeforeToolSelection) — so nothing is dropped, and complete each
adapter's hookEvents map. Add an identityEventMap helper for the agents that
use the canonical names verbatim (Claude Code, Codex). Codex's docs list a
handler statusMessage, so render its hooks with messages preserved.
Add a coverage suite enumerating every event each agent documents and
asserting the adapter maps it (guarding against a future doc event going
unmapped), plus negative checks for events an agent genuinely lacks. Update
the codex round-trip to expect the now-preserved statusMessage.
A declaration missing `command` (allowed by the schema) rendered an empty
httpUrl/url/command into .gemini/settings.json, producing invalid config.
Skip such servers in writeGeminiMcp with a warning naming the server, and
omit each field in toGeminiServer unless present, so no empty value is ever
written.
@rgdevme rgdevme force-pushed the feat/gemini-cli-adapter branch from 61298e7 to d258ea6 Compare July 1, 2026 10:26
@rgdevme rgdevme merged commit 491e9e0 into main Jul 1, 2026
1 check passed
@rgdevme rgdevme deleted the feat/gemini-cli-adapter branch July 1, 2026 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant