{
  "markdown": "# sprintable-gemini\n\nSprintable channel extension for [Gemini CLI](https://github.com/google-gemini/gemini-cli) — real-time two-way chat between a Gemini agent and its Sprintable team, plus the hosted Sprintable MCP toolset.\n\n## Install\n\n```\ngemini extensions install https://github.com/moonklabs/sprintable-gemini\n```\n\nOne command: pulls the extension, prompts for your Sprintable agent API key (stored in your OS keychain, not a plain-text file — see [Settings](#settings)), and installs the bundled MCP server.\n\n## What it does\n\n- **`SessionStart`** hook boots a detached background listener that consumes the Sprintable SSE stream (`GET /api/v2/agent/stream`) for this agent's conversations.\n- **`AfterAgent`** hook is the channel's turn-end heart: it posts the agent's last response back to Sprintable, then drains any queued inbound messages by returning `{\"decision\":\"block\",\"reason\":\"<joined messages>\"}` — which Gemini CLI injects as a new user turn (confirmed against the actual `gemini-cli` source: `continueRequest = [{text: continueReason}]`, capped at `MAX_TURNS=100` per call).\n- **Idle wake**: if a message arrives while the CLI process isn't actively running a turn, the listener actively resumes it via `gemini -r <session_id> -p \"<message>\"` instead of waiting for the next organic turn.\n\n## Settings\n\nDeclared in `gemini-extension.json`'s `settings[]` — collected interactively on install, delivered to the hook subprocess's environment automatically by Gemini CLI itself (no manual `.env` wiring needed on your end):\n\n| Setting | Env var | Sensitive |\n|---|---|---|\n| Sprintable Agent API Key | `SPRINTABLE_API_KEY` | yes (OS keychain) |\n| Sprintable API URL | `SPRINTABLE_API_URL` | no (defaults to `https://app.sprintable.ai`) |\n\nReconfigure any time: `gemini extensions config sprintable`.\n\nThe bundled hosted-tools MCP server is registered under the key `sprintable-mcp`\nin `gemini-extension.json` (not bare `sprintable` — #2577: that name is reserved\nfor the sibling Claude Code plugin's separate bundled *channel* MCP,\n`sprintable-channel`, to avoid `/mcp`-listing confusion when both happen to be\nconfigured on the same machine). The extension's own install/uninstall name\nstays `sprintable`, unaffected.\n\n## Uninstall\n\n```\ngemini extensions uninstall sprintable\n```\n\nThis removes the extension registration, but **does not stop the background listener process** the `SessionStart` hook already spawned — Gemini CLI has no uninstall-time hook, so there's nothing to catch this at (confirmed live, QA finding). Kill it manually:\n\n```\npkill -f 'sprintable-gemini.*scripts/listener.py'\n```\n\n(Adjust the pattern if you installed to a non-default path — check `ps aux | grep listener.py` first if unsure.)\n\n## Design notes (ground-truthed against gemini-cli's own source, not public docs)\n\n- **Hook payload is snake_case** (`session_id`, `prompt_response`, `stop_hook_active`, ...) — matches Claude Code/codex's convention, not grok's camelCase. Confirmed by reading the bundled `gemini-cli` package directly.\n- **`gemini -r <session_id> -p \"<msg>\"` reliably re-fires `AfterAgent`** on the resumed turn. This is the opposite of how `codex exec resume` behaves (codex's resume never re-fires its `Stop` hook, confirmed live). Because of this, the idle-wake listener here does **not** post the reply itself — it only launches the resume and watches for process success/failure. Doing both (as an early draft of this plugin did, copying a pattern from the sibling `sprintable-codex` plugin where it was the correct fix) caused a real, reproduced double-post, since `AfterAgent`'s `prompt_response` and a would-be `-o json` capture aren't guaranteed byte-identical (streaming/buffering boundary), so a naive dedup-by-content-hash didn't catch it. One posting path only, no dedup shortcut needed.\n- **No subdirectory installs.** `gemini extensions install <github-url>` clones the whole repo and only ever looks for `gemini-extension.json` at the clone root (confirmed by reading the install/clone code path); the URL parser itself rejects anything but exactly `owner/repo` (throws on extra path segments). That's why this extension lives in its own repository instead of a subdirectory of `moonklabs/sprintable-agent-plugins` alongside the codex and grok plugins.\n- **No per-agent home directory override exists in Gemini CLI** (`~/.gemini` is hardcoded — no `GEMINI_HOME`-equivalent env var, unlike Codex's `CODEX_HOME` or Grok's `GROK_HOME`). If you're running more than one Sprintable-connected Gemini agent on the same machine, they will share the same OS keychain entry and extension settings by default. Use `SPRINTABLE_STATE_DIR` to at least isolate each agent's own queue/log state; there is currently no way to isolate the credential itself per-agent on a shared machine.\n\n## Development / testing note\n\nThis extension was built and live-tested on a machine where Gemini CLI had no other real users or live consumers at the time (confirmed test-only environment, not a shared production install) — the live install/uninstall/keychain tests in this PR's evidence touched the real, shared `~/.gemini` directory (Gemini CLI provides no isolated per-agent home directory), which was judged acceptable specifically because of that premise. If Gemini CLI becomes a live, in-use tool on that machine, that premise no longer holds for future testing there.\n",
  "bytes": 5377,
  "sha": "71ac9ef42495e0f2b398dff36e3786521fbd98d5ef38586955ab4f12911dbffb",
  "repo_slug": "moonklabs/sprintable-gemini",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_moonklabs_sprintable_gemini_6641d9b5/readme"
}