feat: add bd setup gemini for Gemini CLI integration (#845)

Add support for Gemini CLI hook-based integration, similar to Claude Code:
- bd setup gemini: Install SessionStart/PreCompress hooks
- bd setup gemini --check: Verify installation
- bd setup gemini --remove: Remove hooks
- bd setup gemini --project: Project-only installation
- bd setup gemini --stealth: Use bd prime --stealth

Also adds Gemini CLI integration check to bd doctor.

Gemini CLI's hook system is nearly identical to Claude Code's,
making this a clean, low-maintenance addition.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/wolf
2026-01-02 00:02:24 -08:00
committed by Steve Yegge
parent 6572654cdc
commit a06b40bd48
5 changed files with 470 additions and 12 deletions

View File

@@ -382,6 +382,11 @@ func runDiagnostics(path string) doctorResult {
result.Checks = append(result.Checks, claudeCheck)
// Don't fail overall check for missing Claude integration, just warn
// Check 11b: Gemini CLI integration
geminiCheck := convertWithCategory(doctor.CheckGemini(), doctor.CategoryIntegration)
result.Checks = append(result.Checks, geminiCheck)
// Don't fail overall check for missing Gemini integration, just info
// Check 11a: bd in PATH (needed for Claude hooks to work)
bdPathCheck := convertWithCategory(doctor.CheckBdInPath(), doctor.CategoryIntegration)
result.Checks = append(result.Checks, bdPathCheck)

90
cmd/bd/doctor/gemini.go Normal file
View File

@@ -0,0 +1,90 @@
package doctor
import (
"encoding/json"
"os"
"path/filepath"
)
// CheckGemini returns Gemini CLI integration verification as a DoctorCheck
func CheckGemini() DoctorCheck {
hasHooks := hasGeminiHooks()
if hasHooks {
return DoctorCheck{
Name: "Gemini CLI Integration",
Status: StatusOK,
Message: "Hooks installed",
Detail: "SessionStart and PreCompress hooks enabled",
}
}
return DoctorCheck{
Name: "Gemini CLI Integration",
Status: StatusOK, // Not a warning - Gemini is optional
Message: "Not configured",
Detail: "Run 'bd setup gemini' to enable Gemini CLI integration",
}
}
// hasGeminiHooks checks if Gemini CLI hooks are installed
func hasGeminiHooks() bool {
home, err := os.UserHomeDir()
if err != nil {
return false
}
globalSettings := filepath.Join(home, ".gemini", "settings.json")
projectSettings := ".gemini/settings.json"
return hasGeminiBeadsHooks(globalSettings) || hasGeminiBeadsHooks(projectSettings)
}
// hasGeminiBeadsHooks checks if a settings file has bd prime hooks for Gemini CLI
func hasGeminiBeadsHooks(settingsPath string) bool {
data, err := os.ReadFile(settingsPath) // #nosec G304 -- settingsPath is constructed from known safe locations
if err != nil {
return false
}
var settings map[string]interface{}
if err := json.Unmarshal(data, &settings); err != nil {
return false
}
hooks, ok := settings["hooks"].(map[string]interface{})
if !ok {
return false
}
// Check SessionStart and PreCompress for "bd prime"
for _, event := range []string{"SessionStart", "PreCompress"} {
eventHooks, ok := hooks[event].([]interface{})
if !ok {
continue
}
for _, hook := range eventHooks {
hookMap, ok := hook.(map[string]interface{})
if !ok {
continue
}
commands, ok := hookMap["hooks"].([]interface{})
if !ok {
continue
}
for _, cmd := range commands {
cmdMap, ok := cmd.(map[string]interface{})
if !ok {
continue
}
cmdStr := cmdMap["command"]
if cmdStr == "bd prime" || cmdStr == "bd prime --stealth" {
return true
}
}
}
}
return false
}

View File

@@ -117,6 +117,31 @@ agents from forgetting bd workflow after context compaction.`,
},
}
var setupGeminiCmd = &cobra.Command{
Use: "gemini",
Short: "Setup Gemini CLI integration",
Long: `Install Gemini CLI hooks that auto-inject bd workflow context.
By default, installs hooks globally (~/.gemini/settings.json).
Use --project flag to install only for this project.
Hooks call 'bd prime' on SessionStart and PreCompress events to prevent
agents from forgetting bd workflow after context compaction.`,
Run: func(cmd *cobra.Command, args []string) {
if setupCheck {
setup.CheckGemini()
return
}
if setupRemove {
setup.RemoveGemini(setupProject)
return
}
setup.InstallGemini(setupProject, setupStealth)
},
}
func init() {
setupFactoryCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Factory.ai integration is installed")
setupFactoryCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd section from AGENTS.md")
@@ -132,9 +157,15 @@ func init() {
setupAiderCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Aider integration is installed")
setupAiderCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd config from Aider")
setupGeminiCmd.Flags().BoolVar(&setupProject, "project", false, "Install for this project only (not globally)")
setupGeminiCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Gemini CLI integration is installed")
setupGeminiCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd hooks from Gemini CLI settings")
setupGeminiCmd.Flags().BoolVar(&setupStealth, "stealth", false, "Use 'bd prime --stealth' (flush only, no git operations)")
setupCmd.AddCommand(setupFactoryCmd)
setupCmd.AddCommand(setupClaudeCmd)
setupCmd.AddCommand(setupCursorCmd)
setupCmd.AddCommand(setupAiderCmd)
setupCmd.AddCommand(setupGeminiCmd)
rootCmd.AddCommand(setupCmd)
}

270
cmd/bd/setup/gemini.go Normal file
View File

@@ -0,0 +1,270 @@
package setup
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
var (
geminiEnvProvider = defaultGeminiEnv
errGeminiHooksMissing = errors.New("gemini hooks not installed")
)
type geminiEnv struct {
stdout io.Writer
stderr io.Writer
homeDir string
projectDir string
ensureDir func(string, os.FileMode) error
readFile func(string) ([]byte, error)
writeFile func(string, []byte) error
}
func defaultGeminiEnv() (geminiEnv, error) {
home, err := os.UserHomeDir()
if err != nil {
return geminiEnv{}, fmt.Errorf("home directory: %w", err)
}
workDir, err := os.Getwd()
if err != nil {
return geminiEnv{}, fmt.Errorf("working directory: %w", err)
}
return geminiEnv{
stdout: os.Stdout,
stderr: os.Stderr,
homeDir: home,
projectDir: workDir,
ensureDir: EnsureDir,
readFile: os.ReadFile,
writeFile: func(path string, data []byte) error {
return atomicWriteFile(path, data)
},
}, nil
}
func geminiProjectSettingsPath(base string) string {
return filepath.Join(base, ".gemini", "settings.json")
}
func geminiGlobalSettingsPath(home string) string {
return filepath.Join(home, ".gemini", "settings.json")
}
// InstallGemini installs Gemini CLI hooks
func InstallGemini(project bool, stealth bool) {
env, err := geminiEnvProvider()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
setupExit(1)
return
}
if err := installGemini(env, project, stealth); err != nil {
setupExit(1)
}
}
func installGemini(env geminiEnv, project bool, stealth bool) error {
var settingsPath string
if project {
settingsPath = geminiProjectSettingsPath(env.projectDir)
_, _ = fmt.Fprintln(env.stdout, "Installing Gemini CLI hooks for this project...")
} else {
settingsPath = geminiGlobalSettingsPath(env.homeDir)
_, _ = fmt.Fprintln(env.stdout, "Installing Gemini CLI hooks globally...")
}
if err := env.ensureDir(filepath.Dir(settingsPath), 0o755); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: %v\n", err)
return err
}
settings := make(map[string]interface{})
if data, err := env.readFile(settingsPath); err == nil {
if err := json.Unmarshal(data, &settings); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to parse settings.json: %v\n", err)
return err
}
}
hooks, ok := settings["hooks"].(map[string]interface{})
if !ok {
hooks = make(map[string]interface{})
settings["hooks"] = hooks
}
command := "bd prime"
if stealth {
command = "bd prime --stealth"
}
// Gemini CLI uses "PreCompress" instead of Claude's "PreCompact"
if addHookCommand(hooks, "SessionStart", command) {
_, _ = fmt.Fprintln(env.stdout, "✓ Registered SessionStart hook")
}
if addHookCommand(hooks, "PreCompress", command) {
_, _ = fmt.Fprintln(env.stdout, "✓ Registered PreCompress hook")
}
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: marshal settings: %v\n", err)
return err
}
if err := env.writeFile(settingsPath, data); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write settings: %v\n", err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "\n✓ Gemini CLI integration installed")
_, _ = fmt.Fprintf(env.stdout, " Settings: %s\n", settingsPath)
_, _ = fmt.Fprintln(env.stdout, "\nRestart Gemini CLI for changes to take effect.")
return nil
}
// CheckGemini checks if Gemini integration is installed
func CheckGemini() {
env, err := geminiEnvProvider()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
setupExit(1)
return
}
if err := checkGemini(env); err != nil {
setupExit(1)
}
}
func checkGemini(env geminiEnv) error {
globalSettings := geminiGlobalSettingsPath(env.homeDir)
projectSettings := geminiProjectSettingsPath(env.projectDir)
switch {
case hasGeminiBeadsHooks(globalSettings):
_, _ = fmt.Fprintf(env.stdout, "✓ Global hooks installed: %s\n", globalSettings)
return nil
case hasGeminiBeadsHooks(projectSettings):
_, _ = fmt.Fprintf(env.stdout, "✓ Project hooks installed: %s\n", projectSettings)
return nil
default:
_, _ = fmt.Fprintln(env.stdout, "✗ No hooks installed")
_, _ = fmt.Fprintln(env.stdout, " Run: bd setup gemini")
return errGeminiHooksMissing
}
}
// RemoveGemini removes Gemini CLI hooks
func RemoveGemini(project bool) {
env, err := geminiEnvProvider()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
setupExit(1)
return
}
if err := removeGemini(env, project); err != nil {
setupExit(1)
}
}
func removeGemini(env geminiEnv, project bool) error {
var settingsPath string
if project {
settingsPath = geminiProjectSettingsPath(env.projectDir)
_, _ = fmt.Fprintln(env.stdout, "Removing Gemini CLI hooks from project...")
} else {
settingsPath = geminiGlobalSettingsPath(env.homeDir)
_, _ = fmt.Fprintln(env.stdout, "Removing Gemini CLI hooks globally...")
}
data, err := env.readFile(settingsPath)
if err != nil {
_, _ = fmt.Fprintln(env.stdout, "No settings file found")
return nil
}
var settings map[string]interface{}
if err := json.Unmarshal(data, &settings); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to parse settings.json: %v\n", err)
return err
}
hooks, ok := settings["hooks"].(map[string]interface{})
if !ok {
_, _ = fmt.Fprintln(env.stdout, "No hooks found")
return nil
}
// Remove both variants from both events
removeHookCommand(hooks, "SessionStart", "bd prime")
removeHookCommand(hooks, "PreCompress", "bd prime")
removeHookCommand(hooks, "SessionStart", "bd prime --stealth")
removeHookCommand(hooks, "PreCompress", "bd prime --stealth")
data, err = json.MarshalIndent(settings, "", " ")
if err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: marshal settings: %v\n", err)
return err
}
if err := env.writeFile(settingsPath, data); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write settings: %v\n", err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "✓ Gemini CLI hooks removed")
return nil
}
// hasGeminiBeadsHooks checks if a settings file has bd prime hooks for Gemini CLI
func hasGeminiBeadsHooks(settingsPath string) bool {
data, err := os.ReadFile(settingsPath) // #nosec G304 -- settingsPath is constructed from known safe locations (user home/.gemini), not user input
if err != nil {
return false
}
var settings map[string]interface{}
if err := json.Unmarshal(data, &settings); err != nil {
return false
}
hooks, ok := settings["hooks"].(map[string]interface{})
if !ok {
return false
}
// Check SessionStart and PreCompress for "bd prime"
for _, event := range []string{"SessionStart", "PreCompress"} {
eventHooks, ok := hooks[event].([]interface{})
if !ok {
continue
}
for _, hook := range eventHooks {
hookMap, ok := hook.(map[string]interface{})
if !ok {
continue
}
commands, ok := hookMap["hooks"].([]interface{})
if !ok {
continue
}
for _, cmd := range commands {
cmdMap, ok := cmd.(map[string]interface{})
if !ok {
continue
}
// Check for either variant
cmdStr := cmdMap["command"]
if cmdStr == "bd prime" || cmdStr == "bd prime --stealth" {
return true
}
}
}
}
return false
}

View File

@@ -5,12 +5,13 @@
## Overview
The `bd setup` command configures beads integration with AI coding tools. It supports four integrations:
The `bd setup` command configures beads integration with AI coding tools. It supports five integrations:
| Tool | Command | Integration Type |
|------|---------|-----------------|
| [Factory.ai (Droid)](#factoryai-droid) | `bd setup factory` | AGENTS.md file (universal standard) |
| [Claude Code](#claude-code) | `bd setup claude` | SessionStart/PreCompact hooks |
| [Gemini CLI](#gemini-cli) | `bd setup gemini` | SessionStart/PreCompress hooks |
| [Cursor IDE](#cursor-ide) | `bd setup cursor` | Rules file (.cursor/rules/beads.mdc) |
| [Aider](#aider) | `bd setup aider` | Config file (.aider.conf.yml) |
@@ -20,12 +21,14 @@ The `bd setup` command configures beads integration with AI coding tools. It sup
# Install integration for your tool
bd setup factory # For Factory.ai Droid (and other AGENTS.md-compatible tools)
bd setup claude # For Claude Code
bd setup gemini # For Gemini CLI
bd setup cursor # For Cursor IDE
bd setup aider # For Aider
# Verify installation
bd setup factory --check
bd setup claude --check
bd setup gemini --check
bd setup cursor --check
bd setup aider --check
```
@@ -177,6 +180,64 @@ The hooks call `bd prime` which:
This is more context-efficient than MCP tools (~1-2k tokens vs 10-50k for MCP schemas).
## Gemini CLI
Gemini CLI integration uses hooks to automatically inject beads workflow context at session start and before context compression.
### Installation
```bash
# Global installation (recommended)
bd setup gemini
# Project-only installation
bd setup gemini --project
# With stealth mode (flush only, no git operations)
bd setup gemini --stealth
```
### What Gets Installed
**Global installation** (`~/.gemini/settings.json`):
- `SessionStart` hook: Runs `bd prime` when a new session starts
- `PreCompress` hook: Runs `bd prime` before context compression
**Project installation** (`.gemini/settings.json`):
- Same hooks, but only active for this project
### Flags
| Flag | Description |
|------|-------------|
| `--check` | Check if integration is installed |
| `--remove` | Remove beads hooks |
| `--project` | Install for this project only (not globally) |
| `--stealth` | Use `bd prime --stealth` (flush only, no git operations) |
### Examples
```bash
# Check if hooks are installed
bd setup gemini --check
# Output: ✓ Global hooks installed: /Users/you/.gemini/settings.json
# Remove hooks
bd setup gemini --remove
# Install project-specific hooks with stealth mode
bd setup gemini --project --stealth
```
### How It Works
The hooks call `bd prime` which:
1. Outputs workflow context for Gemini to read
2. Syncs any pending changes
3. Ensures Gemini always knows how to use beads
This works identically to Claude Code integration, using Gemini CLI's hook system (SessionStart and PreCompress events).
## Cursor IDE
Cursor integration creates a rules file that provides beads workflow context to the AI.
@@ -277,14 +338,14 @@ This respects Aider's philosophy of keeping humans in control while still levera
## Comparison
| Feature | Factory.ai | Claude Code | Cursor | Aider |
|---------|-----------|-------------|--------|-------|
| Command execution | Automatic | Automatic | Automatic | Manual (/run) |
| Context injection | AGENTS.md | Hooks | Rules file | Config file |
| Global install | No (per-project) | Yes | No (per-project) | No (per-project) |
| Stealth mode | N/A | Yes | N/A | N/A |
| Standard format | Yes (AGENTS.md) | No (proprietary) | No (proprietary) | No (proprietary) |
| Multi-tool compatible | Yes | No | No | No |
| Feature | Factory.ai | Claude Code | Gemini CLI | Cursor | Aider |
|---------|-----------|-------------|------------|--------|-------|
| Command execution | Automatic | Automatic | Automatic | Automatic | Manual (/run) |
| Context injection | AGENTS.md | Hooks | Hooks | Rules file | Config file |
| Global install | No (per-project) | Yes | Yes | No (per-project) | No (per-project) |
| Stealth mode | N/A | Yes | Yes | N/A | N/A |
| Standard format | Yes (AGENTS.md) | No (proprietary) | No (proprietary) | No (proprietary) | No (proprietary) |
| Multi-tool compatible | Yes | No | No | No | No |
## Best Practices
@@ -295,16 +356,17 @@ This respects Aider's philosophy of keeping humans in control while still levera
2. **Add tool-specific integrations as needed** - Claude hooks, Cursor rules, or Aider config for tool-specific features
3. **Install globally for Claude Code** - You'll get beads context in every project automatically
3. **Install globally for Claude Code or Gemini CLI** - You'll get beads context in every project automatically
4. **Use stealth mode in CI/CD** - `bd setup claude --stealth` avoids git operations that might fail in automated environments
4. **Use stealth mode in CI/CD** - `bd setup claude --stealth` or `bd setup gemini --stealth` avoids git operations that might fail in automated environments
5. **Commit AGENTS.md to git** - This ensures all team members and AI tools get the same instructions
6. **Run `bd doctor` after setup** - Verifies the integration is working:
```bash
bd doctor | grep -i claude
bd doctor | grep -iE "claude|gemini"
# Claude Integration: Hooks installed (CLI mode)
# Gemini CLI Integration: Hooks installed
```
## Troubleshooting