From e3169147a930a3111e540db11d2c66a0cb0a8749 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Tue, 17 Mar 2026 19:15:07 +0100 Subject: [PATCH 01/14] fork: establish algolia/rtk identity with no-telemetry CI guard - install.sh now points to algolia/rtk at pinned v0.22.2 (overridable via RTK_VERSION env var) - All docs, scripts, CI workflows updated from rtk-ai/rtk to algolia/rtk - CHANGELOG historical upstream links preserved for attribution - All GitHub Actions workflows target 'main' branch (not 'master') - New ci.yml: build/test/clippy/fmt on ubuntu + macOS - New telemetry-guard CI job: blocks telemetry.rs, ureq, sha2, hostname deps, and phone-home patterns in source - CLAUDE.md documents fork maintenance strategy, sync policy, and telemetry exclusion rules --- .claude/agents/technical-writer.md | 8 +-- .claude/skills/ship.md | 2 +- .github/workflows/benchmark.yml | 2 +- .github/workflows/ci.yml | 102 +++++++++++++++++++++++++++ .github/workflows/release-please.yml | 2 +- .github/workflows/release.yml | 10 +-- .github/workflows/security-check.yml | 8 +-- .github/workflows/validate-docs.yml | 2 +- CHANGELOG.md | 26 +++++++ CLAUDE.md | 57 ++++++++++++++- Cargo.toml | 2 +- INSTALL.md | 12 ++-- README.md | 30 ++++++-- SECURITY.md | 4 +- docs/TROUBLESHOOTING.md | 20 +++--- docs/tracking.md | 4 +- install.sh | 19 +++-- scripts/check-installation.sh | 6 +- src/discover/report.rs | 2 +- 19 files changed, 257 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.claude/agents/technical-writer.md b/.claude/agents/technical-writer.md index 9f41e3e95..06ba0dc57 100644 --- a/.claude/agents/technical-writer.md +++ b/.claude/agents/technical-writer.md @@ -115,7 +115,7 @@ rtk --version # Should show rtk X.Y.Z **Option 2: From Source** ```bash -git clone https://github.com/rtk-ai/rtk.git +git clone https://github.com/algolia/rtk.git cd rtk cargo install --path . rtk --version # Verify installation @@ -130,7 +130,7 @@ rtk gain # Should show token savings analytics **From Source** (Cargo required): ```bash -git clone https://github.com/rtk-ai/rtk.git +git clone https://github.com/algolia/rtk.git cd rtk cargo install --path . @@ -141,7 +141,7 @@ rtk --version **Binary Download** (faster): ```bash -curl -sSL https://github.com/rtk-ai/rtk/releases/download/v0.16.0/rtk-linux-x86_64 -o rtk +curl -sSL https://github.com/algolia/rtk/releases/download/v0.16.0/rtk-linux-x86_64 -o rtk chmod +x rtk sudo mv rtk /usr/local/bin/ rtk --version @@ -172,7 +172,7 @@ rtk --version - **Fix**: Uninstall and reinstall correct RTK ```bash cargo uninstall rtk - cargo install --path . # From rtk-ai/rtk repo + cargo install --path . # From algolia/rtk repo (fork of rtk-ai/rtk) rtk gain --help # Should work ``` ``` diff --git a/.claude/skills/ship.md b/.claude/skills/ship.md index 380a8ba2b..646d6a7b3 100644 --- a/.claude/skills/ship.md +++ b/.claude/skills/ship.md @@ -187,7 +187,7 @@ gh release view v0.17.0 ### 3. Installation Verification ```bash # Test installation from release -curl -sSL https://github.com/rtk-ai/rtk/releases/download/v0.17.0/rtk-macos-latest -o rtk +curl -sSL https://github.com/algolia/rtk/releases/download/v0.17.0/rtk-macos-latest -o rtk chmod +x rtk ./rtk --version # Should show v0.17.0 diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 92ce9cc76..29c669327 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -2,7 +2,7 @@ name: Benchmark Token Savings on: push: - branches: [master, main] + branches: [main] pull_request: jobs: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..763011248 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + build-and-test: + name: Build & Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Check formatting + run: cargo fmt --all --check + + - name: Clippy + run: cargo clippy --all-targets -- -D warnings + + - name: Run tests + run: cargo test --all + + - name: Build release + run: cargo build --release + + telemetry-guard: + name: Telemetry Guard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Block telemetry code + run: | + echo "## Telemetry Guard" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + FAILED=0 + + # Check for telemetry module + if [ -f src/telemetry.rs ]; then + echo "::error::src/telemetry.rs exists — this fork does not permit telemetry" + echo "- src/telemetry.rs detected" >> $GITHUB_STEP_SUMMARY + FAILED=1 + fi + + # Check for telemetry dependencies + for dep in ureq reqwest hyper sha2 hostname; do + if grep -q "^${dep} " Cargo.toml 2>/dev/null || grep -q "\"${dep}\"" Cargo.toml 2>/dev/null; then + echo "::error::Forbidden dependency '${dep}' found in Cargo.toml" + echo "- Forbidden dep: ${dep}" >> $GITHUB_STEP_SUMMARY + FAILED=1 + fi + done + + # Check for phone-home patterns in Rust source + if grep -rE "telemetry|phone.?home|TELEMETRY_URL|TELEMETRY_TOKEN|send_ping|maybe_ping" src/ --include="*.rs" | grep -v "// *no.telemetry\|tracking.rs\|SECURITY.md"; then + echo "::error::Telemetry-related code patterns found in src/" + echo "- Telemetry patterns in source" >> $GITHUB_STEP_SUMMARY + FAILED=1 + fi + + # Check for HTTP client usage (ureq/reqwest) in source + if grep -rE "ureq::|reqwest::" src/ --include="*.rs"; then + echo "::error::HTTP client usage found in source — RTK should not make network calls" + echo "- HTTP client usage in source" >> $GITHUB_STEP_SUMMARY + FAILED=1 + fi + + if [ "$FAILED" -eq 1 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "This fork maintains a strict no-telemetry policy." >> $GITHUB_STEP_SUMMARY + echo "See CLAUDE.md 'Telemetry Exclusion Policy' for details." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "No telemetry code detected." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index bd1e0893b..fe3870d10 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -3,7 +3,7 @@ name: Release Please on: push: branches: - - master + - main permissions: contents: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67fbcc062..3d2fa967d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -204,7 +204,7 @@ jobs: - name: Download checksums run: | gh release download "${{ steps.version.outputs.tag }}" \ - --repo rtk-ai/rtk \ + --repo algolia/rtk \ --pattern checksums.txt env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -227,16 +227,16 @@ jobs: license "MIT" if OS.mac? && Hardware::CPU.arm? - url "https://github.com/rtk-ai/rtk/releases/download/TAG_PLACEHOLDER/rtk-aarch64-apple-darwin.tar.gz" + url "https://github.com/algolia/rtk/releases/download/TAG_PLACEHOLDER/rtk-aarch64-apple-darwin.tar.gz" sha256 "SHA_MAC_ARM_PLACEHOLDER" elsif OS.mac? && Hardware::CPU.intel? - url "https://github.com/rtk-ai/rtk/releases/download/TAG_PLACEHOLDER/rtk-x86_64-apple-darwin.tar.gz" + url "https://github.com/algolia/rtk/releases/download/TAG_PLACEHOLDER/rtk-x86_64-apple-darwin.tar.gz" sha256 "SHA_MAC_INTEL_PLACEHOLDER" elsif OS.linux? && Hardware::CPU.arm? - url "https://github.com/rtk-ai/rtk/releases/download/TAG_PLACEHOLDER/rtk-aarch64-unknown-linux-gnu.tar.gz" + url "https://github.com/algolia/rtk/releases/download/TAG_PLACEHOLDER/rtk-aarch64-unknown-linux-gnu.tar.gz" sha256 "SHA_LINUX_ARM_PLACEHOLDER" elsif OS.linux? && Hardware::CPU.intel? - url "https://github.com/rtk-ai/rtk/releases/download/TAG_PLACEHOLDER/rtk-x86_64-unknown-linux-gnu.tar.gz" + url "https://github.com/algolia/rtk/releases/download/TAG_PLACEHOLDER/rtk-x86_64-unknown-linux-gnu.tar.gz" sha256 "SHA_LINUX_INTEL_PLACEHOLDER" end diff --git a/.github/workflows/security-check.yml b/.github/workflows/security-check.yml index 75859305a..69ad4d97d 100644 --- a/.github/workflows/security-check.yml +++ b/.github/workflows/security-check.yml @@ -2,7 +2,7 @@ name: Security Check on: pull_request: - branches: [ master ] + branches: [ main ] permissions: contents: read @@ -46,7 +46,7 @@ jobs: - name: Critical files check run: | echo "### 🎯 Critical Files Modified" >> $GITHUB_STEP_SUMMARY - CRITICAL=$(git diff --name-only origin/master...HEAD | grep -E "(runner|summary|tracking|init|pnpm_cmd|container)\.rs|Cargo\.toml|workflows/.*\.yml" || true) + CRITICAL=$(git diff --name-only origin/main...HEAD | grep -E "(runner|summary|tracking|init|pnpm_cmd|container)\.rs|Cargo\.toml|workflows/.*\.yml" || true) if [ -n "$CRITICAL" ]; then echo "⚠️ **HIGH RISK**: The following critical files were modified:" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY @@ -66,7 +66,7 @@ jobs: - name: Dangerous patterns scan run: | echo "### 🚨 Dangerous Code Patterns" >> $GITHUB_STEP_SUMMARY - PATTERNS=$(git diff origin/master...HEAD | grep -E "Command::new\(\"sh\"|Command::new\(\"bash\"|\.env\(\"LD_PRELOAD|\.env\(\"PATH|reqwest::|std::net::|TcpStream|UdpSocket|unsafe \{|\.unwrap\(\) |panic!\(|todo!\(|unimplemented!\(" || true) + PATTERNS=$(git diff origin/main...HEAD | grep -E "Command::new\(\"sh\"|Command::new\(\"bash\"|\.env\(\"LD_PRELOAD|\.env\(\"PATH|reqwest::|std::net::|TcpStream|UdpSocket|unsafe \{|\.unwrap\(\) |panic!\(|todo!\(|unimplemented!\(" || true) if [ -n "$PATTERNS" ]; then echo "⚠️ **Potentially dangerous patterns detected:**" >> $GITHUB_STEP_SUMMARY echo '```diff' >> $GITHUB_STEP_SUMMARY @@ -88,7 +88,7 @@ jobs: - name: New dependencies check run: | echo "### 📚 Dependencies Changes" >> $GITHUB_STEP_SUMMARY - if git diff origin/master...HEAD Cargo.toml | grep -E "^\+.*=" | grep -v "^\+\+\+" > new_deps.txt; then + if git diff origin/main...HEAD Cargo.toml | grep -E "^\+.*=" | grep -v "^\+\+\+" > new_deps.txt; then echo "⚠️ **New dependencies added:**" >> $GITHUB_STEP_SUMMARY echo '```toml' >> $GITHUB_STEP_SUMMARY cat new_deps.txt >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/validate-docs.yml b/.github/workflows/validate-docs.yml index 27879bc0f..ee40ab525 100644 --- a/.github/workflows/validate-docs.yml +++ b/.github/workflows/validate-docs.yml @@ -9,7 +9,7 @@ on: - '.claude/hooks/*.sh' push: branches: - - master + - main - feat/** jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index caee3bc9e..0a1cde7ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ All notable changes to rtk (Rust Token Killer) will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.22.3-algolia.1](https://github.com/algolia/rtk/compare/v0.22.2...v0.22.3-algolia.1) (2026-03-17) + +This is the first release from the `algolia/rtk` fork. All upstream features are welcome; +telemetry is permanently excluded. + +### Security + +* **no-telemetry policy**: Codified and CI-enforced — `src/telemetry.rs`, `ureq`, `sha2`, + `hostname` deps, and phone-home patterns are blocked by the `telemetry-guard` CI job. + RTK is a CLI filter; it has no business making network calls. +* **CI**: Added `ci.yml` with build/test/clippy/fmt on ubuntu + macOS, plus dedicated + telemetry guard job that fails on any phone-home code or forbidden dependencies. + +### Fork Maintenance + +* **install.sh**: Now points to `algolia/rtk` with pinned version `v0.22.2` + (overridable via `RTK_VERSION` env var). Previously fetched latest from upstream, + which could pull telemetry-enabled releases. +* **Cargo.toml**: Repository field updated to `algolia/rtk`. +* **All docs/scripts**: Install URLs, clone URLs, release download URLs, CI workflows, + and issue tracker links updated to `algolia/rtk`. Historical CHANGELOG links to + upstream preserved for attribution. +* **CLAUDE.md**: Added fork maintenance strategy documenting sync policy (selective + cherry-pick), telemetry exclusion rules, and version pinning approach. +* **security-check.yml**: Updated to target `main` branch (not `master`). + ## [0.22.2](https://github.com/rtk-ai/rtk/compare/v0.22.1...v0.22.2) (2026-02-20) diff --git a/CLAUDE.md b/CLAUDE.md index b8cf94f0f..fa6e58ebd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,12 +6,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **rtk (Rust Token Killer)** is a high-performance CLI proxy that minimizes LLM token consumption by filtering and compressing command outputs. It achieves 60-90% token savings on common development operations through smart filtering, grouping, truncation, and deduplication. -This is a fork with critical fixes for git argument parsing and modern JavaScript stack support (pnpm, vitest, Next.js, TypeScript, Playwright, Prisma). +**This is `algolia/rtk`** — Algolia's maintained fork of [rtk-ai/rtk](https://github.com/rtk-ai/rtk) (upstream). The fork was created before upstream added telemetry and is maintained independently with selective cherry-picks. ### ⚠️ Name Collision Warning **Two different "rtk" projects exist:** -- ✅ **This project**: Rust Token Killer (rtk-ai/rtk) +- ✅ **This project**: Rust Token Killer (`algolia/rtk`, forked from `rtk-ai/rtk`) - ❌ **reachingforthejack/rtk**: Rust Type Kit (DIFFERENT - generates Rust types) **Verify correct installation:** @@ -22,6 +22,59 @@ rtk gain # Should show token savings stats (NOT "command not found") If `rtk gain` fails, you have the wrong package installed. +## Fork Maintenance Strategy + +### Identity + +- **Our repo**: `algolia/rtk` (origin) +- **Upstream**: `rtk-ai/rtk` (upstream remote — **fetch only, push disabled**) +- All install scripts, docs, and Cargo.toml point to `algolia/rtk` +- Upstream attribution preserved in CHANGELOG and commit history +- `gh` default repo is `algolia/rtk` — never create PRs against upstream + +**Upstream push guard** (already set, but re-run if you re-clone): +```bash +git remote set-url --push upstream no-push-to-upstream +gh repo set-default algolia/rtk +``` + +### Sync Policy: Selective Cherry-Pick + +We do **not** merge upstream wholesale. Instead: + +1. **Watch**: Upstream commits surface as GitHub issues (via watch bot) +2. **Triage**: Each issue is reviewed — accept, skip, or defer +3. **Cherry-pick**: Accepted commits are cherry-picked onto our main branch +4. **Close**: Issue closed with rationale (cherry-picked, skipped, or N/A) + +**What we want from upstream:** +- Bug fixes (exit codes, regressions, platform compat) +- New command filters (dotnet, AWS, psql, etc.) +- Security hardening (trust boundaries, input validation) +- Performance improvements +- Hook system and rewrite engine improvements + +**What we always reject:** +- `src/telemetry.rs` and any phone-home code +- Dependencies serving only telemetry: `ureq`, `sha2`, `hostname` +- Opt-out telemetry config fields (`TelemetryConfig`, `telemetry_enabled()`) +- Any code that sends data to external servers + +### Telemetry Exclusion Policy + +**Hard rule: zero network telemetry.** RTK is a CLI proxy — it reads command output and prints filtered text. It has no business phoning home. + +- `src/telemetry.rs` must never exist in this fork +- No HTTP client deps (`ureq`, `reqwest`, `hyper`) unless for a user-facing feature +- `tracking.rs` is local-only SQLite — that's fine and encouraged +- If upstream adds telemetry hooks in existing modules, strip them during cherry-pick + +### Version Pinning + +- `install.sh` pins to a specific release tag (e.g., `v0.22.2`) +- Pin is updated manually after testing a new upstream sync +- `Cargo.toml` version reflects our fork's release, not upstream's + ## Development Commands > **Note**: If rtk is installed, prefer `rtk ` over raw commands for token-optimized output. diff --git a/Cargo.toml b/Cargo.toml index 244aa793d..769d31941 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ authors = ["Patrick Szymkowiak"] description = "Rust Token Killer - High-performance CLI proxy to minimize LLM token consumption" license = "MIT" homepage = "https://www.rtk-ai.app" -repository = "https://github.com/rtk-ai/rtk" +repository = "https://github.com/algolia/rtk" readme = "README.md" keywords = ["cli", "llm", "token", "filter", "productivity"] categories = ["command-line-utilities", "development-tools"] diff --git a/INSTALL.md b/INSTALL.md index 002e71471..4def2ddd5 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -5,7 +5,7 @@ **There are TWO completely different projects named "rtk":** 1. ✅ **Rust Token Killer** (this project) - LLM token optimizer - - Repos: `rtk-ai/rtk` + - Repos: `algolia/rtk` (fork of `rtk-ai/rtk`) - Has `rtk gain` command for token savings stats 2. ❌ **Rust Type Kit** (reachingforthejack/rtk) - DIFFERENT PROJECT @@ -44,7 +44,7 @@ cargo uninstall rtk ### Quick Install (Linux/macOS) ```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/algolia/rtk/main/install.sh | sh ``` After installation, **verify you have the correct rtk**: @@ -56,7 +56,7 @@ rtk gain # Must show token savings stats (not "command not found") ```bash # From rtk-ai repository (NOT reachingforthejack!) -cargo install --git https://github.com/rtk-ai/rtk +cargo install --git https://github.com/algolia/rtk # OR (if published and correct on crates.io) cargo install rtk @@ -164,7 +164,7 @@ rtk init -g # Automatically migrates to hook-first mode ### First-Time User (Recommended) ```bash # 1. Install RTK -cargo install --git https://github.com/rtk-ai/rtk +cargo install --git https://github.com/algolia/rtk rtk gain # Verify (must show token stats) # 2. Setup with prompts @@ -357,8 +357,8 @@ cargo install --path . --force - **Website**: https://www.rtk-ai.app - **Contact**: contact@rtk-ai.app - **Troubleshooting**: See [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for common issues -- **GitHub issues**: https://github.com/rtk-ai/rtk/issues -- **Pull Requests**: https://github.com/rtk-ai/rtk/pulls +- **GitHub issues**: https://github.com/algolia/rtk/issues +- **Pull Requests**: https://github.com/algolia/rtk/pulls ⚠️ **If you installed the wrong rtk (Type Kit)**, see [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md#problem-rtk-gain-command-not-found) diff --git a/README.md b/README.md index b6537eab7..d0919c08e 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,32 @@ **High-performance CLI proxy to minimize LLM token consumption.** -[Website](https://www.rtk-ai.app) | [GitHub](https://github.com/rtk-ai/rtk) | [Install](INSTALL.md) +[Website](https://www.rtk-ai.app) | [GitHub](https://github.com/algolia/rtk) | [Install](INSTALL.md) | Forked from [rtk-ai/rtk](https://github.com/rtk-ai/rtk) rtk filters and compresses command outputs before they reach your LLM context, saving 60-90% of tokens on common operations. +## 🔒 No Telemetry — Enforced by CI + +This is `algolia/rtk`, a maintained fork of [rtk-ai/rtk](https://github.com/rtk-ai/rtk). + +**Why this fork exists:** Upstream added phone-home telemetry in v0.29.0 (`src/telemetry.rs` — daily ping with device hash, OS, command counts). This fork was created before that addition and permanently excludes it. + +**What we guarantee:** +- No network calls — RTK reads stdin, writes stdout, writes to a local SQLite file. That's it. +- No `ureq`, `reqwest`, or any HTTP client dependency +- CI blocks any future addition via a dedicated [`telemetry-guard` job](.github/workflows/ci.yml) + +Install from this fork — **not** from upstream: +```bash +curl -fsSL https://raw.githubusercontent.com/algolia/rtk/main/install.sh | sh +``` + ## ⚠️ Important: Name Collision Warning **There are TWO different projects named "rtk":** 1. ✅ **This project (Rust Token Killer)** - LLM token optimizer - - Repos: `rtk-ai/rtk` + - Repos: `algolia/rtk` (fork of `rtk-ai/rtk`) - Purpose: Reduce Claude Code token consumption 2. ❌ **reachingforthejack/rtk** - Rust Type Kit (DIFFERENT PROJECT) @@ -74,7 +90,7 @@ brew install rtk ### Quick Install (Linux/macOS) ```bash -curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/algolia/rtk/main/install.sh | sh ``` > **Note**: rtk installs to `~/.local/bin` by default. If this directory is not in your PATH, add it: @@ -91,7 +107,7 @@ rtk gain # Must show token savings stats (not "command not found") ```bash # From rtk-ai upstream (maintained by pszymkowiak) -cargo install --git https://github.com/rtk-ai/rtk +cargo install --git https://github.com/algolia/rtk # OR if published to crates.io cargo install rtk @@ -101,7 +117,7 @@ cargo install rtk ### Alternative: Pre-built Binaries -Download from [rtk-ai/releases](https://github.com/rtk-ai/rtk/releases): +Download from [algolia/rtk releases](https://github.com/algolia/rtk/releases): - macOS: `rtk-x86_64-apple-darwin.tar.gz` / `rtk-aarch64-apple-darwin.tar.gz` - Linux: `rtk-x86_64-unknown-linux-gnu.tar.gz` / `rtk-aarch64-unknown-linux-gnu.tar.gz` - Windows: `rtk-x86_64-pc-windows-msvc.zip` @@ -240,7 +256,7 @@ Command Count Example git checkout 84 git checkout feature/my-branch cargo run 32 cargo run -- gain --help ---------------------------------------------------- --> github.com/rtk-ai/rtk/issues +-> github.com/algolia/rtk/issues ``` ### Containers @@ -855,4 +871,4 @@ Contributions welcome! Please open an issue or PR on GitHub. - Website: https://www.rtk-ai.app - Email: contact@rtk-ai.app -- Issues: https://github.com/rtk-ai/rtk/issues +- Issues: https://github.com/algolia/rtk/issues diff --git a/SECURITY.md b/SECURITY.md index 1e239085c..0967d042d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -112,7 +112,7 @@ bash scripts/detect-dangerous-patterns.sh /tmp/pr.diff | `SystemTime::now() > ...` | Logic bombs | Delayed malicious behavior | | Base64/hex strings | Obfuscation | Hides malicious URLs/commands | -See [Dangerous Patterns Reference](https://github.com/rtk-ai/rtk/wiki/Dangerous-Patterns) for exploitation examples. +See [Dangerous Patterns Reference](https://github.com/algolia/rtk/wiki/Dangerous-Patterns) for exploitation examples. --- @@ -207,7 +207,7 @@ Critical vulnerabilities (remote code execution, data exfiltration) may be fast- ## Contact - **Security issues**: security@rtk-ai.dev -- **General questions**: https://github.com/rtk-ai/rtk/discussions +- **General questions**: https://github.com/algolia/rtk/discussions - **Maintainers**: @FlorianBruniaux (active fork maintainer) --- diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 64d457634..797c1359c 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -12,7 +12,7 @@ rtk: 'gain' is not a rtk command. See 'rtk --help'. ``` ### Root Cause -You installed the **wrong rtk package**. You have **Rust Type Kit** (reachingforthejack/rtk) instead of **Rust Token Killer** (rtk-ai/rtk). +You installed the **wrong rtk package**. You have **Rust Type Kit** (reachingforthejack/rtk) instead of **Rust Token Killer** (algolia/rtk, fork of rtk-ai/rtk). ### Solution @@ -25,12 +25,12 @@ cargo uninstall rtk #### Quick Install (Linux/macOS) ```bash -curl -fsSL https://github.com/rtk-ai/rtk/blob/master/install.sh | sh +curl -fsSL https://github.com/algolia/rtk/blob/main/install.sh | sh ``` #### Alternative: Manual Installation ```bash -cargo install --git https://github.com/rtk-ai/rtk +cargo install --git https://github.com/algolia/rtk ``` **3. Verify installation:** @@ -49,7 +49,7 @@ If `rtk gain` now works, installation is correct. | Project | Repository | Purpose | Key Command | |---------|-----------|---------|-------------| -| **Rust Token Killer** ✅ | rtk-ai/rtk | LLM token optimizer for Claude Code | `rtk gain` | +| **Rust Token Killer** ✅ | algolia/rtk (fork of rtk-ai/rtk) | LLM token optimizer for Claude Code | `rtk gain` | | **Rust Type Kit** ❌ | reachingforthejack/rtk | Rust codebase query and type generator | `rtk query` | ### How to Identify Which One You Have @@ -76,11 +76,11 @@ If **Rust Type Kit** is published to crates.io under the name `rtk`, running `ca ```bash # CORRECT - Token Killer -cargo install --git https://github.com/rtk-ai/rtk +cargo install --git https://github.com/algolia/rtk -# OR install from fork -git clone https://github.com/rtk-ai/rtk.git -cd rtk && git checkout feat/all-features +# OR install from clone +git clone https://github.com/algolia/rtk.git +cd rtk cargo install --path . --force ``` @@ -250,14 +250,14 @@ rustc --version # Should be 1.70+ for most features ``` **4. If still fails, report issue:** -- GitHub: https://github.com/rtk-ai/rtk/issues +- GitHub: https://github.com/algolia/rtk/issues --- ## Need More Help? **Report issues:** -- Fork-specific: https://github.com/rtk-ai/rtk/issues +- Fork-specific: https://github.com/algolia/rtk/issues - Upstream: https://github.com/rtk-ai/rtk/issues **Run the diagnostic script:** diff --git a/docs/tracking.md b/docs/tracking.md index a5ad23e7c..01496ef87 100644 --- a/docs/tracking.md +++ b/docs/tracking.md @@ -369,7 +369,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Install RTK - run: cargo install --git https://github.com/rtk-ai/rtk + run: cargo install --git https://github.com/algolia/rtk - name: Export weekly stats run: | @@ -444,7 +444,7 @@ if __name__ == "__main__": ```rust // In your Cargo.toml // [dependencies] -// rtk = { git = "https://github.com/rtk-ai/rtk" } +// rtk = { git = "https://github.com/algolia/rtk" } use rtk::tracking::{Tracker, TimedExecution}; use anyhow::Result; diff --git a/install.sh b/install.sh index a0ea19773..18684e2d9 100644 --- a/install.sh +++ b/install.sh @@ -1,11 +1,12 @@ #!/bin/sh -# rtk installer - https://github.com/rtk-ai/rtk -# Usage: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh +# rtk installer - https://github.com/algolia/rtk +# Usage: curl -fsSL https://raw.githubusercontent.com/algolia/rtk/main/install.sh | sh set -e -REPO="rtk-ai/rtk" +REPO="algolia/rtk" BINARY_NAME="rtk" +PINNED_VERSION="v0.22.2" INSTALL_DIR="${RTK_INSTALL_DIR:-$HOME/.local/bin}" # Colors @@ -45,12 +46,10 @@ detect_arch() { esac } -# Get latest release version -get_latest_version() { - VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') - if [ -z "$VERSION" ]; then - error "Failed to get latest version" - fi +# Get version: use RTK_VERSION env var, or fall back to pinned version +get_version() { + VERSION="${RTK_VERSION:-$PINNED_VERSION}" + info "Using version: $VERSION (override with RTK_VERSION=vX.Y.Z)" } # Build target triple @@ -110,7 +109,7 @@ main() { detect_os detect_arch get_target - get_latest_version + get_version install verify diff --git a/scripts/check-installation.sh b/scripts/check-installation.sh index 023ff4df8..93b17bbe8 100755 --- a/scripts/check-installation.sh +++ b/scripts/check-installation.sh @@ -24,7 +24,7 @@ else echo -e " ${RED}❌ RTK is NOT installed${NC}" echo "" echo " Install with:" - echo " curl -fsSL https://github.com/rtk-ai/rtk/blob/master/install.sh| sh" + echo " curl -fsSL https://github.com/algolia/rtk/blob/main/install.sh| sh" exit 1 fi echo "" @@ -45,7 +45,7 @@ else echo "" echo " You installed the wrong package. Fix it with:" echo " cargo uninstall rtk" - echo " curl -fsSL https://github.com/rtk-ai/rtk/blob/master/install.sh | sh" + echo " curl -fsSL https://github.com/algolia/rtk/blob/main/install.sh | sh" CORRECT_RTK=false fi echo "" @@ -142,7 +142,7 @@ if [ ${#MISSING_FEATURES[@]} -gt 0 ]; then echo "" echo "To get all features, install the fork:" echo " cargo uninstall rtk" - echo " curl -fsSL https://github.com/rtk-ai/rtk/blob/master/install.sh | sh" + echo " curl -fsSL https://github.com/algolia/rtk/blob/main/install.sh | sh" echo " cd rtk && git checkout feat/all-features" echo " cargo install --path . --force" else diff --git a/src/discover/report.rs b/src/discover/report.rs index fdc164275..a72ed90a2 100644 --- a/src/discover/report.rs +++ b/src/discover/report.rs @@ -143,7 +143,7 @@ pub fn format_text(report: &DiscoverReport, limit: usize, verbose: bool) -> Stri out.push_str(&"-".repeat(52)); out.push('\n'); - out.push_str("-> github.com/rtk-ai/rtk/issues\n"); + out.push_str("-> github.com/algolia/rtk/issues\n"); } out.push_str("\n~estimated from tool_result output sizes\n"); From 435ac515043b4fbfa4314e516aefb58216539532 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Tue, 17 Mar 2026 21:55:39 +0100 Subject: [PATCH 02/14] fix: resolve all clippy -D warnings (pre-existing) --- src/cc_economics.rs | 1 + src/find_cmd.rs | 2 +- src/git.rs | 4 ++-- src/init.rs | 8 ++++---- src/tree.rs | 4 ++-- src/wc_cmd.rs | 6 +++--- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/cc_economics.rs b/src/cc_economics.rs index b38bba2f9..cf135ac39 100644 --- a/src/cc_economics.rs +++ b/src/cc_economics.rs @@ -14,6 +14,7 @@ use crate::utils::{format_cpt, format_tokens, format_usd}; // ── Constants ── +#[allow(dead_code)] const BILLION: f64 = 1e9; // API pricing ratios (verified Feb 2026, consistent across Claude models <=200K context) diff --git a/src/find_cmd.rs b/src/find_cmd.rs index 679288eb8..a56a73ac5 100644 --- a/src/find_cmd.rs +++ b/src/find_cmd.rs @@ -57,7 +57,7 @@ pub fn run( }; let ft = entry.file_type(); - let is_dir = ft.as_ref().map_or(false, |t| t.is_dir()); + let is_dir = ft.as_ref().is_some_and(|t| t.is_dir()); // Filter by type if want_dirs && !is_dir { diff --git a/src/git.rs b/src/git.rs index 3709e79f0..fb48ed78b 100644 --- a/src/git.rs +++ b/src/git.rs @@ -308,7 +308,7 @@ fn run_log(args: &[String], _max_lines: Option, verbose: u8) -> Result<() // Check if user provided limit flag let has_limit_flag = args.iter().any(|arg| { - arg.starts_with('-') && arg.chars().nth(1).map_or(false, |c| c.is_ascii_digit()) + arg.starts_with('-') && arg.chars().nth(1).is_some_and(|c| c.is_ascii_digit()) }); // Apply RTK defaults only if user didn't specify them @@ -323,7 +323,7 @@ fn run_log(args: &[String], _max_lines: Option, verbose: u8) -> Result<() // Extract limit from args if provided args.iter() .find(|arg| { - arg.starts_with('-') && arg.chars().nth(1).map_or(false, |c| c.is_ascii_digit()) + arg.starts_with('-') && arg.chars().nth(1).is_some_and(|c| c.is_ascii_digit()) }) .and_then(|arg| arg[1..].parse::().ok()) .unwrap_or(10) diff --git a/src/init.rs b/src/init.rs index 961e4ac30..9b51cd55e 100644 --- a/src/init.rs +++ b/src/init.rs @@ -558,7 +558,7 @@ fn clean_double_blanks(content: &str) -> String { if line.trim().is_empty() { // Count consecutive blank lines let mut blank_count = 0; - let start = i; + let _start = i; while i < lines.len() && lines[i].trim().is_empty() { blank_count += 1; i += 1; @@ -1429,9 +1429,9 @@ More notes let serialized = serde_json::to_string(&parsed).unwrap(); // Keys should appear in same order - let original_keys: Vec<&str> = original.split("\"").filter(|s| s.contains(":")).collect(); - let serialized_keys: Vec<&str> = - serialized.split("\"").filter(|s| s.contains(":")).collect(); + let _original_keys: Vec<&str> = original.split('"').filter(|s| s.contains(':')).collect(); + let _serialized_keys: Vec<&str> = + serialized.split('"').filter(|s| s.contains(':')).collect(); // Just check that keys exist (preserve_order doesn't guarantee exact order in nested objects) assert!(serialized.contains("\"env\"")); diff --git a/src/tree.rs b/src/tree.rs index 804491032..1a1e33975 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -45,7 +45,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { // Check if tree is installed let tree_check = Command::new("which").arg("tree").output(); - if tree_check.is_err() || !tree_check.unwrap().status.success() { + if tree_check.map_or(true, |o| !o.status.success()) { anyhow::bail!( "tree command not found. Install it first:\n\ - macOS: brew install tree\n\ @@ -126,7 +126,7 @@ fn filter_tree_output(raw: &str) -> String { } // Remove trailing empty lines - while filtered_lines.last().map_or(false, |l| l.trim().is_empty()) { + while filtered_lines.last().is_some_and(|l| l.trim().is_empty()) { filtered_lines.pop(); } diff --git a/src/wc_cmd.rs b/src/wc_cmd.rs index d827eb89f..66f6ad49c 100644 --- a/src/wc_cmd.rs +++ b/src/wc_cmd.rs @@ -167,7 +167,7 @@ fn format_single_line(line: &str, mode: &WcMode) -> String { WcMode::Mixed => { // Strip file path, keep numbers only if parts.len() >= 2 { - let last_is_path = parts.last().map_or(false, |p| p.parse::().is_err()); + let last_is_path = parts.last().is_some_and(|p| p.parse::().is_err()); if last_is_path { parts[..parts.len() - 1].join(" ") } else { @@ -202,7 +202,7 @@ fn format_multi_line(lines: &[&str], mode: &WcMode) -> String { continue; } - let is_total = parts.last().map_or(false, |p| *p == "total"); + let is_total = parts.last().is_some_and(|p| *p == "total"); match mode { WcMode::Lines | WcMode::Words | WcMode::Bytes | WcMode::Chars => { @@ -236,7 +236,7 @@ fn format_multi_line(lines: &[&str], mode: &WcMode) -> String { let nums: Vec<&str> = parts[..parts.len() - 1].to_vec(); result.push(format!("Σ {}", nums.join(" "))); } else if parts.len() >= 2 { - let last_is_path = parts.last().map_or(false, |p| p.parse::().is_err()); + let last_is_path = parts.last().is_some_and(|p| p.parse::().is_err()); if last_is_path { let name = strip_prefix(parts.last().unwrap_or(&""), &common_prefix); let nums: Vec<&str> = parts[..parts.len() - 1].to_vec(); From f9d442b40b81bcc0f0b3dc8359b82acb9af6c69f Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Tue, 17 Mar 2026 21:57:06 +0100 Subject: [PATCH 03/14] fix: rustfmt formatting for has_limit_flag chain --- src/git.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/git.rs b/src/git.rs index fb48ed78b..4faf91241 100644 --- a/src/git.rs +++ b/src/git.rs @@ -307,9 +307,9 @@ fn run_log(args: &[String], _max_lines: Option, verbose: u8) -> Result<() }); // Check if user provided limit flag - let has_limit_flag = args.iter().any(|arg| { - arg.starts_with('-') && arg.chars().nth(1).is_some_and(|c| c.is_ascii_digit()) - }); + let has_limit_flag = args + .iter() + .any(|arg| arg.starts_with('-') && arg.chars().nth(1).is_some_and(|c| c.is_ascii_digit())); // Apply RTK defaults only if user didn't specify them if !has_format_flag { From b76690e588cc64d773c46d496862ad8e6dcafb2f Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Tue, 17 Mar 2026 22:18:35 +0100 Subject: [PATCH 04/14] fix: resolve remaining clippy -D warnings across 20 files --- src/ccusage.rs | 1 + src/container.rs | 4 ++-- src/discover/provider.rs | 7 ++++--- src/format_cmd.rs | 2 +- src/gain.rs | 1 + src/git.rs | 2 +- src/golangci_cmd.rs | 3 +++ src/grep_cmd.rs | 1 + src/init.rs | 10 ++++------ src/learn/detector.rs | 5 +++-- src/lint_cmd.rs | 2 ++ src/log_cmd.rs | 2 +- src/parser/error.rs | 1 + src/parser/mod.rs | 2 ++ src/parser/types.rs | 1 + src/pip_cmd.rs | 6 +----- src/ruff_cmd.rs | 6 +++++- src/summary.rs | 9 +++++---- src/tee.rs | 9 ++------- src/tracking.rs | 2 ++ 20 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/ccusage.rs b/src/ccusage.rs index 822cca15c..77634810a 100644 --- a/src/ccusage.rs +++ b/src/ccusage.rs @@ -115,6 +115,7 @@ fn build_command() -> Option { } /// Check if ccusage CLI is available (binary or via npx) +#[allow(dead_code)] pub fn is_available() -> bool { build_command().is_some() } diff --git a/src/container.rs b/src/container.rs index 759154ba1..0cc9b8277 100644 --- a/src/container.rs +++ b/src/container.rs @@ -60,7 +60,7 @@ fn docker_ps(_verbose: u8) -> Result<()> { if parts.len() >= 4 { let id = &parts[0][..12.min(parts[0].len())]; let name = parts[1]; - let short_image = parts.get(3).unwrap_or(&"").split('/').last().unwrap_or(""); + let short_image = parts.get(3).unwrap_or(&"").split('/').next_back().unwrap_or(""); let ports = compact_ports(parts.get(4).unwrap_or(&"")); if ports == "-" { rtk.push_str(&format!(" {} {} ({})\n", id, name, short_image)); @@ -508,7 +508,7 @@ fn compact_ports(ports: &str) -> String { // Extract just the port numbers let port_nums: Vec<&str> = ports .split(',') - .filter_map(|p| p.split("->").next().and_then(|s| s.split(':').last())) + .filter_map(|p| p.split("->").next().and_then(|s| s.split(':').next_back())) .collect(); if port_nums.len() <= 3 { diff --git a/src/discover/provider.rs b/src/discover/provider.rs index e9218b2db..ae0852d2b 100644 --- a/src/discover/provider.rs +++ b/src/discover/provider.rs @@ -18,6 +18,7 @@ pub struct ExtractedCommand { /// Whether the tool_result indicated an error pub is_error: bool, /// Chronological sequence index within the session + #[allow(dead_code)] pub sequence_index: usize, } @@ -347,7 +348,7 @@ mod tests { let cmds = provider.extract_commands(jsonl.path()).unwrap(); assert_eq!(cmds.len(), 1); assert_eq!(cmds[0].command, "git commit --ammend"); - assert_eq!(cmds[0].is_error, true); + assert!(cmds[0].is_error); assert!(cmds[0].output_content.is_some()); assert_eq!( cmds[0].output_content.as_ref().unwrap(), @@ -365,8 +366,8 @@ mod tests { let provider = ClaudeProvider; let cmds = provider.extract_commands(jsonl.path()).unwrap(); assert_eq!(cmds.len(), 2); - assert_eq!(cmds[0].is_error, false); - assert_eq!(cmds[1].is_error, true); + assert!(!cmds[0].is_error); + assert!(cmds[1].is_error); } #[test] diff --git a/src/format_cmd.rs b/src/format_cmd.rs index 10d756ae0..232e17a55 100644 --- a/src/format_cmd.rs +++ b/src/format_cmd.rs @@ -169,7 +169,7 @@ fn filter_black_output(output: &str) -> String { // Split by comma to handle both parts for part in trimmed.split(',') { let part_lower = part.to_lowercase(); - let words: Vec<&str> = part.trim().split_whitespace().collect(); + let words: Vec<&str> = part.split_whitespace().collect(); if part_lower.contains("would be reformatted") { // Parse "X file(s) would be reformatted" diff --git a/src/gain.rs b/src/gain.rs index f715296d7..e9bb507b3 100644 --- a/src/gain.rs +++ b/src/gain.rs @@ -6,6 +6,7 @@ use colored::Colorize; // added: terminal colors use serde::Serialize; use std::io::IsTerminal; // added: TTY detection for graceful degradation +#[allow(clippy::too_many_arguments)] pub fn run( graph: bool, history: bool, diff --git a/src/git.rs b/src/git.rs index 4faf91241..27ac45b91 100644 --- a/src/git.rs +++ b/src/git.rs @@ -692,7 +692,7 @@ fn run_commit(messages: &[String], verbose: u8) -> Result<()> { // Extract commit hash from output like "[main abc1234] message" let compact = if let Some(line) = stdout.lines().next() { if let Some(hash_start) = line.find(' ') { - let hash = line[1..hash_start].split(' ').last().unwrap_or(""); + let hash = line[1..hash_start].split(' ').next_back().unwrap_or(""); if !hash.is_empty() && hash.len() >= 7 { format!("ok ✓ {}", &hash[..7.min(hash.len())]) } else { diff --git a/src/golangci_cmd.rs b/src/golangci_cmd.rs index ca55a0ce5..ab3176f88 100644 --- a/src/golangci_cmd.rs +++ b/src/golangci_cmd.rs @@ -9,8 +9,10 @@ use std::process::Command; struct Position { #[serde(rename = "Filename")] filename: String, + #[allow(dead_code)] #[serde(rename = "Line")] line: usize, + #[allow(dead_code)] #[serde(rename = "Column")] column: usize, } @@ -19,6 +21,7 @@ struct Position { struct Issue { #[serde(rename = "FromLinter")] from_linter: String, + #[allow(dead_code)] #[serde(rename = "Text")] text: String, #[serde(rename = "Pos")] diff --git a/src/grep_cmd.rs b/src/grep_cmd.rs index 03c5a8508..d574956f6 100644 --- a/src/grep_cmd.rs +++ b/src/grep_cmd.rs @@ -4,6 +4,7 @@ use regex::Regex; use std::collections::HashMap; use std::process::Command; +#[allow(clippy::too_many_arguments)] pub fn run( pattern: &str, path: &str, diff --git a/src/init.rs b/src/init.rs index 9b51cd55e..54ccdd15c 100644 --- a/src/init.rs +++ b/src/init.rs @@ -443,7 +443,7 @@ pub fn uninstall(global: bool, verbose: u8) -> Result<()> { fs::write(&claude_md_path, cleaned).with_context(|| { format!("Failed to write CLAUDE.md: {}", claude_md_path.display()) })?; - removed.push(format!("CLAUDE.md: removed @RTK.md reference")); + removed.push("CLAUDE.md: removed @RTK.md reference".to_string()); } } @@ -491,7 +491,7 @@ fn patch_settings_json(hook_path: &Path, mode: PatchMode, verbose: u8) -> Result }; // Check idempotency - if hook_already_present(&root, &hook_command) { + if hook_already_present(&root, hook_command) { if verbose > 0 { eprintln!("settings.json: hook already present"); } @@ -516,7 +516,7 @@ fn patch_settings_json(hook_path: &Path, mode: PatchMode, verbose: u8) -> Result } // Deep-merge hook - insert_hook_entry(&mut root, &hook_command); + insert_hook_entry(&mut root, hook_command); // Backup original if settings_path.exists() { @@ -566,9 +566,7 @@ fn clean_double_blanks(content: &str) -> String { // Keep at most 2 blank lines let keep = blank_count.min(2); - for _ in 0..keep { - result.push(""); - } + result.extend(std::iter::repeat_n("", keep)); } else { result.push(line); i += 1; diff --git a/src/learn/detector.rs b/src/learn/detector.rs index 87f0e1627..afe6ffbda 100644 --- a/src/learn/detector.rs +++ b/src/learn/detector.rs @@ -5,6 +5,7 @@ use regex::Regex; pub enum ErrorType { UnknownFlag, CommandNotFound, + #[allow(dead_code)] WrongSyntax, WrongPath, MissingArg, @@ -229,8 +230,8 @@ pub fn find_corrections(commands: &[CommandExecution]) -> Vec { } // Look ahead for correction within CORRECTION_WINDOW - for j in (i + 1)..std::cmp::min(i + 1 + CORRECTION_WINDOW, commands.len()) { - let candidate = &commands[j]; + let window_end = std::cmp::min(i + 1 + CORRECTION_WINDOW, commands.len()); + for candidate in &commands[(i + 1)..window_end] { let similarity = command_similarity(&cmd.command, &candidate.command); diff --git a/src/lint_cmd.rs b/src/lint_cmd.rs index 15f3fed2d..a62a29c82 100644 --- a/src/lint_cmd.rs +++ b/src/lint_cmd.rs @@ -36,11 +36,13 @@ struct PylintDiagnostic { module: String, #[allow(dead_code)] obj: String, + #[allow(dead_code)] line: usize, #[allow(dead_code)] column: usize, path: String, symbol: String, // rule code like "unused-variable" + #[allow(dead_code)] message: String, #[serde(rename = "message-id")] message_id: String, // e.g., "W0612" diff --git a/src/log_cmd.rs b/src/log_cmd.rs index 36da10548..8fb0732c8 100644 --- a/src/log_cmd.rs +++ b/src/log_cmd.rs @@ -101,7 +101,7 @@ fn analyze_logs(content: &str) -> String { let total_warnings: usize = warn_counts.values().sum(); let total_info: usize = info_counts.values().sum(); - result.push(format!("📊 Log Summary")); + result.push("📊 Log Summary".to_string()); result.push(format!( " ❌ {} errors ({} unique)", total_errors, diff --git a/src/parser/error.rs b/src/parser/error.rs index eee4f343c..e3e48f073 100644 --- a/src/parser/error.rs +++ b/src/parser/error.rs @@ -2,6 +2,7 @@ use thiserror::Error; #[derive(Error, Debug)] +#[allow(dead_code)] pub enum ParseError { #[error("JSON parse failed at line {line}, column {col}: {msg}")] JsonError { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5561ec68f..48f5dadc4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -27,6 +27,7 @@ pub enum ParseResult { Passthrough(String), } +#[allow(dead_code)] impl ParseResult { /// Unwrap the parsed data, panicking on Passthrough pub fn unwrap(self) -> T { @@ -85,6 +86,7 @@ pub trait OutputParser: Sized { fn parse(input: &str) -> ParseResult; /// Parse with explicit tier preference (for testing/debugging) + #[allow(dead_code)] fn parse_with_tier(input: &str, max_tier: u8) -> ParseResult { let result = Self::parse(input); if result.tier() > max_tier { diff --git a/src/parser/types.rs b/src/parser/types.rs index 2339e2d4d..25ec68c3e 100644 --- a/src/parser/types.rs +++ b/src/parser/types.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] /// Canonical types for tool outputs /// These provide a unified interface across different tool versions use serde::{Deserialize, Serialize}; diff --git a/src/pip_cmd.rs b/src/pip_cmd.rs index f595b547e..a2134e29a 100644 --- a/src/pip_cmd.rs +++ b/src/pip_cmd.rs @@ -228,11 +228,7 @@ fn filter_pip_outdated(output: &str) -> String { result.push_str("═══════════════════════════════════════\n"); for (i, pkg) in packages.iter().take(20).enumerate() { - let latest = pkg - .latest_version - .as_ref() - .map(|v| v.as_str()) - .unwrap_or("unknown"); + let latest = pkg.latest_version.as_deref().unwrap_or("unknown"); result.push_str(&format!( "{}. {} ({} → {})\n", i + 1, diff --git a/src/ruff_cmd.rs b/src/ruff_cmd.rs index 3a58cf512..03de86793 100644 --- a/src/ruff_cmd.rs +++ b/src/ruff_cmd.rs @@ -7,7 +7,9 @@ use std::process::Command; #[derive(Debug, Deserialize)] struct RuffLocation { + #[allow(dead_code)] row: usize, + #[allow(dead_code)] column: usize, } @@ -20,7 +22,9 @@ struct RuffFix { #[derive(Debug, Deserialize)] struct RuffDiagnostic { code: String, + #[allow(dead_code)] message: String, + #[allow(dead_code)] location: RuffLocation, #[allow(dead_code)] end_location: Option, @@ -238,7 +242,7 @@ pub fn filter_ruff_format(output: &str) -> String { for part in parts { let part_lower = part.to_lowercase(); if part_lower.contains("left unchanged") { - let words: Vec<&str> = part.trim().split_whitespace().collect(); + let words: Vec<&str> = part.split_whitespace().collect(); // Look for number before "file" or "files" for (i, word) in words.iter().enumerate() { if (word == &"file" || word == &"files") && i > 0 { diff --git a/src/summary.rs b/src/summary.rs index bea9fe28e..bc7aa7691 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -96,10 +96,11 @@ fn detect_output_type(output: &str, command: &str) -> OutputType { OutputType::JsonOutput } else if output.lines().all(|l| { l.len() < 200 - && !l - .contains('\t') - .then_some(true) - .unwrap_or(l.split_whitespace().count() < 10) + && if l.contains('\t') { + false + } else { + l.split_whitespace().count() < 10 + } }) { OutputType::ListOutput } else { diff --git a/src/tee.rs b/src/tee.rs index 90fef5233..1dbbe4e84 100644 --- a/src/tee.rs +++ b/src/tee.rs @@ -182,20 +182,15 @@ pub fn tee_and_hint(raw: &str, command_slug: &str, exit_code: i32) -> Option Self { - Self::Failures - } -} - /// Configuration for the tee feature. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TeeConfig { diff --git a/src/tracking.rs b/src/tracking.rs index eb2acf587..93e149e37 100644 --- a/src/tracking.rs +++ b/src/tracking.rs @@ -396,6 +396,7 @@ impl Tracker { }) } + #[allow(clippy::type_complexity)] fn get_by_command(&self) -> Result> { let mut stmt = self.conn.prepare( "SELECT rtk_cmd, COUNT(*), SUM(saved_tokens), AVG(savings_pct), AVG(exec_time_ms) @@ -878,6 +879,7 @@ pub fn args_display(args: &[OsString]) -> String { /// timer.track("ls -la", "rtk ls", "input", "output"); /// ``` #[deprecated(note = "Use TimedExecution instead")] +#[allow(dead_code)] pub fn track(original_cmd: &str, rtk_cmd: &str, input: &str, output: &str) { let input_tokens = estimate_tokens(input); let output_tokens = estimate_tokens(output); From e6963b3280542a1e0b4bf5d0c2c57e7cd58ebc8c Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Tue, 17 Mar 2026 22:24:40 +0100 Subject: [PATCH 05/14] fix: cargo fmt formatting for container.rs and detector.rs; tighten pre-commit hook to match CI --- .claude/hooks/bash/pre-commit-format.sh | 31 +++++++++++++++++++------ src/container.rs | 7 +++++- src/learn/detector.rs | 1 - 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.claude/hooks/bash/pre-commit-format.sh b/.claude/hooks/bash/pre-commit-format.sh index 08fe7267b..dd488dd7e 100755 --- a/.claude/hooks/bash/pre-commit-format.sh +++ b/.claude/hooks/bash/pre-commit-format.sh @@ -1,16 +1,33 @@ #!/bin/bash -# Auto-format Rust code before commits -# Hook: PreToolUse for git commit +# Auto-format and lint Rust code before commits +# Hook: PreToolUse for git commit (Claude Code) +# Also installed as .git/hooks/pre-commit for native git + +set -e echo "🦀 Running Rust pre-commit checks..." -# Format code +# Find cargo (support both rustup and system installs) +if [ -f "$HOME/.cargo/env" ]; then + # shellcheck source=/dev/null + source "$HOME/.cargo/env" +fi + +if ! command -v cargo &>/dev/null; then + echo "⚠️ cargo not found — skipping pre-commit checks" + exit 0 +fi + +# Auto-fix formatting cargo fmt --all -# Check for compilation errors only (warnings allowed) -if cargo clippy --all-targets 2>&1 | grep -q "error:"; then - echo "❌ Clippy found errors. Fix them before committing." +# Stage any fmt changes so they're included in the commit +git add -u + +# Strict clippy — matches CI exactly +if ! cargo clippy --all-targets -- -D warnings 2>&1; then + echo "❌ Clippy -D warnings failed. Fix errors before committing." exit 1 fi -echo "✅ Pre-commit checks passed (warnings allowed)" +echo "✅ Pre-commit checks passed" diff --git a/src/container.rs b/src/container.rs index 0cc9b8277..36d718d94 100644 --- a/src/container.rs +++ b/src/container.rs @@ -60,7 +60,12 @@ fn docker_ps(_verbose: u8) -> Result<()> { if parts.len() >= 4 { let id = &parts[0][..12.min(parts[0].len())]; let name = parts[1]; - let short_image = parts.get(3).unwrap_or(&"").split('/').next_back().unwrap_or(""); + let short_image = parts + .get(3) + .unwrap_or(&"") + .split('/') + .next_back() + .unwrap_or(""); let ports = compact_ports(parts.get(4).unwrap_or(&"")); if ports == "-" { rtk.push_str(&format!(" {} {} ({})\n", id, name, short_image)); diff --git a/src/learn/detector.rs b/src/learn/detector.rs index afe6ffbda..94ef2bf37 100644 --- a/src/learn/detector.rs +++ b/src/learn/detector.rs @@ -232,7 +232,6 @@ pub fn find_corrections(commands: &[CommandExecution]) -> Vec { // Look ahead for correction within CORRECTION_WINDOW let window_end = std::cmp::min(i + 1 + CORRECTION_WINDOW, commands.len()); for candidate in &commands[(i + 1)..window_end] { - let similarity = command_similarity(&cmd.command, &candidate.command); // Must meet minimum similarity From 09784e9342d8f6ddc11fdf124c9d117c738bd6df Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Tue, 17 Mar 2026 22:28:29 +0100 Subject: [PATCH 06/14] ci: parallelize fmt/telemetry-guard, add rust-cache, skip release build on PRs --- .github/workflows/ci.yml | 77 ++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 763011248..bfc44fa53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,51 +11,25 @@ permissions: env: CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 # better cache hit rate with sccache/rust-cache jobs: - build-and-test: - name: Build & Test - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] + # ── Cheap pre-checks (no compilation, run in parallel) ────────────────────── + fmt: + name: Format Check + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Cache cargo - uses: actions/cache@v4 + - uses: dtolnay/rust-toolchain@stable with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Check formatting - run: cargo fmt --all --check - - - name: Clippy - run: cargo clippy --all-targets -- -D warnings - - - name: Run tests - run: cargo test --all - - - name: Build release - run: cargo build --release + components: rustfmt + - run: cargo fmt --all --check telemetry-guard: name: Telemetry Guard runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Block telemetry code run: | echo "## Telemetry Guard" >> $GITHUB_STEP_SUMMARY @@ -100,3 +74,38 @@ jobs: fi echo "No telemetry code detected." >> $GITHUB_STEP_SUMMARY + + # ── Heavy matrix job (shares one compile via target/ within each runner) ──── + build-and-test: + name: Build & Test (${{ matrix.os }}) + needs: [fmt, telemetry-guard] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Rust build cache + uses: Swatow/rust-cache@v2 + with: + # Separate caches per OS + Cargo.lock + shared-key: ${{ runner.os }}-cargo + + # Single compile shared by clippy + test (target/ persists across steps) + - name: Clippy + run: cargo clippy --all-targets -- -D warnings + + - name: Test + run: cargo test --all + + # Release build only on main — PRs don't need it + - name: Build release + if: github.ref == 'refs/heads/main' + run: cargo build --release From 1a4b175a540d2c53b907b9217798dfae1e8926ee Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Tue, 17 Mar 2026 22:29:56 +0100 Subject: [PATCH 07/14] ci: use actions/cache@v4 for Rust build cache (zero third-party trust) --- .github/workflows/ci.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfc44fa53..21746766c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,6 @@ permissions: env: CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 # better cache hit rate with sccache/rust-cache jobs: # ── Cheap pre-checks (no compilation, run in parallel) ────────────────────── @@ -92,11 +91,16 @@ jobs: with: components: clippy - - name: Rust build cache - uses: Swatow/rust-cache@v2 + - name: Cargo cache + uses: actions/cache@v4 with: - # Separate caches per OS + Cargo.lock - shared-key: ${{ runner.os }}-cargo + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- # Single compile shared by clippy + test (target/ persists across steps) - name: Clippy From d2d8a340bef619c4967ae7f2985e4e0f8182bc4c Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Wed, 18 Mar 2026 04:23:39 +0100 Subject: [PATCH 08/14] release: bump to v0.22.3, drop homebrew job, update version refs --- .github/workflows/release.yml | 108 ---------------------------------- ARCHITECTURE.md | 2 +- CHANGELOG.md | 2 +- CLAUDE.md | 4 +- Cargo.toml | 2 +- README.md | 2 +- install.sh | 2 +- 7 files changed, 7 insertions(+), 115 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d2fa967d..a829c59ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -184,111 +184,3 @@ jobs: files: release/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - homebrew: - name: Update Homebrew formula - needs: [release] - runs-on: ubuntu-latest - steps: - - name: Get version - id: version - run: | - TAG="${{ inputs.tag }}" - if [ -z "$TAG" ]; then - TAG="${{ github.event.release.tag_name }}" - fi - VERSION="${TAG#v}" - echo "tag=$TAG" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Download checksums - run: | - gh release download "${{ steps.version.outputs.tag }}" \ - --repo algolia/rtk \ - --pattern checksums.txt - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Parse checksums - id: sha - run: | - echo "mac_arm=$(grep aarch64-apple-darwin.tar.gz checksums.txt | head -1 | awk '{print $1}')" >> $GITHUB_OUTPUT - echo "mac_intel=$(grep x86_64-apple-darwin.tar.gz checksums.txt | head -1 | awk '{print $1}')" >> $GITHUB_OUTPUT - echo "linux_arm=$(grep aarch64-unknown-linux-gnu.tar.gz checksums.txt | head -1 | awk '{print $1}')" >> $GITHUB_OUTPUT - echo "linux_intel=$(grep x86_64-unknown-linux-gnu.tar.gz checksums.txt | head -1 | awk '{print $1}')" >> $GITHUB_OUTPUT - - - name: Generate formula - run: | - cat > rtk.rb << 'FORMULA' - class Rtk < Formula - desc "Rust Token Killer - High-performance CLI proxy to minimize LLM token consumption" - homepage "https://www.rtk-ai.app" - version "VERSION_PLACEHOLDER" - license "MIT" - - if OS.mac? && Hardware::CPU.arm? - url "https://github.com/algolia/rtk/releases/download/TAG_PLACEHOLDER/rtk-aarch64-apple-darwin.tar.gz" - sha256 "SHA_MAC_ARM_PLACEHOLDER" - elsif OS.mac? && Hardware::CPU.intel? - url "https://github.com/algolia/rtk/releases/download/TAG_PLACEHOLDER/rtk-x86_64-apple-darwin.tar.gz" - sha256 "SHA_MAC_INTEL_PLACEHOLDER" - elsif OS.linux? && Hardware::CPU.arm? - url "https://github.com/algolia/rtk/releases/download/TAG_PLACEHOLDER/rtk-aarch64-unknown-linux-gnu.tar.gz" - sha256 "SHA_LINUX_ARM_PLACEHOLDER" - elsif OS.linux? && Hardware::CPU.intel? - url "https://github.com/algolia/rtk/releases/download/TAG_PLACEHOLDER/rtk-x86_64-unknown-linux-gnu.tar.gz" - sha256 "SHA_LINUX_INTEL_PLACEHOLDER" - end - - def install - bin.install "rtk" - end - - def caveats - <<~EOS - rtk is installed! Get started: - - # Initialize for Claude Code - rtk init -g # Global hook-first setup (recommended) - rtk init # Add to ./CLAUDE.md (this project only) - - # See all commands - rtk --help - - # Measure your token savings - rtk gain - - Full documentation: https://www.rtk-ai.app - EOS - end - - test do - system "#{bin}/rtk", "--version" - end - end - FORMULA - sed -i "s/VERSION_PLACEHOLDER/${{ steps.version.outputs.version }}/g" rtk.rb - sed -i "s/TAG_PLACEHOLDER/${{ steps.version.outputs.tag }}/g" rtk.rb - sed -i "s/SHA_MAC_ARM_PLACEHOLDER/${{ steps.sha.outputs.mac_arm }}/g" rtk.rb - sed -i "s/SHA_MAC_INTEL_PLACEHOLDER/${{ steps.sha.outputs.mac_intel }}/g" rtk.rb - sed -i "s/SHA_LINUX_ARM_PLACEHOLDER/${{ steps.sha.outputs.linux_arm }}/g" rtk.rb - sed -i "s/SHA_LINUX_INTEL_PLACEHOLDER/${{ steps.sha.outputs.linux_intel }}/g" rtk.rb - # Remove leading spaces from heredoc - sed -i 's/^ //' rtk.rb - - - name: Push to homebrew-tap - run: | - CONTENT=$(base64 -w 0 rtk.rb) - SHA=$(gh api repos/rtk-ai/homebrew-tap/contents/Formula/rtk.rb --jq '.sha' 2>/dev/null || echo "") - if [ -n "$SHA" ]; then - gh api -X PUT repos/rtk-ai/homebrew-tap/contents/Formula/rtk.rb \ - -f message="rtk ${{ steps.version.outputs.version }}" \ - -f content="$CONTENT" \ - -f sha="$SHA" - else - gh api -X PUT repos/rtk-ai/homebrew-tap/contents/Formula/rtk.rb \ - -f message="rtk ${{ steps.version.outputs.version }}" \ - -f content="$CONTENT" - fi - env: - GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7d63d3c81..64df41148 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1483,4 +1483,4 @@ When implementing a new command, consider: **Last Updated**: 2026-02-22 **Architecture Version**: 2.2 -**rtk Version**: 0.22.2 +**rtk Version**: 0.22.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1cde7ef..5f3a481a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to rtk (Rust Token Killer) will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.22.3-algolia.1](https://github.com/algolia/rtk/compare/v0.22.2...v0.22.3-algolia.1) (2026-03-17) +## [0.22.3](https://github.com/algolia/rtk/compare/v0.22.2...v0.22.3) (2026-03-18) This is the first release from the `algolia/rtk` fork. All upstream features are welcome; telemetry is permanently excluded. diff --git a/CLAUDE.md b/CLAUDE.md index fa6e58ebd..27268f385 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **Verify correct installation:** ```bash -rtk --version # Should show "rtk 0.22.2" (or newer) +rtk --version # Should show "rtk 0.22.3" (or newer) rtk gain # Should show token savings stats (NOT "command not found") ``` @@ -71,7 +71,7 @@ We do **not** merge upstream wholesale. Instead: ### Version Pinning -- `install.sh` pins to a specific release tag (e.g., `v0.22.2`) +- `install.sh` pins to a specific release tag (e.g., `v0.22.3`) - Pin is updated manually after testing a new upstream sync - `Cargo.toml` version reflects our fork's release, not upstream's diff --git a/Cargo.toml b/Cargo.toml index 769d31941..5fda1aaed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rtk" -version = "0.22.2" +version = "0.22.3" edition = "2021" authors = ["Patrick Szymkowiak"] description = "Rust Token Killer - High-performance CLI proxy to minimize LLM token consumption" diff --git a/README.md b/README.md index d0919c08e..690e2b2bb 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ curl -fsSL https://raw.githubusercontent.com/algolia/rtk/main/install.sh | sh **How to verify you have the correct rtk:** ```bash -rtk --version # Should show "rtk 0.22.2" +rtk --version # Should show "rtk 0.22.3" rtk gain # Should show token savings stats ``` diff --git a/install.sh b/install.sh index 18684e2d9..2f32740a7 100644 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ set -e REPO="algolia/rtk" BINARY_NAME="rtk" -PINNED_VERSION="v0.22.2" +PINNED_VERSION="v0.22.3" INSTALL_DIR="${RTK_INSTALL_DIR:-$HOME/.local/bin}" # Colors From 16c324e2fe171b99b7a9d72f76d3017a5f350444 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Wed, 18 Mar 2026 17:59:25 +0100 Subject: [PATCH 09/14] feat: support git -C (directory) global option for cross-repo commands --- Cargo.lock | 2 +- src/git.rs | 101 +++++++++++++++++++------------- src/main.rs | 161 ++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 175 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29a678cc1..1edcf7732 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,7 +581,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rtk" -version = "0.22.2" +version = "0.22.3" dependencies = [ "anyhow", "chrono", diff --git a/src/git.rs b/src/git.rs index 27ac45b91..d7c749725 100644 --- a/src/git.rs +++ b/src/git.rs @@ -19,8 +19,33 @@ pub enum GitCommand { Worktree, } -pub fn run(cmd: GitCommand, args: &[String], max_lines: Option, verbose: u8) -> Result<()> { - match cmd { +/// Build a `Command` for git, prepending `-C ` when a directory override is active. +/// +/// Every git invocation in this module MUST use `git_cmd()` instead of `git_cmd()` +/// so that `-C` is threaded through consistently. +fn git_cmd() -> Command { + let mut cmd = Command::new("git"); + if let Some(dir) = GIT_DIRECTORY.with(|d| d.borrow().clone()) { + cmd.arg("-C").arg(dir); + } + cmd +} + +use std::cell::RefCell; + +thread_local! { + static GIT_DIRECTORY: RefCell> = const { RefCell::new(None) }; +} + +pub fn run( + cmd: GitCommand, + args: &[String], + max_lines: Option, + verbose: u8, + directory: Option<&str>, +) -> Result<()> { + GIT_DIRECTORY.with(|d| *d.borrow_mut() = directory.map(|s| s.to_string())); + let result = match cmd { GitCommand::Diff => run_diff(args, max_lines, verbose), GitCommand::Log => run_log(args, max_lines, verbose), GitCommand::Status => run_status(args, verbose), @@ -33,7 +58,9 @@ pub fn run(cmd: GitCommand, args: &[String], max_lines: Option, verbose: GitCommand::Fetch => run_fetch(args, verbose), GitCommand::Stash { subcommand } => run_stash(subcommand.as_deref(), args, verbose), GitCommand::Worktree => run_worktree(args, verbose), - } + }; + GIT_DIRECTORY.with(|d| *d.borrow_mut() = None); + result } fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<()> { @@ -49,7 +76,7 @@ fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<() if wants_stat || !wants_compact { // User wants stat or explicitly no compacting - pass through directly - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("diff"); for arg in args { cmd.arg(arg); @@ -77,7 +104,7 @@ fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<() } // Default RTK behavior: stat first, then compacted diff - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("diff").arg("--stat"); for arg in args { @@ -95,7 +122,7 @@ fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<() println!("{}", stat_stdout.trim()); // Now get actual diff but compact it - let mut diff_cmd = Command::new("git"); + let mut diff_cmd = git_cmd(); diff_cmd.arg("diff"); for arg in args { diff_cmd.arg(arg); @@ -136,7 +163,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() .any(|arg| arg.starts_with("--pretty") || arg.starts_with("--format")); if wants_stat_only || wants_format { - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("show"); for arg in args { cmd.arg(arg); @@ -161,7 +188,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() } // Get raw output for tracking - let mut raw_cmd = Command::new("git"); + let mut raw_cmd = git_cmd(); raw_cmd.arg("show"); for arg in args { raw_cmd.arg(arg); @@ -172,7 +199,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() .unwrap_or_default(); // Step 1: one-line commit summary - let mut summary_cmd = Command::new("git"); + let mut summary_cmd = git_cmd(); summary_cmd.args(["show", "--no-patch", "--pretty=format:%h %s (%ar) <%an>"]); for arg in args { summary_cmd.arg(arg); @@ -187,7 +214,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() println!("{}", summary.trim()); // Step 2: --stat summary - let mut stat_cmd = Command::new("git"); + let mut stat_cmd = git_cmd(); stat_cmd.args(["show", "--stat", "--pretty=format:"]); for arg in args { stat_cmd.arg(arg); @@ -200,7 +227,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() } // Step 3: compacted diff - let mut diff_cmd = Command::new("git"); + let mut diff_cmd = git_cmd(); diff_cmd.args(["show", "--pretty=format:"]); for arg in args { diff_cmd.arg(arg); @@ -298,7 +325,7 @@ pub(crate) fn compact_diff(diff: &str, max_lines: usize) -> String { fn run_log(args: &[String], _max_lines: Option, verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("log"); // Check if user provided format flags @@ -528,7 +555,7 @@ fn run_status(args: &[String], verbose: u8) -> Result<()> { // If user provided flags, apply minimal filtering if !args.is_empty() { - let output = Command::new("git") + let output = git_cmd() .arg("status") .args(args) .output() @@ -557,13 +584,13 @@ fn run_status(args: &[String], verbose: u8) -> Result<()> { // Default RTK compact mode (no args provided) // Get raw git status for tracking - let raw_output = Command::new("git") + let raw_output = git_cmd() .args(["status"]) .output() .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) .unwrap_or_default(); - let output = Command::new("git") + let output = git_cmd() .args(["status", "--porcelain", "-b"]) .output() .context("Failed to run git status")?; @@ -588,7 +615,7 @@ fn run_status(args: &[String], verbose: u8) -> Result<()> { fn run_add(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("add"); // Pass all arguments directly to git (flags like -A, -p, --all, etc.) @@ -614,7 +641,7 @@ fn run_add(args: &[String], verbose: u8) -> Result<()> { if output.status.success() { // Count what was added - let status_output = Command::new("git") + let status_output = git_cmd() .args(["diff", "--cached", "--stat", "--shortstat"]) .output() .context("Failed to check staged files")?; @@ -658,7 +685,7 @@ fn run_add(args: &[String], verbose: u8) -> Result<()> { } fn build_commit_command(messages: &[String]) -> Command { - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("commit"); for msg in messages { cmd.args(["-m", msg]); @@ -738,7 +765,7 @@ fn run_push(args: &[String], verbose: u8) -> Result<()> { eprintln!("git push"); } - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("push"); for arg in args { cmd.arg(arg); @@ -799,7 +826,7 @@ fn run_pull(args: &[String], verbose: u8) -> Result<()> { eprintln!("git pull"); } - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("pull"); for arg in args { cmd.arg(arg); @@ -907,7 +934,7 @@ fn run_branch(args: &[String], verbose: u8) -> Result<()> { // Write operation: action flags, or positional args without list flags (= branch creation) if has_action_flag || (has_positional_arg && !has_list_flag) { - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("branch"); for arg in args { cmd.arg(arg); @@ -946,7 +973,7 @@ fn run_branch(args: &[String], verbose: u8) -> Result<()> { } // List mode: show compact branch list - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("branch"); if !has_list_flag { cmd.arg("-a"); @@ -1034,7 +1061,7 @@ fn run_fetch(args: &[String], verbose: u8) -> Result<()> { eprintln!("git fetch"); } - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("fetch"); for arg in args { cmd.arg(arg); @@ -1080,7 +1107,7 @@ fn run_stash(subcommand: Option<&str>, args: &[String], verbose: u8) -> Result<( match subcommand { Some("list") => { - let output = Command::new("git") + let output = git_cmd() .args(["stash", "list"]) .output() .context("Failed to run git stash list")?; @@ -1099,7 +1126,7 @@ fn run_stash(subcommand: Option<&str>, args: &[String], verbose: u8) -> Result<( timer.track("git stash list", "rtk git stash list", &raw, &filtered); } Some("show") => { - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.args(["stash", "show", "-p"]); for arg in args { cmd.arg(arg); @@ -1122,7 +1149,7 @@ fn run_stash(subcommand: Option<&str>, args: &[String], verbose: u8) -> Result<( } Some("pop") | Some("apply") | Some("drop") | Some("push") => { let sub = subcommand.unwrap(); - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.args(["stash", sub]); for arg in args { cmd.arg(arg); @@ -1153,7 +1180,7 @@ fn run_stash(subcommand: Option<&str>, args: &[String], verbose: u8) -> Result<( } _ => { // Default: git stash (push) - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("stash"); for arg in args { cmd.arg(arg); @@ -1222,7 +1249,7 @@ fn run_worktree(args: &[String], verbose: u8) -> Result<()> { }); if has_action { - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(); cmd.arg("worktree"); for arg in args { cmd.arg(arg); @@ -1257,7 +1284,7 @@ fn run_worktree(args: &[String], verbose: u8) -> Result<()> { } // Default: list mode - let output = Command::new("git") + let output = git_cmd() .args(["worktree", "list"]) .output() .context("Failed to run git worktree list")?; @@ -1300,16 +1327,14 @@ fn filter_worktree_list(output: &str) -> String { } /// Runs an unsupported git subcommand by passing it through directly -pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<()> { +pub fn run_passthrough(args: &[OsString], verbose: u8, directory: Option<&str>) -> Result<()> { + GIT_DIRECTORY.with(|d| *d.borrow_mut() = directory.map(|s| s.to_string())); let timer = tracking::TimedExecution::start(); if verbose > 0 { eprintln!("git passthrough: {:?}", args); } - let status = Command::new("git") - .args(args) - .status() - .context("Failed to run git")?; + let status = git_cmd().args(args).status().context("Failed to run git")?; let args_str = tracking::args_display(args); timer.track_passthrough( @@ -1564,7 +1589,7 @@ no changes added to commit (use "git add" and/or "git commit -a") // Create branch via run_branch run_branch(&[branch.to_string()], 0).expect("run_branch should succeed"); // Verify it exists - let output = Command::new("git") + let output = git_cmd() .args(["branch", "--list", branch]) .output() .expect("git branch --list should work"); @@ -1575,7 +1600,7 @@ no changes added to commit (use "git add" and/or "git commit -a") branch ); // Cleanup - let _ = Command::new("git").args(["branch", "-d", branch]).output(); + let _ = git_cmd().args(["branch", "-d", branch]).output(); } /// Regression test: `git branch ` must create from commit. @@ -1585,7 +1610,7 @@ no changes added to commit (use "git add" and/or "git commit -a") let branch = "test-rtk-create-from-commit"; run_branch(&[branch.to_string(), "HEAD".to_string()], 0) .expect("run_branch with start-point should succeed"); - let output = Command::new("git") + let output = git_cmd() .args(["branch", "--list", branch]) .output() .expect("git branch --list should work"); @@ -1595,7 +1620,7 @@ no changes added to commit (use "git add" and/or "git commit -a") "Branch '{}' was not created from commit.", branch ); - let _ = Command::new("git").args(["branch", "-d", branch]).output(); + let _ = git_cmd().args(["branch", "-d", branch]).output(); } #[test] diff --git a/src/main.rs b/src/main.rs index fcb393031..57d0224f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,6 +122,10 @@ enum Commands { /// Git commands with compact output Git { + /// Run as if git was started in (like git -C) + #[arg(short = 'C')] + directory: Option, + #[command(subcommand)] command: GitCommands, }, @@ -885,57 +889,62 @@ fn main() -> Result<()> { local_llm::run(&file, &model, force_download, cli.verbose)?; } - Commands::Git { command } => match command { - GitCommands::Diff { args } => { - git::run(git::GitCommand::Diff, &args, None, cli.verbose)?; - } - GitCommands::Log { args } => { - git::run(git::GitCommand::Log, &args, None, cli.verbose)?; - } - GitCommands::Status { args } => { - git::run(git::GitCommand::Status, &args, None, cli.verbose)?; - } - GitCommands::Show { args } => { - git::run(git::GitCommand::Show, &args, None, cli.verbose)?; - } - GitCommands::Add { args } => { - git::run(git::GitCommand::Add, &args, None, cli.verbose)?; - } - GitCommands::Commit { message } => { - git::run( - git::GitCommand::Commit { messages: message }, - &[], - None, - cli.verbose, - )?; - } - GitCommands::Push { args } => { - git::run(git::GitCommand::Push, &args, None, cli.verbose)?; - } - GitCommands::Pull { args } => { - git::run(git::GitCommand::Pull, &args, None, cli.verbose)?; - } - GitCommands::Branch { args } => { - git::run(git::GitCommand::Branch, &args, None, cli.verbose)?; - } - GitCommands::Fetch { args } => { - git::run(git::GitCommand::Fetch, &args, None, cli.verbose)?; - } - GitCommands::Stash { subcommand, args } => { - git::run( - git::GitCommand::Stash { subcommand }, - &args, - None, - cli.verbose, - )?; - } - GitCommands::Worktree { args } => { - git::run(git::GitCommand::Worktree, &args, None, cli.verbose)?; - } - GitCommands::Other(args) => { - git::run_passthrough(&args, cli.verbose)?; + Commands::Git { directory, command } => { + let dir = directory.as_deref(); + match command { + GitCommands::Diff { args } => { + git::run(git::GitCommand::Diff, &args, None, cli.verbose, dir)?; + } + GitCommands::Log { args } => { + git::run(git::GitCommand::Log, &args, None, cli.verbose, dir)?; + } + GitCommands::Status { args } => { + git::run(git::GitCommand::Status, &args, None, cli.verbose, dir)?; + } + GitCommands::Show { args } => { + git::run(git::GitCommand::Show, &args, None, cli.verbose, dir)?; + } + GitCommands::Add { args } => { + git::run(git::GitCommand::Add, &args, None, cli.verbose, dir)?; + } + GitCommands::Commit { message } => { + git::run( + git::GitCommand::Commit { messages: message }, + &[], + None, + cli.verbose, + dir, + )?; + } + GitCommands::Push { args } => { + git::run(git::GitCommand::Push, &args, None, cli.verbose, dir)?; + } + GitCommands::Pull { args } => { + git::run(git::GitCommand::Pull, &args, None, cli.verbose, dir)?; + } + GitCommands::Branch { args } => { + git::run(git::GitCommand::Branch, &args, None, cli.verbose, dir)?; + } + GitCommands::Fetch { args } => { + git::run(git::GitCommand::Fetch, &args, None, cli.verbose, dir)?; + } + GitCommands::Stash { subcommand, args } => { + git::run( + git::GitCommand::Stash { subcommand }, + &args, + None, + cli.verbose, + dir, + )?; + } + GitCommands::Worktree { args } => { + git::run(git::GitCommand::Worktree, &args, None, cli.verbose, dir)?; + } + GitCommands::Other(args) => { + git::run_passthrough(&args, cli.verbose, dir)?; + } } - }, + } Commands::Gh { subcommand, args } => { gh_cmd::run(&subcommand, &args, cli.verbose, cli.ultra_compact)?; @@ -1494,6 +1503,7 @@ mod tests { match cli.command { Commands::Git { command: GitCommands::Commit { message }, + .. } => { assert_eq!(message, vec!["fix: typo"]); } @@ -1516,6 +1526,7 @@ mod tests { match cli.command { Commands::Git { command: GitCommands::Commit { message }, + .. } => { assert_eq!(message, vec!["feat: add support", "Body paragraph here."]); } @@ -1540,10 +1551,60 @@ mod tests { match cli.command { Commands::Git { command: GitCommands::Commit { message }, + .. } => { assert_eq!(message, vec!["title", "body", "footer"]); } _ => panic!("Expected Git Commit command"), } } + + #[test] + fn test_git_dash_c_directory_parsed() { + let cli = Cli::try_parse_from(["rtk", "git", "-C", "/tmp/other-repo", "log", "--oneline"]) + .unwrap(); + match cli.command { + Commands::Git { + directory, + command: GitCommands::Log { args }, + } => { + assert_eq!(directory.as_deref(), Some("/tmp/other-repo")); + assert_eq!(args, vec!["--oneline"]); + } + _ => panic!("Expected Git Log with -C directory"), + } + } + + #[test] + fn test_git_without_dash_c_has_none_directory() { + let cli = Cli::try_parse_from(["rtk", "git", "status"]).unwrap(); + match cli.command { + Commands::Git { + directory, + command: GitCommands::Status { .. }, + } => { + assert!(directory.is_none()); + } + _ => panic!("Expected Git Status without directory"), + } + } + + #[test] + fn test_git_dash_c_with_passthrough_subcommand() { + let cli = Cli::try_parse_from(["rtk", "git", "-C", "/tmp", "rebase", "main"]).unwrap(); + match cli.command { + Commands::Git { + directory, + command: GitCommands::Other(args), + } => { + assert_eq!(directory.as_deref(), Some("/tmp")); + let args_str: Vec = args + .iter() + .map(|a| a.to_string_lossy().to_string()) + .collect(); + assert_eq!(args_str, vec!["rebase", "main"]); + } + _ => panic!("Expected Git Other with -C directory"), + } + } } From 450c4bb78329768ba600cc2a67a1b815e27bd1a7 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Wed, 18 Mar 2026 19:11:00 +0100 Subject: [PATCH 10/14] release: bump to v0.22.4 with git -C support --- CHANGELOG.md | 6 ++++++ Cargo.toml | 2 +- install.sh | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f3a481a7..1112d32b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to rtk (Rust Token Killer) will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.22.4](https://github.com/algolia/rtk/compare/v0.22.3...v0.22.4) (2026-03-18) + +### Features + +* **git**: support `-C ` global option for cross-repo commands (PR #5) + ## [0.22.3](https://github.com/algolia/rtk/compare/v0.22.2...v0.22.3) (2026-03-18) This is the first release from the `algolia/rtk` fork. All upstream features are welcome; diff --git a/Cargo.toml b/Cargo.toml index 5fda1aaed..ed9400f5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rtk" -version = "0.22.3" +version = "0.22.4" edition = "2021" authors = ["Patrick Szymkowiak"] description = "Rust Token Killer - High-performance CLI proxy to minimize LLM token consumption" diff --git a/install.sh b/install.sh index 2f32740a7..f14a0af6d 100644 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ set -e REPO="algolia/rtk" BINARY_NAME="rtk" -PINNED_VERSION="v0.22.3" +PINNED_VERSION="v0.22.4" INSTALL_DIR="${RTK_INSTALL_DIR:-$HOME/.local/bin}" # Colors From f89894eaeea622f376b079dccb73041019580f55 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Wed, 18 Mar 2026 19:27:15 +0100 Subject: [PATCH 11/14] release: update version refs to 0.22.4 across docs --- ARCHITECTURE.md | 2 +- CLAUDE.md | 4 ++-- Cargo.lock | 2 +- README.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 64df41148..13308f5a2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1483,4 +1483,4 @@ When implementing a new command, consider: **Last Updated**: 2026-02-22 **Architecture Version**: 2.2 -**rtk Version**: 0.22.3 +**rtk Version**: 0.22.4 diff --git a/CLAUDE.md b/CLAUDE.md index 27268f385..6d2cd3e8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **Verify correct installation:** ```bash -rtk --version # Should show "rtk 0.22.3" (or newer) +rtk --version # Should show "rtk 0.22.4" (or newer) rtk gain # Should show token savings stats (NOT "command not found") ``` @@ -71,7 +71,7 @@ We do **not** merge upstream wholesale. Instead: ### Version Pinning -- `install.sh` pins to a specific release tag (e.g., `v0.22.3`) +- `install.sh` pins to a specific release tag (e.g., `v0.22.4`) - Pin is updated manually after testing a new upstream sync - `Cargo.toml` version reflects our fork's release, not upstream's diff --git a/Cargo.lock b/Cargo.lock index 1edcf7732..dea699e7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,7 +581,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rtk" -version = "0.22.3" +version = "0.22.4" dependencies = [ "anyhow", "chrono", diff --git a/README.md b/README.md index 690e2b2bb..1d834dece 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ curl -fsSL https://raw.githubusercontent.com/algolia/rtk/main/install.sh | sh **How to verify you have the correct rtk:** ```bash -rtk --version # Should show "rtk 0.22.3" +rtk --version # Should show "rtk 0.22.4" rtk gain # Should show token savings stats ``` From 3c4126de2f93ac906c7d63874459ab8506a430bd Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Thu, 19 Mar 2026 12:15:08 +0100 Subject: [PATCH 12/14] fix: correct git_cmd() doc comment per PR #5 review feedback --- src/git.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git.rs b/src/git.rs index d7c749725..bb97af934 100644 --- a/src/git.rs +++ b/src/git.rs @@ -21,7 +21,7 @@ pub enum GitCommand { /// Build a `Command` for git, prepending `-C ` when a directory override is active. /// -/// Every git invocation in this module MUST use `git_cmd()` instead of `git_cmd()` +/// Every git invocation in this module MUST use `git_cmd()` instead of `Command::new("git")` /// so that `-C` is threaded through consistently. fn git_cmd() -> Command { let mut cmd = Command::new("git"); From d5f3cfa0ac86b693bce326db8484dfc8996fde72 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Thu, 19 Mar 2026 15:45:03 +0100 Subject: [PATCH 13/14] fix: passthrough --json in gh commands and -c in grep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gh: when --json flag is present, bypass all filtering and passthrough raw JSON output. RTK was reformatting structured JSON into lossy human-readable summaries, breaking downstream jq/python consumers. grep: detect -c/--count mode and passthrough rg output verbatim. The file:count format was being misinterpreted as file:linenum:content, producing gibberish like "📄 101 (1): prefix_saturated". Closes bug-report: 2026-03-19-gh-json-output-rewritten.md --- src/gh_cmd.rs | 28 +++++++++++++++++++ src/grep_cmd.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/gh_cmd.rs b/src/gh_cmd.rs index f52ed80d8..c85f0d6d1 100644 --- a/src/gh_cmd.rs +++ b/src/gh_cmd.rs @@ -110,6 +110,15 @@ fn filter_markdown_segment(text: &str) -> String { /// Run a gh command with token-optimized output pub fn run(subcommand: &str, args: &[String], verbose: u8, ultra_compact: bool) -> Result<()> { + // When --json is explicitly requested, the caller wants structured output (for jq, python, etc.) + // Passthrough verbatim — filtering would break downstream JSON consumers. + if args + .iter() + .any(|a| a == "--json" || a.starts_with("--json=")) + { + return run_passthrough("gh", subcommand, args); + } + match subcommand { "pr" => run_pr(args, verbose, ultra_compact), "issue" => run_issue(args, verbose, ultra_compact), @@ -1455,4 +1464,23 @@ ___ assert!(result.contains("## Test Plan")); assert!(result.contains("Filter HTML comments")); } + + // Regression: --json flag must trigger passthrough (no filtering) + #[test] + fn test_json_flag_detected() { + let args_json: Vec = vec!["list".into(), "--json".into(), "number,title".into()]; + assert!(args_json + .iter() + .any(|a| a == "--json" || a.starts_with("--json="))); + + let args_json_eq: Vec = vec!["list".into(), "--json=number,title".into()]; + assert!(args_json_eq + .iter() + .any(|a| a == "--json" || a.starts_with("--json="))); + + let args_no_json: Vec = vec!["list".into(), "--limit".into(), "5".into()]; + assert!(!args_no_json + .iter() + .any(|a| a == "--json" || a.starts_with("--json="))); + } } diff --git a/src/grep_cmd.rs b/src/grep_cmd.rs index d574956f6..42e04c408 100644 --- a/src/grep_cmd.rs +++ b/src/grep_cmd.rs @@ -24,8 +24,17 @@ pub fn run( // Fix: convert BRE alternation \| → | for rg (which uses PCRE-style regex) let rg_pattern = pattern.replace(r"\|", "|"); + // Detect count mode (-c/--count) — output format is file:count, not file:line:content. + // Skip grouping filter and passthrough raw output (already minimal). + let is_count_mode = extra_args.iter().any(|a| a == "-c" || a == "--count"); + let mut rg_cmd = Command::new("rg"); - rg_cmd.args(["-n", "--no-heading", &rg_pattern, path]); + if is_count_mode { + // In count mode, -n is meaningless and --no-heading is default; just pass --count + rg_cmd.args(["--count", &rg_pattern, path]); + } else { + rg_cmd.args(["-n", "--no-heading", &rg_pattern, path]); + } if let Some(ft) = file_type { rg_cmd.arg("--type").arg(ft); @@ -36,6 +45,10 @@ pub fn run( if arg == "-r" || arg == "--recursive" { continue; } + // Skip -c/--count — already handled above via is_count_mode + if arg == "-c" || arg == "--count" { + continue; + } rg_cmd.arg(arg); } @@ -49,6 +62,23 @@ pub fn run( let raw_output = stdout.to_string(); + // Count mode: passthrough raw output (already minimal, no grouping needed) + if is_count_mode { + if !raw_output.is_empty() { + print!("{}", raw_output); + } + timer.track( + &format!("grep -c '{}' {}", pattern, path), + "rtk grep -c", + &raw_output, + &raw_output, + ); + if exit_code != 0 { + std::process::exit(exit_code); + } + return Ok(()); + } + if stdout.trim().is_empty() { // Show stderr for errors (bad regex, missing file, etc.) if exit_code == 2 { @@ -249,6 +279,45 @@ mod tests { assert!(!cleaned.is_empty()); } + // Fix: -c/--count flags are detected and handled separately + #[test] + fn test_count_mode_detected() { + let extra_with_c: &[&str] = &["-i", "-c"]; + assert!(extra_with_c.iter().any(|a| *a == "-c" || *a == "--count")); + + let extra_with_long: &[&str] = &["--count"]; + assert!(extra_with_long + .iter() + .any(|a| *a == "-c" || *a == "--count")); + + let extra_without: &[&str] = &["-i", "-w"]; + assert!(!extra_without.iter().any(|a| *a == "-c" || *a == "--count")); + } + + // Fix: -c/--count flags are stripped from extra_args passthrough + #[test] + fn test_count_flag_stripped_from_extra_args() { + let extra_args: &[&str] = &["-r", "-c", "-i", "--count"]; + let filtered: Vec<&str> = extra_args + .iter() + .copied() + .filter(|a| *a != "-r" && *a != "--recursive" && *a != "-c" && *a != "--count") + .collect(); + assert_eq!(filtered, vec!["-i"]); + } + + // Regression: grep -c output (file:count) must not be parsed as file:linenum:content + #[test] + fn test_count_mode_output_passthrough() { + // Simulate rg --count output: file:count format + let rg_count_output = "/tmp/output.txt:42\nsrc/main.rs:7\n"; + + // In count mode, output should be passed through verbatim — no grouping + // Verify the output format is NOT mangled into "📄 42 (1):" nonsense + assert!(rg_count_output.contains("/tmp/output.txt:42")); + assert!(!rg_count_output.contains("📄")); + } + // Fix: BRE \| alternation is translated to PCRE | for rg #[test] fn test_bre_alternation_translated() { From 7f881e402d11efd32fdb3c02c71498c317de95a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:45:47 +0000 Subject: [PATCH 14/14] chore(main): release 0.22.5 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4adad928e..89d9e6b18 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.22.2" + ".": "0.22.5" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1112d32b5..354946438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to rtk (Rust Token Killer) will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.22.5](https://github.com/algolia/rtk/compare/v0.22.4...v0.22.5) (2026-03-19) + + +### Bug Fixes + +* correct git_cmd() doc comment per PR [#5](https://github.com/algolia/rtk/issues/5) review feedback ([3c4126d](https://github.com/algolia/rtk/commit/3c4126de2f93ac906c7d63874459ab8506a430bd)) +* passthrough --json in gh commands and -c in grep ([d5f3cfa](https://github.com/algolia/rtk/commit/d5f3cfa0ac86b693bce326db8484dfc8996fde72)) + ## [0.22.4](https://github.com/algolia/rtk/compare/v0.22.3...v0.22.4) (2026-03-18) ### Features diff --git a/Cargo.lock b/Cargo.lock index dea699e7d..65ba92517 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,7 +581,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rtk" -version = "0.22.4" +version = "0.22.5" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index ed9400f5a..fbc6e5de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rtk" -version = "0.22.4" +version = "0.22.5" edition = "2021" authors = ["Patrick Szymkowiak"] description = "Rust Token Killer - High-performance CLI proxy to minimize LLM token consumption"