├── test
├── mock-tool-calls
│ ├── simple-read.json
│ ├── multi-file-glob.json
│ ├── grep-search.json
│ └── task-search.json
├── manual-test.sh
└── test-runner.sh
├── LICENSE
├── hooks
├── config
│ └── debug.conf
├── lib
│ ├── path-converter.sh
│ ├── json-parser.sh
│ ├── debug-helpers.sh
│ └── gemini-wrapper.sh
└── gemini-bridge.sh
├── .gitignore
├── CLAUDE.md
├── uninstall.sh
├── docs
└── TROUBLESHOOTING.md
├── CONTRIBUTING.md
├── install.sh
└── README.md
/test/mock-tool-calls/simple-read.json:
--------------------------------------------------------------------------------
1 | {
2 | "tool_name": "Read",
3 | "tool_input": {
4 | "file_path": "@src/main.py"
5 | },
6 | "session_id": "test-session",
7 | "transcript_path": "/tmp/test-transcript.jsonl"
8 | }
--------------------------------------------------------------------------------
/test/mock-tool-calls/multi-file-glob.json:
--------------------------------------------------------------------------------
1 | {
2 | "tool_name": "Glob",
3 | "tool_input": {
4 | "pattern": "**/*.php",
5 | "path": "/Users/tim/Code/smartphones"
6 | },
7 | "session_id": "test-session",
8 | "transcript_path": "/tmp/test-transcript.jsonl"
9 | }
--------------------------------------------------------------------------------
/test/mock-tool-calls/grep-search.json:
--------------------------------------------------------------------------------
1 | {
2 | "tool_name": "Grep",
3 | "tool_input": {
4 | "pattern": "function.*config",
5 | "path": "/Users/tim/Code/smartphones/src/",
6 | "include": "*.php"
7 | },
8 | "session_id": "test-session",
9 | "transcript_path": "/tmp/test-transcript.jsonl"
10 | }
--------------------------------------------------------------------------------
/test/mock-tool-calls/task-search.json:
--------------------------------------------------------------------------------
1 | {
2 | "tool_name": "Task",
3 | "tool_input": {
4 | "prompt": "Search for all occurrences of 'config' in @./ and analyze the configuration patterns",
5 | "description": "Config pattern analysis"
6 | },
7 | "session_id": "test-session",
8 | "transcript_path": "/tmp/test-transcript.jsonl"
9 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Claude-Gemini Bridge Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/hooks/config/debug.conf:
--------------------------------------------------------------------------------
1 | # Debug configuration for Claude-Gemini Bridge
2 | # Debug level: 0=off, 1=basic, 2=verbose, 3=trace
3 | DEBUG_LEVEL=2
4 |
5 | # Save all hook inputs for later analysis
6 | CAPTURE_INPUTS=true
7 | # CAPTURE_DIR will be set dynamically based on script location
8 |
9 | # Enable performance measurements
10 | MEASURE_PERFORMANCE=true
11 |
12 | # Dry-run mode (doesn't actually execute Gemini)
13 | DRY_RUN=false
14 |
15 | # Gemini-specific configuration
16 | GEMINI_CACHE_TTL=3600 # 1 hour cache
17 | GEMINI_TIMEOUT=30 # 30 seconds timeout
18 | GEMINI_RATE_LIMIT=1 # 1 second between calls
19 | GEMINI_MAX_FILES=20 # Max 20 files per call
20 |
21 | # Decision criteria for Gemini delegation
22 | MIN_FILES_FOR_GEMINI=3 # At least 3 files for Task operations
23 | MIN_FILE_SIZE_FOR_GEMINI=20480 # Minimum total size 20KB for delegation
24 | MAX_TOTAL_SIZE_FOR_GEMINI=10485760 # Max 10MB total size (10MB in bytes)
25 | CLAUDE_TOKEN_LIMIT=50000 # Token limit for Claude delegation (50000 == ~200KB)
26 | GEMINI_TOKEN_LIMIT=800000 # Max tokens Gemini can handle
27 |
28 | # Excluded file patterns (never sent to Gemini)
29 | GEMINI_EXCLUDE_PATTERNS="*.secret|*.key|*.env|*.password|*.token|*.pem|*.p12"
30 |
31 | # Automatic cache cleanup
32 | AUTO_CLEANUP_CACHE=true
33 | CACHE_MAX_AGE_HOURS=24
34 |
35 | # Log rotation
36 | AUTO_CLEANUP_LOGS=true
37 | LOG_MAX_AGE_DAYS=7
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Claude-Gemini Bridge .gitignore
2 |
3 | # Runtime files and directories
4 | cache/
5 | logs/
6 | debug/captured/
7 |
8 | # Temporary files
9 | *.tmp
10 | *.temp
11 | *.swp
12 | *.swo
13 | *~
14 |
15 | # OS generated files
16 | .DS_Store
17 | .DS_Store?
18 | ._*
19 | .Spotlight-V100
20 | .Trashes
21 | ehthumbs.db
22 | Thumbs.db
23 |
24 | # IDE and editor files
25 | .vscode/
26 | .idea/
27 | *.sublime-*
28 | .vim/
29 | .nvim/
30 |
31 | # Shell history and runtime
32 | .bash_history
33 | .zsh_history
34 | .history
35 |
36 | # Configuration files that might contain secrets
37 | *.local.conf
38 | *.secret
39 | *.key
40 | *.env
41 | config/*.local.*
42 |
43 | # Installation artifacts
44 | *.backup.*
45 | install.log
46 | *.tar.gz
47 |
48 | # Test artifacts
49 | test/temp/
50 | test/mock-data/temp/
51 | /tmp/claude_bridge_*
52 |
53 | # Claude Code specific
54 | .claude/
55 | claude_cache/
56 |
57 | # Gemini CLI artifacts
58 | .gemini/
59 |
60 | # Node.js (if any JS tools are added later)
61 | node_modules/
62 | npm-debug.log*
63 | yarn-debug.log*
64 | yarn-error.log*
65 |
66 | # Python (if any Python tools are added later)
67 | __pycache__/
68 | *.py[cod]
69 | *$py.class
70 | *.so
71 | .Python
72 | env/
73 | venv/
74 |
75 | # Coverage reports
76 | *.lcov
77 | coverage/
78 |
79 | # Logs
80 | *.log
81 | logs/
82 | *.log.*
83 |
84 | # Runtime data
85 | pids/
86 | *.pid
87 | *.seed
88 | *.pid.lock
89 |
90 | # Directory for instrumented libs generated by jscoverage/JSCover
91 | lib-cov/
92 |
93 | # Coverage directory used by tools like istanbul
94 | coverage/
95 |
96 | # Dependency directories
97 | vendor/
98 |
99 | # Optional npm cache directory
100 | .npm
101 |
102 | # Optional REPL history
103 | .node_repl_history
104 |
105 | # Output of 'npm pack'
106 | *.tgz
107 |
108 | # Yarn Integrity file
109 | .yarn-integrity
110 |
111 | # dotenv environment variables file
112 | .env
113 | .env.local
114 | .env.development.local
115 | .env.test.local
116 | .env.production.local
117 |
118 | # Backup files
119 | *.bak
120 | *.backup
121 | *~
122 |
123 | # Lock files
124 | package-lock.json
125 | yarn.lock
126 |
127 | # JetBrains IDEs
128 | .idea/
129 | *.iml
130 | *.iws
131 | *.ipr
132 |
133 | # Visual Studio Code
134 | .vscode/
135 | *.code-workspace
136 |
137 | # Sublime Text
138 | *.sublime-project
139 | *.sublime-workspace
140 |
141 | # Vim
142 | [._]*.s[a-v][a-z]
143 | [._]*.sw[a-p]
144 | [._]s[a-rt-v][a-z]
145 | [._]ss[a-gi-z]
146 | [._]sw[a-p]
147 |
148 | # Emacs
149 | *~
150 | \#*\#
151 | /.emacs.desktop
152 | /.emacs.desktop.lock
153 | *.elc
154 | auto-save-list
155 | tramp
156 | .\#*
157 |
158 | # Local configuration
159 | local.conf
160 | debug.local.conf
161 | *.local
162 |
163 | # Performance measurement files
164 | *.perf
165 | *.prof
166 |
167 | # Test coverage
168 | .nyc_output/
169 | coverage.lcov
170 |
171 | # Documentation build artifacts
172 | docs/_build/
173 | docs/build/
174 |
175 | # macOS
176 | .AppleDouble
177 | .LSOverride
178 | Icon
179 | .DocumentRevisions-V100
180 | .fseventsd
181 | .TemporaryItems
182 | .VolumeIcon.icns
183 | .com.apple.timemachine.donotpresent
184 | .AppleDB
185 | .AppleDesktop
186 | Network Trash Folder
187 | Temporary Items
188 | .apdisk
189 |
190 | # Windows
191 | Thumbs.db
192 | ehthumbs.db
193 | Desktop.ini
194 | $RECYCLE.BIN/
195 | *.cab
196 | *.msi
197 | *.msm
198 | *.msp
199 | *.lnk
200 |
201 | # Linux
202 | *~
203 | .fuse_hidden*
204 | .directory
205 | .Trash-*
206 | .nfs*
--------------------------------------------------------------------------------
/hooks/lib/path-converter.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ABOUTME: Converts Claude's @ notation to absolute paths for Gemini integration
3 |
4 | # Main function for path conversion with security validation
5 | convert_claude_paths() {
6 | local input="$1"
7 | local base_dir="$2"
8 |
9 | # Input validation
10 | if [ -z "$input" ]; then
11 | echo ""
12 | return 0
13 | fi
14 |
15 | if [ -z "$base_dir" ]; then
16 | base_dir="$(pwd)"
17 | fi
18 |
19 | # Security: Block path traversal attempts
20 | if [[ "$input" =~ \.\./|\.\.\\ ]]; then
21 | echo "" # Return empty on path traversal attempt
22 | return 1
23 | fi
24 |
25 | # Security: Validate base_dir is safe
26 | case "$base_dir" in
27 | /etc/*|/usr/*|/bin/*|/sbin/*|/root/*|/home/*/\.ssh/*)
28 | echo "" # Block access to system directories
29 | return 1
30 | ;;
31 | esac
32 |
33 | # Remove trailing slash from base_dir
34 | base_dir="${base_dir%/}"
35 |
36 | # Perform conversions
37 | # @./ -> empty string (current directory)
38 | input="${input//@\.\//}"
39 |
40 | # @/ -> project root (base_dir)
41 | input="${input//@\//$base_dir/}"
42 |
43 | # @filename or @folder/ -> absolute path
44 | # Use sed for @ followed by file/folder name
45 | input=$(echo "$input" | sed -E "s|@([^/[:space:]]+)|$base_dir/\\1|g")
46 |
47 | # Clean up double slashes
48 | input=$(echo "$input" | sed 's|//|/|g')
49 |
50 | echo "$input"
51 | }
52 |
53 | # Extracts file paths from text with security validation
54 | extract_files_from_text() {
55 | local text="$1"
56 |
57 | # Security: Block path traversal attempts
58 | if [[ "$text" =~ \.\./|\.\.\\ ]]; then
59 | echo "" # Return empty on path traversal attempt
60 | return 1
61 | fi
62 |
63 | # Security: Block absolute paths outside working directory
64 | if [[ "$text" =~ /etc/|/usr/|/bin/|/sbin/|/root/|/home/ ]]; then
65 | echo "" # Return empty on system path access
66 | return 1
67 | fi
68 |
69 | # Finds all file paths with extensions
70 | echo "$text" | grep -oE '(/[^[:space:]]+|[^[:space:]]+/[^[:space:]]+)\.[a-zA-Z0-9]+' | sort -u
71 | }
72 |
73 | # Advanced path conversion for complex patterns
74 | convert_advanced_paths() {
75 | local input="$1"
76 | local base_dir="$2"
77 |
78 | # Basic conversion
79 | local result=$(convert_claude_paths "$input" "$base_dir")
80 |
81 | # Glob pattern support
82 | # **/*.py -> all Python files recursively
83 | if [[ "$result" == *"**/"* ]]; then
84 | result=$(echo "$result" | sed "s|\\*\\*/|**/|g")
85 | fi
86 |
87 | echo "$result"
88 | }
89 |
90 | # Test function for path conversion
91 | test_path_conversion() {
92 | local wd="/Users/tim/Code/project"
93 | local failed=0
94 |
95 | echo "Testing path conversion..."
96 |
97 | # Test 1: @./ -> empty string
98 | local result1=$(convert_claude_paths '@./' "$wd")
99 | if [ "$result1" != "" ]; then
100 | echo "❌ Test 1 failed: '@./' -> '$result1' (expected: '')"
101 | failed=1
102 | else
103 | echo "✅ Test 1 passed: '@./' -> ''"
104 | fi
105 |
106 | # Test 2: @src/main.py -> absolute path
107 | local result2=$(convert_claude_paths '@src/main.py' "$wd")
108 | local expected2="$wd/src/main.py"
109 | if [ "$result2" != "$expected2" ]; then
110 | echo "❌ Test 2 failed: '@src/main.py' -> '$result2' (expected: '$expected2')"
111 | failed=1
112 | else
113 | echo "✅ Test 2 passed: '@src/main.py' -> '$expected2'"
114 | fi
115 |
116 | # Test 3: Multiple @ paths
117 | local input3="Check @README.md and @src/*.py files"
118 | local result3=$(convert_claude_paths "$input3" "$wd")
119 | local expected3="Check $wd/README.md and $wd/src/*.py files"
120 | if [ "$result3" != "$expected3" ]; then
121 | echo "❌ Test 3 failed: '$input3' -> '$result3' (expected: '$expected3')"
122 | failed=1
123 | else
124 | echo "✅ Test 3 passed: Multiple @ paths"
125 | fi
126 |
127 | # Test 4: @/ -> base_dir
128 | local result4=$(convert_claude_paths '@/' "$wd")
129 | local expected4="$wd/"
130 | if [ "$result4" != "$expected4" ]; then
131 | echo "❌ Test 4 failed: '@/' -> '$result4' (expected: '$expected4')"
132 | failed=1
133 | else
134 | echo "✅ Test 4 passed: '@/' -> '$expected4'"
135 | fi
136 |
137 | if [ $failed -eq 0 ]; then
138 | echo "🎉 All path conversion tests passed!"
139 | return 0
140 | else
141 | echo "💥 Some tests failed!"
142 | return 1
143 | fi
144 | }
145 |
146 | # If the script is called directly, run tests
147 | if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
148 | test_path_conversion
149 | fi
--------------------------------------------------------------------------------
/test/manual-test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ABOUTME: Interactive test tool for Claude-Gemini Bridge
3 |
4 | echo "🧪 Claude-Gemini Bridge Test Tool"
5 | echo "=================================="
6 | echo ""
7 |
8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9 |
10 | # Use current directory structure (git repo = installation)
11 | BRIDGE_DIR="$SCRIPT_DIR/.."
12 | echo "🔧 Running from: $BRIDGE_DIR"
13 |
14 | BRIDGE_SCRIPT="$BRIDGE_DIR/hooks/gemini-bridge.sh"
15 |
16 | # Check if bridge script exists
17 | if [ ! -f "$BRIDGE_SCRIPT" ]; then
18 | echo "❌ Bridge script not found: $BRIDGE_SCRIPT"
19 | exit 1
20 | fi
21 |
22 | # Test scenarios
23 | run_test() {
24 | local test_name="$1"
25 | local json_file="$2"
26 |
27 | echo "🔍 Running test: $test_name"
28 | echo "JSON file: $json_file"
29 | echo ""
30 |
31 | if [ ! -f "$json_file" ]; then
32 | echo "❌ Test file not found: $json_file"
33 | return 1
34 | fi
35 |
36 | echo "📋 Input JSON:"
37 | cat "$json_file" | jq '.' 2>/dev/null || cat "$json_file"
38 | echo ""
39 |
40 | echo "⚡ Bridge Response:"
41 | cat "$json_file" | "$BRIDGE_SCRIPT" | jq '.' 2>/dev/null || cat "$json_file" | "$BRIDGE_SCRIPT"
42 | echo ""
43 |
44 | echo "📊 Check logs for details:"
45 | echo " Debug: tail -f $BRIDGE_DIR/logs/debug/$(date +%Y%m%d).log"
46 | echo " Errors: tail -f $BRIDGE_DIR/logs/debug/errors.log"
47 | echo ""
48 | }
49 |
50 | # Interactive menu
51 | while true; do
52 | echo "Choose a test:"
53 | echo "1) Simple Read (@src/main.py)"
54 | echo "2) Task with Search (config analysis)"
55 | echo "3) Multi-File Glob (@**/*.php)"
56 | echo "4) Grep Search (function.*config)"
57 | echo "5) Custom JSON Input"
58 | echo "6) Replay Captured Call"
59 | echo "7) Test All Library Functions"
60 | echo "8) View Recent Logs"
61 | echo "9) Clear Cache and Logs"
62 | echo "0) Exit"
63 | echo ""
64 | read -p "Selection (0-9): " choice
65 |
66 | case $choice in
67 | 1)
68 | run_test "Simple Read" "$SCRIPT_DIR/mock-tool-calls/simple-read.json"
69 | ;;
70 | 2)
71 | run_test "Task Search" "$SCRIPT_DIR/mock-tool-calls/task-search.json"
72 | ;;
73 | 3)
74 | run_test "Multi-File Glob" "$SCRIPT_DIR/mock-tool-calls/multi-file-glob.json"
75 | ;;
76 | 4)
77 | run_test "Grep Search" "$SCRIPT_DIR/mock-tool-calls/grep-search.json"
78 | ;;
79 | 5)
80 | echo "Enter your JSON (Ctrl+D to finish):"
81 | echo "Example:"
82 | echo '{"tool":"Read","parameters":{"file_path":"@test.txt"},"context":{}}'
83 | echo ""
84 | CUSTOM_JSON=$(cat)
85 | echo "$CUSTOM_JSON" | "$BRIDGE_SCRIPT" | jq '.' 2>/dev/null || echo "$CUSTOM_JSON" | "$BRIDGE_SCRIPT"
86 | echo ""
87 | ;;
88 | 6)
89 | CAPTURE_DIR="$BRIDGE_DIR/debug/captured"
90 | if [ -d "$CAPTURE_DIR" ] && [ "$(ls -A "$CAPTURE_DIR" 2>/dev/null)" ]; then
91 | echo "Available captures:"
92 | ls -la "$CAPTURE_DIR/"
93 | echo ""
94 | read -p "Enter filename: " filename
95 | if [ -f "$CAPTURE_DIR/$filename" ]; then
96 | run_test "Replay: $filename" "$CAPTURE_DIR/$filename"
97 | else
98 | echo "❌ File not found: $filename"
99 | fi
100 | else
101 | echo "❌ No captures available in: $CAPTURE_DIR"
102 | fi
103 | echo ""
104 | ;;
105 | 7)
106 | echo "🧪 Testing all library functions..."
107 | echo ""
108 | echo "Path Converter:"
109 | "$BRIDGE_DIR/hooks/lib/path-converter.sh"
110 | echo ""
111 | echo "JSON Parser:"
112 | "$BRIDGE_DIR/hooks/lib/json-parser.sh"
113 | echo ""
114 | echo "Debug Helpers:"
115 | "$BRIDGE_DIR/hooks/lib/debug-helpers.sh"
116 | echo ""
117 | echo "Gemini Wrapper:"
118 | "$BRIDGE_DIR/hooks/lib/gemini-wrapper.sh"
119 | echo ""
120 | ;;
121 | 8)
122 | echo "📋 Recent Debug Logs (last 20 lines):"
123 | LOG_FILE="$BRIDGE_DIR/logs/debug/$(date +%Y%m%d).log"
124 | if [ -f "$LOG_FILE" ]; then
125 | tail -20 "$LOG_FILE"
126 | else
127 | echo "No logs found for today"
128 | fi
129 | echo ""
130 |
131 | echo "📋 Recent Error Logs:"
132 | ERROR_FILE="$BRIDGE_DIR/logs/debug/errors.log"
133 | if [ -f "$ERROR_FILE" ]; then
134 | tail -10 "$ERROR_FILE"
135 | else
136 | echo "No errors logged"
137 | fi
138 | echo ""
139 | ;;
140 | 9)
141 | echo "🧹 Clearing cache and logs..."
142 | rm -rf "$BRIDGE_DIR/cache/gemini/"*
143 | rm -rf "$BRIDGE_DIR/logs/debug/"*
144 | rm -rf "$BRIDGE_DIR/debug/captured/"*
145 | mkdir -p "$BRIDGE_DIR/cache/gemini"
146 | mkdir -p "$BRIDGE_DIR/logs/debug"
147 | mkdir -p "$BRIDGE_DIR/debug/captured"
148 | echo "✅ Cache and logs cleared"
149 | echo ""
150 | ;;
151 | 0)
152 | echo "👋 Goodbye!"
153 | break
154 | ;;
155 | *)
156 | echo "❌ Invalid selection"
157 | echo ""
158 | ;;
159 | esac
160 |
161 | read -p "Press Enter to continue..." dummy
162 | echo ""
163 | done
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with the Claude-Gemini Bridge repository.
4 |
5 | ## Project Overview
6 |
7 | The Claude-Gemini Bridge is an intelligent hook system that seamlessly integrates Claude Code with Google Gemini for large-scale code analysis tasks. When Claude Code encounters complex analysis requests, the bridge automatically delegates appropriate tasks to Gemini while maintaining Claude's control over the conversation flow.
8 |
9 | ## Architecture
10 |
11 | ### Core Components
12 |
13 | - **Hook System**: Uses Claude Code's PreToolUse hooks to intercept tool calls
14 | - **Path Converter**: Translates Claude's `@` notation to absolute file paths
15 | - **Decision Engine**: Intelligently determines when to delegate tasks to Gemini
16 | - **Caching Layer**: Avoids redundant API calls with content-aware caching
17 | - **Debug System**: Comprehensive logging and performance monitoring
18 |
19 | ### How It Works
20 |
21 | 1. **Interception**: Hook catches Claude's tool calls (Read, Glob, Grep, Task)
22 | 2. **Analysis**: Decision engine evaluates file count, size, and task complexity
23 | 3. **Delegation**: Large or complex tasks are sent to Gemini for processing
24 | 4. **Integration**: Gemini's analysis is seamlessly returned to Claude
25 | 5. **Fallback**: Failed delegations continue with normal Claude execution
26 |
27 | ## Usage Patterns
28 |
29 | ### Automatic Delegation Triggers
30 |
31 | The bridge delegates to Gemini when:
32 | - **Token Limit**: Content exceeds ~50k tokens (~200KB, optimized for Claude's 200k context)
33 | - **Multi-File Tasks**: ≥3 files for Task operations (configurable)
34 | - **Safety Limits**: Content must be ≤10MB and ≤800k tokens for Gemini processing
35 | - **File Exclusions**: Automatically excludes sensitive files (*.secret, *.key, *.env, etc.)
36 |
37 | ### Configuration
38 |
39 | Key settings in `hooks/config/debug.conf`:
40 |
41 | ```bash
42 | # Delegation thresholds (optimized for Claude 200k context)
43 | MIN_FILES_FOR_GEMINI=3 # Delegate Task operations with ≥3 files
44 | MIN_FILE_SIZE_FOR_GEMINI=5120 # Minimum 5KB total size
45 | MAX_TOTAL_SIZE_FOR_GEMINI=10485760 # Maximum 10MB total size
46 |
47 | # Debug and performance
48 | DEBUG_LEVEL=2 # 0=off, 1=basic, 2=verbose, 3=trace
49 | DRY_RUN=false # Test mode without Gemini calls
50 | ```
51 |
52 | See README.md for complete configuration reference.
53 |
54 | ## Development Guidelines
55 |
56 | ### Code Style
57 | - All shell scripts use bash and follow POSIX compliance where possible
58 | - Comments are in English
59 | - Functions include single-line ABOUTME comments explaining purpose
60 | - Error handling with proper exit codes and logging
61 |
62 | ### Testing
63 | - Use `test/test-runner.sh` for automated testing
64 | - Use `test/manual-test.sh` for interactive debugging
65 | - All library functions include self-tests
66 |
67 | ### Debugging
68 | - Set `DEBUG_LEVEL=3` for maximum verbosity
69 | - Enable `CAPTURE_INPUTS=true` to save tool calls for replay
70 | - Use `DRY_RUN=true` to test delegation logic without calling Gemini
71 |
72 | ## Security Considerations
73 |
74 | ### File Exclusions
75 | The bridge automatically excludes sensitive files:
76 | - `*.secret`, `*.key`, `*.env`
77 | - `*.password`, `*.token`, `*.pem`, `*.p12`
78 |
79 | ### Rate Limiting
80 | - 1 second between Gemini API calls (configurable)
81 | - 100 requests/day quota monitoring
82 | - Automatic cache cleanup to prevent data accumulation
83 |
84 | ### Permissions
85 | - Scripts run with user permissions only
86 | - No elevated privileges required
87 | - Logs stored in user directory
88 |
89 | ## Performance Optimization
90 |
91 | ### Delegation Strategy (Optimized 2024)
92 | - **Early delegation**: Content >50k tokens (~200KB) goes to Gemini
93 | - **Multi-file threshold**: Task operations with ≥3 files delegate automatically
94 | - **Efficient context usage**: Claude reserves capacity for response generation
95 | - **Gemini utilization**: Leverages 1M token capacity for large analysis tasks
96 |
97 | ### Caching Strategy
98 | - Content-aware cache keys based on file contents and metadata
99 | - 1-hour default TTL with automatic cleanup
100 | - Cache invalidation on file modifications
101 |
102 | ### Resource Management
103 | - Automatic memory cleanup after processing
104 | - Background cache and log rotation
105 | - Configurable file size limits
106 |
107 | ### Performance Impact (2024 Optimization)
108 | - **Early delegation**: Content >50k tokens (~200KB) → Gemini
109 | - **Multi-file threshold**: ≥3 files for Task operations → Gemini
110 | - **Better coverage**: Minimum size reduced to 5KB
111 |
112 | ## Integration Points
113 |
114 | ### Claude Code Integration
115 | - Seamless hook integration via `settings.json`
116 | - No modification of Claude Code required
117 | - Preserves all existing Claude functionality
118 |
119 | ### Gemini API Integration
120 | - Direct CLI integration (no custom API wrappers)
121 | - Automatic error handling and fallbacks
122 | - Structured prompt generation based on task type
123 |
124 | ## Troubleshooting
125 |
126 | ### Common Issues
127 | - **Hook not executing**: Check `~/.claude/settings.json` configuration
128 | - **Gemini not found**: Verify `gemini` CLI is in PATH
129 | - **Cache issues**: Clear cache with `rm -rf cache/gemini/*`
130 | - **Permission errors**: Ensure scripts are executable
131 |
132 | ### Debug Commands
133 | ```bash
134 | # View recent logs
135 | tail -f logs/debug/$(date +%Y%m%d).log
136 |
137 | # Test individual components
138 | hooks/lib/path-converter.sh
139 | hooks/lib/json-parser.sh
140 | hooks/lib/gemini-wrapper.sh
141 |
142 | # Run full test suite
143 | test/test-runner.sh
144 |
145 | # Interactive testing
146 | test/manual-test.sh
147 | ```
148 |
149 | ## Monitoring
150 |
151 | ### Performance Metrics
152 | - Hook execution time
153 | - Gemini processing duration
154 | - Cache hit/miss ratios
155 | - File processing statistics
156 |
157 | ### Health Checks
158 | - Automated testing in CI/CD
159 | - Component-level validation
160 | - API connectivity verification
161 | - Resource usage monitoring
162 |
163 | ## Contribution Guidelines
164 |
165 | ### Pull Requests
166 | - Include test coverage for new features
167 | - Update documentation for API changes
168 | - Follow existing code style conventions
169 | - Add debug logging for new components
170 |
171 | ### Issue Reporting
172 | - Include debug logs and reproduction steps
173 | - Specify Claude Code and Gemini CLI versions
174 | - Provide sample inputs when possible
175 | - Test with latest version before reporting
--------------------------------------------------------------------------------
/test/test-runner.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ABOUTME: Automated test suite for Claude-Gemini Bridge
3 |
4 | echo "🚀 Claude-Gemini Bridge Test Suite"
5 | echo "==================================="
6 | echo ""
7 |
8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9 |
10 | # Use current directory structure (git repo = installation)
11 | BRIDGE_DIR="$SCRIPT_DIR/.."
12 | echo "🔧 Running from: $BRIDGE_DIR"
13 |
14 | BRIDGE_SCRIPT="$BRIDGE_DIR/hooks/gemini-bridge.sh"
15 | MOCK_DIR="$SCRIPT_DIR/mock-tool-calls"
16 |
17 | # Statistics
18 | TOTAL_TESTS=0
19 | PASSED_TESTS=0
20 | FAILED_TESTS=0
21 |
22 | # Test function
23 | run_test() {
24 | local test_name="$1"
25 | local json_file="$2"
26 | local expected_action="$3" # "continue" or "replace"
27 |
28 | TOTAL_TESTS=$((TOTAL_TESTS + 1))
29 |
30 | echo "🧪 Test $TOTAL_TESTS: $test_name"
31 |
32 | if [ ! -f "$json_file" ]; then
33 | echo "❌ FAILED: Test file not found: $json_file"
34 | FAILED_TESTS=$((FAILED_TESTS + 1))
35 | return 1
36 | fi
37 |
38 | # Execute test
39 | local result=$(cat "$json_file" | "$BRIDGE_SCRIPT" 2>/dev/null)
40 | local exit_code=$?
41 |
42 | # Check exit code
43 | if [ $exit_code -ne 0 ]; then
44 | echo "❌ FAILED: Bridge script exit code: $exit_code"
45 | FAILED_TESTS=$((FAILED_TESTS + 1))
46 | return 1
47 | fi
48 |
49 | # Check JSON validity
50 | if ! echo "$result" | jq empty 2>/dev/null; then
51 | echo "❌ FAILED: Invalid JSON response"
52 | echo " Response: $result"
53 | FAILED_TESTS=$((FAILED_TESTS + 1))
54 | return 1
55 | fi
56 |
57 | # Check decision (new Claude Code hook format)
58 | local decision=$(echo "$result" | jq -r '.decision // empty')
59 |
60 | # Map expected actions to decisions
61 | local expected_decision
62 | case "$expected_action" in
63 | "continue") expected_decision="approve" ;;
64 | "replace") expected_decision="block" ;;
65 | *) expected_decision="$expected_action" ;;
66 | esac
67 |
68 | if [ "$decision" != "$expected_decision" ]; then
69 | echo "❌ FAILED: Expected decision '$expected_decision', got '$decision'"
70 | echo " Response: $result"
71 | FAILED_TESTS=$((FAILED_TESTS + 1))
72 | return 1
73 | fi
74 |
75 | # Additional validation for "block" decision (delegation to Gemini)
76 | if [ "$decision" = "block" ]; then
77 | local has_result=$(echo "$result" | jq -r '.result // empty')
78 | if [ -z "$has_result" ]; then
79 | echo "❌ FAILED: Block decision without result"
80 | FAILED_TESTS=$((FAILED_TESTS + 1))
81 | return 1
82 | fi
83 | fi
84 |
85 | echo "✅ PASSED: $test_name"
86 | PASSED_TESTS=$((PASSED_TESTS + 1))
87 | return 0
88 | }
89 |
90 | # Test preparation
91 | echo "📋 Preparing test environment..."
92 |
93 | # Create test files if they don't exist
94 | mkdir -p /tmp/test-project/src
95 | echo "print('Hello World')" > /tmp/test-project/src/main.py
96 | echo "# Test config file" > /tmp/test-project/config.txt
97 |
98 | echo "✅ Test environment ready"
99 | echo ""
100 |
101 | # Execute tests
102 | echo "🏃 Running tests..."
103 | echo ""
104 |
105 | # Test 1: Simple Read (should be "continue" since only 1 file)
106 | run_test "Simple Read (continue)" "$MOCK_DIR/simple-read.json" "continue"
107 |
108 | # Test 2: Task with Search (could be "replace" depending on files)
109 | run_test "Task Search" "$MOCK_DIR/task-search.json" "continue"
110 |
111 | # Test 3: Multi-File Glob (could be "replace" with many files)
112 | run_test "Multi-File Glob" "$MOCK_DIR/multi-file-glob.json" "continue"
113 |
114 | # Test 4: Grep Search
115 | run_test "Grep Search" "$MOCK_DIR/grep-search.json" "continue"
116 |
117 | # Test 5: Invalid JSON
118 | echo "🧪 Test $((TOTAL_TESTS + 1)): Invalid JSON"
119 | TOTAL_TESTS=$((TOTAL_TESTS + 1))
120 | INVALID_RESULT=$(echo '{"invalid": json}' | "$BRIDGE_SCRIPT" 2>/dev/null)
121 | INVALID_EXIT=$?
122 | if [ $INVALID_EXIT -eq 1 ]; then
123 | echo "✅ PASSED: Invalid JSON handled correctly"
124 | PASSED_TESTS=$((PASSED_TESTS + 1))
125 | else
126 | echo "❌ FAILED: Invalid JSON not handled properly"
127 | FAILED_TESTS=$((FAILED_TESTS + 1))
128 | fi
129 |
130 | # Test 6: Empty Input
131 | echo "🧪 Test $((TOTAL_TESTS + 1)): Empty Input"
132 | TOTAL_TESTS=$((TOTAL_TESTS + 1))
133 | EMPTY_RESULT=$(echo '' | "$BRIDGE_SCRIPT" 2>/dev/null)
134 | EMPTY_EXIT=$?
135 | if [ $EMPTY_EXIT -eq 0 ] && [[ "$EMPTY_RESULT" == *"approve"* ]]; then
136 | echo "✅ PASSED: Empty input handled correctly"
137 | PASSED_TESTS=$((PASSED_TESTS + 1))
138 | else
139 | echo "❌ FAILED: Empty input not handled properly (exit: $EMPTY_EXIT, result: $EMPTY_RESULT)"
140 | FAILED_TESTS=$((FAILED_TESTS + 1))
141 | fi
142 |
143 | # Library tests
144 | echo ""
145 | echo "📚 Testing library functions..."
146 |
147 | # Path Converter Test
148 | echo "🧪 Testing path-converter.sh..."
149 | if "$BRIDGE_DIR/hooks/lib/path-converter.sh" >/dev/null 2>&1; then
150 | echo "✅ PASSED: Path converter"
151 | PASSED_TESTS=$((PASSED_TESTS + 1))
152 | else
153 | echo "❌ FAILED: Path converter"
154 | FAILED_TESTS=$((FAILED_TESTS + 1))
155 | fi
156 | TOTAL_TESTS=$((TOTAL_TESTS + 1))
157 |
158 | # JSON Parser Test
159 | echo "🧪 Testing json-parser.sh..."
160 | if "$BRIDGE_DIR/hooks/lib/json-parser.sh" >/dev/null 2>&1; then
161 | echo "✅ PASSED: JSON parser"
162 | PASSED_TESTS=$((PASSED_TESTS + 1))
163 | else
164 | echo "❌ FAILED: JSON parser"
165 | FAILED_TESTS=$((FAILED_TESTS + 1))
166 | fi
167 | TOTAL_TESTS=$((TOTAL_TESTS + 1))
168 |
169 | # Debug Helpers Test
170 | echo "🧪 Testing debug-helpers.sh..."
171 | if "$BRIDGE_DIR/hooks/lib/debug-helpers.sh" >/dev/null 2>&1; then
172 | echo "✅ PASSED: Debug helpers"
173 | PASSED_TESTS=$((PASSED_TESTS + 1))
174 | else
175 | echo "❌ FAILED: Debug helpers"
176 | FAILED_TESTS=$((FAILED_TESTS + 1))
177 | fi
178 | TOTAL_TESTS=$((TOTAL_TESTS + 1))
179 |
180 | # Gemini Wrapper Test
181 | echo "🧪 Testing gemini-wrapper.sh..."
182 | if "$BRIDGE_DIR/hooks/lib/gemini-wrapper.sh" >/dev/null 2>&1; then
183 | echo "✅ PASSED: Gemini wrapper"
184 | PASSED_TESTS=$((PASSED_TESTS + 1))
185 | else
186 | echo "❌ FAILED: Gemini wrapper"
187 | FAILED_TESTS=$((FAILED_TESTS + 1))
188 | fi
189 | TOTAL_TESTS=$((TOTAL_TESTS + 1))
190 |
191 | # Test summary
192 | echo ""
193 | echo "📊 Test Summary"
194 | echo "==============="
195 | echo "Total Tests: $TOTAL_TESTS"
196 | echo "Passed: $PASSED_TESTS"
197 | echo "Failed: $FAILED_TESTS"
198 | echo "Success Rate: $(( PASSED_TESTS * 100 / TOTAL_TESTS ))%"
199 | echo ""
200 |
201 | if [ $FAILED_TESTS -eq 0 ]; then
202 | echo "🎉 All tests passed! Bridge is ready for use."
203 | exit 0
204 | else
205 | echo "💥 Some tests failed. Check the logs for details:"
206 | echo " Debug: $BRIDGE_DIR/logs/debug/$(date +%Y%m%d).log"
207 | echo " Errors: $BRIDGE_DIR/logs/debug/errors.log"
208 | echo ""
209 | echo "Run manual tests for more details:"
210 | echo " $SCRIPT_DIR/manual-test.sh"
211 | exit 1
212 | fi
--------------------------------------------------------------------------------
/uninstall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ABOUTME: Uninstaller for Claude-Gemini Bridge - removes hooks and optionally cleans up data
3 |
4 | echo "🗑️ Claude-Gemini Bridge Uninstaller"
5 | echo "===================================="
6 | echo ""
7 |
8 | # Colors for output
9 | GREEN='\033[0;32m'
10 | RED='\033[0;31m'
11 | YELLOW='\033[1;33m'
12 | BLUE='\033[0;34m'
13 | NC='\033[0m'
14 |
15 | # Global variables
16 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17 | CLAUDE_SETTINGS_FILE="$HOME/.claude/settings.json"
18 | BACKUP_SUFFIX=$(date +%Y%m%d_%H%M%S)
19 |
20 | # Log function
21 | log() {
22 | local level="$1"
23 | local message="$2"
24 |
25 | case $level in
26 | "info") echo -e "${GREEN}✅${NC} $message" ;;
27 | "warn") echo -e "${YELLOW}⚠️${NC} $message" ;;
28 | "error") echo -e "${RED}❌${NC} $message" ;;
29 | "debug") echo -e "${BLUE}🔍${NC} $message" ;;
30 | esac
31 | }
32 |
33 | # Error handling
34 | error_exit() {
35 | log "error" "$1"
36 | echo ""
37 | echo "💥 Uninstallation failed!"
38 | exit 1
39 | }
40 |
41 | # Check if jq is available
42 | check_jq() {
43 | if ! command -v jq &> /dev/null; then
44 | log "warn" "jq not found - will use fallback method for settings removal"
45 | return 1
46 | fi
47 | return 0
48 | }
49 |
50 | # Remove hook from Claude settings
51 | remove_claude_hooks() {
52 | log "info" "Removing Claude Code Hooks..."
53 |
54 | if [ ! -f "$CLAUDE_SETTINGS_FILE" ]; then
55 | log "info" "No Claude settings file found - nothing to remove"
56 | return 0
57 | fi
58 |
59 | # Backup existing settings
60 | cp "$CLAUDE_SETTINGS_FILE" "${CLAUDE_SETTINGS_FILE}.backup.${BACKUP_SUFFIX}"
61 | log "info" "Backup created: ${CLAUDE_SETTINGS_FILE}.backup.${BACKUP_SUFFIX}"
62 |
63 | # Check if our hook exists
64 | local hook_command="$SCRIPT_DIR/hooks/gemini-bridge.sh"
65 | if ! grep -q "gemini-bridge.sh" "$CLAUDE_SETTINGS_FILE" 2>/dev/null; then
66 | log "info" "No Claude-Gemini Bridge hooks found in settings"
67 | return 0
68 | fi
69 |
70 | if check_jq; then
71 | # Use jq to remove our hook
72 | local updated_config=$(jq '
73 | .hooks.PreToolUse = (.hooks.PreToolUse // []) |
74 | .hooks.PreToolUse |= map(
75 | select(.hooks[]?.command? and (.hooks[]?.command | contains("gemini-bridge.sh")) | not)
76 | ) |
77 | if (.hooks.PreToolUse | length) == 0 then
78 | del(.hooks.PreToolUse)
79 | else . end |
80 | if (.hooks | length) == 0 then
81 | del(.hooks)
82 | else . end
83 | ' "$CLAUDE_SETTINGS_FILE" 2>/dev/null)
84 |
85 | if [ $? -eq 0 ] && [ -n "$updated_config" ]; then
86 | echo "$updated_config" > "$CLAUDE_SETTINGS_FILE"
87 | log "info" "Hook removed from Claude settings"
88 | else
89 | log "warn" "Could not remove hook with jq - using fallback method"
90 | remove_hook_fallback
91 | fi
92 | else
93 | remove_hook_fallback
94 | fi
95 | }
96 |
97 | # Fallback method to remove hook (without jq)
98 | remove_hook_fallback() {
99 | log "debug" "Using fallback method to remove hook..."
100 |
101 | # Create a temporary file without our hook
102 | local temp_file=$(mktemp)
103 | local in_our_hook=false
104 | local brace_count=0
105 |
106 | while IFS= read -r line; do
107 | # Check if we're entering our hook section
108 | if [[ "$line" =~ gemini-bridge\.sh ]]; then
109 | in_our_hook=true
110 | # Skip this entire hook object
111 | continue
112 | fi
113 |
114 | # If we're in our hook, count braces to know when we're out
115 | if [ "$in_our_hook" = true ]; then
116 | # Count opening and closing braces
117 | local open_braces=$(echo "$line" | tr -cd '{' | wc -c)
118 | local close_braces=$(echo "$line" | tr -cd '}' | wc -c)
119 | brace_count=$((brace_count + open_braces - close_braces))
120 |
121 | # If brace count is back to 0, we're out of our hook
122 | if [ $brace_count -le 0 ]; then
123 | in_our_hook=false
124 | fi
125 | continue
126 | fi
127 |
128 | # If we're not in our hook, keep the line
129 | echo "$line" >> "$temp_file"
130 | done < "$CLAUDE_SETTINGS_FILE"
131 |
132 | # Replace the original file
133 | mv "$temp_file" "$CLAUDE_SETTINGS_FILE"
134 | log "info" "Hook removed using fallback method"
135 | }
136 |
137 | # Clean up data
138 | cleanup_data() {
139 | local cleanup_choice="$1"
140 |
141 | case $cleanup_choice in
142 | "all")
143 | log "info" "Removing all data (cache, logs, captured inputs)..."
144 | rm -rf "$SCRIPT_DIR/cache" "$SCRIPT_DIR/logs" "$SCRIPT_DIR/debug" 2>/dev/null
145 | log "info" "All data removed"
146 | ;;
147 | "cache")
148 | log "info" "Removing cache only..."
149 | rm -rf "$SCRIPT_DIR/cache" 2>/dev/null
150 | log "info" "Cache removed"
151 | ;;
152 | "logs")
153 | log "info" "Removing logs only..."
154 | rm -rf "$SCRIPT_DIR/logs" "$SCRIPT_DIR/debug" 2>/dev/null
155 | log "info" "Logs removed"
156 | ;;
157 | "none")
158 | log "info" "Keeping all data files"
159 | ;;
160 | esac
161 | }
162 |
163 | # Show uninstallation summary
164 | show_summary() {
165 | echo ""
166 | echo "🎉 Uninstallation completed!"
167 | echo "============================"
168 | echo ""
169 | echo "✅ Claude-Gemini Bridge hooks removed from: $CLAUDE_SETTINGS_FILE"
170 | echo "📁 Bridge directory remains: $SCRIPT_DIR"
171 | echo ""
172 | echo "📚 Next steps:"
173 | echo ""
174 | echo " 1. **RESTART Claude Code** to apply hook changes"
175 | echo ""
176 | echo " 2. Optional: Remove the bridge directory manually:"
177 | echo " rm -rf $SCRIPT_DIR"
178 | echo ""
179 | echo " 3. If needed, restore settings backup:"
180 | echo " cp ${CLAUDE_SETTINGS_FILE}.backup.${BACKUP_SUFFIX} $CLAUDE_SETTINGS_FILE"
181 | echo ""
182 | echo "💡 The Claude-Gemini Bridge can be reinstalled anytime by running:"
183 | echo " git clone [repository] && cd claude-gemini-bridge && ./install.sh"
184 | }
185 |
186 | # Main uninstallation
187 | main() {
188 | echo "This script removes Claude-Gemini Bridge hooks from Claude Code settings."
189 | echo "Bridge directory: $SCRIPT_DIR"
190 | echo ""
191 |
192 | # Ask what to clean up
193 | echo "What would you like to clean up?"
194 | echo ""
195 | echo "1) Remove hooks only (keep cache and logs)"
196 | echo "2) Remove hooks + cache (keep logs)"
197 | echo "3) Remove hooks + logs (keep cache)"
198 | echo "4) Remove hooks + all data (cache, logs, captured inputs)"
199 | echo "5) Cancel"
200 | echo ""
201 | read -p "Choose option (1-5): " choice
202 |
203 | case $choice in
204 | 1)
205 | cleanup_option="none"
206 | ;;
207 | 2)
208 | cleanup_option="cache"
209 | ;;
210 | 3)
211 | cleanup_option="logs"
212 | ;;
213 | 4)
214 | cleanup_option="all"
215 | ;;
216 | 5)
217 | log "info" "Uninstallation cancelled"
218 | exit 0
219 | ;;
220 | *)
221 | log "warn" "Invalid choice. Using option 1 (hooks only)"
222 | cleanup_option="none"
223 | ;;
224 | esac
225 |
226 | echo ""
227 | read -p "Continue with uninstallation? (y/N): " confirm
228 |
229 | if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
230 | log "info" "Uninstallation cancelled"
231 | exit 0
232 | fi
233 |
234 | echo ""
235 |
236 | # Uninstallation steps
237 | remove_claude_hooks
238 | cleanup_data "$cleanup_option"
239 |
240 | show_summary
241 | }
242 |
243 | # Execute script
244 | main "$@"
--------------------------------------------------------------------------------
/hooks/lib/json-parser.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ABOUTME: JSON parsing functions for Claude-Gemini Bridge
3 |
4 | # Extracts tool name from JSON
5 | extract_tool_name() {
6 | local json="$1"
7 | echo "$json" | jq -r '.tool_name // .tool // empty'
8 | }
9 |
10 | # Extracts parameters as JSON object
11 | extract_parameters() {
12 | local json="$1"
13 | echo "$json" | jq -r '.tool_input // .parameters // {}'
14 | }
15 |
16 | # Extracts working directory from context
17 | extract_working_directory() {
18 | local json="$1"
19 | echo "$json" | jq -r '.context.working_directory // empty'
20 | }
21 |
22 | # Extracts file paths based on tool type
23 | extract_file_paths() {
24 | local params="$1"
25 | local tool="$2"
26 |
27 | case "$tool" in
28 | "Read")
29 | echo "$params" | jq -r '.file_path // empty'
30 | ;;
31 | "Glob")
32 | echo "$params" | jq -r '.pattern // empty'
33 | ;;
34 | "Grep")
35 | local path=$(echo "$params" | jq -r '.path // "."')
36 | local pattern=$(echo "$params" | jq -r '.pattern // empty')
37 | echo "$path (pattern: $pattern)"
38 | ;;
39 | "Task")
40 | # Simple Task file extraction - just extract @ paths and file references
41 | local prompt=$(echo "$params" | jq -r '.prompt // empty')
42 |
43 | # Extract @ paths and explicit file patterns
44 | echo "$prompt" | grep -oE '(@[^[:space:]]+|/[^[:space:]]*\.[a-zA-Z0-9]+|[^[:space:]]*\.[a-zA-Z0-9]+)'
45 | ;;
46 | *)
47 | echo ""
48 | ;;
49 | esac
50 | }
51 |
52 | # Extracts prompt from task parameters
53 | extract_task_prompt() {
54 | local params="$1"
55 | echo "$params" | jq -r '.prompt // empty'
56 | }
57 |
58 | # Extracts description from task parameters
59 | extract_task_description() {
60 | local params="$1"
61 | echo "$params" | jq -r '.description // empty'
62 | }
63 |
64 | # Validates if JSON is valid
65 | validate_json() {
66 | local json="$1"
67 | if echo "$json" | jq empty 2>/dev/null; then
68 | return 0
69 | else
70 | return 1
71 | fi
72 | }
73 |
74 | # Creates JSON response for hook response
75 | create_hook_response() {
76 | local action="$1"
77 | local result="$2"
78 | local reason="$3"
79 |
80 | case "$action" in
81 | "continue"|"approve")
82 | # Normal tool execution continues
83 | echo '{"decision": "approve"}'
84 | ;;
85 | "replace"|"block_with_result")
86 | # Block tool execution and provide Gemini result
87 | if [ -n "$result" ]; then
88 | # Extract the actual content from the Gemini response
89 | local gemini_content=$(echo "$result" | jq -r '.content // empty' 2>/dev/null)
90 | if [ -n "$gemini_content" ]; then
91 | # Extract metadata for better context
92 | local tool_name=$(echo "$result" | jq -r '.metadata.original_tool // "unknown"' 2>/dev/null)
93 | local file_count=$(echo "$result" | jq -r '.metadata.file_count // 0' 2>/dev/null)
94 |
95 | # Create a friendly introduction
96 | local intro_text="🤖 Gemini Assistant here! I've analyzed this content for you since it exceeded your optimal processing size.
97 |
98 | Tool: ${tool_name}
99 | Files analyzed: ${file_count}
100 | ───────────────────────────────────────
101 |
102 | "
103 |
104 | # Put Gemini's response with friendly intro in the reason field
105 | jq -n --arg reason "${intro_text}${gemini_content}" \
106 | '{"decision": "block", "reason": $reason}'
107 | else
108 | # Fallback if content extraction fails
109 | jq -n --arg reason "${reason:-Delegated to Gemini for large-scale analysis}" \
110 | '{"decision": "block", "reason": $reason}'
111 | fi
112 | else
113 | jq -n --arg reason "${reason:-No result provided}" \
114 | '{"decision": "block", "reason": $reason}'
115 | fi
116 | ;;
117 | "block")
118 | # Block tool execution with error
119 | jq -n --arg reason "${reason:-Blocked by hook}" \
120 | '{"decision": "block", "reason": $reason}'
121 | ;;
122 | *)
123 | echo '{"decision": "approve"}'
124 | ;;
125 | esac
126 | }
127 |
128 | # Creates structured Gemini response
129 | create_gemini_response() {
130 | local content="$1"
131 | local original_tool="$2"
132 | local file_count="$3"
133 | local processing_time="$4"
134 |
135 | jq -n \
136 | --arg content "$content" \
137 | --arg original_tool "$original_tool" \
138 | --arg file_count "$file_count" \
139 | --arg processing_time "$processing_time" \
140 | '{
141 | type: "gemini_analysis",
142 | content: $content,
143 | metadata: {
144 | original_tool: $original_tool,
145 | file_count: ($file_count | tonumber),
146 | processing_time: $processing_time,
147 | timestamp: now
148 | }
149 | }'
150 | }
151 |
152 | # Counts number of files in a string
153 | count_files() {
154 | local files="$1"
155 | if [ -z "$files" ]; then
156 | echo "0"
157 | else
158 | echo "$files" | wc -w | tr -d ' '
159 | fi
160 | }
161 |
162 | # Test function for JSON parser
163 | test_json_parser() {
164 | echo "Testing JSON parser..."
165 | local failed=0
166 |
167 | # Test JSON
168 | local test_json='{
169 | "tool": "Read",
170 | "parameters": {
171 | "file_path": "@src/main.py"
172 | },
173 | "context": {
174 | "working_directory": "/Users/tim/Code/project"
175 | }
176 | }'
177 |
178 | # Test 1: Tool-Name extrahieren
179 | local tool_name=$(extract_tool_name "$test_json")
180 | if [ "$tool_name" != "Read" ]; then
181 | echo "❌ Test 1 failed: Tool name '$tool_name' != 'Read'"
182 | failed=1
183 | else
184 | echo "✅ Test 1 passed: Tool name extraction"
185 | fi
186 |
187 | # Test 2: Working Directory extrahieren
188 | local wd=$(extract_working_directory "$test_json")
189 | if [ "$wd" != "/Users/tim/Code/project" ]; then
190 | echo "❌ Test 2 failed: Working directory '$wd' != '/Users/tim/Code/project'"
191 | failed=1
192 | else
193 | echo "✅ Test 2 passed: Working directory extraction"
194 | fi
195 |
196 | # Test 3: Dateipfade extrahieren
197 | local params=$(extract_parameters "$test_json")
198 | local file_path=$(extract_file_paths "$params" "Read")
199 | if [ "$file_path" != "@src/main.py" ]; then
200 | echo "❌ Test 3 failed: File path '$file_path' != '@src/main.py'"
201 | failed=1
202 | else
203 | echo "✅ Test 3 passed: File path extraction"
204 | fi
205 |
206 | # Test 4: Hook-Response erstellen
207 | local response=$(create_hook_response "continue")
208 | if ! echo "$response" | jq empty 2>/dev/null; then
209 | echo "❌ Test 4 failed: Invalid JSON response"
210 | failed=1
211 | else
212 | echo "✅ Test 4 passed: Hook response creation"
213 | fi
214 |
215 | # Test 5: Task-JSON
216 | local task_json='{
217 | "tool": "Task",
218 | "parameters": {
219 | "prompt": "Search for config files in @src/ and @config/",
220 | "description": "Config search"
221 | },
222 | "context": {}
223 | }'
224 |
225 | local task_params=$(extract_parameters "$task_json")
226 | local task_prompt=$(extract_task_prompt "$task_params")
227 | if [[ "$task_prompt" != *"@src/"* ]]; then
228 | echo "❌ Test 5 failed: Task prompt extraction"
229 | failed=1
230 | else
231 | echo "✅ Test 5 passed: Task prompt extraction"
232 | fi
233 |
234 | if [ $failed -eq 0 ]; then
235 | echo "🎉 All JSON parser tests passed!"
236 | return 0
237 | else
238 | echo "💥 Some tests failed!"
239 | return 1
240 | fi
241 | }
242 |
243 | # Wenn das Script direkt aufgerufen wird, führe Tests aus
244 | if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
245 | test_json_parser
246 | fi
--------------------------------------------------------------------------------
/docs/TROUBLESHOOTING.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting Guide for Claude-Gemini Bridge
2 |
3 | ## 🔧 Common Problems and Solutions
4 |
5 | ### Installation & Setup
6 |
7 | #### Hook not executing
8 | **Symptom:** Claude behaves normally, but Gemini is never called
9 |
10 | **Solution steps:**
11 | 1. Check Claude Settings:
12 | ```bash
13 | cat ~/.claude/settings.json
14 | ```
15 |
16 | 2. Test hook manually:
17 | ```bash
18 | echo '{"tool_name":"Read","tool_input":{"file_path":"test.txt"},"session_id":"test"}' | ./hooks/gemini-bridge.sh
19 | ```
20 |
21 | 3. Check permissions:
22 | ```bash
23 | ls -la hooks/gemini-bridge.sh
24 | # Should be executable (x-flag)
25 | ```
26 |
27 | 4. Check hook configuration:
28 | ```bash
29 | jq '.hooks' ~/.claude/settings.json
30 | ```
31 |
32 | **Solution:** Run re-installation:
33 | ```bash
34 | ./install.sh
35 | ```
36 |
37 | ---
38 |
39 | #### "command not found: jq"
40 | **Symptom:** Error when running scripts
41 |
42 | **Solution:**
43 | - **macOS:** `brew install jq`
44 | - **Linux:** `sudo apt-get install jq`
45 | - **Alternative:** Use the installer, which checks jq dependencies
46 |
47 | ---
48 |
49 | #### "command not found: gemini"
50 | **Symptom:** Bridge cannot find Gemini
51 |
52 | **Solution steps:**
53 | 1. Check Gemini installation:
54 | ```bash
55 | which gemini
56 | gemini --version
57 | ```
58 |
59 | 2. Test Gemini manually:
60 | ```bash
61 | echo "Test" | gemini -p "Say hello"
62 | ```
63 |
64 | 3. Check PATH:
65 | ```bash
66 | echo $PATH
67 | ```
68 |
69 | **Solution:** Install Gemini CLI or add to PATH
70 |
71 | ---
72 |
73 | ### Gemini Integration
74 |
75 | #### Gemini not responding
76 | **Symptom:** Hook runs, but Gemini doesn't return responses
77 |
78 | **Debug steps:**
79 | 1. Enable verbose logging:
80 | ```bash
81 | # In hooks/config/debug.conf
82 | DEBUG_LEVEL=3
83 | ```
84 |
85 | 2. Check Gemini logs:
86 | ```bash
87 | tail -f logs/debug/$(date +%Y%m%d).log | grep -i gemini
88 | ```
89 |
90 | 3. Test Gemini API key:
91 | ```bash
92 | gemini "test" -p "Hello"
93 | ```
94 |
95 | **Common causes:**
96 | - Missing or invalid API key
97 | - Rate limiting reached
98 | - Network problems
99 | - Gemini service unavailable
100 |
101 | ---
102 |
103 | #### "Rate limiting: sleeping Xs"
104 | **Symptom:** Bridge waits between calls
105 |
106 | **Explanation:** Normal! Prevents API overload.
107 |
108 | **Adjust:**
109 | ```bash
110 | # In debug.conf
111 | GEMINI_RATE_LIMIT=0.5 # Reduce to 0.5 seconds
112 | ```
113 |
114 | ---
115 |
116 | #### Cache problems
117 | **Symptom:** Outdated responses from Gemini
118 |
119 | **Solution:**
120 | ```bash
121 | # Clear cache completely
122 | rm -rf cache/gemini/*
123 |
124 | # Or reduce cache TTL (in debug.conf)
125 | GEMINI_CACHE_TTL=1800 # 30 minutes instead of 1 hour
126 | ```
127 |
128 | ---
129 |
130 | ### Path Conversion
131 |
132 | #### @ paths not converted
133 | **Symptom:** Gemini cannot find files
134 |
135 | **Debug:**
136 | 1. Test path conversion in isolation:
137 | ```bash
138 | cd hooks/lib
139 | source path-converter.sh
140 | convert_claude_paths "@src/main.py" "/Users/tim/project"
141 | ```
142 |
143 | 2. Check working directory in logs:
144 | ```bash
145 | grep "Working directory" logs/debug/$(date +%Y%m%d).log
146 | ```
147 |
148 | **Common causes:**
149 | - Missing working_directory in tool call
150 | - Relative paths without @ prefix
151 | - Incorrect directory structures
152 |
153 | ---
154 |
155 | ### Performance & Behavior
156 |
157 | #### Gemini called too often
158 | **Symptom:** Every small Read command goes to Gemini
159 |
160 | **Adjustments in debug.conf:**
161 | ```bash
162 | MIN_FILES_FOR_GEMINI=5 # Increase minimum file count
163 | CLAUDE_TOKEN_LIMIT=100000 # Increase token threshold
164 | ```
165 |
166 | ---
167 |
168 | #### Gemini never called
169 | **Symptom:** Even large analyses don't go to Gemini
170 |
171 | **Debug:**
172 | 1. Enable DRY_RUN mode:
173 | ```bash
174 | # In debug.conf
175 | DRY_RUN=true
176 | ```
177 |
178 | 2. Check decision logic:
179 | ```bash
180 | grep "should_delegate_to_gemini" logs/debug/$(date +%Y%m%d).log
181 | ```
182 |
183 | **Adjustments:**
184 | ```bash
185 | MIN_FILES_FOR_GEMINI=1 # Reduce thresholds
186 | CLAUDE_TOKEN_LIMIT=10000 # Lower token limit
187 | ```
188 |
189 | ---
190 |
191 | ## 🔍 Debug Workflow
192 |
193 | ### 1. Reproduce problem
194 | ```bash
195 | # Enable input capturing
196 | # In debug.conf: CAPTURE_INPUTS=true
197 |
198 | # Run problematic Claude command
199 | # Input will be automatically saved
200 | ```
201 |
202 | ### 2. Analyze logs
203 | ```bash
204 | # Current debug logs
205 | tail -f logs/debug/$(date +%Y%m%d).log
206 |
207 | # Error logs
208 | tail -f logs/debug/errors.log
209 |
210 | # All logs of the day
211 | less logs/debug/$(date +%Y%m%d).log
212 | ```
213 |
214 | ### 3. Test in isolation
215 | ```bash
216 | # Interactive tests
217 | ./test/manual-test.sh
218 |
219 | # Automated tests
220 | ./test/test-runner.sh
221 |
222 | # Replay saved inputs
223 | ls debug/captured/
224 | cat debug/captured/FILENAME.json | ./hooks/gemini-bridge.sh
225 | ```
226 |
227 | ### 4. Step-by-step debugging
228 | ```bash
229 | # Highest debug level
230 | # In debug.conf: DEBUG_LEVEL=3
231 |
232 | # Dry-run mode (no actual Gemini call)
233 | # In debug.conf: DRY_RUN=true
234 |
235 | # Test individual library functions
236 | ./hooks/lib/path-converter.sh
237 | ./hooks/lib/json-parser.sh
238 | ./hooks/lib/gemini-wrapper.sh
239 | ```
240 |
241 | ---
242 |
243 | ## ⚙️ Configuration
244 |
245 | ### Debug levels
246 | ```bash
247 | # In hooks/config/debug.conf
248 |
249 | DEBUG_LEVEL=0 # No debug output
250 | DEBUG_LEVEL=1 # Basic information (default)
251 | DEBUG_LEVEL=2 # Detailed information
252 | DEBUG_LEVEL=3 # Complete tracing
253 | ```
254 |
255 | ### Gemini settings
256 | ```bash
257 | GEMINI_CACHE_TTL=3600 # Cache time in seconds
258 | GEMINI_TIMEOUT=30 # Timeout per call
259 | GEMINI_RATE_LIMIT=1 # Seconds between calls
260 | GEMINI_MAX_FILES=20 # Max files per call
261 | ```
262 |
263 | ### Decision criteria
264 | ```bash
265 | MIN_FILES_FOR_GEMINI=3 # Minimum file count
266 | CLAUDE_TOKEN_LIMIT=50000 # Token threshold (~200KB)
267 | GEMINI_TOKEN_LIMIT=800000 # Max tokens for Gemini
268 | MAX_TOTAL_SIZE_FOR_GEMINI=10485760 # Max total size (10MB)
269 |
270 | # Excluded files
271 | GEMINI_EXCLUDE_PATTERNS="*.secret|*.key|*.env|*.password"
272 | ```
273 |
274 | ---
275 |
276 | ## 🧹 Maintenance
277 |
278 | ### Clear cache
279 | ```bash
280 | # Manually
281 | rm -rf cache/gemini/*
282 |
283 | # Automatically (via debug.conf)
284 | AUTO_CLEANUP_CACHE=true
285 | CACHE_MAX_AGE_HOURS=24
286 | ```
287 |
288 | ### Clear logs
289 | ```bash
290 | # Manually
291 | rm -rf logs/debug/*
292 |
293 | # Automatically (via debug.conf)
294 | AUTO_CLEANUP_LOGS=true
295 | LOG_MAX_AGE_DAYS=7
296 | ```
297 |
298 | ### Clear captured inputs
299 | ```bash
300 | rm -rf debug/captured/*
301 | ```
302 |
303 | ---
304 |
305 | ## 🆘 Emergency Deactivation
306 |
307 | ### Temporarily disable hook
308 | ```bash
309 | # Backup settings
310 | cp ~/.claude/settings.json ~/.claude/settings.json.backup
311 |
312 | # Remove hook
313 | jq 'del(.hooks)' ~/.claude/settings.json > /tmp/claude_settings
314 | mv /tmp/claude_settings ~/.claude/settings.json
315 | ```
316 |
317 | ### Re-enable hook
318 | ```bash
319 | # Restore settings
320 | cp ~/.claude/settings.json.backup ~/.claude/settings.json
321 |
322 | # Or reinstall
323 | ./install.sh
324 | ```
325 |
326 | ### Complete uninstallation
327 | ```bash
328 | # Remove hook
329 | jq 'del(.hooks)' ~/.claude/settings.json > /tmp/claude_settings
330 | mv /tmp/claude_settings ~/.claude/settings.json
331 |
332 | # Remove bridge
333 | rm -rf ~/claude-gemini-bridge
334 | ```
335 |
336 | ---
337 |
338 | ## 📞 Support & Reporting
339 |
340 | ### Collect logs for support
341 | ```bash
342 | # Create debug package
343 | tar -czf claude-gemini-debug-$(date +%Y%m%d).tar.gz \
344 | ~/.claude/settings.json \
345 | logs/debug/ \
346 | hooks/config/debug.conf
347 | ```
348 |
349 | ### Helpful information
350 | - Claude Version: `claude --version`
351 | - Gemini Version: `gemini --version`
352 | - Operating System: `uname -a`
353 | - Shell: `echo $SHELL`
354 | - PATH: `echo $PATH`
355 |
356 | ### Common error messages
357 | - **"Invalid JSON received"**: Input validation failed
358 | - **"Gemini initialization failed"**: Gemini CLI not available
359 | - **"Files too large/small"**: Thresholds not met
360 | - **"Rate limiting"**: Normal, shows correct function
361 | - **"Cache expired"**: Normal, cache being renewed
--------------------------------------------------------------------------------
/hooks/lib/debug-helpers.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ABOUTME: Debug helper functions for Claude-Gemini Bridge
3 |
4 | # Global variables
5 | DEBUG_LOG_DIR=""
6 | DEBUG_LEVEL=${DEBUG_LEVEL:-1}
7 | DEBUG_COMPONENT=""
8 |
9 | # Helper to get bridge directory
10 | get_bridge_dir() {
11 | echo "${CLAUDE_GEMINI_BRIDGE_DIR:-$HOME/.claude-gemini-bridge}"
12 | }
13 |
14 | # Color codes for terminal output
15 | RED='\033[0;31m'
16 | GREEN='\033[0;32m'
17 | YELLOW='\033[1;33m'
18 | BLUE='\033[0;34m'
19 | PURPLE='\033[0;35m'
20 | CYAN='\033[0;36m'
21 | NC='\033[0m' # No Color
22 |
23 | # Initializes the debug system
24 | init_debug() {
25 | DEBUG_COMPONENT="$1"
26 | DEBUG_LOG_DIR="$2"
27 |
28 | # Fallback to default directory
29 | if [ -z "$DEBUG_LOG_DIR" ]; then
30 | DEBUG_LOG_DIR="${CLAUDE_GEMINI_BRIDGE_DIR:-$HOME/.claude-gemini-bridge}/logs/debug"
31 | fi
32 |
33 | # Create log directory
34 | mkdir -p "$DEBUG_LOG_DIR"
35 |
36 | debug_log 1 "Debug system initialized for component: $DEBUG_COMPONENT"
37 | }
38 |
39 | # Main function for debug logging
40 | debug_log() {
41 | local level=$1
42 | local message="$2"
43 | local timestamp=$(date '+%Y-%m-%d %H:%M:%S.%3N')
44 | local component_prefix=""
45 |
46 | # Add component prefix
47 | if [ -n "$DEBUG_COMPONENT" ]; then
48 | component_prefix="[$DEBUG_COMPONENT] "
49 | fi
50 |
51 | # Only log if level is activated
52 | if [ "$level" -le "$DEBUG_LEVEL" ]; then
53 | local log_entry="[$timestamp] $component_prefix$message"
54 |
55 | # Level-specific handling
56 | case $level in
57 | 1)
58 | prefix="${GREEN}[INFO]${NC}"
59 | log_file="$DEBUG_LOG_DIR/$(date +%Y%m%d).log"
60 | ;;
61 | 2)
62 | prefix="${YELLOW}[DEBUG]${NC}"
63 | log_file="$DEBUG_LOG_DIR/$(date +%Y%m%d).log"
64 | ;;
65 | 3)
66 | prefix="${BLUE}[TRACE]${NC}"
67 | log_file="$DEBUG_LOG_DIR/$(date +%Y%m%d)_trace.log"
68 | ;;
69 | esac
70 |
71 | # Write to file
72 | echo "$log_entry" >> "$log_file"
73 |
74 | # Also output to stderr for higher debug levels
75 | if [ "$DEBUG_LEVEL" -ge 2 ]; then
76 | echo -e "$prefix $log_entry" >&2
77 | fi
78 | fi
79 | }
80 |
81 | # Error logging (always active)
82 | error_log() {
83 | local message="$1"
84 | local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
85 | local component_prefix=""
86 |
87 | if [ -n "$DEBUG_COMPONENT" ]; then
88 | component_prefix="[$DEBUG_COMPONENT] "
89 | fi
90 |
91 | local log_entry="[$timestamp] $component_prefix$message"
92 |
93 | # Both stderr and error log
94 | echo -e "${RED}[ERROR]${NC} $log_entry" >&2
95 | echo "$log_entry" >> "$DEBUG_LOG_DIR/errors.log"
96 | }
97 |
98 | # Start performance measurement
99 | start_timer() {
100 | local timer_name="$1"
101 | local start_time=$(date +%s.%N)
102 | echo "$start_time" > "/tmp/claude_bridge_timer_$timer_name"
103 | debug_log 3 "Timer started: $timer_name"
104 | }
105 |
106 | # End performance measurement
107 | end_timer() {
108 | local timer_name="$1"
109 | local timer_file="/tmp/claude_bridge_timer_$timer_name"
110 |
111 | if [ -f "$timer_file" ]; then
112 | local start_time=$(cat "$timer_file")
113 | local end_time=$(date +%s.%N)
114 | # Use awk instead of bc for better portability
115 | local duration=$(awk -v e="$end_time" -v s="$start_time" 'BEGIN {printf "%.3f", e-s}' 2>/dev/null || echo "0")
116 |
117 | debug_log 2 "Timer finished: $timer_name took ${duration}s"
118 | rm -f "$timer_file"
119 | echo "$duration"
120 | else
121 | debug_log 1 "Timer not found: $timer_name"
122 | echo "0"
123 | fi
124 | }
125 |
126 | # Pretty-print for JSON with syntax highlighting
127 | debug_json() {
128 | local label="$1"
129 | local json="$2"
130 |
131 | debug_log 3 "$label:"
132 |
133 | if [ "$DEBUG_LEVEL" -ge 3 ]; then
134 | if command -v jq >/dev/null 2>&1; then
135 | echo -e "${CYAN}JSON:${NC}" >&2
136 | echo "$json" | jq '.' 2>/dev/null >&2 || echo "$json" >&2
137 | else
138 | echo -e "${CYAN}JSON (raw):${NC}" >&2
139 | echo "$json" >&2
140 | fi
141 | fi
142 | }
143 |
144 | # Variable dump for debugging
145 | debug_vars() {
146 | local prefix="$1"
147 | shift
148 |
149 | debug_log 3 "Variables dump ($prefix):"
150 |
151 | if [ "$DEBUG_LEVEL" -ge 3 ]; then
152 | for var in "$@"; do
153 | if [ -n "${!var}" ]; then
154 | echo -e "${PURPLE} $var=${NC}${!var}" >&2
155 | else
156 | echo -e "${PURPLE} $var=${NC}(empty)" >&2
157 | fi
158 | done
159 | fi
160 | }
161 |
162 | # System info for debug context
163 | debug_system_info() {
164 | debug_log 3 "System info:"
165 | if [ "$DEBUG_LEVEL" -ge 3 ]; then
166 | echo -e "${CYAN}System Information:${NC}" >&2
167 | echo " OS: $(uname -s)" >&2
168 | echo " PWD: $(pwd)" >&2
169 | echo " USER: ${USER:-unknown}" >&2
170 | echo " PID: $$" >&2
171 | echo " Bash version: $BASH_VERSION" >&2
172 | fi
173 | }
174 |
175 | # File size for debug output
176 | debug_file_size() {
177 | local file="$1"
178 | if [ -f "$file" ]; then
179 | local size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null || echo "unknown")
180 | debug_log 3 "File size: $file = $size bytes"
181 | echo "$size"
182 | else
183 | debug_log 3 "File not found: $file"
184 | echo "0"
185 | fi
186 | }
187 |
188 | # Capture input for later analysis
189 | capture_input() {
190 | local input="$1"
191 | local capture_dir="$2"
192 |
193 | if [ -z "$capture_dir" ]; then
194 | capture_dir="$(get_bridge_dir)/debug/captured"
195 | fi
196 |
197 | mkdir -p "$capture_dir"
198 |
199 | local capture_file="$capture_dir/$(date +%Y%m%d_%H%M%S)_$(uuidgen 2>/dev/null || echo $$).json"
200 | echo "$input" > "$capture_file"
201 |
202 | debug_log 2 "Input captured to: $capture_file"
203 | echo "$capture_file"
204 | }
205 |
206 | # Clean up old debug files
207 | cleanup_debug_files() {
208 | local days_to_keep=${1:-7}
209 |
210 | debug_log 2 "Cleaning up debug files older than $days_to_keep days"
211 |
212 | # Delete old log files
213 | find "$DEBUG_LOG_DIR" -name "*.log" -mtime +$days_to_keep -delete 2>/dev/null
214 |
215 | # Delete old capture files
216 | find "$(get_bridge_dir)/debug/captured" -name "*.json" -mtime +$days_to_keep -delete 2>/dev/null
217 |
218 | # Delete old timer files
219 | find "/tmp" -name "claude_bridge_timer_*" -mtime +1 -delete 2>/dev/null
220 | }
221 |
222 | # Test function for debug helpers
223 | test_debug_helpers() {
224 | echo "Testing debug helpers..."
225 | local failed=0
226 |
227 | # Test directory
228 | local test_dir="/tmp/claude_bridge_debug_test"
229 | mkdir -p "$test_dir"
230 |
231 | # Test 1: Initialization
232 | init_debug "test_component" "$test_dir"
233 | if [ ! -d "$test_dir" ]; then
234 | echo "❌ Test 1 failed: Debug directory not created"
235 | failed=1
236 | else
237 | echo "✅ Test 1 passed: Debug initialization"
238 | fi
239 |
240 | # Test 2: Logging
241 | debug_log 1 "Test message"
242 | local log_file="$test_dir/$(date +%Y%m%d).log"
243 | if [ ! -f "$log_file" ]; then
244 | echo "❌ Test 2 failed: Log file not created"
245 | failed=1
246 | else
247 | echo "✅ Test 2 passed: Debug logging"
248 | fi
249 |
250 | # Test 3: Timer
251 | start_timer "test_timer"
252 | sleep 0.1
253 | local duration=$(end_timer "test_timer")
254 | if [ "$duration" = "0" ]; then
255 | echo "❌ Test 3 failed: Timer not working"
256 | failed=1
257 | else
258 | echo "✅ Test 3 passed: Timer functionality"
259 | fi
260 |
261 | # Test 4: JSON Debug
262 | local test_json='{"test": "value"}'
263 | debug_json "Test JSON" "$test_json"
264 | echo "✅ Test 4 passed: JSON debug (check manually)"
265 |
266 | # Cleanup
267 | rm -rf "$test_dir"
268 |
269 | if [ $failed -eq 0 ]; then
270 | echo "🎉 All debug helper tests passed!"
271 | return 0
272 | else
273 | echo "💥 Some tests failed!"
274 | return 1
275 | fi
276 | }
277 |
278 | # If script is called directly, run tests
279 | if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
280 | test_debug_helpers
281 | fi
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Claude-Gemini Bridge
2 |
3 | Thank you for your interest in contributing to the Claude-Gemini Bridge! This document provides guidelines and information for contributors.
4 |
5 | ## 🚀 Getting Started
6 |
7 | ### Prerequisites
8 |
9 | - Bash 4.0+
10 | - Claude Code CLI
11 | - Google Gemini CLI
12 | - `jq` for JSON processing
13 | - `git` for version control
14 |
15 | ### Development Setup
16 |
17 | 1. **Fork the repository**
18 | ```bash
19 | # Fork on GitHub, then clone your fork
20 | git clone https://github.com/your-username/claude-gemini-bridge.git
21 | cd claude-gemini-bridge
22 | ```
23 |
24 | 2. **Set up development environment**
25 | ```bash
26 | # Install development tools (macOS)
27 | brew install shellcheck shfmt
28 |
29 | # Make scripts executable
30 | chmod +x hooks/*.sh test/*.sh
31 |
32 | # Run initial tests
33 | ./test/test-runner.sh
34 | ```
35 |
36 | 3. **Create a development branch**
37 | ```bash
38 | git checkout -b feature/your-feature-name
39 | ```
40 |
41 | ## 📝 Code Standards
42 |
43 | ### Shell Script Guidelines
44 |
45 | - **Shebang**: Always use `#!/bin/bash`
46 | - **ABOUTME**: Include single-line comment explaining file purpose
47 | - **Functions**: Document with inline comments
48 | - **Variables**: Use `local` for function variables
49 | - **Error Handling**: Always check exit codes and handle errors
50 |
51 | #### Example:
52 | ```bash
53 | #!/bin/bash
54 | # ABOUTME: Example script demonstrating code standards
55 |
56 | # Global configuration
57 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
58 |
59 | # Example function with proper documentation
60 | process_files() {
61 | local input_dir="$1"
62 | local output_file="$2"
63 |
64 | # Validate inputs
65 | if [ ! -d "$input_dir" ]; then
66 | echo "Error: Directory not found: $input_dir" >&2
67 | return 1
68 | fi
69 |
70 | # Process files
71 | find "$input_dir" -name "*.txt" > "$output_file"
72 |
73 | return 0
74 | }
75 | ```
76 |
77 | ### Code Style
78 |
79 | - **Indentation**: 4 spaces (no tabs)
80 | - **Line Length**: Maximum 100 characters
81 | - **Comments**: English only
82 | - **Naming**: Use snake_case for variables and functions
83 | - **Constants**: Use UPPER_CASE for constants
84 |
85 | ### Testing Requirements
86 |
87 | Every new feature must include:
88 |
89 | 1. **Unit Tests**: Test individual functions
90 | 2. **Integration Tests**: Test component interactions
91 | 3. **Mock Data**: Provide test inputs when needed
92 |
93 | Example test structure:
94 | ```bash
95 | test_new_feature() {
96 | echo "Testing new feature..."
97 |
98 | # Test 1: Normal case
99 | local result=$(your_function "normal_input")
100 | if [ "$result" != "expected_output" ]; then
101 | echo "❌ Test 1 failed"
102 | return 1
103 | fi
104 |
105 | # Test 2: Error case
106 | your_function "invalid_input" 2>/dev/null
107 | if [ $? -eq 0 ]; then
108 | echo "❌ Test 2 failed: Should have returned error"
109 | return 1
110 | fi
111 |
112 | echo "✅ All tests passed"
113 | return 0
114 | }
115 | ```
116 |
117 | ## 🔧 Development Workflow
118 |
119 | ### 1. Issue Identification
120 |
121 | - Check existing issues before creating new ones
122 | - Use issue templates when available
123 | - Include minimal reproduction steps
124 | - Specify environment details (OS, Claude version, etc.)
125 |
126 | ### 2. Feature Development
127 |
128 | - Create feature branch from `main`
129 | - Implement changes with tests
130 | - Update documentation as needed
131 | - Ensure all tests pass
132 |
133 | ### 3. Testing
134 |
135 | ```bash
136 | # Run all tests
137 | ./test/test-runner.sh
138 |
139 | # Test specific components
140 | ./hooks/lib/path-converter.sh
141 | ./hooks/lib/json-parser.sh
142 |
143 | # Interactive testing
144 | ./test/manual-test.sh
145 |
146 | # Check shell script quality
147 | shellcheck hooks/*.sh hooks/lib/*.sh
148 | ```
149 |
150 | ### 4. Documentation
151 |
152 | Update documentation for:
153 | - New configuration options
154 | - API changes
155 | - New features
156 | - Breaking changes
157 |
158 | ### 5. Pull Request
159 |
160 | - Use descriptive PR titles
161 | - Include detailed description
162 | - Reference related issues
163 | - Ensure CI passes
164 |
165 | ## 🐛 Bug Reports
166 |
167 | ### Before Reporting
168 |
169 | 1. **Search existing issues** for duplicates
170 | 2. **Test with latest version**
171 | 3. **Check troubleshooting guide**
172 | 4. **Enable debug logging** (`DEBUG_LEVEL=3`)
173 |
174 | ### Bug Report Template
175 |
176 | ```markdown
177 | **Bug Description**
178 | Clear description of the issue
179 |
180 | **Environment**
181 | - OS: macOS 14.5 / Ubuntu 20.04 / etc.
182 | - Claude Code Version: 1.0.40
183 | - Gemini CLI Version: 1.2.3
184 | - Bridge Version: commit hash
185 |
186 | **Reproduction Steps**
187 | 1. Step one
188 | 2. Step two
189 | 3. ...
190 |
191 | **Expected Behavior**
192 | What should happen
193 |
194 | **Actual Behavior**
195 | What actually happens
196 |
197 | **Debug Logs**
198 | ```
199 | Paste relevant logs here
200 | ```
201 |
202 | **Additional Context**
203 | Any other relevant information
204 | ```
205 |
206 | ## 🚀 Feature Requests
207 |
208 | ### Feature Request Template
209 |
210 | ```markdown
211 | **Feature Description**
212 | Clear description of the proposed feature
213 |
214 | **Use Case**
215 | Why is this feature needed? What problem does it solve?
216 |
217 | **Proposed Implementation**
218 | High-level approach to implementing the feature
219 |
220 | **Alternatives Considered**
221 | Other approaches that were considered
222 |
223 | **Additional Context**
224 | Screenshots, examples, references, etc.
225 | ```
226 |
227 | ## 📋 Component Overview
228 |
229 | Understanding the codebase structure:
230 |
231 | ```
232 | claude-gemini-bridge/
233 | ├── hooks/
234 | │ ├── gemini-bridge.sh # Main hook script
235 | │ ├── lib/
236 | │ │ ├── path-converter.sh # @ path conversion
237 | │ │ ├── json-parser.sh # JSON handling
238 | │ │ ├── debug-helpers.sh # Logging/debugging
239 | │ │ └── gemini-wrapper.sh # Gemini API interface
240 | │ └── config/
241 | │ └── debug.conf # Configuration
242 | ├── test/
243 | │ ├── test-runner.sh # Automated tests
244 | │ ├── manual-test.sh # Interactive testing
245 | │ └── mock-tool-calls/ # Test data
246 | ├── docs/
247 | │ └── TROUBLESHOOTING.md # Debug guide
248 | └── install.sh # Installation script
249 | ```
250 |
251 | ## 🔍 Debugging Guidelines
252 |
253 | ### Debug Levels
254 |
255 | - **Level 0**: No debug output
256 | - **Level 1**: Basic information (default)
257 | - **Level 2**: Detailed information
258 | - **Level 3**: Full tracing
259 |
260 | ### Debugging Tools
261 |
262 | ```bash
263 | # Enable maximum debugging
264 | echo "DEBUG_LEVEL=3" >> hooks/config/debug.conf
265 |
266 | # Capture all inputs
267 | echo "CAPTURE_INPUTS=true" >> hooks/config/debug.conf
268 |
269 | # Test mode (no actual Gemini calls)
270 | echo "DRY_RUN=true" >> hooks/config/debug.conf
271 |
272 | # View logs in real-time
273 | tail -f logs/debug/$(date +%Y%m%d).log
274 | ```
275 |
276 | ### Common Debug Scenarios
277 |
278 | 1. **Hook not executing**: Check Claude settings and permissions
279 | 2. **Path conversion issues**: Test path-converter.sh directly
280 | 3. **Gemini API problems**: Verify CLI setup and credentials
281 | 4. **Cache problems**: Clear cache and check file permissions
282 |
283 | ## 🎯 Pull Request Guidelines
284 |
285 | ### PR Checklist
286 |
287 | - [ ] Tests pass (`./test/test-runner.sh`)
288 | - [ ] Code follows style guidelines
289 | - [ ] Documentation updated
290 | - [ ] CHANGELOG.md updated (if applicable)
291 | - [ ] Commit messages are descriptive
292 | - [ ] No unnecessary files included
293 |
294 | ### Commit Message Format
295 |
296 | ```
297 | type(scope): description
298 |
299 | Body explaining the change in detail.
300 |
301 | Fixes #123
302 | ```
303 |
304 | Types:
305 | - `feat`: New feature
306 | - `fix`: Bug fix
307 | - `docs`: Documentation changes
308 | - `style`: Code style changes
309 | - `refactor`: Code refactoring
310 | - `test`: Test changes
311 | - `chore`: Maintenance tasks
312 |
313 | Examples:
314 | ```
315 | feat(cache): add content-aware cache invalidation
316 |
317 | Implement cache key generation based on file contents and metadata
318 | to ensure cache invalidation when files are modified.
319 |
320 | Fixes #45
321 | ```
322 |
323 | ## 🏆 Recognition
324 |
325 | Contributors will be recognized in:
326 | - README.md acknowledgments
327 | - Release notes
328 | - GitHub contributor graphs
329 | - Optional Twitter mentions (@claude_bridge)
330 |
331 | ## 📞 Getting Help
332 |
333 | - **GitHub Issues**: For bugs and feature requests
334 | - **GitHub Discussions**: For questions and community chat
335 | - **Documentation**: Check TROUBLESHOOTING.md first
336 | - **Code Review**: Tag maintainers for review assistance
337 |
338 | ## 🎨 Code of Conduct
339 |
340 | We are committed to providing a welcoming and inclusive environment. Please:
341 |
342 | - Be respectful and constructive
343 | - Focus on the technical aspects
344 | - Help others learn and grow
345 | - Report any inappropriate behavior
346 |
347 | ## 📈 Release Process
348 |
349 | ### Versioning
350 |
351 | We use [Semantic Versioning](https://semver.org/):
352 | - `MAJOR.MINOR.PATCH`
353 | - MAJOR: Breaking changes
354 | - MINOR: New features
355 | - PATCH: Bug fixes
356 |
357 | ### Release Checklist
358 |
359 | 1. Update version numbers
360 | 2. Update CHANGELOG.md
361 | 3. Create release PR
362 | 4. Tag release after merge
363 | 5. Update installation documentation
364 |
365 | Thank you for contributing to the Claude-Gemini Bridge! 🎉
--------------------------------------------------------------------------------
/hooks/gemini-bridge.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ABOUTME: Main hook script for Claude-Gemini Bridge - intercepts tool calls and delegates to Gemini when appropriate
3 |
4 | # Determine script directory
5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6 |
7 | # Load configuration
8 | source "$SCRIPT_DIR/config/debug.conf"
9 |
10 | # Set dynamic paths that aren't set in config
11 | if [ -z "$CAPTURE_DIR" ]; then
12 | export CAPTURE_DIR="$SCRIPT_DIR/../debug/captured"
13 | fi
14 | source "$SCRIPT_DIR/lib/debug-helpers.sh"
15 | source "$SCRIPT_DIR/lib/path-converter.sh"
16 | source "$SCRIPT_DIR/lib/json-parser.sh"
17 | source "$SCRIPT_DIR/lib/gemini-wrapper.sh"
18 |
19 | # Initialize debug system
20 | init_debug "gemini-bridge" "$SCRIPT_DIR/../logs/debug"
21 |
22 | # Start performance measurement
23 | start_timer "hook_execution"
24 |
25 | debug_log 1 "Hook execution started"
26 | debug_system_info
27 |
28 | # Read tool call JSON from stdin
29 | TOOL_CALL_JSON=$(cat)
30 |
31 | # Check for empty input
32 | if [ -z "$TOOL_CALL_JSON" ]; then
33 | debug_log 1 "Empty input received, continuing with normal execution"
34 | create_hook_response "continue" "" "Empty input"
35 | exit 0
36 | fi
37 |
38 | debug_log 2 "Received tool call of size: $(echo "$TOOL_CALL_JSON" | wc -c) bytes"
39 |
40 | # Save input for later analysis
41 | if [ "$CAPTURE_INPUTS" = "true" ]; then
42 | CAPTURE_FILE=$(capture_input "$TOOL_CALL_JSON" "$CAPTURE_DIR")
43 | debug_log 1 "Input captured to: $CAPTURE_FILE"
44 | fi
45 |
46 | # Validate JSON
47 | if ! validate_json "$TOOL_CALL_JSON"; then
48 | error_log "Invalid JSON received from Claude"
49 | create_hook_response "continue" "" "Invalid JSON input"
50 | exit 1
51 | fi
52 |
53 | # Determine working directory
54 | WORKING_DIR=$(extract_working_directory "$TOOL_CALL_JSON")
55 | if [ -z "$WORKING_DIR" ]; then
56 | WORKING_DIR=$(pwd)
57 | debug_log 1 "No working_directory in context, using: $WORKING_DIR"
58 | else
59 | debug_log 2 "Working directory from context: $WORKING_DIR"
60 | fi
61 |
62 | # Extract tool type and parameters
63 | TOOL_NAME=$(extract_tool_name "$TOOL_CALL_JSON")
64 | TOOL_PARAMS=$(extract_parameters "$TOOL_CALL_JSON")
65 |
66 | debug_log 1 "Processing tool: $TOOL_NAME"
67 | debug_json "Tool parameters" "$TOOL_PARAMS"
68 |
69 | # Extract file paths based on tool type
70 | case "$TOOL_NAME" in
71 | "Read")
72 | FILE_PATH_RAW=$(extract_file_paths "$TOOL_PARAMS" "$TOOL_NAME")
73 | ABSOLUTE_PATH=$(convert_claude_paths "$FILE_PATH_RAW" "$WORKING_DIR")
74 | FILES="$ABSOLUTE_PATH"
75 | ORIGINAL_PROMPT="Read file: $FILE_PATH_RAW"
76 | ;;
77 | "Glob")
78 | PATTERN_RAW=$(extract_file_paths "$TOOL_PARAMS" "$TOOL_NAME")
79 | ABSOLUTE_PATTERN=$(convert_claude_paths "$PATTERN_RAW" "$WORKING_DIR")
80 | # Get search path from parameters
81 | SEARCH_PATH=$(echo "$TOOL_PARAMS" | jq -r '.path // empty')
82 | if [ -z "$SEARCH_PATH" ]; then
83 | SEARCH_PATH="$WORKING_DIR"
84 | fi
85 | # Expand glob pattern properly and safely
86 | cd "$SEARCH_PATH" 2>/dev/null || cd "$WORKING_DIR" 2>/dev/null || cd /tmp
87 | # Use find for safe glob expansion
88 | if [[ "$PATTERN_RAW" == "**/*"* ]]; then
89 | # Handle recursive patterns
90 | EXTENSION=$(echo "$PATTERN_RAW" | sed 's/.*\*\*\/\*\.\([^*]*\)$/\1/')
91 | if [ "$EXTENSION" != "$PATTERN_RAW" ]; then
92 | FILES=$(find . -name "*.${EXTENSION}" -type f 2>/dev/null | sed 's|^\./||' | head -$GEMINI_MAX_FILES)
93 | else
94 | FILES=$(find . -type f 2>/dev/null | sed 's|^\./||' | head -$GEMINI_MAX_FILES)
95 | fi
96 | else
97 | # Simple glob patterns
98 | FILES=$(ls $PATTERN_RAW 2>/dev/null | head -$GEMINI_MAX_FILES)
99 | fi
100 | # Convert to absolute paths
101 | ABSOLUTE_FILES=""
102 | for file in $FILES; do
103 | ABSOLUTE_FILES="$ABSOLUTE_FILES $(cd "$SEARCH_PATH" && pwd)/$file"
104 | done
105 | FILES="$ABSOLUTE_FILES"
106 | ORIGINAL_PROMPT="Find files matching: $PATTERN_RAW in $SEARCH_PATH"
107 | ;;
108 | "Grep")
109 | GREP_INFO=$(extract_file_paths "$TOOL_PARAMS" "$TOOL_NAME")
110 | GREP_PATH=$(echo "$GREP_INFO" | cut -d' ' -f1)
111 | ABSOLUTE_GREP_PATH=$(convert_claude_paths "$GREP_PATH" "$WORKING_DIR")
112 | # For Grep we use the search path as basis
113 | FILES="$ABSOLUTE_GREP_PATH"
114 | ORIGINAL_PROMPT="Search in: $GREP_INFO"
115 | ;;
116 | "Task")
117 | TASK_PROMPT=$(extract_task_prompt "$TOOL_PARAMS")
118 | CONVERTED_PROMPT=$(convert_claude_paths "$TASK_PROMPT" "$WORKING_DIR")
119 | # Extract file paths from prompt
120 | FILES=$(extract_files_from_text "$CONVERTED_PROMPT")
121 | ORIGINAL_PROMPT="$TASK_PROMPT"
122 | ;;
123 | *)
124 | debug_log 1 "Unknown tool type: $TOOL_NAME, continuing normally"
125 | create_hook_response "continue"
126 | exit 0
127 | ;;
128 | esac
129 |
130 | debug_vars "extracted" TOOL_NAME FILES WORKING_DIR ORIGINAL_PROMPT
131 |
132 | # Decision: Should Gemini be used? Based on Claude's 200k vs Gemini's 1M token limit
133 | should_delegate_to_gemini() {
134 | local tool="$1"
135 | local files="$2"
136 | local prompt="$3"
137 |
138 | # Dry-run mode - always delegate for tests
139 | if [ "$DRY_RUN" = "true" ]; then
140 | debug_log 1 "DRY_RUN mode: would delegate to Gemini"
141 | return 0
142 | fi
143 |
144 | # Calculate estimated token count (rough estimate: 4 chars = 1 token)
145 | local total_size=0
146 | local file_count=0
147 |
148 | if [ -n "$files" ]; then
149 | file_count=$(count_files "$files")
150 | for file in $files; do
151 | if [ -f "$file" ]; then
152 | local file_size=$(debug_file_size "$file")
153 | total_size=$((total_size + file_size))
154 | fi
155 | done
156 | fi
157 |
158 | # Rough token estimation: 4 characters ≈ 1 token
159 | local estimated_tokens=$((total_size / 4))
160 |
161 | debug_log 2 "File count: $file_count, Total size: $total_size bytes, Estimated tokens: $estimated_tokens"
162 |
163 | # Use configurable token limits
164 | local claude_token_limit=${CLAUDE_TOKEN_LIMIT:-50000}
165 | local gemini_token_limit=${GEMINI_TOKEN_LIMIT:-800000}
166 | local min_files_threshold=${MIN_FILES_FOR_GEMINI:-3}
167 | local max_total_size=${MAX_TOTAL_SIZE_FOR_GEMINI:-10485760}
168 |
169 | # Check if total size exceeds maximum limit
170 | if [ "$total_size" -gt "$max_total_size" ]; then
171 | debug_log 1 "Content too large ($total_size bytes > $max_total_size) - exceeds maximum size limit"
172 | return 1
173 | fi
174 |
175 | # If estimated tokens exceed Claude's comfortable limit, use Gemini
176 | if [ "$estimated_tokens" -gt "$claude_token_limit" ]; then
177 | if [ "$estimated_tokens" -le "$gemini_token_limit" ]; then
178 | debug_log 1 "Large content ($estimated_tokens tokens > $claude_token_limit) - delegating to Gemini"
179 | return 0
180 | else
181 | debug_log 1 "Content too large even for Gemini ($estimated_tokens tokens > $gemini_token_limit) - splitting needed"
182 | return 1
183 | fi
184 | fi
185 |
186 | # For smaller content, check if it's a multi-file analysis task that benefits from Gemini
187 | if [ "$file_count" -ge "$min_files_threshold" ] && [[ "$tool" == "Task" ]]; then
188 | debug_log 1 "Multi-file Task ($file_count files >= $min_files_threshold) - delegating to Gemini for better analysis"
189 | return 0
190 | fi
191 |
192 | # Check for excluded file patterns
193 | for file in $files; do
194 | local filename=$(basename "$file")
195 | if [[ "$filename" =~ $GEMINI_EXCLUDE_PATTERNS ]]; then
196 | debug_log 2 "Excluded file pattern detected: $filename"
197 | return 1
198 | fi
199 | done
200 |
201 | debug_log 2 "Content size manageable for Claude - no delegation needed"
202 | return 1
203 | }
204 |
205 | # Main decision
206 | if should_delegate_to_gemini "$TOOL_NAME" "$FILES" "$ORIGINAL_PROMPT"; then
207 | debug_log 1 "Delegating to Gemini for tool: $TOOL_NAME"
208 |
209 | # Initialize Gemini wrapper
210 | if ! init_gemini_wrapper; then
211 | error_log "Failed to initialize Gemini wrapper"
212 | create_hook_response "continue" "" "Gemini initialization failed"
213 | exit 1
214 | fi
215 |
216 | # Call Gemini
217 | start_timer "gemini_processing"
218 | GEMINI_RESULT=$(call_gemini "$TOOL_NAME" "$FILES" "$WORKING_DIR" "$ORIGINAL_PROMPT")
219 | GEMINI_EXIT_CODE=$?
220 | GEMINI_DURATION=$(end_timer "gemini_processing")
221 |
222 | if [ "$GEMINI_EXIT_CODE" -eq 0 ] && [ -n "$GEMINI_RESULT" ]; then
223 | # Successful Gemini response
224 | debug_log 1 "Gemini processing successful (${GEMINI_DURATION}s)"
225 |
226 | # Create structured response
227 | FILE_COUNT=$(count_files "$FILES")
228 | STRUCTURED_RESPONSE=$(create_gemini_response "$GEMINI_RESULT" "$TOOL_NAME" "$FILE_COUNT" "$GEMINI_DURATION")
229 |
230 | # Hook response with Gemini result
231 | create_hook_response "replace" "$STRUCTURED_RESPONSE"
232 | else
233 | # Gemini error - continue normally
234 | error_log "Gemini processing failed, continuing with normal tool execution"
235 | create_hook_response "continue" "" "Gemini processing failed"
236 | fi
237 | else
238 | # Continue normally without Gemini
239 | debug_log 1 "Continuing with normal tool execution"
240 | create_hook_response "continue"
241 | fi
242 |
243 | # End performance measurement
244 | TOTAL_DURATION=$(end_timer "hook_execution")
245 | debug_log 1 "Hook execution completed in ${TOTAL_DURATION}s"
246 |
247 | # Automatic cleanup
248 | if [ "$AUTO_CLEANUP_CACHE" = "true" ]; then
249 | # Only clean occasionally (about 1 in 10 times)
250 | if [ $((RANDOM % 10)) -eq 0 ]; then
251 | cleanup_gemini_cache "$CACHE_MAX_AGE_HOURS" &
252 | fi
253 | fi
254 |
255 | if [ "$AUTO_CLEANUP_LOGS" = "true" ]; then
256 | # Only clean occasionally (about 1 in 20 times)
257 | if [ $((RANDOM % 20)) -eq 0 ]; then
258 | cleanup_debug_files "$LOG_MAX_AGE_DAYS" &
259 | fi
260 | fi
261 |
262 | exit 0
--------------------------------------------------------------------------------
/hooks/lib/gemini-wrapper.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ABOUTME: Wrapper for Gemini CLI with caching and rate limiting
3 |
4 | # Configuration
5 | GEMINI_CACHE_DIR="${GEMINI_CACHE_DIR:-${CLAUDE_GEMINI_BRIDGE_DIR:-$HOME/.claude-gemini-bridge}/cache/gemini}"
6 | GEMINI_CACHE_TTL="${GEMINI_CACHE_TTL:-3600}" # 1 hour
7 | GEMINI_TIMEOUT="${GEMINI_TIMEOUT:-30}" # 30 seconds
8 | GEMINI_RATE_LIMIT="${GEMINI_RATE_LIMIT:-1}" # 1 second between calls
9 | GEMINI_MAX_FILES="${GEMINI_MAX_FILES:-20}" # Max 20 files per call
10 |
11 | # Rate limiting file
12 | RATE_LIMIT_FILE="/tmp/claude_bridge_gemini_last_call"
13 |
14 | # Initialize Gemini wrapper
15 | init_gemini_wrapper() {
16 | mkdir -p "$GEMINI_CACHE_DIR"
17 |
18 | # Test if Gemini is available
19 | if ! command -v gemini >/dev/null 2>&1; then
20 | error_log "Gemini CLI not found in PATH"
21 | return 1
22 | fi
23 |
24 | debug_log 1 "Gemini wrapper initialized"
25 | debug_log 2 "Cache dir: $GEMINI_CACHE_DIR"
26 | debug_log 2 "Cache TTL: $GEMINI_CACHE_TTL seconds"
27 |
28 | return 0
29 | }
30 |
31 | # Implement rate limiting
32 | enforce_rate_limit() {
33 | if [ -f "$RATE_LIMIT_FILE" ]; then
34 | local last_call=$(cat "$RATE_LIMIT_FILE" 2>/dev/null || echo "0")
35 | local current_time=$(date +%s)
36 | local time_diff=$((current_time - last_call))
37 |
38 | if [ "$time_diff" -lt "$GEMINI_RATE_LIMIT" ]; then
39 | local sleep_time=$((GEMINI_RATE_LIMIT - time_diff))
40 | debug_log 2 "Rate limiting: sleeping ${sleep_time}s"
41 | sleep "$sleep_time"
42 | fi
43 | fi
44 |
45 | # Save current time
46 | date +%s > "$RATE_LIMIT_FILE"
47 | }
48 |
49 | # Generate cache key from input
50 | generate_cache_key() {
51 | local prompt="$1"
52 | local files="$2"
53 | local working_dir="$3"
54 |
55 | # Create hash from file contents + metadata
56 | local content_hash=""
57 | local file_array=($files)
58 |
59 | for file in "${file_array[@]}"; do
60 | if [ -f "$file" ] && [ -r "$file" ]; then
61 | # Combine filename, size, modification time and first 1KB of content
62 | local file_info=$(stat -f "%N|%z|%m" "$file" 2>/dev/null || stat -c "%n|%s|%Y" "$file" 2>/dev/null)
63 | local file_sample=$(head -c 1024 "$file" 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
64 | content_hash="${content_hash}${file_info}|${file_sample}|"
65 | fi
66 | done
67 |
68 | # SHA256 hash from all parameters + file contents
69 | local input_string="$prompt|$files|$working_dir|$content_hash"
70 | echo "$input_string" | shasum -a 256 | cut -d' ' -f1
71 | }
72 |
73 | # Check if cache entry is still valid
74 | is_cache_valid() {
75 | local cache_file="$1"
76 |
77 | if [ ! -f "$cache_file" ]; then
78 | return 1
79 | fi
80 |
81 | local cache_age=$(( $(date +%s) - $(stat -f %m "$cache_file" 2>/dev/null || stat -c %Y "$cache_file") ))
82 |
83 | if [ "$cache_age" -lt "$GEMINI_CACHE_TTL" ]; then
84 | debug_log 2 "Cache hit: age ${cache_age}s (TTL: ${GEMINI_CACHE_TTL}s)"
85 | return 0
86 | else
87 | debug_log 2 "Cache expired: age ${cache_age}s"
88 | return 1
89 | fi
90 | }
91 |
92 | # Create Gemini prompt based on tool type
93 | create_gemini_prompt() {
94 | local tool_type="$1"
95 | local original_prompt="$2"
96 | local file_count="$3"
97 |
98 | case "$tool_type" in
99 | "Read")
100 | echo "You are assisting another LLM with file analysis. The user requested to read this file, but it's large enough that I'm helping out. Please analyze this file and provide a concise summary focusing on purpose, main functions, and important details:"
101 | ;;
102 | "Glob"|"Grep")
103 | echo "You are assisting another LLM with multi-file analysis. The user is searching across $file_count files. Please analyze these files and create a structured overview, grouping similar files and explaining the purpose of each group:"
104 | ;;
105 | "Task")
106 | if [[ "$original_prompt" =~ (search|find|suche|finde) ]]; then
107 | echo "You are assisting another LLM with a complex search task. The original request was: $original_prompt
108 |
109 | Please search the provided files for the specified criteria and provide a structured list of findings with context:"
110 | elif [[ "$original_prompt" =~ (analyze|analysiere|verstehe) ]]; then
111 | echo "You are assisting another LLM with a complex analysis task. The original request was: $original_prompt
112 |
113 | Please perform a detailed analysis of the provided files:"
114 | else
115 | echo "You are assisting another LLM with a complex analysis task. The original request was: $original_prompt
116 |
117 | Please process this task with the provided files and give a comprehensive response:"
118 | fi
119 | ;;
120 | *)
121 | echo "You are assisting another LLM with file analysis. Please analyze the provided files and provide a helpful summary."
122 | ;;
123 | esac
124 | }
125 |
126 | # Prepare files for Gemini
127 | prepare_files_for_gemini() {
128 | local files="$1"
129 | local working_dir="$2"
130 | local processed_files=""
131 | local file_count=0
132 |
133 | # Convert to array
134 | local file_array=($files)
135 |
136 | for file in "${file_array[@]}"; do
137 | # Skip if too many files
138 | if [ "$file_count" -ge "$GEMINI_MAX_FILES" ]; then
139 | debug_log 1 "File limit reached: $GEMINI_MAX_FILES"
140 | break
141 | fi
142 |
143 | # Make path absolute if necessary
144 | if [[ "$file" != /* ]]; then
145 | file="$working_dir/$file"
146 | fi
147 |
148 | # Check if file exists and is readable
149 | if [ -f "$file" ] && [ -r "$file" ]; then
150 | # Check file size (max 1MB per file)
151 | local file_size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null || echo "0")
152 | if [ "$file_size" -lt 1048576 ]; then
153 | processed_files="$processed_files $file"
154 | file_count=$((file_count + 1))
155 | debug_log 3 "Added file: $file (${file_size} bytes)"
156 | else
157 | debug_log 2 "Skipping large file: $file (${file_size} bytes)"
158 | fi
159 | else
160 | debug_log 2 "Skipping non-existent/unreadable file: $file"
161 | fi
162 | done
163 |
164 | echo "$processed_files"
165 | }
166 |
167 | # Main function: Call Gemini with caching
168 | call_gemini() {
169 | local tool_type="$1"
170 | local files="$2"
171 | local working_dir="$3"
172 | local original_prompt="$4"
173 |
174 | debug_log 1 "Calling Gemini for tool: $tool_type"
175 | start_timer "gemini_call"
176 |
177 | # Generate cache key
178 | local cache_key=$(generate_cache_key "$tool_type|$original_prompt" "$files" "$working_dir")
179 | local cache_file="$GEMINI_CACHE_DIR/$cache_key"
180 |
181 | # Check cache
182 | if is_cache_valid "$cache_file"; then
183 | debug_log 1 "Using cached result"
184 | cat "$cache_file"
185 | end_timer "gemini_call" >/dev/null
186 | return 0
187 | fi
188 |
189 | # Prepare files
190 | local processed_files=$(prepare_files_for_gemini "$files" "$working_dir")
191 | local file_count=$(echo "$processed_files" | wc -w | tr -d ' ')
192 |
193 | if [ "$file_count" -eq 0 ]; then
194 | debug_log 1 "No valid files found for Gemini"
195 | echo "No valid files found for analysis."
196 | end_timer "gemini_call" >/dev/null
197 | return 1
198 | fi
199 |
200 | # Create prompt
201 | local gemini_prompt=$(create_gemini_prompt "$tool_type" "$original_prompt" "$file_count")
202 |
203 | debug_log 2 "Processing $file_count files with Gemini"
204 | debug_log 3 "Prompt: $gemini_prompt"
205 |
206 | # Rate limiting
207 | enforce_rate_limit
208 |
209 | # Call Gemini
210 | local gemini_result=""
211 | local gemini_exit_code=0
212 |
213 | # Timeout with GNU timeout or gtimeout (macOS)
214 | local timeout_cmd="timeout"
215 | if command -v gtimeout >/dev/null 2>&1; then
216 | timeout_cmd="gtimeout"
217 | fi
218 |
219 | # Prepare file contents for STDIN
220 | local file_contents=""
221 | for file in $processed_files; do
222 | if [ -f "$file" ]; then
223 | file_contents="${file_contents}=== File: $file ===\n\n"
224 | file_contents="${file_contents}$(cat "$file" 2>/dev/null)\n\n"
225 | fi
226 | done
227 |
228 | # Debug: Show exact command being executed
229 | debug_log 3 "Executing: echo [file contents] | gemini -p \"$gemini_prompt\""
230 |
231 | if command -v "$timeout_cmd" >/dev/null 2>&1; then
232 | gemini_result=$(echo -e "$file_contents" | "$timeout_cmd" "$GEMINI_TIMEOUT" gemini -p "$gemini_prompt" 2>&1)
233 | gemini_exit_code=$?
234 | else
235 | # Fallback without timeout
236 | gemini_result=$(echo -e "$file_contents" | gemini -p "$gemini_prompt" 2>&1)
237 | gemini_exit_code=$?
238 | fi
239 |
240 | local duration=$(end_timer "gemini_call")
241 |
242 | # Check result
243 | if [ "$gemini_exit_code" -eq 0 ] && [ -n "$gemini_result" ]; then
244 | # Cache successful response
245 | echo "$gemini_result" > "$cache_file"
246 | debug_log 1 "Gemini call successful (${duration}s, $file_count files)"
247 | echo "$gemini_result"
248 | return 0
249 | else
250 | error_log "Gemini call failed (exit code: $gemini_exit_code)"
251 | debug_log 2 "Gemini error output: $gemini_result"
252 | echo "Gemini analysis failed. Please check the logs."
253 | return 1
254 | fi
255 | }
256 |
257 | # Clean up old cache
258 | cleanup_gemini_cache() {
259 | local max_age_hours=${1:-24} # Default: 24 hours
260 |
261 | debug_log 2 "Cleaning up Gemini cache older than $max_age_hours hours"
262 |
263 | find "$GEMINI_CACHE_DIR" -type f -mtime +$(echo "$max_age_hours/24" | bc) -delete 2>/dev/null
264 |
265 | # Cache statistics
266 | local cache_files=$(find "$GEMINI_CACHE_DIR" -type f | wc -l | tr -d ' ')
267 | local cache_size=$(du -sh "$GEMINI_CACHE_DIR" 2>/dev/null | cut -f1)
268 |
269 | debug_log 1 "Cache stats: $cache_files files, $cache_size total size"
270 | }
271 |
272 | # Test function for Gemini wrapper
273 | test_gemini_wrapper() {
274 | echo "Testing Gemini wrapper..."
275 | local failed=0
276 |
277 | # Test 1: Initialization
278 | if ! init_gemini_wrapper; then
279 | echo "❌ Test 1 failed: Gemini wrapper initialization"
280 | failed=1
281 | else
282 | echo "✅ Test 1 passed: Gemini wrapper initialization"
283 | fi
284 |
285 | # Test 2: Cache-Key Generierung
286 | local key1=$(generate_cache_key "test" "file1.txt" "/tmp")
287 | local key2=$(generate_cache_key "test" "file1.txt" "/tmp")
288 | local key3=$(generate_cache_key "test2" "file1.txt" "/tmp")
289 |
290 | if [ "$key1" != "$key2" ]; then
291 | echo "❌ Test 2a failed: Cache keys should be identical"
292 | failed=1
293 | elif [ "$key1" = "$key3" ]; then
294 | echo "❌ Test 2b failed: Cache keys should be different"
295 | failed=1
296 | else
297 | echo "✅ Test 2 passed: Cache key generation"
298 | fi
299 |
300 | # Test 3: Prompt creation
301 | local prompt=$(create_gemini_prompt "Read" "analyze this file" 1)
302 | local prompt_lower=$(echo "$prompt" | tr '[:upper:]' '[:lower:]')
303 | if [[ "$prompt_lower" != *"analyze"* ]]; then
304 | echo "❌ Test 3 failed: Prompt creation"
305 | failed=1
306 | else
307 | echo "✅ Test 3 passed: Prompt creation"
308 | fi
309 |
310 | # Test 4: Rate limiting (simulated)
311 | echo $(date +%s) > "$RATE_LIMIT_FILE"
312 | local start_time=$(date +%s)
313 | enforce_rate_limit
314 | local end_time=$(date +%s)
315 | local time_diff=$((end_time - start_time))
316 |
317 | if [ "$time_diff" -ge 1 ]; then
318 | echo "✅ Test 4 passed: Rate limiting works"
319 | else
320 | echo "✅ Test 4 passed: Rate limiting (no delay needed)"
321 | fi
322 |
323 | # Cleanup
324 | rm -f "$RATE_LIMIT_FILE"
325 |
326 | if [ $failed -eq 0 ]; then
327 | echo "🎉 All Gemini wrapper tests passed!"
328 | return 0
329 | else
330 | echo "💥 Some tests failed!"
331 | return 1
332 | fi
333 | }
334 |
335 | # If script is called directly, run tests
336 | if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
337 | # Initialize debug system for tests
338 | if [ -f "$(dirname "$0")/debug-helpers.sh" ]; then
339 | source "$(dirname "$0")/debug-helpers.sh"
340 | init_debug "gemini-wrapper-test"
341 | fi
342 |
343 | test_gemini_wrapper
344 | fi
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ABOUTME: Simplified installer for Claude-Gemini Bridge that works in current directory
3 |
4 | echo "🚀 Claude-Gemini Bridge Installer"
5 | echo "=================================="
6 | echo ""
7 |
8 | # Colors for output
9 | GREEN='\033[0;32m'
10 | RED='\033[0;31m'
11 | YELLOW='\033[1;33m'
12 | BLUE='\033[0;34m'
13 | NC='\033[0m'
14 |
15 | # Global variables
16 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17 | CLAUDE_SETTINGS_FILE="$HOME/.claude/settings.json"
18 | BACKUP_SUFFIX=$(date +%Y%m%d_%H%M%S)
19 |
20 | # Log function
21 | log() {
22 | local level="$1"
23 | local message="$2"
24 |
25 | case $level in
26 | "info") echo -e "${GREEN}✅${NC} $message" ;;
27 | "warn") echo -e "${YELLOW}⚠️${NC} $message" ;;
28 | "error") echo -e "${RED}❌${NC} $message" ;;
29 | "debug") echo -e "${BLUE}🔍${NC} $message" ;;
30 | esac
31 | }
32 |
33 | # Error handling
34 | error_exit() {
35 | log "error" "$1"
36 | echo ""
37 | echo "💥 Installation aborted!"
38 | echo "For help see: $SCRIPT_DIR/docs/TROUBLESHOOTING.md"
39 | exit 1
40 | }
41 |
42 | # Check prerequisites
43 | check_requirements() {
44 | log "info" "Checking prerequisites..."
45 |
46 | # Claude CLI
47 | if ! command -v claude &> /dev/null; then
48 | error_exit "Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"
49 | fi
50 | log "debug" "Claude CLI found: $(which claude)"
51 |
52 | # Gemini CLI
53 | if ! command -v gemini &> /dev/null; then
54 | error_exit "Gemini CLI not found. Visit: https://github.com/google-gemini/gemini-cli"
55 | fi
56 | log "debug" "Gemini CLI found: $(which gemini)"
57 |
58 | # jq for JSON processing
59 | if ! command -v jq &> /dev/null; then
60 | log "warn" "jq not found. Install with:"
61 | echo " macOS: brew install jq"
62 | echo " Linux: sudo apt-get install jq"
63 | error_exit "jq is required for JSON processing"
64 | fi
65 | log "debug" "jq found: $(which jq)"
66 |
67 | log "info" "All prerequisites met"
68 | }
69 |
70 | # Create directory structure if needed
71 | create_directories() {
72 | log "info" "Creating directory structure..."
73 |
74 | mkdir -p "$SCRIPT_DIR"/{cache/gemini,logs/debug,debug/captured}
75 |
76 | if [ $? -eq 0 ]; then
77 | log "info" "Directory structure ready"
78 | else
79 | error_exit "Error creating directory structure"
80 | fi
81 | }
82 |
83 | # Test Gemini connection
84 | test_gemini_connection() {
85 | log "info" "Testing Gemini connection..."
86 |
87 | # Simple test call - just check if Gemini CLI works at all
88 | local test_result=$(echo "1+1" | gemini -p "What is the result?" 2>&1)
89 | local exit_code=$?
90 |
91 | # Only check exit code and that we got SOME response
92 | if [ $exit_code -eq 0 ] && [ -n "$test_result" ] && [ ${#test_result} -gt 0 ]; then
93 | log "info" "Gemini connection tested successfully"
94 | log "debug" "Gemini CLI is working and responding"
95 | else
96 | log "warn" "Gemini test failed. API key configured?"
97 | log "debug" "Gemini exit code: $exit_code"
98 | log "debug" "Gemini output: $test_result"
99 | echo ""
100 | echo "Common issues:"
101 | echo " - Missing API key: export GEMINI_API_KEY=your_key"
102 | echo " - Authentication problem with Gemini CLI"
103 | echo " - Network connectivity issues"
104 | echo ""
105 | read -p "Continue anyway? (y/N): " continue_anyway
106 | if [[ ! "$continue_anyway" =~ ^[Yy]$ ]]; then
107 | error_exit "Gemini configuration required. See Gemini CLI documentation."
108 | fi
109 | fi
110 | }
111 |
112 | # Intelligent hook merging for settings.json
113 | configure_claude_hooks() {
114 | log "info" "Configuring Claude Code Hooks..."
115 |
116 | # Create .claude directory if not exists
117 | mkdir -p "$(dirname "$CLAUDE_SETTINGS_FILE")"
118 |
119 | # Backup existing settings
120 | if [ -f "$CLAUDE_SETTINGS_FILE" ]; then
121 | cp "$CLAUDE_SETTINGS_FILE" "${CLAUDE_SETTINGS_FILE}.backup.${BACKUP_SUFFIX}"
122 | log "info" "Backup created: ${CLAUDE_SETTINGS_FILE}.backup.${BACKUP_SUFFIX}"
123 | fi
124 |
125 | # Our hook configuration
126 | local hook_command="$SCRIPT_DIR/hooks/gemini-bridge.sh"
127 |
128 | # Ask user which tools to intercept
129 | echo ""
130 | echo "Which tools should delegate to Gemini?"
131 | echo "Available options:"
132 | echo " Read - File reading operations"
133 | echo " Grep - Search operations"
134 | echo " Glob - File pattern matching"
135 | echo " Task - Complex analysis tasks (recommended)"
136 | echo ""
137 | echo "Enter tools separated by '|' (e.g., 'Task|Grep' or 'Read|Grep|Glob|Task')"
138 | read -p "Tools to intercept [Read|Grep|Glob|Task]: " user_tools
139 |
140 | local hook_matcher="${user_tools:-Read|Grep|Glob|Task}"
141 | log "info" "Configuring hooks for tools: $hook_matcher"
142 |
143 | # Check for any existing Claude-Gemini Bridge installation
144 | if [ -f "$CLAUDE_SETTINGS_FILE" ]; then
145 | if grep -q "gemini-bridge.sh" "$CLAUDE_SETTINGS_FILE" 2>/dev/null; then
146 | log "warn" "Existing Claude-Gemini Bridge installation detected!"
147 |
148 | # Show current hook path
149 | local current_path=$(grep -o '[^"]*gemini-bridge.sh' "$CLAUDE_SETTINGS_FILE" 2>/dev/null | head -1)
150 | if [ -n "$current_path" ]; then
151 | log "debug" "Current hook path: $current_path"
152 | log "debug" "New hook path: $hook_command"
153 | fi
154 |
155 | # Ask user what to do
156 | echo ""
157 | echo "Options:"
158 | echo "1) Update hook path to current location (recommended)"
159 | echo "2) Remove old hook and add new one"
160 | echo "3) Cancel installation"
161 | echo ""
162 | read -p "Choose option (1-3): " update_choice
163 |
164 | case $update_choice in
165 | 1)
166 | log "info" "Updating hook path to current location..."
167 | update_existing_hook "$hook_command" "$hook_matcher"
168 | return 0
169 | ;;
170 | 2)
171 | log "info" "Removing old hook and installing new one..."
172 | remove_existing_hooks
173 | # Continue with normal installation below
174 | ;;
175 | 3|*)
176 | log "info" "Installation cancelled"
177 | exit 0
178 | ;;
179 | esac
180 | fi
181 | fi
182 |
183 | # Merge with existing configuration or create new
184 | if [ -f "$CLAUDE_SETTINGS_FILE" ]; then
185 | log "debug" "Merging with existing settings..."
186 |
187 | # Add our hook to existing PreToolUse array
188 | local merged_config=$(jq --arg cmd "$hook_command" --arg matcher "$hook_matcher" '
189 | .hooks.PreToolUse = (.hooks.PreToolUse // []) + [{
190 | "matcher": $matcher,
191 | "hooks": [{
192 | "type": "command",
193 | "command": $cmd
194 | }]
195 | }]' "$CLAUDE_SETTINGS_FILE" 2>/dev/null)
196 |
197 | if [ $? -eq 0 ] && [ -n "$merged_config" ]; then
198 | echo "$merged_config" > "$CLAUDE_SETTINGS_FILE"
199 | log "info" "Hook added to existing settings"
200 | else
201 | log "warn" "Error merging configuration. Creating new settings file."
202 | create_new_settings_file "$hook_command" "$hook_matcher"
203 | fi
204 | else
205 | log "debug" "Creating new settings file..."
206 | create_new_settings_file "$hook_command" "$hook_matcher"
207 | fi
208 |
209 | log "debug" "Hook configured: $hook_command"
210 | }
211 |
212 | # Update existing hook path
213 | update_existing_hook() {
214 | local hook_command="$1"
215 | local hook_matcher="$2"
216 |
217 | local updated_config=$(jq --arg cmd "$hook_command" --arg matcher "$hook_matcher" '
218 | .hooks.PreToolUse = (.hooks.PreToolUse // []) |
219 | .hooks.PreToolUse |= map(
220 | if .hooks[]?.command? and (.hooks[]?.command | contains("gemini-bridge.sh"))
221 | then (.hooks[0].command = $cmd | .matcher = $matcher)
222 | else . end
223 | )' "$CLAUDE_SETTINGS_FILE" 2>/dev/null)
224 |
225 | if [ $? -eq 0 ] && [ -n "$updated_config" ]; then
226 | echo "$updated_config" > "$CLAUDE_SETTINGS_FILE"
227 | log "info" "Hook path updated successfully"
228 | else
229 | log "error" "Failed to update hook path"
230 | exit 1
231 | fi
232 | }
233 |
234 | # Remove existing gemini-bridge hooks
235 | remove_existing_hooks() {
236 | log "debug" "Removing existing Claude-Gemini Bridge hooks..."
237 |
238 | local cleaned_config=$(jq '
239 | .hooks.PreToolUse = (.hooks.PreToolUse // []) |
240 | .hooks.PreToolUse |= map(
241 | select(.hooks[]?.command? and (.hooks[]?.command | contains("gemini-bridge.sh")) | not)
242 | ) |
243 | if (.hooks.PreToolUse | length) == 0 then
244 | del(.hooks.PreToolUse)
245 | else . end |
246 | if (.hooks | length) == 0 then
247 | del(.hooks)
248 | else . end
249 | ' "$CLAUDE_SETTINGS_FILE" 2>/dev/null)
250 |
251 | if [ $? -eq 0 ] && [ -n "$cleaned_config" ]; then
252 | echo "$cleaned_config" > "$CLAUDE_SETTINGS_FILE"
253 | log "info" "Existing hooks removed"
254 | else
255 | log "warn" "Could not remove existing hooks automatically"
256 | fi
257 | }
258 |
259 | # Create new settings file
260 | create_new_settings_file() {
261 | local hook_command="$1"
262 | local hook_matcher="$2"
263 |
264 | cat > "$CLAUDE_SETTINGS_FILE" << EOF
265 | {
266 | "hooks": {
267 | "PreToolUse": [
268 | {
269 | "matcher": "$hook_matcher",
270 | "hooks": [
271 | {
272 | "type": "command",
273 | "command": "$hook_command"
274 | }
275 | ]
276 | }
277 | ]
278 | }
279 | }
280 | EOF
281 | log "info" "New Claude settings created"
282 | }
283 |
284 | # Set permissions
285 | set_permissions() {
286 | log "info" "Setting file permissions..."
287 |
288 | # Make all shell scripts executable
289 | find "$SCRIPT_DIR" -name "*.sh" -exec chmod +x {} \;
290 |
291 | log "info" "Permissions set"
292 | }
293 |
294 | # Run basic tests
295 | run_basic_tests() {
296 | log "info" "Running basic tests..."
297 |
298 | # Test library functions
299 | local lib_tests=("path-converter.sh" "json-parser.sh" "debug-helpers.sh" "gemini-wrapper.sh")
300 | local test_failures=0
301 |
302 | for test in "${lib_tests[@]}"; do
303 | if [ -f "$SCRIPT_DIR/hooks/lib/$test" ]; then
304 | if "$SCRIPT_DIR/hooks/lib/$test" >/dev/null 2>&1; then
305 | log "debug" "$test: OK"
306 | else
307 | log "warn" "$test: Tests failed"
308 | test_failures=$((test_failures + 1))
309 | fi
310 | else
311 | log "warn" "File not found: $SCRIPT_DIR/hooks/lib/$test"
312 | test_failures=$((test_failures + 1))
313 | fi
314 | done
315 |
316 | # Test hook script with mock input
317 | local test_json='{"tool_name":"Read","tool_input":{"file_path":"test.txt"},"session_id":"test","transcript_path":"/tmp/test"}'
318 | local hook_result=$(echo "$test_json" | "$SCRIPT_DIR/hooks/gemini-bridge.sh" 2>/dev/null)
319 |
320 | if echo "$hook_result" | jq empty 2>/dev/null; then
321 | log "info" "Hook script test successful"
322 | else
323 | log "warn" "Hook script test failed, but installation continued"
324 | log "debug" "Hook output: $hook_result"
325 | test_failures=$((test_failures + 1))
326 | fi
327 |
328 | if [ $test_failures -eq 0 ]; then
329 | log "info" "All tests passed"
330 | else
331 | log "warn" "$test_failures test(s) failed - installation may need troubleshooting"
332 | fi
333 | }
334 |
335 | # Show installation summary
336 | show_summary() {
337 | echo ""
338 | echo "🎉 Installation completed successfully!"
339 | echo "======================================="
340 | echo ""
341 | echo "📁 Installation Directory: $SCRIPT_DIR"
342 | echo "⚙️ Claude Settings: $CLAUDE_SETTINGS_FILE"
343 | echo ""
344 | echo "🧪 Next steps:"
345 | echo ""
346 | echo " 1. **RESTART Claude Code** (hooks are loaded at startup)"
347 | echo " Exit Claude Code completely and restart it"
348 | echo ""
349 | echo " 2. Test the installation:"
350 | echo " $SCRIPT_DIR/test/test-runner.sh"
351 | echo ""
352 | echo " 3. Use Claude Code normally:"
353 | echo " Large file analyses will automatically use Gemini!"
354 | echo ""
355 | echo "📚 Documentation:"
356 | echo " - README: $SCRIPT_DIR/README.md"
357 | echo " - Troubleshooting: $SCRIPT_DIR/docs/TROUBLESHOOTING.md"
358 | echo ""
359 | echo "🔧 Configuration:"
360 | echo " - Debug level: $SCRIPT_DIR/hooks/config/debug.conf"
361 | echo " - Logs: $SCRIPT_DIR/logs/debug/"
362 | echo ""
363 | echo "💡 Debug commands:"
364 | echo " - View logs: tail -f $SCRIPT_DIR/logs/debug/\$(date +%Y%m%d).log"
365 | echo " - Clear cache: rm -rf $SCRIPT_DIR/cache/gemini/*"
366 | echo " - Uninstall: $SCRIPT_DIR/uninstall.sh"
367 | echo ""
368 | echo "🚨 IMPORTANT: You must restart Claude Code for the hooks to take effect!"
369 | }
370 |
371 | # Main installation
372 | main() {
373 | echo "This script configures the Claude-Gemini Bridge in the current directory."
374 | echo "Installation directory: $SCRIPT_DIR"
375 | echo ""
376 | read -p "Continue with installation? (y/N): " confirm
377 |
378 | if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
379 | log "info" "Installation cancelled"
380 | exit 0
381 | fi
382 |
383 | echo ""
384 |
385 | # Installation steps
386 | check_requirements
387 | create_directories
388 | test_gemini_connection
389 | configure_claude_hooks
390 | set_permissions
391 | run_basic_tests
392 |
393 | show_summary
394 | }
395 |
396 | # Execute script
397 | main "$@"
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Claude-Gemini Bridge
2 |
3 | 🤖 **Intelligent integration between Claude Code and Google Gemini for large-scale code analysis**
4 |
5 | The Claude-Gemini Bridge automatically delegates complex code analysis tasks from Claude Code to Google Gemini, combining Claude's reasoning capabilities with Gemini's large context processing power.
6 |
7 | [](#testing)
8 | [](#license)
9 | [](#requirements)
10 |
11 | ## 🚀 Quick Start
12 |
13 | ```bash
14 | # Clone and install for all projects in one simple process
15 | git clone https://github.com/your-username/claude-gemini-bridge.git
16 | cd claude-gemini-bridge
17 | ./install.sh
18 |
19 | # IMPORTANT: Restart Claude Code to load the new hooks
20 | # (Hooks are only loaded once at startup)
21 |
22 | # Test the installation
23 | ./test/test-runner.sh
24 |
25 | # Use Claude Code normally - large analyses will automatically use Gemini!
26 | claude "analyze all Python files in this project"
27 | ```
28 |
29 | ### Installation Details
30 |
31 | The installer:
32 | - ✅ Works in the current directory (no separate installation location)
33 | - ✅ Automatically merges with existing Claude hooks in `~/.claude/settings.json`
34 | - ✅ Creates backups before making changes
35 | - ✅ Tests all components during installation
36 | - ✅ Provides uninstallation via `./uninstall.sh`
37 |
38 | ## 📋 Table of Contents
39 |
40 | - [Architecture](#-architecture)
41 | - [How It Works](#-how-it-works)
42 | - [Installation](#-installation)
43 | - [Configuration](#-configuration)
44 | - [Usage Examples](#-usage-examples)
45 | - [Testing](#-testing)
46 | - [Troubleshooting](#-troubleshooting)
47 | - [Contributing](#-contributing)
48 |
49 | ## 🏗️ Architecture
50 |
51 | ```mermaid
52 | graph TB
53 | subgraph "Claude Code"
54 | CC[Claude Code CLI]
55 | TC[Tool Call]
56 | end
57 |
58 | subgraph "Claude-Gemini Bridge"
59 | HS[Hook System]
60 | DE[Decision Engine]
61 | PC[Path Converter]
62 | CH[Cache Layer]
63 | end
64 |
65 | subgraph "External APIs"
66 | GC[Gemini CLI]
67 | GA[Gemini API]
68 | end
69 |
70 | CC -->|PreToolUse Hook| HS
71 | HS --> PC
72 | PC --> DE
73 | DE -->|Delegate?| CH
74 | CH -->|Yes| GC
75 | GC --> GA
76 | GA -->|Analysis| CH
77 | CH -->|Response| HS
78 | HS -->|Result| CC
79 | DE -->|No| CC
80 |
81 | style CC fill:#e1f5fe
82 | style HS fill:#f3e5f5
83 | style GC fill:#e8f5e8
84 | style DE fill:#fff3e0
85 | ```
86 |
87 | ## 🔄 How It Works
88 |
89 | The bridge operates through Claude Code's hook system, intelligently deciding when to delegate tasks to Gemini:
90 |
91 | ```mermaid
92 | sequenceDiagram
93 | participant User
94 | participant Claude as Claude Code
95 | participant Bridge as Gemini Bridge
96 | participant Gemini as Gemini API
97 |
98 | User->>Claude: "Analyze these 20 Python files"
99 | Claude->>Bridge: PreToolUse Hook (Glob *.py)
100 |
101 | Bridge->>Bridge: Convert @ paths to absolute
102 | Bridge->>Bridge: Count files (20 files)
103 | Bridge->>Bridge: Calculate total size (500KB)
104 | Bridge->>Bridge: Estimate tokens (~125k)
105 |
106 | alt Token limit exceeded (>50k)
107 | Bridge->>Bridge: Check if within Gemini limits (<800k)
108 | Bridge->>Gemini: Analyze 20 files with context
109 | Gemini->>Bridge: Structured analysis result
110 | Bridge->>Claude: Replace tool call with Gemini result
111 | Claude->>User: Comprehensive analysis from Gemini
112 | else Multi-file Task (≥3 files)
113 | Bridge->>Bridge: Check if Task operation
114 | Bridge->>Gemini: Delegate multi-file analysis
115 | Gemini->>Bridge: Analysis result
116 | Bridge->>Claude: Replace with Gemini result
117 | Claude->>User: Multi-file analysis from Gemini
118 | else Small content (<50k tokens, <3 files)
119 | Bridge->>Claude: Continue with normal execution
120 | Claude->>Claude: Process files normally
121 | Claude->>User: Standard Claude response
122 | end
123 | ```
124 |
125 | ### Delegation Criteria
126 |
127 | The bridge delegates to Gemini when:
128 |
129 | - **Token Limit**: Content exceeds ~50k tokens (~200KB, optimized for Claude's 200k context)
130 | - **Multi-File Tasks**: ≥3 files for Task operations (configurable)
131 | - **Safety Limits**: Content must be ≤10MB and ≤800k tokens for Gemini processing
132 | - **File Exclusions**: Automatically excludes sensitive files (*.secret, *.key, *.env, etc.)
133 |
134 | ## 📦 Installation
135 |
136 | ### Prerequisites
137 |
138 | - [Claude Code CLI](https://claude.ai/code) installed and configured
139 | - [Google Gemini CLI](https://github.com/google/generative-ai-cli) installed
140 | - `jq` for JSON processing
141 | - `bash` 4.0+
142 |
143 | ### Installation Options
144 |
145 | The Claude-Gemini Bridge supports two deployment models:
146 |
147 | #### 🌍 Global Installation (Recommended)
148 |
149 | For system-wide use across all projects:
150 |
151 | ```bash
152 | # Clone to a permanent location (one-time setup)
153 | git clone https://github.com/your-username/claude-gemini-bridge.git ~/claude-gemini-bridge
154 | cd ~/claude-gemini-bridge
155 | ./install.sh
156 |
157 | # IMPORTANT: Restart Claude Code after installation!
158 | ```
159 |
160 | **Benefits:**
161 | - ✅ Works with all projects automatically
162 | - ✅ Single bridge installation to maintain
163 | - ✅ Easier updates via git pull
164 | - ✅ Global hooks in `~/.claude/settings.json`
165 |
166 | #### 📁 Project-Specific Configuration
167 |
168 | For project-specific settings without separate installation:
169 |
170 | ```bash
171 | # Use the same global bridge installation
172 | # No additional cloning needed!
173 |
174 | # Create project-specific Claude settings
175 | mkdir -p .claude
176 | cat > .claude/settings.json << 'EOF'
177 | {
178 | "hooks": {
179 | "PreToolUse": [{
180 | "matcher": "Read|Grep|Glob|Task",
181 | "hooks": [{
182 | "type": "command",
183 | "command": "/Users/yourname/claude-gemini-bridge/hooks/gemini-bridge.sh"
184 | }]
185 | }]
186 | }
187 | }
188 | EOF
189 |
190 | # Ensure script is executable (usually not needed after git clone)
191 | chmod +x /Users/yourname/claude-gemini-bridge/hooks/gemini-bridge.sh
192 |
193 | # Create project-specific environment setup (optional)
194 | cat > project-claude-setup.sh << 'EOF'
195 | export DEBUG_LEVEL=1
196 | export GEMINI_TIMEOUT=60
197 | export DRY_RUN=false
198 | EOF
199 | # Note: No chmod needed - script will be sourced, not executed
200 | ```
201 |
202 | **Benefits:**
203 | - ✅ Project-specific settings via environment variables
204 | - ✅ Different thresholds per project
205 | - ✅ Team-shareable setup scripts
206 | - ✅ No duplicate bridge installations
207 | - ✅ Project-local Claude settings override global ones
208 |
209 | The installer automatically:
210 | - ✅ Checks all prerequisites
211 | - ✅ Tests Gemini connectivity
212 | - ✅ Backs up existing Claude settings
213 | - ✅ Intelligently merges hooks into `~/.claude/settings.json`
214 | - ✅ Sets up directory structure and permissions
215 | - ✅ Runs validation tests
216 |
217 | ### Manual Installation
218 |
219 | Click to expand manual installation steps
221 |
222 | 1. **Clone the repository:**
223 | ```bash
224 | git clone https://github.com/your-username/claude-gemini-bridge.git
225 | cd claude-gemini-bridge
226 | ```
227 |
228 | 2. **Set up directory structure:**
229 | ```bash
230 | # Files are already in the right place after git clone
231 | chmod +x hooks/*.sh
232 | mkdir -p cache/gemini logs/debug debug/captured
233 | ```
234 |
235 | 3. **Configure Claude Code hooks:**
236 | ```bash
237 | # Add to ~/.claude/settings.json
238 | {
239 | "hooks": {
240 | "PreToolUse": [{
241 | "matcher": "Read|Grep|Glob|Task",
242 | "hooks": [{
243 | "type": "command",
244 | "command": "/full/path/to/claude-gemini-bridge/hooks/gemini-bridge.sh"
245 | }]
246 | }]
247 | }
248 | }
249 | ```
250 |
251 |
test-runner.sh]
489 | MT[Manual Tester
manual-test.sh]
490 |
491 | subgraph "Component Tests"
492 | PC[Path Converter]
493 | JP[JSON Parser]
494 | DH[Debug Helpers]
495 | GW[Gemini Wrapper]
496 | end
497 |
498 | subgraph "Integration Tests"
499 | HT[Hook Tests]
500 | ET[End-to-End]
501 | CT[Cache Tests]
502 | end
503 |
504 | subgraph "Mock Data"
505 | MTC[Mock Tool Calls]
506 | TF[Test Files]
507 | end
508 | end
509 |
510 | TR --> PC
511 | TR --> JP
512 | TR --> DH
513 | TR --> GW
514 | TR --> HT
515 | TR --> ET
516 | TR --> CT
517 |
518 | MT --> MTC
519 | MT --> TF
520 |
521 | style TR fill:#e1f5fe
522 | style MT fill:#f3e5f5
523 | ```
524 |
525 | ## 🐛 Troubleshooting
526 |
527 | ### Common Issues
528 |
529 | Hook not executing
531 |
532 | **Symptoms:** Claude behaves normally, Gemini never called
533 |
534 | **Solutions:**
535 | ```bash
536 | # Check hook configuration
537 | cat ~/.claude/settings.local.json | jq '.hooks'
538 |
539 | # Test hook manually
540 | echo '{"tool":"Read","parameters":{"file_path":"test.txt"},"context":{}}' | \
541 | ${CLAUDE_GEMINI_BRIDGE_DIR:-~/.claude-gemini-bridge}/hooks/gemini-bridge.sh
542 |
543 | # Verify file permissions
544 | ls -la ${CLAUDE_GEMINI_BRIDGE_DIR:-~/.claude-gemini-bridge}/hooks/gemini-bridge.sh
545 | ```
546 | Gemini API errors
550 |
551 | **Symptoms:** "Gemini initialization failed" errors
552 |
553 | **Solutions:**
554 | ```bash
555 | # Test Gemini CLI directly
556 | echo "test" | gemini -p "Say hello"
557 |
558 | # Check API key
559 | echo $GEMINI_API_KEY
560 |
561 | # Verify rate limits
562 | grep -i "rate limit" ${CLAUDE_GEMINI_BRIDGE_DIR:-~/.claude-gemini-bridge}/logs/debug/*.log
563 | ```
564 | Cache issues
568 |
569 | **Symptoms:** Outdated responses, cache errors
570 |
571 | **Solutions:**
572 | ```bash
573 | # Clear cache
574 | rm -rf ${CLAUDE_GEMINI_BRIDGE_DIR:-~/.claude-gemini-bridge}/cache/gemini/*
575 |
576 | # Check cache settings
577 | grep CACHE ${CLAUDE_GEMINI_BRIDGE_DIR:-~/.claude-gemini-bridge}/hooks/config/debug.conf
578 |
579 | # Monitor cache usage
580 | du -sh ${CLAUDE_GEMINI_BRIDGE_DIR:-~/.claude-gemini-bridge}/cache/
581 | ```
582 |