Skip to content

feat(provider): add Gemini CLI as a headless observation provider#2764

Open
pg-adm1n wants to merge 6 commits into
thedotmack:mainfrom
pg-adm1n:feat/gemini-cli-provider
Open

feat(provider): add Gemini CLI as a headless observation provider#2764
pg-adm1n wants to merge 6 commits into
thedotmack:mainfrom
pg-adm1n:feat/gemini-cli-provider

Conversation

@pg-adm1n

@pg-adm1n pg-adm1n commented Jun 3, 2026

Copy link
Copy Markdown

Summary

Adds GeminiCliProvider — a new observation/summary generation backend that drives the user-installed gemini CLI (@google/gemini-cli), the same role ClaudeProvider plays for the Claude Agent SDK.

Unlike GeminiProvider (which calls the Gemini REST API and needs a GEMINI_API_KEY), this provider piggybacks on the gemini CLI's existing OAuth login, so users on the free/subscription tier need no API key. Selected via CLAUDE_MEM_PROVIDER=gemini-cli.

How it works

Each claude-mem session maps to one native gemini session:

  • First turn starts a new CLI session without a session flag: gemini --skip-trust --approval-mode plan --output-format json -m <model> -p "" (prompt piped on stdin).
  • Later turns resume it with the JSON session_id returned by the CLI: gemini --resume <uuid> ..., so gemini holds conversation context (and reuses its prompt cache) across separate subprocess invocations. The captured UUID is persisted as the session's memory_session_id.

Hardening:

  • --approval-mode plan → read-only; the model runs no tools and writes no files (pure text generation, no disk side effects).
  • --skip-trust → required for headless runs in untrusted dirs.
  • --output-format json → clean { session_id, response, stats } on stdout (warnings go to stderr and are ignored).
  • prompt via stdin + -p "" → flips the CLI into headless mode and sidesteps ARG_MAX on large observation payloads.
  • saved GEMINI_API_KEY from claude-mem's .env is forwarded to the subprocess when the worker env does not already provide one.

The init turn is a priming turn (observer role + output format only, no tool observation yet), so it returns an empty response by design — logged at debug, not error, matching ClaudeProvider's silent handling of the equivalent empty priming response.

Wiring

