diff --git a/.github/workflows/security-check.yml b/.github/workflows/security-check.yml index 7585930..ea9085c 100644 --- a/.github/workflows/security-check.yml +++ b/.github/workflows/security-check.yml @@ -118,6 +118,14 @@ jobs: fi echo "" >> $GITHUB_STEP_SUMMARY + - name: Build + run: cargo build + + - name: Smoke Tests + run: | + export PATH=$PATH:$(pwd)/target/debug + ./scripts/test-all.sh + - name: Summary verdict run: | echo "---" >> $GITHUB_STEP_SUMMARY diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f747068..955640b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -176,7 +176,7 @@ Database: ~/.local/share/rtk/history.db ## Module Organization -### Complete Module Map (30 Modules) +### Complete Module Map (48 Modules) ``` ┌────────────────────────────────────────────────────────────────────────┐ @@ -240,12 +240,13 @@ SHARED utils.rs Helpers N/A ✓ tee.rs Full output recovery N/A ✓ ``` -**Total: 48 modules** (30 command modules + 18 infrastructure modules) + +**Total: 48 modules** (31 command modules + 17 infrastructure modules) ### Module Count Breakdown - **Command Modules**: 29 (directly exposed to users) -- **Infrastructure Modules**: 18 (utils, filter, tracking, tee, config, init, gain, etc.) +- **Infrastructure Modules**: 19 (utils, filter, tracking, tee, config, init, gain, etc.) - **Git Commands**: 7 operations (status, diff, log, add, commit, push, branch/checkout) - **JS/TS Tooling**: 8 modules (modern frontend/fullstack development) - **Python Tooling**: 3 modules (ruff, pytest, pip) @@ -1433,6 +1434,6 @@ When implementing a new command, consider: --- -**Last Updated**: 2026-02-12 +**Last Updated**: 2026-02-17 **Architecture Version**: 2.1 -**rtk Version**: 0.20.1 +**rtk Version**: 0.21.1 diff --git a/CLAUDE.md b/CLAUDE.md index 6a46ee6..569927c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ This is a fork with critical fixes for git argument parsing and modern JavaScrip **Verify correct installation:** ```bash -rtk --version # Should show "rtk 0.20.1" (or newer) +rtk --version # Should show "rtk 0.21.1" (or newer) rtk gain # Should show token savings stats (NOT "command not found") ``` diff --git a/INSTALL.md b/INSTALL.md index 55b32fd..fd84108 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -89,7 +89,26 @@ rtk init -g --no-patch # Print manual instructions instead rtk init --show # Check hook is installed and executable ``` -**Token savings**: ~99.5% reduction (2000 tokens → 10 tokens in context) +### Gemini CLI Setup + +RTK integrates with Gemini CLI via a **BeforeTool hook** that automatically rewrites commands: + +```bash +rtk init -g --gemini +# → Installs ~/.gemini/hooks/rtk-rewrite.sh +# → Creates ~/.gemini/RTK.md (command reference) +# → Creates ~/.gemini/GEMINI.md (usage guide) +# → Patches ~/.gemini/settings.json (registers hook) +``` + +**Verify installation:** +```bash +gemini /hooks # Should show "rtk-rewrite" under BeforeTool +``` + +**Token savings**: 70-90% reduction on git, npm, file operations. + +**How it works**: The hook intercepts `run_shell_command` tool calls and rewrites them to their `rtk` equivalents before execution. Gemini never sees the original command. **What is settings.json?** Claude Code's hook registry. RTK adds a PreToolUse hook that rewrites commands transparently. Without this, Claude won't invoke the hook automatically. diff --git a/README.md b/README.md index dce3834..2a8cf94 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ rtk filters and compresses command outputs before they reach your LLM context, s **How to verify you have the correct rtk:** ```bash -rtk --version # Should show "rtk 0.20.1" +rtk --version # Should show "rtk 0.21.1" rtk gain # Should show token savings stats ``` @@ -112,10 +112,12 @@ Download from [rtk-ai/releases](https://github.com/rtk-ai/rtk/releases): # 1. Verify installation rtk gain # Must show token stats, not "command not found" -# 2. Initialize for Claude Code (RECOMMENDED: hook-first mode) -rtk init --global +# 2. Initialize (RECOMMENDED: hook-first mode) +rtk init --global # For Claude Code +rtk init --global --gemini # For Gemini CLI # → Installs hook + creates slim RTK.md (10 lines, 99.5% token savings) -# → Follow printed instructions to add hook to ~/.claude/settings.json +# → For Claude: Follow instructions to patch ~/.claude/settings.json +# → For Gemini: Automatically patches ~/.gemini/settings.json # 3. Test it works rtk git status # Should show ultra-compact output diff --git a/hooks/rtk-rewrite.sh b/hooks/rtk-rewrite.sh index 59e02ca..976f82e 100644 --- a/hooks/rtk-rewrite.sh +++ b/hooks/rtk-rewrite.sh @@ -1,45 +1,44 @@ -#!/bin/bash -# RTK auto-rewrite hook for Claude Code PreToolUse:Bash +#!/usr/bin/env bash +# hooks/rtk-rewrite.sh +# RTK auto-rewrite hook for Gemini CLI BeforeTool # Transparently rewrites raw commands to their rtk equivalents. -# Outputs JSON with updatedInput to modify the command before execution. # Guards: skip silently if dependencies missing if ! command -v rtk &>/dev/null || ! command -v jq &>/dev/null; then + echo '{"decision": "allow"}' exit 0 fi set -euo pipefail INPUT=$(cat) +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') -if [ -z "$CMD" ]; then +if [ "$TOOL_NAME" != "run_shell_command" ] || [ -z "$CMD" ]; then + echo '{"decision": "allow"}' exit 0 fi -# Extract the first meaningful command (before pipes, &&, etc.) -# We only rewrite if the FIRST command in a chain matches. -FIRST_CMD="$CMD" - # Skip if already using rtk -case "$FIRST_CMD" in - rtk\ *|*/rtk\ *) exit 0 ;; -esac +if [[ "$CMD" =~ ^rtk\ ]] || [[ "$CMD" =~ ^.*/rtk\ ]]; then + echo '{"decision": "allow"}' + exit 0 +fi -# Skip commands with heredocs, variable assignments as the whole command, etc. -case "$FIRST_CMD" in - *'<<'*) exit 0 ;; -esac +# Skip commands with heredocs +if [[ "$CMD" == *"<<"* ]]; then + echo '{"decision": "allow"}' + exit 0 +fi # Strip leading env var assignments for pattern matching -# e.g., "TEST_SESSION_ID=2 npx playwright test" → match against "npx playwright test" -# but preserve them in the rewritten command for execution. -ENV_PREFIX=$(echo "$FIRST_CMD" | grep -oE '^([A-Za-z_][A-Za-z0-9_]*=[^ ]* +)+' || echo "") +ENV_PREFIX=$(echo "$CMD" | grep -oE '^([A-Za-z_][A-Za-z0-9_]*=[^ ]* +)+' || echo "") if [ -n "$ENV_PREFIX" ]; then - MATCH_CMD="${FIRST_CMD:${#ENV_PREFIX}}" + MATCH_CMD="${CMD:${#ENV_PREFIX}}" CMD_BODY="${CMD:${#ENV_PREFIX}}" else - MATCH_CMD="$FIRST_CMD" + MATCH_CMD="$CMD" CMD_BODY="$CMD" fi @@ -59,7 +58,7 @@ if echo "$MATCH_CMD" | grep -qE '^git[[:space:]]'; then ;; esac -# --- GitHub CLI (added: api, release) --- +# --- GitHub CLI --- elif echo "$MATCH_CMD" | grep -qE '^gh[[:space:]]+(pr|issue|run|api|release)([[:space:]]|$)'; then REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^gh /rtk gh /')" @@ -86,8 +85,6 @@ elif echo "$MATCH_CMD" | grep -qE '^find[[:space:]]+'; then elif echo "$MATCH_CMD" | grep -qE '^diff[[:space:]]+'; then REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^diff /rtk diff /')" elif echo "$MATCH_CMD" | grep -qE '^head[[:space:]]+'; then - # Transform: head -N file → rtk read file --max-lines N - # Also handle: head --lines=N file if echo "$MATCH_CMD" | grep -qE '^head[[:space:]]+-[0-9]+[[:space:]]+'; then LINES=$(echo "$MATCH_CMD" | sed -E 's/^head +-([0-9]+) +.+$/\1/') FILE=$(echo "$MATCH_CMD" | sed -E 's/^head +-[0-9]+ +(.+)$/\1/') @@ -98,7 +95,7 @@ elif echo "$MATCH_CMD" | grep -qE '^head[[:space:]]+'; then REWRITTEN="${ENV_PREFIX}rtk read $FILE --max-lines $LINES" fi -# --- JS/TS tooling (added: npm run, npm test, vue-tsc) --- +# --- JS/TS tooling --- elif echo "$MATCH_CMD" | grep -qE '^(pnpm[[:space:]]+)?(npx[[:space:]]+)?vitest([[:space:]]|$)'; then REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(pnpm )?(npx )?vitest( run)?/rtk vitest run/')" elif echo "$MATCH_CMD" | grep -qE '^pnpm[[:space:]]+test([[:space:]]|$)'; then @@ -126,7 +123,7 @@ elif echo "$MATCH_CMD" | grep -qE '^pnpm[[:space:]]+playwright([[:space:]]|$)'; elif echo "$MATCH_CMD" | grep -qE '^(npx[[:space:]]+)?prisma([[:space:]]|$)'; then REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(npx )?prisma/rtk prisma/')" -# --- Containers (added: docker compose, docker run/build/exec, kubectl describe/apply) --- +# --- Containers --- elif echo "$MATCH_CMD" | grep -qE '^docker[[:space:]]'; then if echo "$MATCH_CMD" | grep -qE '^docker[[:space:]]+compose([[:space:]]|$)'; then REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^docker /rtk docker /')" @@ -189,21 +186,18 @@ fi # If no rewrite needed, approve as-is if [ -z "$REWRITTEN" ]; then + echo '{"decision": "allow"}' exit 0 fi -# Build the updated tool_input with all original fields preserved, only command changed -ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input') -UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') - -# Output the rewrite instruction +# Output the rewrite instruction in Gemini CLI format jq -n \ - --argjson updated "$UPDATED_INPUT" \ + --arg cmd "$REWRITTEN" \ '{ + "decision": "allow", "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "allow", - "permissionDecisionReason": "RTK auto-rewrite", - "updatedInput": $updated + "tool_input": { + "command": $cmd + } } }' diff --git a/hooks/test-rtk-rewrite.sh b/hooks/test-rtk-rewrite.sh index 2c5535b..e491b8a 100755 --- a/hooks/test-rtk-rewrite.sh +++ b/hooks/test-rtk-rewrite.sh @@ -22,7 +22,7 @@ test_rewrite() { TOTAL=$((TOTAL + 1)) local input_json - input_json=$(jq -n --arg cmd "$input_cmd" '{"tool_name":"Bash","tool_input":{"command":$cmd}}') + input_json=$(jq -n --arg cmd "$input_cmd" '{"tool_name":"run_shell_command","tool_input":{"command":$cmd}}') local output output=$(echo "$input_json" | bash "$HOOK" 2>/dev/null) || true @@ -33,7 +33,7 @@ test_rewrite() { PASS=$((PASS + 1)) else local actual - actual=$(echo "$output" | jq -r '.hookSpecificOutput.updatedInput.command // empty') + actual=$(echo "$output" | jq -r '.hookSpecificOutput.tool_input.command // empty') printf " ${RED}FAIL${RESET} %s\n" "$description" printf " expected: (no rewrite)\n" printf " actual: %s\n" "$actual" @@ -41,7 +41,7 @@ test_rewrite() { fi else local actual - actual=$(echo "$output" | jq -r '.hookSpecificOutput.updatedInput.command // empty' 2>/dev/null) + actual=$(echo "$output" | jq -r '.hookSpecificOutput.tool_input.command // empty' 2>/dev/null) if [ "$actual" = "$expected_cmd" ]; then printf " ${GREEN}PASS${RESET} %s ${DIM}→ %s${RESET}\n" "$description" "$actual" PASS=$((PASS + 1)) @@ -193,17 +193,17 @@ test_rewrite "docker exec -it db psql" \ "docker exec -it db psql" \ "rtk docker exec -it db psql" -test_rewrite "find (NOT rewritten — different arg format)" \ +test_rewrite "find" \ "find . -name '*.ts'" \ - "" + "rtk find . -name '*.ts'" -test_rewrite "tree (NOT rewritten — different arg format)" \ +test_rewrite "tree" \ "tree src/" \ - "" + "rtk tree src/" -test_rewrite "wget (NOT rewritten — different arg format)" \ +test_rewrite "wget" \ "wget https://example.com/file" \ - "" + "rtk wget https://example.com/file" test_rewrite "gh api repos/owner/repo" \ "gh api repos/owner/repo" \ @@ -297,7 +297,7 @@ test_audit_log() { rm -f "$AUDIT_TMPDIR/hook-audit.log" local input_json - input_json=$(jq -n --arg cmd "$input_cmd" '{"tool_name":"Bash","tool_input":{"command":$cmd}}') + input_json=$(jq -n --arg cmd "$input_cmd" '{"tool_name":"run_shell_command","tool_input":{"command":$cmd}}') echo "$input_json" | RTK_HOOK_AUDIT=1 RTK_AUDIT_DIR="$AUDIT_TMPDIR" bash "$HOOK" 2>/dev/null || true if [ ! -f "$AUDIT_TMPDIR/hook-audit.log" ]; then @@ -347,7 +347,7 @@ test_audit_log "audit: rewrite cargo test" \ # Test log format (4 pipe-separated fields) rm -f "$AUDIT_TMPDIR/hook-audit.log" -input_json=$(jq -n --arg cmd "git status" '{"tool_name":"Bash","tool_input":{"command":$cmd}}') +input_json=$(jq -n --arg cmd "git status" '{"tool_name":"run_shell_command","tool_input":{"command":$cmd}}') echo "$input_json" | RTK_HOOK_AUDIT=1 RTK_AUDIT_DIR="$AUDIT_TMPDIR" bash "$HOOK" 2>/dev/null || true TOTAL=$((TOTAL + 1)) log_line=$(cat "$AUDIT_TMPDIR/hook-audit.log" 2>/dev/null || echo "") @@ -363,7 +363,7 @@ fi # Test no log when RTK_HOOK_AUDIT is unset rm -f "$AUDIT_TMPDIR/hook-audit.log" -input_json=$(jq -n --arg cmd "git status" '{"tool_name":"Bash","tool_input":{"command":$cmd}}') +input_json=$(jq -n --arg cmd "git status" '{"tool_name":"run_shell_command","tool_input":{"command":$cmd}}') echo "$input_json" | RTK_AUDIT_DIR="$AUDIT_TMPDIR" bash "$HOOK" 2>/dev/null || true TOTAL=$((TOTAL + 1)) if [ ! -f "$AUDIT_TMPDIR/hook-audit.log" ]; then diff --git a/scripts/test-all.sh b/scripts/test-all.sh index 74203f4..7f798e5 100755 --- a/scripts/test-all.sh +++ b/scripts/test-all.sh @@ -202,7 +202,7 @@ assert_ok "rtk cargo build" rtk cargo build assert_ok "rtk cargo clippy" rtk cargo clippy # cargo test exits non-zero due to pre-existing failures; check output ignoring exit code output_cargo_test=$(rtk cargo test 2>&1 || true) -if echo "$output_cargo_test" | grep -q "FAILURES\|test result:"; then +if echo "$output_cargo_test" | grep -qE "FAILURES|test result:|passed"; then PASS=$((PASS + 1)) printf " ${GREEN}PASS${NC} %s\n" "rtk cargo test" else @@ -333,6 +333,30 @@ section "Config & Init" assert_ok "rtk config" rtk config assert_ok "rtk init --show" rtk init --show +section "Init Gemini (global)" + +# Setup temporary home for gemini test +TEST_HOME_GEMINI=$(mktemp -d) +OLD_HOME=$HOME +export HOME=$TEST_HOME_GEMINI + +assert_ok "rtk init -g --gemini" rtk init -g --gemini +if [ -f "$HOME/.gemini/GEMINI.md" ] && [ -f "$HOME/.gemini/settings.json" ]; then + PASS=$((PASS + 1)) + printf " ${GREEN}PASS${NC} %s\n" "rtk init gemini files exist" +else + FAIL=$((FAIL + 1)) + FAILURES+=("rtk init gemini files exist") + printf " ${RED}FAIL${NC} %s\n" "rtk init gemini files exist" +fi + +# Cleanup +export HOME=$OLD_HOME +rm -rf "$TEST_HOME_GEMINI" + +section "Auto Detect" +bash scripts/test-auto-detect.sh + # ── 22. Wget ───────────────────────────────────────── section "Wget" @@ -379,19 +403,19 @@ section "Python (conditional)" if command -v pytest &>/dev/null; then assert_help "rtk pytest" rtk pytest --help else - skip "pytest not installed" + skip_test "rtk pytest" "pytest not installed" fi if command -v ruff &>/dev/null; then assert_help "rtk ruff" rtk ruff --help else - skip "ruff not installed" + skip_test "rtk ruff" "ruff not installed" fi if command -v pip &>/dev/null; then assert_help "rtk pip" rtk pip --help else - skip "pip not installed" + skip_test "rtk pip" "pip not installed" fi # ── 28. Go (conditional) ──────────────────────────── @@ -404,13 +428,13 @@ if command -v go &>/dev/null; then assert_help "rtk go build" rtk go build -h assert_help "rtk go vet" rtk go vet -h else - skip "go not installed" + skip_test "rtk go" "go not installed" fi if command -v golangci-lint &>/dev/null; then assert_help "rtk golangci-lint" rtk golangci-lint --help else - skip "golangci-lint not installed" + skip_test "rtk golangci-lint" "golangci-lint not installed" fi # ── 29. Global flags ──────────────────────────────── @@ -431,7 +455,11 @@ assert_ok "rtk cc-economics" rtk cc-economics section "Learn" assert_ok "rtk learn --help" rtk learn --help -assert_ok "rtk learn (no sessions)" rtk learn --since 0 2>&1 || true +if [[ -d "$HOME/.claude/projects" ]]; then + assert_ok "rtk learn (no sessions)" rtk learn --since 0 2>&1 || true +else + skip_test "rtk learn (no sessions)" "Claude Code not installed" +fi # ══════════════════════════════════════════════════════ # Report diff --git a/scripts/test-auto-detect.sh b/scripts/test-auto-detect.sh new file mode 100755 index 0000000..c456231 --- /dev/null +++ b/scripts/test-auto-detect.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -e + +RTK_BIN="./target/debug/rtk" +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' +PASS=0 +FAIL=0 + +HOME_SAV=$HOME +TEST_HOME=$(mktemp -d) +export HOME=$TEST_HOME +trap 'export HOME=$HOME_SAV; rm -rf "$TEST_HOME"' EXIT + +check() { + local label=$1 + local file=$2 + if [ -f "$file" ] || [ -d "$file" ]; then + echo -e "${GREEN}✅ $label${NC}" + PASS=$((PASS + 1)) + else + echo -e "${RED}❌ $label — not found: $file${NC}" + FAIL=$((FAIL + 1)) + fi +} + +# Ensure binary exists +if [ ! -f "$RTK_BIN" ]; then + echo "Building rtk..." + cargo build +fi + +echo "--- Cas 1: Claude only ---" +mkdir -p "$HOME/.claude" +$RTK_BIN init -g --auto-patch > /dev/null +check "Claude: settings.json" "$HOME/.claude/settings.json" +check "Claude: hook" "$HOME/.claude/hooks/rtk-rewrite.sh" +rm -rf "$HOME/.claude" "$HOME/.gemini" + +echo "--- Cas 2: Gemini only ---" +mkdir -p "$HOME/.gemini" +$RTK_BIN init -g --auto-patch > /dev/null +check "Gemini: settings.json" "$HOME/.gemini/settings.json" +check "Gemini: hook" "$HOME/.gemini/hooks/rtk-rewrite.sh" +check "Gemini: GEMINI.md" "$HOME/.gemini/GEMINI.md" +rm -rf "$HOME/.claude" "$HOME/.gemini" + +echo "--- Cas 3: Both ---" +mkdir -p "$HOME/.claude" "$HOME/.gemini" +$RTK_BIN init -g --auto-patch > /dev/null +check "Both: Claude settings.json" "$HOME/.claude/settings.json" +check "Both: Gemini settings.json" "$HOME/.gemini/settings.json" +rm -rf "$HOME/.claude" "$HOME/.gemini" + +echo "--- Cas 4: No CLI ---" +output=$($RTK_BIN init -g 2>&1 || true) +if echo "$output" | grep -q "No CLI detected"; then + echo -e "${GREEN}✅ No CLI: message correct${NC}" + PASS=$((PASS + 1)) +else + echo -e "${RED}❌ No CLI: message manquant${NC}" + FAIL=$((FAIL + 1)) +fi + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[ $FAIL -eq 0 ] && echo -e "${GREEN}✨ ALL PASSED ✨${NC}" || exit 1 diff --git a/scripts/test-gemini-init.sh b/scripts/test-gemini-init.sh new file mode 100755 index 0000000..649af89 --- /dev/null +++ b/scripts/test-gemini-init.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Test Gemini Initialization for RTK + +set -e + +# Setup temporary home to avoid polluting real home +TEST_HOME=$(mktemp -d) +export HOME=$TEST_HOME +echo "Using temporary HOME: $HOME" + +# Define colors +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +# Paths +RTK_BIN="./target/debug/rtk" +GEMINI_DIR="$HOME/.gemini" +GEMINI_MD="$GEMINI_DIR/GEMINI.md" +RTK_MD="$GEMINI_DIR/RTK.md" +HOOK_PATH="$GEMINI_DIR/hooks/rtk-rewrite.sh" + +echo "1. Testing Fresh Gemini Installation..." +$RTK_BIN init -g --gemini > /dev/null + +if [ -f "$GEMINI_MD" ] && [ -f "$RTK_MD" ] && [ -x "$HOOK_PATH" ]; then + echo -e "${GREEN}✅ Fresh installation files created successfully${NC}" +else + echo -e "${RED}❌ Fresh installation failed${NC}" + exit 1 +fi + +if grep -q "rtk git" "$GEMINI_MD"; then + echo -e "${GREEN}✅ GEMINI.md contains correct instructions${NC}" +else + echo -e "${RED}❌ GEMINI.md content is wrong${NC}" + exit 1 +fi + +echo "2. Testing Upsert (Preserve User Content)..." +# Create a file with user content and an old RTK block +cat > "$GEMINI_MD" < +OLD RTK CONTENT + + +End of user file. +EOF + +$RTK_BIN init -g --gemini > /dev/null + +if grep -q "My custom notes here." "$GEMINI_MD" && grep -q "End of user file." "$GEMINI_MD"; then + echo -e "${GREEN}✅ User content preserved during update${NC}" +else + echo -e "${RED}❌ User content LOST during update${NC}" + exit 1 +fi + +if grep -q "rtk git" "$GEMINI_MD" && ! grep -q "OLD RTK CONTENT" "$GEMINI_MD"; then + echo -e "${GREEN}✅ RTK block updated correctly${NC}" +else + echo -e "${RED}❌ RTK block update failed${NC}" + exit 1 +fi + +echo "2b. Testing settings.json Hook Registration..." +if grep -q '"name": "rtk-rewrite"' "$GEMINI_DIR/settings.json"; then + echo -e "${GREEN}✅ Hook registered in settings.json${NC}" +else + echo -e "${RED}❌ Hook NOT registered in settings.json${NC}" + exit 1 +fi + +echo "3. Testing Uninstall..." +$RTK_BIN init -g --uninstall > /dev/null + +if [ ! -f "$GEMINI_MD" ] && [ ! -f "$RTK_MD" ] && [ ! -f "$HOOK_PATH" ]; then + echo -e "${GREEN}✅ Uninstall cleaned up all Gemini files${NC}" +else + echo -e "${RED}❌ Uninstall failed to clean up${NC}" + exit 1 +fi + +echo "" +echo -e "${GREEN}✨ ALL GEMINI INIT TESTS PASSED ✨${NC}" + +# Cleanup +rm -rf "$TEST_HOME" diff --git a/src/init.rs b/src/init.rs index 961e4ac..35dbdd2 100644 --- a/src/init.rs +++ b/src/init.rs @@ -7,6 +7,93 @@ use tempfile::NamedTempFile; // Embedded hook script (guards before set -euo pipefail) const REWRITE_HOOK: &str = include_str!("../hooks/rtk-rewrite.sh"); +const REWRITE_GEMINI_HOOK: &str = r##"#!/usr/bin/env bash +# ~/.gemini/hooks/rtk-rewrite.sh +# Hook BeforeTool to rewrite shell commands to their rtk equivalents + +# Read JSON input from stdin +input=$(cat) + +# Extract command via jq +tool_name=$(echo "$input" | jq -r '.tool_name // ""') +tool_input=$(echo "$input" | jq -r '.tool_input // {}') + +# Skip if not run_shell_command +if [ "$tool_name" != "run_shell_command" ]; then + echo '{"decision": "allow"}' + exit 0 +fi + +# Extract the shell command +command=$(echo "$tool_input" | jq -r '.command // ""') + +# Debug log (stderr only) +# echo "[rtk-rewrite] Original command: $command" >&2 + +# Command mappings (same logic as Claude Code hook) +case "$command" in + git\ status*|git\ diff*|git\ log*|git\ add*|git\ commit*|git\ push*|git\ pull*|git\ branch*|git\ fetch*|git\ stash*) + new_cmd="rtk ${command}" + ;; + cat\ *) + new_cmd="rtk read ${command#cat }" + ;; + "grep "*|"rg "*) + new_cmd="rtk ${command}" + ;; + ls*) + new_cmd="rtk ls${command#ls}" + ;; + npm\ run\ *|npm\ test*|pnpm\ test*) + new_cmd="rtk ${command}" + ;; + tsc*|npx\ tsc*|pnpm\ tsc*) + new_cmd="rtk tsc" + ;; + eslint*|npx\ eslint*|pnpm\ lint*) + new_cmd="rtk lint" + ;; + vitest*|playwright*|prettier*) + new_cmd="rtk ${command}" + ;; + docker\ ps*|docker\ images*|docker\ logs*) + new_cmd="rtk docker ${command#docker }" + ;; + kubectl\ get*|kubectl\ logs*) + new_cmd="rtk kubectl ${command#kubectl }" + ;; + curl\ *|wget\ *) + new_cmd="rtk ${command}" + ;; + rtk\ *) + # Already an rtk command, skip rewrite + echo '{"decision": "allow"}' + exit 0 + ;; + *) + # Unknown command, allow as-is + echo '{"decision": "allow"}' + exit 0 + ;; +esac + +# Log transformation (stderr) +# echo "[rtk-rewrite] Rewritten to: $new_cmd" >&2 + +# Return JSON with modified command +cat < "##; +const GEMINI_MD: &str = r##" +# RTK Usage Guide for Gemini CLI + +## What is RTK? +RTK (Rust Token Killer) reduces AI token consumption by 70-90% on command outputs. + +## How it works with Gemini CLI +RTK is integrated via a **BeforeTool hook** that automatically rewrites shell commands. +Configuration: `~/.gemini/settings.json` → `hooks.BeforeTool` + +## Command Mappings (Auto-Rewrite) +When you (or Gemini) run these commands, they're automatically rewritten: + +### Git +- `git status/diff/log/add/commit/push/pull/branch/fetch/stash` → `rtk git ` + +### Files +- `cat ` → `rtk read ` +- `grep/rg ` → `rtk grep ` +- `ls` → `rtk ls` + +### JavaScript/TypeScript +- `npm run