GeminiCliProvider.ts + find-gemini-executable.ts (new), plus the standard provider wiring points: worker-service.ts, SessionRoutes.ts, SettingsRoutes.ts, SettingsDefaultsManager.ts, worker-types.ts, install.ts, npx-cli/index.ts, and paths.ts (new GEMINI_CLI_SESSIONS_DIR). Rebuilt plugin/scripts/*.cjs bundles included.

New settings: CLAUDE_MEM_GEMINI_CLI_MODEL (default gemini-2.5-flash-lite), CLAUDE_MEM_GEMINI_CLI_PATH (empty = auto-detect), CLAUDE_MEM_GEMINI_CLI_TIMEOUT_MS.

Test plan

  • npm run typecheck — clean.
  • bun test tests/gemini-cli-provider.test.ts — covers fresh-session spawn args, saved API key forwarding, and CLI path cache invalidation.
  • bun test tests/worker/agents/ — 49 pass / 0 fail / 4 skip.
  • bun test — 2064 pass / 0 fail / 18 skip.
  • End-to-end: ran an isolated worker on a custom port (CLAUDE_MEM_DATA_DIR=… CLAUDE_MEM_WORKER_PORT=… CLAUDE_MEM_PROVIDER=gemini-cli) and drove 2 concurrent sessions over HTTP (/api/sessions/{init,observations,summarize}). Result: 4 observations + 2 structured summaries generated by gemini-2.5-flash-lite, with correct type classification (discovery / feature / bugfix), facts extraction, and files_modified tracking — zero error-level log lines.

Adds GeminiCliProvider, which drives the user-installed `gemini` CLI
(@google/gemini-cli) as an observation/summary generation backend — the
same role ClaudeProvider plays for the Claude Agent SDK. Unlike
GeminiProvider (Gemini REST API + API key), this provider piggybacks on
the gemini CLI's existing OAuth login, so users on the free/subscription
tier need no API key.

Each claude-mem session maps to one native gemini session: the first
turn creates it (--session-id <uuid>); later turns resume it
(--resume <uuid>), so gemini holds conversation context (and reuses its
prompt cache) across separate subprocess invocations.

Hardening: --approval-mode plan (read-only, no tool/file side effects),
--skip-trust (headless in untrusted dirs), --output-format json (clean
JSON on stdout), prompt piped via stdin + -p "" (sidesteps ARG_MAX on
large observation payloads).

Selected via CLAUDE_MEM_PROVIDER=gemini-cli. Wiring spans worker-service,
SessionRoutes, SettingsRoutes, SettingsDefaultsManager, worker-types,
install.ts, npx-cli, and paths.ts (new GEMINI_CLI_SESSIONS_DIR).

The init turn is a priming turn (observer role + output format, no tool
observation yet), so it returns an empty response by design — logged at
debug, not error, matching ClaudeProvider's silent handling of the
equivalent empty priming response.

Verified end-to-end: isolated worker on a custom port driven over HTTP,
2 concurrent sessions -> 4 observations + 2 structured summaries
generated by gemini-2.5-flash-lite, with correct type classification
and zero error-level log lines.
@pg-adm1n pg-adm1n force-pushed the feat/gemini-cli-provider branch from 1e8821c to 28e4c7a Compare June 3, 2026 14:24

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e8821c089

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +156 to +160
const args: string[] = [];
if (opts.sessionId) {
args.push('--session-id', opts.sessionId);
} else if (opts.resumeId) {
args.push('--resume', opts.resumeId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Drop unsupported Gemini --session-id flag

Every fresh Gemini CLI session enters this branch, but the current Gemini CLI reference lists --resume/-r for continuing sessions and does not define a --session-id startup option (see https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/cli-reference.md#cli-options). With this flag present, the first headless spawn for any new claude-mem session fails before a session_id can be captured; start without a session flag and use the JSON session_id returned by the CLI for subsequent --resume calls.

Useful? React with 👍 / 👎.

Comment on lines +170 to +173
const child = spawn(opts.executable, args, {
cwd: opts.cwd,
env: process.env,
stdio: ['pipe', 'pipe', 'pipe'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass saved Gemini API keys into the subprocess

When the user has no OAuth cache but did save GEMINI_API_KEY through claude-mem's .env, isGeminiCliAvailable() returns true via getCredential('GEMINI_API_KEY'), but this spawn only forwards the worker's current process.env. Worker daemons are started from sanitized shell envs and do not automatically load ~/.claude-mem/.env, so the Gemini CLI child will not see the API key required for headless auth and will fail despite the provider being reported available; merge the loaded credential into the child env before spawning.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces GeminiCliProvider, a new observation/summary backend that drives the user-installed gemini CLI over subprocess, piggy-backing on the CLI's OAuth login so no API key is required. It wires the provider through all the standard integration points — routes, settings, install flow, and the UI — and includes subprocess-level integration tests and a session-not-found re-priming fix.

  • GeminiCliProvider.ts: spawns the gemini CLI with --output-format json and --skip-trust --approval-mode plan, pipes prompts on stdin, captures the session_id for subsequent --resume turns, and correctly re-primes a fresh session when --resume finds none.
  • cleanXmlLists in parser.ts: converts <item>/<li>/<ul>/<ol> tags in XML field values to markdown bullets; the blank-line normalisation at the end applies to every extracted field regardless of whether list content was present, creating a subtle regression surface for all providers.
  • SettingsDefaultsManager.ts: the default for CLAUDE_MEM_GEMINI_CLI_MODEL is 'auto' in code but gemini-2.5-flash-lite in the PR description — if auto is not a valid -m argument for the gemini CLI, every default installation would fail silently.

Confidence Score: 4/5

Safe to merge with minor follow-up: the two new findings are non-blocking quality issues, while the more serious silent-fallback concern in SessionRoutes was flagged in a prior review cycle.

The core provider implementation — subprocess hardening, stdin error handling, abort/timeout, and the session-not-found re-priming fix — is solid and well-tested. The two new items are: blank-line collapsing in the XML parser that affects all providers (harmless in today's output format but an unintended behaviour change), and a discrepancy between the documented default model and the actual shipped default, which could cause silent failures for users who don't configure the model explicitly.

src/sdk/parser.ts (cleanXmlLists side-effect on all providers) and src/shared/SettingsDefaultsManager.ts (default model value).

Important Files Changed

Filename Overview
src/services/worker/GeminiCliProvider.ts New 553-line provider that drives the gemini CLI as a headless observation backend; recovery re-priming path, stdin error handling, and abort/timeout logic are correctly implemented.
src/shared/find-gemini-executable.ts Executable discovery with a two-level cache keyed on the configured path; hasGeminiExecutable() skips the cache-key check (already flagged in prior review).
src/sdk/parser.ts New cleanXmlLists helper converts <item>/<li> tags to markdown bullets; unconditionally strips blank lines from every extracted field, affecting all providers.
src/shared/SettingsDefaultsManager.ts Adds three new Gemini CLI settings; default model is 'auto', which disagrees with the PR description's stated default of gemini-2.5-flash-lite.
src/services/worker/http/routes/SessionRoutes.ts Removes the throwing getActiveAgent() guard; getSelectedProvider() now silently falls through to 'claude' when gemini-cli is selected but unavailable (previously flagged).
tests/gemini-cli-provider.test.ts Two integration tests covering fresh-session spawning and the session-not-found re-priming recovery path via fake shell scripts; solid subprocess-level coverage.

Sequence Diagram

sequenceDiagram
    participant R as SessionRoutes
    participant G as GeminiCliProvider
    participant F as findGeminiExecutable
    participant CLI as gemini CLI (subprocess)
    participant DB as SessionStore

    R->>R: getSelectedProvider()
    R->>G: startSession(session)
    G->>F: findGeminiExecutable()
    F-->>G: /path/to/gemini
    G->>CLI: spawn(gemini --skip-trust --approval-mode plan --output-format json -m model -p "")
    Note over G,CLI: init / priming turn (no --resume)
    CLI-->>G: "{session_id, response, stats}"
    G->>DB: ensureMemorySessionIdRegistered(sessionId)
    loop processMessageLoop
        G->>CLI: spawn(gemini --resume session_id ...)
        alt session still alive
            CLI-->>G: "{session_id, response}"
            G->>G: processAgentResponse()
        else session_not_found error
            G->>CLI: spawn fresh (re-prime)
            CLI-->>G: "{new_session_id}"
            G->>CLI: spawn(gemini --resume new_session_id ...)
            CLI-->>G: "{session_id, response}"
        end
    end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/sdk/parser.ts:214-219
**Blank-line normalisation applied to all fields, not just list fields**

`cleanXmlLists` unconditionally applies `.replace(/\n\s*\n/g, '\n').trim()` at the end of every non-code-block segment, so multi-paragraph field values from any provider — Claude, Gemini REST API, or gemini-cli — will have their blank-line separators silently collapsed. If, for example, a `learned` or `notes` field legitimately returns paragraphs separated by blank lines today, those blank lines are now stripped for every provider. Guard the normalisation so it only runs when list content was actually transformed.

```suggestion
      .join('\n');
    // Only collapse blank lines when list content was actually transformed,
    // to avoid corrupting intentional paragraph spacing in non-list fields.
    return hasListContent ? processed.replace(/\n\s*\n/g, '\n').trim() : processed;
  });
  
  return processedParts.join('');
```

### Issue 2 of 2
src/shared/SettingsDefaultsManager.ts:101
**Default model `'auto'` disagrees with PR description and may be invalid**

The PR description states the default is `gemini-2.5-flash-lite`, but the code ships `'auto'` — which is then passed literally as `-m auto` to the gemini CLI. The gemini CLI's `-m` flag expects a model identifier (e.g. `gemini-2.5-flash-lite`); if `auto` is not a recognised value, every installation that hasn't explicitly set `CLAUDE_MEM_GEMINI_CLI_MODEL` will fail on the very first turn with a non-zero exit code. The E2E test in the PR description ran with `gemini-2.5-flash-lite`, not `auto`, so this path may be untested. If the CLI does support `auto` as a sentinel, a brief comment explaining its semantics would prevent future confusion.

Reviews (6): Last reviewed commit: "fix(gemini): prevent prose pollution by ..." | Re-trigger Greptile

Comment thread src/services/worker/GeminiCliProvider.ts
Comment thread src/services/worker/GeminiCliProvider.ts Outdated
Comment thread src/services/worker/GeminiCliProvider.ts Outdated
Comment thread src/shared/find-gemini-executable.ts
@thedotmack

thedotmack commented Jun 3, 2026

Copy link
Copy Markdown
Owner

I WANT THIS!! WE NEED THIS! <3 Great Job!

@thedotmack

Copy link
Copy Markdown
Owner

Any chance you can try the same with Codex?

The viewer Settings modal hardcoded only claude/gemini/openrouter in the
AI Provider dropdown, so gemini-cli — already supported by the installer,
worker, backend validation, and settings defaults — could not be selected
from the UI.

- Add the "Gemini CLI" option to the AI Provider dropdown
- Add a gemini-cli config block: model, optional binary path, request
  timeout (no API key; uses the gemini CLI OAuth login)
- Add CLAUDE_MEM_GEMINI_CLI_MODEL/_PATH/_TIMEOUT_MS to the viewer Settings
  type so the new fields type-check
- Rebuild viewer-bundle.js
Comment thread src/services/worker/GeminiCliProvider.ts
When a gemini CLI session vanished mid-run, runTurn cleared the stale
memorySessionId and spawned a fresh session but re-sent the current
observation/summary prompt directly. A brand-new session never received
the init turn (observer role + structured output format), so the model
emitted free-form text that processAgentResponse silently discarded —
every recovery after a mid-run session expiry lost its observations.

runTurn now primes the fresh session with buildContinuationPrompt (the
same priming the normal init->turn flow uses), then resumes it with the
real prompt. Extracted runFreshTurn to share the spawn/capture/register
logic between the first-turn and recovery paths. Added a recovery-path
test asserting the failed-resume -> fresh re-prime -> resume sequence.

Also address two reviewer nits:
- remove dead getActiveAgent() in SessionRoutes: routing goes through
  getSelectedProvider / startGeneratorWithProvider; the method was
  private and uncalled, and carried a divergent error-on-unavailability
  contract that would surprise anyone who wired it back in.
- fix the misleading "leaving queue intact" empty-response log: the
  message was already consumed from the iterator, so nothing is retried.

Rebuilt plugin/scripts/worker-service.cjs.
@pg-adm1n

pg-adm1n commented Jun 4, 2026

Copy link
Copy Markdown
Author

@thedotmack I had build one with opencli+chatgpt web, but I got bot detention alert, and cloudflare come in.

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.

2 participants